Просмотр исходного кода

[price-pusher] sui (#825)

* sui pusher

* cache mapping

* typo

* remove comment

* add mainnet config

* update readme

* bump version
Dev Kalra 2 лет назад
Родитель
Сommit
1c529dd486

+ 254 - 0
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",

+ 23 - 0
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<version> -- <above-arguments>

+ 11 - 0
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"
+}

+ 11 - 0
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"
+}

+ 2 - 1
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",

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

+ 133 - 0
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();
+  },
+};

+ 254 - 0
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<PriceInfo | undefined> {
+    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<void> {
+    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;
+}