From d7d17e5277541602536a830e4206253e6da4c270 Mon Sep 17 00:00:00 2001 From: master_jedy Date: Fri, 4 Jul 2025 11:55:56 +0200 Subject: [PATCH 1/2] add pyth-connecor example Signed-off-by: master_jedy --- price_feeds/ton/pyth-connector/.gitignore | 5 + .../ton/pyth-connector/.prettierignore | 1 + price_feeds/ton/pyth-connector/README.md | 50 + .../ton/pyth-connector/contracts/Pyth/Main.fc | 103 + .../contracts/Pyth/MainNoCheck.fc | 65 + .../ton/pyth-connector/contracts/Pyth/Pyth.fc | 633 ++++++ .../pyth-connector/contracts/Pyth/Wormhole.fc | 255 +++ .../contracts/Pyth/common/constants.fc | 54 + .../contracts/Pyth/common/error_handling.fc | 45 + .../contracts/Pyth/common/errors.fc | 49 + .../contracts/Pyth/common/gas.fc | 15 + .../Pyth/common/governance_actions.fc | 6 + .../contracts/Pyth/common/merkle_tree.fc | 43 + .../contracts/Pyth/common/op.fc | 10 + .../contracts/Pyth/common/storage.fc | 96 + .../contracts/Pyth/common/utils.fc | 105 + .../contracts/Pyth/imports/stdlib.fc | 632 ++++++ .../contracts/Pyth/tests/PythNoCheck.fc | 610 ++++++ .../contracts/Pyth/tests/PythTest.fc | 103 + .../contracts/Pyth/tests/PythTestUpgraded.fc | 102 + .../contracts/Pyth/tests/SendUsd.fc | 154 ++ .../contracts/Pyth/tests/WormholeTest.fc | 60 + .../contracts/PythConnector/configuration.fc | 21 + .../PythConnector/constants/constants.fc | 27 + .../PythConnector/constants/errors.fc | 21 + .../contracts/PythConnector/constants/logs.fc | 15 + .../PythConnector/constants/op_codes.fc | 16 + .../contracts/PythConnector/getters.fc | 12 + .../PythConnector/imports/basic_types.fc | 73 + .../contracts/PythConnector/imports/stdlib.fc | 645 ++++++ .../operations/configure_operation.fc | 27 + .../operations/onchain_getter_operation.fc | 117 + .../operations/proxy_operation.fc | 37 + .../PythConnector/parse_price_feeds.fc | 140 ++ .../contracts/PythConnector/pyth_connector.fc | 150 ++ .../contracts/PythConnector/utils/common.fc | 50 + .../contracts/PythConnector/utils/pyth.fc | 76 + .../contracts/PythConnector/utils/tx-utils.fc | 107 + .../ton/pyth-connector/include/imported.ts | 5 + price_feeds/ton/pyth-connector/index.ts | 3 + price_feeds/ton/pyth-connector/jest.config.ts | 10 + price_feeds/ton/pyth-connector/package.json | 49 + .../ton/pyth-connector/scripts/deployPyth.ts | 134 ++ .../scripts/deployPythConnector.ts | 33 + .../ton/pyth-connector/tests/Main.spec.ts | 57 + .../tests/PythConnector.spec.ts | 260 +++ .../ton/pyth-connector/tests/PythTest.spec.ts | 1907 +++++++++++++++++ .../pyth-connector/tests/WormholeTest.spec.ts | 240 +++ .../ton/pyth-connector/tests/utils/assets.ts | 52 + .../ton/pyth-connector/tests/utils/deploy.ts | 142 ++ .../ton/pyth-connector/tests/utils/feeds.ts | 16 + .../tests/utils/internalAssets.ts | 28 + .../pyth-connector/tests/utils/messages.ts | 43 + .../ton/pyth-connector/tests/utils/opcodes.ts | 18 + .../ton/pyth-connector/tests/utils/prices.ts | 185 ++ .../ton/pyth-connector/tests/utils/pyth.ts | 205 ++ .../ton/pyth-connector/tests/utils/utils.ts | 94 + .../pyth-connector/tests/utils/wormhole.ts | 178 ++ price_feeds/ton/pyth-connector/tsconfig.json | 15 + .../pyth-connector/wrappers/BaseWrapper.ts | 314 +++ .../pyth-connector/wrappers/Main.compile.ts | 6 + .../ton/pyth-connector/wrappers/Main.ts | 111 + .../wrappers/MainNoCheck.compile.ts | 6 + .../wrappers/PythConnector.compile.ts | 6 + .../pyth-connector/wrappers/PythConnector.ts | 151 ++ .../wrappers/PythTest.compile.ts | 6 + .../ton/pyth-connector/wrappers/PythTest.ts | 291 +++ .../wrappers/PythTestUpgraded.compile.ts | 6 + .../wrappers/WormholeTest.compile.ts | 6 + .../pyth-connector/wrappers/WormholeTest.ts | 161 ++ 70 files changed, 9468 insertions(+) create mode 100644 price_feeds/ton/pyth-connector/.gitignore create mode 100644 price_feeds/ton/pyth-connector/.prettierignore create mode 100644 price_feeds/ton/pyth-connector/README.md create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/Main.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/MainNoCheck.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/Pyth.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/Wormhole.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/common/constants.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/common/error_handling.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/common/errors.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/common/gas.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/common/governance_actions.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/common/merkle_tree.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/common/op.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/common/storage.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/common/utils.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/imports/stdlib.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/tests/PythNoCheck.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/tests/PythTest.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/tests/PythTestUpgraded.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/tests/SendUsd.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/Pyth/tests/WormholeTest.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/configuration.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/constants/constants.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/constants/errors.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/constants/logs.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/constants/op_codes.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/getters.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/imports/basic_types.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/imports/stdlib.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/operations/configure_operation.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/operations/onchain_getter_operation.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/operations/proxy_operation.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/parse_price_feeds.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/pyth_connector.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/utils/common.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/utils/pyth.fc create mode 100644 price_feeds/ton/pyth-connector/contracts/PythConnector/utils/tx-utils.fc create mode 100644 price_feeds/ton/pyth-connector/include/imported.ts create mode 100644 price_feeds/ton/pyth-connector/index.ts create mode 100644 price_feeds/ton/pyth-connector/jest.config.ts create mode 100644 price_feeds/ton/pyth-connector/package.json create mode 100644 price_feeds/ton/pyth-connector/scripts/deployPyth.ts create mode 100644 price_feeds/ton/pyth-connector/scripts/deployPythConnector.ts create mode 100644 price_feeds/ton/pyth-connector/tests/Main.spec.ts create mode 100644 price_feeds/ton/pyth-connector/tests/PythConnector.spec.ts create mode 100644 price_feeds/ton/pyth-connector/tests/PythTest.spec.ts create mode 100644 price_feeds/ton/pyth-connector/tests/WormholeTest.spec.ts create mode 100644 price_feeds/ton/pyth-connector/tests/utils/assets.ts create mode 100644 price_feeds/ton/pyth-connector/tests/utils/deploy.ts create mode 100644 price_feeds/ton/pyth-connector/tests/utils/feeds.ts create mode 100644 price_feeds/ton/pyth-connector/tests/utils/internalAssets.ts create mode 100644 price_feeds/ton/pyth-connector/tests/utils/messages.ts create mode 100644 price_feeds/ton/pyth-connector/tests/utils/opcodes.ts create mode 100644 price_feeds/ton/pyth-connector/tests/utils/prices.ts create mode 100644 price_feeds/ton/pyth-connector/tests/utils/pyth.ts create mode 100644 price_feeds/ton/pyth-connector/tests/utils/utils.ts create mode 100644 price_feeds/ton/pyth-connector/tests/utils/wormhole.ts create mode 100644 price_feeds/ton/pyth-connector/tsconfig.json create mode 100644 price_feeds/ton/pyth-connector/wrappers/BaseWrapper.ts create mode 100644 price_feeds/ton/pyth-connector/wrappers/Main.compile.ts create mode 100644 price_feeds/ton/pyth-connector/wrappers/Main.ts create mode 100644 price_feeds/ton/pyth-connector/wrappers/MainNoCheck.compile.ts create mode 100644 price_feeds/ton/pyth-connector/wrappers/PythConnector.compile.ts create mode 100644 price_feeds/ton/pyth-connector/wrappers/PythConnector.ts create mode 100644 price_feeds/ton/pyth-connector/wrappers/PythTest.compile.ts create mode 100644 price_feeds/ton/pyth-connector/wrappers/PythTest.ts create mode 100644 price_feeds/ton/pyth-connector/wrappers/PythTestUpgraded.compile.ts create mode 100644 price_feeds/ton/pyth-connector/wrappers/WormholeTest.compile.ts create mode 100644 price_feeds/ton/pyth-connector/wrappers/WormholeTest.ts diff --git a/price_feeds/ton/pyth-connector/.gitignore b/price_feeds/ton/pyth-connector/.gitignore new file mode 100644 index 0000000..e7e57f3 --- /dev/null +++ b/price_feeds/ton/pyth-connector/.gitignore @@ -0,0 +1,5 @@ +.env +yarn.lock +node_modules +build +dist diff --git a/price_feeds/ton/pyth-connector/.prettierignore b/price_feeds/ton/pyth-connector/.prettierignore new file mode 100644 index 0000000..d163863 --- /dev/null +++ b/price_feeds/ton/pyth-connector/.prettierignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/README.md b/price_feeds/ton/pyth-connector/README.md new file mode 100644 index 0000000..6511e47 --- /dev/null +++ b/price_feeds/ton/pyth-connector/README.md @@ -0,0 +1,50 @@ +# Pyth-connector example +Provides onchain-getter: User -> User JettonWallet -> App -> Pyth -> App -> ... +and proxy call: User -> Pyth -> App -> ... pyth usage examples. + +This example can be used as a separate module providing tools for sandbox testing: exports functions for deploying and configuring a local pyth contract + +It shows techniques how to use the pyth oracle in finacial applications. +The demonstration is fully sandboxed and doesn't need real on-chain contracts nor testnet neither mainnet. +Usage of hermes client is also not required: prices can be formed locally, e.g. **{TON: 3.12345, USDC: 0.998, USDT: 0.999}.** + +## Project structure + +- `contracts` - source code of all the smart contracts of the project and their dependencies. +- `wrappers` - wrapper classes (implementing `Contract` from ton-core) for the contracts, including any [de]serialization primitives and compilation functions. +- `tests` - tests for the contracts. +- `scripts` - scripts used by the project, mainly the deployment scripts. + +## How to use +First you need to install dependencies, node v22 is required, you can use nvm to install it: `nvm use 22` . +Then install dependencies, just run `yarn` + +### Build +to build the module you can run`yarn build` + +### Contracts +To prebuild contracts run`yarn contracts` + +### Test +`yarn test:unit` + +### Deploy +You don't need to deploy this example's contracts to testnet/mainnet, + +## Important Note on Message Handling + +When using the Pyth price feed in the recommended flow (User/App -> Pyth -> Protocol), be aware that: + +### Security Warning ⚠️ + +**CRITICAL**: Integrators MUST validate the sender address in their receive function to ensure messages are coming from the Pyth Oracle contract. Failure to do so could allow attackers to: + +- Send invalid price responses +- Impersonate users via the sender_address and custom_payload fields +- Potentially drain the protocol + +### Message Bouncing Behavior + +- If the target protocol bounces the message (e.g., due to invalid custom payload or other errors), the forwarded TON will remain in the Pyth contract and will not be automatically refunded to the original sender. +- This could be significant when dealing with large amounts of TON (e.g., in DeFi operations). +- Integrators should implement proper error handling and refund mechanisms in their applications. diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/Main.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/Main.fc new file mode 100644 index 0000000..f776dbe --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/Main.fc @@ -0,0 +1,103 @@ +#include "imports/stdlib.fc"; +#include "common/errors.fc"; +#include "common/storage.fc"; +#include "common/op.fc"; +#include "Wormhole.fc"; +#include "Pyth.fc"; + +;; @title Pyth Network Price Oracle Contract for TON +;; @notice This contract serves as the main entry point for the Pyth Network price oracle on TON. +;; @dev The contract handles various operations including: +;; - Updating guardian sets for Wormhole message verification +;; - Updating price feeds with the latest price data +;; - Executing governance actions +;; - Upgrading the contract code +;; - Parsing price feed updates for clients +;; +;; The contract uses Wormhole's cross-chain messaging protocol to verify price updates +;; and governance actions. It maintains a dictionary of price feeds indexed by price ID. +;; Each price feed contains the current price, confidence interval, exponent, and publish time. + +;; Internal message handler +;; @param my_balance - Current contract balance +;; @param msg_value - Amount of TON sent with the message +;; @param in_msg_full - Full incoming message cell +;; @param in_msg_body - Message body as a slice +;; @returns () - No return value +() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { ;; ignore empty messages + return (); + } + + ;; * A 32-bit (big-endian) unsigned integer `op`, identifying the `operation` to be performed, or the `method` of the smart contract to be invoked. + int op = in_msg_body~load_uint(32); + cell data = in_msg_body~load_ref(); + slice data_slice = data.begin_parse(); + + ;; Get sender address from message + slice cs = in_msg_full.begin_parse(); + int flags = cs~load_uint(4); + if (flags & 1) { ;; ignore all bounced messages + return (); + } + slice sender_address = cs~load_msg_addr(); ;; load sender address + + ;; * The remainder of the message body is specific for each supported value of `op`. + if (op == OP_UPDATE_GUARDIAN_SET) { + ;; @notice Updates the guardian set based on a Wormhole VAA + ;; @param data_slice - Slice containing the VAA with guardian set update information + update_guardian_set(data_slice); + } elseif (op == OP_UPDATE_PRICE_FEEDS) { + ;; @notice Updates price feeds with the latest price data + ;; @param msg_value - Amount of TON sent with the message (used for fee calculation) + ;; @param data_slice - Slice containing the price feed update data + update_price_feeds(msg_value, data_slice); + } elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) { + ;; @notice Executes a governance action based on a Wormhole VAA + ;; @param data_slice - Slice containing the VAA with governance action information + execute_governance_action(data_slice); + } elseif (op == OP_UPGRADE_CONTRACT) { + ;; @notice Upgrades the contract code + ;; @param data - Cell containing the new contract code + execute_upgrade_contract(data); + } elseif (op == OP_PARSE_PRICE_FEED_UPDATES) { + ;; @notice Parses price feed updates and returns the results to the caller + ;; @param msg_value - Amount of TON sent with the message (used for fee calculation) + ;; @param data_slice - Slice containing the price feed update data + ;; @param price_ids_slice - Slice containing the price IDs to filter for + ;; @param min_publish_time - Minimum publish time for price updates to be considered + ;; @param max_publish_time - Maximum publish time for price updates to be considered + ;; @param sender_address - Address of the sender (for response) + ;; @param target_address - Address to send the response to + ;; @param custom_payload - Custom payload to include in the response + cell price_ids_cell = in_msg_body~load_ref(); + slice price_ids_slice = price_ids_cell.begin_parse(); + int min_publish_time = in_msg_body~load_uint(64); + int max_publish_time = in_msg_body~load_uint(64); + slice target_address = in_msg_body~load_msg_addr(); + cell custom_payload_cell = in_msg_body~load_ref(); + slice custom_payload = custom_payload_cell.begin_parse(); + parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address, target_address, custom_payload); + } elseif (op == OP_PARSE_UNIQUE_PRICE_FEED_UPDATES) { + ;; @notice Parses unique price feed updates (only the latest for each price ID) and returns the results to the caller + ;; @param msg_val; @notice Parses unique price feed updates (only the latest for each price ID) and returns the results to the caller + ;; @param msg_value - Amount of TON sent with the message (used for fee calculation) + ;; @param data_slice - Slice containing the price feed update data + ;; @param price_ids_slice - Slice containing the price IDs to filter for + ;; @param publish_time - Target publish time for price updates + ;; @param max_staleness - Maximum allowed staleness of price updates (in seconds) + ;; @param sender_address - Address of the sender (for response) + ;; @param target_address - Address to send the response to + ;; @param custom_payload - Custom payload to include in the response + cell price_ids_cell = in_msg_body~load_ref(); + slice price_ids_slice = price_ids_cell.begin_parse(); + int publish_time = in_msg_body~load_uint(64); + int max_staleness = in_msg_body~load_uint(64); + slice target_address = in_msg_body~load_msg_addr(); + cell custom_payload_cell = in_msg_body~load_ref(); + slice custom_payload = custom_payload_cell.begin_parse(); + parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address, target_address, custom_payload); + } else { + throw(0xffff); ;; Throw exception for unknown op + } +} diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/MainNoCheck.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/MainNoCheck.fc new file mode 100644 index 0000000..914f45d --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/MainNoCheck.fc @@ -0,0 +1,65 @@ +{- + This test contract serves two main purposes: + 1. It allows testing of non-getter functions in FunC without requiring specific opcodes for each function. + 2. It provides access to internal functions through wrapper getter functions. + + This approach is common in FunC development, where a separate test contract is used for unit testing. + It enables more comprehensive testing of the contract's functionality, including internal operations + that are not directly accessible through standard getter methods. +-} +{- + The only difference from the Main.fc is that it uses patched Pyth functions, + which don't verify prices sources and allow to run tests with locally generated prices. +-} +#include "imports/stdlib.fc"; +#include "tests/PythNoCheck.fc"; +#include "Wormhole.fc"; +#include "common/op.fc"; + +() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { + return (); + } + + ;; Get sender address from message + slice cs = in_msg_full.begin_parse(); + int flags = cs~load_uint(4); ;; skip flags + if (flags & 1) { + return (); + } + slice sender_address = cs~load_msg_addr(); ;; load sender address + + int op = in_msg_body~load_uint(32); + cell data = in_msg_body~load_ref(); + slice data_slice = data.begin_parse(); + + if (op == OP_UPDATE_GUARDIAN_SET) { + update_guardian_set(data_slice); + } elseif (op == OP_UPDATE_PRICE_FEEDS) { + update_price_feeds(msg_value, data_slice); + } elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) { + execute_governance_action(data_slice); + } elseif (op == OP_UPGRADE_CONTRACT) { + execute_upgrade_contract(data); + } elseif (op == OP_PARSE_PRICE_FEED_UPDATES) { + cell price_ids_cell = in_msg_body~load_ref(); + slice price_ids_slice = price_ids_cell.begin_parse(); + int min_publish_time = in_msg_body~load_uint(64); + int max_publish_time = in_msg_body~load_uint(64); + slice target_address = in_msg_body~load_msg_addr(); + cell custom_payload_cell = in_msg_body~load_ref(); + slice custom_payload = custom_payload_cell.begin_parse(); + parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address, target_address, custom_payload); + } elseif (op == OP_PARSE_UNIQUE_PRICE_FEED_UPDATES) { + cell price_ids_cell = in_msg_body~load_ref(); + slice price_ids_slice = price_ids_cell.begin_parse(); + int publish_time = in_msg_body~load_uint(64); + int max_staleness = in_msg_body~load_uint(64); + slice target_address = in_msg_body~load_msg_addr(); + cell custom_payload_cell = in_msg_body~load_ref(); + slice custom_payload = custom_payload_cell.begin_parse(); + parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address, target_address, custom_payload); + } else { + throw(0xffff); ;; Throw exception for unknown op + } +} diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/Pyth.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/Pyth.fc new file mode 100644 index 0000000..27e9614 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/Pyth.fc @@ -0,0 +1,633 @@ +#include "imports/stdlib.fc"; +#include "common/errors.fc"; +#include "common/storage.fc"; +#include "common/utils.fc"; +#include "common/constants.fc"; +#include "common/merkle_tree.fc"; +#include "common/governance_actions.fc"; +#include "common/gas.fc"; +#include "common/op.fc"; +#include "common/error_handling.fc"; +#include "Wormhole.fc"; + +cell store_price(int price, int conf, int expo, int publish_time) { + return begin_cell() + .store_int(price, 64) + .store_uint(conf, 64) + .store_int(expo, 32) + .store_uint(publish_time, 64) + .end_cell(); +} + +slice read_and_verify_header(slice data) impure { + int magic = data~load_uint(32); + throw_unless(ERROR_INVALID_MAGIC, magic == ACCUMULATOR_MAGIC); + int major_version = data~load_uint(8); + throw_unless(ERROR_INVALID_MAJOR_VERSION, major_version == MAJOR_VERSION); + int minor_version = data~load_uint(8); + throw_if(ERROR_INVALID_MINOR_VERSION, minor_version < MINIMUM_ALLOWED_MINOR_VERSION); + int trailing_header_size = data~load_uint(8); + ;; skip trailing headers + data~skip_bits(trailing_header_size * 8); + int update_type = data~load_uint(8); + throw_unless(ERROR_INVALID_UPDATE_DATA_TYPE, update_type == WORMHOLE_MERKLE_UPDATE_TYPE); + return data; +} + +(int, int, int, int, int, int, int, int, slice) read_and_verify_message(slice cs, int root_digest) impure { + int message_size = cs~load_uint(16); + (cell message, slice cs) = read_and_store_large_data(cs, message_size * 8); + slice message = message.begin_parse(); + slice cs = read_and_verify_proof(root_digest, message, cs); + + int message_type = message~load_uint(8); + throw_unless(ERROR_INVALID_MESSAGE_TYPE, message_type == PRICE_FEED_MESSAGE_TYPE); + + int price_id = message~load_uint(256); + int price = message~load_int(64); + int conf = message~load_uint(64); + int expo = message~load_int(32); + int publish_time = message~load_uint(64); + int prev_publish_time = message~load_uint(64); + int ema_price = message~load_int(64); + int ema_conf = message~load_uint(64); + + return (price_id, price, conf, expo, publish_time, prev_publish_time, ema_price, ema_conf, cs); +} + +(int, int, int, int) parse_price(slice price_feed) { + int price = price_feed~load_int(64); + int conf = price_feed~load_uint(64); + int expo = price_feed~load_int(32); + int publish_time = price_feed~load_uint(64); + return (price, conf, expo, publish_time); +} + +int get_update_fee(slice data) method_id { + load_data(); + slice cs = read_and_verify_header(data); + int wormhole_proof_size_bytes = cs~load_uint(16); + (cell wormhole_proof, slice cs) = read_and_store_large_data(cs, wormhole_proof_size_bytes * 8); + int num_updates = cs~load_uint(8); + return single_update_fee * num_updates; +} + +int get_single_update_fee() method_id { + load_data(); + return single_update_fee; +} + +int get_governance_data_source_index() method_id { + load_data(); + return governance_data_source_index; +} + +cell get_governance_data_source() method_id { + load_data(); + return governance_data_source; +} + +int get_last_executed_governance_sequence() method_id { + load_data(); + return last_executed_governance_sequence; +} + +int get_is_valid_data_source(cell data_source) method_id { + load_data(); + int data_source_key = cell_hash(data_source); + (slice value, int found?) = is_valid_data_source.udict_get?(256, data_source_key); + if (found?) { + return value~load_int(1); + } else { + return 0; + } +} + +(int, int, int, int) get_price_unsafe(int price_feed_id) method_id { + load_data(); + (slice result, int success) = latest_price_feeds.udict_get?(256, price_feed_id); + throw_unless(ERROR_PRICE_FEED_NOT_FOUND, success); + slice price_feed = result~load_ref().begin_parse(); + slice price = price_feed~load_ref().begin_parse(); + return parse_price(price); +} + +(int, int, int, int) get_price_no_older_than(int time_period, int price_feed_id) method_id { + load_data(); + (int price, int conf, int expo, int publish_time) = get_price_unsafe(price_feed_id); + int current_time = now(); + throw_if(ERROR_OUTDATED_PRICE, max(0, current_time - publish_time) > time_period); + return (price, conf, expo, publish_time); +} + +(int, int, int, int) get_ema_price_unsafe(int price_feed_id) method_id { + load_data(); + (slice result, int success) = latest_price_feeds.udict_get?(256, price_feed_id); + throw_unless(ERROR_PRICE_FEED_NOT_FOUND, success); + slice price_feed = result~load_ref().begin_parse(); + slice price = price_feed~load_ref().begin_parse(); + slice ema_price = price_feed~load_ref().begin_parse(); + return parse_price(ema_price); +} + +(int, int, int, int) get_ema_price_no_older_than(int time_period, int price_feed_id) method_id { + load_data(); + (int price, int conf, int expo, int publish_time) = get_ema_price_unsafe(price_feed_id); + int current_time = now(); + throw_if(ERROR_OUTDATED_PRICE, max(0, current_time - publish_time) > time_period); + return (price, conf, expo, publish_time); +} + +(int, int) parse_data_source(cell data_source) { + slice ds = data_source.begin_parse(); + int emitter_chain = ds~load_uint(16); + int emitter_address = ds~load_uint(256); + return (emitter_chain, emitter_address); +} + +int parse_pyth_payload_in_wormhole_vm(slice payload) impure { + int accumulator_wormhole_magic = payload~load_uint(32); + throw_unless(ERROR_INVALID_MAGIC, accumulator_wormhole_magic == ACCUMULATOR_WORMHOLE_MAGIC); + + int update_type = payload~load_uint(8); + throw_unless(ERROR_INVALID_UPDATE_DATA_TYPE, update_type == WORMHOLE_MERKLE_UPDATE_TYPE); + + payload~load_uint(64); ;; Skip slot + payload~load_uint(32); ;; Skip ring_size + + return payload~load_uint(160); ;; Return root_digest +} + +() calculate_and_validate_fees(int msg_value, int num_updates) impure { + int update_fee = single_update_fee * num_updates; + int compute_fee = get_compute_fee( + WORKCHAIN, + UPDATE_PRICE_FEEDS_BASE_GAS + (UPDATE_PRICE_FEEDS_PER_UPDATE_GAS * num_updates) + ); + throw_unless(ERROR_INSUFFICIENT_GAS, msg_value >= compute_fee); + int remaining_msg_value = msg_value - compute_fee; + + ;; Check if the sender has sent enough TON to cover the update_fee + throw_unless(ERROR_INSUFFICIENT_FEE, remaining_msg_value >= update_fee); +} + +(int) find_price_id_index(tuple price_ids, int price_id) { + int len = price_ids.tlen(); + int i = 0; + while (i < len) { + if (price_ids.at(i) == price_id) { + return i; + } + i += 1; + } + return -1; ;; Not found +} + + +tuple parse_price_feeds_from_data(int msg_value, slice data, tuple price_ids, int min_publish_time, int max_publish_time, int unique) { + slice cs = read_and_verify_header(data); + + int wormhole_proof_size_bytes = cs~load_uint(16); + (cell wormhole_proof, slice new_cs) = read_and_store_large_data(cs, wormhole_proof_size_bytes * 8); + cs = new_cs; + + int num_updates = cs~load_uint(8); + + calculate_and_validate_fees(msg_value, num_updates); + + (_, _, _, _, int emitter_chain_id, int emitter_address, _, _, slice payload, _) = parse_and_verify_wormhole_vm(wormhole_proof.begin_parse()); + + ;; Check if the data source is valid + cell data_source = begin_cell() + .store_uint(emitter_chain_id, 16) + .store_uint(emitter_address, 256) + .end_cell(); + + ;; Dictionary doesn't support cell as key, so we use cell_hash to create a 256-bit integer key + int data_source_key = cell_hash(data_source); + (slice value, int found?) = is_valid_data_source.udict_get?(256, data_source_key); + throw_unless(ERROR_UPDATE_DATA_SOURCE_NOT_FOUND, found?); + int valid = value~load_int(1); + throw_unless(ERROR_INVALID_UPDATE_DATA_SOURCE, valid); + + int root_digest = parse_pyth_payload_in_wormhole_vm(payload); + + ;; Create dictionary to store price feeds in order (dict has a udict_get_next? method which returns the next key in order) + cell ordered_feeds = new_dict(); + ;; Track which price IDs we've found + cell found_price_ids = new_dict(); + + int index = 0; + + repeat(num_updates) { + (int price_id, int price, int conf, int expo, int publish_time, int prev_publish_time, int ema_price, int ema_conf, slice new_cs) = read_and_verify_message(cs, root_digest); + cs = new_cs; + + int price_ids_len = price_ids.tlen(); + + ;; Check if we've already processed this price_id to avoid duplicates + (_, int already_processed?) = found_price_ids.udict_get?(256, price_id); + if (~ already_processed?) { ;; Only process if we haven't seen this price_id yet + int should_include = (price_ids_len == 0) + | ((price_ids_len > 0) + & (publish_time >= min_publish_time) + & (publish_time <= max_publish_time) + & ((unique == 0) | (min_publish_time > prev_publish_time))); + + if (should_include) { + ;; Create price feed cell containing both current and EMA prices + cell price_feed_cell = begin_cell() + .store_ref(store_price(price, conf, expo, publish_time)) + .store_ref(store_price(ema_price, ema_conf, expo, publish_time)) + .end_cell(); + + if (price_ids_len == 0) { + ordered_feeds~udict_set(8, index, begin_cell() + .store_uint(price_id, 256) + .store_ref(price_feed_cell) + .end_cell().begin_parse()); + index += 1; + } else { + index = find_price_id_index(price_ids, price_id); + if (index >= 0) { + ordered_feeds~udict_set(8, index, begin_cell() + .store_uint(price_id, 256) + .store_ref(price_feed_cell) + .end_cell().begin_parse()); + } + } + + ;; Mark this price ID as found + found_price_ids~udict_set(256, price_id, begin_cell().store_int(true, 1).end_cell().begin_parse()); + } + } + } + + throw_if(ERROR_INVALID_UPDATE_DATA_LENGTH, ~ cs.slice_empty?()); + + ;; Verify all requested price IDs were found + if (price_ids.tlen() > 0) { + int i = 0; + repeat(price_ids.tlen()) { + int requested_id = price_ids.at(i); + (_, int found?) = found_price_ids.udict_get?(256, requested_id); + throw_unless(ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE, found?); + i += 1; + } + } + + ;; Create final ordered tuple from dictionary + tuple price_feeds = empty_tuple(); + int index = -1; + do { + (index, slice value, int success) = ordered_feeds.udict_get_next?(8, index); + if (success) { + tuple price_feed = empty_tuple(); + price_feed~tpush(value~load_uint(256)); ;; price_id + price_feed~tpush(value~load_ref()); ;; price_feed_cell + price_feeds~tpush(price_feed); + } + } until (~ success); + + return price_feeds; +} + +;; Creates a chain of cells from price feeds, with each cell containing exactly one price_id (256 bits) +;; and one ref to the price feed cell. Returns the head of the chain. +;; Each cell now contains exactly: +;; - One price_id (256 bits) +;; - One ref to price_feed_cell +;; - One optional ref to next cell in chain +;; This approach is: +;; - More consistent with TON's cell model +;; - Easier to traverse and read individual price feeds +;; - Cleaner separation of data +;; - More predictable in terms of cell structure +cell create_price_feed_cell_chain(tuple price_feeds) { + cell result = null(); + + int i = price_feeds.tlen() - 1; + while (i >= 0) { + tuple price_feed = price_feeds.at(i); + int price_id = price_feed.at(0); + cell price_feed_cell = price_feed.at(1); + + ;; Create new cell with single price feed and chain to previous result + builder current_builder = begin_cell() + .store_uint(price_id, 256) ;; Store price_id + .store_ref(price_feed_cell); ;; Store price data ref + + ;; Chain to previous cells if they exist + if (~ cell_null?(result)) { + current_builder = current_builder.store_ref(result); + } + + result = current_builder.end_cell(); + i -= 1; + } + + return result; +} + +() send_price_feeds_response(tuple price_feeds, int msg_value, int op, slice sender_address, slice target_address, slice custom_payload) impure { + ;; Build response cell with price feeds + builder response = begin_cell() + .store_uint(op, 32) ;; Response op + .store_uint(price_feeds.tlen(), 8); ;; Number of price feeds + + ;; Create and store price feed cell chain + cell price_feeds_cell = create_price_feed_cell_chain(price_feeds); + cell custom_payload_cell = begin_cell().store_slice(custom_payload).end_cell(); + response = response.store_ref(price_feeds_cell).store_slice(sender_address).store_ref(custom_payload_cell); + + int num_price_feeds = price_feeds.tlen(); + + ;; Calculate all fees + int compute_fee = get_compute_fee(WORKCHAIN, get_gas_consumed()); + int update_fee = single_update_fee * price_feeds.tlen(); + + ;; Calculate total fees and remaining excess + int total_fees = compute_fee + update_fee; + int excess = msg_value - total_fees; + + ;; SECURITY: Integrators MUST validate that messages are from this Pyth contract + ;; in their receive function. Otherwise, attackers could: + ;; 1. Send invalid price responses + ;; 2. Impersonate users via sender_address and custom_payload fields + ;; 3. Potentially drain the protocol + ;; + ;; Note: This message is bounceable. If the target contract rejects the message, + ;; the excess TON will remain in this contract and won't be automatically refunded to the + ;; original sender. Integrators should handle failed requests and refunds in their implementation. + send_raw_message(begin_cell() + .store_uint(0x18, 6) + .store_slice(target_address) + .store_coins(excess) + .store_uint(1, MSG_SERIALIZE_BITS) + .store_ref(response.end_cell()) + .end_cell(), + 0); +} + +;; Helper function to parse price IDs from a slice, handling cell chain traversal +;; Returns a tuple containing the parsed price IDs +tuple parse_price_ids_from_slice(slice price_ids_slice) { + int price_ids_len = price_ids_slice~load_uint(8); + tuple price_ids = empty_tuple(); + + ;; Process each price ID, handling potential cell boundaries + int i = 0; + while (i < price_ids_len) { + builder price_id_builder = begin_cell(); + int bits_loaded = 0; + + ;; We need to load exactly 256 bits for each price ID + while (bits_loaded < 256) { + ;; Calculate how many bits we can load from the current slice + int bits_to_load = min(price_ids_slice.slice_bits(), 256 - bits_loaded); + + ;; Load and store those bits + price_id_builder = price_id_builder.store_slice(price_ids_slice~load_bits(bits_to_load)); + bits_loaded += bits_to_load; + + ;; If we haven't loaded all 256 bits yet, we need to move to the next cell + if (bits_loaded < 256) { + ;; Make sure we have a next cell to load from + throw_unless(35, ~ price_ids_slice.slice_refs_empty?()); + price_ids_slice = price_ids_slice~load_ref().begin_parse(); + } + } + + ;; Extract the complete price ID from the builder + slice price_id_slice = price_id_builder.end_cell().begin_parse(); + int price_id = price_id_slice~load_uint(256); + price_ids~tpush(price_id); + i += 1; + } + + return price_ids; +} + +() parse_price_feed_updates(int msg_value, slice update_data_slice, slice price_ids_slice, int min_publish_time, int max_publish_time, slice sender_address, slice target_address, slice custom_payload) impure { + try { + load_data(); + + ;; Use the helper function to parse price IDs + tuple price_ids = parse_price_ids_from_slice(price_ids_slice); + + tuple price_feeds = parse_price_feeds_from_data(msg_value, update_data_slice, price_ids, min_publish_time, max_publish_time, false); + send_price_feeds_response(price_feeds, msg_value, OP_PARSE_PRICE_FEED_UPDATES, + sender_address, target_address, custom_payload); + } catch (_, error_code) { + ;; Handle any unexpected errors + emit_error(error_code, OP_PARSE_PRICE_FEED_UPDATES, + sender_address, begin_cell().store_slice(custom_payload).end_cell()); + } +} + +() parse_unique_price_feed_updates(int msg_value, slice update_data_slice, slice price_ids_slice, int publish_time, int max_staleness, slice sender_address, slice target_address, slice custom_payload) impure { + try { + load_data(); + + ;; Use the helper function to parse price IDs + tuple price_ids = parse_price_ids_from_slice(price_ids_slice); + + tuple price_feeds = parse_price_feeds_from_data(msg_value, update_data_slice, price_ids, publish_time, publish_time + max_staleness, true); + send_price_feeds_response(price_feeds, msg_value, OP_PARSE_UNIQUE_PRICE_FEED_UPDATES, sender_address, target_address, custom_payload); + } catch (_, error_code) { + ;; Handle any unexpected errors + emit_error(error_code, OP_PARSE_UNIQUE_PRICE_FEED_UPDATES, + sender_address, begin_cell().store_slice(custom_payload).end_cell()); + } +} + +() update_price_feeds(int msg_value, slice data) impure { + load_data(); + tuple price_feeds = parse_price_feeds_from_data(msg_value, data, empty_tuple(), 0, 0, false); + int num_updates = price_feeds.tlen(); + + int i = 0; + while(i < num_updates) { + tuple price_feed = price_feeds.at(i); + int price_id = price_feed.at(0); + cell price_feed_cell = price_feed.at(1); + slice price_feed = price_feed_cell.begin_parse(); + slice price = price_feed~load_ref().begin_parse(); + slice ema_price = price_feed~load_ref().begin_parse(); + (int price_, int conf, int expo, int publish_time) = parse_price(price); + + (slice latest_price_info, int found?) = latest_price_feeds.udict_get?(256, price_id); + int latest_publish_time = 0; + if (found?) { + slice price_feed_slice = latest_price_info~load_ref().begin_parse(); + slice price_slice = price_feed_slice~load_ref().begin_parse(); + + price_slice~load_int(64); ;; Skip price + price_slice~load_uint(64); ;; Skip conf + price_slice~load_int(32); ;; Skip expo + latest_publish_time = price_slice~load_uint(64); + } + + if (publish_time > latest_publish_time) { + latest_price_feeds~udict_set(256, price_id, begin_cell().store_ref(price_feed_cell).end_cell().begin_parse()); + } + i += 1; + } + + store_data(); +} + +() verify_governance_vm(int emitter_chain_id, int emitter_address, int sequence) impure { + (int gov_chain_id, int gov_address) = parse_data_source(governance_data_source); + throw_unless(ERROR_INVALID_GOVERNANCE_DATA_SOURCE, emitter_chain_id == gov_chain_id); + throw_unless(ERROR_INVALID_GOVERNANCE_DATA_SOURCE, emitter_address == gov_address); + throw_if(ERROR_OLD_GOVERNANCE_MESSAGE, sequence <= last_executed_governance_sequence); + last_executed_governance_sequence = sequence; +} + +(int, int, slice) parse_governance_instruction(slice payload) impure { + int magic = payload~load_uint(32); + throw_unless(ERROR_INVALID_GOVERNANCE_MAGIC, magic == GOVERNANCE_MAGIC); + + int module = payload~load_uint(8); + throw_unless(ERROR_INVALID_GOVERNANCE_MODULE, module == GOVERNANCE_MODULE); + + int action = payload~load_uint(8); + + int target_chain_id = payload~load_uint(16); + + return (target_chain_id, action, payload); +} + +int apply_decimal_expo(int value, int expo) { + int result = value; + repeat (expo) { + result *= 10; + } + return result; +} + +() execute_upgrade_contract(cell new_code) impure { + load_data(); + int hash_code = cell_hash(new_code); + throw_unless(ERROR_INVALID_CODE_HASH, upgrade_code_hash == hash_code); + + ;; Set the new code + set_code(new_code); + + ;; Set the code continuation to the new code + set_c3(new_code.begin_parse().bless()); + + ;; Reset the upgrade code hash + upgrade_code_hash = 0; + + ;; Store the data to persist the reset above + store_data(); + + ;; Throw an exception to end the current execution + ;; The contract will be restarted with the new code + throw(0); +} + +() execute_authorize_upgrade_contract(slice payload) impure { + int code_hash = payload~load_uint(256); + upgrade_code_hash = code_hash; +} + +() execute_authorize_governance_data_source_transfer(slice payload) impure { + ;; Verify the claim VAA + (_, _, _, _, int emitter_chain_id, int emitter_address, int sequence, _, slice claim_payload, _) = parse_and_verify_wormhole_vm(payload); + + ;; Parse the claim payload + (int target_chain_id, int action, slice claim_payload) = parse_governance_instruction(claim_payload); + + ;; Verify that this is a valid governance action for this chain + throw_if(ERROR_INVALID_GOVERNANCE_TARGET, (target_chain_id != 0) & (target_chain_id != chain_id)); + throw_unless(ERROR_INVALID_GOVERNANCE_ACTION, action == REQUEST_GOVERNANCE_DATA_SOURCE_TRANSFER); + + ;; Extract the new governance data source index from the claim payload + int new_governance_data_source_index = claim_payload~load_uint(32); + + ;; Verify that the new index is greater than the current index + int current_index = governance_data_source_index; + throw_if(ERROR_OLD_GOVERNANCE_MESSAGE, new_governance_data_source_index <= current_index); + + ;; Update the governance data source + governance_data_source = begin_cell() + .store_uint(emitter_chain_id, 16) + .store_uint(emitter_address, 256) + .end_cell(); + + governance_data_source_index = new_governance_data_source_index; + + ;; Update the last executed governance sequence + last_executed_governance_sequence = sequence; +} + +() execute_set_data_sources(slice payload) impure { + int num_sources = payload~load_uint(8); + cell new_data_sources = new_dict(); + + repeat(num_sources) { + (cell data_source, slice new_payload) = read_and_store_large_data(payload, 272); ;; 272 = 256 + 16 + payload = new_payload; + slice data_source_slice = data_source.begin_parse(); + int emitter_chain_id = data_source_slice~load_uint(16); + int emitter_address = data_source_slice~load_uint(256); + cell data_source = begin_cell() + .store_uint(emitter_chain_id, 16) + .store_uint(emitter_address, 256) + .end_cell(); + int data_source_key = cell_hash(data_source); + new_data_sources~udict_set(256, data_source_key, begin_cell().store_int(true, 1).end_cell().begin_parse()); + } + + ;; Verify that all data in the payload was processed + throw_unless(ERROR_INVALID_PAYLOAD_LENGTH, payload.slice_empty?()); + + is_valid_data_source = new_data_sources; +} + +() execute_set_fee(slice payload) impure { + int value = payload~load_uint(64); + int expo = payload~load_uint(64); + int new_fee = apply_decimal_expo(value, expo); + single_update_fee = new_fee; +} + +() execute_governance_payload(int action, slice payload) impure { + if (action == AUTHORIZE_UPGRADE_CONTRACT) { + execute_authorize_upgrade_contract(payload); + } elseif (action == AUTHORIZE_GOVERNANCE_DATA_SOURCE_TRANSFER) { + execute_authorize_governance_data_source_transfer(payload); + } elseif (action == SET_DATA_SOURCES) { + execute_set_data_sources(payload); + } elseif (action == SET_FEE) { + execute_set_fee(payload); + } elseif (action == SET_VALID_PERIOD) { + ;; Unsupported governance action + throw(ERROR_INVALID_GOVERNANCE_ACTION); + } elseif (action == REQUEST_GOVERNANCE_DATA_SOURCE_TRANSFER) { + ;; RequestGovernanceDataSourceTransfer can only be part of + ;; AuthorizeGovernanceDataSourceTransfer message + throw(ERROR_INVALID_GOVERNANCE_ACTION); + } else { + throw(ERROR_INVALID_GOVERNANCE_ACTION); + } +} + +() execute_governance_action(slice in_msg_body) impure { + load_data(); + + (_, _, _, _, int emitter_chain_id, int emitter_address, int sequence, _, slice payload, _) = parse_and_verify_wormhole_vm(in_msg_body); + + verify_governance_vm(emitter_chain_id, emitter_address, sequence); + + (int target_chain_id, int action, slice payload) = parse_governance_instruction(payload); + + throw_if(ERROR_INVALID_GOVERNANCE_TARGET, (target_chain_id != 0) & (target_chain_id != chain_id)); + + execute_governance_payload(action, payload); + + store_data(); +} diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/Wormhole.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/Wormhole.fc new file mode 100644 index 0000000..283559e --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/Wormhole.fc @@ -0,0 +1,255 @@ +#include "imports/stdlib.fc"; +#include "common/errors.fc"; +#include "common/utils.fc"; +#include "common/storage.fc"; +#include "common/constants.fc"; + +;; Signature verification function +;; ECRECOVER: Recovers the signer's address from the signature +;; It returns 1 value (0) on failure and 4 values on success +;; NULLSWAPIFNOT and NULLSWAPIFNOT2: Ensure consistent return of 4 values +;; These opcodes swap nulls onto the stack if ECRECOVER fails, maintaining the 4-value return +(int, int, int, int) check_sig (int hash, int v, int r, int s) asm + "ECRECOVER" ;; Attempt to recover the signer's address + "NULLSWAPIFNOT" ;; If recovery failed, insert null under the top of the stack + "NULLSWAPIFNOT2"; ;; If recovery failed, insert two more nulls under the top of the stack + +;; Internal helper methods +(int, cell, int) parse_guardian_set(slice guardian_set) { + slice cs = guardian_set~load_ref().begin_parse(); + int expiration_time = cs~load_uint(64); + cell keys_dict = cs~load_dict(); + int key_count = 0; + int key = -1; + do { + (key, slice address, int found) = keys_dict.udict_get_next?(8, key); + if (found) { + key_count += 1; + } + } until (~ found); + return (expiration_time, keys_dict, key_count); +} + +(int, cell, int) get_guardian_set_internal(int index) { + (slice guardian_set, int found?) = guardian_sets.udict_get?(32, index); + throw_unless(ERROR_GUARDIAN_SET_NOT_FOUND, found?); + (int expiration_time, cell keys_dict, int key_count) = parse_guardian_set(guardian_set); + return (expiration_time, keys_dict, key_count); +} + + +;; Get methods +int get_chain_id() method_id { + load_data(); + return chain_id; +} + +int get_current_guardian_set_index() method_id { + load_data(); + return current_guardian_set_index; +} + +(int, cell, int) get_guardian_set(int index) method_id { + load_data(); + return get_guardian_set_internal(index); +} + +int get_governance_chain_id() method_id { + load_data(); + return governance_chain_id; +} + +int get_governance_contract() method_id { + load_data(); + return governance_contract; +} + +int governance_action_is_consumed(int hash) method_id { + load_data(); + (_, int found?) = consumed_governance_actions.udict_get?(256, hash); + return found?; +} + +() verify_signatures(int hash, cell signatures, int signers_length, cell guardian_set_keys, int guardian_set_size) impure { + slice cs = signatures.begin_parse(); + int i = 0; + int last_signature_index = -1; + int valid_signatures = 0; + + while (i < signers_length) { + int bits_to_load = 528; + builder sig_builder = begin_cell(); + + while (bits_to_load > 0) { + int available_bits = cs.slice_bits(); + int bits = min(bits_to_load, available_bits); + sig_builder = sig_builder.store_slice(cs~load_bits(bits)); + bits_to_load -= bits; + + if (bits_to_load > 0) { + cs = cs~load_ref().begin_parse(); + } + } + + slice sig_slice = sig_builder.end_cell().begin_parse(); + int guardian_index = sig_slice~load_uint(8); + throw_if(ERROR_SIGNATURE_INDEX_NOT_INCREASING, guardian_index <= last_signature_index); + int r = sig_slice~load_uint(256); + int s = sig_slice~load_uint(256); + int v = sig_slice~load_uint(8); + (_, int x1, int x2, int valid) = check_sig(hash, v >= 27 ? v - 27 : v, r, s); + throw_unless(ERROR_INVALID_SIGNATURES, valid); + int parsed_address = pubkey_to_eth_address(x1, x2); + (slice guardian_key, int found?) = guardian_set_keys.udict_get?(8, guardian_index); + int guardian_address = guardian_key~load_uint(160); + throw_unless(ERROR_INVALID_GUARDIAN_ADDRESS, parsed_address == guardian_address); + valid_signatures += 1; + last_signature_index = guardian_index; + i += 1; + } + + ;; Check quorum (2/3 + 1) + ;; We're using a fixed point number transformation with 1 decimal to deal with rounding. + throw_unless(ERROR_NO_QUORUM, valid_signatures >= (((guardian_set_size * 10) / 3) * 2) / 10 + 1); +} + +(int, int, int, int, int, int, int, int, slice, int) parse_and_verify_wormhole_vm(slice in_msg_body) impure { + load_data(); + ;; Parse VM fields + int version = in_msg_body~load_uint(8); + throw_unless(ERROR_INVALID_VERSION, version == WORMHOLE_VM_VERSION); + int vm_guardian_set_index = in_msg_body~load_uint(32); + ;; Verify and check if guardian set is valid + (int expiration_time, cell keys_dict, int key_count) = get_guardian_set_internal(vm_guardian_set_index); + throw_if(ERROR_INVALID_GUARDIAN_SET_KEYS_LENGTH, cell_null?(keys_dict)); + throw_unless(ERROR_INVALID_GUARDIAN_SET, + (current_guardian_set_index >= vm_guardian_set_index) & + ((expiration_time == 0) | (expiration_time > now())) + ); + int signers_length = in_msg_body~load_uint(8); + ;; Calculate signatures_size in bits (66 bytes per signature: 1 (guardianIndex) + 32 (r) + 32 (s) + 1 (v)) + int signatures_size = signers_length * 66 * 8; + + ;; Load signatures + (cell signatures, slice remaining_body) = read_and_store_large_data(in_msg_body, signatures_size); + in_msg_body = remaining_body; + + ;; Calculate total body length across all references + int body_length = 0; + int continue? = true; + do { + body_length += remaining_body.slice_bits(); + if (remaining_body.slice_refs_empty?()) { + continue? = false; + } else { + remaining_body = remaining_body~load_ref().begin_parse(); + } + } until (~ continue?); + + ;; Load body + (cell body_cell, _) = read_and_store_large_data(in_msg_body, body_length); + + int hash = keccak256_int(keccak256_slice(body_cell.begin_parse())); + ;; Verify signatures + verify_signatures(hash, signatures, signers_length, keys_dict, key_count); + + slice body_slice = body_cell.begin_parse(); + int timestamp = body_slice~load_uint(32); + int nonce = body_slice~load_uint(32); + int emitter_chain_id = body_slice~load_uint(16); + int emitter_address = body_slice~load_uint(256); + int sequence = body_slice~load_uint(64); + int consistency_level = body_slice~load_uint(8); + slice payload = body_slice; + + return ( + version, + vm_guardian_set_index, + timestamp, + nonce, + emitter_chain_id, + emitter_address, + sequence, + consistency_level, + payload, + hash + ); +} + +(int, int, int, cell, int) parse_encoded_upgrade(int guardian_set_index, slice payload) impure { + int module = payload~load_uint(256); + throw_unless(ERROR_INVALID_MODULE, module == UPGRADE_MODULE); + + int action = payload~load_uint(8); + throw_unless(ERROR_INVALID_GOVERNANCE_ACTION, action == GUARDIAN_SET_UPGRADE_ACTION); + + int chain = payload~load_uint(16); + int new_guardian_set_index = payload~load_uint(32); + throw_unless(ERROR_NEW_GUARDIAN_SET_INDEX_IS_INVALID, new_guardian_set_index == (guardian_set_index + 1)); + + int guardian_length = payload~load_uint(8); + throw_unless(ERROR_INVALID_GUARDIAN_SET_KEYS_LENGTH, guardian_length > 0); + + cell new_guardian_set_keys = new_dict(); + int key_count = 0; + while (key_count < guardian_length) { + builder key = begin_cell(); + int key_bits_loaded = 0; + while (key_bits_loaded < 160) { + int bits_to_load = min(payload.slice_bits(), 160 - key_bits_loaded); + key = key.store_slice(payload~load_bits(bits_to_load)); + key_bits_loaded += bits_to_load; + if (key_bits_loaded < 160) { + throw_unless(ERROR_INVALID_GUARDIAN_SET_UPGRADE_LENGTH, ~ payload.slice_refs_empty?()); + payload = payload~load_ref().begin_parse(); + } + } + slice key_slice = key.end_cell().begin_parse(); + new_guardian_set_keys~udict_set(8, key_count, key_slice); + key_count += 1; + } + throw_unless(ERROR_GUARDIAN_SET_KEYS_LENGTH_NOT_EQUAL, key_count == guardian_length); + throw_unless(ERROR_INVALID_GUARDIAN_SET_UPGRADE_LENGTH, payload.slice_empty?()); + + return (action, chain, module, new_guardian_set_keys, new_guardian_set_index); +} + +() update_guardian_set(slice in_msg_body) impure { + ;; Verify governance VM + (int version, int vm_guardian_set_index, int timestamp, int nonce, int emitter_chain_id, int emitter_address, int sequence, int consistency_level, slice payload, int hash) = parse_and_verify_wormhole_vm(in_msg_body); + + ;; Verify the emitter chain and address + throw_unless(ERROR_INVALID_GOVERNANCE_CHAIN, emitter_chain_id == governance_chain_id); + throw_unless(ERROR_INVALID_GOVERNANCE_CONTRACT, emitter_address == governance_contract); + + ;; Check if the governance action has already been consumed + throw_if(ERROR_GOVERNANCE_ACTION_ALREADY_CONSUMED, governance_action_is_consumed(hash)); + + ;; Parse the new guardian set from the payload + (int action, int chain, int module, cell new_guardian_set_keys, int new_guardian_set_index) = parse_encoded_upgrade(current_guardian_set_index, payload); + + ;; Set expiry if current GuardianSet exists + (slice current_guardian_set, int found?) = guardian_sets.udict_get?(32, current_guardian_set_index); + if (found?) { + (int expiration_time, cell keys_dict, int key_count) = parse_guardian_set(current_guardian_set); + cell updated_guardian_set = begin_cell() + .store_uint(now() + GUARDIAN_SET_EXPIRY, 64) ;; expiration time + .store_dict(keys_dict) ;; keys + .end_cell(); + guardian_sets~udict_set(32, current_guardian_set_index, begin_cell().store_ref(updated_guardian_set).end_cell().begin_parse()); ;; store reference to updated_guardian_set because storeDict stores a dict into a ref + } + + ;; Store the new guardian set + cell new_guardian_set = begin_cell() + .store_uint(0, 64) ;; expiration_time, set to 0 initially + .store_dict(new_guardian_set_keys) + .end_cell(); + guardian_sets~udict_set(32, new_guardian_set_index, begin_cell().store_ref(new_guardian_set).end_cell().begin_parse()); + + ;; Update the current guardian set index + current_guardian_set_index = new_guardian_set_index; + + ;; Mark the governance action as consumed + consumed_governance_actions~udict_set(256, hash, begin_cell().store_int(true, 1).end_cell().begin_parse()); + store_data(); +} diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/common/constants.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/common/constants.fc new file mode 100644 index 0000000..2d7674c --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/common/constants.fc @@ -0,0 +1,54 @@ +const int ACCUMULATOR_MAGIC = 0x504e4155; ;; "PNAU" (Pyth Network Accumulator Update) +const int ACCUMULATOR_WORMHOLE_MAGIC = 0x41555756; ;; "AUWV" (Accumulator Update Wormhole Verficiation) +const int GOVERNANCE_MAGIC = 0x5054474d; ;; "PTGM" (Pyth Governance Message) +const int GOVERNANCE_MODULE = 1; +const int MAJOR_VERSION = 1; +const int MINIMUM_ALLOWED_MINOR_VERSION = 0; + +const int WORMHOLE_VM_VERSION = 1; + +const int GUARDIAN_SET_EXPIRY = 86400; ;; 1 day in seconds +const int UPGRADE_MODULE = 0x0000000000000000000000000000000000000000000000000000000000436f7265; ;; "Core" (left-padded to 256 bits) in hex +const int GUARDIAN_SET_UPGRADE_ACTION = 2; + +const int WORMHOLE_MERKLE_UPDATE_TYPE = 0; + +const int PRICE_FEED_MESSAGE_TYPE = 0; + +;; Bit layout: (https://docs.ton.org/v3/documentation/smart-contracts/message-management/sending-messages#message-layout) +;; 1 - extra-currencies dictionary (0 = empty) +;; 4 - ihr_fee (VarUInteger 16) +;; 4 - fwd_fee (VarUInteger 16) +;; 64 - created_lt (uint64) +;; 32 - created_at (uint32) +;; 1 - init field presence (0 = no init) +;; 1 - body serialization (0 = in-place) +const int MSG_SERIALIZE_BITS = 1 + 4 + 4 + 64 + 32 + 1 + 1; ;; 107 bits total + +;; Structure: +;; - 256 bits: price_id +;; Price: +;; - 64 bits: price +;; - 64 bits: confidence +;; - 32 bits: exponent +;; - 64 bits: publish_time +;; EMA Price: +;; - 64 bits: price +;; - 64 bits: confidence +;; - 32 bits: exponent +;; - 64 bits: publish_time +const int PRICE_FEED_BITS = 256 + 224 + 224; + +{- + The main workchain ID in TON. Currently, TON has two blockchains: + 1. Masterchain: Used for system-level operations and consensus. + 2. Basechain/Workchain: The primary chain for user accounts and smart contracts. + + While TON supports up to 2^32 workchains, currently only Workchain 0 is active. + This constant defines the default workchain for smart contract deployment and interactions. + + Note: Gas costs differ between chains: + - Basechain: 1 gas = 400 nanotons = 0.0000004 TON + - Masterchain: 1 gas = 10000 nanotons = 0.00001 TON (25x more expensive) +-} +const int WORKCHAIN = 0; diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/common/error_handling.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/common/error_handling.fc new file mode 100644 index 0000000..53809cd --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/common/error_handling.fc @@ -0,0 +1,45 @@ +#include "op.fc"; +#include "errors.fc"; +#include "constants.fc"; +#include "../imports/stdlib.fc"; + +() emit_error(int error_code, int op, slice sender_address, cell custom_payload) impure inline { + ;; Create error message cell with context + cell msg = begin_cell() + .store_uint(OP_RESPONSE_ERROR, 32) + .store_uint(error_code, 32) + .store_uint(op, 32) + .store_ref(custom_payload) + .end_cell(); + + ;; Send error response back to sender + var msg = begin_cell() + .store_uint(0x18, 6) ;; nobounce + .store_slice(sender_address) ;; to_addr + .store_coins(0) ;; value + .store_uint(1, MSG_SERIALIZE_BITS) ;; msg header + .store_ref(msg) ;; error info + .end_cell(); + + send_raw_message(msg, 64); +} + +() emit_success(slice sender_address, cell result, cell custom_payload) impure inline { + ;; Create success message cell + cell msg = begin_cell() + .store_uint(OP_RESPONSE_SUCCESS, 32) + .store_ref(result) ;; Result data + .store_ref(custom_payload) ;; Original custom payload + .end_cell(); + + ;; Send success response + var msg = begin_cell() + .store_uint(0x18, 6) ;; nobounce + .store_slice(sender_address) ;; to_addr + .store_coins(0) ;; value + .store_uint(1, MSG_SERIALIZE_BITS) ;; msg header + .store_ref(msg) ;; success info + .end_cell(); + + send_raw_message(msg, 64); +} diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/common/errors.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/common/errors.fc new file mode 100644 index 0000000..2dd5972 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/common/errors.fc @@ -0,0 +1,49 @@ +;; Error codes enum + +;; Wormhole +const int ERROR_INVALID_GUARDIAN_SET = 1000; +const int ERROR_INVALID_VERSION = 1001; +const int ERROR_GUARDIAN_SET_NOT_FOUND = 1002; +const int ERROR_GUARDIAN_SET_EXPIRED = 1003; +const int ERROR_INVALID_SIGNATURES = 1004; +const int ERROR_INVALID_EMITTER_ADDRESS = 1005; +const int ERROR_GOVERNANCE_ACTION_ALREADY_CONSUMED = 1006; +const int ERROR_INVALID_GUARDIAN_SET_KEYS_LENGTH = 1007; +const int ERROR_INVALID_SIGNATURE_LENGTH = 1008; +const int ERROR_SIGNATURE_INDICES_NOT_ASCENDING = 1009; +const int ERROR_NO_QUORUM = 1010; +const int ERROR_INVALID_MODULE = 1011; +const int ERROR_INVALID_GOVERNANCE_ACTION = 1012; +const int ERROR_NEW_GUARDIAN_SET_INDEX_IS_INVALID = 1013; +const int ERROR_GUARDIAN_SET_KEYS_LENGTH_NOT_EQUAL = 1014; +const int ERROR_INVALID_GUARDIAN_SET_UPGRADE_LENGTH = 1015; +const int ERROR_INVALID_GOVERNANCE_CHAIN = 1016; +const int ERROR_INVALID_GOVERNANCE_CONTRACT = 1017; +const int ERROR_INVALID_GUARDIAN_ADDRESS = 1018; +const int ERROR_SIGNATURE_INDEX_NOT_INCREASING = 1019; + +;; Pyth +const int ERROR_PRICE_FEED_NOT_FOUND = 2000; +const int ERROR_OUTDATED_PRICE = 2001; +const int ERROR_INVALID_MAGIC = 2002; +const int ERROR_INVALID_MAJOR_VERSION = 2003; +const int ERROR_INVALID_MINOR_VERSION = 2004; +const int ERROR_UPDATE_DATA_SOURCE_NOT_FOUND = 2005; +const int ERROR_INVALID_UPDATE_DATA_SOURCE = 2006; +const int ERROR_DIGEST_MISMATCH = 2007; +const int ERROR_INVALID_UPDATE_DATA_LENGTH = 2008; +const int ERROR_INVALID_UPDATE_DATA_TYPE = 2009; +const int ERROR_INVALID_MESSAGE_TYPE = 2010; +const int ERROR_INSUFFICIENT_FEE = 2011; +const int ERROR_INVALID_PROOF_SIZE = 2012; +const int ERROR_INVALID_GOVERNANCE_DATA_SOURCE = 2013; +const int ERROR_OLD_GOVERNANCE_MESSAGE = 2014; +const int ERROR_INVALID_GOVERNANCE_TARGET = 2015; +const int ERROR_INVALID_GOVERNANCE_MAGIC = 2016; +const int ERROR_INVALID_GOVERNANCE_MODULE = 2017; +const int ERROR_INVALID_CODE_HASH = 2018; +const int ERROR_INVALID_PAYLOAD_LENGTH = 2019; +const int ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE = 2020; + +;; Common +const int ERROR_INSUFFICIENT_GAS = 3000; diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/common/gas.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/common/gas.fc new file mode 100644 index 0000000..9a938a4 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/common/gas.fc @@ -0,0 +1,15 @@ +int get_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEE"; +int get_gas_consumed() asm "GASCONSUMED"; + + +;; 1 update: 262,567 gas +;; 2 updates: 347,791 (+85,224) +;; 3 updates: 431,504 (+83,713) +;; 4 updates: 514,442 (+82,938) +;; 5 updates: 604,247 (+89,805) +;; 6 updates: 683,113 (+78,866) +;; 10 updates: 947,594 +;; Upper bound gas increase per additional update: ~90,000 +;; Base cost (1 update): ~262,567 gas +const UPDATE_PRICE_FEEDS_BASE_GAS = 300000; ;; Base cost + 10% safety margin rounded up because the amount of gas used can vary based on the current state of the blockchain +const UPDATE_PRICE_FEEDS_PER_UPDATE_GAS = 90000; ;; Per update cost diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/common/governance_actions.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/common/governance_actions.fc new file mode 100644 index 0000000..adf2d29 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/common/governance_actions.fc @@ -0,0 +1,6 @@ +const int AUTHORIZE_UPGRADE_CONTRACT = 0; +const int AUTHORIZE_GOVERNANCE_DATA_SOURCE_TRANSFER = 1; +const int SET_DATA_SOURCES = 2; +const int SET_FEE = 3; +const int SET_VALID_PERIOD = 4; +const int REQUEST_GOVERNANCE_DATA_SOURCE_TRANSFER = 5; diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/common/merkle_tree.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/common/merkle_tree.fc new file mode 100644 index 0000000..b319f91 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/common/merkle_tree.fc @@ -0,0 +1,43 @@ +#include "errors.fc"; +#include "../imports/stdlib.fc"; +#include "utils.fc"; + +const int MERKLE_LEAF_PREFIX = 0; +const int MERKLE_NODE_PREFIX = 1; + +int leaf_hash(slice message) { + int hash = keccak256_slice(begin_cell() + .store_uint(MERKLE_LEAF_PREFIX, 8) + .store_slice(message) + .end_cell().begin_parse()); + return hash >> 96; +} + +int node_hash(int a, int b) { + int min_value = min(a, b); + int max_value = max(a, b); + int hash = keccak256_slice(begin_cell() + .store_uint(MERKLE_NODE_PREFIX, 8) + .store_uint(min_value, 160) + .store_uint(max_value, 160) + .end_cell().begin_parse()); + return hash >> 96; +} + +slice read_and_verify_proof(int root_digest, slice message, slice cs) impure { + int current_hash = leaf_hash(message); + int proof_size = cs~load_uint(8); + + repeat(proof_size) { + builder sibling_digest = begin_cell(); + (cell digest_cell, cs) = read_and_store_large_data(cs, 160); + slice digest_slice = digest_cell.begin_parse(); + sibling_digest = sibling_digest.store_slice(digest_slice); + slice sibling_digest_slice = sibling_digest.end_cell().begin_parse(); + int sibling_digest_int = sibling_digest_slice~load_uint(160); + current_hash = node_hash(current_hash, sibling_digest_int); + } + + throw_unless(ERROR_DIGEST_MISMATCH, root_digest == current_hash); + return cs; +} diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/common/op.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/common/op.fc new file mode 100644 index 0000000..6d066b1 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/common/op.fc @@ -0,0 +1,10 @@ +const int OP_UPDATE_GUARDIAN_SET = 1; +const int OP_UPDATE_PRICE_FEEDS = 2; +const int OP_EXECUTE_GOVERNANCE_ACTION = 3; +const int OP_UPGRADE_CONTRACT = 4; +const int OP_PARSE_PRICE_FEED_UPDATES = 5; +const int OP_PARSE_UNIQUE_PRICE_FEED_UPDATES = 6; + +;; Response op codes +const int OP_RESPONSE_SUCCESS = 0x10001; +const int OP_RESPONSE_ERROR = 0x10002; diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/common/storage.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/common/storage.fc new file mode 100644 index 0000000..5084596 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/common/storage.fc @@ -0,0 +1,96 @@ +#include "../imports/stdlib.fc"; + +;; Pyth +;; Price struct: {price: int, conf: int, expo: int, publish_time: int} +;; PriceFeed struct: {price: Price, ema_price: Price} +global cell latest_price_feeds; ;; Dictionary of PriceFeed structs, keyed by price_feed_id (256-bit) +global int single_update_fee; +;; DataSource struct: (emitter_chain_id: int, emitter_address: int) +;; emitter_chain_id is a 16-bit unsigned integer +;; emitter_address is a 256-bit unsigned integer +global cell is_valid_data_source; ;; Dictionary of int (0 as false, -1 as true), keyed by DataSource cell_hash +global int upgrade_code_hash; ;; 256-bit unsigned integer + + +;; Wormhole +global int current_guardian_set_index; +;; GuardianSet struct: {expiration_time: int, keys: cell} +;; The 'keys' cell is a dictionary with the following structure: +;; - Key: 8-bit unsigned integer (guardian index) +;; - Value: 160-bit unsigned integer (guardian address) +global cell guardian_sets; +global int chain_id; +global int governance_chain_id; +global int governance_contract; +global cell consumed_governance_actions; ;; Dictionary of int (0 as false, -1 as true), keyed by int (hash of the governance action) +global cell governance_data_source; ;; Single DataSource tuple +global int last_executed_governance_sequence; ;; u64 +global int governance_data_source_index; ;; u32 + + +() store_data() impure inline_ref { + cell price_feeds_cell = begin_cell() + .store_dict(latest_price_feeds) + .store_uint(single_update_fee, 256) + .end_cell(); + + cell data_sources_cell = begin_cell() + .store_dict(is_valid_data_source) + .end_cell(); + + cell guardian_set_cell = begin_cell() + .store_uint(current_guardian_set_index, 32) + .store_dict(guardian_sets) + .end_cell(); + + cell governance_cell = begin_cell() + .store_uint(chain_id, 16) + .store_uint(governance_chain_id, 16) + .store_uint(governance_contract, 256) + .store_dict(consumed_governance_actions) + .store_ref(governance_data_source) + .store_uint(last_executed_governance_sequence, 64) + .store_uint(governance_data_source_index, 32) + .store_uint(upgrade_code_hash, 256) + .end_cell(); + + begin_cell() + .store_ref(price_feeds_cell) + .store_ref(data_sources_cell) + .store_ref(guardian_set_cell) + .store_ref(governance_cell) + .end_cell() + .set_data(); +} + +;; load_data populates storage variables using stored data +() load_data() impure inline_ref { + slice ds = get_data().begin_parse(); + + cell price_feeds_cell = ds~load_ref(); + slice price_feeds_slice = price_feeds_cell.begin_parse(); + latest_price_feeds = price_feeds_slice~load_dict(); + single_update_fee = price_feeds_slice~load_uint(256); + + cell data_sources_cell = ds~load_ref(); + slice data_sources_slice = data_sources_cell.begin_parse(); + is_valid_data_source = data_sources_slice~load_dict(); + + cell guardian_set_cell = ds~load_ref(); + slice guardian_set_slice = guardian_set_cell.begin_parse(); + current_guardian_set_index = guardian_set_slice~load_uint(32); + guardian_sets = guardian_set_slice~load_dict(); + + cell governance_cell = ds~load_ref(); + slice governance_slice = governance_cell.begin_parse(); + chain_id = governance_slice~load_uint(16); + governance_chain_id = governance_slice~load_uint(16); + governance_contract = governance_slice~load_uint(256); + consumed_governance_actions = governance_slice~load_dict(); + governance_data_source = governance_slice~load_ref(); + last_executed_governance_sequence = governance_slice~load_uint(64); + governance_data_source_index = governance_slice~load_uint(32); + upgrade_code_hash = governance_slice~load_uint(256); + + ds.end_parse(); +} diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/common/utils.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/common/utils.fc new file mode 100644 index 0000000..22e20b6 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/common/utils.fc @@ -0,0 +1,105 @@ +#include "../imports/stdlib.fc"; + +;; Built-in assembly functions +int keccak256(slice s) asm "1 PUSHINT HASHEXT_KECCAK256"; ;; Keccak-256 hash function +int keccak256_tuple(tuple t) asm "DUP TLEN EXPLODEVAR HASHEXT_KECCAK256"; +int tlen(tuple t) asm "TLEN"; + +const MAX_BITS = 1016; + +int keccak256_int(int hash) inline { + slice hash_slice = begin_cell().store_uint(hash, 256).end_cell().begin_parse(); + return keccak256(hash_slice); +} + +int keccak256_slice(slice s) inline { + tuple slices = empty_tuple(); + int continue = true; + + while (continue) { + if (~ s.slice_empty?()) { + slice current_slice = s~load_bits(s.slice_bits()); + slices~tpush(current_slice); + + if (s.slice_refs_empty?()) { + continue = false; + } else { + s = s~load_ref().begin_parse(); + } + } else { + continue = false; + } + } + + return keccak256_tuple(slices); +} + +;; Splits a slice into chunks of MAX_BITS bits or less, in reverse order +(cell, slice) split_into_reverse_chunks(slice data, int size) { + cell chunks = null(); + int total_bits_loaded = 0; + builder current_chunk = begin_cell(); + while ((~ data.slice_empty?()) & (total_bits_loaded < size)) { + int bits_to_load = min(min(data.slice_bits(), MAX_BITS - current_chunk.builder_bits()), size - total_bits_loaded); + current_chunk = current_chunk.store_slice(data~load_bits(bits_to_load)); + total_bits_loaded += bits_to_load; + if ((current_chunk.builder_bits() == MAX_BITS) | (size - total_bits_loaded == 0)) { + slice current_chunk_slice = current_chunk.end_cell().begin_parse(); + if (cell_null?(chunks)) { + chunks = begin_cell().store_slice(current_chunk_slice).end_cell(); + } else { + chunks = begin_cell().store_slice(current_chunk_slice).store_ref(chunks).end_cell(); + } + current_chunk = begin_cell(); + } + if ((data.slice_bits() == 0) & (~ data.slice_refs_empty?())) { + data = data~load_ref().begin_parse(); + } + } + return (chunks, data); +} + +{- +This function reads a specified number of bits from the input slice and stores them in a cell structure, +handling data that may exceed the maximum cell capacity in FunC (1023 bits). + +Parameters: + - in_msg_body: The input slice containing the data to be read + - size: The number of bits to read from the input +Returns: + - A tuple containing: + 1. A cell containing the read data, potentially spanning multiple cells if the size exceeds 1016 bits + 2. A slice containing the remaining unread data from the input + +Note: + - The function uses a maximum of 1016 bits per cell (instead of 1023) to ensure byte alignment + - If the input data exceeds 1016 bits, it is split into multiple cells linked by references +-} +(cell, slice) read_and_store_large_data(slice in_msg_body, int size) { + (cell chunks, slice remaining) = split_into_reverse_chunks(in_msg_body, size); + cell last_cell = null(); + while (~ cell_null?(chunks)) { + slice chunk = chunks.begin_parse(); + builder cb = begin_cell().store_slice(chunk~load_bits(chunk.slice_bits())); + if (~ cell_null?(last_cell)) { + cb = cb.store_ref(last_cell); + } + last_cell = cb.end_cell(); + if (chunk.slice_refs_empty?()) { + chunks = null(); + } else { + chunks = chunk~load_ref(); + } + } + + return (last_cell, remaining); +} + +(int) pubkey_to_eth_address(int x1, int x2) { + slice pubkey = begin_cell() + .store_uint(x1, 256) + .store_uint(x2, 256) + .end_cell() + .begin_parse(); + return keccak256(pubkey) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; +} diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/imports/stdlib.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/imports/stdlib.fc new file mode 100644 index 0000000..4e72d93 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/imports/stdlib.fc @@ -0,0 +1,632 @@ +;; Standard library for funC +;; + +{- + # Tuple manipulation primitives + The names and the types are mostly self-explaining. + See [polymorhism with forall](https://ton.org/docs/#/func/functions?id=polymorphism-with-forall) + for more info on the polymorphic functions. + + Note that currently values of atomic type `tuple` can't be cast to composite tuple type (e.g. `[int, cell]`) + and vise versa. +-} + +{- + # Lisp-style lists + + Lists can be represented as nested 2-elements tuples. + Empty list is conventionally represented as TVM `null` value (it can be obtained by calling [null()]). + For example, tuple `(1, (2, (3, null)))` represents list `[1, 2, 3]`. Elements of a list can be of different types. +-} + +;;; Adds an element to the beginning of lisp-style list. +forall X -> tuple cons(X head, tuple tail) asm "CONS"; + +;;; Extracts the head and the tail of lisp-style list. +forall X -> (X, tuple) uncons(tuple list) asm "UNCONS"; + +;;; Extracts the tail and the head of lisp-style list. +forall X -> (tuple, X) list_next(tuple list) asm( -> 1 0) "UNCONS"; + +;;; Returns the head of lisp-style list. +forall X -> X car(tuple list) asm "CAR"; + +;;; Returns the tail of lisp-style list. +tuple cdr(tuple list) asm "CDR"; + +;;; Creates tuple with zero elements. +tuple empty_tuple() asm "NIL"; + +;;; Appends a value `x` to a `Tuple t = (x1, ..., xn)`, but only if the resulting `Tuple t' = (x1, ..., xn, x)` +;;; is of length at most 255. Otherwise throws a type check exception. +forall X -> tuple tpush(tuple t, X value) asm "TPUSH"; +forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH"; + +;;; Creates a tuple of length one with given argument as element. +forall X -> [X] single(X x) asm "SINGLE"; + +;;; Unpacks a tuple of length one +forall X -> X unsingle([X] t) asm "UNSINGLE"; + +;;; Creates a tuple of length two with given arguments as elements. +forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR"; + +;;; Unpacks a tuple of length two +forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR"; + +;;; Creates a tuple of length three with given arguments as elements. +forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE"; + +;;; Unpacks a tuple of length three +forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE"; + +;;; Creates a tuple of length four with given arguments as elements. +forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE"; + +;;; Unpacks a tuple of length four +forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE"; + +;;; Returns the first element of a tuple (with unknown element types). +forall X -> X first(tuple t) asm "FIRST"; + +;;; Returns the second element of a tuple (with unknown element types). +forall X -> X second(tuple t) asm "SECOND"; + +;;; Returns the third element of a tuple (with unknown element types). +forall X -> X third(tuple t) asm "THIRD"; + +;;; Returns the fourth element of a tuple (with unknown element types). +forall X -> X fourth(tuple t) asm "3 INDEX"; + +;;; Returns the first element of a pair tuple. +forall X, Y -> X pair_first([X, Y] p) asm "FIRST"; + +;;; Returns the second element of a pair tuple. +forall X, Y -> Y pair_second([X, Y] p) asm "SECOND"; + +;;; Returns the first element of a triple tuple. +forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST"; + +;;; Returns the second element of a triple tuple. +forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND"; + +;;; Returns the third element of a triple tuple. +forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD"; + + +;;; Push null element (casted to given type) +;;; By the TVM type `Null` FunC represents absence of a value of some atomic type. +;;; So `null` can actually have any atomic type. +forall X -> X null() asm "PUSHNULL"; + +;;; Moves a variable [x] to the top of the stack +forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP"; + + + +;;; Returns the current Unix time as an Integer +int now() asm "NOW"; + +;;; Returns the internal address of the current smart contract as a Slice with a `MsgAddressInt`. +;;; If necessary, it can be parsed further using primitives such as [parse_std_addr]. +slice my_address() asm "MYADDR"; + +;;; Returns the balance of the smart contract as a tuple consisting of an int +;;; (balance in nanotoncoins) and a `cell` +;;; (a dictionary with 32-bit keys representing the balance of "extra currencies") +;;; at the start of Computation Phase. +;;; Note that RAW primitives such as [send_raw_message] do not update this field. +[int, cell] get_balance() asm "BALANCE"; + +;;; Returns the logical time of the current transaction. +int cur_lt() asm "LTIME"; + +;;; Returns the starting logical time of the current block. +int block_lt() asm "BLOCKLT"; + +;;; Computes the representation hash of a `cell` [c] and returns it as a 256-bit unsigned integer `x`. +;;; Useful for signing and checking signatures of arbitrary entities represented by a tree of cells. +int cell_hash(cell c) asm "HASHCU"; + +;;; Computes the hash of a `slice s` and returns it as a 256-bit unsigned integer `x`. +;;; The result is the same as if an ordinary cell containing only data and references from `s` had been created +;;; and its hash computed by [cell_hash]. +int slice_hash(slice s) asm "HASHSU"; + +;;; Computes sha256 of the data bits of `slice` [s]. If the bit length of `s` is not divisible by eight, +;;; throws a cell underflow exception. The hash value is returned as a 256-bit unsigned integer `x`. +int string_hash(slice s) asm "SHA256U"; + +{- + # Signature checks +-} + +;;; Checks the Ed25519-`signature` of a `hash` (a 256-bit unsigned integer, usually computed as the hash of some data) +;;; using [public_key] (also represented by a 256-bit unsigned integer). +;;; The signature must contain at least 512 data bits; only the first 512 bits are used. +;;; The result is `−1` if the signature is valid, `0` otherwise. +;;; Note that `CHKSIGNU` creates a 256-bit slice with the hash and calls `CHKSIGNS`. +;;; That is, if [hash] is computed as the hash of some data, these data are hashed twice, +;;; the second hashing occurring inside `CHKSIGNS`. +int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU"; + +;;; Checks whether [signature] is a valid Ed25519-signature of the data portion of `slice data` using `public_key`, +;;; similarly to [check_signature]. +;;; If the bit length of [data] is not divisible by eight, throws a cell underflow exception. +;;; The verification of Ed25519 signatures is the standard one, +;;; with sha256 used to reduce [data] to the 256-bit number that is actually signed. +int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS"; + +{--- + # Computation of boc size + The primitives below may be useful for computing storage fees of user-provided data. +-} + +;;; Returns `(x, y, z, -1)` or `(null, null, null, 0)`. +;;; Recursively computes the count of distinct cells `x`, data bits `y`, and cell references `z` +;;; in the DAG rooted at `cell` [c], effectively returning the total storage used by this DAG taking into account +;;; the identification of equal cells. +;;; The values of `x`, `y`, and `z` are computed by a depth-first traversal of this DAG, +;;; with a hash table of visited cell hashes used to prevent visits of already-visited cells. +;;; The total count of visited cells `x` cannot exceed non-negative [max_cells]; +;;; otherwise the computation is aborted before visiting the `(max_cells + 1)`-st cell and +;;; a zero flag is returned to indicate failure. If [c] is `null`, returns `x = y = z = 0`. +(int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE"; + +;;; Similar to [compute_data_size?], but accepting a `slice` [s] instead of a `cell`. +;;; The returned value of `x` does not take into account the cell that contains the `slice` [s] itself; +;;; however, the data bits and the cell references of [s] are accounted for in `y` and `z`. +(int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE"; + +;;; A non-quiet version of [compute_data_size?] that throws a cell overflow exception (`8`) on failure. +(int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; + +;;; A non-quiet version of [slice_compute_data_size?] that throws a cell overflow exception (8) on failure. +(int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; + +;;; Throws an exception with exit_code excno if cond is not 0 (commented since implemented in compilator) +;; () throw_if(int excno, int cond) impure asm "THROWARGIF"; + +{-- + # Debug primitives + Only works for local TVM execution with debug level verbosity +-} +;;; Dumps the stack (at most the top 255 values) and shows the total stack depth. +() dump_stack() impure asm "DUMPSTK"; + +{- + # Persistent storage save and load +-} + +;;; Returns the persistent contract storage cell. It can be parsed or modified with slice and builder primitives later. +cell get_data() asm "c4 PUSH"; + +;;; Sets `cell` [c] as persistent contract data. You can update persistent contract storage with this primitive. +() set_data(cell c) impure asm "c4 POP"; + +{- + # Continuation primitives +-} +;;; Usually `c3` has a continuation initialized by the whole code of the contract. It is used for function calls. +;;; The primitive returns the current value of `c3`. +cont get_c3() impure asm "c3 PUSH"; + +;;; Updates the current value of `c3`. Usually, it is used for updating smart contract code in run-time. +;;; Note that after execution of this primitive the current code +;;; (and the stack of recursive function calls) won't change, +;;; but any other function call will use a function from the new code. +() set_c3(cont c) impure asm "c3 POP"; + +;;; Transforms a `slice` [s] into a simple ordinary continuation `c`, with `c.code = s` and an empty stack and savelist. +cont bless(slice s) impure asm "BLESS"; + +{--- + # Gas related primitives +-} + +;;; Sets current gas limit `gl` to its maximal allowed value `gm`, and resets the gas credit `gc` to zero, +;;; decreasing the value of `gr` by `gc` in the process. +;;; In other words, the current smart contract agrees to buy some gas to finish the current transaction. +;;; This action is required to process external messages, which bring no value (hence no gas) with themselves. +;;; +;;; For more details check [accept_message effects](https://ton.org/docs/#/smart-contracts/accept). +() accept_message() impure asm "ACCEPT"; + +;;; Sets current gas limit `gl` to the minimum of limit and `gm`, and resets the gas credit `gc` to zero. +;;; If the gas consumed so far (including the present instruction) exceeds the resulting value of `gl`, +;;; an (unhandled) out of gas exception is thrown before setting new gas limits. +;;; Notice that [set_gas_limit] with an argument `limit ≥ 2^63 − 1` is equivalent to [accept_message]. +() set_gas_limit(int limit) impure asm "SETGASLIMIT"; + +;;; Commits the current state of registers `c4` (“persistent data”) and `c5` (“actions”) +;;; so that the current execution is considered “successful” with the saved values even if an exception +;;; in Computation Phase is thrown later. +() commit() impure asm "COMMIT"; + +;;; Not implemented +;;() buy_gas(int gram) impure asm "BUYGAS"; + +;;; Computes the amount of gas that can be bought for `amount` nanoTONs, +;;; and sets `gl` accordingly in the same way as [set_gas_limit]. +() buy_gas(int amount) impure asm "BUYGAS"; + +;;; Computes the minimum of two integers [x] and [y]. +int min(int x, int y) asm "MIN"; + +;;; Computes the maximum of two integers [x] and [y]. +int max(int x, int y) asm "MAX"; + +;;; Sorts two integers. +(int, int) minmax(int x, int y) asm "MINMAX"; + +;;; Computes the absolute value of an integer [x]. +int abs(int x) asm "ABS"; + +{- + # Slice primitives + + It is said that a primitive _loads_ some data, + if it returns the data and the remainder of the slice + (so it can also be used as [modifying method](https://ton.org/docs/#/func/statements?id=modifying-methods)). + + It is said that a primitive _preloads_ some data, if it returns only the data + (it can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods)). + + Unless otherwise stated, loading and preloading primitives read the data from a prefix of the slice. +-} + + +;;; Converts a `cell` [c] into a `slice`. Notice that [c] must be either an ordinary cell, +;;; or an exotic cell (see [TVM.pdf](https://ton-blockchain.github.io/docs/tvm.pdf), 3.1.2) +;;; which is automatically loaded to yield an ordinary cell `c'`, converted into a `slice` afterwards. +slice begin_parse(cell c) asm "CTOS"; + +;;; Checks if [s] is empty. If not, throws an exception. +() end_parse(slice s) impure asm "ENDS"; + +;;; Loads the first reference from the slice. +(slice, cell) load_ref(slice s) asm( -> 1 0) "LDREF"; + +;;; Preloads the first reference from the slice. +cell preload_ref(slice s) asm "PLDREF"; + +{- Functions below are commented because are implemented on compilator level for optimisation -} + +;;; Loads a signed [len]-bit integer from a slice [s]. +;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX"; + +;;; Loads an unsigned [len]-bit integer from a slice [s]. +;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX"; + +;;; Preloads a signed [len]-bit integer from a slice [s]. +;; int preload_int(slice s, int len) asm "PLDIX"; + +;;; Preloads an unsigned [len]-bit integer from a slice [s]. +;; int preload_uint(slice s, int len) asm "PLDUX"; + +;;; Loads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`. +;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX"; + +;;; Preloads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`. +;; slice preload_bits(slice s, int len) asm "PLDSLICEX"; + +;;; Loads serialized amount of TonCoins (any unsigned integer up to `2^128 - 1`). +(slice, int) load_grams(slice s) asm( -> 1 0) "LDGRAMS"; +(slice, int) load_coins(slice s) asm( -> 1 0) "LDGRAMS"; + +;;; Returns all but the first `0 ≤ len ≤ 1023` bits of `slice` [s]. +slice skip_bits(slice s, int len) asm "SDSKIPFIRST"; +(slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST"; + +;;; Returns the first `0 ≤ len ≤ 1023` bits of `slice` [s]. +slice first_bits(slice s, int len) asm "SDCUTFIRST"; + +;;; Returns all but the last `0 ≤ len ≤ 1023` bits of `slice` [s]. +slice skip_last_bits(slice s, int len) asm "SDSKIPLAST"; +(slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST"; + +;;; Returns the last `0 ≤ len ≤ 1023` bits of `slice` [s]. +slice slice_last(slice s, int len) asm "SDCUTLAST"; + +;;; Loads a dictionary `D` (HashMapE) from `slice` [s]. +;;; (returns `null` if `nothing` constructor is used). +(slice, cell) load_dict(slice s) asm( -> 1 0) "LDDICT"; + +;;; Preloads a dictionary `D` from `slice` [s]. +cell preload_dict(slice s) asm "PLDDICT"; + +;;; Loads a dictionary as [load_dict], but returns only the remainder of the slice. +slice skip_dict(slice s) asm "SKIPDICT"; + +;;; Loads (Maybe ^Cell) from `slice` [s]. +;;; In other words loads 1 bit and if it is true +;;; loads first ref and return it with slice remainder +;;; otherwise returns `null` and slice remainder +(slice, cell) load_maybe_ref(slice s) asm( -> 1 0) "LDOPTREF"; + +;;; Preloads (Maybe ^Cell) from `slice` [s]. +cell preload_maybe_ref(slice s) asm "PLDOPTREF"; + + +;;; Returns the depth of `cell` [c]. +;;; If [c] has no references, then return `0`; +;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [c]. +;;; If [c] is a `null` instead of a cell, returns zero. +int cell_depth(cell c) asm "CDEPTH"; + + +{- + # Slice size primitives +-} + +;;; Returns the number of references in `slice` [s]. +int slice_refs(slice s) asm "SREFS"; + +;;; Returns the number of data bits in `slice` [s]. +int slice_bits(slice s) asm "SBITS"; + +;;; Returns both the number of data bits and the number of references in `slice` [s]. +(int, int) slice_bits_refs(slice s) asm "SBITREFS"; + +;;; Checks whether a `slice` [s] is empty (i.e., contains no bits of data and no cell references). +int slice_empty?(slice s) asm "SEMPTY"; + +;;; Checks whether `slice` [s] has no bits of data. +int slice_data_empty?(slice s) asm "SDEMPTY"; + +;;; Checks whether `slice` [s] has no references. +int slice_refs_empty?(slice s) asm "SREMPTY"; + +;;; Returns the depth of `slice` [s]. +;;; If [s] has no references, then returns `0`; +;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [s]. +int slice_depth(slice s) asm "SDEPTH"; + +{- + # Builder size primitives +-} + +;;; Returns the number of cell references already stored in `builder` [b] +int builder_refs(builder b) asm "BREFS"; + +;;; Returns the number of data bits already stored in `builder` [b]. +int builder_bits(builder b) asm "BBITS"; + +;;; Returns the depth of `builder` [b]. +;;; If no cell references are stored in [b], then returns 0; +;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [b]. +int builder_depth(builder b) asm "BDEPTH"; + +{- + # Builder primitives + It is said that a primitive _stores_ a value `x` into a builder `b` + if it returns a modified version of the builder `b'` with the value `x` stored at the end of it. + It can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods). + + All the primitives below first check whether there is enough space in the `builder`, + and only then check the range of the value being serialized. +-} + +;;; Creates a new empty `builder`. +builder begin_cell() asm "NEWC"; + +;;; Converts a `builder` into an ordinary `cell`. +cell end_cell(builder b) asm "ENDC"; + +;;; Stores a reference to `cell` [c] into `builder` [b]. +builder store_ref(builder b, cell c) asm(c b) "STREF"; + +;;; Stores an unsigned [len]-bit integer `x` into `b` for `0 ≤ len ≤ 256`. +;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX"; + +;;; Stores a signed [len]-bit integer `x` into `b` for` 0 ≤ len ≤ 257`. +;; builder store_int(builder b, int x, int len) asm(x b len) "STIX"; + + +;;; Stores `slice` [s] into `builder` [b] +builder store_slice(builder b, slice s) asm "STSLICER"; + +;;; Stores (serializes) an integer [x] in the range `0..2^128 − 1` into `builder` [b]. +;;; The serialization of [x] consists of a 4-bit unsigned big-endian integer `l`, +;;; which is the smallest integer `l ≥ 0`, such that `x < 2^8l`, +;;; followed by an `8l`-bit unsigned big-endian representation of [x]. +;;; If [x] does not belong to the supported range, a range check exception is thrown. +;;; +;;; Store amounts of TonCoins to the builder as VarUInteger 16 +builder store_grams(builder b, int x) asm "STGRAMS"; +builder store_coins(builder b, int x) asm "STGRAMS"; + +;;; Stores dictionary `D` represented by `cell` [c] or `null` into `builder` [b]. +;;; In other words, stores a `1`-bit and a reference to [c] if [c] is not `null` and `0`-bit otherwise. +builder store_dict(builder b, cell c) asm(c b) "STDICT"; + +;;; Stores (Maybe ^Cell) to builder: +;;; if cell is null store 1 zero bit +;;; otherwise store 1 true bit and ref to cell +builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF"; + + +{- + # Address manipulation primitives + The address manipulation primitives listed below serialize and deserialize values according to the following TL-B scheme: + ```TL-B + addr_none$00 = MsgAddressExt; + addr_extern$01 len:(## 8) external_address:(bits len) + = MsgAddressExt; + anycast_info$_ depth:(#<= 30) { depth >= 1 } + rewrite_pfx:(bits depth) = Anycast; + addr_std$10 anycast:(Maybe Anycast) + workchain_id:int8 address:bits256 = MsgAddressInt; + addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9) + workchain_id:int32 address:(bits addr_len) = MsgAddressInt; + _ _:MsgAddressInt = MsgAddress; + _ _:MsgAddressExt = MsgAddress; + + int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool + src:MsgAddress dest:MsgAddressInt + value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams + created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; + ext_out_msg_info$11 src:MsgAddress dest:MsgAddressExt + created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; + ``` + A deserialized `MsgAddress` is represented by a tuple `t` as follows: + + - `addr_none` is represented by `t = (0)`, + i.e., a tuple containing exactly one integer equal to zero. + - `addr_extern` is represented by `t = (1, s)`, + where slice `s` contains the field `external_address`. In other words, ` + t` is a pair (a tuple consisting of two entries), containing an integer equal to one and slice `s`. + - `addr_std` is represented by `t = (2, u, x, s)`, + where `u` is either a `null` (if `anycast` is absent) or a slice `s'` containing `rewrite_pfx` (if anycast is present). + Next, integer `x` is the `workchain_id`, and slice `s` contains the address. + - `addr_var` is represented by `t = (3, u, x, s)`, + where `u`, `x`, and `s` have the same meaning as for `addr_std`. +-} + +;;; Loads from slice [s] the only prefix that is a valid `MsgAddress`, +;;; and returns both this prefix `s'` and the remainder `s''` of [s] as slices. +(slice, slice) load_msg_addr(slice s) asm( -> 1 0) "LDMSGADDR"; + +;;; Decomposes slice [s] containing a valid `MsgAddress` into a `tuple t` with separate fields of this `MsgAddress`. +;;; If [s] is not a valid `MsgAddress`, a cell deserialization exception is thrown. +tuple parse_addr(slice s) asm "PARSEMSGADDR"; + +;;; Parses slice [s] containing a valid `MsgAddressInt` (usually a `msg_addr_std`), +;;; applies rewriting from the anycast (if present) to the same-length prefix of the address, +;;; and returns both the workchain and the 256-bit address as integers. +;;; If the address is not 256-bit, or if [s] is not a valid serialization of `MsgAddressInt`, +;;; throws a cell deserialization exception. +(int, int) parse_std_addr(slice s) asm "REWRITESTDADDR"; + +;;; A variant of [parse_std_addr] that returns the (rewritten) address as a slice [s], +;;; even if it is not exactly 256 bit long (represented by a `msg_addr_var`). +(int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR"; + +{- + # Dictionary primitives +-} + + +;;; Sets the value associated with [key_len]-bit key signed index in dictionary [dict] to [value] (cell), +;;; and returns the resulting dictionary. +cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; +(cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; + +;;; Sets the value associated with [key_len]-bit key unsigned index in dictionary [dict] to [value] (cell), +;;; and returns the resulting dictionary. +cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; +(cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; + +cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF"; +(cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF" "NULLSWAPIFNOT"; +(cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF" "NULLSWAPIFNOT"; +(cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF"; +(cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF"; +(cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL"; +(cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL"; +(slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT"; +(slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT"; +(cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; +(cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; +(cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; +(cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; +cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; +(cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; +cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; +(cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; +cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; +(cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; +(cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD"; +(cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE"; +(cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD"; +(cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE"; +cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; +(cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; +cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; +(cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; +cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; +(cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; +(cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB"; +(cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB"; +(cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB"; +(cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB"; +(cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; +(cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; +(cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; +(cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; +(cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; +(cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; +(cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; +(cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; +(cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; +(cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; +(cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; +(cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; +(int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2"; +(int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2"; +(int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2"; +(int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2"; +(int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2"; +(int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2"; +(int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2"; +(int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2"; +(int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2"; +(int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2"; +(int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2"; +(int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2"; +(int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2"; +(int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2"; +(int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2"; +(int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2"; + +;;; Creates an empty dictionary, which is actually a null value. Equivalent to PUSHNULL +cell new_dict() asm "NEWDICT"; +;;; Checks whether a dictionary is empty. Equivalent to cell_null?. +int dict_empty?(cell c) asm "DICTEMPTY"; + + +{- Prefix dictionary primitives -} +(slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2"; +(cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET"; +(cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL"; + +;;; Returns the value of the global configuration parameter with integer index `i` as a `cell` or `null` value. +cell config_param(int x) asm "CONFIGOPTPARAM"; +;;; Checks whether c is a null. Note, that FunC also has polymorphic null? built-in. +int cell_null?(cell c) asm "ISNULL"; + +;;; Creates an output action which would reserve exactly amount nanotoncoins (if mode = 0), at most amount nanotoncoins (if mode = 2), or all but amount nanotoncoins (if mode = 1 or mode = 3), from the remaining balance of the account. It is roughly equivalent to creating an outbound message carrying amount nanotoncoins (or b − amount nanotoncoins, where b is the remaining balance) to oneself, so that the subsequent output actions would not be able to spend more money than the remainder. Bit +2 in mode means that the external action does not fail if the specified amount cannot be reserved; instead, all remaining balance is reserved. Bit +8 in mode means `amount <- -amount` before performing any further actions. Bit +4 in mode means that amount is increased by the original balance of the current account (before the compute phase), including all extra currencies, before performing any other checks and actions. Currently, amount must be a non-negative integer, and mode must be in the range 0..15. +() raw_reserve(int amount, int mode) impure asm "RAWRESERVE"; +;;; Similar to raw_reserve, but also accepts a dictionary extra_amount (represented by a cell or null) with extra currencies. In this way currencies other than TonCoin can be reserved. +() raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX"; +;;; Sends a raw message contained in msg, which should contain a correctly serialized object Message X, with the only exception that the source address is allowed to have dummy value addr_none (to be automatically replaced with the current smart contract address), and ihr_fee, fwd_fee, created_lt and created_at fields can have arbitrary values (to be rewritten with correct values during the action phase of the current transaction). Integer parameter mode contains the flags. Currently mode = 0 is used for ordinary messages; mode = 128 is used for messages that are to carry all the remaining balance of the current smart contract (instead of the value originally indicated in the message); mode = 64 is used for messages that carry all the remaining value of the inbound message in addition to the value initially indicated in the new message (if bit 0 is not set, the gas fees are deducted from this amount); mode' = mode + 1 means that the sender wants to pay transfer fees separately; mode' = mode + 2 means that any errors arising while processing this message during the action phase should be ignored. Finally, mode' = mode + 32 means that the current account must be destroyed if its resulting balance is zero. This flag is usually employed together with +128. +() send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG"; +;;; Creates an output action that would change this smart contract code to that given by cell new_code. Notice that this change will take effect only after the successful termination of the current run of the smart contract +() set_code(cell new_code) impure asm "SETCODE"; + +;;; Generates a new pseudo-random unsigned 256-bit integer x. The algorithm is as follows: if r is the old value of the random seed, considered as a 32-byte array (by constructing the big-endian representation of an unsigned 256-bit integer), then its sha512(r) is computed; the first 32 bytes of this hash are stored as the new value r' of the random seed, and the remaining 32 bytes are returned as the next random value x. +int random() impure asm "RANDU256"; +;;; Generates a new pseudo-random integer z in the range 0..range−1 (or range..−1, if range < 0). More precisely, an unsigned random value x is generated as in random; then z := x * range / 2^256 is computed. +int rand(int range) impure asm "RAND"; +;;; Returns the current random seed as an unsigned 256-bit Integer. +int get_seed() impure asm "RANDSEED"; +;;; Sets the random seed to unsigned 256-bit seed. +() set_seed(int x) impure asm "SETRAND"; +;;; Mixes unsigned 256-bit integer x into the random seed r by setting the random seed to sha256 of the concatenation of two 32-byte strings: the first with the big-endian representation of the old seed r, and the second with the big-endian representation of x. +() randomize(int x) impure asm "ADDRAND"; +;;; Equivalent to randomize(cur_lt());. +() randomize_lt() impure asm "LTIME" "ADDRAND"; + +;;; Checks whether the data parts of two slices coinside +int equal_slice_bits(slice a, slice b) asm "SDEQ"; +int equal_slices(slice a, slice b) asm "SDEQ"; + +;;; Concatenates two builders +builder store_builder(builder to, builder from) asm "STBR"; + +int gas_consumed() asm "GASCONSUMED"; +int get_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEE"; +;;; Checks whether the data parts of two slices coinside +int equal_slices_bits(slice a, slice b) asm "SDEQ"; + +() dump_tuple_index3 (slice a, slice b, slice c) impure asm "dump_tuple_index3"; \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/tests/PythNoCheck.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/tests/PythNoCheck.fc new file mode 100644 index 0000000..062d140 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/tests/PythNoCheck.fc @@ -0,0 +1,610 @@ +{- + This file aims to provide support for usage of locally generated prices without merkle-tree structure. + It allows to generate test prices and run local tests without using hermes client. + Any price feed ids can be used. + Authenticity check has been removed, all other checks and logic are preserved. +-} +#include "../imports/stdlib.fc"; +#include "../common/errors.fc"; +#include "../common/storage.fc"; +#include "../common/utils.fc"; +#include "../common/constants.fc"; +#include "../common/merkle_tree.fc"; +#include "../common/governance_actions.fc"; +#include "../common/gas.fc"; +#include "../common/op.fc"; +#include "../common/error_handling.fc"; +#include "../Wormhole.fc"; + +cell store_price(int price, int conf, int expo, int publish_time) { + return begin_cell() + .store_int(price, 64) + .store_uint(conf, 64) + .store_int(expo, 32) + .store_uint(publish_time, 64) + .end_cell(); +} + +slice read_and_verify_header(slice data) impure { + int magic = data~load_uint(32); + throw_unless(ERROR_INVALID_MAGIC, magic == ACCUMULATOR_MAGIC); + int major_version = data~load_uint(8); + throw_unless(ERROR_INVALID_MAJOR_VERSION, major_version == MAJOR_VERSION); + int minor_version = data~load_uint(8); + throw_if(ERROR_INVALID_MINOR_VERSION, minor_version < MINIMUM_ALLOWED_MINOR_VERSION); + int trailing_header_size = data~load_uint(8); + ;; skip trailing headers + data~skip_bits(trailing_header_size * 8); + int update_type = data~load_uint(8); + throw_unless(ERROR_INVALID_UPDATE_DATA_TYPE, update_type == WORMHOLE_MERKLE_UPDATE_TYPE); + return data; +} + +;; reads generated price cell without authenticity checks +(int, int, int, int, int, int, int, int, cell) read_message_no_verify(slice message) impure { + int price_id = message~load_uint(256); + int price = message~load_int(64); + int conf = message~load_uint(64); + int expo = message~load_int(32); + int publish_time = message~load_uint(64); + int prev_publish_time = message~load_uint(64); + int ema_price = message~load_int(64); + int ema_conf = message~load_uint(64); + cell next = message.slice_refs_empty?() ? null() : message~load_ref(); + message.end_parse(); + + return (price_id, price, conf, expo, publish_time, prev_publish_time, ema_price, ema_conf, next); +} + +(int, int, int, int) parse_price(slice price_feed) { + int price = price_feed~load_int(64); + int conf = price_feed~load_uint(64); + int expo = price_feed~load_int(32); + int publish_time = price_feed~load_uint(64); + return (price, conf, expo, publish_time); +} + +int get_update_fee(slice data) method_id { + load_data(); + slice cs = read_and_verify_header(data); + int wormhole_proof_size_bytes = cs~load_uint(16); + (cell wormhole_proof, slice cs) = read_and_store_large_data(cs, wormhole_proof_size_bytes * 8); + int num_updates = cs~load_uint(8); + return single_update_fee * num_updates; +} + +int get_single_update_fee() method_id { + load_data(); + return single_update_fee; +} + +int get_governance_data_source_index() method_id { + load_data(); + return governance_data_source_index; +} + +cell get_governance_data_source() method_id { + load_data(); + return governance_data_source; +} + +int get_last_executed_governance_sequence() method_id { + load_data(); + return last_executed_governance_sequence; +} + +int get_is_valid_data_source(cell data_source) method_id { + load_data(); + int data_source_key = cell_hash(data_source); + (slice value, int found?) = is_valid_data_source.udict_get?(256, data_source_key); + if (found?) { + return value~load_int(1); + } else { + return 0; + } +} + +(int, int, int, int) get_price_unsafe(int price_feed_id) method_id { + load_data(); + (slice result, int success) = latest_price_feeds.udict_get?(256, price_feed_id); + throw_unless(ERROR_PRICE_FEED_NOT_FOUND, success); + slice price_feed = result~load_ref().begin_parse(); + slice price = price_feed~load_ref().begin_parse(); + return parse_price(price); +} + +(int, int, int, int) get_price_no_older_than(int time_period, int price_feed_id) method_id { + load_data(); + (int price, int conf, int expo, int publish_time) = get_price_unsafe(price_feed_id); + int current_time = now(); + throw_if(ERROR_OUTDATED_PRICE, max(0, current_time - publish_time) > time_period); + return (price, conf, expo, publish_time); +} + +(int, int, int, int) get_ema_price_unsafe(int price_feed_id) method_id { + load_data(); + (slice result, int success) = latest_price_feeds.udict_get?(256, price_feed_id); + throw_unless(ERROR_PRICE_FEED_NOT_FOUND, success); + slice price_feed = result~load_ref().begin_parse(); + slice price = price_feed~load_ref().begin_parse(); + slice ema_price = price_feed~load_ref().begin_parse(); + return parse_price(ema_price); +} + +(int, int, int, int) get_ema_price_no_older_than(int time_period, int price_feed_id) method_id { + load_data(); + (int price, int conf, int expo, int publish_time) = get_ema_price_unsafe(price_feed_id); + int current_time = now(); + throw_if(ERROR_OUTDATED_PRICE, max(0, current_time - publish_time) > time_period); + return (price, conf, expo, publish_time); +} + +(int, int) parse_data_source(cell data_source) { + slice ds = data_source.begin_parse(); + int emitter_chain = ds~load_uint(16); + int emitter_address = ds~load_uint(256); + return (emitter_chain, emitter_address); +} + +int parse_pyth_payload_in_wormhole_vm(slice payload) impure { + int accumulator_wormhole_magic = payload~load_uint(32); + throw_unless(ERROR_INVALID_MAGIC, accumulator_wormhole_magic == ACCUMULATOR_WORMHOLE_MAGIC); + + int update_type = payload~load_uint(8); + throw_unless(ERROR_INVALID_UPDATE_DATA_TYPE, update_type == WORMHOLE_MERKLE_UPDATE_TYPE); + + payload~load_uint(64); ;; Skip slot + payload~load_uint(32); ;; Skip ring_size + + return payload~load_uint(160); ;; Return root_digest +} + +() calculate_and_validate_fees(int msg_value, int num_updates) impure { + int update_fee = single_update_fee * num_updates; + int compute_fee = get_compute_fee( + WORKCHAIN, + UPDATE_PRICE_FEEDS_BASE_GAS + (UPDATE_PRICE_FEEDS_PER_UPDATE_GAS * num_updates) + ); + throw_unless(ERROR_INSUFFICIENT_GAS, msg_value >= compute_fee); + int remaining_msg_value = msg_value - compute_fee; + + ;; Check if the sender has sent enough TON to cover the update_fee + throw_unless(ERROR_INSUFFICIENT_FEE, remaining_msg_value >= update_fee); +} + +(int) find_price_id_index(tuple price_ids, int price_id) { + int len = price_ids.tlen(); + int i = 0; + while (i < len) { + if (price_ids.at(i) == price_id) { + return i; + } + i += 1; + } + return -1; ;; Not found +} + +;; parse price feeds as raw prices, no merkle-trie structure +tuple parse_price_feeds_from_data_no_verify(int msg_value, slice cs, tuple price_ids, int min_publish_time, int max_publish_time, int unique) { + int num_updates = cs~load_uint(8); + + calculate_and_validate_fees(msg_value, num_updates); + + ;; Create dictionary to store price feeds in order (dict has a udict_get_next? method which returns the next key in order) + cell ordered_feeds = new_dict(); + ;; Track which price IDs we've found + cell found_price_ids = new_dict(); + + int index = 0; + + repeat(num_updates) { + throw_if(ERROR_INVALID_UPDATE_DATA_LENGTH, null?(cs)); + (int price_id, int price, int conf, int expo, int publish_time, int prev_publish_time, int ema_price, int ema_conf, cell next) = read_message_no_verify(cs); + cs = null?(next) ? null() : next.begin_parse(); + + int price_ids_len = price_ids.tlen(); + + ;; Check if we've already processed this price_id to avoid duplicates + (_, int already_processed?) = found_price_ids.udict_get?(256, price_id); + if (~ already_processed?) { ;; Only process if we haven't seen this price_id yet + int should_include = (price_ids_len == 0) + | ((price_ids_len > 0) + & (publish_time >= min_publish_time) + & (publish_time <= max_publish_time) + & ((unique == 0) | (min_publish_time > prev_publish_time))); + + if (should_include) { + ;; Create price feed cell containing both current and EMA prices + cell price_feed_cell = begin_cell() + .store_ref(store_price(price, conf, expo, publish_time)) + .store_ref(store_price(ema_price, ema_conf, expo, publish_time)) + .end_cell(); + + if (price_ids_len == 0) { + ordered_feeds~udict_set(8, index, begin_cell() + .store_uint(price_id, 256) + .store_ref(price_feed_cell) + .end_cell().begin_parse()); + index += 1; + } else { + index = find_price_id_index(price_ids, price_id); + if (index >= 0) { + ordered_feeds~udict_set(8, index, begin_cell() + .store_uint(price_id, 256) + .store_ref(price_feed_cell) + .end_cell().begin_parse()); + } + } + + ;; Mark this price ID as found + found_price_ids~udict_set(256, price_id, begin_cell().store_int(true, 1).end_cell().begin_parse()); + } + } + } + + ;; Verify all requested price IDs were found + if (price_ids.tlen() > 0) { + int i = 0; + repeat(price_ids.tlen()) { + int requested_id = price_ids.at(i); + (_, int found?) = found_price_ids.udict_get?(256, requested_id); + throw_unless(ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE, found?); + i += 1; + } + } + + ;; Create final ordered tuple from dictionary + tuple price_feeds = empty_tuple(); + int index = -1; + do { + (index, slice value, int success) = ordered_feeds.udict_get_next?(8, index); + if (success) { + tuple price_feed = empty_tuple(); + price_feed~tpush(value~load_uint(256)); ;; price_id + price_feed~tpush(value~load_ref()); ;; price_feed_cell + price_feeds~tpush(price_feed); + } + } until (~ success); + + return price_feeds; +} + +;; Creates a chain of cells from price feeds, with each cell containing exactly one price_id (256 bits) +;; and one ref to the price feed cell. Returns the head of the chain. +;; Each cell now contains exactly: +;; - One price_id (256 bits) +;; - One ref to price_feed_cell +;; - One optional ref to next cell in chain +;; This approach is: +;; - More consistent with TON's cell model +;; - Easier to traverse and read individual price feeds +;; - Cleaner separation of data +;; - More predictable in terms of cell structure +cell create_price_feed_cell_chain(tuple price_feeds) { + cell result = null(); + + int i = price_feeds.tlen() - 1; + while (i >= 0) { + tuple price_feed = price_feeds.at(i); + int price_id = price_feed.at(0); + cell price_feed_cell = price_feed.at(1); + + ;; Create new cell with single price feed and chain to previous result + builder current_builder = begin_cell() + .store_uint(price_id, 256) ;; Store price_id + .store_ref(price_feed_cell); ;; Store price data ref + + ;; Chain to previous cells if they exist + if (~ cell_null?(result)) { + current_builder = current_builder.store_ref(result); + } + + result = current_builder.end_cell(); + i -= 1; + } + + return result; +} + +() send_price_feeds_response(tuple price_feeds, int msg_value, int op, slice sender_address, slice target_address, slice custom_payload) impure { + ;; Build response cell with price feeds + builder response = begin_cell() + .store_uint(op, 32) ;; Response op + .store_uint(price_feeds.tlen(), 8); ;; Number of price feeds + + ;; Create and store price feed cell chain + cell price_feeds_cell = create_price_feed_cell_chain(price_feeds); + cell custom_payload_cell = begin_cell().store_slice(custom_payload).end_cell(); + response = response.store_ref(price_feeds_cell).store_slice(sender_address).store_ref(custom_payload_cell); + + int num_price_feeds = price_feeds.tlen(); + + ;; Calculate all fees + int compute_fee = get_compute_fee(WORKCHAIN, get_gas_consumed()); + int update_fee = single_update_fee * price_feeds.tlen(); + + ;; Calculate total fees and remaining excess + int total_fees = compute_fee + update_fee; + int excess = msg_value - total_fees; + + ;; SECURITY: Integrators MUST validate that messages are from this Pyth contract + ;; in their receive function. Otherwise, attackers could: + ;; 1. Send invalid price responses + ;; 2. Impersonate users via sender_address and custom_payload fields + ;; 3. Potentially drain the protocol + ;; + ;; Note: This message is bounceable. If the target contract rejects the message, + ;; the excess TON will remain in this contract and won't be automatically refunded to the + ;; original sender. Integrators should handle failed requests and refunds in their implementation. + send_raw_message(begin_cell() + .store_uint(0x18, 6) + .store_slice(target_address) + .store_coins(excess) + .store_uint(1, MSG_SERIALIZE_BITS) + .store_ref(response.end_cell()) + .end_cell(), + 0); +} + +;; Helper function to parse price IDs from a slice, handling cell chain traversal +;; Returns a tuple containing the parsed price IDs +tuple parse_price_ids_from_slice(slice price_ids_slice) { + int price_ids_len = price_ids_slice~load_uint(8); + tuple price_ids = empty_tuple(); + + ;; Process each price ID, handling potential cell boundaries + int i = 0; + while (i < price_ids_len) { + builder price_id_builder = begin_cell(); + int bits_loaded = 0; + + ;; We need to load exactly 256 bits for each price ID + while (bits_loaded < 256) { + ;; Calculate how many bits we can load from the current slice + int bits_to_load = min(price_ids_slice.slice_bits(), 256 - bits_loaded); + + ;; Load and store those bits + price_id_builder = price_id_builder.store_slice(price_ids_slice~load_bits(bits_to_load)); + bits_loaded += bits_to_load; + + ;; If we haven't loaded all 256 bits yet, we need to move to the next cell + if (bits_loaded < 256) { + ;; Make sure we have a next cell to load from + throw_unless(35, ~ price_ids_slice.slice_refs_empty?()); + price_ids_slice = price_ids_slice~load_ref().begin_parse(); + } + } + + ;; Extract the complete price ID from the builder + slice price_id_slice = price_id_builder.end_cell().begin_parse(); + int price_id = price_id_slice~load_uint(256); + price_ids~tpush(price_id); + i += 1; + } + + return price_ids; +} + +() parse_price_feed_updates(int msg_value, slice update_data_slice, slice price_ids_slice, int min_publish_time, int max_publish_time, slice sender_address, slice target_address, slice custom_payload) impure { + try { + load_data(); + + ;; Use the helper function to parse price IDs + tuple price_ids = parse_price_ids_from_slice(price_ids_slice); + + tuple price_feeds = parse_price_feeds_from_data_no_verify(msg_value, update_data_slice, price_ids, min_publish_time, max_publish_time, false); + send_price_feeds_response(price_feeds, msg_value, OP_PARSE_PRICE_FEED_UPDATES, + sender_address, target_address, custom_payload); + } catch (_, error_code) { + ;; Handle any unexpected errors + emit_error(error_code, OP_PARSE_PRICE_FEED_UPDATES, + sender_address, begin_cell().store_slice(custom_payload).end_cell()); + } +} + +() parse_unique_price_feed_updates(int msg_value, slice update_data_slice, slice price_ids_slice, int publish_time, int max_staleness, slice sender_address, slice target_address, slice custom_payload) impure { + try { + load_data(); + + ;; Use the helper function to parse price IDs + tuple price_ids = parse_price_ids_from_slice(price_ids_slice); + + tuple price_feeds = parse_price_feeds_from_data_no_verify(msg_value, update_data_slice, price_ids, publish_time, publish_time + max_staleness, true); + send_price_feeds_response(price_feeds, msg_value, OP_PARSE_UNIQUE_PRICE_FEED_UPDATES, sender_address, target_address, custom_payload); + } catch (_, error_code) { + ;; Handle any unexpected errors + emit_error(error_code, OP_PARSE_UNIQUE_PRICE_FEED_UPDATES, + sender_address, begin_cell().store_slice(custom_payload).end_cell()); + } +} + +() update_price_feeds(int msg_value, slice data) impure { + load_data(); + tuple price_feeds = parse_price_feeds_from_data_no_verify(msg_value, data, empty_tuple(), 0, 0, false); + int num_updates = price_feeds.tlen(); + + int i = 0; + while(i < num_updates) { + tuple price_feed = price_feeds.at(i); + int price_id = price_feed.at(0); + cell price_feed_cell = price_feed.at(1); + slice price_feed = price_feed_cell.begin_parse(); + slice price = price_feed~load_ref().begin_parse(); + slice ema_price = price_feed~load_ref().begin_parse(); + (int price_, int conf, int expo, int publish_time) = parse_price(price); + + (slice latest_price_info, int found?) = latest_price_feeds.udict_get?(256, price_id); + int latest_publish_time = 0; + if (found?) { + slice price_feed_slice = latest_price_info~load_ref().begin_parse(); + slice price_slice = price_feed_slice~load_ref().begin_parse(); + + price_slice~load_int(64); ;; Skip price + price_slice~load_uint(64); ;; Skip conf + price_slice~load_int(32); ;; Skip expo + latest_publish_time = price_slice~load_uint(64); + } + + if (publish_time > latest_publish_time) { + latest_price_feeds~udict_set(256, price_id, begin_cell().store_ref(price_feed_cell).end_cell().begin_parse()); + } + i += 1; + } + + store_data(); +} + +() verify_governance_vm(int emitter_chain_id, int emitter_address, int sequence) impure { + (int gov_chain_id, int gov_address) = parse_data_source(governance_data_source); + throw_unless(ERROR_INVALID_GOVERNANCE_DATA_SOURCE, emitter_chain_id == gov_chain_id); + throw_unless(ERROR_INVALID_GOVERNANCE_DATA_SOURCE, emitter_address == gov_address); + throw_if(ERROR_OLD_GOVERNANCE_MESSAGE, sequence <= last_executed_governance_sequence); + last_executed_governance_sequence = sequence; +} + +(int, int, slice) parse_governance_instruction(slice payload) impure { + int magic = payload~load_uint(32); + throw_unless(ERROR_INVALID_GOVERNANCE_MAGIC, magic == GOVERNANCE_MAGIC); + + int module = payload~load_uint(8); + throw_unless(ERROR_INVALID_GOVERNANCE_MODULE, module == GOVERNANCE_MODULE); + + int action = payload~load_uint(8); + + int target_chain_id = payload~load_uint(16); + + return (target_chain_id, action, payload); +} + +int apply_decimal_expo(int value, int expo) { + int result = value; + repeat (expo) { + result *= 10; + } + return result; +} + +() execute_upgrade_contract(cell new_code) impure { + load_data(); + int hash_code = cell_hash(new_code); + throw_unless(ERROR_INVALID_CODE_HASH, upgrade_code_hash == hash_code); + + ;; Set the new code + set_code(new_code); + + ;; Set the code continuation to the new code + set_c3(new_code.begin_parse().bless()); + + ;; Reset the upgrade code hash + upgrade_code_hash = 0; + + ;; Store the data to persist the reset above + store_data(); + + ;; Throw an exception to end the current execution + ;; The contract will be restarted with the new code + throw(0); +} + +() execute_authorize_upgrade_contract(slice payload) impure { + int code_hash = payload~load_uint(256); + upgrade_code_hash = code_hash; +} + +() execute_authorize_governance_data_source_transfer(slice payload) impure { + ;; Verify the claim VAA + (_, _, _, _, int emitter_chain_id, int emitter_address, int sequence, _, slice claim_payload, _) = parse_and_verify_wormhole_vm(payload); + + ;; Parse the claim payload + (int target_chain_id, int action, slice claim_payload) = parse_governance_instruction(claim_payload); + + ;; Verify that this is a valid governance action for this chain + throw_if(ERROR_INVALID_GOVERNANCE_TARGET, (target_chain_id != 0) & (target_chain_id != chain_id)); + throw_unless(ERROR_INVALID_GOVERNANCE_ACTION, action == REQUEST_GOVERNANCE_DATA_SOURCE_TRANSFER); + + ;; Extract the new governance data source index from the claim payload + int new_governance_data_source_index = claim_payload~load_uint(32); + + ;; Verify that the new index is greater than the current index + int current_index = governance_data_source_index; + throw_if(ERROR_OLD_GOVERNANCE_MESSAGE, new_governance_data_source_index <= current_index); + + ;; Update the governance data source + governance_data_source = begin_cell() + .store_uint(emitter_chain_id, 16) + .store_uint(emitter_address, 256) + .end_cell(); + + governance_data_source_index = new_governance_data_source_index; + + ;; Update the last executed governance sequence + last_executed_governance_sequence = sequence; +} + +() execute_set_data_sources(slice payload) impure { + int num_sources = payload~load_uint(8); + cell new_data_sources = new_dict(); + + repeat(num_sources) { + (cell data_source, slice new_payload) = read_and_store_large_data(payload, 272); ;; 272 = 256 + 16 + payload = new_payload; + slice data_source_slice = data_source.begin_parse(); + int emitter_chain_id = data_source_slice~load_uint(16); + int emitter_address = data_source_slice~load_uint(256); + cell data_source = begin_cell() + .store_uint(emitter_chain_id, 16) + .store_uint(emitter_address, 256) + .end_cell(); + int data_source_key = cell_hash(data_source); + new_data_sources~udict_set(256, data_source_key, begin_cell().store_int(true, 1).end_cell().begin_parse()); + } + + ;; Verify that all data in the payload was processed + throw_unless(ERROR_INVALID_PAYLOAD_LENGTH, payload.slice_empty?()); + + is_valid_data_source = new_data_sources; +} + +() execute_set_fee(slice payload) impure { + int value = payload~load_uint(64); + int expo = payload~load_uint(64); + int new_fee = apply_decimal_expo(value, expo); + single_update_fee = new_fee; +} + +() execute_governance_payload(int action, slice payload) impure { + if (action == AUTHORIZE_UPGRADE_CONTRACT) { + execute_authorize_upgrade_contract(payload); + } elseif (action == AUTHORIZE_GOVERNANCE_DATA_SOURCE_TRANSFER) { + execute_authorize_governance_data_source_transfer(payload); + } elseif (action == SET_DATA_SOURCES) { + execute_set_data_sources(payload); + } elseif (action == SET_FEE) { + execute_set_fee(payload); + } elseif (action == SET_VALID_PERIOD) { + ;; Unsupported governance action + throw(ERROR_INVALID_GOVERNANCE_ACTION); + } elseif (action == REQUEST_GOVERNANCE_DATA_SOURCE_TRANSFER) { + ;; RequestGovernanceDataSourceTransfer can only be part of + ;; AuthorizeGovernanceDataSourceTransfer message + throw(ERROR_INVALID_GOVERNANCE_ACTION); + } else { + throw(ERROR_INVALID_GOVERNANCE_ACTION); + } +} + +() execute_governance_action(slice in_msg_body) impure { + load_data(); + + (_, _, _, _, int emitter_chain_id, int emitter_address, int sequence, _, slice payload, _) = parse_and_verify_wormhole_vm(in_msg_body); + + verify_governance_vm(emitter_chain_id, emitter_address, sequence); + + (int target_chain_id, int action, slice payload) = parse_governance_instruction(payload); + + throw_if(ERROR_INVALID_GOVERNANCE_TARGET, (target_chain_id != 0) & (target_chain_id != chain_id)); + + execute_governance_payload(action, payload); + + store_data(); +} diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/tests/PythTest.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/tests/PythTest.fc new file mode 100644 index 0000000..b1d3db1 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/tests/PythTest.fc @@ -0,0 +1,103 @@ +{- + This test contract serves two main purposes: + 1. It allows testing of non-getter functions in FunC without requiring specific opcodes for each function. + 2. It provides access to internal functions through wrapper getter functions. + + This approach is common in FunC development, where a separate test contract is used for unit testing. + It enables more comprehensive testing of the contract's functionality, including internal operations + that are not directly accessible through standard getter methods. +-} + +#include "../imports/stdlib.fc"; +#include "../Pyth.fc"; +#include "../Wormhole.fc"; +#include "../common/op.fc"; + +() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { + return (); + } + + int op = in_msg_body~load_uint(32); + cell data = in_msg_body~load_ref(); + slice data_slice = data.begin_parse(); + + ;; Get sender address from message + slice cs = in_msg_full.begin_parse(); + cs~skip_bits(4); ;; skip flags + slice sender_address = cs~load_msg_addr(); ;; load sender address + + if (op == OP_UPDATE_GUARDIAN_SET) { + update_guardian_set(data_slice); + } elseif (op == OP_UPDATE_PRICE_FEEDS) { + update_price_feeds(msg_value, data_slice); + } elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) { + execute_governance_action(data_slice); + } elseif (op == OP_UPGRADE_CONTRACT) { + execute_upgrade_contract(data); + } elseif (op == OP_PARSE_PRICE_FEED_UPDATES) { + cell price_ids_cell = in_msg_body~load_ref(); + slice price_ids_slice = price_ids_cell.begin_parse(); + int min_publish_time = in_msg_body~load_uint(64); + int max_publish_time = in_msg_body~load_uint(64); + slice target_address = in_msg_body~load_msg_addr(); + cell custom_payload_cell = in_msg_body~load_ref(); + slice custom_payload = custom_payload_cell.begin_parse(); + parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address, target_address, custom_payload); + } elseif (op == OP_PARSE_UNIQUE_PRICE_FEED_UPDATES) { + cell price_ids_cell = in_msg_body~load_ref(); + slice price_ids_slice = price_ids_cell.begin_parse(); + int publish_time = in_msg_body~load_uint(64); + int max_staleness = in_msg_body~load_uint(64); + slice target_address = in_msg_body~load_msg_addr(); + cell custom_payload_cell = in_msg_body~load_ref(); + slice custom_payload = custom_payload_cell.begin_parse(); + parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address, target_address, custom_payload); + } else { + throw(0xffff); ;; Throw exception for unknown op + } +} + +(int, int, int, int) test_get_price_unsafe(int price_feed_id) method_id { + return get_price_unsafe(price_feed_id); +} + +(int, int, int, int) test_get_price_no_older_than(int time_period, int price_feed_id) method_id { + return get_price_no_older_than(time_period, price_feed_id); +} + +(int, int, int, int) test_get_ema_price_unsafe(int price_feed_id) method_id { + return get_ema_price_unsafe(price_feed_id); +} + +(int, int, int, int) test_get_ema_price_no_older_than(int time_period, int price_feed_id) method_id { + return get_ema_price_no_older_than(time_period, price_feed_id); +} + +(int) test_get_update_fee(slice in_msg_body) method_id { + return get_update_fee(in_msg_body); +} + +(int) test_get_single_update_fee() method_id { + return get_single_update_fee(); +} + +(int) test_get_chain_id() method_id { + return get_chain_id(); +} + +(int) test_get_last_executed_governance_sequence() method_id { + return get_last_executed_governance_sequence(); +} + +(int) test_get_governance_data_source_index() method_id { + return get_governance_data_source_index(); +} + +(cell) test_get_governance_data_source() method_id { + return get_governance_data_source(); +} + +(int) test_get_is_valid_data_source(cell data_source) method_id { + return get_is_valid_data_source(data_source); +} diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/tests/PythTestUpgraded.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/tests/PythTestUpgraded.fc new file mode 100644 index 0000000..7f8a2a2 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/tests/PythTestUpgraded.fc @@ -0,0 +1,102 @@ +{- + This test contract is an upgraded version of PythTest.fc. This is used to test the upgrade functionality of the Pyth contract. +-} + +#include "../imports/stdlib.fc"; +#include "../Pyth.fc"; +#include "../Wormhole.fc"; +#include "../common/op.fc"; + +() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { + return (); + } + + int op = in_msg_body~load_uint(32); + cell data = in_msg_body~load_ref(); + slice data_slice = data.begin_parse(); + + ;; Get sender address from message + slice cs = in_msg_full.begin_parse(); + cs~skip_bits(4); ;; skip flags + slice sender_address = cs~load_msg_addr(); ;; load sender address + + if (op == OP_UPDATE_GUARDIAN_SET) { + update_guardian_set(data_slice); + } elseif (op == OP_UPDATE_PRICE_FEEDS) { + update_price_feeds(msg_value, data_slice); + } elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) { + execute_governance_action(data_slice); + } elseif (op == OP_UPGRADE_CONTRACT) { + execute_upgrade_contract(data); + } elseif (op == OP_PARSE_PRICE_FEED_UPDATES) { + cell price_ids_cell = in_msg_body~load_ref(); + slice price_ids_slice = price_ids_cell.begin_parse(); + int min_publish_time = in_msg_body~load_uint(64); + int max_publish_time = in_msg_body~load_uint(64); + slice target_address = in_msg_body~load_msg_addr(); + cell custom_payload_cell = in_msg_body~load_ref(); + slice custom_payload = custom_payload_cell.begin_parse(); + parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address, target_address, custom_payload); + } elseif (op == OP_PARSE_UNIQUE_PRICE_FEED_UPDATES) { + cell price_ids_cell = in_msg_body~load_ref(); + slice price_ids_slice = price_ids_cell.begin_parse(); + int publish_time = in_msg_body~load_uint(64); + int max_staleness = in_msg_body~load_uint(64); + slice target_address = in_msg_body~load_msg_addr(); + cell custom_payload_cell = in_msg_body~load_ref(); + slice custom_payload = custom_payload_cell.begin_parse(); + parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address, target_address, custom_payload); + } else { + throw(0xffff); ;; Throw exception for unknown op + } +} + +(int, int, int, int) test_get_price_unsafe(int price_feed_id) method_id { + return get_price_unsafe(price_feed_id); +} + +(int, int, int, int) test_get_price_no_older_than(int time_period, int price_feed_id) method_id { + return get_price_no_older_than(time_period, price_feed_id); +} + +(int, int, int, int) test_get_ema_price_unsafe(int price_feed_id) method_id { + return get_ema_price_unsafe(price_feed_id); +} + +(int, int, int, int) test_get_ema_price_no_older_than(int time_period, int price_feed_id) method_id { + return get_ema_price_no_older_than(time_period, price_feed_id); +} + +(int) test_get_update_fee(slice in_msg_body) method_id { + return get_update_fee(in_msg_body); +} + +(int) test_get_single_update_fee() method_id { + return get_single_update_fee(); +} + +(int) test_get_chain_id() method_id { + return get_chain_id(); +} + +(int) test_get_last_executed_governance_sequence() method_id { + return get_last_executed_governance_sequence(); +} + +(int) test_get_governance_data_source_index() method_id { + return get_governance_data_source_index(); +} + +(cell) test_get_governance_data_source() method_id { + return get_governance_data_source(); +} + +(int) test_get_is_valid_data_source(cell data_source) method_id { + return get_is_valid_data_source(data_source); +} + +;; Add a new function to demonstrate the upgrade +(int) test_new_function() method_id { + return 1; +} diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/tests/SendUsd.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/tests/SendUsd.fc new file mode 100644 index 0000000..cd217fc --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/tests/SendUsd.fc @@ -0,0 +1,154 @@ +#include "../imports/stdlib.fc"; + +;; Storage variables +global int ctx_id; +global slice ctx_pyth_address; + +const WORKCHAIN = 0; +const OP_SEND_USD = 1; +const PYTH_OP_PARSE_PRICE_FEED_UPDATES = 5; +const TON_PRICE_FEED_ID = 0x8963217838ab4cf5cadc172203c1f0b763fbaa45f346d8ee50ba994bbcac3026; +const ERROR_UNAUTHORIZED = 401; + +;; Load contract data from storage +() load_data() impure { + var ds = get_data().begin_parse(); + ctx_pyth_address = ds~load_msg_addr(); + ds.end_parse(); +} + +;; Save contract data to storage +() save_data() impure { + set_data( + begin_cell() + .store_slice(ctx_pyth_address) + .end_cell() + ); +} + +;; Helper to send TON with a comment +() send_ton(slice recipient, int amount) impure { + cell msg = begin_cell() + .store_uint(0x18, 6) ;; nobounce + .store_slice(recipient) ;; destination address + .store_coins(amount) ;; amount to send + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers + .store_uint(0, 32) ;; op = 0 for comment + .end_cell(); + + send_raw_message(msg, 0); +} + +;; Helper to send message to Pyth contract +() request_price_feed(int msg_value, int query_id, slice recipient, int usd_amount, cell price_update_data) impure { + ;; Create price IDs cell + cell price_ids = begin_cell() + .store_uint(1, 8) + .store_uint(TON_PRICE_FEED_ID, 256) + .end_cell(); + + ;; Create custom payload with unique query ID to identify response + cell custom_payload = begin_cell() + .store_uint(query_id, 64) + .store_slice(recipient) + .store_uint(usd_amount, 16) + .end_cell(); + + + int compute_fee = get_compute_fee(WORKCHAIN, gas_consumed()); + int forward_amount = msg_value - compute_fee; + + ;; Create message to Pyth contract according to schema + cell msg = begin_cell() + .store_uint(0x18, 6) ;; nobounce + .store_slice(ctx_pyth_address) ;; pyth contract address + .store_coins(forward_amount) ;; forward amount minus fees + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers + .store_uint(PYTH_OP_PARSE_PRICE_FEED_UPDATES, 32) ;; pyth opcode + .store_ref(price_update_data) ;; update data + .store_ref(price_ids) ;; price feed IDs + .store_uint(now() - 100, 64) ;; min_publish_time + .store_uint(now() + 100, 64) ;; max_publish_time + .store_slice(my_address()) ;; target address (this contract) + .store_ref(custom_payload) ;; custom payload with recipient and amount + .end_cell(); + + send_raw_message(msg, 0); +} + +;; Main message handler +() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { + return (); + } + + ;; Get sender address from message + slice cs = in_msg_full.begin_parse(); + cs~skip_bits(4); ;; skip flags + slice sender_address = cs~load_msg_addr(); ;; load sender address + + load_data(); + + int op = in_msg_body~load_uint(32); + + if (op == OP_SEND_USD) { + int query_id = in_msg_body~load_uint(64); ;; Unique ID for this request + slice recipient = in_msg_body~load_msg_addr(); + int usd_amount = in_msg_body~load_uint(16); + cell price_update = in_msg_body~load_ref(); + + ;; Request price from Pyth with recipient and amount in custom payload + request_price_feed(msg_value, query_id, recipient, usd_amount, price_update); + return (); + } + + if (op == PYTH_OP_PARSE_PRICE_FEED_UPDATES) { + ;; Check if sender is the official Pyth contract + throw_unless(ERROR_UNAUTHORIZED, equal_slices_bits(sender_address, ctx_pyth_address)); + + int num_price_feeds = in_msg_body~load_uint(8); + + cell price_feeds_cell = in_msg_body~load_ref(); + + slice price_feeds = price_feeds_cell.begin_parse(); + int price_id = price_feeds~load_uint(256); + cell price_data = price_feeds~load_ref(); + slice price_data_s = price_data.begin_parse(); + cell current_price_cell = price_data_s~load_ref(); + slice current_price_s = current_price_cell.begin_parse(); + int price = current_price_s~load_int(64); + int conf = current_price_s~load_uint(64); + int expo = current_price_s~load_int(32); + int timestamp = current_price_s~load_uint(64); + + ;; Load sender address and custom payload + slice sender = in_msg_body~load_msg_addr(); + cell custom_payload = in_msg_body~load_ref(); + slice cp = custom_payload.begin_parse(); + + int query_id = cp~load_uint(64); + slice recipient = cp~load_msg_addr(); + int usd_amount = cp~load_uint(16); + + int decimals = 1; + repeat((0 - expo)) { + decimals = decimals * 10; + } + + ;; Calculate: (1e9 * decimals * usd_amount) / price + int ton_amount = ((1000000000 * decimals * usd_amount) / price); + + ;; Send TON to recipient, please note that extra transferred TON will be stored in this contract + send_ton(recipient, ton_amount); + + return (); + } + + throw(0xffff); ;; Throw on unknown op +} + +;; Get methods +slice get_pyth_address() method_id { + load_data(); + return ctx_pyth_address; +} \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/tests/WormholeTest.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/tests/WormholeTest.fc new file mode 100644 index 0000000..249444f --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/tests/WormholeTest.fc @@ -0,0 +1,60 @@ +{- + This test contract serves two main purposes: + 1. It allows testing of non-getter functions in FunC without requiring specific opcodes for each function. + 2. It provides access to internal functions through wrapper getter functions. + + This approach is common in FunC development, where a separate test contract is used for unit testing. + It enables more comprehensive testing of the contract's functionality, including internal operations + that are not directly accessible through standard getter methods. +-} + +#include "../imports/stdlib.fc"; +#include "../Wormhole.fc"; +#include "../common/op.fc"; + +() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { + return (); + } + + int op = in_msg_body~load_uint(32); + cell data = in_msg_body~load_ref(); + slice data_slice = data.begin_parse(); + if (op == OP_UPDATE_GUARDIAN_SET) { + update_guardian_set(data_slice); + } else { + throw(0xffff); ;; Throw exception for unknown op + } +} + +(int, int, int, cell, int) test_parse_encoded_upgrade(int current_guardian_set_index, slice payload) method_id { + return parse_encoded_upgrade(current_guardian_set_index, payload); +} + +(int, int, int, int, int, int, int, int, slice, int) test_parse_and_verify_wormhole_vm(slice in_msg_body) method_id { + return parse_and_verify_wormhole_vm(in_msg_body); +} + +(int) test_get_current_guardian_set_index() method_id { + return get_current_guardian_set_index(); +} + +(int, cell, int) test_get_guardian_set(int index) method_id { + return get_guardian_set(index); +} + +(int) test_get_chain_id() method_id { + return get_chain_id(); +} + +(int) test_get_governance_chain_id() method_id { + return get_governance_chain_id(); +} + +(int) test_get_governance_contract() method_id { + return get_governance_contract(); +} + +(int) test_governance_action_is_consumed(int hash) method_id { + return governance_action_is_consumed(hash); +} diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/configuration.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/configuration.fc new file mode 100644 index 0000000..ff0cfcf --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/configuration.fc @@ -0,0 +1,21 @@ +#include "imports/stdlib.fc"; + +;; load contract data from storage +(slice, cell) load_configuration() impure { + var ds = get_data().begin_parse(); + slice pyth_address = ds~load_msg_addr(); + cell ids_map = ds~load_dict(); + ds.end_parse(); + + return (pyth_address, ids_map); +} + +;; save contract data to storage +() save_configuration(slice pyth_address, cell ids_map) impure { + cell config_cell = begin_cell() + .store_slice(pyth_address) + .store_dict(ids_map) + .end_cell(); + + set_data(config_cell); +} diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/constants/constants.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/constants/constants.fc new file mode 100644 index 0000000..0cf3eb5 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/constants/constants.fc @@ -0,0 +1,27 @@ +;; common +const int CONST::WORKCHAIN = 0; + +const int CONST::PUBLISH_GAP = 10; ;; 10 seconds +const int CONST::MAX_STALENESS = 180; ;; 180 seconds + +;; internal +const int CONST::INTERNAL_PRICE_SCALE = 1000000000; + + +;; message modes +;;; x = 64 is used for messages that carry all the remaining value of the inbound message in addition to the value initially indicated in the new message. +const int SENDMODE::CARRY_ALL_REMAINING_MESSAGE_VALUE = 64; +;;; x = 128 is used for messages that are to carry all the remaining balance of the current smart contract (instead of the value originally indicated in the message). +const int SENDMODE::CARRY_ALL_BALANCE = 128; + +;; reserve +;;; Creates an output action which would reserve exactly x nanograms (if y = 0). +const int RESERVE::REGULAR = 0; +;;; Creates an output action which would reserve at most x nanograms (if y = 2). +;;; Bit +2 in y means that the external action does not fail if the specified amount cannot be reserved; instead, all remaining balance is reserved. +const int RESERVE::AT_MOST = 2; +;;; in the case of action fail - bounce transaction. No effect if RESERVE_AT_MOST (+2) is used. TVM UPGRADE 2023-07. +const int RESERVE::BOUNCE_ON_ACTION_FAIL = 16; + + +const int FEE::SUPPLY_MARGIN = 100000000; ;; 0.1 TON margin \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/constants/errors.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/constants/errors.fc new file mode 100644 index 0000000..b2a65ef --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/constants/errors.fc @@ -0,0 +1,21 @@ +;; jettons +const int ERROR::RECEIVED_UNSUPPORTED_JETTON = 0x40FD; +const int ERROR::UNSUPPORTED_JETTON_OP_CODE = 0x40FC; +const int ERROR::JETTON_EXECUTION_CRASHED = 0x40FE; + +;; connector +const int ERROR::NOT_AUTHORIZED = 401; + +const int ERROR::UNSUPPORTED_PRICE_ID = 4000; +const int ERROR::NOT_SAVED_PRICE_ID = 4001; +const int ERROR::INCORRECT_PRICE_FEEDS_NUM = 4002; +const int ERROR::REFERRED_PRICE_ID_NOT_FOUND = 4003; +const int ERROR::PRICE_ID_NOT_ACTUAL = 4004; +const int ERROR::PYTH_ERROR_RECEIVED = 4005; +const int ERROR::CUSTOM_OPERATOIN_FAILED = 4006; +const int ERROR::INVALID_PYTH_OPERATION_CODE = 4007; +const int ERROR::UNKNOWN_OPERATION = 4008; +const int ERROR::ONCHAIN_GETTER_FAILURE = 4009; +const int ERROR::NOT_IMPLEMENTED = 4010; +const int ERROR::UNSUPPORTED_OPERATION = 4011; +const int ERROR::NOT_ENOUGH_TRANSFERRED = 4012; \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/constants/logs.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/constants/logs.fc new file mode 100644 index 0000000..dda8b4e --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/constants/logs.fc @@ -0,0 +1,15 @@ +;; logs +const int LOG::PRICE_PARSED = 128 + 4; +const int LOG::DEBUG = 128 + 5; +const int LOG::CONFIGURE = 128 + 6; +const int LOG::UNSUPPORTED_OPERATION_FROM_PYTH = 128 + 7; +const int LOG::UNSUPPORTED_FEEDS_OPERATION = 128 + 8; +const int LOG::PROCESS_ONCHAIN_GETTER_JETTON = 128 + 9; + +const int LOG::CUSTOM_OPERATION_PROCESSING = 128 + 10; +const int LOG::ONCHAIN_GETTER_OPERATION_PROCESSING = 128 + 11; +const int LOG::PROXY_OPERATION_PROCESSING = 128 + 12; + +const int LOG::CUSTOM_OPERATION_STARTED = 128 + 20; +const int LOG::ONCHAIN_GETTER_OPERATOIN_STARTED = 128 + 21; +const int LOG::PROXY_OPERATION_STARTED = 128 + 22; diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/constants/op_codes.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/constants/op_codes.fc new file mode 100644 index 0000000..bd4fc58 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/constants/op_codes.fc @@ -0,0 +1,16 @@ +;; pyth +const int OP::PYTH_PARSE_UNIQUE_PRICE_FEED_UPDATES = 6; +const int OP::PYTH_PARSE_PRICE_FEED_UPDATES = 5; +const int OP::PYTH_RESPONSE_ERROR = 0x10002; + +;; connector +const OP::CONNECTOR_CONFIGURE = 0x2; +const OP::CONNECTOR_CUSTOM_OPERATION = 0x3; +const OP::CONNECTOR_PROXY_OPERATION = 0x4; +const OP::CONNECTOR_ONCHAIN_GETTER_REQUEST = 0x7; +const OP::CONNECTOR_ONCHAIN_GETTER_PROCESS = 0x8; + +;; jetton +const int OP::JETTON_TRANSFER = 0x0f8a7ea5; +const int OP::JETTON_TRANSFER_NOTIFICATION = 0x7362d09c; +const int OP::JETTON_EXCESSES = 0xd53276db; diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/getters.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/getters.fc new file mode 100644 index 0000000..66ca24c --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/getters.fc @@ -0,0 +1,12 @@ +#include "configuration.fc"; + +;; getter methods +slice get_pyth_address() method_id { + (slice pyth_address, _) = load_configuration(); + return pyth_address; +} + +cell get_feeds_dict() method_id { + ( _, cell ids_map) = load_configuration(); + return ids_map; +} diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/imports/basic_types.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/imports/basic_types.fc new file mode 100644 index 0000000..8668ccd --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/imports/basic_types.fc @@ -0,0 +1,73 @@ +#include "stdlib.fc"; + +(slice, int) load_op_code(slice cs) inline { + return cs.load_uint(32); +} + +builder store_op_code(builder b, int op_code) inline { + return b.store_uint(op_code, 32); +} + +builder store_price(builder b, int price) inline { + return b.store_uint(price, 64); +} + +(slice, int) load_price(slice cs) inline { + return cs.load_uint(64); +} + +builder store_address_hash(builder b, int address_hash) inline { + return b.store_uint(address_hash, 256); +} + +(slice, int) load_address_hash(slice cs) inline { + return cs.load_uint(256); +} + +builder store_asset_id(builder b, int asset_id) inline { + return b.store_uint(asset_id, 256); +} + +(slice, int) load_asset_id(slice cs) inline { + return cs.load_uint(256); +} + +builder store_amount(builder b, int amount) inline { + return b.store_uint(amount, 64); +} + +(slice, int) load_amount(slice cs) inline { + return cs.load_uint(64); +} + +builder store_balance(builder b, int balance) inline { + return b.store_uint(balance, 64); +} + +(slice, int) load_balance(slice cs) inline { + return cs.load_uint(64); +} + +builder store_principal(builder b, int principal) inline { + return b.store_int(principal, 64); +} + +(slice, int) load_principal(slice cs) inline { + return cs.load_int(64); +} + +int preload_principal(slice cs) inline { + return cs.preload_int(64); +} + +builder store_timestamp(builder b, int timestamp) inline { + return b.store_uint(timestamp, 32); +} + +(slice, int) load_timestamp(slice cs) inline { + return cs.load_uint(32); +} + +(slice, int) load_bool_ext(slice cs) inline { + return cs.load_int(2); +} diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/imports/stdlib.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/imports/stdlib.fc new file mode 100644 index 0000000..5291eaa --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/imports/stdlib.fc @@ -0,0 +1,645 @@ +;; Standard library for funC +;; + +{- + # Tuple manipulation primitives + The names and the types are mostly self-explaining. + See [polymorhism with forall](https://ton.org/docs/#/func/functions?id=polymorphism-with-forall) + for more info on the polymorphic functions. + + Note that currently values of atomic type `tuple` can't be cast to composite tuple type (e.g. `[int, cell]`) + and vise versa. +-} + +{- + # Lisp-style lists + + Lists can be represented as nested 2-elements tuples. + Empty list is conventionally represented as TVM `null` value (it can be obtained by calling [null()]). + For example, tuple `(1, (2, (3, null)))` represents list `[1, 2, 3]`. Elements of a list can be of different types. +-} + +;;; Adds an element to the beginning of lisp-style list. +forall X -> tuple cons(X head, tuple tail) asm "CONS"; + +;;; Extracts the head and the tail of lisp-style list. +forall X -> (X, tuple) uncons(tuple list) asm "UNCONS"; + +;;; Extracts the tail and the head of lisp-style list. +forall X -> (tuple, X) list_next(tuple list) asm( -> 1 0) "UNCONS"; + +;;; Returns the head of lisp-style list. +forall X -> X car(tuple list) asm "CAR"; + +;;; Returns the tail of lisp-style list. +tuple cdr(tuple list) asm "CDR"; + +;;; Creates tuple with zero elements. +tuple empty_tuple() asm "NIL"; + +;;; Appends a value `x` to a `Tuple t = (x1, ..., xn)`, but only if the resulting `Tuple t' = (x1, ..., xn, x)` +;;; is of length at most 255. Otherwise throws a type check exception. +forall X -> tuple tpush(tuple t, X value) asm "TPUSH"; +forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH"; + +;;; Creates a tuple of length one with given argument as element. +forall X -> [X] single(X x) asm "SINGLE"; + +;;; Unpacks a tuple of length one +forall X -> X unsingle([X] t) asm "UNSINGLE"; + +;;; Creates a tuple of length two with given arguments as elements. +forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR"; + +;;; Unpacks a tuple of length two +forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR"; + +;;; Creates a tuple of length three with given arguments as elements. +forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE"; + +;;; Unpacks a tuple of length three +forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE"; + +;;; Creates a tuple of length four with given arguments as elements. +forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE"; + +;;; Unpacks a tuple of length four +forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE"; + +;;; Returns the first element of a tuple (with unknown element types). +forall X -> X first(tuple t) asm "FIRST"; + +;;; Returns the second element of a tuple (with unknown element types). +forall X -> X second(tuple t) asm "SECOND"; + +;;; Returns the third element of a tuple (with unknown element types). +forall X -> X third(tuple t) asm "THIRD"; + +;;; Returns the fourth element of a tuple (with unknown element types). +forall X -> X fourth(tuple t) asm "3 INDEX"; + +;;; Returns the first element of a pair tuple. +forall X, Y -> X pair_first([X, Y] p) asm "FIRST"; + +;;; Returns the second element of a pair tuple. +forall X, Y -> Y pair_second([X, Y] p) asm "SECOND"; + +;;; Returns the first element of a triple tuple. +forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST"; + +;;; Returns the second element of a triple tuple. +forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND"; + +;;; Returns the third element of a triple tuple. +forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD"; + + +;;; Push null element (casted to given type) +;;; By the TVM type `Null` FunC represents absence of a value of some atomic type. +;;; So `null` can actually have any atomic type. +forall X -> X null() asm "PUSHNULL"; + +;;; Moves a variable [x] to the top of the stack +forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP"; + + + +;;; Returns the current Unix time as an Integer +int now() asm "NOW"; + +;;; Returns the internal address of the current smart contract as a Slice with a `MsgAddressInt`. +;;; If necessary, it can be parsed further using primitives such as [parse_std_addr]. +slice my_address() asm "MYADDR"; + +;;; Returns the balance of the smart contract as a tuple consisting of an int +;;; (balance in nanotoncoins) and a `cell` +;;; (a dictionary with 32-bit keys representing the balance of "extra currencies") +;;; at the start of Computation Phase. +;;; Note that RAW primitives such as [send_raw_message] do not update this field. +[int, cell] get_balance() asm "BALANCE"; + +;;; Returns the logical time of the current transaction. +int cur_lt() asm "LTIME"; + +;;; Returns the starting logical time of the current block. +int block_lt() asm "BLOCKLT"; + +;;; Computes the representation hash of a `cell` [c] and returns it as a 256-bit unsigned integer `x`. +;;; Useful for signing and checking signatures of arbitrary entities represented by a tree of cells. +int cell_hash(cell c) asm "HASHCU"; + +;;; Computes the hash of a `slice s` and returns it as a 256-bit unsigned integer `x`. +;;; The result is the same as if an ordinary cell containing only data and references from `s` had been created +;;; and its hash computed by [cell_hash]. +int slice_hash(slice s) asm "HASHSU"; + +;;; Computes sha256 of the data bits of `slice` [s]. If the bit length of `s` is not divisible by eight, +;;; throws a cell underflow exception. The hash value is returned as a 256-bit unsigned integer `x`. +int string_hash(slice s) asm "SHA256U"; + +{- + # Signature checks +-} + +;;; Checks the Ed25519-`signature` of a `hash` (a 256-bit unsigned integer, usually computed as the hash of some data) +;;; using [public_key] (also represented by a 256-bit unsigned integer). +;;; The signature must contain at least 512 data bits; only the first 512 bits are used. +;;; The result is `−1` if the signature is valid, `0` otherwise. +;;; Note that `CHKSIGNU` creates a 256-bit slice with the hash and calls `CHKSIGNS`. +;;; That is, if [hash] is computed as the hash of some data, these data are hashed twice, +;;; the second hashing occurring inside `CHKSIGNS`. +int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU"; + +;;; Checks whether [signature] is a valid Ed25519-signature of the data portion of `slice data` using `public_key`, +;;; similarly to [check_signature]. +;;; If the bit length of [data] is not divisible by eight, throws a cell underflow exception. +;;; The verification of Ed25519 signatures is the standard one, +;;; with sha256 used to reduce [data] to the 256-bit number that is actually signed. +int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS"; + +{--- + # Computation of boc size + The primitives below may be useful for computing storage fees of user-provided data. +-} + +;;; Returns `(x, y, z, -1)` or `(null, null, null, 0)`. +;;; Recursively computes the count of distinct cells `x`, data bits `y`, and cell references `z` +;;; in the DAG rooted at `cell` [c], effectively returning the total storage used by this DAG taking into account +;;; the identification of equal cells. +;;; The values of `x`, `y`, and `z` are computed by a depth-first traversal of this DAG, +;;; with a hash table of visited cell hashes used to prevent visits of already-visited cells. +;;; The total count of visited cells `x` cannot exceed non-negative [max_cells]; +;;; otherwise the computation is aborted before visiting the `(max_cells + 1)`-st cell and +;;; a zero flag is returned to indicate failure. If [c] is `null`, returns `x = y = z = 0`. +(int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE"; + +;;; Similar to [compute_data_size?], but accepting a `slice` [s] instead of a `cell`. +;;; The returned value of `x` does not take into account the cell that contains the `slice` [s] itself; +;;; however, the data bits and the cell references of [s] are accounted for in `y` and `z`. +(int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE"; + +;;; A non-quiet version of [compute_data_size?] that throws a cell overflow exception (`8`) on failure. +(int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; + +;;; A non-quiet version of [slice_compute_data_size?] that throws a cell overflow exception (8) on failure. +(int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; + +;;; Throws an exception with exit_code excno if cond is not 0 (commented since implemented in compilator) +;; () throw_if(int excno, int cond) impure asm "THROWARGIF"; + +{-- + # Debug primitives + Only works for local TVM execution with debug level verbosity +-} +;;; Dumps the stack (at most the top 255 values) and shows the total stack depth. +() dump_stack() impure asm "DUMPSTK"; + +{- + # Persistent storage save and load +-} + +;;; Returns the persistent contract storage cell. It can be parsed or modified with slice and builder primitives later. +cell get_data() asm "c4 PUSH"; + +;;; Sets `cell` [c] as persistent contract data. You can update persistent contract storage with this primitive. +() set_data(cell c) impure asm "c4 POP"; + +{- + # Continuation primitives +-} +;;; Usually `c3` has a continuation initialized by the whole code of the contract. It is used for function calls. +;;; The primitive returns the current value of `c3`. +cont get_c3() impure asm "c3 PUSH"; + +;;; Updates the current value of `c3`. Usually, it is used for updating smart contract code in run-time. +;;; Note that after execution of this primitive the current code +;;; (and the stack of recursive function calls) won't change, +;;; but any other function call will use a function from the new code. +() set_c3(cont c) impure asm "c3 POP"; + +;;; Transforms a `slice` [s] into a simple ordinary continuation `c`, with `c.code = s` and an empty stack and savelist. +cont bless(slice s) impure asm "BLESS"; + +{--- + # Gas related primitives +-} + +;;; Sets current gas limit `gl` to its maximal allowed value `gm`, and resets the gas credit `gc` to zero, +;;; decreasing the value of `gr` by `gc` in the process. +;;; In other words, the current smart contract agrees to buy some gas to finish the current transaction. +;;; This action is required to process external messages, which bring no value (hence no gas) with themselves. +;;; +;;; For more details check [accept_message effects](https://ton.org/docs/#/smart-contracts/accept). +() accept_message() impure asm "ACCEPT"; + +;;; Sets current gas limit `gl` to the minimum of limit and `gm`, and resets the gas credit `gc` to zero. +;;; If the gas consumed so far (including the present instruction) exceeds the resulting value of `gl`, +;;; an (unhandled) out of gas exception is thrown before setting new gas limits. +;;; Notice that [set_gas_limit] with an argument `limit ≥ 2^63 − 1` is equivalent to [accept_message]. +() set_gas_limit(int limit) impure asm "SETGASLIMIT"; + +;;; Commits the current state of registers `c4` (“persistent data”) and `c5` (“actions”) +;;; so that the current execution is considered “successful” with the saved values even if an exception +;;; in Computation Phase is thrown later. +() commit() impure asm "COMMIT"; + +;;; Not implemented +;;() buy_gas(int gram) impure asm "BUYGAS"; + +;;; Computes the amount of gas that can be bought for `amount` nanoTONs, +;;; and sets `gl` accordingly in the same way as [set_gas_limit]. +() buy_gas(int amount) impure asm "BUYGAS"; + +;;; Computes the minimum of two integers [x] and [y]. +int min(int x, int y) asm "MIN"; + +;;; Computes the maximum of two integers [x] and [y]. +int max(int x, int y) asm "MAX"; + +;;; Sorts two integers. +(int, int) minmax(int x, int y) asm "MINMAX"; + +;;; Computes the absolute value of an integer [x]. +int abs(int x) asm "ABS"; + +{- + # Slice primitives + + It is said that a primitive _loads_ some data, + if it returns the data and the remainder of the slice + (so it can also be used as [modifying method](https://ton.org/docs/#/func/statements?id=modifying-methods)). + + It is said that a primitive _preloads_ some data, if it returns only the data + (it can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods)). + + Unless otherwise stated, loading and preloading primitives read the data from a prefix of the slice. +-} + + +;;; Converts a `cell` [c] into a `slice`. Notice that [c] must be either an ordinary cell, +;;; or an exotic cell (see [TVM.pdf](https://ton-blockchain.github.io/docs/tvm.pdf), 3.1.2) +;;; which is automatically loaded to yield an ordinary cell `c'`, converted into a `slice` afterwards. +slice begin_parse(cell c) asm "CTOS"; + +;;; Checks if [s] is empty. If not, throws an exception. +() end_parse(slice s) impure asm "ENDS"; + +;;; Loads the first reference from the slice. +(slice, cell) load_ref(slice s) asm( -> 1 0) "LDREF"; + +;;; Preloads the first reference from the slice. +cell preload_ref(slice s) asm "PLDREF"; + +{- Functions below are commented because are implemented on compilator level for optimisation -} + +;;; Loads a signed [len]-bit integer from a slice [s]. +;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX"; + +;;; Loads an unsigned [len]-bit integer from a slice [s]. +;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX"; + +;;; Preloads a signed [len]-bit integer from a slice [s]. +;; int preload_int(slice s, int len) asm "PLDIX"; + +;;; Preloads an unsigned [len]-bit integer from a slice [s]. +;; int preload_uint(slice s, int len) asm "PLDUX"; + +;;; Loads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`. +;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX"; + +;;; Preloads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`. +;; slice preload_bits(slice s, int len) asm "PLDSLICEX"; + +;;; Loads serialized amount of TonCoins (any unsigned integer up to `2^128 - 1`). +(slice, int) load_grams(slice s) asm( -> 1 0) "LDGRAMS"; +(slice, int) load_coins(slice s) asm( -> 1 0) "LDGRAMS"; + +;;; Returns all but the first `0 ≤ len ≤ 1023` bits of `slice` [s]. +slice skip_bits(slice s, int len) asm "SDSKIPFIRST"; +(slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST"; + +;;; Returns the first `0 ≤ len ≤ 1023` bits of `slice` [s]. +slice first_bits(slice s, int len) asm "SDCUTFIRST"; + +;;; Returns all but the last `0 ≤ len ≤ 1023` bits of `slice` [s]. +slice skip_last_bits(slice s, int len) asm "SDSKIPLAST"; +(slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST"; + +;;; Returns the last `0 ≤ len ≤ 1023` bits of `slice` [s]. +slice slice_last(slice s, int len) asm "SDCUTLAST"; + +;;; Loads a dictionary `D` (HashMapE) from `slice` [s]. +;;; (returns `null` if `nothing` constructor is used). +(slice, cell) load_dict(slice s) asm( -> 1 0) "LDDICT"; + +;;; Preloads a dictionary `D` from `slice` [s]. +cell preload_dict(slice s) asm "PLDDICT"; + +;;; Loads a dictionary as [load_dict], but returns only the remainder of the slice. +slice skip_dict(slice s) asm "SKIPDICT"; + +;;; Loads (Maybe ^Cell) from `slice` [s]. +;;; In other words loads 1 bit and if it is true +;;; loads first ref and return it with slice remainder +;;; otherwise returns `null` and slice remainder +(slice, cell) load_maybe_ref(slice s) asm( -> 1 0) "LDOPTREF"; + +;;; Preloads (Maybe ^Cell) from `slice` [s]. +cell preload_maybe_ref(slice s) asm "PLDOPTREF"; + + +;;; Returns the depth of `cell` [c]. +;;; If [c] has no references, then return `0`; +;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [c]. +;;; If [c] is a `null` instead of a cell, returns zero. +int cell_depth(cell c) asm "CDEPTH"; + + +{- + # Slice size primitives +-} + +;;; Returns the number of references in `slice` [s]. +int slice_refs(slice s) asm "SREFS"; + +;;; Returns the number of data bits in `slice` [s]. +int slice_bits(slice s) asm "SBITS"; + +;;; Returns both the number of data bits and the number of references in `slice` [s]. +(int, int) slice_bits_refs(slice s) asm "SBITREFS"; + +;;; Checks whether a `slice` [s] is empty (i.e., contains no bits of data and no cell references). +int slice_empty?(slice s) asm "SEMPTY"; + +;;; Checks whether `slice` [s] has no bits of data. +int slice_data_empty?(slice s) asm "SDEMPTY"; + +;;; Checks whether `slice` [s] has no references. +int slice_refs_empty?(slice s) asm "SREMPTY"; + +;;; Returns the depth of `slice` [s]. +;;; If [s] has no references, then returns `0`; +;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [s]. +int slice_depth(slice s) asm "SDEPTH"; + +{- + # Builder size primitives +-} + +;;; Returns the number of cell references already stored in `builder` [b] +int builder_refs(builder b) asm "BREFS"; + +;;; Returns the number of data bits already stored in `builder` [b]. +int builder_bits(builder b) asm "BBITS"; + +;;; Returns the depth of `builder` [b]. +;;; If no cell references are stored in [b], then returns 0; +;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [b]. +int builder_depth(builder b) asm "BDEPTH"; + +{- + # Builder primitives + It is said that a primitive _stores_ a value `x` into a builder `b` + if it returns a modified version of the builder `b'` with the value `x` stored at the end of it. + It can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods). + + All the primitives below first check whether there is enough space in the `builder`, + and only then check the range of the value being serialized. +-} + +;;; Creates a new empty `builder`. +builder begin_cell() asm "NEWC"; + +;;; Converts a `builder` into an ordinary `cell`. +cell end_cell(builder b) asm "ENDC"; + +;;; Stores a reference to `cell` [c] into `builder` [b]. +builder store_ref(builder b, cell c) asm(c b) "STREF"; + +;;; Stores an unsigned [len]-bit integer `x` into `b` for `0 ≤ len ≤ 256`. +;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX"; + +;;; Stores a signed [len]-bit integer `x` into `b` for` 0 ≤ len ≤ 257`. +;; builder store_int(builder b, int x, int len) asm(x b len) "STIX"; + + +;;; Stores `slice` [s] into `builder` [b] +builder store_slice(builder b, slice s) asm "STSLICER"; + +;;; Stores (serializes) an integer [x] in the range `0..2^128 − 1` into `builder` [b]. +;;; The serialization of [x] consists of a 4-bit unsigned big-endian integer `l`, +;;; which is the smallest integer `l ≥ 0`, such that `x < 2^8l`, +;;; followed by an `8l`-bit unsigned big-endian representation of [x]. +;;; If [x] does not belong to the supported range, a range check exception is thrown. +;;; +;;; Store amounts of TonCoins to the builder as VarUInteger 16 +builder store_grams(builder b, int x) asm "STGRAMS"; +builder store_coins(builder b, int x) asm "STGRAMS"; + +;;; Stores dictionary `D` represented by `cell` [c] or `null` into `builder` [b]. +;;; In other words, stores a `1`-bit and a reference to [c] if [c] is not `null` and `0`-bit otherwise. +builder store_dict(builder b, cell c) asm(c b) "STDICT"; + +;;; Stores (Maybe ^Cell) to builder: +;;; if cell is null store 1 zero bit +;;; otherwise store 1 true bit and ref to cell +builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF"; + + +{- + # Address manipulation primitives + The address manipulation primitives listed below serialize and deserialize values according to the following TL-B scheme: + ```TL-B + addr_none$00 = MsgAddressExt; + addr_extern$01 len:(## 8) external_address:(bits len) + = MsgAddressExt; + anycast_info$_ depth:(#<= 30) { depth >= 1 } + rewrite_pfx:(bits depth) = Anycast; + addr_std$10 anycast:(Maybe Anycast) + workchain_id:int8 address:bits256 = MsgAddressInt; + addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9) + workchain_id:int32 address:(bits addr_len) = MsgAddressInt; + _ _:MsgAddressInt = MsgAddress; + _ _:MsgAddressExt = MsgAddress; + + int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool + src:MsgAddress dest:MsgAddressInt + value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams + created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; + ext_out_msg_info$11 src:MsgAddress dest:MsgAddressExt + created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; + ``` + A deserialized `MsgAddress` is represented by a tuple `t` as follows: + + - `addr_none` is represented by `t = (0)`, + i.e., a tuple containing exactly one integer equal to zero. + - `addr_extern` is represented by `t = (1, s)`, + where slice `s` contains the field `external_address`. In other words, ` + t` is a pair (a tuple consisting of two entries), containing an integer equal to one and slice `s`. + - `addr_std` is represented by `t = (2, u, x, s)`, + where `u` is either a `null` (if `anycast` is absent) or a slice `s'` containing `rewrite_pfx` (if anycast is present). + Next, integer `x` is the `workchain_id`, and slice `s` contains the address. + - `addr_var` is represented by `t = (3, u, x, s)`, + where `u`, `x`, and `s` have the same meaning as for `addr_std`. +-} + +;;; Loads from slice [s] the only prefix that is a valid `MsgAddress`, +;;; and returns both this prefix `s'` and the remainder `s''` of [s] as slices. +(slice, slice) load_msg_addr(slice s) asm( -> 1 0) "LDMSGADDR"; + +;;; Decomposes slice [s] containing a valid `MsgAddress` into a `tuple t` with separate fields of this `MsgAddress`. +;;; If [s] is not a valid `MsgAddress`, a cell deserialization exception is thrown. +tuple parse_addr(slice s) asm "PARSEMSGADDR"; + +;;; Parses slice [s] containing a valid `MsgAddressInt` (usually a `msg_addr_std`), +;;; applies rewriting from the anycast (if present) to the same-length prefix of the address, +;;; and returns both the workchain and the 256-bit address as integers. +;;; If the address is not 256-bit, or if [s] is not a valid serialization of `MsgAddressInt`, +;;; throws a cell deserialization exception. +(int, int) parse_std_addr(slice s) asm "REWRITESTDADDR"; + +;;; A variant of [parse_std_addr] that returns the (rewritten) address as a slice [s], +;;; even if it is not exactly 256 bit long (represented by a `msg_addr_var`). +(int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR"; + +{- + # Dictionary primitives +-} + + +;;; Sets the value associated with [key_len]-bit key signed index in dictionary [dict] to [value] (cell), +;;; and returns the resulting dictionary. +cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; +(cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; + +;;; Sets the value associated with [key_len]-bit key unsigned index in dictionary [dict] to [value] (cell), +;;; and returns the resulting dictionary. +cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; +(cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; + +cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF"; +(cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF" "NULLSWAPIFNOT"; +(cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF" "NULLSWAPIFNOT"; +(cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF"; +(cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF"; +(cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL"; +(cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL"; +(slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT"; +(slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT"; +(cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; +(cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; +(cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; +(cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; +cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; +(cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; +cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; +(cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; +cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; +(cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; +(cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD"; +(cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE"; +(cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD"; +(cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE"; +cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; +(cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; +cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; +(cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; +cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; +(cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; +(cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB"; +(cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB"; +(cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB"; +(cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB"; +(cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; +(cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; +(cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; +(cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; +(cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; +(cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; +(cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; +(cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; +(cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; +(cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; +(cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; +(cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; +(int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2"; +(int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2"; +(int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2"; +(int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2"; +(int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2"; +(int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2"; +(int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2"; +(int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2"; +(int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2"; +(int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2"; +(int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2"; +(int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2"; +(int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2"; +(int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2"; +(int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2"; +(int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2"; + +;;; Creates an empty dictionary, which is actually a null value. Equivalent to PUSHNULL +cell new_dict() asm "NEWDICT"; +;;; Checks whether a dictionary is empty. Equivalent to cell_null?. +int dict_empty?(cell c) asm "DICTEMPTY"; + + +{- Prefix dictionary primitives -} +(slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2"; +(cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET"; +(cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL"; + +;;; Returns the value of the global configuration parameter with integer index `i` as a `cell` or `null` value. +cell config_param(int x) asm "CONFIGOPTPARAM"; +;;; Checks whether c is a null. Note, that FunC also has polymorphic null? built-in. +int cell_null?(cell c) asm "ISNULL"; + +;;; Creates an output action which would reserve exactly amount nanotoncoins (if mode = 0), at most amount nanotoncoins (if mode = 2), or all but amount nanotoncoins (if mode = 1 or mode = 3), from the remaining balance of the account. It is roughly equivalent to creating an outbound message carrying amount nanotoncoins (or b − amount nanotoncoins, where b is the remaining balance) to oneself, so that the subsequent output actions would not be able to spend more money than the remainder. Bit +2 in mode means that the external action does not fail if the specified amount cannot be reserved; instead, all remaining balance is reserved. Bit +8 in mode means `amount <- -amount` before performing any further actions. Bit +4 in mode means that amount is increased by the original balance of the current account (before the compute phase), including all extra currencies, before performing any other checks and actions. Currently, amount must be a non-negative integer, and mode must be in the range 0..15. +() raw_reserve(int amount, int mode) impure asm "RAWRESERVE"; +;;; Similar to raw_reserve, but also accepts a dictionary extra_amount (represented by a cell or null) with extra currencies. In this way currencies other than TonCoin can be reserved. +() raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX"; +;;; Sends a raw message contained in msg, which should contain a correctly serialized object Message X, with the only exception that the source address is allowed to have dummy value addr_none (to be automatically replaced with the current smart contract address), and ihr_fee, fwd_fee, created_lt and created_at fields can have arbitrary values (to be rewritten with correct values during the action phase of the current transaction). Integer parameter mode contains the flags. Currently mode = 0 is used for ordinary messages; mode = 128 is used for messages that are to carry all the remaining balance of the current smart contract (instead of the value originally indicated in the message); mode = 64 is used for messages that carry all the remaining value of the inbound message in addition to the value initially indicated in the new message (if bit 0 is not set, the gas fees are deducted from this amount); mode' = mode + 1 means that the sender wants to pay transfer fees separately; mode' = mode + 2 means that any errors arising while processing this message during the action phase should be ignored. Finally, mode' = mode + 32 means that the current account must be destroyed if its resulting balance is zero. This flag is usually employed together with +128. +() send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG"; +;;; Creates an output action that would change this smart contract code to that given by cell new_code. Notice that this change will take effect only after the successful termination of the current run of the smart contract +() set_code(cell new_code) impure asm "SETCODE"; + +;;; Generates a new pseudo-random unsigned 256-bit integer x. The algorithm is as follows: if r is the old value of the random seed, considered as a 32-byte array (by constructing the big-endian representation of an unsigned 256-bit integer), then its sha512(r) is computed; the first 32 bytes of this hash are stored as the new value r' of the random seed, and the remaining 32 bytes are returned as the next random value x. +int random() impure asm "RANDU256"; +;;; Generates a new pseudo-random integer z in the range 0..range−1 (or range..−1, if range < 0). More precisely, an unsigned random value x is generated as in random; then z := x * range / 2^256 is computed. +int rand(int range) impure asm "RAND"; +;;; Returns the current random seed as an unsigned 256-bit Integer. +int get_seed() impure asm "RANDSEED"; +;;; Sets the random seed to unsigned 256-bit seed. +() set_seed(int x) impure asm "SETRAND"; +;;; Mixes unsigned 256-bit integer x into the random seed r by setting the random seed to sha256 of the concatenation of two 32-byte strings: the first with the big-endian representation of the old seed r, and the second with the big-endian representation of x. +() randomize(int x) impure asm "ADDRAND"; +;;; Equivalent to randomize(cur_lt());. +() randomize_lt() impure asm "LTIME" "ADDRAND"; + +;;; Checks whether the data parts of two slices coinside +int equal_slice_bits(slice a, slice b) asm "SDEQ"; +int equal_slices(slice a, slice b) asm "SDEQ"; + +;;; Concatenates two builders +builder store_builder(builder to, builder from) asm "STBR"; + +int gas_consumed() asm "GASCONSUMED"; +int get_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEE"; +;;; Checks whether the data parts of two slices coinside +int equal_slices_bits(slice a, slice b) asm "SDEQ"; + +() dump_tuple_index3 (slice a, slice b, slice c) impure asm "dump_tuple_index3"; +int tlen(tuple t) asm "TLEN"; + +;;; Store standard uint32 operation code into `builder` [b] +builder store_op(builder b, int op) asm(op b) "32 STU"; +;;; Store standard uint64 query id into `builder` [b] +builder store_query_id(builder b, int query_id) asm(query_id b) "64 STU"; +;;; Load standard uint32 operation code from `slice` [s] +(slice, int) load_op(slice s) asm(-> 1 0) "32 LDU"; +;;; Load standard uint64 query id from `slice` [s] +(slice, int) load_query_id(slice s) asm(-> 1 0) "64 LDU"; + +int ext::addr_std_any_wc?(slice addr) asm "b{100} PUSHSLICE SDPPFXREV"; +int ext::is_on_same_workchain?(slice addr) asm "REWRITESTDADDR DROP MYADDR REWRITESTDADDR DROP EQUAL"; diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/operations/configure_operation.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/operations/configure_operation.fc new file mode 100644 index 0000000..14b7c6d --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/operations/configure_operation.fc @@ -0,0 +1,27 @@ +#include "../configuration.fc"; +#include "../constants/constants.fc"; +#include "../constants/logs.fc"; +#include "../imports/stdlib.fc"; +#include "../utils/tx-utils.fc"; + +(slice, cell) parse_configure_params(slice in_msg_body) impure { + slice pyth_address = in_msg_body~load_msg_addr(); + cell ids_map = in_msg_body~load_dict(); + + in_msg_body.end_parse(); + + return (pyth_address, ids_map); +} + +() process_configure(slice sender_address, slice pyth_address, cell ids_map) impure { + save_configuration(pyth_address, ids_map); + + emit_log_simple(begin_cell() + .store_uint(LOG::CONFIGURE, 8) + .store_slice(pyth_address) + .store_dict(ids_map) + .end_cell() + ); + + return_excesses(sender_address, makeTextBody("Configure success")); +} diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/operations/onchain_getter_operation.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/operations/onchain_getter_operation.fc new file mode 100644 index 0000000..1ef78b8 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/operations/onchain_getter_operation.fc @@ -0,0 +1,117 @@ +#include "../utils/pyth.fc"; +#include "../configuration.fc"; +#include "../constants/constants.fc"; +#include "../constants/errors.fc"; +#include "../constants/logs.fc"; +#include "../constants/op_codes.fc"; +#include "../imports/basic_types.fc"; +#include "../imports/stdlib.fc"; +#include "../utils/tx-utils.fc"; + +(slice, slice, int, cell) parse_onchain_getter_payload(cell payload) impure { + slice s = payload.begin_parse(); + slice initial_sender = s~load_msg_addr(); + slice my_jetton_wallet = s~load_msg_addr(); + int jetton_amount = s~load_uint(64); + cell operation_payload = s~load_ref(); + + return (initial_sender, my_jetton_wallet, jetton_amount, operation_payload); +} + +() handle_onchain_getter_failure(int query_id, int error_code, cell external_custom_payload) impure { + ;; return excesses and jettons to sender + (slice initial_sender, slice my_jetton_wallet, int jetton_amount, cell operation_payload) = parse_onchain_getter_payload(external_custom_payload); + + respond_send_jetton(my_jetton_wallet, initial_sender, query_id, jetton_amount, + begin_cell() + .store_uint(ERROR::ONCHAIN_GETTER_FAILURE, 32) + .store_query_id(query_id) + .store_uint(error_code, 32) + .store_ref(operation_payload) + .store_ref(makeTextBody("onchain_getter failed")) + .end_cell(), + 0 + ); +} + +(int, int, cell, cell, cell) parse_onchain_getter_jetton_params(slice in_msg_body) impure { + int publish_gap = in_msg_body~load_uint(64); + int max_staleness = in_msg_body~load_uint(64); + cell price_update = in_msg_body~load_ref(); + cell pyth_price_ids = in_msg_body~load_ref(); + cell external_custom_payload = in_msg_body~load_ref(); + in_msg_body.end_parse(); + + return (publish_gap, max_staleness, price_update, pyth_price_ids, external_custom_payload); +} + +() onchain_getter_jetton_request( + slice in_msg_body, int msg_value, + slice sender_address, slice initial_sender, int query_id, int jetton_amount +) impure { + try { + emit_log(LOG::PROCESS_ONCHAIN_GETTER_JETTON, begin_cell().store_ref(makeTextBody("process onchain_getter jetton")).end_cell()); + + (int publish_gap, int max_staleness, cell price_update, cell pyth_price_ids, cell operation_body) = parse_onchain_getter_jetton_params(in_msg_body); + + ;; Create custom payload with unique query ID to identify response + cell custom_payload = begin_cell() + .store_op(OP::CONNECTOR_ONCHAIN_GETTER_PROCESS) + .store_query_id(query_id) + .store_ref( + begin_cell() + .store_slice(initial_sender) + .store_slice(sender_address) + .store_amount(jetton_amount) + .store_ref(operation_body) + .end_cell() + ) + .end_cell(); + + request_price_feed(my_address(), publish_gap, max_staleness, price_update, pyth_price_ids, custom_payload); + } catch(_, error_code) { + respond_send_jetton( + sender_address, initial_sender, query_id, jetton_amount, + begin_cell() + .store_uint(ERROR::ONCHAIN_GETTER_FAILURE, 32) + .store_uint(error_code, 32) + .store_ref(makeTextBody("onchain_getter failure")) + .end_cell(), + 0 + ); + } +} + +() process_onchain_getter_operation(int query_id, slice initial_sender, slice jw_address, int jetton_amount, cell prices_dict, cell operation_body) impure { + ;; maybe do something + + ;; return excesses + send_message( + initial_sender, 0, + begin_cell() + .store_query_id(query_id) + .store_ref(makeTextBody("Thank you!")) + .end_cell(), + SENDMODE::CARRY_ALL_REMAINING_MESSAGE_VALUE + ); +} + +() onchain_getter_process(cell prices_dict, int query_id, cell operation_payload) impure { + + (slice initial_sender, slice jw_address, int jetton_amount, cell operation_body) = parse_onchain_getter_payload(operation_payload); + + ;; parse operation body + emit_log(LOG::CUSTOM_OPERATION_PROCESSING, + begin_cell() + .store_query_id(query_id) + .store_op_code(OP::CONNECTOR_ONCHAIN_GETTER_PROCESS) + .store_slice(initial_sender) + .store_slice(jw_address) + .store_amount(jetton_amount) + .store_dict(prices_dict) + .store_ref(operation_body) + .end_cell() + ); + + process_onchain_getter_operation(query_id, initial_sender, jw_address, jetton_amount, prices_dict, operation_body); +} diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/operations/proxy_operation.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/operations/proxy_operation.fc new file mode 100644 index 0000000..1298891 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/operations/proxy_operation.fc @@ -0,0 +1,37 @@ +#include "../constants/constants.fc"; +#include "../constants/errors.fc"; +#include "../constants/logs.fc"; +#include "../constants/op_codes.fc"; +#include "../imports/stdlib.fc"; +#include "../utils/tx-utils.fc"; + +(int) parse_proxy_operation_payload(cell operation_payload) impure inline { + slice s = operation_payload.begin_parse(); + + int transferred_amount = s~load_uint(64); + s.end_parse(); + + return (transferred_amount); +} + +;; todo: test, debug +() proxy_operation_process(int my_balance, int msg_value, slice initial_sender, cell prices_dict, int query_id, cell operation_payload) impure { + var (transferred_amount) = parse_proxy_operation_payload(operation_payload); + throw_unless(ERROR::NOT_ENOUGH_TRANSFERRED, msg_value < transferred_amount + FEE::SUPPLY_MARGIN); + + raw_reserve(my_balance + transferred_amount, RESERVE::REGULAR); + + emit_log(LOG::PROXY_OPERATION_PROCESSING, + begin_cell() + .store_query_id(query_id) + .store_op(OP::CONNECTOR_PROXY_OPERATION) + .store_ref(prices_dict) + .end_cell() + ); + + send_message( + initial_sender, 0, + begin_cell().store_query_id(query_id).store_ref(makeTextBody("Thank you!")).end_cell(), + SENDMODE::CARRY_ALL_BALANCE + ); +} diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/parse_price_feeds.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/parse_price_feeds.fc new file mode 100644 index 0000000..7f9192f --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/parse_price_feeds.fc @@ -0,0 +1,140 @@ +#include "constants/constants.fc"; +#include "constants/errors.fc"; +#include "imports/stdlib.fc"; +#include "utils/common.fc"; + +;; pack internal price data into a new slice +;; arguments: +;; internal_price: internal price value +;; timestamp: price timestamp +;; reurns a new slice containing serialized arguments +(slice) pack_prices_slice(int internal_price, int timestamp) impure inline { + return + begin_cell() + .store_uint(internal_price, 64) + .store_uint(timestamp, 64) + .end_cell().begin_parse(); +} + +;; find and unpack item of pyth_feeds=>internal_asset_ids map +;; arguments: +;; ids_map: the map to be looked through +;; pyth_feed_id: specified pyth feed id +;; returns: +;; internal_id: internal assset id +;; pyth_referred_feed_id: referred pyth feed id if provided, 0 (256 bit) otherwise +(int, int) unpack_price_map_item(cell ids_map, int pyth_feed_id) impure { + (slice ids_map_item_slice, int found?) = ids_map.udict_get?(256, pyth_feed_id); + throw_unless(ERROR::UNSUPPORTED_PRICE_ID, found?); + + int pyth_referred_feed_id = 0; + int internal_id = ids_map_item_slice~load_uint(256); ;; todo check : move to unpack + if (ids_map_item_slice.slice_bits() > 0) { + pyth_referred_feed_id = ids_map_item_slice~load_uint(256); + } + ids_map_item_slice.end_parse(); + + return (internal_id, pyth_referred_feed_id); +} + +;; parse pyth price data +;; arguments: +;; price_feeds_cell: data cell received from pyth +;; returns: +;; pyth_feeds_dict: dict: pyth_feed_id=>feed_data +;; pyth_ids_tuple: tuple of pyth feeds in the dict +(cell, tuple) parse_pyth_price_data(cell price_feeds_cell) impure { + cell pyth_feeds_dict = new_dict(); + tuple pyth_ids_tuple = empty_tuple(); + + while (~ null?(price_feeds_cell)) { + slice price_feeds_slice = price_feeds_cell.begin_parse(); + int pyth_price_feed_id = price_feeds_slice~load_uint(256); + cell price_data = price_feeds_slice~load_ref(); + slice price_data_slice = price_data.begin_parse(); + cell current_price_cell = price_data_slice~load_ref(); + + pyth_feeds_dict~udict_set(256, pyth_price_feed_id, current_price_cell.begin_parse()); + pyth_ids_tuple~tpush(pyth_price_feed_id); + + if (price_feeds_slice.slice_refs_empty?()) { + price_feeds_cell = null(); + } else { + price_feeds_cell = price_feeds_slice~load_ref(); + } + } + + return (pyth_feeds_dict, pyth_ids_tuple); +} + +;; parse pyth data item slice +;; returns: +;; found?: flag - true if specified feed is found, false otherwise +;; price: pyth price value +;; conf: conf +;; scale:: price scale value +;; timestamp: price timestamp +(int, int, int, int, int) unpack_pyth_data_item(cell pyth_feeds_dict, int pyth_feed) impure { + (slice current_price_slice, int found?) = pyth_feeds_dict.udict_get?(256, pyth_feed); + if (~ found?) { + return (0, 0, 0, 0, 0); + } + + int price = current_price_slice~load_int(64); + int conf = current_price_slice~load_uint(64); + int expo = current_price_slice~load_int(32); + int timestamp = current_price_slice~load_uint(64); + int scale = fast_dec_pow(0 - expo); + current_price_slice.end_parse(); + + return (found?, price, conf, scale, timestamp); +} + +;; calculate referred() price based on original assset price and referred +int calculate_referred_price(int price_original, int scale_original, int price_referred, int scale_referred) impure inline { + return price_referred * CONST::INTERNAL_PRICE_SCALE / scale_referred * price_original / scale_original; +} + +;; calculate internal asset price based on pyth feed id +;; arguments: +;; pyth_feeds_dict: dict: pyth_feed_id => pyth_feed_data +;; pyth_original_id: specified pyth feed_id +;; ids_map: map(dict): pyth_feed_id => [internal_asset_id, referred_pyth_id | 0] +(int, int, int) calculate_internal_price(cell pyth_feeds_dict, int pyth_original_id, cell ids_map) impure { + (int found_original?, int price_original, _, int scale_original, int timestamp_original) = pyth_feeds_dict.unpack_pyth_data_item(pyth_original_id); + ;; throw_unless(error::price_not_actual, check_price_actual(timestamp_original)); + + throw_unless(ERROR::NOT_SAVED_PRICE_ID, found_original?); + + (int internal_id, int pyth_referred_id) = ids_map.unpack_price_map_item(pyth_original_id); + if (~ pyth_referred_id) { + int internal_price = muldiv(price_original, CONST::INTERNAL_PRICE_SCALE, scale_original); ;; todo: maybe use muldivr/muldivc? + return (internal_id, internal_price, timestamp_original); + } + + (int found_referred?, int price_referred, _, int scale_referred, int timestamp_referred) = pyth_feeds_dict.unpack_pyth_data_item(pyth_referred_id); + throw_unless(ERROR::REFERRED_PRICE_ID_NOT_FOUND, found_referred?); + int timestamp = min(timestamp_original, timestamp_referred); + int internal_price = calculate_referred_price(price_original, scale_original, price_referred, scale_referred); + + return (internal_id, internal_price, timestamp); +} + +;; Parse pyth prices feeds and translate it to internal asset_id => internal_price dict +(cell, int) parse_price_feeds(cell price_feeds_cell, cell ids_map) impure { + cell prices_dict = new_dict(); + + ;; we need to parse all data first, to have ability to access any item later, + ;; because some of them require other arbitrary items + (cell pyth_feeds_dict, tuple pyth_ids_tuple) = parse_pyth_price_data(price_feeds_cell); + + int count = 0; + while (count < pyth_ids_tuple.tlen()) { + int pyth_original_id = pyth_ids_tuple.at(count); + count += 1; + (int internal_id, int internal_price, int timestamp) = pyth_feeds_dict.calculate_internal_price(pyth_original_id, ids_map); + prices_dict~udict_set(256, internal_id, pack_prices_slice(internal_price, timestamp)); + } + + return (prices_dict, pyth_ids_tuple.tlen()); +} diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/pyth_connector.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/pyth_connector.fc new file mode 100644 index 0000000..9fca155 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/pyth_connector.fc @@ -0,0 +1,150 @@ +#include "imports/basic_types.fc"; +#include "imports/stdlib.fc"; + +#include "configuration.fc"; +#include "operations/configure_operation.fc"; +#include "constants/constants.fc"; +#include "constants/errors.fc"; +#include "utils/common.fc"; +#include "utils/tx-utils.fc"; +#include "getters.fc"; +#include "parse_price_feeds.fc"; +#include "constants/op_codes.fc"; +#include "operations/onchain_getter_operation.fc"; +#include "operations/proxy_operation.fc"; +#include "utils/pyth.fc"; + +;; main message handler +() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { + return (); + } + + ;; get sender address from message + slice cs = in_msg_full.begin_parse(); + cs~skip_bits(4); ;; skip flags + slice sender_address = cs~load_msg_addr(); ;; load sender address + + int op = in_msg_body~load_op(); + + ;; cannot read query_id yet, because pyth error response doesn't have it + if (op == OP::PYTH_RESPONSE_ERROR) { + (slice pyth_address, _) = load_configuration(); + throw_unless(ERROR::NOT_AUTHORIZED, equal_msg_addr?(sender_address, pyth_address)); + + ;; it's what pyth returns in case of error + (int error_code, int operation_code, cell custom_payload) = parse_pyth_response_error(in_msg_body); + ;; maybe try/catch and log + + ;; if pyth op code don't match, we have an error in our contract + ifnot (operation_code == OP::PYTH_PARSE_PRICE_FEED_UPDATES) { ;; don't throw to keep the coins + emit_log(LOG::UNSUPPORTED_FEEDS_OPERATION, makeTextBody("Unsupported feeds operation")); + return (); + } + + ;; handle pyth error response + ;; custom_payload was composed by this contract, so we parse it + (int op_code, int query_id, cell external_custom_payload) = parse_custom_payload(custom_payload); + + ;; this contract supposes only one operation as onchain-getter + if (op_code == OP::CONNECTOR_ONCHAIN_GETTER_PROCESS) { + handle_onchain_getter_failure(query_id, error_code, external_custom_payload); + return (); + } + + ;; don't throw, because it bounces the value back to pyth + emit_log(LOG::UNSUPPORTED_OPERATION_FROM_PYTH, makeTextBody("Unsupported operation from pyth")); + + return (); + } + + if (op == OP::CONNECTOR_CONFIGURE) { + (slice pyth_address, cell ids_map) = parse_configure_params(in_msg_body); + ;; todo: add owner address to config + ;; (slice pyth_address, cell ids_map, cell tokens_keys) = load_configuration(); + ;; todo check sender is owner + ;; throw_unless(error::unauthorized, equal_msg_addr?(sender_address, owner_address)); + + process_configure(sender_address, pyth_address, ids_map); + + return (); + } + + if (op == OP::CONNECTOR_CUSTOM_OPERATION) { + throw(ERROR::NOT_IMPLEMENTED); + } + + ;; we received response from pyth oracle contract, handle it + if (op == OP::PYTH_PARSE_PRICE_FEED_UPDATES) { + (slice pyth_address, cell ids_map) = load_configuration(); + throw_unless(ERROR::NOT_AUTHORIZED, equal_msg_addr?(sender_address, pyth_address)); + + (_, cell price_feeds_cell, slice initial_sender, cell pyth_payload) = parse_price_feed_updates_response(in_msg_body); + + (cell prices_dict, int num_unique_price_feeds) = price_feeds_cell.parse_price_feeds(ids_map); + ;; it can throw only when there are repeating feeds + ;; throw_unless(error::incorrect_price_feeds_num, (num_price_feeds == actual_price_feeds)); + + (int op_code, int query_id, cell operation_payload) = parse_operation_payload(pyth_payload); + + if (op_code == OP::CONNECTOR_ONCHAIN_GETTER_PROCESS) { + throw_unless(ERROR::NOT_AUTHORIZED, equal_msg_addr?(initial_sender, my_address())); + + onchain_getter_process(prices_dict, query_id, operation_payload); + return (); + } + + if (op_code == OP::CONNECTOR_PROXY_OPERATION) { + proxy_operation_process(my_balance, msg_value, initial_sender, prices_dict, query_id, operation_payload); + return (); + } + + throw(ERROR::UNSUPPORTED_OPERATION); + + } + + int query_id = in_msg_body~load_query_id(); + ;; var (wc, addr_hash) = parse_std_addr(sender_address); + + ;; part of onchain-getter pattern + if (op == OP::JETTON_TRANSFER_NOTIFICATION) { + int jetton_amount = in_msg_body~load_coins(); + slice initial_sender = in_msg_body~load_msg_addr(); + + ;; (slice pyth_address, cell ids_map) = load_configuration(); + + ;; todo in a real sc it is crucial to check token is allowed + ;; (_, int f) = tokens_keys.udict_get?(256, addr_hash); + ;; throw_unless(error::received_unsupported_jetton, f); + + int load_ref = in_msg_body~load_int(1); + if (load_ref) { + in_msg_body = in_msg_body.preload_ref().begin_parse(); + } + + int jetton_op_code = in_msg_body~load_uint(32); + + if (jetton_op_code == OP::CONNECTOR_ONCHAIN_GETTER_REQUEST) { + onchain_getter_jetton_request(in_msg_body, msg_value, sender_address, initial_sender, query_id, jetton_amount); + return (); + } + + respond_send_jetton( + sender_address, initial_sender, query_id, jetton_amount, + begin_cell() + .store_uint(ERROR::UNKNOWN_OPERATION, 32) + .store_ref(makeTextBody("Unknown jetton operation")) + .end_cell(), + 0 + ); + + return (); + } + + if (op == OP::JETTON_EXCESSES) { + ;; note Just accept TON excesses after sending jettons + return (); + } + + throw(0xffff); ;; Throw on unknown op +} diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/utils/common.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/utils/common.fc new file mode 100644 index 0000000..170962a --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/utils/common.fc @@ -0,0 +1,50 @@ +#include "../imports/stdlib.fc"; +tuple tuple_of_13_ints([int, int, int, int, int, int, int, int, int, int, int, int, int] specific_tuple) asm "NOP"; + +int fast_dec_pow(int e) impure { + var t = tuple_of_13_ints([ + 1, ;; 0 + 10, + 100, + 1000, + 10000, + 100000, ;; 5 + 1000000, + 10000000, + 100000000, + 1000000000, + 10000000000, ;; 10 + 100000000000, + 1000000000000 ;; 12 + ]); + return t.at(e); +} + +() dump_tuple(tuple t) impure inline { + if (null?(t)) { + return (); + } + + int len = t.tlen(); + int i = 0; + while (i < len) { + int x = t.at(i); + x~dump(); + i += 1; + } + + return (); +} + +(int) is_valid_address?(slice address) inline { + ifnot (ext::addr_std_any_wc?(address)) { + return false; + } + return ext::is_on_same_workchain?(address); +} + +int equal_msg_addr?(slice addr1, slice addr2) impure inline { + var (wc1, hash1) = parse_std_addr(addr1); + var (wc2, hash2) = parse_std_addr(addr2); + return (wc1 == wc2) & (hash1 == hash2); +} \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/utils/pyth.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/utils/pyth.fc new file mode 100644 index 0000000..60961bc --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/utils/pyth.fc @@ -0,0 +1,76 @@ +;; #include "onchain_getter_operation.fc"; +#include "../configuration.fc"; +#include "../constants/logs.fc"; +#include "../imports/stdlib.fc"; +#include "tx-utils.fc"; + +;; this function only required for onchain-getter type operation +() request_price_feed( + slice response_address, + int publish_gap, + int max_staleness, + cell price_update_data, ;; pyth price update data + cell pyth_price_ids, ;; required pyth price ids + cell custom_payload;; custom payload to forward from pyth +) impure { + (slice pyth_address, _) = load_configuration(); + + ;; create message to pyth contract according to schema + int min_publish_time = now() - publish_gap; + int max_publish_time = now() + max_staleness; + + cell msg = begin_cell() + .store_uint(0x18, 6) ;; nobounce + .store_slice(pyth_address) ;; pyth contract address + .store_coins(0) ;; forward amount will be filled automatically (mode 64 + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers + .store_uint(OP::PYTH_PARSE_PRICE_FEED_UPDATES, 32) ;; pyth opcode + .store_ref(price_update_data) ;; update data + .store_ref(pyth_price_ids) ;; price feed IDs + .store_uint(min_publish_time, 64) ;; min_publish_time + .store_uint(max_publish_time, 64) ;; max_publush_time + .store_slice(response_address) ;; target address (this contract) + .store_ref(custom_payload) ;; custom payload with recipient and amount + .end_cell(); + + send_raw_message(msg, SENDMODE::CARRY_ALL_REMAINING_MESSAGE_VALUE); +} + +(int, cell, slice, cell) parse_price_feed_updates_response(slice response) impure { + int num_price_feeds = response~load_uint(8); ;; can be ignored + cell price_feeds_cell = response~load_ref(); + slice initial_sender = response~load_msg_addr(); + cell custom_payload = response~load_ref(); + response.end_parse(); + + return (num_price_feeds, price_feeds_cell, initial_sender, custom_payload); +} + +(int, int, cell) parse_custom_payload(cell custom_payload) impure { + slice s = custom_payload.begin_parse(); + + int op_code = s~load_op(); + int query_id = s~load_query_id(); + cell external_custom_payload = s~load_ref(); + + s.end_parse(); + + return (op_code, query_id, external_custom_payload); +} + +(int, int, cell) parse_pyth_response_error(slice s) impure { + int error_code = s~load_uint(32); + int operation_code = s~load_uint(32); + cell custom_payload = s~load_ref(); + s.end_parse(); + return (error_code, operation_code, custom_payload); +} + +(int, int, cell) parse_operation_payload(cell payload) impure { + slice s = payload.begin_parse(); + int op_code = s~load_op(); + int query_id = s~load_query_id(); + cell operation_payload = s~load_ref(); + + return (op_code, query_id, operation_payload); +} \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/contracts/PythConnector/utils/tx-utils.fc b/price_feeds/ton/pyth-connector/contracts/PythConnector/utils/tx-utils.fc new file mode 100644 index 0000000..ea3cd66 --- /dev/null +++ b/price_feeds/ton/pyth-connector/contracts/PythConnector/utils/tx-utils.fc @@ -0,0 +1,107 @@ +#include "../constants/constants.fc"; +#include "../constants/op_codes.fc"; +#include "../imports/stdlib.fc"; + +() emit_log_simple (cell data) impure inline { + var msg = begin_cell() + .store_uint(12, 4) ;; ext_out_msg_info$11 src:MsgAddressInt () + .store_uint(1, 2) ;; addr_extern$01 + .store_uint(256, 9) ;; len:(## 9) + .store_uint(0, 256) ;; external_address:(bits len) + .store_uint(1, 64 + 32 + 2) ;; created_lt, created_at, init:Maybe, body:Either + .store_ref(data) + .end_cell(); + send_raw_message(msg, 0); +} + +() emit_log(int status, cell body) impure inline { + emit_log_simple( + begin_cell() + .store_uint(status, 8) + .store_ref(body) + .end_cell() + ); +} + +(cell) makeTextBody(slice text) impure inline { + return begin_cell().store_uint(0, 32).store_slice(text).end_cell(); +} + +(cell) makeIntBody(int value, int bits) impure inline { + return begin_cell().store_uint(value, bits).end_cell(); +} + +() send_message( + slice to_address, int nano_ton_amount, + cell content, int mode +) impure { + var msg = begin_cell() + .store_uint(0x10, 6) + .store_slice(to_address) + .store_grams(nano_ton_amount) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1) + .store_maybe_ref(content); ;; body:(Either X ^X) + + send_raw_message(msg.end_cell(), mode); +} + +() send_jetton( + slice my_jetton_wallet_address, + slice to_address, + int query_id, int amount, + int nano_ton_attachment, cell body, int mode +) impure { + send_message( + my_jetton_wallet_address, + 0, ;; because we using mode 128 +raw_reserve everywhere we dont need ton amount here + begin_cell() + .store_uint(OP::JETTON_TRANSFER, 32) + .store_uint(query_id, 64) + .store_grams(amount) ;; jetton amount + .store_slice(to_address) ;; new owner + .store_slice(to_address) ;; response_destination -> refund excess fees to the owner + .store_maybe_ref(body) ;; custom_response_payload + .store_grams(nano_ton_attachment) ;; minimum nano-TON amount to send transfer_notification + ;;.store_bool(false) ;; forward_payload + .store_maybe_ref(body) ;; custom_response_payload + .end_cell(), + mode ;; send mode + ); +} + +;; Carries all the remaining TON balance +() respond_send_jetton( + slice my_jetton_wallet_address, + slice to_address, + int query_id, int amount, + cell body, int forward_ton_amount +) impure { + send_jetton( + my_jetton_wallet_address, + to_address, + query_id, amount, + forward_ton_amount, ;; nanotons + body, + SENDMODE::CARRY_ALL_REMAINING_MESSAGE_VALUE + ); +} + +() reserve_and_send_rest( + int nano_ton_amount_to_reserve, + slice to_address, cell content +) impure { + raw_reserve(nano_ton_amount_to_reserve, RESERVE::REGULAR); + send_message(to_address, 0, content, SENDMODE::CARRY_ALL_BALANCE); +} + +() try_reserve_and_send_rest( + int nano_ton_amount_to_reserve, + slice to_address, cell content +) impure { + raw_reserve(nano_ton_amount_to_reserve, RESERVE::AT_MOST); + send_message(to_address, 0, content, SENDMODE::CARRY_ALL_BALANCE + 2); +} + +() return_excesses(slice sender_address, cell body) impure { + send_message(sender_address, 0, body, SENDMODE::CARRY_ALL_REMAINING_MESSAGE_VALUE); +} diff --git a/price_feeds/ton/pyth-connector/include/imported.ts b/price_feeds/ton/pyth-connector/include/imported.ts new file mode 100644 index 0000000..e29ae6f --- /dev/null +++ b/price_feeds/ton/pyth-connector/include/imported.ts @@ -0,0 +1,5 @@ +/** A data source is a wormhole emitter, i.e., a specific contract on a specific chain. */ +export interface DataSource { + emitterChain: number; + emitterAddress: string; +} \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/index.ts b/price_feeds/ton/pyth-connector/index.ts new file mode 100644 index 0000000..410d2f8 --- /dev/null +++ b/price_feeds/ton/pyth-connector/index.ts @@ -0,0 +1,3 @@ +export {deployPythConnector, deployAndConfigurePyth} from "./tests/utils/deploy"; +export {Main} from "./wrappers/Main"; +export {BaseWrapper} from "./wrappers/BaseWrapper"; \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/jest.config.ts b/price_feeds/ton/pyth-connector/jest.config.ts new file mode 100644 index 0000000..3ab357b --- /dev/null +++ b/price_feeds/ton/pyth-connector/jest.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "jest"; + +const config: Config = { + preset: "ts-jest", + testEnvironment: "node", + testPathIgnorePatterns: ["/node_modules/", "/dist/"], + maxWorkers: 1, // Prevents serialization issues with BigInt during error reporting +}; + +export default config; diff --git a/price_feeds/ton/pyth-connector/package.json b/price_feeds/ton/pyth-connector/package.json new file mode 100644 index 0000000..0d25cf4 --- /dev/null +++ b/price_feeds/ton/pyth-connector/package.json @@ -0,0 +1,49 @@ +{ + "name": "@evaafi/pyth-connector", + "version": "0.0.1", + "description": "Pyth connector example module with tests", + "main": "dist/index.js", + "files": [ + "dist", "build" + ], + "private": true, + "packageManager": "yarn@1.22.22", + "engines": { + "node": "^22.11.0" + }, + "scripts": { + "start": "blueprint run", + "contracts": "blueprint build Main && blueprint build MainNoCheck && blueprint build PythConnector && blueprint build MainNoCheck", + "build": "tsc --declaration", + "test:unit": "jest --verbose", + "test:format": "prettier --check .", + "fix:format": "prettier --write ." + }, + "devDependencies": { + "@pythnetwork/hermes-client": "^2.0.0", + "@pythnetwork/price-service-sdk": "^1.8.0", + "@pythnetwork/pyth-ton-js": "^0.1.2", + "@tact-lang/compiler": "^1.6.10", + "@ton-community/assets-sdk": "^0.0.5", + "@ton-community/func-js": "^0.9.1", + "@ton/blueprint": "^0.28.0", + "@ton/core": "0.56.0", + "@ton/crypto": "3.3.0", + "@ton/sandbox": "^0.26.0", + "@ton/test-utils": "^0.5.0", + "@ton/tolk-js": "^0.8.0", + "@ton/ton": "14.0.0", + "@tonconnect/sdk": "^3.0.6", + "@types/crypto-js": "^4.2.2", + "@types/jest": "^29.5.12", + "@types/node": "^20.14.10", + "@wormhole-foundation/sdk-definitions": "^0.10.7", + "crypto-js": "^4.2.0", + "dotenv": "^16.4.7", + "jest": "^29.7.0", + "prettier": "^3.5.3", + "ts-jest": "^29.2.0", + "ts-node": "^10.9.2", + "typescript": "^5.8.2" + } +} diff --git a/price_feeds/ton/pyth-connector/scripts/deployPyth.ts b/price_feeds/ton/pyth-connector/scripts/deployPyth.ts new file mode 100644 index 0000000..83cf273 --- /dev/null +++ b/price_feeds/ton/pyth-connector/scripts/deployPyth.ts @@ -0,0 +1,134 @@ +import { toNano } from "@ton/core"; +import { MainConfig } from "../wrappers/Main"; +import { compile, NetworkProvider, sleep } from "@ton/blueprint"; +import { DataSource } from "../include/imported"; +import { HermesClient } from "@pythnetwork/hermes-client"; +import { Main } from "../wrappers/Main"; +import { + GOVERNANCE_DATA_SOURCE, + GUARDIAN_SET_0, + MAINNET_UPGRADE_VAAS, +} from "../tests/utils/wormhole"; +import { BTC_PRICE_FEED_ID, ETH_PRICE_FEED_ID } from "../tests/utils/pyth"; +import { calculateUpdatePriceFeedsFee } from "@pythnetwork/pyth-ton-js"; + +export async function run(provider: NetworkProvider) { + const SINGLE_UPDATE_FEE = 1; + const DATA_SOURCES: DataSource[] = [ + { + emitterChain: 26, + emitterAddress: + "e101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71", + }, + ]; + + // Require CHAIN_ID environment variable + if (!process.env.CHAIN_ID) { + throw new Error( + "CHAIN_ID environment variable is required. Example usage: CHAIN_ID=2 npx blueprint run ...", + ); + } + + const chainId = parseInt(process.env.CHAIN_ID, 10); + + // Validate that chainId is a valid number + if (isNaN(chainId)) { + throw new Error("CHAIN_ID must be a valid number"); + } + + console.log("Chain ID:", chainId); + + const config: MainConfig = { + singleUpdateFee: SINGLE_UPDATE_FEE, + dataSources: DATA_SOURCES, + guardianSetIndex: 0, + guardianSet: GUARDIAN_SET_0, + chainId, + governanceChainId: 1, + governanceContract: + "0000000000000000000000000000000000000000000000000000000000000004", + governanceDataSource: GOVERNANCE_DATA_SOURCE, + upgradeCodeHash: 0, + }; + + const main = provider.open( + Main.createFromConfig(config, await compile("Main")), + ); + + await main.sendDeploy(provider.sender(), toNano("0.005")); + + await provider.waitForDeploy(main.address); + + console.log("Main contract deployed at:", main.address.toString()); + + // Call sendUpdateGuardianSet for each VAA + const currentGuardianSetIndex = await main.getCurrentGuardianSetIndex(); + console.log(`Current guardian set index: ${currentGuardianSetIndex}`); + + for (let i = currentGuardianSetIndex; i < MAINNET_UPGRADE_VAAS.length; i++) { + const vaa = MAINNET_UPGRADE_VAAS[i]; + const vaaBuffer = Buffer.from(vaa, "hex"); + await main.sendUpdateGuardianSet(provider.sender(), vaaBuffer); + console.log(`Successfully updated guardian set ${i + 1} with VAA: ${vaa.slice(0, 20)}...`); + + // Wait for 30 seconds before checking the guardian set index + console.log("Waiting for 30 seconds before checking guardian set index..."); + await sleep(30000); + + // Verify the update + const newIndex = await main.getCurrentGuardianSetIndex(); + if (newIndex !== i + 1) { + console.error(`Failed to update guardian set. Expected index ${i + 1}, got ${newIndex}`); + break; + } + } + + console.log("Guardian set update process completed."); + + // Initialize HermesClient + const hermesEndpoint = "https://hermes.pyth.network"; + const hermesClient = new HermesClient(hermesEndpoint); + + // Fetch latest price updates for BTC and ETH + const priceIds = [BTC_PRICE_FEED_ID, ETH_PRICE_FEED_ID]; + const latestPriceUpdates = await hermesClient.getLatestPriceUpdates( + priceIds, + { encoding: "hex" }, + ); + console.log("Hermes BTC price:", latestPriceUpdates.parsed?.[0].price); + console.log("Hermes ETH price:", latestPriceUpdates.parsed?.[1].price); + + // Combine updates into a single buffer + const updateData = Buffer.from(latestPriceUpdates.binary.data[0], "hex"); + console.log("Update data:", latestPriceUpdates.binary.data[0]); + + const singleUpdateFee = await main.getSingleUpdateFee(); + console.log("Single update fee:", singleUpdateFee); + + // NOTE: As of 2024/10/14 There's a bug with TON Access (https://ton.access.orbs.network) RPC service where if you provide an update data buffer with length of more than ~320 then the rpc returns error 404 and the function fails + const updateFee = await main.getUpdateFee(updateData); + + const totalFee = + calculateUpdatePriceFeedsFee(BigInt(updateFee)) + BigInt(updateFee); + + const result = await main.sendUpdatePriceFeeds( + provider.sender(), + updateData, + totalFee, + ); + console.log("Price feeds updated successfully."); + + console.log("Waiting for 30 seconds before checking price feeds..."); + await sleep(30000); + + // Query updated price feeds + const btcPrice = await main.getPriceUnsafe(BTC_PRICE_FEED_ID); + console.log( + `Updated BTC price: ${btcPrice.price}, publish time: ${btcPrice.publishTime}`, + ); + + const ethPrice = await main.getPriceUnsafe(ETH_PRICE_FEED_ID); + console.log( + `Updated ETH price: ${ethPrice.price}, publish time: ${ethPrice.publishTime}`, + ); +} diff --git a/price_feeds/ton/pyth-connector/scripts/deployPythConnector.ts b/price_feeds/ton/pyth-connector/scripts/deployPythConnector.ts new file mode 100644 index 0000000..76189f5 --- /dev/null +++ b/price_feeds/ton/pyth-connector/scripts/deployPythConnector.ts @@ -0,0 +1,33 @@ +import { toNano, Address } from '@ton/core'; +import {makeEmptyIds, PythConnector} from '../wrappers/PythConnector'; +import { compile, NetworkProvider } from '@ton/blueprint'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +export async function run(provider: NetworkProvider) { + const PYTH_CONTRACT_ADDRESS = Address.parse(process.env.PYTH_CONTRACT_ADDRESS!); + + // Compile the contract + const pythConnector = provider.open( + PythConnector.createFromConfig( + { + pythAddress: PYTH_CONTRACT_ADDRESS, + ids: makeEmptyIds(), + }, + await compile('PythConnector'), + ), + ); + + // Deploy contract + const deployAmount = toNano('0.001'); + await pythConnector.sendDeploy(provider.sender(), deployAmount); + + // Get the contract address + const address = pythConnector.address; + console.log('Deploy transaction sent, waiting for confirmation...'); + + // Wait for deployment + await provider.waitForDeploy(pythConnector.address); + console.log(`PYTH_CONNECTOR_CONTRACT_ADDRESS="${address.toString()}"`); +} diff --git a/price_feeds/ton/pyth-connector/tests/Main.spec.ts b/price_feeds/ton/pyth-connector/tests/Main.spec.ts new file mode 100644 index 0000000..39f360c --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/Main.spec.ts @@ -0,0 +1,57 @@ +import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; +import { Cell, toNano } from "@ton/core"; +import { Main, MainConfig } from "../wrappers/Main"; +import "@ton/test-utils"; +import { compile } from "@ton/blueprint"; + +describe("Main", () => { + let code: Cell; + + beforeAll(async () => { + code = await compile("Main"); + }); + + let blockchain: Blockchain; + let deployer: SandboxContract; + let main: SandboxContract
; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + const config: MainConfig = { + singleUpdateFee: 0, + dataSources: [], + guardianSetIndex: 0, + guardianSet: [], + chainId: 0, + governanceChainId: 0, + governanceContract: + "0000000000000000000000000000000000000000000000000000000000000000", + governanceDataSource: { + emitterChain: 0, + emitterAddress: + "0000000000000000000000000000000000000000000000000000000000000000", + }, + }; + + main = blockchain.openContract(Main.createFromConfig(config, code)); + + deployer = await blockchain.treasury("deployer"); + + const deployResult = await main.sendDeploy( + deployer.getSender(), + toNano("0.05"), + ); + + expect(deployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: main.address, + deploy: true, + success: true, + }); + }); + + it("should deploy", async () => { + // the check is done inside beforeEach + // blockchain and main are ready to use + }); +}); diff --git a/price_feeds/ton/pyth-connector/tests/PythConnector.spec.ts b/price_feeds/ton/pyth-connector/tests/PythConnector.spec.ts new file mode 100644 index 0000000..430bf02 --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/PythConnector.spec.ts @@ -0,0 +1,260 @@ +import { Blockchain, EventAccountCreated, SandboxContract, TreasuryContract } from '@ton/sandbox'; +import { Address, beginCell, Cell, Dictionary, toNano } from '@ton/core'; +import { PythConnector } from '../wrappers/PythConnector'; +import '@ton/test-utils'; + +import * as dotenv from 'dotenv'; +import { deployAndConfigurePyth, deployJettonMinter, deployPythConnector, mintJettons } from "./utils/deploy"; +import { Main } from "../wrappers/Main"; +import { compile } from "@ton/blueprint"; + +import { composeFeedsCell, expectCompareDicts, getPriceUpdates, sendJetton } from "./utils/utils"; +import { Buffer } from "buffer"; +import { TreasuryParams } from "@ton/sandbox/dist/blockchain/Blockchain"; +import { + PYTH_TON_PRICE_FEED_ID, PYTH_USDC_PRICE_FEED_ID, PYTH_USDT_PRICE_FEED_ID, + TEST_FEED_NAMES, TEST_FEEDS, TEST_FEEDS_MAP +} from "./utils/assets"; +import { packNamedPrices } from "./utils/prices"; +import { makeOnchainGetterPayload, makeTransferMessage } from "./utils/messages"; +import { EventMessageSent } from "@ton/sandbox/dist/event/Event"; + +dotenv.config(); + +const HERMES_ENDPOINT = 'https://hermes.pyth.network'; + +// todo: check gas fees +export type OraclesInfo = { + pythAddress: Address; + feedsMap: Dictionary, + // pricesTtl: number, + // pythComputeBaseGas: bigint, + // pythComputePerUpdateGas: bigint, + // pythSingleUpdateFee: bigint, +}; + +describe('Generated Prices', () => { + let blockchain: Blockchain; + let deployer: SandboxContract; + let pyth: SandboxContract
; + let pythConnector: SandboxContract; + + let treasuries: Map>; + let addressSeedMap: Map; + + let aliceWallet: SandboxContract; + let bobWallet: SandboxContract; + + beforeAll(async () => { + await compile('MainNoCheck'); + await compile('PythConnector'); + }) + + beforeEach(async () => { + blockchain = await Blockchain.create(); + blockchain.now = Math.floor(Date.now() / 1000); + deployer = await blockchain.treasury('deployer'); + pyth = await deployAndConfigurePyth(blockchain, deployer, {noCheck: true, shouldBuild: true}); + pythConnector = await deployPythConnector(blockchain, deployer, pyth.address); + treasuries = new Map>(); + addressSeedMap = new Map(); + aliceWallet = await treasury('Alice', {predeploy: true}); + bobWallet = await treasury('Bob', {predeploy: true}); + }); + + const treasury = async (seed: string, params?: TreasuryParams) => { + const r = await blockchain.treasury(seed, params); + addressSeedMap.set(r.address.toString(), seed); + treasuries.set(r.address.toString(), r); + return r; + }; + + it('should accept generated prices', async () => { + const timeGetter = () => blockchain.now!; + const packedPrices = packNamedPrices({TON: 3.1, USDT: 0.99, USDC: 1.01}, timeGetter); + + const actorWallets = [aliceWallet]; + + const assetName = 'USDT'; + let jettonMinter = await deployJettonMinter(assetName, blockchain, deployer); + await mintJettons({actorWallets, jettonMinter, deployer}); + + await pythConnector.sendConfigure(deployer.getSender(), { + value: toNano('0.05'), + pythAddress: pyth.address, + feedsMap: TEST_FEEDS_MAP + }); + + const queryId: number = 0x12345; + const value: bigint = toNano('1.5'); + const updateDataCell = packedPrices; + let pythPriceIds = composeFeedsCell([PYTH_TON_PRICE_FEED_ID, PYTH_USDC_PRICE_FEED_ID, PYTH_USDT_PRICE_FEED_ID]); + const publishTimeGap = 10; + const maxStaleness = 180; + + const operationBody = makeOnchainGetterPayload({ + publishTimeGap, maxStaleness, pythPriceIds, updateDataCell, + operationBody: beginCell().endCell() + }); + + const transferMessage = makeTransferMessage({ + queryId: 12345n, + jettonAmount: 1_000_000_000n, + payloadDestination: pythConnector.address, + sender: aliceWallet.address, + notificationBody: operationBody, + forwardAmount: toNano('1'), + }); + + const res = await sendJetton(jettonMinter, aliceWallet, transferMessage, value); + console.log(res.events, res.externals); + }) +}); + +describe('PythConnector', () => { + let blockchain: Blockchain; + let deployer: SandboxContract; + let pyth: SandboxContract
; + let oraclesInfo: OraclesInfo; + let pythConnector: SandboxContract; + + let treasuries: Map>; + let addressSeedMap: Map; + + let aliceWallet: SandboxContract; + let bobWallet: SandboxContract; + + beforeAll(async () => { + await compile('Main'); + await compile('PythConnector'); + }) + + beforeEach(async () => { + blockchain = await Blockchain.create(); + blockchain.now = Math.floor(Date.now() / 1000); + deployer = await blockchain.treasury('deployer'); + pyth = await deployAndConfigurePyth(blockchain, deployer, {noCheck: true, shouldBuild: true}); + oraclesInfo = { + feedsMap: TEST_FEEDS_MAP, + pythAddress: pyth.address, + // pricesTtl: 180, + // pythComputeBaseGas: 100n, + // pythComputePerUpdateGas: 200n, + // pythSingleUpdateFee: 300n, + } as OraclesInfo; + + pythConnector = await deployPythConnector(blockchain, deployer, pyth.address); + treasuries = new Map>(); + addressSeedMap = new Map(); + aliceWallet = await treasury('Alice', {predeploy: true}); + bobWallet = await treasury('Bob', {predeploy: true}); + }); + + const treasury = async (seed: string, params?: TreasuryParams) => { + const r = await blockchain.treasury(seed, params); + addressSeedMap.set(r.address.toString(), seed); + treasuries.set(r.address.toString(), r); + return r; + }; + + it('should deploy: check initial state', async () => { + const pythAddress = await pythConnector.getPythAddress(); + expect(pythAddress.equals(pyth.address)).toBe(true); + }); + + const defaultFeeds = [PYTH_TON_PRICE_FEED_ID, PYTH_USDT_PRICE_FEED_ID, PYTH_USDC_PRICE_FEED_ID]; + + + it('should configure', async () => { + const res = await pythConnector.sendConfigure(deployer.getSender(), { + value: toNano('0.05'), + pythAddress: pyth.address, + feedsMap: TEST_FEEDS_MAP + }); + + console.log('events: ', res.events); + console.log('externals: ', res.externals); + + const _address = await pythConnector.getPythAddress(); + expect(_address.toRawString()).toEqual(pyth.address.toRawString()); + + const feedsMapRes = await pythConnector.getFeedsMap(); + + feedsMapRes.keys().map(key => { + const buffer = feedsMapRes.get(key)! + const hex1 = '0x' + buffer.toString('hex', 0, 32); + const hex2 = '0x' + buffer.toString('hex', 32); + return { + key: key, + evaa_id: BigInt(hex1), + reffered_id: BigInt(hex2) + }; + }).forEach((x) => { + const {key, evaa_id, reffered_id} = x; + console.log(`${key} : (${evaa_id}, ${reffered_id})`); + }) + + expectCompareDicts(TEST_FEEDS_MAP, feedsMapRes); + }); + + it('should get price updates from Hermes Client', async () => { + const defaultFeeds = [PYTH_TON_PRICE_FEED_ID, PYTH_USDT_PRICE_FEED_ID, PYTH_USDC_PRICE_FEED_ID]; + const res = await getPriceUpdates(HERMES_ENDPOINT, defaultFeeds); + console.log('feeds: ', res.parsed); + for (const item of res.parsed!) { + console.log(item.id, TEST_FEED_NAMES.get(item.id), item.price.price, item.price.expo, item.price.conf, item.price.publish_time); + } + }); + + it('should succeed onchain-getter operation', async () => { + const actorWallets = [aliceWallet]; + let jettonMinter = await deployJettonMinter('USDT', blockchain, deployer); + await mintJettons({actorWallets, jettonMinter, deployer}); + + await pythConnector.sendConfigure(deployer.getSender(), + {value: toNano('0.05'), pythAddress: pyth.address, feedsMap: TEST_FEEDS_MAP} + ); + + const bcTimeGetter = () => blockchain.now!; + const updateDataCell = packNamedPrices({TON: 3.1, USDT: 0.99, USDC: 1.01}, bcTimeGetter); + + const operationBody = makeOnchainGetterPayload({ + publishTimeGap: 10, + maxStaleness: 180, + pythPriceIds: composeFeedsCell(defaultFeeds), + updateDataCell, + operationBody: beginCell().endCell() + }) + + const transferMessage = makeTransferMessage({ + queryId: 12345n, + jettonAmount: 1_000_000_000n, + payloadDestination: pythConnector.address, + sender: aliceWallet.address, + notificationBody: operationBody, + forwardAmount: toNano('1'), + }); + + const res = await sendJetton(jettonMinter, aliceWallet, transferMessage, toNano('1.5')); + console.log(res.events, res.externals); + + res.events.forEach(event=>{ + const _event = event as EventMessageSent; + if (event.type !== 'account_created') { + expect(_event.bounced).toBe(false); + } + }) + + console.log({ + 'alice': aliceWallet.address, + 'bob': bobWallet.address, + 'jetton': jettonMinter.address, + 'connector': pythConnector.address, + 'pyth': pyth.address + }); + }) + + it.skip('should succeed pyth proxy operation', async () => { + + }) +}); diff --git a/price_feeds/ton/pyth-connector/tests/PythTest.spec.ts b/price_feeds/ton/pyth-connector/tests/PythTest.spec.ts new file mode 100644 index 0000000..ebc1a2b --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/PythTest.spec.ts @@ -0,0 +1,1907 @@ +import {Blockchain, SandboxContract, TreasuryContract} from "@ton/sandbox"; +import {Cell, CommonMessageInfoInternal, toNano} from "@ton/core"; +import "@ton/test-utils"; +import {compile} from "@ton/blueprint"; +import {HexString, Price} from "@pythnetwork/price-service-sdk"; +import {PythTest, PythTestConfig} from "../wrappers/PythTest"; +import { + BTC_PRICE_FEED_ID, + ETH_PRICE_FEED_ID, + HERMES_BTC_CONF, + HERMES_BTC_EMA_CONF, + HERMES_BTC_EMA_EXPO, + HERMES_BTC_EMA_PRICE, + HERMES_BTC_ETH_UNIQUE_UPDATE, + HERMES_BTC_ETH_UPDATE, + HERMES_BTC_EXPO, + HERMES_BTC_PRICE, + HERMES_BTC_PUBLISH_TIME, + HERMES_BTC_UNIQUE_CONF, + HERMES_BTC_UNIQUE_EMA_CONF, + HERMES_BTC_UNIQUE_EMA_EXPO, + HERMES_BTC_UNIQUE_EMA_PRICE, + HERMES_BTC_UNIQUE_EMA_PUBLISH_TIME, + HERMES_BTC_UNIQUE_EXPO, + HERMES_BTC_UNIQUE_PRICE, + HERMES_BTC_UNIQUE_PUBLISH_TIME, + HERMES_ETH_CONF, + HERMES_ETH_EMA_CONF, + HERMES_ETH_EMA_EXPO, + HERMES_ETH_EMA_PRICE, + HERMES_ETH_EXPO, + HERMES_ETH_PRICE, + HERMES_ETH_PUBLISH_TIME, + HERMES_ETH_UNIQUE_CONF, + HERMES_ETH_UNIQUE_EMA_CONF, + HERMES_ETH_UNIQUE_EMA_EXPO, + HERMES_ETH_UNIQUE_EMA_PRICE, + HERMES_ETH_UNIQUE_EMA_PUBLISH_TIME, + HERMES_ETH_UNIQUE_EXPO, + HERMES_ETH_UNIQUE_PRICE, + HERMES_ETH_UNIQUE_PUBLISH_TIME, + HERMES_SOL_TON_PYTH_USDT_UPDATE, + HERMES_SOL_UNIQUE_CONF, + HERMES_SOL_UNIQUE_EXPO, + HERMES_SOL_UNIQUE_PRICE, + HERMES_SOL_UNIQUE_PUBLISH_TIME, + PYTH_AUTHORIZE_GOVERNANCE_DATA_SOURCE_TRANSFER, + PYTH_PRICE_FEED_ID, + PYTH_REQUEST_GOVERNANCE_DATA_SOURCE_TRANSFER, + PYTH_SET_DATA_SOURCES, + PYTH_SET_FEE, + SOL_PRICE_FEED_ID, + TEST_GUARDIAN_ADDRESS1, + TEST_GUARDIAN_ADDRESS2, + TON_PRICE_FEED_ID, + USDT_PRICE_FEED_ID, +} from "./utils/pyth"; +import {GUARDIAN_SET_0, MAINNET_UPGRADE_VAAS} from "./utils/wormhole"; +// import {DataSource} from ''; +import {createAuthorizeUpgradePayload} from "./utils/utils"; +import {createVAA, serialize, UniversalAddress,} from "@wormhole-foundation/sdk-definitions"; +import {mocks} from "@wormhole-foundation/sdk-definitions/testing"; +import {calculateUpdatePriceFeedsFee, DataSource} from "@pythnetwork/pyth-ton-js"; + +const TIME_PERIOD = 60; +const PRICE = new Price({ + price: "1", + conf: "2", + expo: 3, + publishTime: 4, +}); +const EMA_PRICE = new Price({ + price: "5", + conf: "6", + expo: 7, + publishTime: 8, +}); +const SINGLE_UPDATE_FEE = 1; +const DATA_SOURCES: DataSource[] = [ + { + emitterChain: 26, + emitterAddress: + "e101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71", + }, +]; +const TEST_GOVERNANCE_DATA_SOURCES: DataSource[] = [ + { + emitterChain: 1, + emitterAddress: + "0000000000000000000000000000000000000000000000000000000000000029", + }, + { + emitterChain: 2, + emitterAddress: + "000000000000000000000000000000000000000000000000000000000000002b", + }, + { + emitterChain: 1, + emitterAddress: + "0000000000000000000000000000000000000000000000000000000000000000", + }, +]; +const CUSTOM_PAYLOAD = Buffer.from("1234567890abcdef", "hex"); + +describe("PythTest", () => { + let code: Cell; + + beforeAll(async () => { + code = await compile("PythTest"); + }); + + let blockchain: Blockchain; + let deployer: SandboxContract; + let mockDeployer: SandboxContract; + let pythTest: SandboxContract; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + deployer = await blockchain.treasury("deployer"); + mockDeployer = await blockchain.treasury("mockDeployer"); + }); + + async function deployContract( + priceFeedId: HexString = BTC_PRICE_FEED_ID, + price: Price = PRICE, + emaPrice: Price = EMA_PRICE, + singleUpdateFee: number = SINGLE_UPDATE_FEE, + dataSources: DataSource[] = DATA_SOURCES, + guardianSetIndex: number = 0, + guardianSet: string[] = GUARDIAN_SET_0, + chainId: number = 1, + governanceChainId: number = 1, + governanceContract: string = "0000000000000000000000000000000000000000000000000000000000000004", + governanceDataSource?: DataSource, + ) { + const config: PythTestConfig = { + priceFeedId, + price, + emaPrice, + singleUpdateFee, + dataSources, + guardianSetIndex, + guardianSet, + chainId, + governanceChainId, + governanceContract, + governanceDataSource, + }; + + pythTest = blockchain.openContract(PythTest.createFromConfig(config, code)); + + const deployResult = await pythTest.sendDeploy( + deployer.getSender(), + toNano("0.05"), + ); + + expect(deployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + deploy: true, + success: true, + }); + } + + async function updateGuardianSets( + pythTest: SandboxContract, + deployer: SandboxContract, + ) { + for (const vaa of MAINNET_UPGRADE_VAAS) { + const result = await pythTest.sendUpdateGuardianSet( + deployer.getSender(), + Buffer.from(vaa, "hex"), + ); + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + } + } + + it("should correctly get price unsafe", async () => { + await deployContract(); + + const result = await pythTest.getPriceUnsafe(BTC_PRICE_FEED_ID); + expect(result.price).toBe(1); + expect(result.conf).toBe(2); + expect(result.expo).toBe(3); + expect(result.publishTime).toBe(4); + }); + + it("should correctly get price no older than", async () => { + const timeNow = Math.floor(Date.now() / 1000) - TIME_PERIOD + 5; // 5 seconds buffer + const price = new Price({ + price: "1", + conf: "2", + expo: 3, + publishTime: timeNow, + }); + await deployContract(BTC_PRICE_FEED_ID, price, EMA_PRICE); + + const result = await pythTest.getPriceNoOlderThan( + TIME_PERIOD, + BTC_PRICE_FEED_ID, + ); + + expect(result.price).toBe(1); + expect(result.conf).toBe(2); + expect(result.expo).toBe(3); + expect(result.publishTime).toBe(timeNow); + }); + + it("should fail to get price no older than", async () => { + await deployContract(); + + await expect( + pythTest.getPriceNoOlderThan(TIME_PERIOD, BTC_PRICE_FEED_ID), + ).rejects.toThrow("Unable to execute get method. Got exit_code: 2001"); // ERROR_OUTDATED_PRICE = 2001 + }); + + it("should correctly get ema price no older than", async () => { + const timeNow = Math.floor(Date.now() / 1000) - TIME_PERIOD + 5; // 5 seconds buffer + const emaPrice = new Price({ + price: "5", + conf: "6", + expo: 7, + publishTime: timeNow, + }); + await deployContract(BTC_PRICE_FEED_ID, PRICE, emaPrice); + + const result = await pythTest.getEmaPriceNoOlderThan( + TIME_PERIOD, + BTC_PRICE_FEED_ID, + ); + + expect(result.price).toBe(5); + expect(result.conf).toBe(6); + expect(result.expo).toBe(7); + expect(result.publishTime).toBe(timeNow); + }); + + it("should fail to get ema price no older than", async () => { + await deployContract(); + + await expect( + pythTest.getEmaPriceNoOlderThan(TIME_PERIOD, BTC_PRICE_FEED_ID), + ).rejects.toThrow("Unable to execute get method. Got exit_code: 2001"); // ERROR_OUTDATED_PRICE = 2001 + }); + + it("should correctly get ema price unsafe", async () => { + await deployContract(); + + const result = await pythTest.getEmaPriceUnsafe(BTC_PRICE_FEED_ID); + + expect(result.price).toBe(5); + expect(result.conf).toBe(6); + expect(result.expo).toBe(7); + expect(result.publishTime).toBe(8); + }); + + it("should correctly get update fee", async () => { + await deployContract(); + + const result = await pythTest.getUpdateFee( + Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"), + ); + + expect(result).toBe(2); + }); + + it("should correctly update price feeds", async () => { + await deployContract(); + let result; + + await updateGuardianSets(pythTest, deployer); + + // Check initial prices + const initialBtcPrice = await pythTest.getPriceUnsafe(BTC_PRICE_FEED_ID); + expect(initialBtcPrice.price).not.toBe(HERMES_BTC_PRICE); + // Expect an error for ETH price feed as it doesn't exist initially + await expect(pythTest.getPriceUnsafe(ETH_PRICE_FEED_ID)).rejects.toThrow( + "Unable to execute get method. Got exit_code: 2000", + ); // ERROR_PRICE_FEED_NOT_FOUND = 2000 + + const updateData = Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"); + const updateFee = await pythTest.getUpdateFee(updateData); + + result = await pythTest.sendUpdatePriceFeeds( + deployer.getSender(), + updateData, + toNano(updateFee), + ); + + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + // Check if both BTC and ETH prices have been updated + const updatedBtcPrice = await pythTest.getPriceUnsafe(BTC_PRICE_FEED_ID); + expect(updatedBtcPrice.price).toBe(HERMES_BTC_PRICE); + expect(updatedBtcPrice.publishTime).toBe(HERMES_BTC_PUBLISH_TIME); + + const updatedEthPrice = await pythTest.getPriceUnsafe(ETH_PRICE_FEED_ID); + expect(updatedEthPrice.price).toBe(HERMES_ETH_PRICE); + expect(updatedEthPrice.publishTime).toBe(HERMES_ETH_PUBLISH_TIME); + }); + + it("should fail to get update fee with invalid data", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const invalidUpdateData = Buffer.from("invalid data"); + + await expect(pythTest.getUpdateFee(invalidUpdateData)).rejects.toThrow( + "Unable to execute get method. Got exit_code: 2002", + ); // ERROR_INVALID_MAGIC = 2002 + }); + + it("should fail to update price feeds with invalid data", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const invalidUpdateData = Buffer.from("invalid data"); + + // Use a fixed value for updateFee since we can't get it from getUpdateFee + const updateFee = toNano("0.1"); // Use a reasonable amount + + const result = await pythTest.sendUpdatePriceFeeds( + deployer.getSender(), + invalidUpdateData, + updateFee, + ); + + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 2002, // ERROR_INVALID_MAGIC + }); + }); + + it("should fail to update price feeds with outdated guardian set", async () => { + await deployContract(); + // Don't update guardian sets + + const updateData = Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"); + const updateFee = await pythTest.getUpdateFee(updateData); + + const result = await pythTest.sendUpdatePriceFeeds( + deployer.getSender(), + updateData, + toNano(updateFee), + ); + + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 1002, // ERROR_GUARDIAN_SET_NOT_FOUND + }); + }); + + it("should fail to update price feeds with invalid data source", async () => { + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + [], // Empty data sources + ); + await updateGuardianSets(pythTest, deployer); + + const updateData = Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"); + const updateFee = await pythTest.getUpdateFee(updateData); + + const result = await pythTest.sendUpdatePriceFeeds( + deployer.getSender(), + updateData, + toNano(updateFee), + ); + + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 2005, // ERROR_UPDATE_DATA_SOURCE_NOT_FOUND + }); + }); + + it("should correctly handle stale prices", async () => { + const staleTime = Math.floor(Date.now() / 1000) - TIME_PERIOD - 10; // 10 seconds past the allowed period + const stalePrice = new Price({ + price: "1", + conf: "2", + expo: 3, + publishTime: staleTime, + }); + await deployContract(BTC_PRICE_FEED_ID, stalePrice, EMA_PRICE); + + await expect( + pythTest.getPriceNoOlderThan(TIME_PERIOD, BTC_PRICE_FEED_ID), + ).rejects.toThrow("Unable to execute get method. Got exit_code: 2001"); // ERROR_OUTDATED_PRICE = 2001 + }); + + it("should fail to update price feeds with insufficient gas", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const updateData = Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"); + + let result = await pythTest.sendUpdatePriceFeeds( + deployer.getSender(), + updateData, + calculateUpdatePriceFeedsFee(1n), // Send enough gas for 1 update instead of 2 + ); + + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 3000, // ERROR_INSUFFICIENT_GAS + }); + }); + + it("should fail to update price feeds with insufficient fee", async () => { + await deployContract(); + + await updateGuardianSets(pythTest, deployer); + + const updateData = Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"); + const updateFee = await pythTest.getUpdateFee(updateData); + + // Send less than the required fee + const insufficientFee = updateFee - 1; + + const result = await pythTest.sendUpdatePriceFeeds( + deployer.getSender(), + updateData, + calculateUpdatePriceFeedsFee(2n) + BigInt(insufficientFee), + ); + + // Check that the transaction did not succeed + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 2011, // ERROR_INSUFFICIENT_FEE = 2011 + }); + }); + + it("should fail to get prices for non-existent price feed", async () => { + await deployContract(); + + const nonExistentPriceFeedId = + "0000000000000000000000000000000000000000000000000000000000000000"; + + await expect( + pythTest.getPriceUnsafe(nonExistentPriceFeedId), + ).rejects.toThrow("Unable to execute get method. Got exit_code: 2000"); // ERROR_PRICE_FEED_NOT_FOUND = 2000 + + await expect( + pythTest.getPriceNoOlderThan(TIME_PERIOD, nonExistentPriceFeedId), + ).rejects.toThrow("Unable to execute get method. Got exit_code: 2000"); // ERROR_PRICE_FEED_NOT_FOUND + + await expect( + pythTest.getEmaPriceUnsafe(nonExistentPriceFeedId), + ).rejects.toThrow("Unable to execute get method. Got exit_code: 2000"); // ERROR_PRICE_FEED_NOT_FOUND + }); + + it("should correctly get chain ID", async () => { + await deployContract(); + + const result = await pythTest.getChainId(); + expect(result).toEqual(1); + }); + + it("should correctly get last executed governance sequence", async () => { + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + DATA_SOURCES, + 0, + [TEST_GUARDIAN_ADDRESS1], + 60051, + 1, + "0000000000000000000000000000000000000000000000000000000000000004", + TEST_GOVERNANCE_DATA_SOURCES[0], + ); + + // Check initial value + let result = await pythTest.getLastExecutedGovernanceSequence(); + expect(result).toEqual(0); + + // Execute a governance action (e.g., set fee) + await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(PYTH_SET_FEE, "hex"), + ); + + // Check that the sequence has increased + result = await pythTest.getLastExecutedGovernanceSequence(); + expect(result).toEqual(1); + }); + + it("should correctly get governance data source index", async () => { + // Deploy contract with initial governance data source + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + DATA_SOURCES, + 0, + [TEST_GUARDIAN_ADDRESS1], + 60051, + 1, + "0000000000000000000000000000000000000000000000000000000000000004", + TEST_GOVERNANCE_DATA_SOURCES[0], + ); + + // Check initial value + let result = await pythTest.getGovernanceDataSourceIndex(); + expect(result).toEqual(0); + + // Execute governance action to change data source + await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(PYTH_AUTHORIZE_GOVERNANCE_DATA_SOURCE_TRANSFER, "hex"), + ); + + // Check that the index has increased + result = await pythTest.getGovernanceDataSourceIndex(); + expect(result).toEqual(1); + }); + + it("should correctly get governance data source", async () => { + // Deploy contract without initial governance data source + await deployContract(); + + // Check initial value (should be empty) + let result = await pythTest.getGovernanceDataSource(); + expect(result).toEqual(null); + + // Deploy contract with initial governance data source + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + DATA_SOURCES, + 0, + [TEST_GUARDIAN_ADDRESS1], + 60051, + 1, + "0000000000000000000000000000000000000000000000000000000000000004", + TEST_GOVERNANCE_DATA_SOURCES[0], + ); + + // Check that the governance data source is set + result = await pythTest.getGovernanceDataSource(); + expect(result).toEqual(TEST_GOVERNANCE_DATA_SOURCES[0]); + + // Execute governance action to change data source + await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(PYTH_AUTHORIZE_GOVERNANCE_DATA_SOURCE_TRANSFER, "hex"), + ); + + // Check that the data source has changed + result = await pythTest.getGovernanceDataSource(); + expect(result).toEqual(TEST_GOVERNANCE_DATA_SOURCES[1]); + }); + + it("should correctly get single update fee", async () => { + await deployContract(); + + // Get the initial fee + const result = await pythTest.getSingleUpdateFee(); + + expect(result).toBe(SINGLE_UPDATE_FEE); + }); + + it("should execute set data sources governance instruction", async () => { + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + DATA_SOURCES, + 0, + [TEST_GUARDIAN_ADDRESS1], + 60051, // CHAIN_ID of starknet since we are using the test payload for starknet + 1, + "0000000000000000000000000000000000000000000000000000000000000004", + TEST_GOVERNANCE_DATA_SOURCES[0], + ); + + // Execute the governance action + const result = await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(PYTH_SET_DATA_SOURCES, "hex"), + ); + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + // Verify that the new data sources are set correctly + const newDataSources: DataSource[] = [ + { + emitterChain: 1, + emitterAddress: + "6bb14509a612f01fbbc4cffeebd4bbfb492a86df717ebe92eb6df432a3f00a25", + }, + { + emitterChain: 3, + emitterAddress: + "000000000000000000000000000000000000000000000000000000000000012d", + }, + ]; + + for (const dataSource of newDataSources) { + const isValid = await pythTest.getIsValidDataSource(dataSource); + expect(isValid).toBe(true); + } + + // Verify that the old data source is no longer valid + const oldDataSource = DATA_SOURCES[0]; + const oldDataSourceIsValid = + await pythTest.getIsValidDataSource(oldDataSource); + expect(oldDataSourceIsValid).toBe(false); + }); + + it("should execute set fee governance instruction", async () => { + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + DATA_SOURCES, + 0, + [TEST_GUARDIAN_ADDRESS1], + 60051, // CHAIN_ID of starknet since we are using the test payload for starknet + 1, + "0000000000000000000000000000000000000000000000000000000000000004", + TEST_GOVERNANCE_DATA_SOURCES[0], + ); + + // Get the initial fee + const initialFee = await pythTest.getSingleUpdateFee(); + expect(initialFee).toBe(SINGLE_UPDATE_FEE); + + // Execute the governance action + const result = await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(PYTH_SET_FEE, "hex"), + ); + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + // Get the new fee + const newFee = await pythTest.getSingleUpdateFee(); + expect(newFee).toBe(4200); // The new fee value is 4200 in the PYTH_SET_FEE payload + + // Verify that the new fee is used for updates + const updateData = Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"); + const updateFee = await pythTest.getUpdateFee(updateData); + expect(updateFee).toBe(8400); // There are two price updates in HERMES_BTC_ETH_UPDATE + }); + + it("should execute authorize governance data source transfer", async () => { + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + DATA_SOURCES, + 0, + [TEST_GUARDIAN_ADDRESS1], + 60051, // CHAIN_ID of starknet since we are using the test payload for starknet + 1, + "0000000000000000000000000000000000000000000000000000000000000004", + TEST_GOVERNANCE_DATA_SOURCES[0], + ); + + // Get the initial governance data source index + const initialIndex = await pythTest.getGovernanceDataSourceIndex(); + expect(initialIndex).toEqual(0); // Initial value should be 0 + + // Get the initial governance data source + const initialDataSource = await pythTest.getGovernanceDataSource(); + expect(initialDataSource).toEqual(TEST_GOVERNANCE_DATA_SOURCES[0]); + + // Get the initial last executed governance sequence + const initialSequence = await pythTest.getLastExecutedGovernanceSequence(); + expect(initialSequence).toEqual(0); // Initial value should be 0 + + // Execute the governance action + const result = await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(PYTH_AUTHORIZE_GOVERNANCE_DATA_SOURCE_TRANSFER, "hex"), + ); + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + // Get the new governance data source index + const newIndex = await pythTest.getGovernanceDataSourceIndex(); + expect(newIndex).toEqual(1); // The new index value should match the one in the test payload + + // Get the new governance data source + const newDataSource = await pythTest.getGovernanceDataSource(); + expect(newDataSource).not.toEqual(initialDataSource); // The data source should have changed + expect(newDataSource).toEqual(TEST_GOVERNANCE_DATA_SOURCES[1]); // The data source should have changed + + // Get the new last executed governance sequence + const newSequence = await pythTest.getLastExecutedGovernanceSequence(); + expect(newSequence).toBeGreaterThan(initialSequence); // The sequence should have increased + expect(newSequence).toBe(1); + }); + + it("should fail when executing request governance data source transfer directly", async () => { + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + DATA_SOURCES, + 0, + [TEST_GUARDIAN_ADDRESS1], + 60051, // CHAIN_ID of starknet since we are using the test payload for starknet + 1, + "0000000000000000000000000000000000000000000000000000000000000004", + TEST_GOVERNANCE_DATA_SOURCES[1], + ); + + const result = await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(PYTH_REQUEST_GOVERNANCE_DATA_SOURCE_TRANSFER, "hex"), + ); + + // Check that the transaction did not succeed + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 1012, // ERROR_INVALID_GOVERNANCE_ACTION + }); + + // Verify that the governance data source index hasn't changed + const index = await pythTest.getGovernanceDataSourceIndex(); + expect(index).toEqual(0); // Should still be the initial value + + // Verify that the governance data source hasn't changed + const dataSource = await pythTest.getGovernanceDataSource(); + expect(dataSource).toEqual(TEST_GOVERNANCE_DATA_SOURCES[1]); // Should still be the initial value + }); + + it("should fail to execute governance action with invalid governance data source", async () => { + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + DATA_SOURCES, + 0, + [TEST_GUARDIAN_ADDRESS1], + 60051, + 1, + "0000000000000000000000000000000000000000000000000000000000000004", + TEST_GOVERNANCE_DATA_SOURCES[1], + ); + + const result = await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(PYTH_SET_FEE, "hex"), + ); + + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 2013, // ERROR_INVALID_GOVERNANCE_DATA_SOURCE + }); + }); + + it("should fail to execute governance action with old sequence number", async () => { + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + DATA_SOURCES, + 0, + [TEST_GUARDIAN_ADDRESS1], + 60051, + 1, + "0000000000000000000000000000000000000000000000000000000000000004", + TEST_GOVERNANCE_DATA_SOURCES[0], + ); + + // Execute a governance action to increase the sequence number + await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(PYTH_SET_FEE, "hex"), + ); + + // Try to execute the same governance action again + const result = await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(PYTH_SET_FEE, "hex"), + ); + + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 2014, // ERROR_OLD_GOVERNANCE_MESSAGE + }); + }); + + it("should fail to execute governance action with invalid chain ID", async () => { + const invalidChainId = 999; + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + DATA_SOURCES, + 0, + [TEST_GUARDIAN_ADDRESS1], + invalidChainId, + 1, + "0000000000000000000000000000000000000000000000000000000000000004", + TEST_GOVERNANCE_DATA_SOURCES[0], + ); + + const result = await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(PYTH_SET_FEE, "hex"), + ); + + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 2015, // ERROR_INVALID_GOVERNANCE_TARGET + }); + }); + + it("should successfully upgrade the contract", async () => { + // Compile the upgraded contract + const upgradedCode = await compile("PythTestUpgraded"); + const upgradedCodeHash = upgradedCode.hash(); + + // Create the authorize upgrade payload + const authorizeUpgradePayload = + createAuthorizeUpgradePayload(upgradedCodeHash); + + const authorizeUpgradeVaa = createVAA("Uint8Array", { + guardianSet: 0, + timestamp: 0, + nonce: 0, + emitterChain: "Solana", + emitterAddress: new UniversalAddress(new Uint8Array(32)), + sequence: 1n, + consistencyLevel: 0, + signatures: [], + payload: authorizeUpgradePayload, + }); + + const guardianSet = mocks.devnetGuardianSet(); + guardianSet.setSignatures(authorizeUpgradeVaa); + + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + DATA_SOURCES, + 0, + [TEST_GUARDIAN_ADDRESS2], + 1, + 1, + "0000000000000000000000000000000000000000000000000000000000000000", + TEST_GOVERNANCE_DATA_SOURCES[2], + ); + + // Execute the upgrade + const sendExecuteGovernanceActionResult = + await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(serialize(authorizeUpgradeVaa)), + ); + + expect(sendExecuteGovernanceActionResult.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + // Execute the upgrade + const sendUpgradeContractResult = await pythTest.sendUpgradeContract( + deployer.getSender(), + upgradedCode, + ); + + expect(sendUpgradeContractResult.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + // Verify that the contract has been upgraded by calling a new method + const newMethodResult = await pythTest.getNewFunction(); + expect(newMethodResult).toBe(1); + }); + + it("should fail to upgrade the contract with modified code", async () => { + // Compile the upgraded contract + const upgradedCode = await compile("PythTestUpgraded"); + const upgradedCodeHash = upgradedCode.hash(); + + // Create the authorize upgrade payload + const authorizeUpgradePayload = + createAuthorizeUpgradePayload(upgradedCodeHash); + + const authorizeUpgradeVaa = createVAA("Uint8Array", { + guardianSet: 0, + timestamp: 0, + nonce: 0, + emitterChain: "Solana", + emitterAddress: new UniversalAddress(new Uint8Array(32)), + sequence: 1n, + consistencyLevel: 0, + signatures: [], + payload: authorizeUpgradePayload, + }); + + const guardianSet = mocks.devnetGuardianSet(); + guardianSet.setSignatures(authorizeUpgradeVaa); + + await deployContract( + BTC_PRICE_FEED_ID, + PRICE, + EMA_PRICE, + SINGLE_UPDATE_FEE, + DATA_SOURCES, + 0, + [TEST_GUARDIAN_ADDRESS2], + 1, + 1, + "0000000000000000000000000000000000000000000000000000000000000000", + TEST_GOVERNANCE_DATA_SOURCES[2], + ); + + // Execute the upgrade authorization + const sendExecuteGovernanceActionResult = + await pythTest.sendExecuteGovernanceAction( + deployer.getSender(), + Buffer.from(serialize(authorizeUpgradeVaa)), + ); + + expect(sendExecuteGovernanceActionResult.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + // Attempt to execute the upgrade with a different code + const wormholeTestCode = await compile("WormholeTest"); + const sendUpgradeContractResult = await pythTest.sendUpgradeContract( + deployer.getSender(), + wormholeTestCode, + ); + + // Expect the transaction to fail + expect(sendUpgradeContractResult.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 2018, // ERROR_INVALID_CODE_HASH + }); + + // Verify that the contract has not been upgraded by attempting to call the new method + await expect(pythTest.getNewFunction()).rejects.toThrow(); + }); + + it("should successfully parse price feed updates", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const sentValue = toNano("1"); + const result = await pythTest.sendParsePriceFeedUpdates( + deployer.getSender(), + Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"), + sentValue, + [BTC_PRICE_FEED_ID, ETH_PRICE_FEED_ID], + HERMES_BTC_PUBLISH_TIME, + HERMES_BTC_PUBLISH_TIME, + deployer.address, + CUSTOM_PAYLOAD, + ); + + // Verify transaction success and message count + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + outMessagesCount: 1, + }); + + // Get the output message + const outMessage = result.transactions[1].outMessages.values()[0]; + + // Verify excess value is returned + expect( + (outMessage.info as CommonMessageInfoInternal).value.coins, + ).toBeGreaterThan(0); + + const cs = outMessage.body.beginParse(); + + // Verify message header + const op = cs.loadUint(32); + expect(op).toBe(5); // OP_PARSE_PRICE_FEED_UPDATES + + // Verify number of price feeds + const numPriceFeeds = cs.loadUint(8); + expect(numPriceFeeds).toBe(2); // We expect BTC and ETH price feeds + + // Load and verify price feeds + const priceFeedsCell = cs.loadRef(); + let currentCell = priceFeedsCell; + + // First price feed (BTC) + const btcCs = currentCell.beginParse(); + const btcPriceId = + "0x" + btcCs.loadUintBig(256).toString(16).padStart(64, "0"); + expect(btcPriceId).toBe(BTC_PRICE_FEED_ID); + + const btcPriceFeedCell = btcCs.loadRef(); + const btcPriceFeedSlice = btcPriceFeedCell.beginParse(); + + // Verify BTC current price + const btcCurrentPriceCell = btcPriceFeedSlice.loadRef(); + const btcCurrentPrice = btcCurrentPriceCell.beginParse(); + expect(btcCurrentPrice.loadInt(64)).toBe(HERMES_BTC_PRICE); + expect(btcCurrentPrice.loadUint(64)).toBe(HERMES_BTC_CONF); + expect(btcCurrentPrice.loadInt(32)).toBe(HERMES_BTC_EXPO); + expect(btcCurrentPrice.loadUint(64)).toBe(HERMES_BTC_PUBLISH_TIME); + + // Verify BTC EMA price + const btcEmaPriceCell = btcPriceFeedSlice.loadRef(); + const btcEmaPrice = btcEmaPriceCell.beginParse(); + expect(btcEmaPrice.loadInt(64)).toBe(HERMES_BTC_EMA_PRICE); + expect(btcEmaPrice.loadUint(64)).toBe(HERMES_BTC_EMA_CONF); + expect(btcEmaPrice.loadInt(32)).toBe(HERMES_BTC_EMA_EXPO); + expect(btcEmaPrice.loadUint(64)).toBe(HERMES_BTC_PUBLISH_TIME); + + // Move to ETH price feed + currentCell = btcCs.loadRef(); + + // Second price feed (ETH) + const ethCs = currentCell.beginParse(); + const ethPriceId = + "0x" + ethCs.loadUintBig(256).toString(16).padStart(64, "0"); + expect(ethPriceId).toBe(ETH_PRICE_FEED_ID); + + const ethPriceFeedCell = ethCs.loadRef(); + const ethPriceFeedSlice = ethPriceFeedCell.beginParse(); + + // Verify ETH current price + const ethCurrentPriceCell = ethPriceFeedSlice.loadRef(); + const ethCurrentPrice = ethCurrentPriceCell.beginParse(); + expect(ethCurrentPrice.loadInt(64)).toBe(HERMES_ETH_PRICE); + expect(ethCurrentPrice.loadUint(64)).toBe(HERMES_ETH_CONF); + expect(ethCurrentPrice.loadInt(32)).toBe(HERMES_ETH_EXPO); + expect(ethCurrentPrice.loadUint(64)).toBe(HERMES_ETH_PUBLISH_TIME); + + // Verify ETH EMA price + const ethEmaPriceCell = ethPriceFeedSlice.loadRef(); + const ethEmaPrice = ethEmaPriceCell.beginParse(); + expect(ethEmaPrice.loadInt(64)).toBe(HERMES_ETH_EMA_PRICE); + expect(ethEmaPrice.loadUint(64)).toBe(HERMES_ETH_EMA_CONF); + expect(ethEmaPrice.loadInt(32)).toBe(HERMES_ETH_EMA_EXPO); + expect(ethEmaPrice.loadUint(64)).toBe(HERMES_ETH_PUBLISH_TIME); + + // Verify this is the end of the chain + expect(ethCs.remainingRefs).toBe(0); + + // Verify sender address + const senderAddress = cs.loadAddress(); + expect(senderAddress?.toString()).toBe( + deployer.getSender().address.toString(), + ); + + // Verify custom payload + const customPayloadCell = cs.loadRef(); + const customPayloadSlice = customPayloadCell.beginParse(); + const receivedPayload = Buffer.from( + customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length), + ); + expect(receivedPayload.toString("hex")).toBe( + CUSTOM_PAYLOAD.toString("hex"), + ); + }); + + it("should successfully parse price feed updates with more than 3 price feed ids", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const sentValue = toNano("1"); + const result = await pythTest.sendParsePriceFeedUpdates( + deployer.getSender(), + Buffer.from(HERMES_SOL_TON_PYTH_USDT_UPDATE, "hex"), + sentValue, + [ + SOL_PRICE_FEED_ID, + TON_PRICE_FEED_ID, + PYTH_PRICE_FEED_ID, + USDT_PRICE_FEED_ID, + ], + HERMES_SOL_UNIQUE_PUBLISH_TIME, + HERMES_SOL_UNIQUE_PUBLISH_TIME, + deployer.address, + CUSTOM_PAYLOAD, + ); + + // Verify transaction success and message count + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + outMessagesCount: 1, + }); + + // Get the output message + const outMessage = result.transactions[1].outMessages.values()[0]; + + // Verify excess value is returned + expect( + (outMessage.info as CommonMessageInfoInternal).value.coins, + ).toBeGreaterThan(0); + + const cs = outMessage.body.beginParse(); + + // Verify message header + const op = cs.loadUint(32); + expect(op).toBe(5); // OP_PARSE_PRICE_FEED_UPDATES + + // Verify number of price feeds + const numPriceFeeds = cs.loadUint(8); + expect(numPriceFeeds).toBe(4); // We expect SOL, TON, PYTH and USDT price feeds + + // Load and verify price feeds + const priceFeedsCell = cs.loadRef(); + let currentCell = priceFeedsCell; + + // First price feed (SOL) + const solCs = currentCell.beginParse(); + const solPriceId = + "0x" + solCs.loadUintBig(256).toString(16).padStart(64, "0"); + expect(solPriceId).toBe(SOL_PRICE_FEED_ID); + + const solPriceFeedCell = solCs.loadRef(); + const solPriceFeedSlice = solPriceFeedCell.beginParse(); + + // Verify SOL current price + const solCurrentPriceCell = solPriceFeedSlice.loadRef(); + const solCurrentPrice = solCurrentPriceCell.beginParse(); + expect(solCurrentPrice.loadInt(64)).toBe(HERMES_SOL_UNIQUE_PRICE); + expect(solCurrentPrice.loadUint(64)).toBe(HERMES_SOL_UNIQUE_CONF); + expect(solCurrentPrice.loadInt(32)).toBe(HERMES_SOL_UNIQUE_EXPO); + expect(solCurrentPrice.loadUint(64)).toBe(HERMES_SOL_UNIQUE_PUBLISH_TIME); + + // Verify sender address + const senderAddress = cs.loadAddress(); + expect(senderAddress?.toString()).toBe( + deployer.getSender().address.toString(), + ); + + // Verify custom payload + const customPayloadCell = cs.loadRef(); + const customPayloadSlice = customPayloadCell.beginParse(); + const receivedPayload = Buffer.from( + customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length), + ); + expect(receivedPayload.toString("hex")).toBe( + CUSTOM_PAYLOAD.toString("hex"), + ); + }); + + it("should successfully parse unique price feed updates", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const sentValue = toNano("1"); + const result = await pythTest.sendParseUniquePriceFeedUpdates( + deployer.getSender(), + Buffer.from(HERMES_BTC_ETH_UNIQUE_UPDATE, "hex"), + sentValue, + [BTC_PRICE_FEED_ID, ETH_PRICE_FEED_ID], + HERMES_BTC_PUBLISH_TIME, + 60, + deployer.address, + CUSTOM_PAYLOAD, + ); + + // Verify transaction success and message count + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + outMessagesCount: 1, + }); + + // Get the output message + const outMessage = result.transactions[1].outMessages.values()[0]; + + // Verify excess value is returned + expect( + (outMessage.info as CommonMessageInfoInternal).value.coins, + ).toBeGreaterThan(0); + + const cs = outMessage.body.beginParse(); + + // Verify message header + const op = cs.loadUint(32); + expect(op).toBe(6); // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES + + // Verify number of price feeds + const numPriceFeeds = cs.loadUint(8); + expect(numPriceFeeds).toBe(2); // We expect BTC and ETH price feeds + + // Load and verify price feeds + const priceFeedsCell = cs.loadRef(); + let currentCell = priceFeedsCell; + + // First price feed (BTC) + const btcCs = currentCell.beginParse(); + const btcPriceId = + "0x" + btcCs.loadUintBig(256).toString(16).padStart(64, "0"); + expect(btcPriceId).toBe(BTC_PRICE_FEED_ID); + + const btcPriceFeedCell = btcCs.loadRef(); + const btcPriceFeedSlice = btcPriceFeedCell.beginParse(); + + // Verify BTC current price + const btcCurrentPriceCell = btcPriceFeedSlice.loadRef(); + const btcCurrentPrice = btcCurrentPriceCell.beginParse(); + expect(btcCurrentPrice.loadInt(64)).toBe(HERMES_BTC_UNIQUE_PRICE); + expect(btcCurrentPrice.loadUint(64)).toBe(HERMES_BTC_UNIQUE_CONF); + expect(btcCurrentPrice.loadInt(32)).toBe(HERMES_BTC_UNIQUE_EXPO); + expect(btcCurrentPrice.loadUint(64)).toBe(HERMES_BTC_UNIQUE_PUBLISH_TIME); + + // Verify BTC EMA price + const btcEmaPriceCell = btcPriceFeedSlice.loadRef(); + const btcEmaPrice = btcEmaPriceCell.beginParse(); + expect(btcEmaPrice.loadInt(64)).toBe(HERMES_BTC_UNIQUE_EMA_PRICE); + expect(btcEmaPrice.loadUint(64)).toBe(HERMES_BTC_UNIQUE_EMA_CONF); + expect(btcEmaPrice.loadInt(32)).toBe(HERMES_BTC_UNIQUE_EMA_EXPO); + expect(btcEmaPrice.loadUint(64)).toBe(HERMES_BTC_UNIQUE_EMA_PUBLISH_TIME); + + // Move to ETH price feed + currentCell = btcCs.loadRef(); + + // Second price feed (ETH) + const ethCs = currentCell.beginParse(); + const ethPriceId = + "0x" + ethCs.loadUintBig(256).toString(16).padStart(64, "0"); + expect(ethPriceId).toBe(ETH_PRICE_FEED_ID); + + const ethPriceFeedCell = ethCs.loadRef(); + const ethPriceFeedSlice = ethPriceFeedCell.beginParse(); + + // Verify ETH current price + const ethCurrentPriceCell = ethPriceFeedSlice.loadRef(); + const ethCurrentPrice = ethCurrentPriceCell.beginParse(); + expect(ethCurrentPrice.loadInt(64)).toBe(HERMES_ETH_UNIQUE_PRICE); + expect(ethCurrentPrice.loadUint(64)).toBe(HERMES_ETH_UNIQUE_CONF); + expect(ethCurrentPrice.loadInt(32)).toBe(HERMES_ETH_UNIQUE_EXPO); + expect(ethCurrentPrice.loadUint(64)).toBe(HERMES_ETH_UNIQUE_PUBLISH_TIME); + + // Verify ETH EMA price + const ethEmaPriceCell = ethPriceFeedSlice.loadRef(); + const ethEmaPrice = ethEmaPriceCell.beginParse(); + expect(ethEmaPrice.loadInt(64)).toBe(HERMES_ETH_UNIQUE_EMA_PRICE); + expect(ethEmaPrice.loadUint(64)).toBe(HERMES_ETH_UNIQUE_EMA_CONF); + expect(ethEmaPrice.loadInt(32)).toBe(HERMES_ETH_UNIQUE_EMA_EXPO); + expect(ethEmaPrice.loadUint(64)).toBe(HERMES_ETH_UNIQUE_EMA_PUBLISH_TIME); + + // Verify this is the end of the chain + expect(ethCs.remainingRefs).toBe(0); + + // Verify sender address + const senderAddress = cs.loadAddress(); + expect(senderAddress?.toString()).toBe( + deployer.getSender().address.toString(), + ); + + // Verify custom payload + const customPayloadCell = cs.loadRef(); + const customPayloadSlice = customPayloadCell.beginParse(); + const receivedPayload = Buffer.from( + customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length), + ); + expect(receivedPayload.toString("hex")).toBe( + CUSTOM_PAYLOAD.toString("hex"), + ); + }); + + it("should successfully parse unique price feed updates with more than 3 price feed ids", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const sentValue = toNano("1"); + const result = await pythTest.sendParseUniquePriceFeedUpdates( + deployer.getSender(), + Buffer.from(HERMES_SOL_TON_PYTH_USDT_UPDATE, "hex"), + sentValue, + [ + SOL_PRICE_FEED_ID, + TON_PRICE_FEED_ID, + PYTH_PRICE_FEED_ID, + USDT_PRICE_FEED_ID, + ], + HERMES_SOL_UNIQUE_PUBLISH_TIME, + 60, + deployer.address, + CUSTOM_PAYLOAD, + ); + + // Verify transaction success and message count + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + outMessagesCount: 1, + }); + + // Get the output message + const outMessage = result.transactions[1].outMessages.values()[0]; + + // Verify excess value is returned + expect( + (outMessage.info as CommonMessageInfoInternal).value.coins, + ).toBeGreaterThan(0); + + const cs = outMessage.body.beginParse(); + + // Verify message header + const op = cs.loadUint(32); + expect(op).toBe(6); // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES + + // Verify number of price feeds + const numPriceFeeds = cs.loadUint(8); + expect(numPriceFeeds).toBe(4); // We expect SOL, TON, PYTH and USDT price feeds + + // Load and verify price feeds + const priceFeedsCell = cs.loadRef(); + let currentCell = priceFeedsCell; + + // First price feed (SOL) + const solCs = currentCell.beginParse(); + const solPriceId = + "0x" + solCs.loadUintBig(256).toString(16).padStart(64, "0"); + expect(solPriceId).toBe(SOL_PRICE_FEED_ID); + + const solPriceFeedCell = solCs.loadRef(); + const solPriceFeedSlice = solPriceFeedCell.beginParse(); + + // Verify SOL current price + const solCurrentPriceCell = solPriceFeedSlice.loadRef(); + const solCurrentPrice = solCurrentPriceCell.beginParse(); + expect(solCurrentPrice.loadInt(64)).toBe(HERMES_SOL_UNIQUE_PRICE); + expect(solCurrentPrice.loadUint(64)).toBe(HERMES_SOL_UNIQUE_CONF); + expect(solCurrentPrice.loadInt(32)).toBe(HERMES_SOL_UNIQUE_EXPO); + expect(solCurrentPrice.loadUint(64)).toBe(HERMES_SOL_UNIQUE_PUBLISH_TIME); + // Verify sender address + const senderAddress = cs.loadAddress(); + expect(senderAddress?.toString()).toBe( + deployer.getSender().address.toString(), + ); + + // Verify custom payload + const customPayloadCell = cs.loadRef(); + const customPayloadSlice = customPayloadCell.beginParse(); + const receivedPayload = Buffer.from( + customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length), + ); + expect(receivedPayload.toString("hex")).toBe( + CUSTOM_PAYLOAD.toString("hex"), + ); + }); + + it("should fail to parse invalid price feed updates", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const invalidUpdateData = Buffer.from("invalid data"); + + const result = await pythTest.sendParsePriceFeedUpdates( + deployer.getSender(), + invalidUpdateData, + toNano("1"), + [BTC_PRICE_FEED_ID, ETH_PRICE_FEED_ID], + HERMES_BTC_PUBLISH_TIME, + HERMES_BTC_PUBLISH_TIME, + deployer.address, + CUSTOM_PAYLOAD, + ); + + // Verify transaction success but error response sent + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + // Find the error response message - it's in the second transaction's outMessages + const errorTx = result.transactions[1]; // The PythTest contract transaction + expect(errorTx.outMessages.values().length).toBeGreaterThan(0); + + const errorMessage = errorTx.outMessages.values()[0]; + expect(errorMessage).toBeDefined(); + + const cs = errorMessage.body.beginParse(); + + // Verify error response format + const op = cs.loadUint(32); + expect(op).toBe(0x10002); // OP_RESPONSE_ERROR + + const errorCode = cs.loadUint(32); + expect(errorCode).toBe(2002); // ERROR_INVALID_MAGIC + + const originalOp = cs.loadUint(32); + expect(originalOp).toBe(5); // OP_PARSE_PRICE_FEED_UPDATES + + // Verify custom payload is preserved + const customPayloadCell = cs.loadRef(); + const customPayloadSlice = customPayloadCell.beginParse(); + expect( + Buffer.from( + customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length), + ).toString("hex"), + ).toBe(CUSTOM_PAYLOAD.toString("hex")); + }); + + it("should fail to parse price feed updates within range", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const sentValue = toNano("1"); + const result = await pythTest.sendParseUniquePriceFeedUpdates( + deployer.getSender(), + Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"), + sentValue, + [BTC_PRICE_FEED_ID, ETH_PRICE_FEED_ID], + HERMES_BTC_PUBLISH_TIME + 1, + HERMES_BTC_PUBLISH_TIME + 1, + deployer.address, + CUSTOM_PAYLOAD, + ); + + // Verify transaction success but error response sent + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + // Find the error response message - it's in the second transaction's outMessages + const errorTx = result.transactions[1]; // The PythTest contract transaction + expect(errorTx.outMessages.values().length).toBeGreaterThan(0); + + const errorMessage = errorTx.outMessages.values()[0]; + expect(errorMessage).toBeDefined(); + + const cs = errorMessage.body.beginParse(); + + // Verify error response format + const op = cs.loadUint(32); + expect(op).toBe(0x10002); // OP_RESPONSE_ERROR + + const errorCode = cs.loadUint(32); + expect(errorCode).toBe(2020); // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE + + const originalOp = cs.loadUint(32); + expect(originalOp).toBe(6); // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES + + // Verify custom payload is preserved + const customPayloadCell = cs.loadRef(); + const customPayloadSlice = customPayloadCell.beginParse(); + expect( + Buffer.from( + customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length), + ).toString("hex"), + ).toBe(CUSTOM_PAYLOAD.toString("hex")); + }); + + it("should fail to parse unique price feed updates", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const sentValue = toNano("1"); + const result = await pythTest.sendParseUniquePriceFeedUpdates( + deployer.getSender(), + Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"), + sentValue, + [BTC_PRICE_FEED_ID, ETH_PRICE_FEED_ID], + HERMES_BTC_PUBLISH_TIME, + 60, + deployer.address, + CUSTOM_PAYLOAD, + ); + + // Verify transaction success but error response sent + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + // Find the error response message - it's in the second transaction's outMessages + const errorTx = result.transactions[1]; // The PythTest contract transaction + expect(errorTx.outMessages.values().length).toBeGreaterThan(0); + + const errorMessage = errorTx.outMessages.values()[0]; + expect(errorMessage).toBeDefined(); + + const cs = errorMessage.body.beginParse(); + + // Verify error response format + const op = cs.loadUint(32); + expect(op).toBe(0x10002); // OP_RESPONSE_ERROR + + const errorCode = cs.loadUint(32); + expect(errorCode).toBe(2020); // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE + + const originalOp = cs.loadUint(32); + expect(originalOp).toBe(6); // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES + + // Verify custom payload is preserved + const customPayloadCell = cs.loadRef(); + const customPayloadSlice = customPayloadCell.beginParse(); + expect( + Buffer.from( + customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length), + ).toString("hex"), + ).toBe(CUSTOM_PAYLOAD.toString("hex")); + }); + + it("should successfully parse price feed updates in price ids order", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const sentValue = toNano("1"); + const result = await pythTest.sendParsePriceFeedUpdates( + deployer.getSender(), + Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"), + sentValue, + [ETH_PRICE_FEED_ID, BTC_PRICE_FEED_ID], + HERMES_BTC_PUBLISH_TIME, + HERMES_BTC_PUBLISH_TIME, + deployer.address, + CUSTOM_PAYLOAD, + ); + + // Verify transaction success and message count + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + outMessagesCount: 1, + }); + + // Get the output message + const outMessage = result.transactions[1].outMessages.values()[0]; + + // Verify excess value is returned + expect( + (outMessage.info as CommonMessageInfoInternal).value.coins, + ).toBeGreaterThan(0); + + expect((outMessage.info as CommonMessageInfoInternal).dest.toString()).toBe( + deployer.address.toString(), + ); + + const cs = outMessage.body.beginParse(); + + // Verify message header + const op = cs.loadUint(32); + expect(op).toBe(5); // OP_PARSE_PRICE_FEED_UPDATES + + // Verify number of price feeds + const numPriceFeeds = cs.loadUint(8); + expect(numPriceFeeds).toBe(2); // We expect BTC and ETH price feeds + + // Load and verify price feeds + const priceFeedsCell = cs.loadRef(); + let currentCell = priceFeedsCell; + + // First price feed (ETH) + const ethCs = currentCell.beginParse(); + const ethPriceId = + "0x" + ethCs.loadUintBig(256).toString(16).padStart(64, "0"); + expect(ethPriceId).toBe(ETH_PRICE_FEED_ID); + + const ethPriceFeedCell = ethCs.loadRef(); + const ethPriceFeedSlice = ethPriceFeedCell.beginParse(); + + // Verify ETH current price + const ethCurrentPriceCell = ethPriceFeedSlice.loadRef(); + const ethCurrentPrice = ethCurrentPriceCell.beginParse(); + expect(ethCurrentPrice.loadInt(64)).toBe(HERMES_ETH_PRICE); + expect(ethCurrentPrice.loadUint(64)).toBe(HERMES_ETH_CONF); + expect(ethCurrentPrice.loadInt(32)).toBe(HERMES_ETH_EXPO); + expect(ethCurrentPrice.loadUint(64)).toBe(HERMES_ETH_PUBLISH_TIME); + + // Verify ETH EMA price + const ethEmaPriceCell = ethPriceFeedSlice.loadRef(); + const ethEmaPrice = ethEmaPriceCell.beginParse(); + expect(ethEmaPrice.loadInt(64)).toBe(HERMES_ETH_EMA_PRICE); + expect(ethEmaPrice.loadUint(64)).toBe(HERMES_ETH_EMA_CONF); + expect(ethEmaPrice.loadInt(32)).toBe(HERMES_ETH_EMA_EXPO); + expect(ethEmaPrice.loadUint(64)).toBe(HERMES_ETH_PUBLISH_TIME); + + // Move to ETH price feed + currentCell = ethCs.loadRef(); + + // Second price feed (BTC) + const btcCs = currentCell.beginParse(); + const btcPriceId = + "0x" + btcCs.loadUintBig(256).toString(16).padStart(64, "0"); + expect(btcPriceId).toBe(BTC_PRICE_FEED_ID); + + const btcPriceFeedCell = btcCs.loadRef(); + const btcPriceFeedSlice = btcPriceFeedCell.beginParse(); + + // Verify BTC current price + const btcCurrentPriceCell = btcPriceFeedSlice.loadRef(); + const btcCurrentPrice = btcCurrentPriceCell.beginParse(); + expect(btcCurrentPrice.loadInt(64)).toBe(HERMES_BTC_PRICE); + expect(btcCurrentPrice.loadUint(64)).toBe(HERMES_BTC_CONF); + expect(btcCurrentPrice.loadInt(32)).toBe(HERMES_BTC_EXPO); + expect(btcCurrentPrice.loadUint(64)).toBe(HERMES_BTC_PUBLISH_TIME); + + // Verify BTC EMA price + const btcEmaPriceCell = btcPriceFeedSlice.loadRef(); + const btcEmaPrice = btcEmaPriceCell.beginParse(); + expect(btcEmaPrice.loadInt(64)).toBe(HERMES_BTC_EMA_PRICE); + expect(btcEmaPrice.loadUint(64)).toBe(HERMES_BTC_EMA_CONF); + expect(btcEmaPrice.loadInt(32)).toBe(HERMES_BTC_EMA_EXPO); + expect(btcEmaPrice.loadUint(64)).toBe(HERMES_BTC_PUBLISH_TIME); + + // Verify this is the end of the chain + expect(ethCs.remainingRefs).toBe(0); + }); + + it("should successfully parse unique price feed updates in price ids order", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const sentValue = toNano("1"); + const result = await pythTest.sendParseUniquePriceFeedUpdates( + deployer.getSender(), + Buffer.from(HERMES_BTC_ETH_UNIQUE_UPDATE, "hex"), + sentValue, + [ETH_PRICE_FEED_ID, BTC_PRICE_FEED_ID], + HERMES_BTC_PUBLISH_TIME, + 60, + deployer.address, + CUSTOM_PAYLOAD, + ); + + // Verify transaction success and message count + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + outMessagesCount: 1, + }); + + // Get the output message + const outMessage = result.transactions[1].outMessages.values()[0]; + + // Verify excess value is returned + expect( + (outMessage.info as CommonMessageInfoInternal).value.coins, + ).toBeGreaterThan(0); + + const cs = outMessage.body.beginParse(); + + // Verify message header + const op = cs.loadUint(32); + expect(op).toBe(6); // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES + + // Verify number of price feeds + const numPriceFeeds = cs.loadUint(8); + expect(numPriceFeeds).toBe(2); // We expect BTC and ETH price feeds + + // Load and verify price feeds + const priceFeedsCell = cs.loadRef(); + let currentCell = priceFeedsCell; + + // First price feed (ETH) + const ethCs = currentCell.beginParse(); + const ethPriceId = + "0x" + ethCs.loadUintBig(256).toString(16).padStart(64, "0"); + expect(ethPriceId).toBe(ETH_PRICE_FEED_ID); + + const ethPriceFeedCell = ethCs.loadRef(); + const ethPriceFeedSlice = ethPriceFeedCell.beginParse(); + + // Verify ETH current price + const ethCurrentPriceCell = ethPriceFeedSlice.loadRef(); + const ethCurrentPrice = ethCurrentPriceCell.beginParse(); + expect(ethCurrentPrice.loadInt(64)).toBe(HERMES_ETH_UNIQUE_PRICE); + expect(ethCurrentPrice.loadUint(64)).toBe(HERMES_ETH_UNIQUE_CONF); + expect(ethCurrentPrice.loadInt(32)).toBe(HERMES_ETH_UNIQUE_EXPO); + expect(ethCurrentPrice.loadUint(64)).toBe(HERMES_ETH_UNIQUE_PUBLISH_TIME); + + // Verify ETH EMA price + const ethEmaPriceCell = ethPriceFeedSlice.loadRef(); + const ethEmaPrice = ethEmaPriceCell.beginParse(); + expect(ethEmaPrice.loadInt(64)).toBe(HERMES_ETH_UNIQUE_EMA_PRICE); + expect(ethEmaPrice.loadUint(64)).toBe(HERMES_ETH_UNIQUE_EMA_CONF); + expect(ethEmaPrice.loadInt(32)).toBe(HERMES_ETH_UNIQUE_EMA_EXPO); + expect(ethEmaPrice.loadUint(64)).toBe(HERMES_ETH_UNIQUE_EMA_PUBLISH_TIME); + + currentCell = ethCs.loadRef(); + + // Second price feed (BTC) + const btcCs = currentCell.beginParse(); + const btcPriceId = + "0x" + btcCs.loadUintBig(256).toString(16).padStart(64, "0"); + expect(btcPriceId).toBe(BTC_PRICE_FEED_ID); + + const btcPriceFeedCell = btcCs.loadRef(); + const btcPriceFeedSlice = btcPriceFeedCell.beginParse(); + + // Verify BTC current price + const btcCurrentPriceCell = btcPriceFeedSlice.loadRef(); + const btcCurrentPrice = btcCurrentPriceCell.beginParse(); + expect(btcCurrentPrice.loadInt(64)).toBe(HERMES_BTC_UNIQUE_PRICE); + expect(btcCurrentPrice.loadUint(64)).toBe(HERMES_BTC_UNIQUE_CONF); + expect(btcCurrentPrice.loadInt(32)).toBe(HERMES_BTC_UNIQUE_EXPO); + expect(btcCurrentPrice.loadUint(64)).toBe(HERMES_BTC_UNIQUE_PUBLISH_TIME); + + // Verify BTC EMA price + const btcEmaPriceCell = btcPriceFeedSlice.loadRef(); + const btcEmaPrice = btcEmaPriceCell.beginParse(); + expect(btcEmaPrice.loadInt(64)).toBe(HERMES_BTC_UNIQUE_EMA_PRICE); + expect(btcEmaPrice.loadUint(64)).toBe(HERMES_BTC_UNIQUE_EMA_CONF); + expect(btcEmaPrice.loadInt(32)).toBe(HERMES_BTC_UNIQUE_EMA_EXPO); + expect(btcEmaPrice.loadUint(64)).toBe(HERMES_BTC_UNIQUE_EMA_PUBLISH_TIME); + + // Verify this is the end of the chain + expect(btcCs.remainingRefs).toBe(0); + }); + + it("should successfully parse price feed updates with a different target address", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const sentValue = toNano("1"); + const result = await pythTest.sendParsePriceFeedUpdates( + deployer.getSender(), + Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"), + sentValue, + [BTC_PRICE_FEED_ID, ETH_PRICE_FEED_ID], + HERMES_BTC_PUBLISH_TIME, + HERMES_BTC_PUBLISH_TIME, + mockDeployer.address, + CUSTOM_PAYLOAD, + ); + + // Verify transaction success and message count + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + outMessagesCount: 1, + }); + + // Verify message success to target address + expect(result.transactions).toHaveTransaction({ + from: pythTest.address, + to: mockDeployer.address, + success: true, + }); + + // Get the output message + const outMessage = result.transactions[1].outMessages.values()[0]; + + // Verify excess value is returned + expect( + (outMessage.info as CommonMessageInfoInternal).value.coins, + ).toBeGreaterThan(0); + + const cs = outMessage.body.beginParse(); + + // Verify message header + const op = cs.loadUint(32); + expect(op).toBe(5); // OP_PARSE_PRICE_FEED_UPDATES + + // Verify number of price feeds + const numPriceFeeds = cs.loadUint(8); + expect(numPriceFeeds).toBe(2); // We expect BTC and ETH price feeds + + cs.loadRef(); // Skip price feeds + + // Verify sender address + const senderAddress = cs.loadAddress(); + expect(senderAddress?.toString()).toBe( + deployer.getSender().address.toString(), + ); + + // Verify custom payload + const customPayloadCell = cs.loadRef(); + const customPayloadSlice = customPayloadCell.beginParse(); + const receivedPayload = Buffer.from( + customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length), + ); + expect(receivedPayload.toString("hex")).toBe( + CUSTOM_PAYLOAD.toString("hex"), + ); + }); + + it("should successfully parse unique price feed updates with a different target address", async () => { + await deployContract(); + await updateGuardianSets(pythTest, deployer); + + const sentValue = toNano("1"); + const result = await pythTest.sendParseUniquePriceFeedUpdates( + deployer.getSender(), + Buffer.from(HERMES_BTC_ETH_UNIQUE_UPDATE, "hex"), + sentValue, + [BTC_PRICE_FEED_ID, ETH_PRICE_FEED_ID], + HERMES_BTC_PUBLISH_TIME, + 60, + mockDeployer.address, + CUSTOM_PAYLOAD, + ); + + // Verify transaction success and message count + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + outMessagesCount: 1, + }); + + // Verify message success to target address + expect(result.transactions).toHaveTransaction({ + from: pythTest.address, + to: mockDeployer.address, + success: true, + }); + + // Get the output message + const outMessage = result.transactions[1].outMessages.values()[0]; + + // Verify excess value is returned + expect( + (outMessage.info as CommonMessageInfoInternal).value.coins, + ).toBeGreaterThan(0); + + const cs = outMessage.body.beginParse(); + + // Verify message header + const op = cs.loadUint(32); + expect(op).toBe(6); // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES + + // Verify number of price feeds + const numPriceFeeds = cs.loadUint(8); + expect(numPriceFeeds).toBe(2); // We expect BTC and ETH price feeds + + cs.loadRef(); // Skip price feeds + + // Verify sender address + const senderAddress = cs.loadAddress(); + expect(senderAddress?.toString()).toBe( + deployer.getSender().address.toString(), + ); + + // Verify custom payload + const customPayloadCell = cs.loadRef(); + const customPayloadSlice = customPayloadCell.beginParse(); + const receivedPayload = Buffer.from( + customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length), + ); + expect(receivedPayload.toString("hex")).toBe( + CUSTOM_PAYLOAD.toString("hex"), + ); + }); +}); \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/tests/WormholeTest.spec.ts b/price_feeds/ton/pyth-connector/tests/WormholeTest.spec.ts new file mode 100644 index 0000000..10aad3a --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/WormholeTest.spec.ts @@ -0,0 +1,240 @@ +import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; +import { Cell, toNano } from "@ton/core"; +import { WormholeTest, WormholeTestConfig } from "../wrappers/WormholeTest"; +import "@ton/test-utils"; +import { compile } from "@ton/blueprint"; +import { + createGuardianSetUpgradeBytes, + GUARDIAN_SET_0, + GUARDIAN_SET_4, + MAINNET_UPGRADE_VAAS, +} from "./utils/wormhole"; + +const CHAIN_ID = 1; +const GOVERNANCE_CHAIN_ID = 1; +const GOVERNANCE_CONTRACT = + "0000000000000000000000000000000000000000000000000000000000000004"; + +describe("WormholeTest", () => { + let code: Cell; + + beforeAll(async () => { + code = await compile("WormholeTest"); + }); + + let blockchain: Blockchain; + let deployer: SandboxContract; + let wormholeTest: SandboxContract; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + deployer = await blockchain.treasury("deployer"); + }); + + async function deployContract( + guardianSetIndex: number = 0, + guardianSet: string[] = GUARDIAN_SET_0, + chainId: number = CHAIN_ID, + governanceChainId: number = GOVERNANCE_CHAIN_ID, + governanceContract: string = GOVERNANCE_CONTRACT, + ) { + const config: WormholeTestConfig = { + guardianSetIndex, + guardianSet, + chainId, + governanceChainId, + governanceContract, + }; + + wormholeTest = blockchain.openContract( + WormholeTest.createFromConfig(config, code), + ); + + const deployResult = await wormholeTest.sendDeploy( + deployer.getSender(), + toNano("0.05"), + ); + + expect(deployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: wormholeTest.address, + deploy: true, + success: true, + }); + + const guardianSetIndexRes = await wormholeTest.getCurrentGuardianSetIndex(); + expect(guardianSetIndexRes).toBe(guardianSetIndex); + } + + it("should correctly parse encoded upgrade", async () => { + await deployContract(); + + const currentGuardianSetIndex = 3; + const newGuardianSetIndex = 4; + const chainId = 1; // Example chain ID + const encodedUpgrade = createGuardianSetUpgradeBytes( + chainId, + newGuardianSetIndex, + GUARDIAN_SET_4, + ); + + const result = await wormholeTest.getParseEncodedUpgrade( + currentGuardianSetIndex, + encodedUpgrade, + ); + + expect(result.action).toBe(2); + expect(result.chain).toBe(chainId); + expect(result.module.toString(16)).toBe("436f7265"); + expect(result.newGuardianSetIndex).toBeGreaterThan(currentGuardianSetIndex); + expect(result.newGuardianSetIndex).toBe(newGuardianSetIndex); + expect(result.newGuardianSetKeys).toEqual(GUARDIAN_SET_4); + }); + + it("should fail with invalid encoded upgrade", async () => { + await deployContract(); + + const currentGuardianSetIndex = 3; + const newGuardianSetIndex = 4; + const chainId = 1; // Example chain ID + const encodedUpgrade = createGuardianSetUpgradeBytes( + chainId, + newGuardianSetIndex, + GUARDIAN_SET_4, + ); + + // Replace the first 32 bytes with zeros + const zeroBytes = Buffer.alloc(32, 0); + zeroBytes.copy(encodedUpgrade, 0, 0, 32); + + await expect( + wormholeTest.getParseEncodedUpgrade( + currentGuardianSetIndex, + encodedUpgrade, + ), + ).rejects.toThrow("Unable to execute get method. Got exit_code: 1011"); // ERROR_INVALID_MODULE = 1011 + }); + + it("should correctly parse and verify wormhole vm", async () => { + await deployContract(); + + const mainnet_upgrade_vaa_1 = MAINNET_UPGRADE_VAAS[0]; + + const result = await wormholeTest.getParseAndVerifyWormholeVm( + Buffer.from(mainnet_upgrade_vaa_1, "hex"), + ); + expect(result.version).toBe(1); + expect(result.vm_guardian_set_index).toBe(0); + expect(result.timestamp).toBe(1628094930); + expect(result.nonce).toBe(3); + expect(result.emitter_chain_id).toBe(1); + expect(result.emitter_address.toString()).toBe( + "0000000000000000000000000000000000000000000000000000000000000004", + ); + expect(result.sequence).toBe(1337); + expect(result.consistency_level).toBe(0); + expect(result.payload).toBe(mainnet_upgrade_vaa_1.slice(246)); + expect(result.hash).toBe( + "ed3a5600d44b9dcc889daf0178dd69ab1e9356308194ba3628a7b720ae48a8d5", + ); + }); + + it("should correctly update guardian set", async () => { + await deployContract(); + + const mainnet_upgrade_vaa_1 = MAINNET_UPGRADE_VAAS[0]; + + const getUpdateGuardianSetResult = await wormholeTest.sendUpdateGuardianSet( + deployer.getSender(), + Buffer.from(mainnet_upgrade_vaa_1, "hex"), + ); + expect(getUpdateGuardianSetResult.transactions).toHaveTransaction({ + from: deployer.address, + to: wormholeTest.address, + success: true, + }); + + const getCurrentGuardianSetIndexResult = + await wormholeTest.getCurrentGuardianSetIndex(); + expect(getCurrentGuardianSetIndexResult).toBe(1); + }); + + it("should fail with wrong vaa", async () => { + await deployContract(); + const invalid_mainnet_upgrade_vaa = "00" + MAINNET_UPGRADE_VAAS[0].slice(2); + const result = await wormholeTest.sendUpdateGuardianSet( + deployer.getSender(), + Buffer.from(invalid_mainnet_upgrade_vaa, "hex"), + ); + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: wormholeTest.address, + success: false, + exitCode: 1001, // ERROR_INVALID_VERSION = 1001 + }); + }); + + it("should correctly get guardian set", async () => { + await deployContract(); + + const getGuardianSetResult = await wormholeTest.getGuardianSet(0); + expect(getGuardianSetResult.keys).toEqual(GUARDIAN_SET_0); + }); + + it("should return the correct chain ID", async () => { + await deployContract(); + + const result = await wormholeTest.getChainId(); + expect(result).toEqual(CHAIN_ID); + }); + + it("should return the correct governance chain ID", async () => { + await deployContract(); + + const result = await wormholeTest.getGovernanceChainId(); + expect(result).toEqual(GOVERNANCE_CHAIN_ID); + }); + + it("should return the correct governance contract address", async () => { + await deployContract(); + + const result = await wormholeTest.getGovernanceContract(); + expect(result).toEqual(GOVERNANCE_CONTRACT); + }); + + it("should correctly check if a governance action is consumed", async () => { + await deployContract(); + + const hash = 12345n; + let getGovernanceActionIsConsumedResult = + await wormholeTest.getGovernanceActionIsConsumed(hash); + expect(getGovernanceActionIsConsumedResult).toEqual(false); + + const mainnet_upgrade_vaa_1 = MAINNET_UPGRADE_VAAS[0]; + + const getParseAndVerifyWormholeVmResult = + await wormholeTest.getParseAndVerifyWormholeVm( + Buffer.from(mainnet_upgrade_vaa_1, "hex"), + ); + expect(getParseAndVerifyWormholeVmResult.hash).toBe( + "ed3a5600d44b9dcc889daf0178dd69ab1e9356308194ba3628a7b720ae48a8d5", + ); + + const sendUpdateGuardianSetResult = + await wormholeTest.sendUpdateGuardianSet( + deployer.getSender(), + Buffer.from(mainnet_upgrade_vaa_1, "hex"), + ); + expect(sendUpdateGuardianSetResult.transactions).toHaveTransaction({ + from: deployer.address, + to: wormholeTest.address, + success: true, + }); + + getGovernanceActionIsConsumedResult = + await wormholeTest.getGovernanceActionIsConsumed( + BigInt("0x" + getParseAndVerifyWormholeVmResult.hash), + ); + expect(getGovernanceActionIsConsumedResult).toEqual(true); + }); +}); diff --git a/price_feeds/ton/pyth-connector/tests/utils/assets.ts b/price_feeds/ton/pyth-connector/tests/utils/assets.ts new file mode 100644 index 0000000..aab25de --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/utils/assets.ts @@ -0,0 +1,52 @@ +import {Dictionary} from "@ton/ton"; +import {Buffer} from "buffer"; + +import {INTERNAL_ASSET_ID as ASSET_ID} from "./internalAssets"; +import { packConnectedFeeds } from "./feeds"; + +export const PYTH_TON_PRICE_FEED_ID = "0x8963217838ab4cf5cadc172203c1f0b763fbaa45f346d8ee50ba994bbcac3026"; +export const PYTH_USDT_PRICE_FEED_ID = "0x2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b"; +export const PYTH_STTON_PRICE_FEED_ID = "0x9145e059026a4d5a46f3b96408f7e572e33b3257b9c2dbe8dba551c772762002"; +export const PYTH_TSTON_PRICE_FEED_ID = "0x3d1784128eeab5961ec60648fe497d3901eebd211b7f51e4bb0db9f024977d25"; +export const PYTH_USDC_PRICE_FEED_ID = "0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a"; + +export const PYTH_WTON_PRICE_FEED_ID = "0xff00ff7838ab4cf5cadc172203c1f0b763fbaa45f346d8ee50ba994bbcff00ff"; +export const PYTH_BUX_PRICE_FEED_ID = "0xff00ffdc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688ff00ff"; + +export const INTERNAL_TON_PRICE_FEED_ID = ASSET_ID.TON; +export const INTERNAL_USDT_PRICE_FEED_ID = ASSET_ID.jUSDT; +export const INTERNAL_STTON_PRICE_FEED_ID = ASSET_ID.stTON; +export const INTERNAL_TSTON_PRICE_FEED_ID = ASSET_ID.tsTON; +export const INTERNAL_USDC_PRICE_FEED_ID = ASSET_ID.jUSDC; + +export const INTERNAL_WTON_PRICE_FEED_ID = ASSET_ID.TON_STORM; +export const INTERNAL_BUX_PRICE_FEED_ID = ASSET_ID.USDT_STORM; + +export const TEST_FEEDS_MAP: Dictionary = (()=>{ + const map = Dictionary.empty(); + map.set(BigInt(PYTH_TON_PRICE_FEED_ID), packConnectedFeeds(INTERNAL_TON_PRICE_FEED_ID, 0n)); + map.set(BigInt(PYTH_USDT_PRICE_FEED_ID), packConnectedFeeds(INTERNAL_USDT_PRICE_FEED_ID, 0n)); + map.set(BigInt(PYTH_STTON_PRICE_FEED_ID), packConnectedFeeds(INTERNAL_STTON_PRICE_FEED_ID, BigInt(PYTH_TON_PRICE_FEED_ID))); + map.set(BigInt(PYTH_TSTON_PRICE_FEED_ID), packConnectedFeeds(INTERNAL_TSTON_PRICE_FEED_ID, BigInt(PYTH_TON_PRICE_FEED_ID))); + map.set(BigInt(PYTH_USDC_PRICE_FEED_ID), packConnectedFeeds(INTERNAL_USDC_PRICE_FEED_ID, 0n)); + + map.set(BigInt(PYTH_WTON_PRICE_FEED_ID), packConnectedFeeds(INTERNAL_WTON_PRICE_FEED_ID, BigInt(PYTH_TON_PRICE_FEED_ID))); + map.set(BigInt(PYTH_BUX_PRICE_FEED_ID), packConnectedFeeds(INTERNAL_BUX_PRICE_FEED_ID, BigInt(PYTH_USDT_PRICE_FEED_ID))); + return map; +})(); + +export const TEST_FEEDS = [ + PYTH_TON_PRICE_FEED_ID, + PYTH_USDT_PRICE_FEED_ID, PYTH_USDC_PRICE_FEED_ID, + PYTH_STTON_PRICE_FEED_ID, PYTH_TSTON_PRICE_FEED_ID, + PYTH_WTON_PRICE_FEED_ID, PYTH_BUX_PRICE_FEED_ID +]; + +export const TEST_FEED_NAMES = new Map() + .set(PYTH_TON_PRICE_FEED_ID.slice(2), 'TON') + .set(PYTH_USDT_PRICE_FEED_ID.slice(2), 'USDT') + .set(PYTH_USDC_PRICE_FEED_ID.slice(2), 'USDC') + .set(PYTH_STTON_PRICE_FEED_ID.slice(2), 'stTON') + .set(PYTH_TSTON_PRICE_FEED_ID.slice(2), 'tsTON') + .set(PYTH_WTON_PRICE_FEED_ID.slice(2), 'wTON') + .set(PYTH_BUX_PRICE_FEED_ID.slice(2), 'BUX'); diff --git a/price_feeds/ton/pyth-connector/tests/utils/deploy.ts b/price_feeds/ton/pyth-connector/tests/utils/deploy.ts new file mode 100644 index 0000000..99996f6 --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/utils/deploy.ts @@ -0,0 +1,142 @@ +import {Blockchain, SandboxContract, TreasuryContract} from "@ton/sandbox"; +import { Address, beginCell, Cell, toNano } from "@ton/core"; +import {compile, sleep} from "@ton/blueprint"; +import {makeEmptyIds, PythConnector} from "../../wrappers/PythConnector"; +import {DataSource} from "@pythnetwork/pyth-ton-js"; +import {Main, MainConfig} from "../../wrappers/Main"; +import {GOVERNANCE_DATA_SOURCE, GUARDIAN_SET_0, MAINNET_UPGRADE_VAAS} from "./wormhole"; +import * as fs from "node:fs"; +import { JettonMinter } from "@ton-community/assets-sdk"; + +export async function deployPythConnector(blockchain: Blockchain, deployer: SandboxContract, pythContractAddress: Address) { + const code: Cell = await compile('PythConnector'); + const contract = PythConnector.createFromConfig({ + pythAddress: pythContractAddress, + ids: makeEmptyIds() + }, code + ); + + const openedContract = blockchain.openContract(contract); + + const deployResult = await openedContract.sendDeploy(deployer.getSender(), toNano('0.05')); + + expect(deployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: openedContract.address, + deploy: true, + success: true, + }); + + return openedContract; +} + +export async function deployAndConfigurePyth( + blockchain: Blockchain, + deployer: SandboxContract, + options?:{ + noCheck?: boolean, + shouldBuild?: boolean, + chainId?: number, + upgradeCodeHash?: number | bigint, // allows varying contract address in blockchain + singleUpdateFee?: number | bigint, +}) { + const _chainId = options?.chainId ?? 0; + const _shouldBuild = options?.shouldBuild ?? false; + const _singleUpdateFee = options?.singleUpdateFee ?? 0; + + const DATA_SOURCES: DataSource[] = [ + { + emitterChain: 26, + emitterAddress: + "e101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71", + }, + ]; + + // Validate that chainId is a valid number + if (isNaN(_chainId)) { + throw new Error("CHAIN_ID must be a valid number"); + } + + console.log("Chain ID:", _chainId); + + const config: MainConfig = { + singleUpdateFee: _singleUpdateFee ? Number.parseInt( _singleUpdateFee.toString()) : 0, + dataSources: DATA_SOURCES, + guardianSetIndex: 0, + guardianSet: GUARDIAN_SET_0, + chainId: _chainId, + governanceChainId: 1, + governanceContract: "0000000000000000000000000000000000000000000000000000000000000004", + governanceDataSource: GOVERNANCE_DATA_SOURCE, + upgradeCodeHash: options?.upgradeCodeHash ?? 0, + }; + + + let code: Cell + + const contractName = options?.noCheck ? "MainNoCheck" : "Main"; + if (_shouldBuild) { + code = await compile(contractName); + } else { + const path = `node_modules/@evaafi/pyth-connector/build/${contractName}.compiled.json`; + code = Cell.fromBoc(Buffer.from(JSON.parse(fs.readFileSync(path, 'utf8'))['hex'], 'hex'))[0]; + } + + // const path = `node_modules/@evaafi/pyth-connector/build/${contractName}.compiled.json`; + // const code = Cell.fromBoc(Buffer.from(JSON.parse(fs.readFileSync(path, 'utf8'))['hex'], 'hex'))[0]; + const pythContract = Main.createFromConfig(config, code); + + const main = blockchain.openContract(pythContract); + await main.sendDeploy(deployer.getSender(), toNano("0.005")); + console.log("Main contract deployed at:", main.address.toString()); + + // Call sendUpdateGuardianSet for each VAA + const currentGuardianSetIndex = await main.getCurrentGuardianSetIndex(); + console.log(`Current guardian set index: ${currentGuardianSetIndex}`); + + for (let i = currentGuardianSetIndex; i < MAINNET_UPGRADE_VAAS.length; i++) { + const vaa = MAINNET_UPGRADE_VAAS[i]; + const vaaBuffer = Buffer.from(vaa, "hex"); + await main.sendUpdateGuardianSet(deployer.getSender(), vaaBuffer); + console.log(`Successfully updated guardian set ${i + 1} with VAA: ${vaa.slice(0, 20,)}...`,); + + // Wait for 30 seconds before checking the guardian set index + // console.log("Waiting for 30 seconds before checking guardian set index..."); + // await sleep(1000); + + // Verify the update + const newIndex = await main.getCurrentGuardianSetIndex(); + if (newIndex !== i + 1) { + console.error(`Failed to update guardian set. Expected index ${i + 1}, got ${newIndex}`); + break; + } + } + + console.log("Guardian set update process completed."); + + return main; +} + +export const deployJettonMinter = async (assetName: string, blockchain: Blockchain, deployer: SandboxContract) => { + const jetton = JettonMinter.createFromConfig({ + admin: deployer.address, + content: beginCell().storeStringTail(assetName).endCell() + }); + + const jettonMinter = blockchain.openContract(jetton); + await jettonMinter.sendDeploy(deployer.getSender()); + + return jettonMinter; +} + +export const mintJettons = async (args: { + jettonMinter: SandboxContract, + deployer: SandboxContract, + actorWallets: SandboxContract[] + jettonAmount?: bigint +}) => { + const amount = args.jettonAmount ?? 1000000_000000n; + for (let w of args.actorWallets) { + await args.jettonMinter.sendMint(args.deployer.getSender(), w.address, amount); + } +} diff --git a/price_feeds/ton/pyth-connector/tests/utils/feeds.ts b/price_feeds/ton/pyth-connector/tests/utils/feeds.ts new file mode 100644 index 0000000..6f8df0f --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/utils/feeds.ts @@ -0,0 +1,16 @@ +import { Buffer } from "buffer"; + +export function bigintToBuffer(value: bigint, size: number): Buffer { + if (value < 0n) { + throw new Error('Only non-negative bigint is supported'); + } + // it's questionable whether it stores in LE or BE + // and what option will TVM use, now by default it's BE + const hex = value.toString(16); + const padded = hex.padStart(size * 2, '0'); + return Buffer.from(padded, 'hex'); +} + +export const packConnectedFeeds = (evaa_id: bigint, reffered_id: bigint) => { + return Buffer.concat([bigintToBuffer(evaa_id, 32), bigintToBuffer(reffered_id, 32)]); +} diff --git a/price_feeds/ton/pyth-connector/tests/utils/internalAssets.ts b/price_feeds/ton/pyth-connector/tests/utils/internalAssets.ts new file mode 100644 index 0000000..c746577 --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/utils/internalAssets.ts @@ -0,0 +1,28 @@ +import sha256 from "crypto-js/sha256"; + +export function sha256Hash(input: string): bigint { + const hash = sha256(input); + const hashHex = hash.toString(); + return BigInt('0x' + hashHex); +} + +export const INTERNAL_ASSET_ID = { + TON: sha256Hash('TON'), + USDT: sha256Hash('USDT'), + jUSDT: sha256Hash('jUSDT'), + jUSDC: sha256Hash('jUSDC'), + stTON: sha256Hash('stTON'), + tsTON: sha256Hash('tsTON'), + uTON: sha256Hash('uTON'), + + // LP + TONUSDT_DEDUST: sha256Hash('TONUSDT_DEDUST'), + TONUSDT_STONFI: sha256Hash('TONUSDT_STONFI'), + TON_STORM: sha256Hash('TON_STORM'), + USDT_STORM: sha256Hash('USDT_STORM'), + + // ALTS + NOT: sha256Hash('NOT'), + DOGS: sha256Hash('DOGS'), + CATI: sha256Hash('CATI'), +}; diff --git a/price_feeds/ton/pyth-connector/tests/utils/messages.ts b/price_feeds/ton/pyth-connector/tests/utils/messages.ts new file mode 100644 index 0000000..ae150b1 --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/utils/messages.ts @@ -0,0 +1,43 @@ +import { Address, beginCell, Cell } from "@ton/core"; +import { JETTON_OPCODES, OPCODES } from "./opcodes"; + +export function makeOnchainGetterPayload(args: { + // queryId: number | bigint, + publishTimeGap: number, + maxStaleness: number, + updateDataCell: Cell, + pythPriceIds: Cell, + operationBody: Cell +}) { + + return beginCell() + .storeUint(OPCODES.ONCHAIN_GETTER_OPERATION, 32) + .storeUint(args.publishTimeGap, 64) + .storeUint(args.maxStaleness, 64) + .storeRef(args.updateDataCell) + .storeRef(args.pythPriceIds) + .storeRef(beginCell().endCell()) + .endCell(); +} + +export function makeTransferMessage(args: { + queryId: bigint, + jettonAmount: bigint, + sender: Address, // for excesses + payloadDestination: Address, + notificationBody: Cell, + forwardAmount: bigint, +}) { + const TRANSFER_JETTON_OP_CODE = JETTON_OPCODES.TRANSFER; + return beginCell() + .storeUint(TRANSFER_JETTON_OP_CODE, 32) + .storeUint(args.queryId, 64) + .storeCoins(args.jettonAmount) + .storeAddress(args.payloadDestination) + .storeAddress(args.sender) + .storeBit(0) + .storeCoins(args.forwardAmount) + .storeBit(1) + .storeRef(args.notificationBody) + .endCell(); +} \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/tests/utils/opcodes.ts b/price_feeds/ton/pyth-connector/tests/utils/opcodes.ts new file mode 100644 index 0000000..73b0a33 --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/utils/opcodes.ts @@ -0,0 +1,18 @@ +export const OPCODES = { + CONFIGURE: 0x2, + ONCHAIN_GETTER_OPERATION: 0x7, + ONCHAIN_GETTER_PROCESS: 0x8, + PROXY_OPERATION: 0x4, + JETTON_TRANSFER: 0xf8a7ea5, +} + +export const PYTH_OPCODES = { + UPDATE_PRICE_FEEDS: 0x5, + RESPONSE_ERROR: 0x10002, +} + +export const JETTON_OPCODES = { + TRANSFER: 0x0f8a7ea5, + TRANSFER_NOTIFICATION: 0x7362d09c, + EXCESSES: 0xd53276db, +} diff --git a/price_feeds/ton/pyth-connector/tests/utils/prices.ts b/price_feeds/ton/pyth-connector/tests/utils/prices.ts new file mode 100644 index 0000000..78ae1f1 --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/utils/prices.ts @@ -0,0 +1,185 @@ +import {Cell, Dictionary} from "@ton/core"; +import {Maybe} from "@ton/core/dist/utils/maybe"; +import {beginCell} from "@ton/ton"; +import { + INTERNAL_BUX_PRICE_FEED_ID, + INTERNAL_STTON_PRICE_FEED_ID, + INTERNAL_TON_PRICE_FEED_ID, + INTERNAL_TSTON_PRICE_FEED_ID, + INTERNAL_USDC_PRICE_FEED_ID, + INTERNAL_USDT_PRICE_FEED_ID, INTERNAL_WTON_PRICE_FEED_ID, PYTH_BUX_PRICE_FEED_ID, + PYTH_STTON_PRICE_FEED_ID, + PYTH_TON_PRICE_FEED_ID, + PYTH_TSTON_PRICE_FEED_ID, + PYTH_USDC_PRICE_FEED_ID, + PYTH_USDT_PRICE_FEED_ID, PYTH_WTON_PRICE_FEED_ID +} from "./assets"; + +export type PythPrice = { + priceId: bigint, // pyth price feed + price: bigint, // price value + digits: number, // decimals + publishTime: number // publish time timestamp +}; + +export function packPriceCell(price: PythPrice, nextCell: Maybe) { + const conf = price.priceId % 65536n; + // console.log('current price: ', price); + const builder = beginCell() + .storeUint(price.priceId, 256) + .storeInt(price.price, 64) + .storeUint(conf, 64) + .storeInt(-price.digits, 32) // we receive e.g. 8, but need to store -8 + .storeUint(price.publishTime, 64) + .storeUint(price.publishTime, 64) // prev publish time (fake) + .storeInt(price.price, 64) // ema price (fake) + .storeInt(conf + 1n, 64) // ema conf (fake) + + if (nextCell) { + builder.storeRef(nextCell); + } + + return builder.endCell(); +} + +export type PythAssetInfo = { + pythFeedId: string, // pyth feed id of asset + decimals: number, // original evaa asset decimals + digits: number, // pyth feed asset decimals + conf: number // some identifier related to pyth price feed object +}; + +const PYTH_ASSET_CONFIG_MAP = new Map() + .set('TON', {pythFeedId: PYTH_TON_PRICE_FEED_ID, decimals: 9, digits: 8, conf: 12345}) + .set('USDC', {pythFeedId: PYTH_USDC_PRICE_FEED_ID, decimals: 6, digits: 8, conf: 12346}) + .set('USDT', {pythFeedId: PYTH_USDT_PRICE_FEED_ID, decimals: 6, digits: 10, conf: 12347}) + .set('TSTON', {pythFeedId: PYTH_TSTON_PRICE_FEED_ID, decimals: 9, digits: 8, conf: 12348}) + .set('STTON', {pythFeedId: PYTH_STTON_PRICE_FEED_ID, decimals: 9, digits: 8, conf: 12349}) + .set('WTON', {pythFeedId: PYTH_WTON_PRICE_FEED_ID, decimals: 9, digits: 8, conf: 12350}) + .set('BUX', {pythFeedId: PYTH_BUX_PRICE_FEED_ID, decimals: 6, digits: 10, conf: 12351}); + +const EVAA_ASSETS_MAP = new Map() + .set('TON', INTERNAL_TON_PRICE_FEED_ID) + .set('USDC', INTERNAL_USDC_PRICE_FEED_ID) + .set('USDT', INTERNAL_USDT_PRICE_FEED_ID) + .set('TSTON', INTERNAL_TSTON_PRICE_FEED_ID) + .set('STTON', INTERNAL_STTON_PRICE_FEED_ID) + .set('WTON', INTERNAL_WTON_PRICE_FEED_ID) + .set('BUX', INTERNAL_BUX_PRICE_FEED_ID); + +function makePrice(assetName: string, assetPrice: bigint, timestampGetter: () => number) { + const timestamp = timestampGetter(); + if (!PYTH_ASSET_CONFIG_MAP.has(assetName)) { + throw new Error(`No feed config for asset: ${assetName}`); + } + + const feedConfig = PYTH_ASSET_CONFIG_MAP.get(assetName)!; + const price = assetPrice; // * (10n ** BigInt(feedConfig.decimals)) / (10n ** BigInt(feedConfig.digits)); + + return { + priceId: BigInt(feedConfig.pythFeedId), + price, + digits: feedConfig.digits, + publishTime: timestamp, + } as PythPrice; +} + +export function collectPriceFeeds(pricesObject: Object) { + const feeds: string[] = []; + + const entries = Object.entries(pricesObject); + for (const [k, v] of entries) { + if (!PYTH_ASSET_CONFIG_MAP.has(k)) { + throw new Error(`No feed config for asset: ${k}`); + } + feeds.push(PYTH_ASSET_CONFIG_MAP.get(k)?.pythFeedId!); + } + return feeds; +} + +export function makePricesDict(pricesObject: Object, pricePrecisionMultiplier: number): Dictionary { + const dict = Dictionary.empty(); + const entries = Object.entries(pricesObject); + for (const [k, v] of entries) { + if (!PYTH_ASSET_CONFIG_MAP.has(k)) { + throw new Error(`No feed config for asset: ${k}`); + } + + const cfg = PYTH_ASSET_CONFIG_MAP.get(k)!; + let value = (typeof v === 'number') ? BigInt(Math.round(v * pricePrecisionMultiplier)) : BigInt(v); + if (!EVAA_ASSETS_MAP.has(k)) throw new Error(`No evaa_id for asset ${k}`); + const evaaId = EVAA_ASSETS_MAP.get(k)!; + // console.warn({k, v, evaaId, value}); + dict.set(evaaId, value); + } + return dict; +} + +export function makePythPrices(pricesObject: Object, timeGetter: () => number) { + const pythPrices: PythPrice[] = []; + const entries = Object.entries(pricesObject); + for (const [k, v] of entries) { + if (!PYTH_ASSET_CONFIG_MAP.has(k)) { + throw new Error(`No feed config for asset: ${k}`); + } + + const cfg = PYTH_ASSET_CONFIG_MAP.get(k)!; + let value = (typeof v === 'number') ? BigInt(Math.round(v * 10 ** cfg.digits)) : BigInt(v); + pythPrices.push(makePrice(k, value, timeGetter)); + + } + + return pythPrices; +} + +const FEEDS_CONFORMITY = new Map() + .set(INTERNAL_BUX_PRICE_FEED_ID, [PYTH_BUX_PRICE_FEED_ID, PYTH_USDT_PRICE_FEED_ID]) + .set(INTERNAL_WTON_PRICE_FEED_ID, [PYTH_WTON_PRICE_FEED_ID, PYTH_TON_PRICE_FEED_ID]) + .set(INTERNAL_STTON_PRICE_FEED_ID, [PYTH_STTON_PRICE_FEED_ID, PYTH_TON_PRICE_FEED_ID]) + .set(INTERNAL_TSTON_PRICE_FEED_ID, [PYTH_TSTON_PRICE_FEED_ID, PYTH_TON_PRICE_FEED_ID]) + .set(INTERNAL_USDC_PRICE_FEED_ID, [PYTH_USDC_PRICE_FEED_ID]) + .set(INTERNAL_USDT_PRICE_FEED_ID, [PYTH_USDT_PRICE_FEED_ID]) + .set(INTERNAL_TON_PRICE_FEED_ID, [PYTH_TON_PRICE_FEED_ID]); + +/** + * collects list of pyth feeds required to get specified evaa list of ids + * @param evaaIds + */ +function collectRequiredPythFeeds(evaaIds: bigint[]): string[] { + const set = new Set(); + for (const id of evaaIds) { + if (!FEEDS_CONFORMITY.has(id)) throw new Error(`Evaa id ${id} not found in the map`); + const feeds = FEEDS_CONFORMITY.get(id)!; + for (const feed of feeds) { + set.add(feed); + } + } + return [...set.keys()]; +} + +export const packPythPrices = (feeds: PythPrice[] | null): Cell => { + if (!feeds || feeds.length === 0) { + return beginCell().storeUint(0, 8).endCell(); + } + + const reversedTail = feeds.slice(1).reverse(); + const packedTail = reversedTail.reduce( + (prev: Cell | null, curr) => { + const builder = beginCell().storeSlice(packPriceCell(curr, prev).beginParse()); + return builder.endCell(); + }, null + ); + const firstFeed = feeds[0]; + const builder = beginCell() + .storeUint(feeds.length, 8) + .storeSlice(packPriceCell(firstFeed, packedTail).beginParse()); + + + return builder.endCell(); +} + +export function packNamedPrices(namedPrices: Object, timeGetter: () => number) { + const pythPrices = makePythPrices(namedPrices, timeGetter); + // console.log('pyth prices: ', pythPrices); + return packPythPrices(pythPrices); +} \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/tests/utils/pyth.ts b/price_feeds/ton/pyth-connector/tests/utils/pyth.ts new file mode 100644 index 0000000..ec1c503 --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/utils/pyth.ts @@ -0,0 +1,205 @@ +/* + This update contains price information for two assets: + 1. BTC/USD (id: e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43) + price: + - price: 5924002645461 + - conf: 2528354539 + - expo: -8 + - publish_time: 1724826310 + ema_price: + - price: 5938984900000 + - conf: 2304424610 + - expo: -8 + - publish_time: 1724826310 + metadata: + - slot: 161371489 + - proof_available_time: 1724826311 + - prev_publish_time: 1724826310 + 2. ETH/USD (id: ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace) + price: + - price: 246682322909 + - conf: 87014791 + - expo: -8 + - publish_time: 1724826310 + ema_price: + - price: 247166473000 + - conf: 113877153 + - expo: -8 + - publish_time: 1724826310 + metadata: + - slot: 161371489 + - proof_available_time: 1724826311 + - prev_publish_time: 1724826310 +*/ +export const HERMES_BTC_ETH_UPDATE = + "504e41550100000003b801000000040d00a0bb18e08c0a4152eba8293e88d0ed43084dfd4677fd5dc0ff48b05d065e25511ea12181325e92541290f28e00487a7ed852fdecbee414cab803dbe1dac2392201023e177888eba8922eac9b0668566f15c61e945cd47c10fa4ca2e4d472d7d216f149f3378b4edc5f8d802c3ef9b8156ca53c9ae2d4f75dd91f7713946b4108c5910003af26c2426a1bf19f24a171bcc990dad056b670f76894e3bdb9925b21b40b3904757d8e6175133b8608431d7435e29c5fcc2912349c2c8b5588803c06f203c73401048a30050ebafd161c3cfa5531896040b6da88502734c8e42ca3197d52ea08f6ec785a24f24bc9325c16ee7b6a9791bc523523f9086162ed4ccf746b55e1b0f192010886c5256df6ca2719fe97c10b79a4a8c8574fb70add9bfe0d879ae5f6c69b2459360b50b58c43a65e881081174cce56827880e0c330b5c5681294dc3fcb78a86d010a4e0ebb1992f0e48263f6188cb5f8e871cdcd7879f54fe7ad53bbd28d5e7ff70e73441836f0d11076bd7a791aceb05d501500f6878cf26e641fffa7c8fd143371000b3ad70bd80e52a82cd740ffbd4a080bd22523bc7ac2b1242169516d7aaf2753cd7ee5b500134ef32c02284e9d806fbeab2e055ea4a94be9cbfcfbc39b249b5e6b010c97d60c0f15b18c8fb2f36331ab0a1ce0efa13e9f2118c32140bd2118823d50f12deffc40b5d0c9642b4e44a6bd1cf4f38de471536a6610e698a942f049abef35010da2004619d8b31e33037ffed4afdd97459a241dfc7fa3bcc426f175461c938a182db560547dfcdd8ede345f0cc69da33fd588c30e912b7521c3ac1b0455882628000e81868d37eb16e1988451c26cfea8bb7969ce11c89488cedea30c80e3416dd2147c0554e9a9cce1a864eb0db625baa2cbb226ae2c2f1051f84b0a711c4bf69647010f02f18088ddbabd7c4528a1f7582f5fb11e60c5e434e9fd4ca2b33d6646e2ac6e6459c651778d1531711b44d2a1204a0d9c17e218aba5e60800e80aade9f1d90400108c783ad40f93184ad4f7e84229b207b17099e78b8bd93ddf2434cba21c99b4a904d74555ced9977e6becc34fa346c3cca9332b3598e66e58eb56f9ac700074e0001270a31d95bd5426ffe943dcc2b93f05b93f8301848f0b19c56e0dea51b7742c467b6bb557f6fc6762ef4600988c2dbcad0a2be84d4c6839fbae05d227e30ce5f50166cec2c600000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71000000000493073601415557560000000000099e556100002710c0905b1576f0bb86fe861a51273f2bcc43d12dd702005500e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000005634a12c5d50000000096b3a0ebfffffff80000000066cec2c60000000066cec2c600000566c715d5a000000000895abaa20a83db6da7dfbe7cc34f56265123320d7765dda3ae132c1518c53ead6cde500c139f68a894f564d817c0dfaeefa80d4ed93d36b82f7fcfe80e4092bb54d4dae770124803c592f17cb918c9ac381ce82bd6817041aa5ae95d917d75687b7a389a188846dd79bd55cb6cb9b9d0e1c0c040f1110362a8e1e87c74887326d66b213d19f7dcd1766b6f8505be50f5c98783c07ec08f913cbe38c20a31e440e42bb5a8883356dd19288e618e938ae4e7031d52d684f7bd1ddcf0d4ff63844800e14ff0c6888ff26626ea9874005500ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace000000396f6987dd00000000052fbd87fffffff80000000066cec2c60000000066cec2c6000000398c4513280000000006c9a0a10a5307d6b780c8c5579b4b61de5fe0f12a789efdc628f14603df77ba31734c5611291df9a86e51430b243b3b61ee716bd758360783bdb7ef2f96ab8e92d509d85940a832b3de426d243a6d52ccf1e538af48b1bfad061dec1e4293898ca9ca9f37d0050d7b9e6a54357e77e7d79156fa1c70d54484ce52df49e79ab3fde203359f760fd9cf78be10644a43b52a171026829814590e8ba4853f6c8f0a16ce0fe3b532bcc96ea72723ab7d19f700c153e7cfb950f4aaa691e731e6f1de4edf43a6b9dd532a8ee785f084"; + +export const HERMES_BTC_PRICE = 5924002645461; +export const HERMES_BTC_CONF = 2528354539; +export const HERMES_BTC_EXPO = -8; +export const HERMES_BTC_PUBLISH_TIME = 1724826310; + +export const HERMES_BTC_EMA_PRICE = 5938984900000; +export const HERMES_BTC_EMA_CONF = 2304424610; +export const HERMES_BTC_EMA_EXPO = -8; +export const HERMES_BTC_EMA_PUBLISH_TIME = 1724826310; + +export const HERMES_ETH_PRICE = 246682322909; +export const HERMES_ETH_CONF = 87014791; +export const HERMES_ETH_EXPO = -8; +export const HERMES_ETH_PUBLISH_TIME = 1724826310; + +export const HERMES_ETH_EMA_PRICE = 247166473000; +export const HERMES_ETH_EMA_CONF = 113877153; +export const HERMES_ETH_EMA_EXPO = -8; +export const HERMES_ETH_EMA_PUBLISH_TIME = 1724826310; + +export const HERMES_BTC_ETH_UNIQUE_UPDATE = + "504e41550100000003b801000000040d0036bf02a43e271be952caab93581376617b6ce1ea33c6c810f2ea05dc5e73adb003369322e430b8749817093b4546523f26efb06c7e4068587691f6479cef53690002399fc49d1e2815e936e46f112a85f094633bb9af2fd414a8122013f287713c21759bc02fc95b3e2073ca907ead2db91b3c2e5701febfe24c4420f9b8e45ee938010323ce2457366f3b453bf286ef5026fd7fece8bbc9fa02f2d316df9097ad7c2ae343f75ba7ca53d701703e9f4e66bc71a8cfca8fff2087113f0469c4d901fbb2d601049960f6f319ad2e22b064529a1b40b3ea697b9692981db4940783aa271b2da68f4feaa101b58bfa127a6a7f9843b014bfb750a351f3f20287e04182153f579c380006ba22a51132b9f38de53e2f46e146b8aa49d865417c14fe7efc17ffa2c0945c7f08637107f3528e2c61e084f52248b6c13a88fa75f476b026f7db2f3aa63d9248000acbe7e8bb8057d839dd19d879a68c7f74d0dbe6450535fcddcc9ebd1d4e8aef2a2968dbcf156351ee2987e721a4b01675efe88e47ab888935edf5db5d45ad8ee8000b09a49f2a94d04d38fac6d33e9bc2518bffe5265237e36dd999f6edd328aecc47284410119c92855d55fceac24c785c9cf28942f7c0ec2cb6a87fd4d7a124eb4f000cfd5b6d77371c46f985200b90b17b1ec122e2709c5d731cc26d99a3b183eed4f0346db22eb7c2a974363bb802e19a8b7defce613374bf18eada86cb29367c73b4010d6a15c7eb185373caeace64ab63bfaa17120cfa363afd2e7568d6630300542ab6289aa39fc6b4654023c14a0c83ecfeeb26b089c87e2188319014a7c0fdf5e393000e1020e269b9d38b534395d14c05663ace5b20e6af8ff979b127caac57ec4ee14e686b5dca2ef0c4674b6bb6ac171a75cba45d6b17ef73ca342612d75e5da0942d000f5310a1d6ac0c93c8f86f6789a8d9b851ec70b78cf14cbbac7f8c6d025716ed4973c3e93ba6c5ff92986cb6fae5739812a652b85f8415183b8f4e90e7146b1340001028fcb58b991b7ba2d999421d42c76463ed42d567a46152e032bfac60ff290db4313c419f871acfb3a7fcc9b1cae99be524495a892eeff6028f7e0c7941bd8b450112eed81133c734a572b9cfc4962d5f38f9f218ec82bd6db5d68fee8e6c6c8e5b3f53f027be41ab7db81a16af16519ce0ea65b2bb02ecb07cc844d483ec77b8bfdd0166cec2c600000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71000000000493073501415557560000000000099e5560000027101e07b1de5bc0f0449e922f6ff882596667459b9702005500e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000005634a12c5d5000000009d050f6bfffffff80000000066cec2c60000000066cec2c500000566c72e3fa000000000895a5be80a56b110b67fa13e81a694f01d156b09d0fc3ca532bed56ba04cd1aadacfadccf91eb463b0da42cc74b987f26f13e89fec95a274207c9e6e019e699400d44860e9a41f258ed2ad937520be66607236e52dde013c70a704fef620f19168a9aedd126b353b1e68c388c561f62326019faf13de141d19513bbf9b750e27ed5e4e44c452c35ede3c7c7696f4a355fb2d33757c61024404bb6896da18611556b3eae69af15a3e7231465f274a8faab7851d9fd713e16ff81d4b4db112f27e69cc900e389d98cdb414eb0419005500ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace000000396f893b2e00000000057adddefffffff80000000066cec2c60000000066cec2c5000000398c4638200000000006c9b0f50a016155b4e21ff0c0c853b753dc9c9be2cc0935e428f14603df77ba31734c5611291df9a86e51430b84fea648c60abfda6c127c519d2077998ac84073283c6be89a2472767cee82c96b97e47a55bf0da45b4de6a7d5c593a7fb8164fad71749c56cda248d2bf7c00b5ed3f886936b821f6df3a9bdf4c29cf1c615be380d0d2f9e61d39c4a62371e35043550ba08fca431b0e71fd3e46cd52003f1d1f6b5f399903e6692111a348d1fbff22ee272edb2b850ee475c8b6c5b54e37c718272e02879cb13380629b28ab8"; + +export const HERMES_BTC_UNIQUE_PRICE = 5924002645461; +export const HERMES_BTC_UNIQUE_CONF = 2634354539; +export const HERMES_BTC_UNIQUE_EXPO = -8; +export const HERMES_BTC_UNIQUE_PUBLISH_TIME = 1724826310; + +export const HERMES_BTC_UNIQUE_EMA_PRICE = 5938986500000; +export const HERMES_BTC_UNIQUE_EMA_CONF = 2304400360; +export const HERMES_BTC_UNIQUE_EMA_EXPO = -8; +export const HERMES_BTC_UNIQUE_EMA_PUBLISH_TIME = 1724826310; + +export const HERMES_ETH_UNIQUE_PRICE = 246684400430; +export const HERMES_ETH_UNIQUE_CONF = 91938270; +export const HERMES_ETH_UNIQUE_EXPO = -8; +export const HERMES_ETH_UNIQUE_PUBLISH_TIME = 1724826310; + +export const HERMES_ETH_UNIQUE_EMA_PRICE = 247166548000; +export const HERMES_ETH_UNIQUE_EMA_CONF = 113881333; +export const HERMES_ETH_UNIQUE_EMA_EXPO = -8; +export const HERMES_ETH_UNIQUE_EMA_PUBLISH_TIME = 1724826310; + +export const BTC_PRICE_FEED_ID = + "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; + +export const ETH_PRICE_FEED_ID = + "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"; + +export const TEST_GUARDIAN_ADDRESS1 = + "0x686b9ea8e3237110eaaba1f1b7467559a3273819"; + +export const TEST_GUARDIAN_ADDRESS2 = + "0xbefa429d57cd18b7f8a4d91a2da9ab4af05d0fbe"; + +// A Pyth governance instruction to authorize governance data source transfer signed by the test guardian #1. +// From: target_chains/starknet/contracts/tests/data.cairo::pyth_auth_transfer() +export const PYTH_AUTHORIZE_GOVERNANCE_DATA_SOURCE_TRANSFER = + "01000000000100e4c6595b44ed764ebf9d563e8b2e8233cc24f7c35737e83c4ca1ec51f77dfd6214a146fa57420f97d51e7161342b4833b8e75c89a3895e609d7d58da7ffb5b1a000000000100000002000100000000000000000000000000000000000000000000000000000000000000290000000000000001065054474d0101ea9301000000000100b3abee2eed7d504284c57387abcbb87fe9cf4807228d2d08b776ea94347bdaa73e6958ce95a49d11b9a07fdc93a6705b666fb8a76c0cff0578eb2c881d80b29e0100000001000000020002000000000000000000000000000000000000000000000000000000000000002b0000000000000001065054474d0105ea9300000001"; + +// A Pyth governance instruction to set fee signed by the test guardian #1. +// From: target_chains/starknet/contracts/tests/data.cairo::pyth_set_fee() +export const PYTH_SET_FEE = + "010000000001006da27b990a357166853242ffec67013c89696f82d009ce79b6cb302db14f2e2e3ec3513c47ce572524ac42fedd7fb4100303baafd9ad5de6e7ed587713a36a2b010000000100000002000100000000000000000000000000000000000000000000000000000000000000290000000000000001065054474d0103ea93000000000000002a0000000000000002"; + +// A Pyth governance instruction to set data sources signed by the test guardian #1. +// From: target_chains/starknet/contracts/tests/data.cairo::pyth_set_data_sources() +export const PYTH_SET_DATA_SOURCES = + "01000000000100671d487654ad77101243588c74a9f9d90de187b9807445f9b4b0bc2eb3363b1d72aff4ad4f80a09e6cdd84e29b2e513a50efc66c979beef21ca5095d425fa9df000000000100000002000100000000000000000000000000000000000000000000000000000000000000290000000000000001065054474d0102ea930200016bb14509a612f01fbbc4cffeebd4bbfb492a86df717ebe92eb6df432a3f00a250003000000000000000000000000000000000000000000000000000000000000012d"; + +// A Pyth governance instruction to request governance data source transfer signed by the test guardian #1. +// From: target_chains/starknet/contracts/tests/data.cairo::pyth_request_transfer() +export const PYTH_REQUEST_GOVERNANCE_DATA_SOURCE_TRANSFER = + "01000000000100b3abee2eed7d504284c57387abcbb87fe9cf4807228d2d08b776ea94347bdaa73e6958ce95a49d11b9a07fdc93a6705b666fb8a76c0cff0578eb2c881d80b29e0100000001000000020002000000000000000000000000000000000000000000000000000000000000002b0000000000000001065054474d0105ea9300000001"; + +/* + This update contains price information for four assets: + 1. SOL/USD (id: ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d) + price: + - price: 11497583407 + - conf: 5485109 + - expo: -8 + - publish_time: 1743742256 + ema_price: + - price: 11524090900 + - conf: 6082720 + - expo: -8 + - publish_time: 1743742256 + metadata: + - slot: 208345111 + - proof_available_time: 1743742257 + - prev_publish_time: 1743742255 + 2. TON/USD (id: 8963217838ab4cf5cadc172203c1f0b763fbaa45f346d8ee50ba994bbcac3026) + price: + - price: 350942142 + - conf: 410727 + - expo: -8 + - publish_time: 1743742256 + ema_price: + - price: 351975630 + - conf: 406678 + - expo: -8 + - publish_time: 1743742256 + metadata: + - slot: 208345111 + - proof_available_time: 1743742257 + - prev_publish_time: 1743742255 + 3. PYTH/USD (id: 0bbf28e9a841a1cc788f6a361b17ca072d0ea3098a1e5df1c3922d06719579ff) + price: + - price: 12970522 + - conf: 16981 + - expo: -8 + - publish_time: 1743742256 + ema_price: + - price: 13006200 + - conf: 17661 + - expo: -8 + - publish_time: 1743742256 + metadata: + - slot: 208345111 + - proof_available_time: 1743742257 + - prev_publish_time: 1743742255 + 4. USDT/USD (id: 2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b) + price: + - price: 99964242 + - conf: 52240 + - expo: -8 + - publish_time: 1743742256 + ema_price: + - price: 99963419 + - conf: 65279 + - expo: -8 + - publish_time: 1743742256 + metadata: + - slot: 208345111 + - proof_available_time: 1743742257 + - prev_publish_time: 1743742255 +*/ +export const HERMES_SOL_TON_PYTH_USDT_UPDATE = + "504e41550100000003b801000000040d00a699130558c81e1326548d3e8066a9ad7e00d775bbd76ecc0d23b986eb049a2e3f8d6717318a5ac1316ca37c25251b28ecf900a86acb196e87311d1d663fc1b301022bab82b2b65d442f9018583a12a46ea6d51734dd00b8110627cb250ea45e1c040fd95b82ed60d2e998323a373e60dddf96a50f09e5b57ec8eca3b1bbcc2a3d3d0103762903038517f06d83f26e0dfc8e96aca662be75384d7533b5664cae4463e7db083c0c3a3fab130651a2e75b6e8c39cbe828025b18dbfe2869761769a27bfdab0004e55635d2887f1ed0fe95fe96396ae7fc36d9c099e3f580035b4e0dfe44e6273f31c60c3adcad567cb983dbdf954fac688d34c9b429fcf6dd09eeaa9ab64ec23401060572e9399dc50608ecabc7a58389b2df675d727e3f7f8cd697685c3555b6ff463368b535a6100e84e445a420bcd4369d1b76320cac160ba60a60a9e0ad007dad0008ab89305615bdfebdd35377dbe6e665cf4aa59150bfe18b31a545d48eb75c023242854f3a9bec6c5a38898323f56e41747f1f8c4c2d8e98fc57f3b840d120435c000a5f726e236ffdadad272a0cc5972ee326a3436db009de80dcf4aa6491a71321722c8e68a7c65ac3528b87c42a50c268148aec512defb2e074281bbb85835dbbd3000b361860629b7cea436a81f7e73d3daaa6697ab6a250a35e35ca9da121a4fc627a304b18aa2740b1f9462c28c07a60a3f964b38979ba5bc0d7f44c961637ab67d5000cc9994b20b8fa52c3d3e461a173d8ca062fc2965eefde9551d054693095a0ced619958b2c76b9f385496abb7bdf6618cccf2ffb355d43ae287504b9e4105c52f6010d69300bae67d8a39ee458f351f34ab3ae81c0ab005c9bdb70b27db266d9a105315c9259da11cdae73155d9b4db7e89da6c2367721c2e015efc8d3a6e3259cbd7d010e63d9e740f33b345557887702bc75be800631cfcad686b23d04072be36d02416e3deee9d090390651f53099df558f7e56a95e8f2cb4bb0bbfb23a74508d58f39c000f6d64d1f7e23ebc8164ebed052796ce35f150fe24a89129bd1f00b9d79b1bf4e256a308c0b77777b1fb29d72e0219867648e097f67c3782c9f3f097ffd7b237d3011088a52c3298ec2f5ea9f4b205afb9413fecbf55531399c429d79d9fed568cb7db02620c43ee573cabb4992c9f43b056bae4c19747965d366e4ac018896c43015a0067ef653000000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa7100000000075dd077014155575600000000000c6b18170000271001792ab62bbcfe4a0075c69ec3d0efc811aba31304005500ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d00000002ad4f332f000000000053b235fffffff80000000067ef65300000000067ef652f00000002aee3ac1400000000005cd0a00cba900a539aff592eec3badf4d8ce65b2f2defcd16c3707475f792d837dae4aadb7ecf0885006f67a1eeb41aea7b27d7164fe5c7d48f284aad1cdb0117fb3b3c2966dc36ad1fc2f13d80090d83f8a1cb73354aedeeacce8d01445dcaf91b0fce506b2fafa680621e940629ed7ff7e30beb0fa65f2419d692e44c3c88c780c00932c814f99c07a2c1434eeae42dab0406471a6fe43128d89e8a57239acf78b24d4886af436c8e5fa15900dc9314576a0beaa191bdf1eaea528893c7823097f5b66cd2e53a954a01e5018ec3a52b1c8fdd57b611e46c8b0bf327543d953a0168a859e832e5bbe3b68be339667fd74870dc70055008963217838ab4cf5cadc172203c1f0b763fbaa45f346d8ee50ba994bbcac30260000000014eaf3be0000000000064467fffffff80000000067ef65300000000067ef652f0000000014fab8ce00000000000634960c7ce9b36e2e29e615f61a96aab7de93a24352ec1b16d3d1817dedfc059ba8bc6122841f57c631ed55d9fc8f1255ab59fec80446534b48bef6ae94398a4152943498df64ab8617d0bcba8a34f09a910aedd4f28fb48e0b985b06b79535b0db48aa93fdeb1e9441e62912428632000ec14968f1f7e57c06932dae5d5199a436f53b39ea32534b4d831b42b1dced15f9fecf554f775dd826e2baf6dc24b50b057bb856458cb740d61ebb3573bc47e5bff2fcb8f920f1c3e05e16bb2882f9f8e9985aa95e8b0eefb49e77494dca59cdbd03b1ab12edb5b9500e660595d114a0168a859e832e5bbe3b68be339667fd74870dc70055000bbf28e9a841a1cc788f6a361b17ca072d0ea3098a1e5df1c3922d06719579ff0000000000c5ea1a0000000000004255fffffff80000000067ef65300000000067ef652f0000000000c6757800000000000044fd0cd96d0a4d7bcd660823ec72348e515182e431c160bce78f8bd62d8c4b25880827df715a65da0fc7b7d307adf46e376ce0bebbd3fbb4b57ce6abc0d535eff844a06ee0cc53b8799f034b6103c93a35f9ecee5f66cb95c873414d6d8f5ed3b9589d4d7a9e9a69da9c2becb6532935e74ecdd2c514e19e3d0f567510f182db3aad233517aac06a5a4ad07ce53304a0574a3eb9592abb0aa82de1047945659e374a7507d4fb148a9492a1ea00a7e1946f86fd2a310f8e5ed290e2d75627e1d7549cab8411b5c595b196e1494dca59cdbd03b1ab12edb5b9500e660595d114a0168a859e832e5bbe3b68be339667fd74870dc70055002b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b0000000005f55552000000000000cc10fffffff80000000067ef65300000000067ef652f0000000005f5521b000000000000feff0ce0fcd4d8f57fab900d72db1dd5bcfc0827dbf3c7f0fd02d308f63ea261b7033de5fb74e9ed7ea9fa7b1cea9c390503d0a0141ec54bb59366e9091339087139a2710703cedf2e021e244d3a0e0ec875482a744865366074d5f3c2412bb72042bf1f1f80f2cf854b3cb3ca2338b91559d5ab17404631d4f38989db698290fac116d1db42abcb0dc0f1af93d4a0a63d9f371c5d78f79248f4b4b4156dd7b044333b8c728e42c745dfb341a8802b1fd033dd7864aa0a5ed290e2d75627e1d7549cab8411b5c595b196e1494dca59cdbd03b1ab12edb5b9500e660595d114a0168a859e832e5bbe3b68be339667fd74870dc7"; + +export const SOL_PRICE_FEED_ID = + "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; +export const TON_PRICE_FEED_ID = + "0x8963217838ab4cf5cadc172203c1f0b763fbaa45f346d8ee50ba994bbcac3026"; +export const PYTH_PRICE_FEED_ID = + "0x0bbf28e9a841a1cc788f6a361b17ca072d0ea3098a1e5df1c3922d06719579ff"; +export const USDT_PRICE_FEED_ID = + "0x2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b"; + +export const HERMES_SOL_UNIQUE_PRICE = 11497583407; +export const HERMES_SOL_UNIQUE_CONF = 5485109; +export const HERMES_SOL_UNIQUE_EXPO = -8; +export const HERMES_SOL_UNIQUE_PUBLISH_TIME = 1743742256; + +export const HERMES_TON_UNIQUE_PRICE = 350942142; +export const HERMES_TON_UNIQUE_CONF = 410727; +export const HERMES_TON_UNIQUE_EXPO = -8; +export const HERMES_TON_UNIQUE_PUBLISH_TIME = 1743742256; + +export const HERMES_PYTH_UNIQUE_PRICE = 12970522; +export const HERMES_PYTH_UNIQUE_CONF = 16981; +export const HERMES_PYTH_UNIQUE_EXPO = -8; +export const HERMES_PYTH_UNIQUE_PUBLISH_TIME = 1743742256; + +export const HERMES_USDT_UNIQUE_EMA_PRICE = 99964242; +export const HERMES_USDT_UNIQUE_EMA_CONF = 52240; +export const HERMES_USDT_UNIQUE_EMA_EXPO = -8; +export const HERMES_USDT_UNIQUE_EMA_PUBLISH_TIME = 1743742256; diff --git a/price_feeds/ton/pyth-connector/tests/utils/utils.ts b/price_feeds/ton/pyth-connector/tests/utils/utils.ts new file mode 100644 index 0000000..8bf7c57 --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/utils/utils.ts @@ -0,0 +1,94 @@ +import { beginCell, Cell, Dictionary, SendMode, Transaction } from "@ton/core"; +import { Buffer } from "buffer"; +import { HermesClient, HexString } from "@pythnetwork/hermes-client"; +import { SandboxContract, TreasuryContract } from "@ton/sandbox"; +import { JettonMinter } from "@ton-community/assets-sdk"; + +const GOVERNANCE_MAGIC = 0x5054474d; +const GOVERNANCE_MODULE = 1; +const AUTHORIZE_UPGRADE_CONTRACT_ACTION = 0; +const TARGET_CHAIN_ID = 1; + +function computedGeneric(transaction: Transaction) { + if (transaction.description.type !== "generic") + throw "Expected generic transactionaction"; + if (transaction.description.computePhase.type !== "vm") + throw "Compute phase expected"; + return transaction.description.computePhase; +} + +export function printTxGasStats(name: string, transaction: Transaction) { + const txComputed = computedGeneric(transaction); + console.log(`${name} used ${txComputed.gasUsed} gas`); + console.log(`${name} gas cost: ${txComputed.gasFees}`); + return txComputed.gasFees; +} + +export function createAuthorizeUpgradePayload(newCodeHash: Buffer): Buffer { + const payload = Buffer.alloc(8); + payload.writeUInt32BE(GOVERNANCE_MAGIC, 0); + payload.writeUInt8(GOVERNANCE_MODULE, 4); + payload.writeUInt8(AUTHORIZE_UPGRADE_CONTRACT_ACTION, 5); + payload.writeUInt16BE(TARGET_CHAIN_ID, 6); + + return Buffer.concat([payload, newCodeHash]); +} + +export function expectCompareDicts(dict1: Dictionary, dict2: Dictionary) { + const keys1 = dict1.keys(); + const keys2 = dict2.keys(); + expect(keys1.sort()).toEqual(keys2.sort()); + + for (const key of keys1) { + expect(dict1.has(key)).toBe(true); + expect(dict2.has(key)).toBe(true); + expect(dict1.get(key)).toEqual(dict2.get(key)); + } +} + +export const composeFeedsCell = (feeds: HexString[]): Cell => { + if (feeds.length === 0) { + return beginCell().storeUint(0, 8).endCell(); + } + + const reversedTail = feeds.slice(1).reverse(); + const packedTail = reversedTail.reduce( + (prev: Cell | null, curr) => { + const builder = beginCell().storeUint(BigInt(curr), 256); + if (prev !== null) builder.storeRef(prev); + return builder.endCell(); + }, null + ); + const firstFeed = feeds[0]; + const builder = beginCell().storeUint(feeds.length, 8).storeUint(BigInt(firstFeed), 256); + if (packedTail !== null) { + builder.storeRef(packedTail!); + } + + return builder.endCell(); +} + +export async function sendJetton( + jetton: SandboxContract, + from: SandboxContract, + message: Cell, + value: bigint +) { + const jwAddress = await jetton.getWalletAddress(from.address); + return await from.send({ + value, + to: jwAddress, + sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, + body: message + }); +} + +export async function getPriceUpdates(hermesEndpoint: string, feedIds: HexString[]) { + const hermesClient = new HermesClient(hermesEndpoint); + const latestPriceUpdates = await hermesClient.getLatestPriceUpdates(feedIds, {encoding: 'hex'}); + + const parsed = latestPriceUpdates.parsed; + const binary = Buffer.from(latestPriceUpdates.binary.data[0], 'hex'); + + return {binary, parsed}; +} \ No newline at end of file diff --git a/price_feeds/ton/pyth-connector/tests/utils/wormhole.ts b/price_feeds/ton/pyth-connector/tests/utils/wormhole.ts new file mode 100644 index 0000000..3623240 --- /dev/null +++ b/price_feeds/ton/pyth-connector/tests/utils/wormhole.ts @@ -0,0 +1,178 @@ +import { DataSource } from "../../include/imported"; +import { beginCell, Cell, Dictionary } from "@ton/core"; + +export const GUARDIAN_SET_0 = ["0x58CC3AE5C097B213CE3C81979E1B9F9570746AA5"]; + +export const GUARDIAN_SET_1 = [ + "0x58CC3AE5C097B213CE3C81979E1B9F9570746AA5", + "0xFF6CB952589BDE862C25EF4392132FB9D4A42157", + "0x114DE8460193BDF3A2FCF81F86A09765F4762FD1", + "0x107A0086B32D7A0977926A205131D8731D39CBEB", + "0x8C82B2FD82FAED2711D59AF0F2499D16E726F6B2", + "0x11B39756C042441BE6D8650B69B54EBE715E2343", + "0x54CE5B4D348FB74B958E8966E2EC3DBD4958A7CD", + "0xEB5F7389FA26941519F0863349C223B73A6DDEE7", + "0x74A3BF913953D695260D88BC1AA25A4EEE363EF0", + "0x000AC0076727B35FBEA2DAC28FEE5CCB0FEA768E", + "0xAF45CED136B9D9E24903464AE889F5C8A723FC14", + "0xF93124B7C738843CBB89E864C862C38CDDCCCF95", + "0xD2CC37A4DC036A8D232B48F62CDD4731412F4890", + "0xDA798F6896A3331F64B48C12D1D57FD9CBE70811", + "0x71AA1BE1D36CAFE3867910F99C09E347899C19C3", + "0x8192B6E7387CCD768277C17DAB1B7A5027C0B3CF", + "0x178E21AD2E77AE06711549CFBB1F9C7A9D8096E8", + "0x5E1487F35515D02A92753504A8D75471B9F49EDB", + "0x6FBEBC898F403E4773E95FEB15E80C9A99C8348D", +]; + +export const GUARDIAN_SET_2 = [ + "0x58CC3AE5C097B213CE3C81979E1B9F9570746AA5", + "0xFF6CB952589BDE862C25EF4392132FB9D4A42157", + "0x114DE8460193BDF3A2FCF81F86A09765F4762FD1", + "0x107A0086B32D7A0977926A205131D8731D39CBEB", + "0x8C82B2FD82FAED2711D59AF0F2499D16E726F6B2", + "0x11B39756C042441BE6D8650B69B54EBE715E2343", + "0x54CE5B4D348FB74B958E8966E2EC3DBD4958A7CD", + "0x66B9590E1C41E0B226937BF9217D1D67FD4E91F5", + "0x74A3BF913953D695260D88BC1AA25A4EEE363EF0", + "0x000AC0076727B35FBEA2DAC28FEE5CCB0FEA768E", + "0xAF45CED136B9D9E24903464AE889F5C8A723FC14", + "0xF93124B7C738843CBB89E864C862C38CDDCCCF95", + "0xD2CC37A4DC036A8D232B48F62CDD4731412F4890", + "0xDA798F6896A3331F64B48C12D1D57FD9CBE70811", + "0x71AA1BE1D36CAFE3867910F99C09E347899C19C3", + "0x8192B6E7387CCD768277C17DAB1B7A5027C0B3CF", + "0x178E21AD2E77AE06711549CFBB1F9C7A9D8096E8", + "0x5E1487F35515D02A92753504A8D75471B9F49EDB", + "0x6FBEBC898F403E4773E95FEB15E80C9A99C8348D", +]; + +export const GUARDIAN_SET_3 = [ + "0x58CC3AE5C097B213CE3C81979E1B9F9570746AA5", + "0xFF6CB952589BDE862C25EF4392132FB9D4A42157", + "0x114DE8460193BDF3A2FCF81F86A09765F4762FD1", + "0x107A0086B32D7A0977926A205131D8731D39CBEB", + "0x8C82B2FD82FAED2711D59AF0F2499D16E726F6B2", + "0x11B39756C042441BE6D8650B69B54EBE715E2343", + "0x54CE5B4D348FB74B958E8966E2EC3DBD4958A7CD", + "0x15E7CAF07C4E3DC8E7C469F92C8CD88FB8005A20", + "0x74A3BF913953D695260D88BC1AA25A4EEE363EF0", + "0x000AC0076727B35FBEA2DAC28FEE5CCB0FEA768E", + "0xAF45CED136B9D9E24903464AE889F5C8A723FC14", + "0xF93124B7C738843CBB89E864C862C38CDDCCCF95", + "0xD2CC37A4DC036A8D232B48F62CDD4731412F4890", + "0xDA798F6896A3331F64B48C12D1D57FD9CBE70811", + "0x71AA1BE1D36CAFE3867910F99C09E347899C19C3", + "0x8192B6E7387CCD768277C17DAB1B7A5027C0B3CF", + "0x178E21AD2E77AE06711549CFBB1F9C7A9D8096E8", + "0x5E1487F35515D02A92753504A8D75471B9F49EDB", + "0x6FBEBC898F403E4773E95FEB15E80C9A99C8348D", +]; + +export const GUARDIAN_SET_4 = [ + "0x5893B5A76C3F739645648885BDCCC06CD70A3CD3", + "0xFF6CB952589BDE862C25EF4392132FB9D4A42157", + "0x114DE8460193BDF3A2FCF81F86A09765F4762FD1", + "0x107A0086B32D7A0977926A205131D8731D39CBEB", + "0x8C82B2FD82FAED2711D59AF0F2499D16E726F6B2", + "0x11B39756C042441BE6D8650B69B54EBE715E2343", + "0x54CE5B4D348FB74B958E8966E2EC3DBD4958A7CD", + "0x15E7CAF07C4E3DC8E7C469F92C8CD88FB8005A20", + "0x74A3BF913953D695260D88BC1AA25A4EEE363EF0", + "0x000AC0076727B35FBEA2DAC28FEE5CCB0FEA768E", + "0xAF45CED136B9D9E24903464AE889F5C8A723FC14", + "0xF93124B7C738843CBB89E864C862C38CDDCCCF95", + "0xD2CC37A4DC036A8D232B48F62CDD4731412F4890", + "0xDA798F6896A3331F64B48C12D1D57FD9CBE70811", + "0x71AA1BE1D36CAFE3867910F99C09E347899C19C3", + "0x8192B6E7387CCD768277C17DAB1B7A5027C0B3CF", + "0x178E21AD2E77AE06711549CFBB1F9C7A9D8096E8", + "0x5E1487F35515D02A92753504A8D75471B9F49EDB", + "0x6FBEBC898F403E4773E95FEB15E80C9A99C8348D", +]; + +export function createGuardianSetUpgradeBytes( + chainId: number, + newGuardianSetIndex: number, + guardians: string[], +): Buffer { + const payload = Buffer.alloc(1024); + let offset = 0; + + // Write the "Core" module (32 bytes / 256 bits) in big-endian format + // We split it into 4 64-bit chunks to ensure proper alignment and endianness + // This is necessary because the FunC code expects a 256-bit integer + // The last chunk contains the actual "Core" string (0x436f7265) at the end, + // preceded by leading zeros to fill the 64 bits + payload.writeBigUInt64BE(BigInt("0x0000000000000000"), offset); + offset += 8; + payload.writeBigUInt64BE(BigInt("0x0000000000000000"), offset); + offset += 8; + payload.writeBigUInt64BE(BigInt("0x0000000000000000"), offset); + offset += 8; + payload.writeBigUInt64BE(BigInt("0x00000000436f7265"), offset); + offset += 8; + + // Action (2 for GuardianSetUpgrade) + payload.writeUInt8(2, offset); + offset += 1; + + // Chain ID + payload.writeUInt16BE(chainId, offset); + offset += 2; + + // New Guardian Set Index + payload.writeUInt32BE(newGuardianSetIndex, offset); + offset += 4; + + // Number of guardians + payload.writeUInt8(guardians.length, offset); + offset += 1; + + // Guardian addresses + for (const guardian of guardians) { + payload.write(guardian.slice(2), offset, 20, "hex"); + offset += 20; + } + + return payload.subarray(0, offset); +} + +export function createGuardianSetsDict( + guardianSet: string[], + guardianSetIndex: number, +): Dictionary { + const guardianSetDict = Dictionary.empty( + Dictionary.Keys.Uint(8), + Dictionary.Values.Buffer(20), + ); + guardianSet.forEach((key, index) => { + guardianSetDict.set(index, Buffer.from(key.slice(2), "hex")); + }); + + const guardianSets = Dictionary.empty( + Dictionary.Keys.Uint(32), + Dictionary.Values.Cell(), + ); + const guardianSetCell = beginCell() + .storeUint(0, 64) // expiration_time, set to 0 for testing + .storeDict(guardianSetDict) + .endCell(); + guardianSets.set(guardianSetIndex, guardianSetCell); + + return guardianSets; +} + +// Taken from https://github.com/pyth-network/pyth-crosschain/blob/main/contract_manager/src/contracts/wormhole.ts#L32-L37 +export const MAINNET_UPGRADE_VAAS = [ + "010000000001007ac31b282c2aeeeb37f3385ee0de5f8e421d30b9e5ae8ba3d4375c1c77a86e77159bb697d9c456d6f8c02d22a94b1279b65b0d6a9957e7d3857423845ac758e300610ac1d2000000030001000000000000000000000000000000000000000000000000000000000000000400000000000005390000000000000000000000000000000000000000000000000000000000436f7265020000000000011358cc3ae5c097b213ce3c81979e1b9f9570746aa5ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cdeb5f7389fa26941519f0863349c223b73a6ddee774a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d", + "01000000010d0012e6b39c6da90c5dfd3c228edbb78c7a4c97c488ff8a346d161a91db067e51d638c17216f368aa9bdf4836b8645a98018ca67d2fec87d769cabfdf2406bf790a0002ef42b288091a670ef3556596f4f47323717882881eaf38e03345078d07a156f312b785b64dae6e9a87e3d32872f59cb1931f728cecf511762981baf48303668f0103cef2616b84c4e511ff03329e0853f1bd7ee9ac5ba71d70a4d76108bddf94f69c2a8a84e4ee94065e8003c334e899184943634e12043d0dda78d93996da073d190104e76d166b9dac98f602107cc4b44ac82868faf00b63df7d24f177aa391e050902413b71046434e67c770b19aecdf7fce1d1435ea0be7262e3e4c18f50ddc8175c0105d9450e8216d741e0206a50f93b750a47e0a258b80eb8fed1314cc300b3d905092de25cd36d366097b7103ae2d184121329ba3aa2d7c6cc53273f11af14798110010687477c8deec89d36a23e7948feb074df95362fc8dcbd8ae910ac556a1dee1e755c56b9db5d710c940938ed79bc1895a3646523a58bc55f475a23435a373ecfdd0107fb06734864f79def4e192497362513171530daea81f07fbb9f698afe7e66c6d44db21323144f2657d4a5386a954bb94eef9f64148c33aef6e477eafa2c5c984c01088769e82216310d1827d9bd48645ec23e90de4ef8a8de99e2d351d1df318608566248d80cdc83bdcac382b3c30c670352be87f9069aab5037d0b747208eae9c650109e9796497ff9106d0d1c62e184d83716282870cef61a1ee13d6fc485b521adcce255c96f7d1bca8d8e7e7d454b65783a830bddc9d94092091a268d311ecd84c26010c468c9fb6d41026841ff9f8d7368fa309d4dbea3ea4bbd2feccf94a92cc8a20a226338a8e2126cd16f70eaf15b4fc9be2c3fa19def14e071956a605e9d1ac4162010e23fcb6bd445b7c25afb722250c1acbc061ed964ba9de1326609ae012acdfb96942b2a102a2de99ab96327859a34a2b49a767dbdb62e0a1fb26af60fe44fd496a00106bb0bac77ac68b347645f2fb1ad789ea9bd76fb9b2324f25ae06f97e65246f142df717f662e73948317182c62ce87d79c73def0dba12e5242dfc038382812cfe00126da03c5e56cb15aeeceadc1e17a45753ab4dc0ec7bf6a75ca03143ed4a294f6f61bc3f478a457833e43084ecd7c985bf2f55a55f168aac0e030fc49e845e497101626e9d9a5d9e343f00010000000000000000000000000000000000000000000000000000000000000004c1759167c43f501c2000000000000000000000000000000000000000000000000000000000436f7265020000000000021358cc3ae5c097b213ce3c81979e1b9f9570746aa5ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cd66b9590e1c41e0b226937bf9217d1d67fd4e91f574a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d", + "01000000020d00ce45474d9e1b1e7790a2d210871e195db53a70ffd6f237cfe70e2686a32859ac43c84a332267a8ef66f59719cf91cc8df0101fd7c36aa1878d5139241660edc0010375cc906156ae530786661c0cd9aef444747bc3d8d5aa84cac6a6d2933d4e1a031cffa30383d4af8131e929d9f203f460b07309a647d6cd32ab1cc7724089392c000452305156cfc90343128f97e499311b5cae174f488ff22fbc09591991a0a73d8e6af3afb8a5968441d3ab8437836407481739e9850ad5c95e6acfcc871e951bc30105a7956eefc23e7c945a1966d5ddbe9e4be376c2f54e45e3d5da88c2f8692510c7429b1ea860ae94d929bd97e84923a18187e777aa3db419813a80deb84cc8d22b00061b2a4f3d2666608e0aa96737689e3ba5793810ff3a52ff28ad57d8efb20967735dc5537a2e43ef10f583d144c12a1606542c207f5b79af08c38656d3ac40713301086b62c8e130af3411b3c0d91b5b50dcb01ed5f293963f901fc36e7b0e50114dce203373b32eb45971cef8288e5d928d0ed51cd86e2a3006b0af6a65c396c009080009e93ab4d2c8228901a5f4525934000b2c26d1dc679a05e47fdf0ff3231d98fbc207103159ff4116df2832eea69b38275283434e6cd4a4af04d25fa7a82990b707010aa643f4cf615dfff06ffd65830f7f6cf6512dabc3690d5d9e210fdc712842dc2708b8b2c22e224c99280cd25e5e8bfb40e3d1c55b8c41774e287c1e2c352aecfc010b89c1e85faa20a30601964ccc6a79c0ae53cfd26fb10863db37783428cd91390a163346558239db3cd9d420cfe423a0df84c84399790e2e308011b4b63e6b8015010ca31dcb564ac81a053a268d8090e72097f94f366711d0c5d13815af1ec7d47e662e2d1bde22678113d15963da100b668ba26c0c325970d07114b83c5698f46097010dc9fda39c0d592d9ed92cd22b5425cc6b37430e236f02d0d1f8a2ef45a00bde26223c0a6eb363c8b25fd3bf57234a1d9364976cefb8360e755a267cbbb674b39501108db01e444ab1003dd8b6c96f8eb77958b40ba7a85fefecf32ad00b7a47c0ae7524216262495977e09c0989dd50f280c21453d3756843608eacd17f4fdfe47600001261025228ef5af837cb060bcd986fcfa84ccef75b3fa100468cfd24e7fadf99163938f3b841a33496c2706d0208faab088bd155b2e20fd74c625bb1cc8c43677a0163c53c409e0c5dfa000100000000000000000000000000000000000000000000000000000000000000046c5a054d7833d1e42000000000000000000000000000000000000000000000000000000000436f7265020000000000031358cc3ae5c097b213ce3c81979e1b9f9570746aa5ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cd15e7caf07c4e3dc8e7c469f92c8cd88fb8005a2074a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d", + "01000000030d03d4a37a6ff4361d91714730831e9d49785f61624c8f348a9c6c1d82bc1d98cadc5e936338204445c6250bb4928f3f3e165ad47ca03a5d63111168a2de4576856301049a5df10464ea4e1961589fd30fc18d1970a7a2ffaad617e56a0f7777f25275253af7d10a0f0f2494dc6e99fc80e444ab9ebbbee252ded2d5dcb50cbf7a54bb5a01055f4603b553b9ba9e224f9c55c7bca3da00abb10abd19e0081aecd3b352be061a70f79f5f388ebe5190838ef3cd13a2f22459c9a94206883b739c90b40d5d74640006a8fade3997f650a36e46bceb1f609edff201ab32362266f166c5c7da713f6a19590c20b68ed3f0119cb24813c727560ede086b3d610c2d7a1efa66f655bad90900080f5e495a75ea52241c59d145c616bfac01e57182ad8d784cbcc9862ed3afb60c0983ccbc690553961ffcf115a0c917367daada8e60be2cbb8b8008bac6341a8c010935ab11e0eea28b87a1edc5ccce3f1fac25f75b5f640fe6b0673a7cd74513c9dc01c544216cf364cc9993b09fda612e0cd1ced9c00fb668b872a16a64ebb55d27010ab2bc39617a2396e7defa24cd7c22f42dc31f3c42ffcd9d1472b02df8468a4d0563911e8fb6a4b5b0ce0bd505daa53779b08ff660967b31f246126ed7f6f29a7e000bdb6d3fd7b33bdc9ac3992916eb4aacb97e7e21d19649e7fa28d2dd6e337937e4274516a96c13ac7a8895da9f91948ea3a09c25f44b982c62ce8842b58e20c8a9000d3d1b19c8bb000856b6610b9d28abde6c35cb7705c6ca5db711f7be96d60eed9d72cfa402a6bfe8bf0496dbc7af35796fc768da51a067b95941b3712dce8ae1e7010ec80085033157fd1a5628fc0c56267469a86f0e5a66d7dede1ad4ce74ecc3dff95b60307a39c3bfbeedc915075070da30d0395def9635130584f709b3885e1bdc0010fc480eb9ee715a2d151b23722b48b42581d7f4001fc1696c75425040bfc1ffc5394fe418adb2b64bd3dc692efda4cc408163677dbe233b16bcdabb853a20843301118ee9e115e1a0c981f19d0772b850e666591322da742a9a12cce9f52a5665bd474abdd59c580016bee8aae67fdf39b315be2528d12eec3a652910e03cc4c6fa3801129d0d1e2e429e969918ec163d16a7a5b2c6729aa44af5dccad07d25d19891556a79b574f42d9adbd9e2a9ae5a6b8750331d2fccb328dd94c3bf8791ee1bfe85aa00661e99781981faea00010000000000000000000000000000000000000000000000000000000000000004fd4c6c55ec8dfd342000000000000000000000000000000000000000000000000000000000436f726502000000000004135893b5a76c3f739645648885bdccc06cd70a3cd3ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cd15e7caf07c4e3dc8e7c469f92c8cd88fb8005a2074a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d", +]; + +export const GOVERNANCE_DATA_SOURCE: DataSource = { + emitterChain: 1, + emitterAddress: + "5635979a221c34931e32620b9293a463065555ea71fe97cd6237ade875b12e9e", +}; diff --git a/price_feeds/ton/pyth-connector/tsconfig.json b/price_feeds/ton/pyth-connector/tsconfig.json new file mode 100644 index 0000000..6ba4042 --- /dev/null +++ b/price_feeds/ton/pyth-connector/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "outDir": "dist", + "module": "commonjs", + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "node", + }, + + "include": ["./wrappers", "./tests", "./scripts", "./include", "./index.ts", "./contracts", "./build"] +} diff --git a/price_feeds/ton/pyth-connector/wrappers/BaseWrapper.ts b/price_feeds/ton/pyth-connector/wrappers/BaseWrapper.ts new file mode 100644 index 0000000..fdd165e --- /dev/null +++ b/price_feeds/ton/pyth-connector/wrappers/BaseWrapper.ts @@ -0,0 +1,314 @@ +import { + Address, + beginCell, + Cell, + Contract, + ContractProvider, + Dictionary, + Sender, + SendMode, + toNano, +} from "@ton/core"; +import { createCellChain } from "@pythnetwork/pyth-ton-js"; +import { createGuardianSetsDict } from "../tests/utils/wormhole"; +import { HexString, Price } from "@pythnetwork/price-service-sdk"; +import { DataSource } from "../include/imported"; + +export class BaseWrapper implements Contract { + constructor( + readonly address: Address, + readonly init?: { code: Cell; data: Cell }, + ) {} + + static createFromAddress(address: Address) { + return new this(address); + } + + static createInitData(config: { + priceFeedId?: HexString; + price?: Price; + emaPrice?: Price; + singleUpdateFee?: number; + dataSources?: DataSource[]; + guardianSetIndex: number; + guardianSet: string[]; + chainId: number; + governanceChainId: number; + governanceContract: string; + governanceDataSource?: DataSource; + upgradeCodeHash?: number|bigint + }): Cell { + const priceDict = Dictionary.empty( + Dictionary.Keys.BigUint(256), + Dictionary.Values.Cell(), + ); + + if (config.priceFeedId && config.price && config.emaPrice) { + const priceCell = beginCell() + .storeInt( + config.price.getPriceAsNumberUnchecked() * 10 ** -config.price.expo, + 64, + ) + .storeUint( + config.price.getConfAsNumberUnchecked() * 10 ** -config.price.expo, + 64, + ) + .storeInt(config.price.expo, 32) + .storeUint(config.price.publishTime, 64) + .endCell(); + + const emaPriceCell = beginCell() + .storeInt( + config.emaPrice.getPriceAsNumberUnchecked() * + 10 ** -config.emaPrice.expo, + 64, + ) + .storeUint( + config.emaPrice.getConfAsNumberUnchecked() * + 10 ** -config.emaPrice.expo, + 64, + ) + .storeInt(config.emaPrice.expo, 32) + .storeUint(config.emaPrice.publishTime, 64) + .endCell(); + + const priceFeedCell = beginCell() + .storeRef(priceCell) + .storeRef(emaPriceCell) + .endCell(); + + priceDict.set(BigInt(config.priceFeedId), priceFeedCell); + } + + // Create a dictionary for valid data sources + const isValidDataSourceDict = Dictionary.empty( + Dictionary.Keys.BigUint(256), + Dictionary.Values.Bool(), + ); + + if (config.dataSources) { + config.dataSources.forEach((source) => { + const sourceCell = beginCell() + .storeUint(source.emitterChain, 16) + .storeBuffer(Buffer.from(source.emitterAddress, "hex")) + .endCell(); + const cellHash = BigInt("0x" + sourceCell.hash().toString("hex")); + isValidDataSourceDict.set(cellHash, true); + }); + } + + // Group price feeds and update fee + const priceFeedsCell = beginCell() + .storeDict(priceDict) + .storeUint(config.singleUpdateFee || 0, 256) + .endCell(); + + // Group data sources information + const dataSourcesCell = beginCell() + .storeDict(isValidDataSourceDict) + .endCell(); + + // Group guardian set information + const guardianSetCell = beginCell() + .storeUint(config.guardianSetIndex, 32) + .storeDict( + createGuardianSetsDict(config.guardianSet, config.guardianSetIndex), + ) + .endCell(); + + // Group chain and governance information + const governanceCell = beginCell() + .storeUint(config.chainId, 16) + .storeUint(config.governanceChainId, 16) + .storeBuffer(Buffer.from(config.governanceContract, "hex")) + .storeDict(Dictionary.empty()) // consumed_governance_actions + .storeRef( + config.governanceDataSource + ? beginCell() + .storeUint(config.governanceDataSource.emitterChain, 16) + .storeBuffer( + Buffer.from(config.governanceDataSource.emitterAddress, "hex"), + ) + .endCell() + : beginCell().endCell(), + ) // governance_data_source + .storeUint(0, 64) // last_executed_governance_sequence, set to 0 for initial state + .storeUint(0, 32) // governance_data_source_index, set to 0 for initial state + .storeUint(config.upgradeCodeHash ?? 0, 256) // upgrade_code_hash, set to 0 for initial state + .endCell(); + + // Create the main cell with references to grouped data + return beginCell() + .storeRef(priceFeedsCell) + .storeRef(dataSourcesCell) + .storeRef(guardianSetCell) + .storeRef(governanceCell) + .endCell(); + } + + async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { + await provider.internal(via, { + value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell().endCell(), + }); + } + + async getCurrentGuardianSetIndex( + provider: ContractProvider, + methodName: string, + ) { + const result = await provider.get(methodName, []); + return result.stack.readNumber(); + } + + async sendUpdateGuardianSet( + provider: ContractProvider, + via: Sender, + vm: Buffer, + ) { + const messageBody = beginCell() + .storeUint(1, 32) // OP_UPDATE_GUARDIAN_SET + .storeRef(createCellChain(vm)) + .endCell(); + + await provider.internal(via, { + value: toNano("0.1"), + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageBody, + }); + } + + async sendUpdatePriceFeeds( + provider: ContractProvider, + via: Sender, + updateData: Buffer, + updateFee: bigint, + ) { + const messageBody = beginCell() + .storeUint(2, 32) // OP_UPDATE_PRICE_FEEDS + .storeRef(createCellChain(updateData)) + .endCell(); + + await provider.internal(via, { + value: updateFee, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageBody, + }); + } + + async getChainId(provider: ContractProvider, methodName: string) { + const result = await provider.get(methodName, []); + return result.stack.readNumber(); + } + + async getPriceUnsafe( + provider: ContractProvider, + priceFeedId: HexString, + methodName: string, + ) { + const result = await provider.get(methodName, [ + { type: "int", value: BigInt(priceFeedId) }, + ]); + + const price = result.stack.readNumber(); + const conf = result.stack.readNumber(); + const expo = result.stack.readNumber(); + const publishTime = result.stack.readNumber(); + + return { + price, + conf, + expo, + publishTime, + }; + } + + async getPriceNoOlderThan( + provider: ContractProvider, + timePeriod: number, + priceFeedId: HexString, + methodName: string, + ) { + const result = await provider.get(methodName, [ + { type: "int", value: BigInt(timePeriod) }, + { type: "int", value: BigInt(priceFeedId) }, + ]); + + const price = result.stack.readNumber(); + const conf = result.stack.readNumber(); + const expo = result.stack.readNumber(); + const publishTime = result.stack.readNumber(); + + return { + price, + conf, + expo, + publishTime, + }; + } + + async getEmaPriceUnsafe( + provider: ContractProvider, + priceFeedId: HexString, + methodName: string, + ) { + const result = await provider.get(methodName, [ + { type: "int", value: BigInt(priceFeedId) }, + ]); + + const price = result.stack.readNumber(); + const conf = result.stack.readNumber(); + const expo = result.stack.readNumber(); + const publishTime = result.stack.readNumber(); + + return { + price, + conf, + expo, + publishTime, + }; + } + + async getEmaPriceNoOlderThan( + provider: ContractProvider, + timePeriod: number, + priceFeedId: HexString, + methodName: string, + ) { + const result = await provider.get(methodName, [ + { type: "int", value: BigInt(timePeriod) }, + { type: "int", value: BigInt(priceFeedId) }, + ]); + + const price = result.stack.readNumber(); + const conf = result.stack.readNumber(); + const expo = result.stack.readNumber(); + const publishTime = result.stack.readNumber(); + + return { + price, + conf, + expo, + publishTime, + }; + } + + async getUpdateFee( + provider: ContractProvider, + vm: Buffer, + methodName: string, + ) { + const result = await provider.get(methodName, [ + { type: "slice", cell: createCellChain(vm) }, + ]); + + return result.stack.readNumber(); + } + + async getSingleUpdateFee(provider: ContractProvider, methodName: string) { + const result = await provider.get(methodName, []); + + return result.stack.readNumber(); + } +} diff --git a/price_feeds/ton/pyth-connector/wrappers/Main.compile.ts b/price_feeds/ton/pyth-connector/wrappers/Main.compile.ts new file mode 100644 index 0000000..b06f8ef --- /dev/null +++ b/price_feeds/ton/pyth-connector/wrappers/Main.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from "@ton/blueprint"; + +export const compile: CompilerConfig = { + lang: "func", + targets: ["contracts/Pyth/Main.fc", "contracts/Pyth/Pyth.fc", "contracts/Pyth/Wormhole.fc"], +}; diff --git a/price_feeds/ton/pyth-connector/wrappers/Main.ts b/price_feeds/ton/pyth-connector/wrappers/Main.ts new file mode 100644 index 0000000..73d5766 --- /dev/null +++ b/price_feeds/ton/pyth-connector/wrappers/Main.ts @@ -0,0 +1,111 @@ +import { Cell, contractAddress, ContractProvider, Sender } from "@ton/core"; +import { HexString } from "@pythnetwork/price-service-sdk"; + +import { BaseWrapper } from "./BaseWrapper"; +import { DataSource } from "../include/imported"; + +export type MainConfig = { + singleUpdateFee: number; + dataSources: DataSource[]; + guardianSetIndex: number; + guardianSet: string[]; + chainId: number; + governanceChainId: number; + governanceContract: string; + governanceDataSource?: DataSource; + upgradeCodeHash?: number | bigint; +}; + +export class Main extends BaseWrapper { + static createFromConfig(config: MainConfig, code: Cell, workchain = 0) { + const data = Main.mainConfigToCell(config); + const init = { code, data }; + return new Main(contractAddress(workchain, init), init); + } + + static mainConfigToCell(config: MainConfig): Cell { + return BaseWrapper.createInitData(config); + } + + async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { + await super.sendDeploy(provider, via, value); + } + + async getCurrentGuardianSetIndex(provider: ContractProvider) { + return await super.getCurrentGuardianSetIndex( + provider, + "get_current_guardian_set_index", + ); + } + + async sendUpdateGuardianSet( + provider: ContractProvider, + via: Sender, + vm: Buffer, + ) { + await super.sendUpdateGuardianSet(provider, via, vm); + } + + async sendUpdatePriceFeeds( + provider: ContractProvider, + via: Sender, + updateData: Buffer, + updateFee: bigint, + ) { + await super.sendUpdatePriceFeeds(provider, via, updateData, updateFee); + } + + async getPriceUnsafe(provider: ContractProvider, priceFeedId: HexString) { + return await super.getPriceUnsafe( + provider, + priceFeedId, + "get_price_unsafe", + ); + } + + async getPriceNoOlderThan( + provider: ContractProvider, + timePeriod: number, + priceFeedId: HexString, + ) { + return await super.getPriceNoOlderThan( + provider, + timePeriod, + priceFeedId, + "get_price_no_older_than", + ); + } + + async getEmaPriceUnsafe(provider: ContractProvider, priceFeedId: HexString) { + return await super.getEmaPriceUnsafe( + provider, + priceFeedId, + "get_ema_price_unsafe", + ); + } + + async getEmaPriceNoOlderThan( + provider: ContractProvider, + timePeriod: number, + priceFeedId: HexString, + ) { + return await super.getEmaPriceNoOlderThan( + provider, + timePeriod, + priceFeedId, + "get_ema_price_no_older_than", + ); + } + + async getUpdateFee(provider: ContractProvider, vm: Buffer) { + return await super.getUpdateFee(provider, vm, "get_update_fee"); + } + + async getSingleUpdateFee(provider: ContractProvider) { + return await super.getSingleUpdateFee(provider, "get_single_update_fee"); + } + + async getChainId(provider: ContractProvider) { + return await super.getChainId(provider, "get_chain_id"); + } +} diff --git a/price_feeds/ton/pyth-connector/wrappers/MainNoCheck.compile.ts b/price_feeds/ton/pyth-connector/wrappers/MainNoCheck.compile.ts new file mode 100644 index 0000000..914dbb1 --- /dev/null +++ b/price_feeds/ton/pyth-connector/wrappers/MainNoCheck.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from "@ton/blueprint"; + +export const compile: CompilerConfig = { + lang: "func", + targets: ["contracts/Pyth/MainNoCheck.fc"], +}; diff --git a/price_feeds/ton/pyth-connector/wrappers/PythConnector.compile.ts b/price_feeds/ton/pyth-connector/wrappers/PythConnector.compile.ts new file mode 100644 index 0000000..80d0868 --- /dev/null +++ b/price_feeds/ton/pyth-connector/wrappers/PythConnector.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from '@ton/blueprint'; + +export const compile: CompilerConfig = { + lang: 'func', + targets: ['contracts/PythConnector/pyth_connector.fc'], +}; diff --git a/price_feeds/ton/pyth-connector/wrappers/PythConnector.ts b/price_feeds/ton/pyth-connector/wrappers/PythConnector.ts new file mode 100644 index 0000000..b3bc9b5 --- /dev/null +++ b/price_feeds/ton/pyth-connector/wrappers/PythConnector.ts @@ -0,0 +1,151 @@ +import {createCellChain} from '@pythnetwork/pyth-ton-js'; +import { + Address, + beginCell, + Cell, + Contract, + contractAddress, + ContractProvider, + Dictionary, + Sender, + SendMode +} from '@ton/core'; + +export type PythConnectorConfig = { + pythAddress: Address; + ids: Dictionary; +}; + +export function makeEmptyIds(): Dictionary { + return Dictionary.empty( + Dictionary.Keys.BigInt(256), + Dictionary.Values.Buffer(64)); +} + +export function PythConnectorConfigToCell(config: PythConnectorConfig): Cell { + return beginCell() + .storeAddress(config.pythAddress) + .storeDict(makeEmptyIds()) + .endCell(); +} + +export const Opcodes = { + customOperationUnique: 1, + configure: 2, + customOperation: 3, + parseUniquePriceFeedUpdates: 6, +}; + +export type EvaaPythTuple = { + evaa_id: bigint; + reffered_id: bigint; +}; + +export class PythConnector implements Contract { + constructor( + readonly address: Address, + readonly ids: Dictionary, + readonly init?: { code: Cell; data: Cell }, + ) { + } + + static createFromAddress(address: Address) { + return new PythConnector(address, makeEmptyIds()); + } + + static createFromConfig(config: PythConnectorConfig, code: Cell, workchain = 0) { + const data = PythConnectorConfigToCell(config); + const init = {code, data}; + return new PythConnector(contractAddress(workchain, init), config.ids, init); + } + + async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { + await provider.internal(via, { + value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell().endCell(), + }); + } + + async sendConfigure( + provider: ContractProvider, + via: Sender, + opts: { + value: bigint; + pythAddress: Address; + feedsMap: Dictionary; + } + ) { + console.log('feedsMap: ', opts.feedsMap); + return provider.internal(via, { + value: opts.value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell() + .storeUint(Opcodes.configure, 32) + .storeAddress(opts.pythAddress) + .storeDict(opts.feedsMap, Dictionary.Keys.BigUint(256), Dictionary.Values.Buffer(64)) + .endCell() + }) + } + + async sendCustomOperation( + provider: ContractProvider, + via: Sender, + opts: { + queryId: number; + value: bigint; + updateData: Buffer; + pythPriceIds: Cell; + publishTimeGap: number; + maxStaleness: number; + payload: Cell; + } + ) { + return provider.internal(via, { + value: opts.value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell() + .storeUint(Opcodes.customOperation, 32) + .storeUint(opts.queryId, 64) + .storeUint(opts.publishTimeGap, 64) + .storeUint(opts.maxStaleness, 64) + .storeRef(createCellChain(opts.updateData)) + .storeRef(opts.pythPriceIds) + .storeMaybeRef(opts.payload) + .endCell(), + }); + } + + + + async sendOnchainGetterOperation(provider: ContractProvider, via: Sender, opts: { + }) { + + } + + async sendWithdrawLikeJetton(provider: ContractProvider, via: Sender, opts: { + queryId: number; + value: bigint; + updateData: Buffer; + pythPriceIds: Cell; + publishTimeGap: number; + maxStaleness: number; + + }) {} + + async getPythAddress(provider: ContractProvider) { + const result = await provider.get('get_pyth_address', []); + return result.stack.readAddress(); + } + + async getFeedsMap(provider: ContractProvider) { + const result = await provider.get('get_feeds_dict', []); + const stack = result.stack; + if (stack.remaining === 0) { + return Dictionary.empty(Dictionary.Keys.BigUint(256), Dictionary.Values.Buffer(64)); + } + + const dictCell = stack.readCell(); + return Dictionary.loadDirect(Dictionary.Keys.BigUint(256), Dictionary.Values.Buffer(64), dictCell); + } +} diff --git a/price_feeds/ton/pyth-connector/wrappers/PythTest.compile.ts b/price_feeds/ton/pyth-connector/wrappers/PythTest.compile.ts new file mode 100644 index 0000000..a6fedfb --- /dev/null +++ b/price_feeds/ton/pyth-connector/wrappers/PythTest.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from "@ton/blueprint"; + +export const compile: CompilerConfig = { + lang: "func", + targets: ["contracts/Pyth/tests/PythTest.fc"], +}; diff --git a/price_feeds/ton/pyth-connector/wrappers/PythTest.ts b/price_feeds/ton/pyth-connector/wrappers/PythTest.ts new file mode 100644 index 0000000..effdf7a --- /dev/null +++ b/price_feeds/ton/pyth-connector/wrappers/PythTest.ts @@ -0,0 +1,291 @@ +import { + beginCell, + Cell, + contractAddress, + ContractProvider, + Sender, + SendMode, + toNano, + Address, +} from "@ton/core"; +import { BaseWrapper } from "./BaseWrapper"; +import { HexString, Price } from "@pythnetwork/price-service-sdk"; +import { + createCellChain, + parseDataSource, + parseDataSources, +} from "@pythnetwork/pyth-ton-js"; +import { DataSource } from "../include/imported"; + +export type PythTestConfig = { + priceFeedId: HexString; + price: Price; + emaPrice: Price; + singleUpdateFee: number; + dataSources: DataSource[]; + guardianSetIndex: number; + guardianSet: string[]; + chainId: number; + governanceChainId: number; + governanceContract: string; + governanceDataSource?: DataSource; +}; + +export class PythTest extends BaseWrapper { + static createFromConfig(config: PythTestConfig, code: Cell, workchain = 0) { + const data = PythTest.getPythInitData(config); + const init = { code, data }; + return new PythTest(contractAddress(workchain, init), init); + } + + static getPythInitData(config: PythTestConfig): Cell { + return BaseWrapper.createInitData(config); + } + + async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { + await super.sendDeploy(provider, via, value); + } + + async getPriceUnsafe(provider: ContractProvider, priceFeedId: HexString) { + return await super.getPriceUnsafe( + provider, + priceFeedId, + "test_get_price_unsafe", + ); + } + + async getPriceNoOlderThan( + provider: ContractProvider, + timePeriod: number, + priceFeedId: HexString, + ) { + return await super.getPriceNoOlderThan( + provider, + timePeriod, + priceFeedId, + "test_get_price_no_older_than", + ); + } + + async getEmaPriceUnsafe(provider: ContractProvider, priceFeedId: HexString) { + return await super.getEmaPriceUnsafe( + provider, + priceFeedId, + "test_get_ema_price_unsafe", + ); + } + + async getEmaPriceNoOlderThan( + provider: ContractProvider, + timePeriod: number, + priceFeedId: HexString, + ) { + return await super.getEmaPriceNoOlderThan( + provider, + timePeriod, + priceFeedId, + "test_get_ema_price_no_older_than", + ); + } + + async getUpdateFee(provider: ContractProvider, vm: Buffer) { + return await super.getUpdateFee(provider, vm, "test_get_update_fee"); + } + + async getSingleUpdateFee(provider: ContractProvider) { + return await super.getSingleUpdateFee( + provider, + "test_get_single_update_fee", + ); + } + + async sendUpdatePriceFeeds( + provider: ContractProvider, + via: Sender, + updateData: Buffer, + updateFee: bigint, + ) { + await super.sendUpdatePriceFeeds(provider, via, updateData, updateFee); + } + + async sendUpdateGuardianSet( + provider: ContractProvider, + via: Sender, + vm: Buffer, + ) { + await super.sendUpdateGuardianSet(provider, via, vm); + } + + async getChainId(provider: ContractProvider) { + return await super.getChainId(provider, "test_get_chain_id"); + } + + // Add PythTest-specific methods here + async getLastExecutedGovernanceSequence(provider: ContractProvider) { + const result = await provider.get( + "test_get_last_executed_governance_sequence", + [], + ); + return result.stack.readNumber(); + } + + async getGovernanceDataSourceIndex(provider: ContractProvider) { + const result = await provider.get( + "test_get_governance_data_source_index", + [], + ); + return result.stack.readNumber(); + } + + async getGovernanceDataSource(provider: ContractProvider) { + const result = await provider.get("test_get_governance_data_source", []); + return parseDataSource(result.stack.readCell()); + } + + async sendExecuteGovernanceAction( + provider: ContractProvider, + via: Sender, + governanceAction: Buffer, + ) { + const messageBody = beginCell() + .storeUint(3, 32) // OP_EXECUTE_GOVERNANCE_ACTION + .storeRef(createCellChain(governanceAction)) + .endCell(); + + await provider.internal(via, { + value: toNano("0.1"), + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageBody, + }); + } + + async sendUpgradeContract( + provider: ContractProvider, + via: Sender, + newCode: Cell, + ) { + const messageBody = beginCell() + .storeUint(4, 32) // OP_UPGRADE_CONTRACT + .storeRef(newCode) + .endCell(); + + await provider.internal(via, { + value: toNano("0.1"), + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageBody, + }); + } + + async getIsValidDataSource( + provider: ContractProvider, + dataSource: DataSource, + ) { + const result = await provider.get("test_get_is_valid_data_source", [ + { + type: "cell", + cell: beginCell() + .storeUint(dataSource.emitterChain, 16) + .storeUint(BigInt("0x" + dataSource.emitterAddress), 256) + .endCell(), + }, + ]); + return result.stack.readBoolean(); + } + + async getDataSources(provider: ContractProvider) { + const result = await provider.get("test_get_data_sources", []); + return parseDataSources(result.stack.readCell()); + } + + private createPriceFeedMessage( + op: number, + updateData: Buffer, + priceIds: HexString[], + time1: number, + time2: number, + targetAddress: Address, + customPayload: Buffer, + ): Cell { + // Create a buffer for price IDs: 1 byte length + (32 bytes per ID) + const priceIdsBuffer = Buffer.alloc(1 + priceIds.length * 32); + priceIdsBuffer.writeUint8(priceIds.length, 0); + + // Write each price ID as a 32-byte value + priceIds.forEach((id, index) => { + // Remove '0x' prefix if present and pad to 64 hex chars (32 bytes) + const hexId = id.replace("0x", "").padStart(64, "0"); + Buffer.from(hexId, "hex").copy(priceIdsBuffer, 1 + index * 32); + }); + + return beginCell() + .storeUint(op, 32) + .storeRef(createCellChain(updateData)) + .storeRef(createCellChain(priceIdsBuffer)) + .storeUint(time1, 64) + .storeUint(time2, 64) + .storeAddress(targetAddress) + .storeRef(createCellChain(customPayload)) + .endCell(); + } + + async sendParsePriceFeedUpdates( + provider: ContractProvider, + via: Sender, + updateData: Buffer, + updateFee: bigint, + priceIds: HexString[], + minPublishTime: number, + maxPublishTime: number, + targetAddress: Address, + customPayload: Buffer, + ) { + const messageCell = this.createPriceFeedMessage( + 5, // OP_PARSE_PRICE_FEED_UPDATES + updateData, + priceIds, + minPublishTime, + maxPublishTime, + targetAddress, + customPayload, + ); + + await provider.internal(via, { + value: updateFee, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageCell, + }); + } + + async sendParseUniquePriceFeedUpdates( + provider: ContractProvider, + via: Sender, + updateData: Buffer, + updateFee: bigint, + priceIds: HexString[], + publishTime: number, + maxStaleness: number, + targetAddress: Address, + customPayload: Buffer, + ) { + const messageCell = this.createPriceFeedMessage( + 6, // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES + updateData, + priceIds, + publishTime, + maxStaleness, + targetAddress, + customPayload, + ); + + await provider.internal(via, { + value: updateFee, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageCell, + }); + } + + async getNewFunction(provider: ContractProvider) { + const result = await provider.get("test_new_function", []); + return result.stack.readNumber(); + } +} diff --git a/price_feeds/ton/pyth-connector/wrappers/PythTestUpgraded.compile.ts b/price_feeds/ton/pyth-connector/wrappers/PythTestUpgraded.compile.ts new file mode 100644 index 0000000..d9d3122 --- /dev/null +++ b/price_feeds/ton/pyth-connector/wrappers/PythTestUpgraded.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from "@ton/blueprint"; + +export const compile: CompilerConfig = { + lang: "func", + targets: ["contracts/Pyth/tests/PythTestUpgraded.fc"], +}; diff --git a/price_feeds/ton/pyth-connector/wrappers/WormholeTest.compile.ts b/price_feeds/ton/pyth-connector/wrappers/WormholeTest.compile.ts new file mode 100644 index 0000000..b2df658 --- /dev/null +++ b/price_feeds/ton/pyth-connector/wrappers/WormholeTest.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from "@ton/blueprint"; + +export const compile: CompilerConfig = { + lang: "func", + targets: ["contracts/Pyth/tests/WormholeTest.fc"], +}; diff --git a/price_feeds/ton/pyth-connector/wrappers/WormholeTest.ts b/price_feeds/ton/pyth-connector/wrappers/WormholeTest.ts new file mode 100644 index 0000000..a5600e5 --- /dev/null +++ b/price_feeds/ton/pyth-connector/wrappers/WormholeTest.ts @@ -0,0 +1,161 @@ +import { Cell, contractAddress, ContractProvider, Sender } from "@ton/core"; +import { BaseWrapper } from "./BaseWrapper"; +import { + createCellChain, + parseGuardianSetKeys, +} from "@pythnetwork/pyth-ton-js"; + +export type WormholeTestConfig = { + guardianSetIndex: number; + guardianSet: string[]; + chainId: number; + governanceChainId: number; + governanceContract: string; +}; + +export class WormholeTest extends BaseWrapper { + static createFromConfig( + config: WormholeTestConfig, + code: Cell, + workchain = 0, + ) { + const data = WormholeTest.getWormholeInitData(config); + const init = { code, data }; + return new WormholeTest(contractAddress(workchain, init), init); + } + + static getWormholeInitData(config: WormholeTestConfig): Cell { + return BaseWrapper.createInitData({ + guardianSetIndex: config.guardianSetIndex, + guardianSet: config.guardianSet, + chainId: config.chainId, + governanceChainId: config.governanceChainId, + governanceContract: config.governanceContract, + }); + } + + async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { + await super.sendDeploy(provider, via, value); + } + + async getParseEncodedUpgrade( + provider: ContractProvider, + currentGuardianSetIndex: number, + encodedUpgrade: Buffer, + ) { + const result = await provider.get("test_parse_encoded_upgrade", [ + { type: "int", value: BigInt(currentGuardianSetIndex) }, + { type: "slice", cell: createCellChain(encodedUpgrade) }, + ]); + + return { + action: result.stack.readNumber(), + chain: result.stack.readNumber(), + module: result.stack.readBigNumber(), + newGuardianSetKeys: parseGuardianSetKeys(result.stack.readCell()), + newGuardianSetIndex: result.stack.readNumber(), + }; + } + + async getParseAndVerifyWormholeVm(provider: ContractProvider, vm: Buffer) { + const cell = createCellChain(vm); + const result = await provider.get("test_parse_and_verify_wormhole_vm", [ + { type: "slice", cell: cell }, + ]); + + const version = result.stack.readNumber(); + const vm_guardian_set_index = result.stack.readNumber(); + const timestamp = result.stack.readNumber(); + const nonce = result.stack.readNumber(); + const emitter_chain_id = result.stack.readNumber(); + const emitter_address = result.stack + .readBigNumber() + .toString(16) + .padStart(64, "0"); + const sequence = result.stack.readNumber(); + const consistency_level = result.stack.readNumber(); + const payloadCell = result.stack.readCell(); + + let payload = ""; + let currentCell = payloadCell; + + for (let i = 0; i < 4; i++) { + // Original cell + up to 3 references since payload span across 4 cells + const slice = currentCell.beginParse(); + payload += slice.loadBits(slice.remainingBits).toString().toLowerCase(); + + if (slice.remainingRefs === 0) break; + currentCell = slice.loadRef(); + } + const hash = result.stack.readBigNumber().toString(16); + + return { + version, + vm_guardian_set_index, + timestamp, + nonce, + emitter_chain_id, + emitter_address, + sequence, + consistency_level, + payload, + hash, + }; + } + + async getCurrentGuardianSetIndex(provider: ContractProvider) { + return await super.getCurrentGuardianSetIndex( + provider, + "test_get_current_guardian_set_index", + ); + } + + async getGuardianSet(provider: ContractProvider, index: number) { + const result = await provider.get("test_get_guardian_set", [ + { type: "int", value: BigInt(index) }, + ]); + + const expirationTime = result.stack.readNumber(); + const keys = parseGuardianSetKeys(result.stack.readCell()); + const keyCount = result.stack.readNumber(); + + return { + expirationTime, + keys, + keyCount, + }; + } + + async getGovernanceChainId(provider: ContractProvider) { + const result = await provider.get("test_get_governance_chain_id", []); + return result.stack.readNumber(); + } + + async getChainId(provider: ContractProvider) { + return await super.getChainId(provider, "test_get_chain_id"); + } + + async getGovernanceContract(provider: ContractProvider) { + const result = await provider.get("test_get_governance_contract", []); + const bigNumber = result.stack.readBigNumber(); + return bigNumber.toString(16).padStart(64, "0"); + } + + async getGovernanceActionIsConsumed( + provider: ContractProvider, + hash: bigint, + ) { + const result = await provider.get("test_governance_action_is_consumed", [ + { type: "int", value: hash }, + ]); + return result.stack.readBoolean(); + } + + async sendUpdateGuardianSet( + provider: ContractProvider, + via: Sender, + vm: Buffer, + ) { + await super.sendUpdateGuardianSet(provider, via, vm); + } +} From 72a062c0b4def2629a89c30a710f92cd4b0b717c Mon Sep 17 00:00:00 2001 From: master_jedy Date: Sat, 5 Jul 2025 01:15:04 +0200 Subject: [PATCH 2/2] update readme & fix comment Signed-off-by: master_jedy --- price_feeds/ton/pyth-connector/README.md | 17 ++++++++++------- .../contracts/Pyth/MainNoCheck.fc | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/price_feeds/ton/pyth-connector/README.md b/price_feeds/ton/pyth-connector/README.md index 6511e47..971f6c2 100644 --- a/price_feeds/ton/pyth-connector/README.md +++ b/price_feeds/ton/pyth-connector/README.md @@ -1,12 +1,15 @@ # Pyth-connector example -Provides onchain-getter: User -> User JettonWallet -> App -> Pyth -> App -> ... -and proxy call: User -> Pyth -> App -> ... pyth usage examples. +Provides onchain-getter: **User -> User JettonWallet -> App -> Pyth -> App -> ...** and proxy call: **User -> Pyth -> App -> ...** pyth usage examples. -This example can be used as a separate module providing tools for sandbox testing: exports functions for deploying and configuring a local pyth contract +This example can be used as a standalone module that provides tools for sandbox testing by exporting functions for deploying and configuring a local Pyth contract. -It shows techniques how to use the pyth oracle in finacial applications. -The demonstration is fully sandboxed and doesn't need real on-chain contracts nor testnet neither mainnet. -Usage of hermes client is also not required: prices can be formed locally, e.g. **{TON: 3.12345, USDC: 0.998, USDT: 0.999}.** +It demonstrates techniques for using the Pyth oracle in financial applications. + +The demonstration is fully sandboxed and does not require real on-chain contracts on either testnet or mainnet. +Using the Hermes client is also not required — prices can be generated locally, for example: **{TON: 3.12345, USDC: 0.998, USDT: 0.999}**. + +This is achieved by using a patched Pyth contract that accepts simplified prices without a Merkle trie proof, and therefore does not verify the authenticity of the prices. +Important: This patched contract is intended for testing purposes only. The production version must use the authentic Pyth contract deployed on the mainnet. ## Project structure @@ -20,7 +23,7 @@ First you need to install dependencies, node v22 is required, you can use nvm to Then install dependencies, just run `yarn` ### Build -to build the module you can run`yarn build` +to build the module you can run `yarn build` ### Contracts To prebuild contracts run`yarn contracts` diff --git a/price_feeds/ton/pyth-connector/contracts/Pyth/MainNoCheck.fc b/price_feeds/ton/pyth-connector/contracts/Pyth/MainNoCheck.fc index 914f45d..d1197a0 100644 --- a/price_feeds/ton/pyth-connector/contracts/Pyth/MainNoCheck.fc +++ b/price_feeds/ton/pyth-connector/contracts/Pyth/MainNoCheck.fc @@ -23,7 +23,7 @@ ;; Get sender address from message slice cs = in_msg_full.begin_parse(); - int flags = cs~load_uint(4); ;; skip flags + int flags = cs~load_uint(4); ;; load flags if (flags & 1) { return (); }