Przeglądaj źródła

feat(target_chains/ton): add pyth contract functions (price_unsafe, price_no_older_than, ema_price_unsafe, ema_price_no_older_than) (#1845)

* organize file structure

* add price_unsafe and ema_price_unsafe

* add price_no_older_than and ema_price_no_older_than

* add tests for price_unsafe and ema_price_unsafe

* fix ci

* add tests for price_no_older_than and ema_price_no_older_than

* address comments

* update lock file
Daniel Chew 1 rok temu
rodzic
commit
0714994eff

+ 3 - 0
pnpm-lock.yaml

@@ -1863,6 +1863,9 @@ importers:
 
   target_chains/ton/contracts:
     devDependencies:
+      '@pythnetwork/price-service-sdk':
+        specifier: workspace:*
+        version: link:../../../price_service/sdk/js
       '@ton/blueprint':
         specifier: ^0.22.0
         version: 0.22.0(@ton/core@0.57.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0)(@ton/ton@13.11.2(@ton/core@0.57.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0))(@types/node@20.14.15)(encoding@0.1.13)(typescript@5.5.4)

+ 29 - 0
target_chains/ton/contracts/contracts/Main.fc

@@ -0,0 +1,29 @@
+#include "imports/stdlib.fc";
+#include "common/errors.fc";
+#include "common/storage.fc";
+#include "Wormhole.fc";
+
+;; Opcodes
+const int OP_UPDATE_GUARDIAN_SET = 1;
+const int OP_EXECUTE_GOVERNANCE_ACTION = 2;
+
+;; Internal message handler
+() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
+    if (in_msg_body.slice_empty?()) { ;; ignore empty messages
+        return ();
+    }
+
+    ;; * A 32-bit (big-endian) unsigned integer `op`, identifying the `operation` to be performed, or the `method` of the smart contract to be invoked.
+    int op = in_msg_body~load_uint(32);
+    ;; * A 64-bit (big-endian) unsigned integer `query_id`, used in all query-response internal messages to indicate that a response is related to a query (the `query_id` of a response must be equal to the `query_id` of the corresponding query). If `op` is not a query-response method (e.g., it invokes a method that is not expected to send an answer), then `query_id` may be omitted.
+    int query_id = in_msg_body~load_uint(64);
+
+    ;; * The remainder of the message body is specific for each supported value of `op`.
+    if (op == OP_UPDATE_GUARDIAN_SET) {
+        update_guardian_set(in_msg_body);
+    } elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) {
+        execute_governance_action(in_msg_body);
+    } else {
+        throw(0xffff); ;; Throw exception for unknown op
+    }
+}

+ 37 - 18
target_chains/ton/contracts/contracts/Pyth.fc

@@ -1,27 +1,46 @@
 #include "imports/stdlib.fc";
-#include "Wormhole.fc";
+#include "common/errors.fc";
+#include "common/storage.fc";
 
 ;; Opcodes
 const int OP_UPDATE_GUARDIAN_SET = 1;
 const int OP_EXECUTE_GOVERNANCE_ACTION = 2;
 
-;; Internal message handler
-() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
-    if (in_msg_body.slice_empty?()) { ;; ignore empty messages
-        return ();
-    }
+(int, int, int, int) parse_price(slice price_feed) {
+    int price = price_feed~load_int(256);
+    int conf = price_feed~load_uint(64);
+    int expo = price_feed~load_int(32);
+    int publish_time = price_feed~load_uint(64);
+    return (price, conf, expo, publish_time);
+}
+
+(int, int, int, int) price_unsafe(int price_feed_id) method_id {
+    (slice result, int success) = latest_price_feeds.udict_get?(256, price_feed_id);
+    throw_unless(ERROR_PRICE_FEED_NOT_FOUND, success);
+    slice price_feed = result~load_ref().begin_parse();
+    slice price = price_feed~load_ref().begin_parse();
+    return parse_price(price);
+}
 
