Browse Source

feat(target_chains/ton): add parse_price_feed_updates (#2099)

* add parse_price_feed_updates

* remove unnecessary comments

* fix parse_price_feed_updates

* uncomment test

* add more tests

* address comment

* address comments

* address comments
Daniel Chew 1 năm trước cách đây
mục cha
commit
d3d0f9fa24

+ 17 - 0
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
     }

+ 242 - 22
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();
 }
 

+ 14 - 0
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.

+ 1 - 0
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;

+ 3 - 0
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)

+ 2 - 0
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;

+ 1 - 0
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;
 

+ 18 - 0
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
     }

+ 18 - 0
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
     }

+ 503 - 14
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);
+  });
 });

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
target_chains/ton/contracts/tests/utils/pyth.ts


+ 76 - 0
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();

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác