diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd48105af5..4d57a5e404 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -775,7 +775,7 @@ importers: dependencies: '@certusone/wormhole-sdk': specifier: ^0.10.15 - version: 0.10.15(bufferutil@4.0.8)(encoding@0.1.13)(google-protobuf@3.21.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10) + version: 0.10.15(bufferutil@4.0.8)(encoding@0.1.13)(google-protobuf@3.21.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10) '@coral-xyz/anchor': specifier: ^0.29.0 version: 0.29.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) @@ -809,7 +809,7 @@ importers: dependencies: '@certusone/wormhole-sdk': specifier: ^0.10.15 - version: 0.10.15(bufferutil@4.0.8)(encoding@0.1.13)(google-protobuf@3.21.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10) + version: 0.10.15(bufferutil@4.0.8)(encoding@0.1.13)(google-protobuf@3.21.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10) '@coral-xyz/anchor': specifier: ^0.29.0 version: 0.29.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) @@ -1944,30 +1944,36 @@ importers: target_chains/ton/contracts: devDependencies: + '@pythnetwork/hermes-client': + specifier: workspace:* + version: link:../../../apps/hermes/client/js '@pythnetwork/price-service-sdk': specifier: workspace:* version: link:../../../price_service/sdk/js + '@pythnetwork/pyth-ton-js': + specifier: workspace:* + version: link:../sdk/js '@pythnetwork/xc-admin-common': specifier: workspace:* version: link:../../../governance/xc_admin/packages/xc_admin_common '@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) + version: 0.22.0(@ton/core@0.59.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0)(@ton/ton@15.1.0(@ton/core@0.59.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0))(@types/node@20.14.15)(encoding@0.1.13)(typescript@5.5.4) '@ton/core': - specifier: ~0 - version: 0.57.0(@ton/crypto@3.3.0) + specifier: ^0.59.0 + version: 0.59.0(@ton/crypto@3.3.0) '@ton/crypto': - specifier: ^3.2.0 + specifier: ^3.3.0 version: 3.3.0 '@ton/sandbox': specifier: ^0.20.0 - version: 0.20.0(@ton/core@0.57.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0) + version: 0.20.0(@ton/core@0.59.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0) '@ton/test-utils': specifier: ^0.4.2 - version: 0.4.2(@jest/globals@29.7.0)(@ton/core@0.57.0(@ton/crypto@3.3.0))(chai@4.5.0) + version: 0.4.2(@jest/globals@29.7.0)(@ton/core@0.59.0(@ton/crypto@3.3.0))(chai@4.5.0) '@ton/ton': - specifier: ^13.11.2 - version: 13.11.2(@ton/core@0.57.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0) + specifier: ^15.1.0 + version: 15.1.0(@ton/core@0.59.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0) '@types/jest': specifier: ^29.5.12 version: 29.5.12 @@ -1993,6 +1999,45 @@ importers: specifier: ^5.5.3 version: 5.5.4 + target_chains/ton/sdk/js: + devDependencies: + '@ton/core': + specifier: ^0.59.0 + version: 0.59.0(@ton/crypto@3.3.0) + '@ton/crypto': + specifier: ^3.3.0 + version: 3.3.0 + '@ton/ton': + specifier: ^15.1.0 + version: 15.1.0(@ton/core@0.59.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0) + '@types/node': + specifier: ^18.11.18 + version: 18.19.44 + '@typescript-eslint/eslint-plugin': + specifier: ^5.21.0 + version: 5.49.0(@typescript-eslint/parser@5.49.0(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/parser': + specifier: ^5.21.0 + version: 5.49.0(eslint@8.57.0)(typescript@4.9.5) + eslint: + specifier: ^8.14.0 + version: 8.57.0 + jest: + specifier: ^29.4.1 + version: 29.7.0(@types/node@18.19.44)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)) + prettier: + specifier: ^2.6.2 + version: 2.8.8 + ts-jest: + specifier: ^29.0.5 + version: 29.2.4(@babel/core@7.24.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(jest@29.7.0(@types/node@18.19.44)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)))(typescript@4.9.5) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@18.19.44)(typescript@4.9.5) + typescript: + specifier: ^4.6.3 + version: 4.9.5 + packages: '@0no-co/graphql.web@1.0.7': @@ -8270,8 +8315,8 @@ packages: peerDependencies: '@ton/crypto': '>=3.2.0' - '@ton/core@0.57.0': - resolution: {integrity: sha512-UOehEXEV5yqi+17qmmWdD01YfVgQlYtitSm5OfN/WMg6PAMkt+Uf91JRC4mdPNtkKDhyKuujJuhYs6QiOsHPfw==} + '@ton/core@0.59.0': + resolution: {integrity: sha512-LSIkGst7BoY7fMWshejzcH0UJnoW21JGlRrW0ch+6A7Xb/7EuekxgdKym7fHxcry6OIf6FoeFg97lJ960N/Ghg==} peerDependencies: '@ton/crypto': '>=3.2.0' @@ -8299,10 +8344,10 @@ packages: chai: optional: true - '@ton/ton@13.11.2': - resolution: {integrity: sha512-EPqW+ZTe0MmfqguJEIGMuAqTAFRKMEce95HlDx8h6CGn2y3jiMgV1/oO+WpDIOiX+1wnTu+xtajk8JTWr8nKRQ==} + '@ton/ton@15.1.0': + resolution: {integrity: sha512-almetcfTu7jLjcNcEEPB7wAc8yl90ES1M//sOr1QE+kv7RbmEvMkaPSc7kFxzs10qrjIPKxlodBJlMSWP5LuVQ==} peerDependencies: - '@ton/core': '>=0.56.0' + '@ton/core': '>=0.59.0' '@ton/crypto': '>=3.2.0' '@tonconnect/isomorphic-eventsource@0.0.1': @@ -12127,11 +12172,6 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - ejs@3.1.8: - resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} - engines: {node: '>=0.10.0'} - hasBin: true - electron-to-chromium@1.4.797: resolution: {integrity: sha512-RWMYymqyWwIdCEb7Psag5zyAHirYnB354ZREoF8c5QOHbt8AodF7lwVxGUnu5gzBVjzDo9R3XeTwy7pbvubxGw==} @@ -27477,6 +27517,41 @@ snapshots: - supports-color - ts-node + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.14.15 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.10 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4))': dependencies: '@jest/console': 29.7.0 @@ -29792,7 +29867,7 @@ snapshots: '@nrwl/devkit@15.6.3(nx@15.6.3)(typescript@4.9.5)': dependencies: '@phenomnomnominal/tsquery': 4.1.1(typescript@4.9.5) - ejs: 3.1.8 + ejs: 3.1.10 ignore: 5.3.1 nx: 15.6.3 semver: 7.3.4 @@ -33720,14 +33795,14 @@ snapshots: '@ton-community/func-js-bin': 0.4.4-newops.1 arg: 5.0.2 - '@ton/blueprint@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)': + '@ton/blueprint@0.22.0(@ton/core@0.59.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0)(@ton/ton@15.1.0(@ton/core@0.59.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0))(@types/node@20.14.15)(encoding@0.1.13)(typescript@5.5.4)': dependencies: '@orbs-network/ton-access': 2.3.3(encoding@0.1.13) '@tact-lang/compiler': 1.4.4(encoding@0.1.13) '@ton-community/func-js': 0.7.0 - '@ton/core': 0.57.0(@ton/crypto@3.3.0) + '@ton/core': 0.59.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) + '@ton/ton': 15.1.0(@ton/core@0.59.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0) '@tonconnect/sdk': 2.2.0(encoding@0.1.13) arg: 5.0.2 chalk: 4.1.2 @@ -33750,7 +33825,7 @@ snapshots: '@ton/crypto': 3.3.0 symbol.inspect: 1.0.1 - '@ton/core@0.57.0(@ton/crypto@3.3.0)': + '@ton/core@0.59.0(@ton/crypto@3.3.0)': dependencies: '@ton/crypto': 3.3.0 symbol.inspect: 1.0.1 @@ -33765,24 +33840,24 @@ snapshots: jssha: 3.2.0 tweetnacl: 1.0.3 - '@ton/sandbox@0.20.0(@ton/core@0.57.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0)': + '@ton/sandbox@0.20.0(@ton/core@0.59.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0)': dependencies: - '@ton/core': 0.57.0(@ton/crypto@3.3.0) + '@ton/core': 0.59.0(@ton/crypto@3.3.0) '@ton/crypto': 3.3.0 - '@ton/test-utils@0.4.2(@jest/globals@29.7.0)(@ton/core@0.57.0(@ton/crypto@3.3.0))(chai@4.5.0)': + '@ton/test-utils@0.4.2(@jest/globals@29.7.0)(@ton/core@0.59.0(@ton/crypto@3.3.0))(chai@4.5.0)': dependencies: - '@ton/core': 0.57.0(@ton/crypto@3.3.0) + '@ton/core': 0.59.0(@ton/crypto@3.3.0) node-inspect-extracted: 2.0.2 optionalDependencies: '@jest/globals': 29.7.0 chai: 4.5.0 - '@ton/ton@13.11.2(@ton/core@0.57.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0)': + '@ton/ton@15.1.0(@ton/core@0.59.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0)': dependencies: - '@ton/core': 0.57.0(@ton/crypto@3.3.0) + '@ton/core': 0.59.0(@ton/crypto@3.3.0) '@ton/crypto': 3.3.0 - axios: 1.7.3 + axios: 1.7.4(debug@4.3.6) dataloader: 2.1.0 symbol.inspect: 1.0.1 teslabot: 1.5.0 @@ -39147,6 +39222,21 @@ snapshots: - supports-color - ts-node + create-jest@29.7.0(@types/node@18.19.44)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@18.19.44)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)): dependencies: '@jest/types': 29.6.3 @@ -39946,10 +40036,6 @@ snapshots: dependencies: jake: 10.8.5 - ejs@3.1.8: - dependencies: - jake: 10.8.5 - electron-to-chromium@1.4.797: {} electron-to-chromium@1.5.6: {} @@ -43882,6 +43968,25 @@ snapshots: - supports-color - ts-node + jest-cli@29.7.0(@types/node@18.19.44)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@18.19.44)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@18.19.44)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) @@ -44154,6 +44259,37 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@18.19.44)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)): + dependencies: + '@babel/core': 7.24.7 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.24.7) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.7 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 18.19.44 + ts-node: 10.9.2(@types/node@18.19.44)(typescript@4.9.5) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@20.14.15)(ts-node@10.9.1(@types/node@22.5.1)(typescript@5.4.5)): dependencies: '@babel/core': 7.24.7 @@ -44309,6 +44445,37 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)): + dependencies: + '@babel/core': 7.24.7 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.24.7) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.7 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.14.15 + ts-node: 10.9.2(@types/node@18.19.44)(typescript@4.9.5) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)): dependencies: '@babel/core': 7.24.7 @@ -45201,6 +45368,18 @@ snapshots: - supports-color - ts-node + jest@29.7.0(@types/node@18.19.44)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)) + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@18.19.44)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) @@ -51334,6 +51513,25 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.24.7) + ts-jest@29.2.4(@babel/core@7.24.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(jest@29.7.0(@types/node@18.19.44)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)))(typescript@4.9.5): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@18.19.44)(ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5)) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.3 + typescript: 4.9.5 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.24.7 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.24.7) + ts-jest@29.2.4(@babel/core@7.24.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)))(typescript@5.5.4): dependencies: bs-logger: 0.2.6 @@ -51564,6 +51762,24 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + ts-node@10.9.2(@types/node@18.19.44)(typescript@4.9.5): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.3 + '@types/node': 18.19.44 + acorn: 8.12.1 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4): dependencies: '@cspotcode/source-map-support': 0.8.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b8abd396d1..0accc1c0d6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -26,4 +26,5 @@ packages: - "target_chains/solana/sdk/js/solana_utils" - "target_chains/solana/sdk/js/pyth_solana_receiver" - "target_chains/ton/contracts" + - "target_chains/ton/sdk/js" - "contract_manager" diff --git a/target_chains/ton/contracts/contracts/Main.fc b/target_chains/ton/contracts/contracts/Main.fc index e9a2e3bd01..ac16660a1e 100644 --- a/target_chains/ton/contracts/contracts/Main.fc +++ b/target_chains/ton/contracts/contracts/Main.fc @@ -19,6 +19,8 @@ ;; * 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); + } elseif (op == OP_UPDATE_PRICE_FEEDS) { + update_price_feeds(msg_value, data_slice); } elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) { execute_governance_action(data_slice); } elseif (op == OP_UPGRADE_CONTRACT) { diff --git a/target_chains/ton/contracts/package.json b/target_chains/ton/contracts/package.json index b7e2f21ac5..92efcd3cc4 100644 --- a/target_chains/ton/contracts/package.json +++ b/target_chains/ton/contracts/package.json @@ -8,14 +8,16 @@ "test": "jest --verbose" }, "devDependencies": { + "@pythnetwork/pyth-ton-js": "workspace:*", "@pythnetwork/price-service-sdk": "workspace:*", "@pythnetwork/xc-admin-common": "workspace:*", + "@pythnetwork/hermes-client": "workspace:*", "@ton/blueprint": "^0.22.0", - "@ton/core": "~0", - "@ton/crypto": "^3.2.0", + "@ton/core": "^0.59.0", + "@ton/crypto": "^3.3.0", "@ton/sandbox": "^0.20.0", "@ton/test-utils": "^0.4.2", - "@ton/ton": "^13.11.2", + "@ton/ton": "^15.1.0", "@types/jest": "^29.5.12", "@types/node": "^20.14.10", "@wormhole-foundation/sdk-definitions": "^0.10.7", diff --git a/target_chains/ton/contracts/scripts/deployPyth.ts b/target_chains/ton/contracts/scripts/deployPyth.ts index f20444eee7..4cd1f33be1 100644 --- a/target_chains/ton/contracts/scripts/deployPyth.ts +++ b/target_chains/ton/contracts/scripts/deployPyth.ts @@ -1,13 +1,122 @@ import { toNano } from "@ton/core"; +import { MainConfig } from "../wrappers/Main"; +import { compile, NetworkProvider, sleep } from "@ton/blueprint"; +import { DataSource } from "@pythnetwork/xc-admin-common"; +import { HermesClient } from "@pythnetwork/hermes-client"; import { Main } from "../wrappers/Main"; -import { compile, NetworkProvider } from "@ton/blueprint"; +import { + GOVERNANCE_DATA_SOURCE, + GUARDIAN_SET_0, + MAINNET_UPGRADE_VAAS, +} from "../tests/utils/wormhole"; +import { BTC_PRICE_FEED_ID, ETH_PRICE_FEED_ID } from "../tests/utils/pyth"; export async function run(provider: NetworkProvider) { - const main = provider.open(Main.createFromConfig({}, await compile("Main"))); + const SINGLE_UPDATE_FEE = 1; + const DATA_SOURCES: DataSource[] = [ + { + emitterChain: 26, + emitterAddress: + "e101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71", + }, + ]; - await main.sendDeploy(provider.sender(), toNano("0.05")); + const config: MainConfig = { + singleUpdateFee: SINGLE_UPDATE_FEE, + dataSources: DATA_SOURCES, + guardianSetIndex: 0, + guardianSet: GUARDIAN_SET_0, + chainId: 1, + governanceChainId: 1, + governanceContract: + "0000000000000000000000000000000000000000000000000000000000000004", + governanceDataSource: GOVERNANCE_DATA_SOURCE, + }; + + const main = provider.open( + Main.createFromConfig(config, await compile("Main")) + ); + + await main.sendDeploy(provider.sender(), toNano("0.005")); await provider.waitForDeploy(main.address); - // run methods on `main` + console.log("Main contract deployed at:", main.address.toString()); + + // Call sendUpdateGuardianSet for each VAA + const currentGuardianSetIndex = await main.getCurrentGuardianSetIndex(); + console.log(`Current guardian set index: ${currentGuardianSetIndex}`); + + for (let i = currentGuardianSetIndex; i < MAINNET_UPGRADE_VAAS.length; i++) { + const vaa = MAINNET_UPGRADE_VAAS[i]; + const vaaBuffer = Buffer.from(vaa, "hex"); + await main.sendUpdateGuardianSet(provider.sender(), vaaBuffer); + console.log( + `Successfully updated guardian set ${i + 1} with VAA: ${vaa.slice( + 0, + 20 + )}...` + ); + + // Wait for 30 seconds before checking the guardian set index + console.log("Waiting for 30 seconds before checking guardian set index..."); + await sleep(30000); + + // Verify the update + const newIndex = await main.getCurrentGuardianSetIndex(); + if (newIndex !== i + 1) { + console.error( + `Failed to update guardian set. Expected index ${ + i + 1 + }, got ${newIndex}` + ); + break; + } + } + + console.log("Guardian set update process completed."); + + // Initialize HermesClient + const hermesEndpoint = "https://hermes.pyth.network"; + const hermesClient = new HermesClient(hermesEndpoint); + + // Fetch latest price updates for BTC and ETH + const priceIds = [BTC_PRICE_FEED_ID, ETH_PRICE_FEED_ID]; + const latestPriceUpdates = await hermesClient.getLatestPriceUpdates( + priceIds, + { encoding: "hex" } + ); + console.log("Hermes BTC price:", latestPriceUpdates.parsed?.[0].price); + console.log("Hermes ETH price:", latestPriceUpdates.parsed?.[1].price); + // Combine updates into a single buffer + const updateData = Buffer.from(latestPriceUpdates.binary.data[0], "hex"); + console.log("Update data:", latestPriceUpdates.binary.data[0]); + + const singleUpdateFee = await main.getSingleUpdateFee(); + console.log("Single update fee:", singleUpdateFee); + + // NOTE: As of 2024/10/14 There's a bug with TON Access (https://ton.access.orbs.network) RPC service where if you provide an update data buffer with length of more than ~320 then the rpc returns error 404 and the function fails + const updateFee = await main.getUpdateFee(updateData); + console.log("Update fee:", updateFee); + + await main.sendUpdatePriceFeeds( + provider.sender(), + updateData, + toNano(updateFee) + ); + console.log("Price feeds updated successfully."); + + console.log("Waiting for 30 seconds before checking price feeds..."); + await sleep(30000); + + // Query updated price feeds + const btcPrice = await main.getPriceUnsafe(BTC_PRICE_FEED_ID); + console.log( + `Updated BTC price: ${btcPrice.price}, publish time: ${btcPrice.publishTime}` + ); + + const ethPrice = await main.getPriceUnsafe(ETH_PRICE_FEED_ID); + console.log( + `Updated ETH price: ${ethPrice.price}, publish time: ${ethPrice.publishTime}` + ); } diff --git a/target_chains/ton/contracts/tests/Main.spec.ts b/target_chains/ton/contracts/tests/Main.spec.ts index 0ee7bcaca8..74438ceb92 100644 --- a/target_chains/ton/contracts/tests/Main.spec.ts +++ b/target_chains/ton/contracts/tests/Main.spec.ts @@ -1,6 +1,6 @@ import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; import { Cell, toNano } from "@ton/core"; -import { Main } from "../wrappers/Main"; +import { Main, MainConfig } from "../wrappers/Main"; import "@ton/test-utils"; import { compile } from "@ton/blueprint"; @@ -17,8 +17,23 @@ describe("Main", () => { beforeEach(async () => { blockchain = await Blockchain.create(); - - main = blockchain.openContract(Main.createFromConfig({}, code)); + const config: MainConfig = { + singleUpdateFee: 0, + dataSources: [], + guardianSetIndex: 0, + guardianSet: [], + chainId: 0, + governanceChainId: 0, + governanceContract: + "0000000000000000000000000000000000000000000000000000000000000000", + governanceDataSource: { + emitterChain: 0, + emitterAddress: + "0000000000000000000000000000000000000000000000000000000000000000", + }, + }; + + main = blockchain.openContract(Main.createFromConfig(config, code)); deployer = await blockchain.treasury("deployer"); diff --git a/target_chains/ton/contracts/tests/utils.ts b/target_chains/ton/contracts/tests/utils.ts index bd94fd8789..cae3fa6cba 100644 --- a/target_chains/ton/contracts/tests/utils.ts +++ b/target_chains/ton/contracts/tests/utils.ts @@ -1,5 +1,5 @@ import { DataSource } from "@pythnetwork/xc-admin-common"; -import { Cell, Transaction, beginCell } from "@ton/core"; +import { Cell, Transaction } from "@ton/core"; import { Buffer } from "buffer"; const GOVERNANCE_MAGIC = 0x5054474d; @@ -7,50 +7,6 @@ const GOVERNANCE_MODULE = 1; const AUTHORIZE_UPGRADE_CONTRACT_ACTION = 0; const TARGET_CHAIN_ID = 1; -export function createCellChain(buffer: Buffer): Cell { - let chunks = bufferToChunks(buffer, 127); - let lastCell: Cell | null = null; - // Iterate through chunks in reverse order - for (let i = chunks.length - 1; i >= 0; i--) { - const chunk = chunks[i]; - const cellBuilder = beginCell(); - const buffer = Buffer.from(chunk); - cellBuilder.storeBuffer(buffer); - - if (lastCell) { - cellBuilder.storeRef(lastCell); - } - - lastCell = cellBuilder.endCell(); - } - - // lastCell will be the root cell of our chain - if (!lastCell) { - throw new Error("Failed to create cell chain"); - } - return lastCell; -} - -function bufferToChunks( - buff: Buffer, - chunkSizeBytes: number = 127 -): Uint8Array[] { - const chunks: Uint8Array[] = []; - const uint8Array = new Uint8Array( - buff.buffer, - buff.byteOffset, - buff.byteLength - ); - - for (let offset = 0; offset < uint8Array.length; offset += chunkSizeBytes) { - const remainingBytes = Math.min(chunkSizeBytes, uint8Array.length - offset); - const chunk = uint8Array.subarray(offset, offset + remainingBytes); - chunks.push(chunk); - } - - return chunks; -} - // Helper function to parse DataSource from a Cell export function parseDataSource(cell: Cell): DataSource { const slice = cell.beginParse(); diff --git a/target_chains/ton/contracts/tests/utils/wormhole.ts b/target_chains/ton/contracts/tests/utils/wormhole.ts index b5607caebd..097493a691 100644 --- a/target_chains/ton/contracts/tests/utils/wormhole.ts +++ b/target_chains/ton/contracts/tests/utils/wormhole.ts @@ -1,3 +1,4 @@ +import { DataSource } from "@pythnetwork/xc-admin-common"; import { beginCell, Cell, Dictionary } from "@ton/core"; export const GUARDIAN_SET_0 = ["0x58CC3AE5C097B213CE3C81979E1B9F9570746AA5"]; @@ -191,3 +192,9 @@ export const MAINNET_UPGRADE_VAAS = [ "01000000020d00ce45474d9e1b1e7790a2d210871e195db53a70ffd6f237cfe70e2686a32859ac43c84a332267a8ef66f59719cf91cc8df0101fd7c36aa1878d5139241660edc0010375cc906156ae530786661c0cd9aef444747bc3d8d5aa84cac6a6d2933d4e1a031cffa30383d4af8131e929d9f203f460b07309a647d6cd32ab1cc7724089392c000452305156cfc90343128f97e499311b5cae174f488ff22fbc09591991a0a73d8e6af3afb8a5968441d3ab8437836407481739e9850ad5c95e6acfcc871e951bc30105a7956eefc23e7c945a1966d5ddbe9e4be376c2f54e45e3d5da88c2f8692510c7429b1ea860ae94d929bd97e84923a18187e777aa3db419813a80deb84cc8d22b00061b2a4f3d2666608e0aa96737689e3ba5793810ff3a52ff28ad57d8efb20967735dc5537a2e43ef10f583d144c12a1606542c207f5b79af08c38656d3ac40713301086b62c8e130af3411b3c0d91b5b50dcb01ed5f293963f901fc36e7b0e50114dce203373b32eb45971cef8288e5d928d0ed51cd86e2a3006b0af6a65c396c009080009e93ab4d2c8228901a5f4525934000b2c26d1dc679a05e47fdf0ff3231d98fbc207103159ff4116df2832eea69b38275283434e6cd4a4af04d25fa7a82990b707010aa643f4cf615dfff06ffd65830f7f6cf6512dabc3690d5d9e210fdc712842dc2708b8b2c22e224c99280cd25e5e8bfb40e3d1c55b8c41774e287c1e2c352aecfc010b89c1e85faa20a30601964ccc6a79c0ae53cfd26fb10863db37783428cd91390a163346558239db3cd9d420cfe423a0df84c84399790e2e308011b4b63e6b8015010ca31dcb564ac81a053a268d8090e72097f94f366711d0c5d13815af1ec7d47e662e2d1bde22678113d15963da100b668ba26c0c325970d07114b83c5698f46097010dc9fda39c0d592d9ed92cd22b5425cc6b37430e236f02d0d1f8a2ef45a00bde26223c0a6eb363c8b25fd3bf57234a1d9364976cefb8360e755a267cbbb674b39501108db01e444ab1003dd8b6c96f8eb77958b40ba7a85fefecf32ad00b7a47c0ae7524216262495977e09c0989dd50f280c21453d3756843608eacd17f4fdfe47600001261025228ef5af837cb060bcd986fcfa84ccef75b3fa100468cfd24e7fadf99163938f3b841a33496c2706d0208faab088bd155b2e20fd74c625bb1cc8c43677a0163c53c409e0c5dfa000100000000000000000000000000000000000000000000000000000000000000046c5a054d7833d1e42000000000000000000000000000000000000000000000000000000000436f7265020000000000031358cc3ae5c097b213ce3c81979e1b9f9570746aa5ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cd15e7caf07c4e3dc8e7c469f92c8cd88fb8005a2074a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d", "01000000030d03d4a37a6ff4361d91714730831e9d49785f61624c8f348a9c6c1d82bc1d98cadc5e936338204445c6250bb4928f3f3e165ad47ca03a5d63111168a2de4576856301049a5df10464ea4e1961589fd30fc18d1970a7a2ffaad617e56a0f7777f25275253af7d10a0f0f2494dc6e99fc80e444ab9ebbbee252ded2d5dcb50cbf7a54bb5a01055f4603b553b9ba9e224f9c55c7bca3da00abb10abd19e0081aecd3b352be061a70f79f5f388ebe5190838ef3cd13a2f22459c9a94206883b739c90b40d5d74640006a8fade3997f650a36e46bceb1f609edff201ab32362266f166c5c7da713f6a19590c20b68ed3f0119cb24813c727560ede086b3d610c2d7a1efa66f655bad90900080f5e495a75ea52241c59d145c616bfac01e57182ad8d784cbcc9862ed3afb60c0983ccbc690553961ffcf115a0c917367daada8e60be2cbb8b8008bac6341a8c010935ab11e0eea28b87a1edc5ccce3f1fac25f75b5f640fe6b0673a7cd74513c9dc01c544216cf364cc9993b09fda612e0cd1ced9c00fb668b872a16a64ebb55d27010ab2bc39617a2396e7defa24cd7c22f42dc31f3c42ffcd9d1472b02df8468a4d0563911e8fb6a4b5b0ce0bd505daa53779b08ff660967b31f246126ed7f6f29a7e000bdb6d3fd7b33bdc9ac3992916eb4aacb97e7e21d19649e7fa28d2dd6e337937e4274516a96c13ac7a8895da9f91948ea3a09c25f44b982c62ce8842b58e20c8a9000d3d1b19c8bb000856b6610b9d28abde6c35cb7705c6ca5db711f7be96d60eed9d72cfa402a6bfe8bf0496dbc7af35796fc768da51a067b95941b3712dce8ae1e7010ec80085033157fd1a5628fc0c56267469a86f0e5a66d7dede1ad4ce74ecc3dff95b60307a39c3bfbeedc915075070da30d0395def9635130584f709b3885e1bdc0010fc480eb9ee715a2d151b23722b48b42581d7f4001fc1696c75425040bfc1ffc5394fe418adb2b64bd3dc692efda4cc408163677dbe233b16bcdabb853a20843301118ee9e115e1a0c981f19d0772b850e666591322da742a9a12cce9f52a5665bd474abdd59c580016bee8aae67fdf39b315be2528d12eec3a652910e03cc4c6fa3801129d0d1e2e429e969918ec163d16a7a5b2c6729aa44af5dccad07d25d19891556a79b574f42d9adbd9e2a9ae5a6b8750331d2fccb328dd94c3bf8791ee1bfe85aa00661e99781981faea00010000000000000000000000000000000000000000000000000000000000000004fd4c6c55ec8dfd342000000000000000000000000000000000000000000000000000000000436f726502000000000004135893b5a76c3f739645648885bdccc06cd70a3cd3ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cd15e7caf07c4e3dc8e7c469f92c8cd88fb8005a2074a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d", ]; + +export const GOVERNANCE_DATA_SOURCE: DataSource = { + emitterChain: 1, + emitterAddress: + "0000000000000000000000000000000000000000000000000000000000000029", +}; diff --git a/target_chains/ton/contracts/wrappers/BaseWrapper.ts b/target_chains/ton/contracts/wrappers/BaseWrapper.ts new file mode 100644 index 0000000000..548534858f --- /dev/null +++ b/target_chains/ton/contracts/wrappers/BaseWrapper.ts @@ -0,0 +1,328 @@ +import { + Address, + beginCell, + Cell, + Contract, + ContractProvider, + Dictionary, + Sender, + SendMode, + toNano, +} from "@ton/core"; +import { createCellChain } from "@pythnetwork/pyth-ton-js"; +import { createGuardianSetsDict } from "../tests/utils/wormhole"; +import { HexString, Price } from "@pythnetwork/price-service-sdk"; +import { DataSource } from "@pythnetwork/xc-admin-common"; + +export class BaseWrapper implements Contract { + constructor( + readonly address: Address, + readonly init?: { code: Cell; data: Cell } + ) {} + + static createFromAddress(address: Address) { + return new this(address); + } + + static createInitData(config: { + priceFeedId?: HexString; + timePeriod?: number; + price?: Price; + emaPrice?: Price; + singleUpdateFee?: number; + dataSources?: DataSource[]; + guardianSetIndex: number; + guardianSet: string[]; + chainId: number; + governanceChainId: number; + governanceContract: string; + governanceDataSource?: DataSource; + }): Cell { + const priceDict = Dictionary.empty( + Dictionary.Keys.BigUint(256), + Dictionary.Values.Cell() + ); + + if ( + config.priceFeedId && + config.price && + config.emaPrice && + config.timePeriod + ) { + const priceCell = beginCell() + .storeInt( + config.price.getPriceAsNumberUnchecked() * 10 ** -config.price.expo, + 64 + ) + .storeUint( + config.price.getConfAsNumberUnchecked() * 10 ** -config.price.expo, + 64 + ) + .storeInt(config.price.expo, 32) + .storeUint(config.price.publishTime, 64) + .endCell(); + + const emaPriceCell = beginCell() + .storeInt( + config.emaPrice.getPriceAsNumberUnchecked() * + 10 ** -config.emaPrice.expo, + 64 + ) + .storeUint( + config.emaPrice.getConfAsNumberUnchecked() * + 10 ** -config.emaPrice.expo, + 64 + ) + .storeInt(config.emaPrice.expo, 32) + .storeUint(config.emaPrice.publishTime, 64) + .endCell(); + + const priceFeedCell = beginCell() + .storeRef(priceCell) + .storeRef(emaPriceCell) + .storeUint(config.timePeriod, 32) + .endCell(); + + priceDict.set(BigInt(config.priceFeedId), priceFeedCell); + } + + // Create a dictionary for data sources + const dataSourcesDict = Dictionary.empty( + Dictionary.Keys.Uint(32), + Dictionary.Values.Cell() + ); + // Create a dictionary for valid data sources + const isValidDataSourceDict = Dictionary.empty( + Dictionary.Keys.BigUint(256), + Dictionary.Values.Bool() + ); + + if (config.dataSources) { + config.dataSources.forEach((source, index) => { + const sourceCell = beginCell() + .storeUint(source.emitterChain, 16) + .storeBuffer(Buffer.from(source.emitterAddress, "hex")) + .endCell(); + dataSourcesDict.set(index, sourceCell); + const cellHash = BigInt("0x" + sourceCell.hash().toString("hex")); + isValidDataSourceDict.set(cellHash, true); + }); + } + + // Group price feeds and update fee + const priceFeedsCell = beginCell() + .storeDict(priceDict) + .storeUint(config.singleUpdateFee || 0, 256) + .endCell(); + + // Group data sources information + const dataSourcesCell = beginCell() + .storeDict(dataSourcesDict) + .storeUint(config.dataSources ? config.dataSources.length : 0, 32) + .storeDict(isValidDataSourceDict) + .endCell(); + + // Group guardian set information + const guardianSetCell = beginCell() + .storeUint(config.guardianSetIndex, 32) + .storeDict( + createGuardianSetsDict(config.guardianSet, config.guardianSetIndex) + ) + .endCell(); + + // Group chain and governance information + const governanceCell = beginCell() + .storeUint(config.chainId, 16) + .storeUint(config.governanceChainId, 16) + .storeBuffer(Buffer.from(config.governanceContract, "hex")) + .storeDict(Dictionary.empty()) // consumed_governance_actions + .storeRef( + config.governanceDataSource + ? beginCell() + .storeUint(config.governanceDataSource.emitterChain, 16) + .storeBuffer( + Buffer.from(config.governanceDataSource.emitterAddress, "hex") + ) + .endCell() + : beginCell().endCell() + ) // governance_data_source + .storeUint(0, 64) // last_executed_governance_sequence, set to 0 for initial state + .storeUint(0, 32) // governance_data_source_index, set to 0 for initial state + .storeUint(0, 256) // upgrade_code_hash, set to 0 for initial state + .endCell(); + + // Create the main cell with references to grouped data + return beginCell() + .storeRef(priceFeedsCell) + .storeRef(dataSourcesCell) + .storeRef(guardianSetCell) + .storeRef(governanceCell) + .endCell(); + } + + async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { + await provider.internal(via, { + value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell().endCell(), + }); + } + + async getCurrentGuardianSetIndex( + provider: ContractProvider, + methodName: string + ) { + const result = await provider.get(methodName, []); + return result.stack.readNumber(); + } + + async sendUpdateGuardianSet( + provider: ContractProvider, + via: Sender, + vm: Buffer + ) { + const messageBody = beginCell() + .storeUint(1, 32) // OP_UPDATE_GUARDIAN_SET + .storeRef(createCellChain(vm)) + .endCell(); + + await provider.internal(via, { + value: toNano("0.1"), + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageBody, + }); + } + + async sendUpdatePriceFeeds( + provider: ContractProvider, + via: Sender, + updateData: Buffer, + updateFee: bigint + ) { + const messageBody = beginCell() + .storeUint(2, 32) // OP_UPDATE_PRICE_FEEDS + .storeRef(createCellChain(updateData)) + .endCell(); + + await provider.internal(via, { + value: updateFee, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageBody, + }); + } + + async getChainId(provider: ContractProvider, methodName: string) { + const result = await provider.get(methodName, []); + return result.stack.readNumber(); + } + + async getPriceUnsafe( + provider: ContractProvider, + priceFeedId: HexString, + methodName: string + ) { + const result = await provider.get(methodName, [ + { type: "int", value: BigInt(priceFeedId) }, + ]); + + const price = result.stack.readNumber(); + const conf = result.stack.readNumber(); + const expo = result.stack.readNumber(); + const publishTime = result.stack.readNumber(); + + return { + price, + conf, + expo, + publishTime, + }; + } + + async getPriceNoOlderThan( + provider: ContractProvider, + timePeriod: number, + priceFeedId: HexString, + methodName: string + ) { + const result = await provider.get(methodName, [ + { type: "int", value: BigInt(timePeriod) }, + { type: "int", value: BigInt(priceFeedId) }, + ]); + + const price = result.stack.readNumber(); + const conf = result.stack.readNumber(); + const expo = result.stack.readNumber(); + const publishTime = result.stack.readNumber(); + + return { + price, + conf, + expo, + publishTime, + }; + } + + async getEmaPriceUnsafe( + provider: ContractProvider, + priceFeedId: HexString, + methodName: string + ) { + const result = await provider.get(methodName, [ + { type: "int", value: BigInt(priceFeedId) }, + ]); + + const price = result.stack.readNumber(); + const conf = result.stack.readNumber(); + const expo = result.stack.readNumber(); + const publishTime = result.stack.readNumber(); + + return { + price, + conf, + expo, + publishTime, + }; + } + + async getEmaPriceNoOlderThan( + provider: ContractProvider, + timePeriod: number, + priceFeedId: HexString, + methodName: string + ) { + const result = await provider.get(methodName, [ + { type: "int", value: BigInt(timePeriod) }, + { type: "int", value: BigInt(priceFeedId) }, + ]); + + const price = result.stack.readNumber(); + const conf = result.stack.readNumber(); + const expo = result.stack.readNumber(); + const publishTime = result.stack.readNumber(); + + return { + price, + conf, + expo, + publishTime, + }; + } + + async getUpdateFee( + provider: ContractProvider, + vm: Buffer, + methodName: string + ) { + const result = await provider.get(methodName, [ + { type: "slice", cell: createCellChain(vm) }, + ]); + + return result.stack.readNumber(); + } + + async getSingleUpdateFee(provider: ContractProvider, methodName: string) { + const result = await provider.get(methodName, []); + + return result.stack.readNumber(); + } +} diff --git a/target_chains/ton/contracts/wrappers/Main.ts b/target_chains/ton/contracts/wrappers/Main.ts index d13d563bf8..0c04af1351 100644 --- a/target_chains/ton/contracts/wrappers/Main.ts +++ b/target_chains/ton/contracts/wrappers/Main.ts @@ -1,41 +1,110 @@ -import { - Address, - beginCell, - Cell, - Contract, - contractAddress, - ContractProvider, - Sender, - SendMode, -} from "@ton/core"; - -export type MainConfig = {}; - -export function mainConfigToCell(config: MainConfig): Cell { - return beginCell().endCell(); -} +import { Cell, contractAddress, ContractProvider, Sender } from "@ton/core"; +import { HexString } from "@pythnetwork/price-service-sdk"; -export class Main implements Contract { - constructor( - readonly address: Address, - readonly init?: { code: Cell; data: Cell } - ) {} +import { BaseWrapper } from "./BaseWrapper"; +import { DataSource } from "@pythnetwork/xc-admin-common"; - static createFromAddress(address: Address) { - return new Main(address); - } +export type MainConfig = { + singleUpdateFee: number; + dataSources: DataSource[]; + guardianSetIndex: number; + guardianSet: string[]; + chainId: number; + governanceChainId: number; + governanceContract: string; + governanceDataSource?: DataSource; +}; +export class Main extends BaseWrapper { static createFromConfig(config: MainConfig, code: Cell, workchain = 0) { - const data = mainConfigToCell(config); + const data = Main.mainConfigToCell(config); const init = { code, data }; return new Main(contractAddress(workchain, init), init); } + static mainConfigToCell(config: MainConfig): Cell { + return BaseWrapper.createInitData(config); + } + async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { - await provider.internal(via, { - value, - sendMode: SendMode.PAY_GAS_SEPARATELY, - body: beginCell().endCell(), - }); + await super.sendDeploy(provider, via, value); + } + + async getCurrentGuardianSetIndex(provider: ContractProvider) { + return await super.getCurrentGuardianSetIndex( + provider, + "get_current_guardian_set_index" + ); + } + + async sendUpdateGuardianSet( + provider: ContractProvider, + via: Sender, + vm: Buffer + ) { + await super.sendUpdateGuardianSet(provider, via, vm); + } + + async sendUpdatePriceFeeds( + provider: ContractProvider, + via: Sender, + updateData: Buffer, + updateFee: bigint + ) { + await super.sendUpdatePriceFeeds(provider, via, updateData, updateFee); + } + + async getPriceUnsafe(provider: ContractProvider, priceFeedId: HexString) { + return await super.getPriceUnsafe( + provider, + priceFeedId, + "get_price_unsafe" + ); + } + + async getPriceNoOlderThan( + provider: ContractProvider, + timePeriod: number, + priceFeedId: HexString + ) { + return await super.getPriceNoOlderThan( + provider, + timePeriod, + priceFeedId, + "get_price_no_older_than" + ); + } + + async getEmaPriceUnsafe(provider: ContractProvider, priceFeedId: HexString) { + return await super.getEmaPriceUnsafe( + provider, + priceFeedId, + "get_ema_price_unsafe" + ); + } + + async getEmaPriceNoOlderThan( + provider: ContractProvider, + timePeriod: number, + priceFeedId: HexString + ) { + return await super.getEmaPriceNoOlderThan( + provider, + timePeriod, + priceFeedId, + "get_ema_price_no_older_than" + ); + } + + async getUpdateFee(provider: ContractProvider, vm: Buffer) { + return await super.getUpdateFee(provider, vm, "get_update_fee"); + } + + async getSingleUpdateFee(provider: ContractProvider) { + return await super.getSingleUpdateFee(provider, "get_single_update_fee"); + } + + async getChainId(provider: ContractProvider) { + return await super.getChainId(provider, "get_chain_id"); } } diff --git a/target_chains/ton/contracts/wrappers/PythTest.ts b/target_chains/ton/contracts/wrappers/PythTest.ts index 302b1230ff..809219dcef 100644 --- a/target_chains/ton/contracts/wrappers/PythTest.ts +++ b/target_chains/ton/contracts/wrappers/PythTest.ts @@ -1,18 +1,15 @@ import { - Address, beginCell, Cell, - Contract, contractAddress, ContractProvider, - Dictionary, Sender, SendMode, toNano, } from "@ton/core"; +import { BaseWrapper } from "./BaseWrapper"; import { HexString, Price } from "@pythnetwork/price-service-sdk"; -import { createCellChain } from "../tests/utils"; -import { createGuardianSetsDict } from "../tests/utils/wormhole"; +import { createCellChain } from "@pythnetwork/pyth-ton-js"; import { DataSource } from "@pythnetwork/xc-admin-common"; export type PythTestConfig = { @@ -30,170 +27,27 @@ export type PythTestConfig = { governanceDataSource?: DataSource; }; -export class PythTest implements Contract { - constructor( - readonly address: Address, - readonly init?: { code: Cell; data: Cell } - ) {} - - static createFromAddress(address: Address) { - return new PythTest(address); - } - +export class PythTest extends BaseWrapper { static createFromConfig(config: PythTestConfig, code: Cell, workchain = 0) { - const data = PythTest.getPythInitData( - config.priceFeedId, - config.timePeriod, - config.price, - config.emaPrice, - config.singleUpdateFee, - config.dataSources, - config.guardianSetIndex, - config.guardianSet, - config.chainId, - config.governanceChainId, - config.governanceContract, - config.governanceDataSource - ); + const data = PythTest.getPythInitData(config); const init = { code, data }; return new PythTest(contractAddress(workchain, init), init); } - static getPythInitData( - priceFeedId: HexString, - timePeriod: number, - price: Price, - emaPrice: Price, - singleUpdateFee: number, - dataSources: DataSource[], - guardianSetIndex: number, - guardianSet: string[], - chainId: number, - governanceChainId: number, - governanceContract: string, - governanceDataSource?: DataSource - ): Cell { - const priceDict = Dictionary.empty( - Dictionary.Keys.BigUint(256), - Dictionary.Values.Cell() - ); - - const priceCell = beginCell() - .storeInt(price.getPriceAsNumberUnchecked() * 10 ** -price.expo, 64) - .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, 64) - .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); - - // Create a dictionary for data sources - const dataSourcesDict = Dictionary.empty( - Dictionary.Keys.Uint(32), - Dictionary.Values.Cell() - ); - // Create a dictionary for valid data sources - const isValidDataSourceDict = Dictionary.empty( - Dictionary.Keys.BigUint(256), - Dictionary.Values.Bool() - ); - - dataSources.forEach((source, index) => { - const sourceCell = beginCell() - .storeUint(source.emitterChain, 16) - .storeBuffer(Buffer.from(source.emitterAddress, "hex")) - .endCell(); - dataSourcesDict.set(index, sourceCell); - const cellHash = BigInt("0x" + sourceCell.hash().toString("hex")); - isValidDataSourceDict.set(cellHash, true); - }); - - // Group price feeds and update fee - const priceFeedsCell = beginCell() - .storeDict(priceDict) - .storeUint(singleUpdateFee, 256) - .endCell(); - - // Group data sources information - const dataSourcesCell = beginCell() - .storeDict(dataSourcesDict) - .storeUint(dataSources.length, 32) - .storeDict(isValidDataSourceDict) - .endCell(); - - // Group guardian set information - const guardianSetCell = beginCell() - .storeUint(guardianSetIndex, 32) - .storeDict(createGuardianSetsDict(guardianSet, guardianSetIndex)) - .endCell(); - - // Group chain and governance information - const governanceCell = beginCell() - .storeUint(chainId, 16) - .storeUint(governanceChainId, 16) - .storeBuffer(Buffer.from(governanceContract, "hex")) - .storeDict(Dictionary.empty()) // consumed_governance_actions - .storeRef( - governanceDataSource - ? beginCell() - .storeUint(governanceDataSource.emitterChain, 16) - .storeBuffer( - Buffer.from(governanceDataSource.emitterAddress, "hex") - ) - .endCell() - : beginCell().endCell() - ) // governance_data_source - .storeUint(0, 64) // last_executed_governance_sequence, set to 0 for initial state - .storeUint(0, 32) // governance_data_source_index, set to 0 for initial state - .storeUint(0, 256) // upgrade_code_hash, set to 0 for initial state - .endCell(); - - // Create the main cell with references to grouped data - return beginCell() - .storeRef(priceFeedsCell) - .storeRef(dataSourcesCell) - .storeRef(guardianSetCell) - .storeRef(governanceCell) - .endCell(); + static getPythInitData(config: PythTestConfig): Cell { + return BaseWrapper.createInitData(config); } async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { - await provider.internal(via, { - value, - sendMode: SendMode.PAY_GAS_SEPARATELY, - body: beginCell().endCell(), - }); + await super.sendDeploy(provider, via, value); } async getPriceUnsafe(provider: ContractProvider, priceFeedId: HexString) { - const result = await provider.get("test_get_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, - }; + return await super.getPriceUnsafe( + provider, + priceFeedId, + "test_get_price_unsafe" + ); } async getPriceNoOlderThan( @@ -201,40 +55,20 @@ export class PythTest implements Contract { timePeriod: number, priceFeedId: HexString ) { - const result = await provider.get("test_get_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, - }; + return await super.getPriceNoOlderThan( + provider, + timePeriod, + priceFeedId, + "test_get_price_no_older_than" + ); } async getEmaPriceUnsafe(provider: ContractProvider, priceFeedId: HexString) { - const result = await provider.get("test_get_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, - }; + return await super.getEmaPriceUnsafe( + provider, + priceFeedId, + "test_get_ema_price_unsafe" + ); } async getEmaPriceNoOlderThan( @@ -242,35 +76,23 @@ export class PythTest implements Contract { timePeriod: number, priceFeedId: HexString ) { - const result = await provider.get("test_get_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, - }; + return await super.getEmaPriceNoOlderThan( + provider, + timePeriod, + priceFeedId, + "test_get_ema_price_no_older_than" + ); } async getUpdateFee(provider: ContractProvider, vm: Buffer) { - const result = await provider.get("test_get_update_fee", [ - { type: "slice", cell: createCellChain(vm) }, - ]); - - return result.stack.readNumber(); + return await super.getUpdateFee(provider, vm, "test_get_update_fee"); } async getSingleUpdateFee(provider: ContractProvider) { - const result = await provider.get("test_get_single_update_fee", []); - return result.stack.readNumber(); + return await super.getSingleUpdateFee( + provider, + "test_get_single_update_fee" + ); } async sendUpdatePriceFeeds( @@ -279,16 +101,7 @@ export class PythTest implements Contract { updateData: Buffer, updateFee: bigint ) { - const messageBody = beginCell() - .storeUint(2, 32) // OP_UPDATE_PRICE_FEEDS - .storeRef(createCellChain(updateData)) - .endCell(); - - await provider.internal(via, { - value: updateFee, - sendMode: SendMode.PAY_GAS_SEPARATELY, - body: messageBody, - }); + await super.sendUpdatePriceFeeds(provider, via, updateData, updateFee); } async sendUpdateGuardianSet( @@ -296,23 +109,14 @@ export class PythTest implements Contract { via: Sender, vm: Buffer ) { - const messageBody = beginCell() - .storeUint(1, 32) // OP_UPDATE_GUARDIAN_SET - .storeRef(createCellChain(vm)) - .endCell(); - - await provider.internal(via, { - value: toNano("0.1"), - sendMode: SendMode.PAY_GAS_SEPARATELY, - body: messageBody, - }); + await super.sendUpdateGuardianSet(provider, via, vm); } async getChainId(provider: ContractProvider) { - const result = await provider.get("test_get_chain_id", []); - return result.stack.readNumber(); + return await super.getChainId(provider, "test_get_chain_id"); } + // Add PythTest-specific methods here async getLastExecutedGovernanceSequence(provider: ContractProvider) { const result = await provider.get( "test_get_last_executed_governance_sequence", diff --git a/target_chains/ton/contracts/wrappers/WormholeTest.ts b/target_chains/ton/contracts/wrappers/WormholeTest.ts index fed414792b..946e5f00b6 100644 --- a/target_chains/ton/contracts/wrappers/WormholeTest.ts +++ b/target_chains/ton/contracts/wrappers/WormholeTest.ts @@ -1,20 +1,7 @@ -import { - Address, - beginCell, - Cell, - Contract, - contractAddress, - ContractProvider, - Dictionary, - Sender, - SendMode, - toNano, -} from "@ton/core"; -import { createCellChain } from "../tests/utils"; -import { - createGuardianSetsDict, - parseGuardianSetKeys, -} from "../tests/utils/wormhole"; +import { Cell, contractAddress, ContractProvider, Sender } from "@ton/core"; +import { BaseWrapper } from "./BaseWrapper"; +import { createCellChain } from "@pythnetwork/pyth-ton-js"; +import { parseGuardianSetKeys } from "../tests/utils/wormhole"; export type WormholeTestConfig = { guardianSetIndex: number; @@ -24,88 +11,31 @@ export type WormholeTestConfig = { governanceContract: string; }; -export class WormholeTest implements Contract { - constructor( - readonly address: Address, - readonly init?: { code: Cell; data: Cell } - ) {} - - static createFromAddress(address: Address) { - return new WormholeTest(address); - } - +export class WormholeTest extends BaseWrapper { static createFromConfig( config: WormholeTestConfig, code: Cell, workchain = 0 ) { - const data = WormholeTest.getWormholeInitData( - config.guardianSetIndex, - config.guardianSet, - config.chainId, - config.governanceChainId, - config.governanceContract - ); + const data = WormholeTest.getWormholeInitData(config); const init = { code, data }; return new WormholeTest(contractAddress(workchain, init), init); } - static getWormholeInitData( - guardianSetIndex: number, - guardianSet: string[], - chainId: number, - governanceChainId: number, - governanceContract: string - ): Cell { - // Group price feeds and update fee (empty for Wormhole) - const priceFeedsCell = beginCell() - .storeDict(Dictionary.empty()) // latest_price_feeds, empty for initial state - .storeUint(0, 256) // single_update_fee, set to 0 for testing - .endCell(); - - // Group data sources information (empty for initial state) - const dataSourcesCell = beginCell() - .storeDict(Dictionary.empty()) // data_sources, empty for initial state - .storeUint(0, 32) // num_data_sources, set to 0 for initial state - .storeDict(Dictionary.empty()) // is_valid_data_source, empty for initial state - .endCell(); - - // Group guardian set information - const guardianSetCell = beginCell() - .storeUint(guardianSetIndex, 32) - .storeDict(createGuardianSetsDict(guardianSet, guardianSetIndex)) - .endCell(); - - // Group chain and governance information - const governanceCell = beginCell() - .storeUint(chainId, 16) - .storeUint(governanceChainId, 16) - .storeBuffer(Buffer.from(governanceContract, "hex")) - .storeDict(Dictionary.empty()) // consumed_governance_actions, empty for initial state - .storeRef(beginCell()) // governance_data_source, empty for initial state - .storeUint(0, 64) // last_executed_governance_sequence, set to 0 for initial state - .storeUint(0, 32) // governance_data_source_index, set to 0 for initial state - .storeUint(0, 256) // upgrade_code_hash, set to 0 for initial state - .endCell(); - - // Create the main cell with references to grouped data - return beginCell() - .storeRef(priceFeedsCell) - .storeRef(dataSourcesCell) - .storeRef(guardianSetCell) - .storeRef(governanceCell) - .endCell(); + static getWormholeInitData(config: WormholeTestConfig): Cell { + return BaseWrapper.createInitData({ + guardianSetIndex: config.guardianSetIndex, + guardianSet: config.guardianSet, + chainId: config.chainId, + governanceChainId: config.governanceChainId, + governanceContract: config.governanceContract, + }); } async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { - await provider.internal(via, { - value, - sendMode: SendMode.PAY_GAS_SEPARATELY, - body: beginCell().endCell(), - }); + await super.sendDeploy(provider, via, value); } - // NOTE: the function name has to start with "send" or "get" so that it automatically inserts `provider` as a first argument async getParseEncodedUpgrade( provider: ContractProvider, currentGuardianSetIndex: number, @@ -172,12 +102,10 @@ export class WormholeTest implements Contract { } async getCurrentGuardianSetIndex(provider: ContractProvider) { - const result = await provider.get( - "test_get_current_guardian_set_index", - [] + return await super.getCurrentGuardianSetIndex( + provider, + "test_get_current_guardian_set_index" ); - - return result.stack.readNumber(); } async getGuardianSet(provider: ContractProvider, index: number) { @@ -202,8 +130,7 @@ export class WormholeTest implements Contract { } async getChainId(provider: ContractProvider) { - const result = await provider.get("test_get_chain_id", []); - return result.stack.readNumber(); + return await super.getChainId(provider, "test_get_chain_id"); } async getGovernanceContract(provider: ContractProvider) { @@ -227,15 +154,6 @@ export class WormholeTest implements Contract { via: Sender, vm: Buffer ) { - const messageBody = beginCell() - .storeUint(1, 32) // OP_UPDATE_GUARDIAN_SET - .storeRef(createCellChain(vm)) - .endCell(); - - await provider.internal(via, { - value: toNano("0.1"), - sendMode: SendMode.PAY_GAS_SEPARATELY, - body: messageBody, - }); + await super.sendUpdateGuardianSet(provider, via, vm); } } diff --git a/target_chains/ton/sdk/js/.eslintrc.js b/target_chains/ton/sdk/js/.eslintrc.js new file mode 100644 index 0000000000..fca472f38b --- /dev/null +++ b/target_chains/ton/sdk/js/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], +}; diff --git a/target_chains/ton/sdk/js/.gitignore b/target_chains/ton/sdk/js/.gitignore new file mode 100644 index 0000000000..a65b41774a --- /dev/null +++ b/target_chains/ton/sdk/js/.gitignore @@ -0,0 +1 @@ +lib diff --git a/target_chains/ton/sdk/js/README.md b/target_chains/ton/sdk/js/README.md new file mode 100644 index 0000000000..da66eedcd7 --- /dev/null +++ b/target_chains/ton/sdk/js/README.md @@ -0,0 +1,106 @@ +# Pyth Network TON SDK + +This SDK provides a JavaScript interface for interacting with the Pyth Network on the TON blockchain. + +## Installation + +```bash +npm install @pythnetwork/pyth-ton-js +``` + +## Usage + +Here's a basic example of how to use the SDK: + +```bash +npm install @pythnetwork/pyth-ton-js @pythnetwork/hermes-client @ton/core @ton/ton @ton/crypto +``` + +```typescript +import { TonClient, Address, WalletContractV4 } from "@ton/ton"; +import { toNano } from "@ton/core"; +import { mnemonicToPrivateKey } from "@ton/crypto"; +import { HermesClient } from "@pythnetwork/hermes-client"; +import { + PythContract, + PYTH_CONTRACT_ADDRESS_TESTNET, +} from "@pythnetwork/pyth-ton-js"; + +const BTC_PRICE_FEED_ID = + "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; + +async function main() { + const client = new TonClient({ + endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC", + apiKey: "your-api-key-here", // Optional, but note that without api-key you need to send requests once per second + }); + + const contractAddress = Address.parse(PYTH_CONTRACT_ADDRESS_TESTNET); + const contract = client.open(PythContract.createFromAddress(contractAddress)); + + const guardianSetIndex = await contract.getCurrentGuardianSetIndex(); + console.log("Guardian Set Index:", guardianSetIndex); + + const price = await contract.getPriceUnsafe(BTC_PRICE_FEED_ID); + console.log("BTC Price from TON contract:", price); + + const hermesEndpoint = "https://hermes.pyth.network"; + const hermesClient = new HermesClient(hermesEndpoint); + + const priceIds = [BTC_PRICE_FEED_ID]; + const latestPriceUpdates = await hermesClient.getLatestPriceUpdates( + priceIds, + { + encoding: "hex", + } + ); + console.log("Hermes BTC price:", latestPriceUpdates.parsed?.[0].price); + + const updateData = Buffer.from(latestPriceUpdates.binary.data[0], "hex"); + console.log("Update data:", updateData); + + const updateFee = await contract.getUpdateFee(updateData); + console.log("Update fee:", updateFee); + + const mnemonic = "your mnemonic here"; + const key = await mnemonicToPrivateKey(mnemonic.split(" ")); + const wallet = WalletContractV4.create({ + publicKey: key.publicKey, + workchain: 0, + }); + const provider = client.open(wallet); + + await contract.sendUpdatePriceFeeds( + provider.sender(key.secretKey), + updateData, + 156000000n + BigInt(updateFee) // 156000000 = 390000 (estimated gas used for the transaction, this is defined in contracts/common/gas.fc as UPDATE_PRICE_FEEDS_GAS) * 400 (current settings in basechain are as follows: 1 unit of gas costs 400 nanotons) + ); + console.log("Price feeds updated successfully."); + + const updatedPrice = await contract.getPriceUnsafe(BTC_PRICE_FEED_ID); + console.log("Updated BTC Price from TON contract:", updatedPrice); +} + +main().catch(console.error); +``` + +## API Reference + +### `PythContract` + +The main class for interacting with the Pyth contract on TON. + +#### Methods: + +- `getCurrentGuardianSetIndex(provider: ContractProvider): Promise` +- `getPriceUnsafe(provider: ContractProvider, priceFeedId: string): Promise` +- `getPriceNoOlderThan(provider: ContractProvider, timePeriod: number, priceFeedId: string): Promise` +- `getEmaPriceUnsafe(provider: ContractProvider, priceFeedId: string): Promise` +- `getEmaPriceNoOlderThan(provider: ContractProvider, timePeriod: number, priceFeedId: string): Promise` +- `getUpdateFee(provider: ContractProvider, vm: Buffer): Promise` +- `getSingleUpdateFee(provider: ContractProvider): Promise` +- `sendUpdatePriceFeeds(provider: ContractProvider, via: Sender, updateData: Buffer, updateFee: bigint): Promise` + +### Constants + +- `PYTH_CONTRACT_ADDRESS_TESTNET`: The address of the Pyth contract on the TON testnet. diff --git a/target_chains/ton/sdk/js/package.json b/target_chains/ton/sdk/js/package.json new file mode 100644 index 0000000000..6dc9d711cd --- /dev/null +++ b/target_chains/ton/sdk/js/package.json @@ -0,0 +1,51 @@ +{ + "name": "@pythnetwork/pyth-ton-js", + "version": "0.1.0", + "description": "Pyth Network TON Utilities", + "homepage": "https://pyth.network", + "author": { + "name": "Pyth Data Association" + }, + "main": "lib/index.js", + "types": "lib/index.d.ts", + "files": [ + "lib/**/*" + ], + "repository": { + "type": "git", + "url": "https://github.com/pyth-network/pyth-crosschain", + "directory": "target_chains/ton/sdk/js" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "jest --passWithNoTests", + "build": "tsc", + "format": "prettier --write \"src/**/*.ts\"", + "lint": "eslint src/", + "prepublishOnly": "pnpm run build && pnpm test && pnpm run lint", + "preversion": "pnpm run lint", + "version": "pnpm run format && git add -A src" + }, + "keywords": [ + "pyth", + "oracle" + ], + "license": "Apache-2.0", + "devDependencies": { + "@ton/core": "^0.59.0", + "@ton/ton": "^15.1.0", + "@ton/crypto": "^3.3.0", + "@types/node": "^18.11.18", + "@typescript-eslint/eslint-plugin": "^5.21.0", + "@typescript-eslint/parser": "^5.21.0", + "eslint": "^8.14.0", + "jest": "^29.4.1", + "prettier": "^2.6.2", + "ts-jest": "^29.0.5", + "typescript": "^4.6.3", + "ts-node": "^10.9.2" + }, + "dependencies": {} +} diff --git a/target_chains/ton/sdk/js/src/index.ts b/target_chains/ton/sdk/js/src/index.ts new file mode 100644 index 0000000000..7d6fdfda5e --- /dev/null +++ b/target_chains/ton/sdk/js/src/index.ts @@ -0,0 +1,184 @@ +import { + Address, + beginCell, + Cell, + Contract, + Sender, + SendMode, +} from "@ton/core"; +import { ContractProvider } from "@ton/ton"; + +export const PYTH_CONTRACT_ADDRESS_TESTNET = + "EQDwGkJmcj7MMmWAHmhldnY-lAKI6hcTQ2tAEcapmwCnztQU"; + +export class PythContract implements Contract { + constructor( + readonly address: Address, + readonly init?: { code: Cell; data: Cell } + ) {} + + static createFromAddress(address: Address) { + return new PythContract(address); + } + + async getCurrentGuardianSetIndex(provider: ContractProvider) { + const result = await provider.get("get_current_guardian_set_index", []); + + return result.stack.readNumber(); + } + + async sendUpdatePriceFeeds( + provider: ContractProvider, + via: Sender, + updateData: Buffer, + updateFee: bigint + ) { + const messageBody = beginCell() + .storeUint(2, 32) // OP_UPDATE_PRICE_FEEDS + .storeRef(createCellChain(updateData)) + .endCell(); + + await provider.internal(via, { + value: updateFee, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageBody, + }); + } + + async getPriceUnsafe(provider: ContractProvider, priceFeedId: string) { + const result = await provider.get("get_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: string + ) { + const result = await provider.get("get_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: string) { + const result = await provider.get("get_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: string + ) { + const result = await provider.get("get_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, + }; + } + + async getUpdateFee(provider: ContractProvider, vm: Buffer) { + const result = await provider.get("get_update_fee", [ + { type: "slice", cell: createCellChain(vm) }, + ]); + + return result.stack.readNumber(); + } + + async getSingleUpdateFee(provider: ContractProvider) { + const result = await provider.get("get_single_update_fee", []); + + return result.stack.readNumber(); + } +} + +export function createCellChain(buffer: Buffer): Cell { + const chunks = bufferToChunks(buffer, 127); + let lastCell: Cell | null = null; + // Iterate through chunks in reverse order + for (let i = chunks.length - 1; i >= 0; i--) { + const chunk = chunks[i]; + const cellBuilder = beginCell(); + const buffer = Buffer.from(chunk); + cellBuilder.storeBuffer(buffer); + + if (lastCell) { + cellBuilder.storeRef(lastCell); + } + + lastCell = cellBuilder.endCell(); + } + + // lastCell will be the root cell of our chain + if (!lastCell) { + throw new Error("Failed to create cell chain"); + } + return lastCell; +} + +function bufferToChunks(buff: Buffer, chunkSizeBytes = 127): Uint8Array[] { + const chunks: Uint8Array[] = []; + const uint8Array = new Uint8Array( + buff.buffer, + buff.byteOffset, + buff.byteLength + ); + + for (let offset = 0; offset < uint8Array.length; offset += chunkSizeBytes) { + const remainingBytes = Math.min(chunkSizeBytes, uint8Array.length - offset); + const chunk = uint8Array.subarray(offset, offset + remainingBytes); + chunks.push(chunk); + } + + return chunks; +} diff --git a/target_chains/ton/sdk/js/tsconfig.json b/target_chains/ton/sdk/js/tsconfig.json new file mode 100644 index 0000000000..bae5d05cde --- /dev/null +++ b/target_chains/ton/sdk/js/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "target": "esnext", + "module": "commonjs", + "declaration": true, + "outDir": "./lib", + "rootDir": "src/", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": ["src", "src/**/*.json"], + "exclude": ["node_modules", "**/__tests__/*"] +}