-    ;; * A 32-bit (big-endian) unsigned integer `op`, identifying the `operation` to be performed, or the `method` of the smart contract to be invoked.
-    int op = in_msg_body~load_uint(32);
-    ;; * A 64-bit (big-endian) unsigned integer `query_id`, used in all query-response internal messages to indicate that a response is related to a query (the `query_id` of a response must be equal to the `query_id` of the corresponding query). If `op` is not a query-response method (e.g., it invokes a method that is not expected to send an answer), then `query_id` may be omitted.
-    int query_id = in_msg_body~load_uint(64);
+(int, int, int, int) price_no_older_than(int time_period, int price_feed_id) method_id {
+    (int price, int conf, int expo, int publish_time) = price_unsafe(price_feed_id);
+    int current_time = now();
+    throw_if(ERROR_OUTDATED_PRICE, current_time - publish_time > time_period);
+    return (price, conf, expo, publish_time);
+}
+
+(int, int, int, int) ema_price_unsafe(int price_feed_id) method_id {
+    (slice result, int success) = latest_price_feeds.udict_get?(256, price_feed_id);
+    throw_unless(ERROR_PRICE_FEED_NOT_FOUND, success);
+    slice price_feed = result~load_ref().begin_parse();
+    slice price = price_feed~load_ref().begin_parse();
+    slice ema_price = price_feed~load_ref().begin_parse();
+    return parse_price(ema_price);
+}
 
-    ;; * The remainder of the message body is specific for each supported value of `op`.
-    if (op == OP_UPDATE_GUARDIAN_SET) {
-        update_guardian_set(in_msg_body);
-    } elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) {
-        execute_governance_action(in_msg_body);
-    } else {
-        throw(0xffff); ;; Throw exception for unknown op
-    }
+(int, int, int, int) ema_price_no_older_than(int time_period, int price_feed_id) method_id {
+    (int price, int conf, int expo, int publish_time) = ema_price_unsafe(price_feed_id);
+    int current_time = now();
+    throw_if(ERROR_OUTDATED_PRICE, current_time - publish_time > time_period);
+    return (price, conf, expo, publish_time);
 }

+ 3 - 29
target_chains/ton/contracts/contracts/Wormhole.fc

@@ -1,7 +1,7 @@
 #include "imports/stdlib.fc";
-#include "imports/errors.fc";
-#include "imports/utils.fc";
-#include "imports/storage.fc";
+#include "common/errors.fc";
+#include "common/utils.fc";
+#include "common/storage.fc";
 
 ;; Signature verification function
 ;; ECRECOVER: Recovers the signer's address from the signature
@@ -55,32 +55,6 @@ const int UPGRADE_MODULE = 0x000000000000000000000000000000000000000000000000000
     return (expiration_time, keys, key_count);
 }
 
