@pythnetwork/price-pusher 4.1.2 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -71,6 +71,37 @@ npm run start -- injective --grpc-endpoint https://grpc-endpoint.com \
71
71
  [--pushing-frequency 10] \
72
72
  [--polling-frequency 5] \
73
73
 
74
+ # For Aptos
75
+ npm run start -- aptos --endpoint https://fullnode.testnet.aptoslabs.com/v1 \
76
+ --pyth-contract-address 0x7e783b349d3e89cf5931af376ebeadbfab855b3fa239b7ada8f5a92fbea6b387 --price-service-endpoint "https://xc-testnet.pyth.network" \
77
+ --price-config-file "./price-config.testnet.sample.yaml" \
78
+ --mnemonic-file "path/to/mnemonic.txt" \
79
+ [--pushing-frequency 10] \
80
+ [--polling-frequency 5] \
81
+
82
+ # For Sui
83
+ npm run start -- sui
84
+ --endpoint https://sui-testnet-rpc.allthatnode.com,
85
+ --pyth-package-id 0x975e063f398f720af4f33ec06a927f14ea76ca24f7f8dd544aa62ab9d5d15f44,
86
+ --pyth-state-id 0xd8afde3a48b4ff7212bd6829a150f43f59043221200d63504d981f62bff2e27a,
87
+ --wormhole-package-id 0xcc029e2810f17f9f43f52262f40026a71fbdca40ed3803ad2884994361910b7e,
88
+ --wormhole-state-id 0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02,
89
+ --price-feed-to-price-info-object-table-id 0xf8929174008c662266a1adde78e1e8e33016eb7ad37d379481e860b911e40ed5,
90
+ --price-service-endpoint https://xc-testnet.pyth.network,
91
+ --mnemonic-file ./mnemonic,
92
+ --price-config-file ./price-config.testnet.sample.yaml
93
+ [--pushing-frequency 10] \
94
+ [--polling-frequency 5] \
95
+
96
+
97
+
98
+ --endpoint https://fullnode.testnet.aptoslabs.com/v1 \
99
+ --pyth-contract-address 0x7e783b349d3e89cf5931af376ebeadbfab855b3fa239b7ada8f5a92fbea6b387 --price-service-endpoint "https://xc-testnet.pyth.network" \
100
+ --price-config-file "./price-config.testnet.sample.yaml" \
101
+ --mnemonic-file "path/to/mnemonic.txt" \
102
+ [--pushing-frequency 10] \
103
+ [--polling-frequency 5] \
104
+
74
105
 
75
106
  # Or, run the price pusher docker image instead of building from the source
76
107
  docker run public.ecr.aws/pyth-network/xc-price-pusher:v<version> -- <above-arguments>
