From 1c529dd486c1cee13f99049af56ff3c076a7e090 Mon Sep 17 00:00:00 2001 From: Dev Kalra Date: Sat, 20 May 2023 00:05:34 +0530 Subject: [PATCH] [price-pusher] sui (#825) * sui pusher * cache mapping * typo * remove comment * add mainnet config * update readme * bump version --- package-lock.json | 254 ++++++++++++++++++++ price_pusher/README.md | 23 ++ price_pusher/config.sui.mainnet.sample.json | 11 + price_pusher/config.sui.testnet.sample.json | 11 + price_pusher/package.json | 3 +- price_pusher/src/index.ts | 2 + price_pusher/src/sui/command.ts | 133 ++++++++++ price_pusher/src/sui/sui.ts | 254 ++++++++++++++++++++ 8 files changed, 690 insertions(+), 1 deletion(-) create mode 100644 price_pusher/config.sui.mainnet.sample.json create mode 100644 price_pusher/config.sui.testnet.sample.json create mode 100644 price_pusher/src/sui/command.ts create mode 100644 price_pusher/src/sui/sui.ts diff --git a/package-lock.json b/package-lock.json index c750884d..9bc9239b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9568,6 +9568,147 @@ "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz", "integrity": "sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==" }, + "node_modules/@mysten/bcs": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-0.7.1.tgz", + "integrity": "sha512-wFPb8bkhwrbiStfZMV5rFM7J+umpke59/dNjDp+UYJKykNlW23LCk2ePyEUvGdb62HGJM1jyOJ8g4egE3OmdKA==", + "dependencies": { + "bs58": "^5.0.0" + } + }, + "node_modules/@mysten/bcs/node_modules/base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + }, + "node_modules/@mysten/bcs/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/@mysten/sui.js": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.34.0.tgz", + "integrity": "sha512-mNb4vX+HSm/Y2oJSDeCNOUV7L7IXW1fRQ0zU7fFUAeJdNgf1ObFmxiItVCA7GU0EXoSPtYnpxcdJFiBcSnQtbA==", + "dependencies": { + "@mysten/bcs": "0.7.1", + "@noble/curves": "^1.0.0", + "@noble/hashes": "^1.3.0", + "@scure/bip32": "^1.3.0", + "@scure/bip39": "^1.2.0", + "@suchipi/femver": "^1.0.0", + "jayson": "^4.0.0", + "rpc-websockets": "^7.5.1", + "superstruct": "^1.0.3", + "tweetnacl": "^1.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@mysten/sui.js/node_modules/@noble/hashes": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", + "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@mysten/sui.js/node_modules/@scure/bip32": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.0.tgz", + "integrity": "sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "@noble/curves": "~1.0.0", + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/@mysten/sui.js/node_modules/@scure/bip39": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.0.tgz", + "integrity": "sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/@mysten/sui.js/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "node_modules/@mysten/sui.js/node_modules/jayson": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz", + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "JSONStream": "^1.3.5", + "uuid": "^8.3.2", + "ws": "^7.4.5" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@mysten/sui.js/node_modules/superstruct": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.3.tgz", + "integrity": "sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@mysten/sui.js/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@next/env": { "version": "12.2.5", "resolved": "https://registry.npmjs.org/@next/env/-/env-12.2.5.tgz", @@ -15074,6 +15215,11 @@ "uuid": "^8.3.2" } }, + "node_modules/@suchipi/femver": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@suchipi/femver/-/femver-1.0.0.tgz", + "integrity": "sha512-bprE8+K5V+DPX7q2e2K57ImqNBdfGHDIWaGI5xHxZoxbKOuQZn4wzPiUxOAHnsUr3w3xHrWXwN7gnG/iIuEMIg==" + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -55432,6 +55578,7 @@ "license": "Apache-2.0", "dependencies": { "@injectivelabs/sdk-ts": "1.10.72", + "@mysten/sui.js": "^0.34.0", "@pythnetwork/price-service-client": "*", "@pythnetwork/pyth-sdk-solidity": "*", "@truffle/hdwallet-provider": "^2.1.3", @@ -64893,6 +65040,107 @@ "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz", "integrity": "sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==" }, + "@mysten/bcs": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-0.7.1.tgz", + "integrity": "sha512-wFPb8bkhwrbiStfZMV5rFM7J+umpke59/dNjDp+UYJKykNlW23LCk2ePyEUvGdb62HGJM1jyOJ8g4egE3OmdKA==", + "requires": { + "bs58": "^5.0.0" + }, + "dependencies": { + "base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + }, + "bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "requires": { + "base-x": "^4.0.0" + } + } + } + }, + "@mysten/sui.js": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.34.0.tgz", + "integrity": "sha512-mNb4vX+HSm/Y2oJSDeCNOUV7L7IXW1fRQ0zU7fFUAeJdNgf1ObFmxiItVCA7GU0EXoSPtYnpxcdJFiBcSnQtbA==", + "requires": { + "@mysten/bcs": "0.7.1", + "@noble/curves": "^1.0.0", + "@noble/hashes": "^1.3.0", + "@scure/bip32": "^1.3.0", + "@scure/bip39": "^1.2.0", + "@suchipi/femver": "^1.0.0", + "jayson": "^4.0.0", + "rpc-websockets": "^7.5.1", + "superstruct": "^1.0.3", + "tweetnacl": "^1.0.3" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", + "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==" + }, + "@scure/bip32": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.0.tgz", + "integrity": "sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q==", + "requires": { + "@noble/curves": "~1.0.0", + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + } + }, + "@scure/bip39": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.0.tgz", + "integrity": "sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg==", + "requires": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + } + }, + "@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "jayson": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz", + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", + "requires": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "JSONStream": "^1.3.5", + "uuid": "^8.3.2", + "ws": "^7.4.5" + } + }, + "superstruct": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.3.tgz", + "integrity": "sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg==" + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "requires": {} + } + } + }, "@next/env": { "version": "12.2.5", "resolved": "https://registry.npmjs.org/@next/env/-/env-12.2.5.tgz", @@ -67057,6 +67305,7 @@ "version": "file:price_pusher", "requires": { "@injectivelabs/sdk-ts": "1.10.72", + "@mysten/sui.js": "*", "@pythnetwork/price-service-client": "*", "@pythnetwork/pyth-sdk-solidity": "*", "@truffle/hdwallet-provider": "^2.1.3", @@ -72813,6 +73062,11 @@ "uuid": "^8.3.2" } }, + "@suchipi/femver": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@suchipi/femver/-/femver-1.0.0.tgz", + "integrity": "sha512-bprE8+K5V+DPX7q2e2K57ImqNBdfGHDIWaGI5xHxZoxbKOuQZn4wzPiUxOAHnsUr3w3xHrWXwN7gnG/iIuEMIg==" + }, "@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", diff --git a/price_pusher/README.md b/price_pusher/README.md index 9553db55..04a2cb57 100644 --- a/price_pusher/README.md +++ b/price_pusher/README.md @@ -79,6 +79,29 @@ npm run start -- aptos --endpoint https://fullnode.testnet.aptoslabs.com/v1 \ [--pushing-frequency 10] \ [--polling-frequency 5] \ +# For Sui +npm run start -- sui + --endpoint https://sui-testnet-rpc.allthatnode.com, + --pyth-package-id 0x975e063f398f720af4f33ec06a927f14ea76ca24f7f8dd544aa62ab9d5d15f44, + --pyth-state-id 0xd8afde3a48b4ff7212bd6829a150f43f59043221200d63504d981f62bff2e27a, + --wormhole-package-id 0xcc029e2810f17f9f43f52262f40026a71fbdca40ed3803ad2884994361910b7e, + --wormhole-state-id 0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02, + --price-feed-to-price-info-object-table-id 0xf8929174008c662266a1adde78e1e8e33016eb7ad37d379481e860b911e40ed5, + --price-service-endpoint https://xc-testnet.pyth.network, + --mnemonic-file ./mnemonic, + --price-config-file ./price-config.testnet.sample.yaml + [--pushing-frequency 10] \ + [--polling-frequency 5] \ + + + +--endpoint https://fullnode.testnet.aptoslabs.com/v1 \ + --pyth-contract-address 0x7e783b349d3e89cf5931af376ebeadbfab855b3fa239b7ada8f5a92fbea6b387 --price-service-endpoint "https://xc-testnet.pyth.network" \ + --price-config-file "./price-config.testnet.sample.yaml" \ + --mnemonic-file "path/to/mnemonic.txt" \ + [--pushing-frequency 10] \ + [--polling-frequency 5] \ + # Or, run the price pusher docker image instead of building from the source docker run public.ecr.aws/pyth-network/xc-price-pusher:v -- diff --git a/price_pusher/config.sui.mainnet.sample.json b/price_pusher/config.sui.mainnet.sample.json new file mode 100644 index 00000000..00f098ad --- /dev/null +++ b/price_pusher/config.sui.mainnet.sample.json @@ -0,0 +1,11 @@ +{ + "endpoint": "https://sui-testnet-rpc.allthatnode.com", + "pyth-package-id": "0x00b53b0f4174108627fbee72e2498b58d6a2714cded53fac537034c220d26302", + "pyth-state-id": "0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f", + "wormhole-package-id": "0x5306f64e312b581766351c07af79c72fcb1cd25147157fdc2f8ad76de9a3fb6a", + "wormhole-state-id": "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c", + "price-feed-to-price-info-object-table-id": "0x14b4697477d24c30c8eecc31dd1bd49a3115a9fe0db6bd4fd570cf14640b79a0", + "price-service-endpoint": "https://xc-mainnet.pyth.network", + "mnemonic-file": "./mnemonic", + "price-config-file": "./price-config.mainnet.sample.yaml" +} diff --git a/price_pusher/config.sui.testnet.sample.json b/price_pusher/config.sui.testnet.sample.json new file mode 100644 index 00000000..4baf1a3c --- /dev/null +++ b/price_pusher/config.sui.testnet.sample.json @@ -0,0 +1,11 @@ +{ + "endpoint": "https://sui-testnet-rpc.allthatnode.com", + "pyth-package-id": "0x975e063f398f720af4f33ec06a927f14ea76ca24f7f8dd544aa62ab9d5d15f44", + "pyth-state-id": "0xd8afde3a48b4ff7212bd6829a150f43f59043221200d63504d981f62bff2e27a", + "wormhole-package-id": "0xcc029e2810f17f9f43f52262f40026a71fbdca40ed3803ad2884994361910b7e", + "wormhole-state-id": "0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02", + "price-feed-to-price-info-object-table-id": "0xf8929174008c662266a1adde78e1e8e33016eb7ad37d379481e860b911e40ed5", + "price-service-endpoint": "https://xc-testnet.pyth.network", + "mnemonic-file": "./mnemonic", + "price-config-file": "./price-config.testnet.sample.yaml" +} diff --git a/price_pusher/package.json b/price_pusher/package.json index 0ba1b26a..5becae66 100644 --- a/price_pusher/package.json +++ b/price_pusher/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/price-pusher", - "version": "5.1.0", + "version": "5.2.0", "description": "Pyth Price Pusher", "homepage": "https://pyth.network", "main": "lib/index.js", @@ -52,6 +52,7 @@ }, "dependencies": { "@injectivelabs/sdk-ts": "1.10.72", + "@mysten/sui.js": "^0.34.0", "@pythnetwork/price-service-client": "*", "@pythnetwork/pyth-sdk-solidity": "*", "@truffle/hdwallet-provider": "^2.1.3", diff --git a/price_pusher/src/index.ts b/price_pusher/src/index.ts index 29b37f17..55662eb1 100644 --- a/price_pusher/src/index.ts +++ b/price_pusher/src/index.ts @@ -4,6 +4,7 @@ import { hideBin } from "yargs/helpers"; import injective from "./injective/command"; import evm from "./evm/command"; import aptos from "./aptos/command"; +import sui from "./sui/command"; yargs(hideBin(process.argv)) .config("config") @@ -11,4 +12,5 @@ yargs(hideBin(process.argv)) .command(evm) .command(injective) .command(aptos) + .command(sui) .help().argv; diff --git a/price_pusher/src/sui/command.ts b/price_pusher/src/sui/command.ts new file mode 100644 index 00000000..38001acc --- /dev/null +++ b/price_pusher/src/sui/command.ts @@ -0,0 +1,133 @@ +import { PriceServiceConnection } from "@pythnetwork/price-service-client"; +import * as options from "../options"; +import { readPriceConfigFile } from "../price-config"; +import fs from "fs"; +import { PythPriceListener } from "../pyth-price-listener"; +import { Controller } from "../controller"; +import { Options } from "yargs"; +import { SuiPriceListener, SuiPricePusher } from "./sui"; + +export default { + command: "sui", + describe: + "Run price pusher for sui. Most of the arguments below are" + + "network specific, so there's one set of values for mainnet and" + + "another for testnet. See config.sui..sample.json for the " + + "appropriate values for your network. ", + builder: { + endpoint: { + description: + "RPC endpoint URL for sui. The pusher will periodically" + + "poll for updates. The polling interval is configurable via the " + + "`polling-frequency` command-line argument.", + type: "string", + required: true, + } as Options, + "pyth-package-id": { + description: + "Pyth Package Id. Can be found here" + + "https://docs.pyth.network/pythnet-price-feeds/sui", + type: "string", + required: true, + } as Options, + "pyth-state-id": { + description: + "Pyth State Id. Can be found here" + + "https://docs.pyth.network/pythnet-price-feeds/sui", + type: "string", + required: true, + } as Options, + "wormhole-package-id": { + description: + "Wormhole Package Id. Can be found here" + + "https://docs.pyth.network/pythnet-price-feeds/sui", + type: "string", + required: true, + } as Options, + "wormhole-state-id": { + description: + "Wormhole State Id. Can be found here" + + "https://docs.pyth.network/pythnet-price-feeds/sui", + type: "string", + required: true, + } as Options, + "price-feed-to-price-info-object-table-id": { + description: + "This is the id of the table which stored the information related to price data. You can find it here: " + + "https://docs.pyth.network/pythnet-price-feeds/sui", + type: "string", + required: true, + } as Options, + ...options.priceConfigFile, + ...options.priceServiceEndpoint, + ...options.mnemonicFile, + ...options.pollingFrequency, + ...options.pushingFrequency, + }, + handler: function (argv: any) { + const { + endpoint, + priceConfigFile, + priceServiceEndpoint, + mnemonicFile, + pushingFrequency, + pollingFrequency, + pythPackageId, + pythStateId, + wormholePackageId, + wormholeStateId, + priceFeedToPriceInfoObjectTableId, + } = argv; + + const priceConfigs = readPriceConfigFile(priceConfigFile); + const priceServiceConnection = new PriceServiceConnection( + priceServiceEndpoint, + { + logger: { + // Log only warnings and errors from the price service client + info: () => undefined, + warn: console.warn, + error: console.error, + debug: () => undefined, + trace: () => undefined, + }, + } + ); + const mnemonic = fs.readFileSync(mnemonicFile, "utf-8").trim(); + + const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias })); + + const pythListener = new PythPriceListener( + priceServiceConnection, + priceItems + ); + + const suiListener = new SuiPriceListener( + pythPackageId, + priceFeedToPriceInfoObjectTableId, + endpoint, + priceItems, + { pollingFrequency } + ); + const suiPusher = new SuiPricePusher( + priceServiceConnection, + pythPackageId, + pythStateId, + wormholePackageId, + wormholeStateId, + priceFeedToPriceInfoObjectTableId, + endpoint, + mnemonic + ); + + const controller = new Controller( + priceConfigs, + pythListener, + suiListener, + suiPusher, + { pushingFrequency } + ); + + controller.start(); + }, +}; diff --git a/price_pusher/src/sui/sui.ts b/price_pusher/src/sui/sui.ts new file mode 100644 index 00000000..16b8b750 --- /dev/null +++ b/price_pusher/src/sui/sui.ts @@ -0,0 +1,254 @@ +import { + ChainPriceListener, + IPricePusher, + PriceInfo, + PriceItem, +} from "../interface"; +import { DurationInSeconds } from "../utils"; +import { PriceServiceConnection } from "@pythnetwork/price-service-client"; +import { + JsonRpcProvider, + Connection, + Ed25519Keypair, + RawSigner, + TransactionBlock, + SUI_CLOCK_OBJECT_ID, +} from "@mysten/sui.js"; + +export class SuiPriceListener extends ChainPriceListener { + constructor( + private pythPackageId: string, + private priceFeedToPriceInfoObjectTableId: string, + private endpoint: string, + priceItems: PriceItem[], + config: { + pollingFrequency: DurationInSeconds; + } + ) { + super("sui", config.pollingFrequency, priceItems); + } + + async getOnChainPriceInfo(priceId: string): Promise { + try { + const provider = new JsonRpcProvider( + new Connection({ fullnode: this.endpoint }) + ); + + const priceInfoObjectId = await priceIdToPriceInfoObjectId( + provider, + this.pythPackageId, + this.priceFeedToPriceInfoObjectTableId, + priceId + ); + + // Fetching the price info object for the above priceInfoObjectId + const priceInfoObject = await provider.getObject({ + id: priceInfoObjectId, + options: { showContent: true }, + }); + + if ( + priceInfoObject.data === undefined || + priceInfoObject.data.content === undefined + ) + throw new Error("Price not found on chain for price id " + priceId); + + if (priceInfoObject.data.content.dataType !== "moveObject") + throw new Error("fetched object datatype should be moveObject"); + + const { magnitude, negative } = + priceInfoObject.data.content.fields.price_info.fields.price_feed.fields + .price.fields.price.fields; + + const conf = + priceInfoObject.data.content.fields.price_info.fields.price_feed.fields + .price.fields.conf; + + const timestamp = + priceInfoObject.data.content.fields.price_info.fields.price_feed.fields + .price.fields.timestamp; + + return { + price: negative ? "-" + magnitude : magnitude, + conf, + publishTime: Number(timestamp), + }; + } catch (e) { + console.error(`Polling Sui on-chain price for ${priceId} failed. Error:`); + console.error(e); + return undefined; + } + } +} + +export class SuiPricePusher implements IPricePusher { + private readonly signer: RawSigner; + constructor( + private priceServiceConnection: PriceServiceConnection, + private pythPackageId: string, + private pythStateId: string, + private wormholePackageId: string, + private wormholeStateId: string, + private priceFeedToPriceInfoObjectTableId: string, + endpoint: string, + mnemonic: string + ) { + this.signer = new RawSigner( + Ed25519Keypair.deriveKeypair(mnemonic), + new JsonRpcProvider(new Connection({ fullnode: endpoint })) + ); + } + + async updatePriceFeed( + priceIds: string[], + pubTimesToPush: number[] + ): Promise { + if (priceIds.length === 0) { + return; + } + + if (priceIds.length !== pubTimesToPush.length) + throw new Error("Invalid arguments"); + + const tx = new TransactionBlock(); + + const vaas = await this.priceServiceConnection.getLatestVaas(priceIds); + + // Parse our batch price attestation VAA bytes using Wormhole. + // Check out the Wormhole cross-chain bridge and generic messaging protocol here: + // https://github.com/wormhole-foundation/wormhole + let verified_vaas: any = []; + for (const vaa of vaas) { + const [verified_vaa] = tx.moveCall({ + target: `${this.wormholePackageId}::vaa::parse_and_verify`, + arguments: [ + tx.object(this.wormholeStateId), + tx.pure([...Buffer.from(vaa, "base64")]), + tx.object(SUI_CLOCK_OBJECT_ID), + ], + }); + verified_vaas = verified_vaas.concat(verified_vaa); + } + + // Create a hot potato vector of price feed updates that will + // be used to update price feeds. + let [price_updates_hot_potato] = tx.moveCall({ + target: `${this.pythPackageId}::pyth::create_price_infos_hot_potato`, + arguments: [ + tx.object(this.pythStateId), + tx.makeMoveVec({ + type: `${this.wormholePackageId}::vaa::VAA`, + objects: verified_vaas, + }), + tx.object(SUI_CLOCK_OBJECT_ID), + ], + }); + + // Update each price info object (containing our price feeds of interest) + // using the hot potato vector. + for (const priceId of priceIds) { + let priceInfoObjectId; + try { + priceInfoObjectId = await priceIdToPriceInfoObjectId( + this.signer.provider, + this.pythPackageId, + this.priceFeedToPriceInfoObjectTableId, + priceId + ); + } catch (e) { + console.log("Error fetching price info object id for ", priceId); + console.error(e); + return; + } + const coin = tx.splitCoins(tx.gas, [tx.pure(1)]); + [price_updates_hot_potato] = tx.moveCall({ + target: `${this.pythPackageId}::pyth::update_single_price_feed`, + arguments: [ + tx.object(this.pythStateId), + price_updates_hot_potato, + tx.object(priceInfoObjectId), + coin, + tx.object(SUI_CLOCK_OBJECT_ID), + ], + }); + } + + // Explicitly destroy the hot potato vector, since it can't be dropped + // automatically. + tx.moveCall({ + target: `${this.pythPackageId}::hot_potato_vector::destroy`, + arguments: [price_updates_hot_potato], + typeArguments: [`${this.pythPackageId}::price_info::PriceInfo`], + }); + + try { + const result = await this.signer.signAndExecuteTransactionBlock({ + transactionBlock: tx, + options: { + showInput: true, + showEffects: true, + showEvents: true, + showObjectChanges: true, + showBalanceChanges: true, + }, + }); + + console.log( + "Successfully updated price with transaction digest ", + result.digest + ); + } catch (e) { + console.log("Error when signAndExecuteTransactionBlock"); + if (String(e).includes("GasBalanceTooLow")) { + console.log("Insufficient Gas Amount. Please top up your account"); + process.exit(); + } + console.error(e); + return; + } + } +} + +// We are calculating stored price info object id for given price id +// The mapping between which is static. Hence, we are caching it here. +const CACHE: { [priceId: string]: string } = {}; + +// For given priceid, this method will fetch the price info object id +// where the price information for the corresponding price feed is stored +async function priceIdToPriceInfoObjectId( + provider: JsonRpcProvider, + pythPackageId: string, + priceFeedToPriceInfoObjectTableId: string, + priceId: string +) { + // Check if this was fetched before. + if (CACHE[priceId] !== undefined) return CACHE[priceId]; + + const storedObjectID = await provider.getDynamicFieldObject({ + parentId: priceFeedToPriceInfoObjectTableId, + name: { + type: `${pythPackageId}::price_identifier::PriceIdentifier`, + value: { + bytes: "0x" + priceId, + }, + }, + }); + + if (storedObjectID.error !== undefined) throw storedObjectID.error; + + if ( + storedObjectID.data === undefined || + storedObjectID.data.content === undefined + ) + throw new Error("Price not found on chain for price id " + priceId); + + if (storedObjectID.data.content.dataType !== "moveObject") + throw new Error("fetched object datatype should be moveObject"); + // This ID points to the price info object for the given price id stored on chain + const priceInfoObjectId = storedObjectID.data.content.fields.value; + + // cache the price info object id + CACHE[priceId] = priceInfoObjectId; + + return priceInfoObjectId; +}