-;; store_data stores data in the contract
-() store_data() impure inline_ref {
-    begin_cell()
-        .store_uint(current_guardian_set_index, 32)
-        .store_dict(guardian_sets)
-        .store_uint(chain_id, 16)
-        .store_uint(governance_chain_id, 16)
-        .store_uint(governance_contract, 256)
-        .store_dict(consumed_governance_actions)
-        .end_cell()
-        .set_data();
-}
-
-;; load_data populates storage variables using stored data
-() load_data() impure inline_ref {
-    var ds = get_data().begin_parse();
-    current_guardian_set_index = ds~load_uint(32);
-    guardian_sets = ds~load_dict();
-    (int expiration_time, cell keys, int key_count) = get_guardian_set_internal(current_guardian_set_index);
-    chain_id = ds~load_uint(16);
-    governance_chain_id = ds~load_uint(16);
-    governance_contract = ds~load_uint(256);
-    consumed_governance_actions = ds~load_dict();
-    ds.end_parse();
-}
-
 
 ;; Get methods
 int get_current_guardian_set_index() method_id {

+ 6 - 0
target_chains/ton/contracts/contracts/imports/errors.fc → target_chains/ton/contracts/contracts/common/errors.fc

@@ -1,4 +1,6 @@
 ;; Error codes enum
+
+;; Wormhole
 const int ERROR_INVALID_GUARDIAN_SET = 1000;
 const int ERROR_INVALID_VERSION = 1001;
 const int ERROR_GUARDIAN_SET_NOT_FOUND = 1002;
@@ -18,3 +20,7 @@ const int ERROR_INVALID_GUARDIAN_SET_UPGRADE_LENGTH = 1015;
 const int ERROR_INVALID_GOVERNANCE_CHAIN = 1016;
 const int ERROR_INVALID_GOVERNANCE_CONTRACT = 1017;
 const int ERROR_INVALID_GUARDIAN_ADDRESS = 1018;
+
+;; Pyth
+const int ERROR_PRICE_FEED_NOT_FOUND = 1019;
+const int ERROR_OUTDATED_PRICE = 1020;

+ 46 - 0
target_chains/ton/contracts/contracts/common/storage.fc

@@ -0,0 +1,46 @@
+#include "../imports/stdlib.fc";
+
+;; Pyth
+;; Price struct: {price: int, conf: int, expo: int, publish_time: int}
+;; PriceFeed struct: {price: Price, ema_price: Price}
+global cell latest_price_feeds; ;; Dictionary of PriceFeed structs, keyed by price_feed_id (256-bit)
+
+;; Wormhole
+global int current_guardian_set_index;
+;; GuardianSet struct: {expiration_time: int, keys: cell}
+;; The 'keys' cell is a dictionary with the following structure:
+;; - Key: 8-bit unsigned integer (guardian index)
+;; - Value: 160-bit unsigned integer (guardian address)
+global cell guardian_sets;
+global int chain_id;
+global int governance_chain_id;
+;; GovernanceContract struct: {chain_id: int, address: slice}
+global int governance_contract;
+global cell consumed_governance_actions;
+
+
+() store_data() impure inline_ref {
+    begin_cell()
+        .store_dict(latest_price_feeds)
+        .store_uint(current_guardian_set_index, 32)
+        .store_dict(guardian_sets)
+        .store_uint(chain_id, 16)
+        .store_uint(governance_chain_id, 16)
+        .store_uint(governance_contract, 256)
+        .store_dict(consumed_governance_actions)
+        .end_cell()
+        .set_data();
+}
+
+;; load_data populates storage variables using stored data
+() load_data() impure inline_ref {
+    var ds = get_data().begin_parse();
+    latest_price_feeds = ds~load_dict();
+    current_guardian_set_index = ds~load_uint(32);
+    guardian_sets = ds~load_dict();
+    chain_id = ds~load_uint(16);
+    governance_chain_id = ds~load_uint(16);
+    governance_contract = ds~load_uint(256);
+    consumed_governance_actions = ds~load_dict();
+    ds.end_parse();
+}

+ 0 - 0
target_chains/ton/contracts/contracts/imports/utils.fc → target_chains/ton/contracts/contracts/common/utils.fc


+ 0 - 11
target_chains/ton/contracts/contracts/imports/storage.fc

@@ -1,11 +0,0 @@
-global int current_guardian_set_index;
-;; GuardianSet struct: {expiration_time: int, keys: cell}
-;; The 'keys' cell is a dictionary with the following structure:
-;; - Key: 8-bit unsigned integer (guardian index)
-;; - Value: 160-bit unsigned integer (guardian address)
-global cell guardian_sets;
-global int chain_id;
-global int governance_chain_id;
-;; GovernanceContract struct: {chain_id: int, address: slice}
-global int governance_contract;
-global cell consumed_governance_actions;

+ 26 - 0
target_chains/ton/contracts/contracts/tests/PythTest.fc

@@ -0,0 +1,26 @@
+#include "../imports/stdlib.fc";
+#include "../Pyth.fc";
+
+() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
+    ;; nop;
+}
+
+(int, int, int, int) test_price_unsafe(int price_feed_id) method_id {
+    load_data();
+    return price_unsafe(price_feed_id);
+}
+
+(int, int, int, int) test_price_no_older_than(int time_period, int price_feed_id) method_id {
+    load_data();
+    return price_no_older_than(time_period, price_feed_id);
+}
+
+(int, int, int, int) test_ema_price_unsafe(int price_feed_id) method_id {
+    load_data();
+    return ema_price_unsafe(price_feed_id);
+}
+
+(int, int, int, int) test_ema_price_no_older_than(int time_period, int price_feed_id) method_id {
+    load_data();
+    return ema_price_no_older_than(time_period, price_feed_id);
+}

+ 5 - 4
target_chains/ton/contracts/package.json