@@ -0,0 +1,30 @@
1
+ import { ChainPriceListener, IPricePusher, PriceInfo, PriceItem } from "../interface";
2
+ import { DurationInSeconds } from "../utils";
3
+ import { PriceServiceConnection } from "@pythnetwork/price-service-client";
4
+ export declare class AptosPriceListener extends ChainPriceListener {
5
+ private pythModule;
6
+ private endpoint;
7
+ constructor(pythModule: string, endpoint: string, priceItems: PriceItem[], config: {
8
+ pollingFrequency: DurationInSeconds;
9
+ });
10
+ getOnChainPriceInfo(priceId: string): Promise<PriceInfo | undefined>;
11
+ }
12
+ export declare class AptosPricePusher implements IPricePusher {
13
+ private priceServiceConnection;
14
+ private pythContractAddress;
15
+ private endpoint;
16
+ private mnemonic;
17
+ private overrideGasPriceMultiplier;
18
+ private lastPushAttempt;
19
+ private readonly accountHDPath;
20
+ constructor(priceServiceConnection: PriceServiceConnection, pythContractAddress: string, endpoint: string, mnemonic: string, overrideGasPriceMultiplier: number);
21
+ /**
22
+ * Gets price update data which then can be submitted to the Pyth contract to update the prices.
23
+ * This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price ids)
24
+ *
25
+ * @param priceIds Array of hex-encoded price ids.
26
+ * @returns Array of price update data.
27
+ */
28
+ getPriceFeedsUpdateData(priceIds: string[]): Promise<number[][]>;
29
+ updatePriceFeed(priceIds: string[], pubTimesToPush: number[]): Promise<void>;
30
+ }
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AptosPricePusher = exports.AptosPriceListener = void 0;
4
+ const interface_1 = require("../interface");
5
+ const aptos_1 = require("aptos");
6
+ class AptosPriceListener extends interface_1.ChainPriceListener {
7
+ pythModule;
8
+ endpoint;
9
+ constructor(pythModule, endpoint, priceItems, config) {
10
+ super("aptos", config.pollingFrequency, priceItems);
11
+ this.pythModule = pythModule;
12
+ this.endpoint = endpoint;
13
+ }
14
+ async getOnChainPriceInfo(priceId) {
15
+ try {
16
+ const client = new aptos_1.AptosClient(this.endpoint);
17
+ const res = await client.getAccountResource(this.pythModule, `${this.pythModule}::state::LatestPriceInfo`);
18
+ // This depends upon the pyth contract storage on Aptos and should not be undefined.
19
+ // If undefined, there has been some change and we would need to update accordingly.
20
+ const handle = res.data.info.handle;
21
+ const priceItemRes = await client.getTableItem(handle, {
22
+ key_type: `${this.pythModule}::price_identifier::PriceIdentifier`,
23
+ value_type: `${this.pythModule}::price_info::PriceInfo`,
24
+ key: {
25
+ bytes: priceId,
26
+ },
27
+ });
28
+ const multiplier = priceItemRes.price_feed.price.price.negative === true ? -1 : 1;
29
+ const price = multiplier * Number(priceItemRes.price_feed.price.price.magnitude);
30
+ console.log(`Polled an Aptos on-chain price for feed ${this.priceIdToAlias.get(priceId)} (${priceId}).`);
31
+ return {
32
+ price: price.toString(),
33
+ conf: priceItemRes.price_feed.price.conf,
34
+ publishTime: Number(priceItemRes.price_feed.price.timestamp),
35
+ };
36
+ }
37
+ catch (e) {
38
+ console.error(`Polling Aptos on-chain price for ${priceId} failed. Error:`);
39
+ console.error(e);
40
+ return undefined;
41
+ }
42
+ }
43
+ }
44
+ exports.AptosPriceListener = AptosPriceListener;
45
+ class AptosPricePusher {
46
+ priceServiceConnection;
47
+ pythContractAddress;
48
+ endpoint;
49
+ mnemonic;
50
+ overrideGasPriceMultiplier;
51
+ lastPushAttempt;
52
+ accountHDPath = "m/44'/637'/0'/0'/0'";
53
+ constructor(priceServiceConnection, pythContractAddress, endpoint, mnemonic, overrideGasPriceMultiplier) {
54
+ this.priceServiceConnection = priceServiceConnection;
55
+ this.pythContractAddress = pythContractAddress;
56
+ this.endpoint = endpoint;
57
+ this.mnemonic = mnemonic;
58
+ this.overrideGasPriceMultiplier = overrideGasPriceMultiplier;
59
+ }
60
+ /**
61
+ * Gets price update data which then can be submitted to the Pyth contract to update the prices.
62
+ * This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price ids)
63
+ *
64
+ * @param priceIds Array of hex-encoded price ids.
65
+ * @returns Array of price update data.
66
+ */
67
+ async getPriceFeedsUpdateData(priceIds) {
68
+ // Fetch the latest price feed update VAAs from the price service
69
+ const latestVaas = await this.priceServiceConnection.getLatestVaas(priceIds);
70
+ return latestVaas.map((vaa) => Array.from(Buffer.from(vaa, "base64")));
71
+ }
72
+ async updatePriceFeed(priceIds, pubTimesToPush) {
73
+ if (priceIds.length === 0) {
74
+ return;
75
+ }
76
+ if (priceIds.length !== pubTimesToPush.length)
77
+ throw new Error("Invalid arguments");
78
+ let priceFeedUpdateData;
79
+ try {
80
+ // get the latest VAAs for updatePriceFeed and then push them
81
+ priceFeedUpdateData = await this.getPriceFeedsUpdateData(priceIds);
82
+ }
83
+ catch (e) {
84
+ console.error("Error fetching the latest vaas to push");
85
+ console.error(e);
86
+ return;
87
+ }
88
+ try {
89
+ const account = aptos_1.AptosAccount.fromDerivePath(this.accountHDPath, this.mnemonic);
90
+ const client = new aptos_1.AptosClient(this.endpoint);
91
+ const rawTx = await client.generateTransaction(account.address(), {
92
+ function: `${this.pythContractAddress}::pyth::update_price_feeds_if_fresh_with_funder`,
93
+ type_arguments: [],
94
+ arguments: [
95
+ priceFeedUpdateData,
96
+ priceIds.map((priceId) => Buffer.from(priceId, "hex")),
97
+ pubTimesToPush,
98
+ ],
99
+ });
100
+ const simulation = await client.simulateTransaction(account, rawTx, {
101
+ estimateGasUnitPrice: true,
102
+ estimateMaxGasAmount: true,
103
+ estimatePrioritizedGasUnitPrice: true,
104
+ });
105
+ // Transactions on Aptos can be prioritized by paying a higher gas unit price.
106
+ // We are storing the gas unit price paid for the last transaction.
107
+ // If that transaction is not added to the block, we are increasing the gas unit price
108
+ // by multiplying the old gas unit price with `this.overrideGasPriceMultiplier`.
109
+ // After which we are sending a transaction with the same sequence number as the last
110
+ // transaction. Since they have the same sequence number only one of them will be added to
111
+ // the block and we won't be paying fees twice.
112
+ let gasUnitPrice = Number(simulation[0].gas_unit_price);
113
+ if (this.lastPushAttempt !== undefined &&
114
+ Number(simulation[0].sequence_number) === this.lastPushAttempt.nonce) {
115
+ const newGasUnitPrice = Number(this.lastPushAttempt.gasPrice * this.overrideGasPriceMultiplier);
116
+ if (gasUnitPrice < newGasUnitPrice)
117
+ gasUnitPrice = newGasUnitPrice;
118
+ }
119
+ const gasUsed = Number(simulation[0].gas_used) * 1.5;
120
+ const maxGasAmount = Number(gasUnitPrice * gasUsed);
121
+ const rawTxWithFee = new aptos_1.TxnBuilderTypes.RawTransaction(rawTx.sender, rawTx.sequence_number, rawTx.payload, BigInt(maxGasAmount.toFixed()), BigInt(gasUnitPrice.toFixed()), rawTx.expiration_timestamp_secs, rawTx.chain_id);
122
+ const signedTx = await client.signTransaction(account, rawTxWithFee);
123
+ const pendingTx = await client.submitTransaction(signedTx);
124
+ console.log("Succesfully broadcasted txHash:", pendingTx.hash);
125
+ // Update lastAttempt
126
+ this.lastPushAttempt = {
127
+ nonce: Number(pendingTx.sequence_number),
128
+ gasPrice: gasUnitPrice,
129
+ };
130
+ return;
131
+ }
132
+ catch (e) {
133
+ console.error("Error executing messages");
134
+ console.log(e);
135
+ return;
136
+ }
137
+ }
138
+ }
139
+ exports.AptosPricePusher = AptosPricePusher;
@@ -0,0 +1,17 @@
1
+ import { Options } from "yargs";
2
+ declare const _default: {
3
+ command: string;
4
+ describe: string;
5
+ builder: {
6
+ "pushing-frequency": Options;
7
+ "polling-frequency": Options;
8
+ "pyth-contract-address": Options;
9
+ "mnemonic-file": Options;
10
+ "price-service-endpoint": Options;
11
+ "price-config-file": Options;
12
+ endpoint: Options;
13
+ "override-gas-price-multiplier": Options;
14
+ };
15
+ handler: (argv: any) => void;
16
+ };
17
+ export default _default;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ const price_service_client_1 = require("@pythnetwork/price-service-client");
30
+ const options = __importStar(require("../options"));
31
+ const price_config_1 = require("../price-config");
32
+ const fs_1 = __importDefault(require("fs"));
33
+ const pyth_price_listener_1 = require("../pyth-price-listener");
34
+ const controller_1 = require("../controller");
35
+ const aptos_1 = require("./aptos");
36
+ exports.default = {
37
+ command: "aptos",
38
+ describe: "run price pusher for aptos",
39
+ builder: {
40
+ endpoint: {
41
+ description: "RPC endpoint endpoint URL for aptos. The pusher will periodically" +
42
+ "poll for updates. The polling interval is configurable via the " +
43
+ "`polling-frequency` command-line argument.",
44
+ type: "string",
45
+ required: true,
46
+ },
47
+ "override-gas-price-multiplier": {
48
+ description: "Multiply the gas price by this number if the transaction is not landing to override it. Default 2",
49
+ type: "number",
50
+ required: false,
51
+ default: 2,
52
+ },
53
+ ...options.priceConfigFile,
54
+ ...options.priceServiceEndpoint,
55
+ ...options.mnemonicFile,
56
+ ...options.pythContractAddress,
57
+ ...options.pollingFrequency,
58
+ ...options.pushingFrequency,
59
+ },
60
+ handler: function (argv) {
61
+ // FIXME: type checks for this
62
+ const { endpoint, priceConfigFile, priceServiceEndpoint, mnemonicFile, pythContractAddress, pushingFrequency, pollingFrequency, overrideGasPriceMultiplier, } = argv;
63
+ const priceConfigs = (0, price_config_1.readPriceConfigFile)(priceConfigFile);
64
+ const priceServiceConnection = new price_service_client_1.PriceServiceConnection(priceServiceEndpoint, {
65
+ logger: {
66
+ // Log only warnings and errors from the price service client
67
+ info: () => undefined,
68
+ warn: console.warn,
69
+ error: console.error,
70
+ debug: () => undefined,
71
+ trace: () => undefined,
72
+ },
73
+ });
74
+ const mnemonic = fs_1.default.readFileSync(mnemonicFile, "utf-8").trim();
75
+ const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
76
+ const pythListener = new pyth_price_listener_1.PythPriceListener(priceServiceConnection, priceItems);
77
+ const aptosListener = new aptos_1.AptosPriceListener(pythContractAddress, endpoint, priceItems, { pollingFrequency });
78
+ const aptosPusher = new aptos_1.AptosPricePusher(priceServiceConnection, pythContractAddress, endpoint, mnemonic, overrideGasPriceMultiplier);
79
+ const controller = new controller_1.Controller(priceConfigs, pythListener, aptosListener, aptosPusher, { pushingFrequency });
80
+ controller.start();
81
+ },
82
+ };
@@ -0,0 +1,4 @@
1
+ export type PushAttempt = {
2
+ nonce: number;
3
+ gasPrice: number;
4
+ };
package/lib/common.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/lib/evm/evm.js CHANGED
@@ -160,6 +160,14 @@ class EvmPricePusher {
160
160
  console.log("Multiple users are using the same accounts and nonce is incorrect. Skipping this push.");
161
161
  return;
162
162
  }
