diff --git a/target_chains/ton/contracts/contracts/Main.fc b/target_chains/ton/contracts/contracts/Main.fc index ac16660a1e..fa6ce18b71 100644 --- a/target_chains/ton/contracts/contracts/Main.fc +++ b/target_chains/ton/contracts/contracts/Main.fc @@ -16,6 +16,11 @@ 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 + ;; * The remainder of the message body is specific for each supported value of `op`. if (op == OP_UPDATE_GUARDIAN_SET) { update_guardian_set(data_slice); @@ -25,6 +30,18 @@ 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); + parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address); + } 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); + parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address); } else { throw(0xffff); ;; Throw exception for unknown op } diff --git a/target_chains/ton/contracts/contracts/Pyth.fc b/target_chains/ton/contracts/contracts/Pyth.fc index ddee6141ef..3704910593 100644 --- a/target_chains/ton/contracts/contracts/Pyth.fc +++ b/target_chains/ton/contracts/contracts/Pyth.fc @@ -6,6 +6,7 @@ #include "common/merkle_tree.fc"; #include "common/governance_actions.fc"; #include "common/gas.fc"; +#include "common/op.fc"; #include "./Wormhole.fc"; cell store_price(int price, int conf, int expo, int publish_time) { @@ -156,16 +157,7 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure { return payload~load_uint(160); ;; Return root_digest } - -() update_price_feeds(int msg_value, slice data) impure { - load_data(); - 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(int msg_value, int num_updates) impure { int update_fee = single_update_fee * num_updates; int compute_fee = get_compute_fee( WORKCHAIN, @@ -176,6 +168,31 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure { ;; 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()); @@ -183,23 +200,232 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure { cell data_source = begin_cell() .store_uint(emitter_chain_id, 16) .store_uint(emitter_address, 256) - .end_cell(); + .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 ema_price, int ema_conf, slice new_cs) = read_and_verify_message(cs, root_digest); + (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) 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); + response = response.store_ref(price_feeds_cell); + + ;; Build the complete message cell (https://docs.ton.org/v3/documentation/smart-contracts/message-management/sending-messages#message-layout) + cell msg = begin_cell() + .store_uint(0x18, 6) + .store_slice(sender_address) + .store_coins(0) ;; Will fill in actual amount after fee calculations + .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_ref(response.end_cell()) + .end_cell(); + + int num_price_feeds = price_feeds.tlen(); + + ;; Number of cells in the message + ;; - 2 cells: msg + response + int cells = 2 + num_price_feeds; + + ;; Bit layout per TL-B spec (https://github.com/ton-blockchain/ton/blob/master/crypto/block/block.tlb): + ;; - 6 bits: optimized way of serializing the tag and the first 4 fields + ;; - 256 bits: owner address + ;; - 128 bits: coins (VarUInteger 16) from grams$_ amount:(VarUInteger 16) = Grams + ;; - 107 bits: other data (extra_currencies + ihr_fee + fwd_fee + lt of transaction + unixtime of transaction + no init-field flag + inplace message body flag) + ;; - PRICE_FEED_BITS * num_price_feeds: space for each price feed + int bits = 6 + 256 + 128 + 107 + (PRICE_FEED_BITS * num_price_feeds); + int fwd_fee = get_forward_fee(cells, bits, WORKCHAIN); + + ;; 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 + fwd_fee; + int excess = msg_value - total_fees; + + ;; Send response message back to sender with exact excess amount + send_raw_message(begin_cell() + .store_uint(0x18, 6) + .store_slice(sender_address) + .store_coins(excess) + .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_ref(response.end_cell()) + .end_cell(), + 0); +} + +() 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) impure { + load_data(); + + ;; Load price_ids tuple + int price_ids_len = price_ids_slice~load_uint(8); + tuple price_ids = empty_tuple(); + repeat(price_ids_len) { + int price_id = price_ids_slice~load_uint(256); + price_ids~tpush(price_id); + } + + 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); +} + +() 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) impure { + load_data(); + + ;; Load price_ids tuple + int price_ids_len = price_ids_slice~load_uint(8); + tuple price_ids = empty_tuple(); + repeat(price_ids_len) { + int price_id = price_ids_slice~load_uint(256); + price_ids~tpush(price_id); + } + + 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); +} + +() 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?) { @@ -213,17 +439,11 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure { } if (publish_time > latest_publish_time) { - cell price_feed = begin_cell() - .store_ref(store_price(price, conf, expo, publish_time)) - .store_ref(store_price(ema_price, ema_conf, expo, publish_time)) - .end_cell(); - - latest_price_feeds~udict_set(256, price_id, begin_cell().store_ref(price_feed).end_cell().begin_parse()); + latest_price_feeds~udict_set(256, price_id, begin_cell().store_ref(price_feed_cell).end_cell().begin_parse()); } + i += 1; } - throw_if(ERROR_INVALID_UPDATE_DATA_LENGTH, ~ cs.slice_empty?()); - store_data(); } diff --git a/target_chains/ton/contracts/contracts/common/constants.fc b/target_chains/ton/contracts/contracts/common/constants.fc index 6da733475f..a087175755 100644 --- a/target_chains/ton/contracts/contracts/common/constants.fc +++ b/target_chains/ton/contracts/contracts/common/constants.fc @@ -15,6 +15,20 @@ const int WORMHOLE_MERKLE_UPDATE_TYPE = 0; const int PRICE_FEED_MESSAGE_TYPE = 0; +;; 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. diff --git a/target_chains/ton/contracts/contracts/common/errors.fc b/target_chains/ton/contracts/contracts/common/errors.fc index 0d08b75472..2dd5972367 100644 --- a/target_chains/ton/contracts/contracts/common/errors.fc +++ b/target_chains/ton/contracts/contracts/common/errors.fc @@ -43,6 +43,7 @@ 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/target_chains/ton/contracts/contracts/common/gas.fc b/target_chains/ton/contracts/contracts/common/gas.fc index 541e3eaa8b..1b24fdc3e9 100644 --- a/target_chains/ton/contracts/contracts/common/gas.fc +++ b/target_chains/ton/contracts/contracts/common/gas.fc @@ -1,4 +1,7 @@ int get_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEE"; +int get_gas_consumed() asm "GASCONSUMED"; +int get_forward_fee(int cells, int bits, int workchain) asm(cells bits workchain) "GETFORWARDFEE"; + ;; 1 update: 262,567 gas ;; 2 updates: 347,791 (+85,224) diff --git a/target_chains/ton/contracts/contracts/common/op.fc b/target_chains/ton/contracts/contracts/common/op.fc index 87471657f2..2d15b5cc2f 100644 --- a/target_chains/ton/contracts/contracts/common/op.fc +++ b/target_chains/ton/contracts/contracts/common/op.fc @@ -2,3 +2,5 @@ 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; diff --git a/target_chains/ton/contracts/contracts/common/utils.fc b/target_chains/ton/contracts/contracts/common/utils.fc index 4c0c94d2c6..22e20b60d9 100644 --- a/target_chains/ton/contracts/contracts/common/utils.fc +++ b/target_chains/ton/contracts/contracts/common/utils.fc @@ -3,6 +3,7 @@ ;; 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; diff --git a/target_chains/ton/contracts/contracts/tests/PythTest.fc b/target_chains/ton/contracts/contracts/tests/PythTest.fc index d65bab5c40..76f08f9270 100644 --- a/target_chains/ton/contracts/contracts/tests/PythTest.fc +++ b/target_chains/ton/contracts/contracts/tests/PythTest.fc @@ -21,6 +21,12 @@ 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) { @@ -29,6 +35,18 @@ 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); + parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address); + } 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); + parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address); } else { throw(0xffff); ;; Throw exception for unknown op } diff --git a/target_chains/ton/contracts/contracts/tests/PythTestUpgraded.fc b/target_chains/ton/contracts/contracts/tests/PythTestUpgraded.fc index eb2ba19111..0dc535ea70 100644 --- a/target_chains/ton/contracts/contracts/tests/PythTestUpgraded.fc +++ b/target_chains/ton/contracts/contracts/tests/PythTestUpgraded.fc @@ -15,6 +15,12 @@ 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) { @@ -23,6 +29,18 @@ 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); + parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address); + } 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); + parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address); } else { throw(0xffff); ;; Throw exception for unknown op } diff --git a/target_chains/ton/contracts/tests/PythTest.spec.ts b/target_chains/ton/contracts/tests/PythTest.spec.ts index 0312f2af76..f435f0588b 100644 --- a/target_chains/ton/contracts/tests/PythTest.spec.ts +++ b/target_chains/ton/contracts/tests/PythTest.spec.ts @@ -1,5 +1,5 @@ import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; -import { Cell, toNano } from "@ton/core"; +import { Cell, CommonMessageInfoInternal, Message, toNano } from "@ton/core"; import "@ton/test-utils"; import { compile } from "@ton/blueprint"; import { HexString, Price } from "@pythnetwork/price-service-sdk"; @@ -18,6 +18,35 @@ import { HERMES_ETH_PRICE, HERMES_ETH_PUBLISH_TIME, HERMES_BTC_PUBLISH_TIME, + HERMES_BTC_CONF, + HERMES_BTC_EXPO, + HERMES_BTC_EMA_CONF, + HERMES_BTC_EMA_EXPO, + HERMES_BTC_EMA_PRICE, + HERMES_BTC_EMA_PUBLISH_TIME, + HERMES_ETH_CONF, + HERMES_ETH_EMA_CONF, + HERMES_ETH_EMA_EXPO, + HERMES_ETH_EMA_PRICE, + HERMES_ETH_EMA_PUBLISH_TIME, + HERMES_ETH_EXPO, + HERMES_BTC_ETH_UNIQUE_UPDATE, + HERMES_ETH_UNIQUE_EMA_PRICE, + 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_UNIQUE_CONF, + HERMES_ETH_UNIQUE_EMA_CONF, + HERMES_ETH_UNIQUE_EMA_EXPO, + HERMES_ETH_UNIQUE_EMA_PUBLISH_TIME, + HERMES_ETH_UNIQUE_EXPO, + HERMES_ETH_UNIQUE_PRICE, + HERMES_ETH_UNIQUE_PUBLISH_TIME, } from "./utils/pyth"; import { GUARDIAN_SET_0, MAINNET_UPGRADE_VAAS } from "./utils/wormhole"; import { DataSource } from "@pythnetwork/xc-admin-common"; @@ -376,19 +405,6 @@ describe("PythTest", () => { const updateData = Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"); let result = await pythTest.sendUpdatePriceFeeds( - deployer.getSender(), - updateData, - toNano("0.1") // Insufficient gas - ); - - expect(result.transactions).toHaveTransaction({ - from: deployer.address, - to: pythTest.address, - success: false, - exitCode: 3000, // ERROR_INSUFFICIENT_GAS - }); - - result = await pythTest.sendUpdatePriceFeeds( deployer.getSender(), updateData, calculateUpdatePriceFeedsFee(1n) // Send enough gas for 1 update instead of 2 @@ -974,4 +990,477 @@ describe("PythTest", () => { // 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 + ); + + // 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); + }); + + 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 + ); + + // 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); + }); + + 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 + ); + + // Verify transaction success and message count + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 2002, // ERROR_INVALID_MAGIC + }); + }); + + 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 + ); + + // Verify transaction success and message count + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 2020, // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE + }); + }); + + 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 + ); + + // Verify transaction success and message count + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: false, + exitCode: 2020, // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE + }); + }); + + 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 + ); + + // 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 (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 + ); + + // 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); + }); }); diff --git a/target_chains/ton/contracts/tests/utils/pyth.ts b/target_chains/ton/contracts/tests/utils/pyth.ts index 8da8d67794..b289d11458 100644 --- a/target_chains/ton/contracts/tests/utils/pyth.ts +++ b/target_chains/ton/contracts/tests/utils/pyth.ts @@ -35,10 +35,48 @@ 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"; diff --git a/target_chains/ton/contracts/wrappers/PythTest.ts b/target_chains/ton/contracts/wrappers/PythTest.ts index cf4133ae19..717cb6b7a5 100644 --- a/target_chains/ton/contracts/wrappers/PythTest.ts +++ b/target_chains/ton/contracts/wrappers/PythTest.ts @@ -3,6 +3,7 @@ import { Cell, contractAddress, ContractProvider, + parseTuple, Sender, SendMode, toNano, @@ -196,6 +197,81 @@ export class PythTest extends BaseWrapper { return parseDataSources(result.stack.readCell()); } + private createPriceFeedMessage( + op: number, + updateData: Buffer, + priceIds: HexString[], + time1: number, + time2: number + ): 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) + .endCell(); + } + + async sendParsePriceFeedUpdates( + provider: ContractProvider, + via: Sender, + updateData: Buffer, + updateFee: bigint, + priceIds: HexString[], + minPublishTime: number, + maxPublishTime: number + ) { + const messageCell = this.createPriceFeedMessage( + 5, // OP_PARSE_PRICE_FEED_UPDATES + updateData, + priceIds, + minPublishTime, + maxPublishTime + ); + + 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 + ) { + const messageCell = this.createPriceFeedMessage( + 6, // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES + updateData, + priceIds, + publishTime, + maxStaleness + ); + + 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();