@@ -1,5 +1,5 @@
 {
-  "name": "pyth",
+  "name": "@pythnetwork/pyth-ton",
   "version": "0.0.1",
   "scripts": {
     "start": "blueprint run",
@@ -7,16 +7,17 @@
     "test": "jest --verbose"
   },
   "devDependencies": {
+    "@pythnetwork/price-service-sdk": "workspace:*",
     "@ton/blueprint": "^0.22.0",
+    "@ton/core": "~0",
+    "@ton/crypto": "^3.2.0",
     "@ton/sandbox": "^0.20.0",
     "@ton/test-utils": "^0.4.2",
+    "@ton/ton": "^13.11.2",
     "@types/jest": "^29.5.12",
     "@types/node": "^20.14.10",
     "jest": "^29.7.0",
     "prettier": "^3.3.2",
-    "@ton/ton": "^13.11.2",
-    "@ton/core": "~0",
-    "@ton/crypto": "^3.2.0",
     "ts-jest": "^29.2.0",
     "ts-node": "^10.9.2",
     "typescript": "^5.5.3"

+ 5 - 5
target_chains/ton/contracts/scripts/deployPyth.ts

@@ -1,13 +1,13 @@
 import { toNano } from "@ton/core";
-import { Pyth } from "../wrappers/Pyth";
+import { Main } from "../wrappers/Main";
 import { compile, NetworkProvider } from "@ton/blueprint";
 
 export async function run(provider: NetworkProvider) {
-  const pyth = provider.open(Pyth.createFromConfig({}, await compile("Pyth")));
+  const main = provider.open(Main.createFromConfig({}, await compile("Main")));
 
-  await pyth.sendDeploy(provider.sender(), toNano("0.05"));
+  await main.sendDeploy(provider.sender(), toNano("0.05"));
 
-  await provider.waitForDeploy(pyth.address);
+  await provider.waitForDeploy(main.address);
 
-  // run methods on `pyth`
+  // run methods on `main`
 }

+ 8 - 8
target_chains/ton/contracts/tests/Pyth.spec.ts → target_chains/ton/contracts/tests/Main.spec.ts

@@ -1,35 +1,35 @@
 import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox";
 import { Cell, toNano } from "@ton/core";
-import { Pyth } from "../wrappers/Pyth";
+import { Main } from "../wrappers/Main";
 import "@ton/test-utils";
 import { compile } from "@ton/blueprint";
 
-describe("Pyth", () => {
+describe("Main", () => {
   let code: Cell;
 
   beforeAll(async () => {
-    code = await compile("Pyth");
+    code = await compile("Main");
   });
 
   let blockchain: Blockchain;
   let deployer: SandboxContract<TreasuryContract>;
-  let pyth: SandboxContract<Pyth>;
+  let main: SandboxContract<Main>;
 
   beforeEach(async () => {
     blockchain = await Blockchain.create();
 
-    pyth = blockchain.openContract(Pyth.createFromConfig({}, code));
+    main = blockchain.openContract(Main.createFromConfig({}, code));
 
     deployer = await blockchain.treasury("deployer");
 
-    const deployResult = await pyth.sendDeploy(
+    const deployResult = await main.sendDeploy(
       deployer.getSender(),
       toNano("0.05")
     );
 
     expect(deployResult.transactions).toHaveTransaction({
       from: deployer.address,
-      to: pyth.address,
+      to: main.address,
       deploy: true,
       success: true,
     });
@@ -37,6 +37,6 @@ describe("Pyth", () => {
 
   it("should deploy", async () => {
     // the check is done inside beforeEach
-    // blockchain and pyth are ready to use
+    // blockchain and main are ready to use
   });
 });

+ 147 - 0
target_chains/ton/contracts/tests/PythTest.spec.ts

@@ -0,0 +1,147 @@
+import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox";
+import { Cell, toNano } from "@ton/core";
+import "@ton/test-utils";
+import { compile } from "@ton/blueprint";
+import { HexString, Price } from "@pythnetwork/price-service-sdk";
+import { PythTest, PythTestConfig } from "../wrappers/PythTest";
+
+const PRICE_FEED_ID =
+  "0x0000000000000000000000000000000000000000000000000000000000000000";
+const TIME_PERIOD = 60;
+const PRICE = new Price({
+  price: "1",
+  conf: "2",
+  expo: 3,
+  publishTime: 4,
+});
+const EMA_PRICE = new Price({
+  price: "5",
+  conf: "6",
+  expo: 7,
+  publishTime: 8,
+});
+
+describe("PythTest", () => {
+  let code: Cell;
+
+  beforeAll(async () => {
+    code = await compile("PythTest");
+  });
+
+  let blockchain: Blockchain;
+  let deployer: SandboxContract<TreasuryContract>;
+  let pythTest: SandboxContract<PythTest>;
+
+  beforeEach(async () => {
+    blockchain = await Blockchain.create();
+    deployer = await blockchain.treasury("deployer");
+  });
+
+  async function deployContract(
+    priceFeedId: HexString = PRICE_FEED_ID,
+    timePeriod: number = TIME_PERIOD,
+    price: Price = PRICE,
+    emaPrice: Price = EMA_PRICE
+  ) {
+    const config: PythTestConfig = {
+      priceFeedId,
+      timePeriod,
+      price,
+      emaPrice,
+    };
+
+    pythTest = blockchain.openContract(PythTest.createFromConfig(config, code));
+
+    const deployResult = await pythTest.sendDeploy(
+      deployer.getSender(),
+      toNano("0.05")
+    );
+
+    expect(deployResult.transactions).toHaveTransaction({
+      from: deployer.address,
+      to: pythTest.address,
+      deploy: true,
+      success: true,
+    });
+  }
+
+  it("should correctly get price unsafe", async () => {
+    await deployContract();
+
+    const result = await pythTest.getPriceUnsafe(PRICE_FEED_ID);
+
+    expect(result.price).toBe(1);
+    expect(result.conf).toBe(2);
+    expect(result.expo).toBe(3);
+    expect(result.publishTime).toBe(4);
+  });
+
+  it("should correctly get price no older than", async () => {
+    const timeNow = Math.floor(Date.now() / 1000) - TIME_PERIOD + 5; // 5 seconds buffer
+    const price = new Price({
+      price: "1",
+      conf: "2",
+      expo: 3,
+      publishTime: timeNow,
+    });
+    await deployContract(PRICE_FEED_ID, TIME_PERIOD, price, EMA_PRICE);
+
+    const result = await pythTest.getPriceNoOlderThan(
+      TIME_PERIOD,
+      PRICE_FEED_ID
+    );
+
+    expect(result.price).toBe(1);
+    expect(result.conf).toBe(2);
+    expect(result.expo).toBe(3);
+    expect(result.publishTime).toBe(timeNow);
+  });
+
+  it("should fail to get price no older than", async () => {
+    await deployContract();
+
+    await expect(
+      pythTest.getPriceNoOlderThan(TIME_PERIOD, PRICE_FEED_ID)
+    ).rejects.toThrow("Unable to execute get method. Got exit_code: 1020"); // ERROR_OUTDATED_PRICE = 1020
+  });
+
+  it("should correctly get ema price no older than", async () => {
+    const timeNow = Math.floor(Date.now() / 1000) - TIME_PERIOD + 5; // 5 seconds buffer
+    const emaPrice = new Price({
+      price: "5",
+      conf: "6",
+      expo: 7,
+      publishTime: timeNow,
+    });
+    await deployContract(PRICE_FEED_ID, TIME_PERIOD, PRICE, emaPrice);
+
+    const result = await pythTest.getEmaPriceNoOlderThan(
+      TIME_PERIOD,
+      PRICE_FEED_ID
+    );
+
+    expect(result.price).toBe(5);
+    expect(result.conf).toBe(6);
+    expect(result.expo).toBe(7);
+    expect(result.publishTime).toBe(timeNow);
+  });
+
+  it("should fail to get ema price no older than", async () => {
+    await deployContract();
+
+    await expect(
+      pythTest.getEmaPriceNoOlderThan(TIME_PERIOD, PRICE_FEED_ID)
+    ).rejects.toThrow("Unable to execute get method. Got exit_code: 1020"); // ERROR_OUTDATED_PRICE = 1020
+  });
+
+  it("should correctly get ema price unsafe", async () => {
+    await deployContract();
+
+    const result = await pythTest.getEmaPriceUnsafe(PRICE_FEED_ID);
+
+    expect(result.price).toBe(5);
+    expect(result.conf).toBe(6);
+    expect(result.expo).toBe(7);
+    expect(result.publishTime).toBe(8);
+  });
+});

+ 0 - 1
target_chains/ton/contracts/tests/WormholeTest.spec.ts

@@ -8,7 +8,6 @@ import {
   GUARDIAN_SET_0,
   GUARDIAN_SET_4,
   MAINNET_UPGRADE_VAAS,
-  parseGuardianSetKeys,
 } from "./utils/wormhole";
 
 describe("WormholeTest", () => {

+ 6 - 0
target_chains/ton/contracts/wrappers/Main.compile.ts

@@ -0,0 +1,6 @@
+import { CompilerConfig } from "@ton/blueprint";
+
+export const compile: CompilerConfig = {
+  lang: "func",
+  targets: ["contracts/Main.fc", "contracts/Pyth.fc", "contracts/Wormhole.fc"],
+};

+ 7 - 7
target_chains/ton/contracts/wrappers/Pyth.ts → target_chains/ton/contracts/wrappers/Main.ts

@@ -9,26 +9,26 @@ import {
   SendMode,
 } from "@ton/core";
 
-export type PythConfig = {};
+export type MainConfig = {};
 
-export function pythConfigToCell(config: PythConfig): Cell {
+export function mainConfigToCell(config: MainConfig): Cell {
   return beginCell().endCell();
 }
 
-export class Pyth implements Contract {
+export class Main implements Contract {
   constructor(
     readonly address: Address,
     readonly init?: { code: Cell; data: Cell }
   ) {}
 
   static createFromAddress(address: Address) {
-    return new Pyth(address);
+    return new Main(address);
   }
 
-  static createFromConfig(config: PythConfig, code: Cell, workchain = 0) {
-    const data = pythConfigToCell(config);
+  static createFromConfig(config: MainConfig, code: Cell, workchain = 0) {
+    const data = mainConfigToCell(config);
     const init = { code, data };
-    return new Pyth(contractAddress(workchain, init), init);
+    return new Main(contractAddress(workchain, init), init);
   }
 
   async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) {

+ 1 - 1
target_chains/ton/contracts/wrappers/Pyth.compile.ts → target_chains/ton/contracts/wrappers/PythTest.compile.ts

@@ -2,5 +2,5 @@ import { CompilerConfig } from "@ton/blueprint";
 
 export const compile: CompilerConfig = {
   lang: "func",
-  targets: ["contracts/Pyth.fc", "contracts/Wormhole.fc"],
+  targets: ["contracts/tests/PythTest.fc"],
 };

+ 183 - 0
target_chains/ton/contracts/wrappers/PythTest.ts

@@ -0,0 +1,183 @@
+import {
+  Address,
+  beginCell,
+  Cell,
+  Contract,
+  contractAddress,
+  ContractProvider,
+  Dictionary,
+  Sender,
+  SendMode,
+} from "@ton/core";
+import { HexString, Price } from "@pythnetwork/price-service-sdk";
+
+export type PythTestConfig = {
+  priceFeedId: HexString;
+  timePeriod: number;
+  price: Price;
+  emaPrice: Price;
+};
+
+export class PythTest implements Contract {
+  constructor(
+    readonly address: Address,
+    readonly init?: { code: Cell; data: Cell }
+  ) {}
+
+  static createFromAddress(address: Address) {
+    return new PythTest(address);
+  }
+
+  static createFromConfig(config: PythTestConfig, code: Cell, workchain = 0) {
+    const data = PythTest.getPythInitData(
+      config.priceFeedId,
+      config.timePeriod,
+      config.price,
+      config.emaPrice
+    );
+    const init = { code, data };
+    return new PythTest(contractAddress(workchain, init), init);
+  }
+
+  static getPythInitData(
+    priceFeedId: HexString,
+    timePeriod: number,
+    price: Price,
+    emaPrice: Price
+  ): Cell {
+    const priceDict = Dictionary.empty(
+      Dictionary.Keys.BigUint(256),
+      Dictionary.Values.Cell()
+    );
+
+    const priceCell = beginCell()
+      .storeInt(price.getPriceAsNumberUnchecked() * 10 ** -price.expo, 256)
+      .storeUint(price.getConfAsNumberUnchecked() * 10 ** -price.expo, 64)
+      .storeInt(price.expo, 32)
+      .storeUint(price.publishTime, 64)
+      .endCell();
+
+    const emaPriceCell = beginCell()
+      .storeInt(
+        emaPrice.getPriceAsNumberUnchecked() * 10 ** -emaPrice.expo,
+        256
+      )
+      .storeUint(emaPrice.getConfAsNumberUnchecked() * 10 ** -emaPrice.expo, 64)
+      .storeInt(emaPrice.expo, 32)
+      .storeUint(emaPrice.publishTime, 64)
+      .endCell();
+
+    const priceFeedCell = beginCell()
+      .storeRef(priceCell)
+      .storeRef(emaPriceCell)
+      .storeUint(timePeriod, 32)
+      .endCell();
+
+    priceDict.set(BigInt(priceFeedId), priceFeedCell);
+
+    return beginCell()
+      .storeDict(priceDict) // latest_price_feeds
+      .storeUint(0, 32)
+      .storeDict(Dictionary.empty())
+      .storeUint(0, 16)
+      .storeUint(0, 16)
+      .storeBuffer(
+        Buffer.from(
+          "0000000000000000000000000000000000000000000000000000000000000000",
+          "hex"
+        )
+      )
+      .storeDict(Dictionary.empty()) // consumed_governance_actions,
+      .endCell();
+  }
+
+  async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) {
+    await provider.internal(via, {
+      value,
+      sendMode: SendMode.PAY_GAS_SEPARATELY,
+      body: beginCell().endCell(),
+    });
+  }
+
+  async getPriceUnsafe(provider: ContractProvider, priceFeedId: HexString) {
+    const result = await provider.get("test_price_unsafe", [
+      { type: "int", value: BigInt(priceFeedId) },
+    ]);
+
+    const price = result.stack.readNumber();
+    const conf = result.stack.readNumber();
+    const expo = result.stack.readNumber();
+    const publishTime = result.stack.readNumber();
+
+    return {
+      price,
+      conf,
+      expo,
+      publishTime,
+    };
+  }
+
+  async getPriceNoOlderThan(
+    provider: ContractProvider,
+    timePeriod: number,
+    priceFeedId: HexString
+  ) {
+    const result = await provider.get("test_price_no_older_than", [
+      { type: "int", value: BigInt(timePeriod) },
+      { type: "int", value: BigInt(priceFeedId) },
+    ]);
+
+    const price = result.stack.readNumber();
+    const conf = result.stack.readNumber();
+    const expo = result.stack.readNumber();
+    const publishTime = result.stack.readNumber();
+
+    return {
+      price,
+      conf,
+      expo,
+      publishTime,
+    };
+  }
+
+  async getEmaPriceUnsafe(provider: ContractProvider, priceFeedId: HexString) {
+    const result = await provider.get("test_ema_price_unsafe", [
+      { type: "int", value: BigInt(priceFeedId) },
+    ]);
+
+    const price = result.stack.readNumber();
+    const conf = result.stack.readNumber();
+    const expo = result.stack.readNumber();
+    const publishTime = result.stack.readNumber();
+
+    return {
+      price,
+      conf,
+      expo,
+      publishTime,
+    };
+  }
+
+  async getEmaPriceNoOlderThan(
+    provider: ContractProvider,
+    timePeriod: number,
+    priceFeedId: HexString
+  ) {
+    const result = await provider.get("test_ema_price_no_older_than", [
+      { type: "int", value: BigInt(timePeriod) },
+      { type: "int", value: BigInt(priceFeedId) },
+    ]);
+
+    const price = result.stack.readNumber();
+    const conf = result.stack.readNumber();
+    const expo = result.stack.readNumber();
+    const publishTime = result.stack.readNumber();
+
+    return {
+      price,
+      conf,
+      expo,
+      publishTime,
+    };
+  }
+}

+ 1 - 0
target_chains/ton/contracts/wrappers/WormholeTest.ts

@@ -71,6 +71,7 @@ export class WormholeTest implements Contract {
     guardianSets.set(guardianSetIndex, guardianSetCell);
 
     return beginCell()
+      .storeDict(Dictionary.empty()) // latest_price_feeds, empty for initial state
       .storeUint(guardianSetIndex, 32)
       .storeDict(guardianSets)
       .storeUint(chainId, 16)