163
+ if (err.message.includes("max fee per gas less than block base fee")) {
164
+ // We just have to handle this error and return.
165
+ // LastPushAttempt was stored with the class
166
+ // Next time the update will be executing, it will check the last attempt
167
+ // and increase the gas price accordingly.
168
+ console.log("The transaction failed with error: max fee per gas less than block base fee ");
169
+ return;
170
+ }
163
171
  if (err.message.includes("sender doesn't have enough funds to send tx.")) {
164
172
  console.error("Payer is out of balance, please top it up.");
165
173
  throw err;
package/lib/index.js CHANGED
@@ -8,9 +8,13 @@ const yargs_1 = __importDefault(require("yargs"));
8
8
  const helpers_1 = require("yargs/helpers");
9
9
  const command_1 = __importDefault(require("./injective/command"));
10
10
  const command_2 = __importDefault(require("./evm/command"));
11
+ const command_3 = __importDefault(require("./aptos/command"));
12
+ const command_4 = __importDefault(require("./sui/command"));
11
13
  (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
12
14
  .config("config")
13
15
  .global("config")
14
16
  .command(command_2.default)
15
17
  .command(command_1.default)
18
+ .command(command_3.default)
19
+ .command(command_4.default)
16
20
  .help().argv;
@@ -10,6 +10,7 @@ declare const _default: {
10
10
  "price-service-endpoint": Options;
11
11
  "price-config-file": Options;
12
12
  "grpc-endpoint": Options;
13
+ network: Options;
13
14
  };
14
15
  handler: (argv: any) => void;
15
16
  };
@@ -33,6 +33,7 @@ const fs_1 = __importDefault(require("fs"));
33
33
  const injective_1 = require("./injective");
34
34
  const pyth_price_listener_1 = require("../pyth-price-listener");
35
35
  const controller_1 = require("../controller");
36
+ const networks_1 = require("@injectivelabs/networks");
36
37
  exports.default = {
37
38
  command: "injective",
38
39
  describe: "run price pusher for injective",
@@ -44,6 +45,11 @@ exports.default = {
44
45
  type: "string",
45
46
  required: true,
46
47
  },
48
+ network: {
49
+ description: "testnet or mainnet",
50
+ type: "string",
51
+ required: true,
52
+ },
47
53
  ...options.priceConfigFile,
48
54
  ...options.priceServiceEndpoint,
49
55
  ...options.mnemonicFile,
@@ -53,7 +59,10 @@ exports.default = {
53
59
  },
54
60
  handler: function (argv) {
55
61
  // FIXME: type checks for this
56
- const { grpcEndpoint, priceConfigFile, priceServiceEndpoint, mnemonicFile, pythContractAddress, pushingFrequency, pollingFrequency, } = argv;
62
+ const { grpcEndpoint, priceConfigFile, priceServiceEndpoint, mnemonicFile, pythContractAddress, pushingFrequency, pollingFrequency, network, } = argv;
63
+ if (network !== "testnet" && network !== "mainnet") {
64
+ throw new Error("Please specify network. One of [testnet, mainnet]");
65
+ }
57
66
  const priceConfigs = (0, price_config_1.readPriceConfigFile)(priceConfigFile);
58
67
  const priceServiceConnection = new price_service_client_1.PriceServiceConnection(priceServiceEndpoint, {
59
68
  logger: {
@@ -71,7 +80,9 @@ exports.default = {
71
80
  const injectiveListener = new injective_1.InjectivePriceListener(pythContractAddress, grpcEndpoint, priceItems, {
72
81
  pollingFrequency,
73
82
  });
74
- const injectivePusher = new injective_1.InjectivePricePusher(priceServiceConnection, pythContractAddress, grpcEndpoint, mnemonic);
83
+ const injectivePusher = new injective_1.InjectivePricePusher(priceServiceConnection, pythContractAddress, grpcEndpoint, mnemonic, {
84
+ chainId: (0, networks_1.getNetworkInfo)(network).chainId,
85
+ });
75
86
  const controller = new controller_1.Controller(priceConfigs, pythListener, injectiveListener, injectivePusher, { pushingFrequency });
76
87
  controller.start();
77
88
  },
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InjectivePricePusher = exports.InjectivePriceListener = void 0;
4
4
  const interface_1 = require("../interface");
5
5
  const sdk_ts_1 = require("@injectivelabs/sdk-ts");
6
- const utils_1 = require("@injectivelabs/utils");
6
+ const DEFAULT_GAS_PRICE = 500000000;
7
7
  // this use price without leading 0x
8
8
  class InjectivePriceListener extends interface_1.ChainPriceListener {
9
9
  pythContractAddress;
@@ -18,7 +18,7 @@ class InjectivePriceListener extends interface_1.ChainPriceListener {
18
18
  try {
19
19
  const api = new sdk_ts_1.ChainGrpcWasmApi(this.grpcEndpoint);
20
20
  const { data } = await api.fetchSmartContractState(this.pythContractAddress, Buffer.from(`{"price_feed":{"id":"${priceId}"}}`).toString("base64"));
21
- const json = Buffer.from(data, "base64").toString();
21
+ const json = Buffer.from(data).toString();
22
22
  priceQueryResponse = JSON.parse(json);
23
23
  }
24
24
  catch (e) {
@@ -49,7 +49,7 @@ class InjectivePricePusher {
49
49
  this.chainConfig = {
50
50
  chainId: chainConfig?.chainId ?? "injective-888",
51
51
  gasMultiplier: chainConfig?.gasMultiplier ?? 1.2,
52
- gasPrice: chainConfig?.gasPrice ?? utils_1.DEFAULT_GAS_PRICE,
52
+ gasPrice: chainConfig?.gasPrice ?? DEFAULT_GAS_PRICE,
53
53
  };
54
54
  }
55
55
  injectiveAddress() {
@@ -93,7 +93,7 @@ class InjectivePricePusher {
93
93
  });
94
94
  const sig = await this.wallet.sign(Buffer.from(signBytes));
95
95
  /** Append Signatures */
96
- txRaw.setSignaturesList([sig]);
96
+ txRaw.signatures = [sig];
97
97
  const txResponse = await txService.broadcast(txRaw);
98
98
  return txResponse;
99
99
  }
@@ -129,7 +129,7 @@ class InjectivePricePusher {
129
129
  vaas: priceFeedUpdateObject.update_price_feeds.data,
130
130
  },
131
131
  })).toString("base64"));
132
- const json = Buffer.from(data, "base64").toString();
132
+ const json = Buffer.from(data).toString();
133
133
  updateFeeQueryResponse = JSON.parse(json);
134
134
  }
135
135
  catch (e) {
@@ -0,0 +1,20 @@
1
+ import { Options } from "yargs";
2
+ declare const _default: {
3
+ command: string;
4
+ describe: string;
5
+ builder: {
6
+ "pushing-frequency": Options;
7
+ "polling-frequency": Options;
8
+ "mnemonic-file": Options;
9
+ "price-service-endpoint": Options;
10
+ "price-config-file": Options;
11
+ endpoint: Options;
12
+ "pyth-package-id": Options;
13
+ "pyth-state-id": Options;
14
+ "wormhole-package-id": Options;
15
+ "wormhole-state-id": Options;
16
+ "price-feed-to-price-info-object-table-id": Options;
17
+ };
18
+ handler: (argv: any) => void;
19
+ };
20
+ export default _default;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ const price_service_client_1 = require("@pythnetwork/price-service-client");
30
+ const options = __importStar(require("../options"));
31
+ const price_config_1 = require("../price-config");
32
+ const fs_1 = __importDefault(require("fs"));
33
+ const pyth_price_listener_1 = require("../pyth-price-listener");
34
+ const controller_1 = require("../controller");
35
+ const sui_1 = require("./sui");
36
+ exports.default = {
37
+ command: "sui",
38
+ describe: "Run price pusher for sui. Most of the arguments below are" +
39
+ "network specific, so there's one set of values for mainnet and" +
40
+ "another for testnet. See config.sui..sample.json for the " +
41
+ "appropriate values for your network. ",
42
+ builder: {
43
+ endpoint: {
44
+ description: "RPC endpoint URL for sui. The pusher will periodically" +
45
+ "poll for updates. The polling interval is configurable via the " +
46
+ "`polling-frequency` command-line argument.",
47
+ type: "string",
48
+ required: true,
49
+ },
50
+ "pyth-package-id": {
51
+ description: "Pyth Package Id. Can be found here" +
52
+ "https://docs.pyth.network/pythnet-price-feeds/sui",
53
+ type: "string",
54
+ required: true,
55
+ },
56
+ "pyth-state-id": {
57
+ description: "Pyth State Id. Can be found here" +
58
+ "https://docs.pyth.network/pythnet-price-feeds/sui",
59
+ type: "string",
60
+ required: true,
61
+ },
62
+ "wormhole-package-id": {
63
+ description: "Wormhole Package Id. Can be found here" +
64
+ "https://docs.pyth.network/pythnet-price-feeds/sui",
65
+ type: "string",
66
+ required: true,
67
+ },
68
+ "wormhole-state-id": {
69
+ description: "Wormhole State Id. Can be found here" +
70
+ "https://docs.pyth.network/pythnet-price-feeds/sui",
71
+ type: "string",
72
+ required: true,
73
+ },
74
+ "price-feed-to-price-info-object-table-id": {
75
+ description: "This is the id of the table which stored the information related to price data. You can find it here: " +
76
+ "https://docs.pyth.network/pythnet-price-feeds/sui",
77
+ type: "string",
78
+ required: true,
79
+ },
80
+ ...options.priceConfigFile,
81
+ ...options.priceServiceEndpoint,
82
+ ...options.mnemonicFile,
83
+ ...options.pollingFrequency,
84
+ ...options.pushingFrequency,
85
+ },
86
+ handler: function (argv) {
87
+ const { endpoint, priceConfigFile, priceServiceEndpoint, mnemonicFile, pushingFrequency, pollingFrequency, pythPackageId, pythStateId, wormholePackageId, wormholeStateId, priceFeedToPriceInfoObjectTableId, } = argv;
88
+ const priceConfigs = (0, price_config_1.readPriceConfigFile)(priceConfigFile);
89
+ const priceServiceConnection = new price_service_client_1.PriceServiceConnection(priceServiceEndpoint, {
90
+ logger: {
91
+ // Log only warnings and errors from the price service client
92
+ info: () => undefined,
93
+ warn: console.warn,
94
+ error: console.error,
95
+ debug: () => undefined,
96
+ trace: () => undefined,
97
+ },
98
+ });
99
+ const mnemonic = fs_1.default.readFileSync(mnemonicFile, "utf-8").trim();
100
+ const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
101
+ const pythListener = new pyth_price_listener_1.PythPriceListener(priceServiceConnection, priceItems);
102
+ const suiListener = new sui_1.SuiPriceListener(pythPackageId, priceFeedToPriceInfoObjectTableId, endpoint, priceItems, { pollingFrequency });
103
+ const suiPusher = new sui_1.SuiPricePusher(priceServiceConnection, pythPackageId, pythStateId, wormholePackageId, wormholeStateId, priceFeedToPriceInfoObjectTableId, endpoint, mnemonic);
104
+ const controller = new controller_1.Controller(priceConfigs, pythListener, suiListener, suiPusher, { pushingFrequency });
105
+ controller.start();
106
+ },
107
+ };
@@ -0,0 +1,23 @@
1
+ import { ChainPriceListener, IPricePusher, PriceInfo, PriceItem } from "../interface";
2
+ import { DurationInSeconds } from "../utils";
3
+ import { PriceServiceConnection } from "@pythnetwork/price-service-client";
4
+ export declare class SuiPriceListener extends ChainPriceListener {
5
+ private pythPackageId;
6
+ private priceFeedToPriceInfoObjectTableId;
7
+ private endpoint;
8
+ constructor(pythPackageId: string, priceFeedToPriceInfoObjectTableId: string, endpoint: string, priceItems: PriceItem[], config: {
9
+ pollingFrequency: DurationInSeconds;
10
+ });
11
+ getOnChainPriceInfo(priceId: string): Promise<PriceInfo | undefined>;
12
+ }
13
+ export declare class SuiPricePusher implements IPricePusher {
14
+ private priceServiceConnection;
15
+ private pythPackageId;
16
+ private pythStateId;
17
+ private wormholePackageId;
18
+ private wormholeStateId;
19
+ private priceFeedToPriceInfoObjectTableId;
20
+ private readonly signer;
21
+ constructor(priceServiceConnection: PriceServiceConnection, pythPackageId: string, pythStateId: string, wormholePackageId: string, wormholeStateId: string, priceFeedToPriceInfoObjectTableId: string, endpoint: string, mnemonic: string);
22
+ updatePriceFeed(priceIds: string[], pubTimesToPush: number[]): Promise<void>;
23
+ }
package/lib/sui/sui.js ADDED
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SuiPricePusher = exports.SuiPriceListener = void 0;
4
+ const interface_1 = require("../interface");
5
+ const sui_js_1 = require("@mysten/sui.js");
6
+ class SuiPriceListener extends interface_1.ChainPriceListener {
7
+ pythPackageId;
8
+ priceFeedToPriceInfoObjectTableId;
9
+ endpoint;
10
+ constructor(pythPackageId, priceFeedToPriceInfoObjectTableId, endpoint, priceItems, config) {
11
+ super("sui", config.pollingFrequency, priceItems);
12
+ this.pythPackageId = pythPackageId;
13
+ this.priceFeedToPriceInfoObjectTableId = priceFeedToPriceInfoObjectTableId;
14
+ this.endpoint = endpoint;
15
+ }
16
+ async getOnChainPriceInfo(priceId) {
17
+ try {
18
+ const provider = new sui_js_1.JsonRpcProvider(new sui_js_1.Connection({ fullnode: this.endpoint }));
19
+ const priceInfoObjectId = await priceIdToPriceInfoObjectId(provider, this.pythPackageId, this.priceFeedToPriceInfoObjectTableId, priceId);
20
+ // Fetching the price info object for the above priceInfoObjectId
21
+ const priceInfoObject = await provider.getObject({
22
+ id: priceInfoObjectId,
23
+ options: { showContent: true },
24
+ });
25
+ if (priceInfoObject.data === undefined ||
26
+ priceInfoObject.data.content === undefined)
27
+ throw new Error("Price not found on chain for price id " + priceId);
28
+ if (priceInfoObject.data.content.dataType !== "moveObject")
29
+ throw new Error("fetched object datatype should be moveObject");
30
+ const { magnitude, negative } = priceInfoObject.data.content.fields.price_info.fields.price_feed.fields
31
+ .price.fields.price.fields;
32
+ const conf = priceInfoObject.data.content.fields.price_info.fields.price_feed.fields
33
+ .price.fields.conf;
34
+ const timestamp = priceInfoObject.data.content.fields.price_info.fields.price_feed.fields
35
+ .price.fields.timestamp;
36
+ return {
37
+ price: negative ? "-" + magnitude : magnitude,
38
+ conf,
39
+ publishTime: Number(timestamp),
40
+ };
41
+ }
42
+ catch (e) {
43
+ console.error(`Polling Sui on-chain price for ${priceId} failed. Error:`);
44
+ console.error(e);
45
+ return undefined;
46
+ }
47
+ }
48
+ }
49
+ exports.SuiPriceListener = SuiPriceListener;
50
+ class SuiPricePusher {
51
+ priceServiceConnection;
52
+ pythPackageId;
53
+ pythStateId;
54
+ wormholePackageId;
55
+ wormholeStateId;
56
+ priceFeedToPriceInfoObjectTableId;
57
+ signer;
58
+ constructor(priceServiceConnection, pythPackageId, pythStateId, wormholePackageId, wormholeStateId, priceFeedToPriceInfoObjectTableId, endpoint, mnemonic) {
59
+ this.priceServiceConnection = priceServiceConnection;
60
+ this.pythPackageId = pythPackageId;
61
+ this.pythStateId = pythStateId;
62
+ this.wormholePackageId = wormholePackageId;
63
+ this.wormholeStateId = wormholeStateId;
64
+ this.priceFeedToPriceInfoObjectTableId = priceFeedToPriceInfoObjectTableId;
65
+ this.signer = new sui_js_1.RawSigner(sui_js_1.Ed25519Keypair.deriveKeypair(mnemonic), new sui_js_1.JsonRpcProvider(new sui_js_1.Connection({ fullnode: endpoint })));
66
+ }
67
+ async updatePriceFeed(priceIds, pubTimesToPush) {
68
+ if (priceIds.length === 0) {
69
+ return;
70
+ }
71
+ if (priceIds.length !== pubTimesToPush.length)
72
+ throw new Error("Invalid arguments");
73
+ const tx = new sui_js_1.TransactionBlock();
74
+ const vaas = await this.priceServiceConnection.getLatestVaas(priceIds);
75
+ // Parse our batch price attestation VAA bytes using Wormhole.
76
+ // Check out the Wormhole cross-chain bridge and generic messaging protocol here:
77
+ // https://github.com/wormhole-foundation/wormhole
78
+ let verified_vaas = [];
79
+ for (const vaa of vaas) {
80
+ const [verified_vaa] = tx.moveCall({
81
+ target: `${this.wormholePackageId}::vaa::parse_and_verify`,
82
+ arguments: [
83
+ tx.object(this.wormholeStateId),
84
+ tx.pure([...Buffer.from(vaa, "base64")]),
85
+ tx.object(sui_js_1.SUI_CLOCK_OBJECT_ID),
86
+ ],
87
+ });
88
+ verified_vaas = verified_vaas.concat(verified_vaa);
89
+ }
90
+ // Create a hot potato vector of price feed updates that will
91
+ // be used to update price feeds.
92
+ let [price_updates_hot_potato] = tx.moveCall({
93
+ target: `${this.pythPackageId}::pyth::create_price_infos_hot_potato`,
94
+ arguments: [
95
+ tx.object(this.pythStateId),
96
+ tx.makeMoveVec({
97
+ type: `${this.wormholePackageId}::vaa::VAA`,
98
+ objects: verified_vaas,
99
+ }),
100
+ tx.object(sui_js_1.SUI_CLOCK_OBJECT_ID),
101
+ ],
102
+ });
103
+ // Update each price info object (containing our price feeds of interest)
104
+ // using the hot potato vector.
105
+ for (const priceId of priceIds) {
106
+ let priceInfoObjectId;
107
+ try {
108
+ priceInfoObjectId = await priceIdToPriceInfoObjectId(this.signer.provider, this.pythPackageId, this.priceFeedToPriceInfoObjectTableId, priceId);
109
+ }
110
+ catch (e) {
111
+ console.log("Error fetching price info object id for ", priceId);
112
+ console.error(e);
113
+ return;
114
+ }
115
+ const coin = tx.splitCoins(tx.gas, [tx.pure(1)]);
116
+ [price_updates_hot_potato] = tx.moveCall({
117
+ target: `${this.pythPackageId}::pyth::update_single_price_feed`,
118
+ arguments: [
119
+ tx.object(this.pythStateId),
120
+ price_updates_hot_potato,
121
+ tx.object(priceInfoObjectId),
122
+ coin,
123
+ tx.object(sui_js_1.SUI_CLOCK_OBJECT_ID),
124
+ ],
125
+ });
126
+ }
127
+ // Explicitly destroy the hot potato vector, since it can't be dropped
128
+ // automatically.
129
+ tx.moveCall({
130
+ target: `${this.pythPackageId}::hot_potato_vector::destroy`,
131
+ arguments: [price_updates_hot_potato],
132
+ typeArguments: [`${this.pythPackageId}::price_info::PriceInfo`],
133
+ });
134
+ try {
135
+ const result = await this.signer.signAndExecuteTransactionBlock({
136
+ transactionBlock: tx,
137
+ options: {
138
+ showInput: true,
139
+ showEffects: true,
140
+ showEvents: true,
141
+ showObjectChanges: true,
142
+ showBalanceChanges: true,
143
+ },
144
+ });
145
+ console.log("Successfully updated price with transaction digest ", result.digest);
146
+ }
147
+ catch (e) {
148
+ console.log("Error when signAndExecuteTransactionBlock");
149
+ if (String(e).includes("GasBalanceTooLow")) {
150
+ console.log("Insufficient Gas Amount. Please top up your account");
151
+ process.exit();
152
+ }
153
+ console.error(e);
154
+ return;
155
+ }
156
+ }
157
+ }
158
+ exports.SuiPricePusher = SuiPricePusher;
159
+ // We are calculating stored price info object id for given price id
160
+ // The mapping between which is static. Hence, we are caching it here.
161
+ const CACHE = {};
162
+ // For given priceid, this method will fetch the price info object id
163
+ // where the price information for the corresponding price feed is stored
164
+ async function priceIdToPriceInfoObjectId(provider, pythPackageId, priceFeedToPriceInfoObjectTableId, priceId) {
165
+ // Check if this was fetched before.
166
+ if (CACHE[priceId] !== undefined)
167
+ return CACHE[priceId];
168
+ const storedObjectID = await provider.getDynamicFieldObject({
169
+ parentId: priceFeedToPriceInfoObjectTableId,
170
+ name: {
171
+ type: `${pythPackageId}::price_identifier::PriceIdentifier`,
172
+ value: {
173
+ bytes: "0x" + priceId,
174
+ },
175
+ },
176
+ });
177
+ if (storedObjectID.error !== undefined)
178
+ throw storedObjectID.error;
179
+ if (storedObjectID.data === undefined ||
180
+ storedObjectID.data.content === undefined)
181
+ throw new Error("Price not found on chain for price id " + priceId);
182
+ if (storedObjectID.data.content.dataType !== "moveObject")
183
+ throw new Error("fetched object datatype should be moveObject");
184
+ // This ID points to the price info object for the given price id stored on chain
185
+ const priceInfoObjectId = storedObjectID.data.content.fields.value;
186
+ // cache the price info object id
187
+ CACHE[priceId] = priceInfoObjectId;
188
+ return priceInfoObjectId;
189
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pythnetwork/price-pusher",
3
- "version": "4.1.2",
3
+ "version": "5.3.0",
4
4
  "description": "Pyth Price Pusher",
5
5
  "homepage": "https://pyth.network",
6
6
  "main": "lib/index.js",
@@ -51,15 +51,17 @@
51
51
  "typescript": "^4.6.3"
52
52
  },
53
53
  "dependencies": {
54
- "@injectivelabs/sdk-ts": "^1.0.484",
54
+ "@injectivelabs/sdk-ts": "1.10.72",
55
+ "@mysten/sui.js": "^0.34.0",
55
56
  "@pythnetwork/price-service-client": "*",
56
57
  "@pythnetwork/pyth-sdk-solidity": "*",
57
58
  "@truffle/hdwallet-provider": "^2.1.3",
59
+ "aptos": "^1.8.5",
58
60
  "joi": "^17.6.0",
59
61
  "web3": "^1.8.1",
60
62
  "web3-eth-contract": "^1.8.1",
61
63
  "yaml": "^2.1.1",
62
64
  "yargs": "^17.5.1"
63
65
  },
64
- "gitHead": "61a66eecaa8c6480509e32df56d826ee484eefae"
66
+ "gitHead": "6cdcf4dffd54017e19168be450df01acd68f07ab"
65
67
  }