@pythnetwork/price-pusher 4.1.1

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/lib/evm/evm.js ADDED
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PythContractFactory = exports.EvmPricePusher = exports.EvmPriceListener = void 0;
7
+ const interface_1 = require("../interface");
8
+ const utils_1 = require("../utils");
9
+ const AbstractPyth_json_1 = __importDefault(require("@pythnetwork/pyth-sdk-solidity/abis/AbstractPyth.json"));
10
+ const hdwallet_provider_1 = __importDefault(require("@truffle/hdwallet-provider"));
11
+ const web3_1 = __importDefault(require("web3"));
12
+ const utils_2 = require("../utils");
13
+ class EvmPriceListener extends interface_1.ChainPriceListener {
14
+ pythContractFactory;
15
+ pythContract;
16
+ constructor(pythContractFactory, priceItems, config) {
17
+ super("Evm", config.pollingFrequency, priceItems);
18
+ this.pythContractFactory = pythContractFactory;
19
+ this.pythContract = this.pythContractFactory.createPythContract();
20
+ }
21
+ // This method should be awaited on and once it finishes it has the latest value
22
+ // for the given price feeds (if they exist).
23
+ async start() {
24
+ if (this.pythContractFactory.hasWebsocketProvider()) {
25
+ console.log("Subscribing to the target network pyth contract events...");
26
+ this.startSubscription();
27
+ }
28
+ else {
29
+ console.log("The target network RPC endpoint is not Websocket. " +
30
+ "Listening for updates only via polling....");
31
+ }
32
+ // base class for polling
33
+ await super.start();
34
+ }
35
+ async startSubscription() {
36
+ for (const { id: priceId } of this.priceItems) {
37
+ this.pythContract.events.PriceFeedUpdate({
38
+ filter: {
39
+ id: (0, utils_1.addLeading0x)(priceId),
40
+ fresh: true,
41
+ },
42
+ }, this.onPriceFeedUpdate.bind(this));
43
+ }
44
+ }
45
+ onPriceFeedUpdate(err, event) {
46
+ if (err !== null) {
47
+ console.error("PriceFeedUpdate EventEmitter received an error..");
48
+ throw err;
49
+ }
50
+ const priceId = (0, utils_1.removeLeading0x)(event.returnValues.id);
51
+ console.log(`Received a new Evm PriceFeedUpdate event for price feed ${this.priceIdToAlias.get(priceId)} (${priceId}).`);
52
+ const priceInfo = {
53
+ conf: event.returnValues.conf,
54
+ price: event.returnValues.price,
55
+ publishTime: Number(event.returnValues.publishTime),
56
+ };
57
+ this.updateLatestPriceInfo(priceId, priceInfo);
58
+ }
59
+ async getOnChainPriceInfo(priceId) {
60
+ let priceRaw;
61
+ try {
62
+ priceRaw = await this.pythContract.methods
63
+ .getPriceUnsafe((0, utils_1.addLeading0x)(priceId))
64
+ .call();
65
+ }
66
+ catch (e) {
67
+ console.error(`Polling on-chain price for ${priceId} failed. Error:`);
68
+ console.error(e);
69
+ return undefined;
70
+ }
71
+ console.log(`Polled an EVM on chain price for feed ${this.priceIdToAlias.get(priceId)} (${priceId}).`);
72
+ return {
73
+ conf: priceRaw.conf,
74
+ price: priceRaw.price,
75
+ publishTime: Number(priceRaw.publishTime),
76
+ };
77
+ }
78
+ }
79
+ exports.EvmPriceListener = EvmPriceListener;
80
+ class EvmPricePusher {
81
+ connection;
82
+ overrideGasPriceMultiplier;
83
+ customGasStation;
84
+ pythContract;
85
+ web3;
86
+ pusherAddress;
87
+ lastPushAttempt;
88
+ constructor(connection, pythContractFactory, overrideGasPriceMultiplier, customGasStation) {
89
+ this.connection = connection;
90
+ this.overrideGasPriceMultiplier = overrideGasPriceMultiplier;
91
+ this.customGasStation = customGasStation;
92
+ this.pythContract = pythContractFactory.createPythContractWithPayer();
93
+ this.web3 = new web3_1.default(pythContractFactory.createWeb3PayerProvider());
94
+ }
95
+ // The pubTimes are passed here to use the values that triggered the push.
96
+ // This is an optimization to avoid getting a newer value (as an update comes)
97
+ // and will help multiple price pushers to have consistent behaviour.
98
+ // To ensure that we transactions are landing and we are not pushing the prices twice
99
+ // we will re-use the same nonce (with a higher gas price) if the previous transaction
100
+ // is not landed yet.
101
+ async updatePriceFeed(priceIds, pubTimesToPush) {
102
+ if (priceIds.length === 0) {
103
+ return;
104
+ }
105
+ if (priceIds.length !== pubTimesToPush.length)
106
+ throw new Error("Invalid arguments");
107
+ const priceIdsWith0x = priceIds.map((priceId) => (0, utils_1.addLeading0x)(priceId));
108
+ const priceFeedUpdateData = await this.getPriceFeedsUpdateData(priceIdsWith0x);
109
+ console.log("Pushing ", priceIdsWith0x);
110
+ let updateFee;
111
+ try {
112
+ updateFee = await this.pythContract.methods
113
+ .getUpdateFee(priceFeedUpdateData)
114
+ .call();
115
+ console.log(`Update fee: ${updateFee}`);
116
+ }
117
+ catch (e) {
118
+ console.error("An unidentified error has occured when getting the update fee:");
119
+ throw e;
120
+ }
121
+ let gasPrice = Number((await this.customGasStation?.getCustomGasPrice()) ||
122
+ (await this.web3.eth.getGasPrice()));
123
+ // Try to re-use the same nonce and increase the gas if the last tx is not landed yet.
124
+ if (this.pusherAddress === undefined) {
125
+ this.pusherAddress = (await this.web3.eth.getAccounts())[0];
126
+ }
127
+ const lastExecutedNonce = (await this.web3.eth.getTransactionCount(this.pusherAddress)) - 1;
128
+ let gasPriceToOverride = undefined;
129
+ if (this.lastPushAttempt !== undefined) {
130
+ if (this.lastPushAttempt.nonce <= lastExecutedNonce) {
131
+ this.lastPushAttempt = undefined;
132
+ }
133
+ else {
134
+ gasPriceToOverride = Math.ceil(this.lastPushAttempt.gasPrice * this.overrideGasPriceMultiplier);
135
+ }
136
+ }
137
+ if (gasPriceToOverride !== undefined && gasPriceToOverride > gasPrice) {
138
+ gasPrice = gasPriceToOverride;
139
+ }
140
+ const txNonce = lastExecutedNonce + 1;
141
+ console.log(`Using gas price: ${gasPrice} and nonce: ${txNonce}`);
142
+ this.pythContract.methods
143
+ .updatePriceFeedsIfNecessary(priceFeedUpdateData, priceIdsWith0x, pubTimesToPush)
144
+ .send({ value: updateFee, gasPrice, nonce: txNonce })
145
+ .on("transactionHash", (hash) => {
146
+ console.log(`Successful. Tx hash: ${hash}`);
147
+ })
148
+ .on("error", (err, receipt) => {
149
+ if (err.message.includes("revert")) {
150
+ // Since we are using custom error structs on solidity the rejection
151
+ // doesn't return any information why the call has reverted. Assuming that
152
+ // the update data is valid there is no possible rejection cause other than
153
+ // the target chain price being already updated.
154
+ console.log("Execution reverted. With high probablity, the target chain price " +
155
+ "has already updated, Skipping this push.");
156
+ return;
157
+ }
158
+ if (err.message.includes("the tx doesn't have the correct nonce.") ||
159
+ err.message.includes("nonce too low")) {
160
+ console.log("Multiple users are using the same accounts and nonce is incorrect. Skipping this push.");
161
+ return;
162
+ }
163
+ if (err.message.includes("sender doesn't have enough funds to send tx.")) {
164
+ console.error("Payer is out of balance, please top it up.");
165
+ throw err;
166
+ }
167
+ if (err.message.includes("transaction underpriced")) {
168
+ console.error("The gas price of the transaction is too low. Skipping this push. " +
169
+ "You might want to use a custom gas station or increase the override gas price " +
170
+ "multiplier to increase the likelihood of the transaction landing on-chain.");
171
+ return;
172
+ }
173
+ if (err.message.includes("could not replace existing tx")) {
174
+ console.log("A transaction with the same nonce has been mined and this one is no longer needed.");
175
+ return;
176
+ }
177
+ console.error("An unidentified error has occured:");
178
+ console.error(receipt);
179
+ throw err;
180
+ });
181
+ // Update lastAttempt
182
+ this.lastPushAttempt = {
183
+ nonce: txNonce,
184
+ gasPrice: gasPrice,
185
+ };
186
+ }
187
+ async getPriceFeedsUpdateData(priceIds) {
188
+ const latestVaas = await this.connection.getLatestVaas(priceIds);
189
+ return latestVaas.map((vaa) => "0x" + Buffer.from(vaa, "base64").toString("hex"));
190
+ }
191
+ }
192
+ exports.EvmPricePusher = EvmPricePusher;
193
+ class PythContractFactory {
194
+ endpoint;
195
+ mnemonic;
196
+ pythContractAddress;
197
+ constructor(endpoint, mnemonic, pythContractAddress) {
198
+ this.endpoint = endpoint;
199
+ this.mnemonic = mnemonic;
200
+ this.pythContractAddress = pythContractAddress;
201
+ }
202
+ /**
203
+ * This method creates a web3 Pyth contract with payer (based on HDWalletProvider). As this
204
+ * provider is an HDWalletProvider it does not support subscriptions even if the
205
+ * endpoint is a websocket endpoint.
206
+ *
207
+ * @returns Pyth contract
208
+ */
209
+ createPythContractWithPayer() {
210
+ const provider = this.createWeb3PayerProvider();
211
+ const web3 = new web3_1.default(provider);
212
+ return new web3.eth.Contract(AbstractPyth_json_1.default, this.pythContractAddress, {
213
+ from: provider.getAddress(0),
214
+ });
215
+ }
216
+ /**
217
+ * This method creates a web3 Pyth contract with the given endpoint as its provider. If
218
+ * the endpoint is a websocket endpoint the contract will support subscriptions.
219
+ *
220
+ * @returns Pyth contract
221
+ */
222
+ createPythContract() {
223
+ const provider = this.createWeb3Provider();
224
+ const web3 = new web3_1.default(provider);
225
+ return new web3.eth.Contract(AbstractPyth_json_1.default, this.pythContractAddress);
226
+ }
227
+ hasWebsocketProvider() {
228
+ return (0, utils_2.isWsEndpoint)(this.endpoint);
229
+ }
230
+ createWeb3Provider() {
231
+ if ((0, utils_2.isWsEndpoint)(this.endpoint)) {
232
+ web3_1.default.providers.WebsocketProvider.prototype.sendAsync =
233
+ web3_1.default.providers.WebsocketProvider.prototype.send;
234
+ return new web3_1.default.providers.WebsocketProvider(this.endpoint, {
235
+ clientConfig: {
236
+ keepalive: true,
237
+ keepaliveInterval: 30000,
238
+ },
239
+ reconnect: {
240
+ auto: true,
241
+ delay: 1000,
242
+ onTimeout: true,
243
+ },
244
+ timeout: 30000,
245
+ });
246
+ }
247
+ else {
248
+ web3_1.default.providers.HttpProvider.prototype.sendAsync =
249
+ web3_1.default.providers.HttpProvider.prototype.send;
250
+ return new web3_1.default.providers.HttpProvider(this.endpoint, {
251
+ keepAlive: true,
252
+ timeout: 30000,
253
+ });
254
+ }
255
+ }
256
+ createWeb3PayerProvider() {
257
+ return new hdwallet_provider_1.default({
258
+ mnemonic: {
259
+ phrase: this.mnemonic,
260
+ },
261
+ providerOrUrl: this.createWeb3Provider(),
262
+ });
263
+ }
264
+ }
265
+ exports.PythContractFactory = PythContractFactory;
package/lib/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/lib/index.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // #!/usr/bin/env node
7
+ const yargs_1 = __importDefault(require("yargs"));
8
+ const helpers_1 = require("yargs/helpers");
9
+ const command_1 = __importDefault(require("./injective/command"));
10
+ const command_2 = __importDefault(require("./evm/command"));
11
+ (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
12
+ .config("config")
13
+ .global("config")
14
+ .command(command_2.default)
15
+ .command(command_1.default)
16
+ .help().argv;
@@ -0,0 +1,16 @@
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
+ "grpc-endpoint": Options;
13
+ };
14
+ handler: (argv: any) => void;
15
+ };
16
+ export default _default;
@@ -0,0 +1,78 @@
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 injective_1 = require("./injective");
34
+ const pyth_price_listener_1 = require("../pyth-price-listener");
35
+ const controller_1 = require("../controller");
36
+ exports.default = {
37
+ command: "injective",
38
+ describe: "run price pusher for injective",
39
+ builder: {
40
+ "grpc-endpoint": {
41
+ description: "gRPC endpoint URL for injective. 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
+ ...options.priceConfigFile,
48
+ ...options.priceServiceEndpoint,
49
+ ...options.mnemonicFile,
50
+ ...options.pythContractAddress,
51
+ ...options.pollingFrequency,
52
+ ...options.pushingFrequency,
53
+ },
54
+ handler: function (argv) {
55
+ // FIXME: type checks for this
56
+ const { grpcEndpoint, priceConfigFile, priceServiceEndpoint, mnemonicFile, pythContractAddress, pushingFrequency, pollingFrequency, } = argv;
57
+ const priceConfigs = (0, price_config_1.readPriceConfigFile)(priceConfigFile);
58
+ const priceServiceConnection = new price_service_client_1.PriceServiceConnection(priceServiceEndpoint, {
59
+ logger: {
60
+ // Log only warnings and errors from the price service client
61
+ info: () => undefined,
62
+ warn: console.warn,
63
+ error: console.error,
64
+ debug: () => undefined,
65
+ trace: () => undefined,
66
+ },
67
+ });
68
+ const mnemonic = fs_1.default.readFileSync(mnemonicFile, "utf-8").trim();
69
+ const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
70
+ const pythListener = new pyth_price_listener_1.PythPriceListener(priceServiceConnection, priceItems);
71
+ const injectiveListener = new injective_1.InjectivePriceListener(pythContractAddress, grpcEndpoint, priceItems, {
72
+ pollingFrequency,
73
+ });
74
+ const injectivePusher = new injective_1.InjectivePricePusher(priceServiceConnection, pythContractAddress, grpcEndpoint, mnemonic);
75
+ const controller = new controller_1.Controller(priceConfigs, pythListener, injectiveListener, injectivePusher, { pushingFrequency });
76
+ controller.start();
77
+ },
78
+ };
@@ -0,0 +1,29 @@
1
+ import { HexString, PriceServiceConnection } from "@pythnetwork/price-service-client";
2
+ import { IPricePusher, PriceInfo, ChainPriceListener, PriceItem } from "../interface";
3
+ import { DurationInSeconds } from "../utils";
4
+ export declare class InjectivePriceListener extends ChainPriceListener {
5
+ private pythContractAddress;
6
+ private grpcEndpoint;
7
+ constructor(pythContractAddress: string, grpcEndpoint: string, priceItems: PriceItem[], config: {
8
+ pollingFrequency: DurationInSeconds;
9
+ });
10
+ getOnChainPriceInfo(priceId: HexString): Promise<PriceInfo | undefined>;
11
+ }
12
+ type InjectiveConfig = {
13
+ chainId: string;
14
+ gasMultiplier: number;
15
+ gasPrice: number;
16
+ };
17
+ export declare class InjectivePricePusher implements IPricePusher {
18
+ private priceServiceConnection;
19
+ private pythContractAddress;
20
+ private grpcEndpoint;
21
+ private wallet;
22
+ private chainConfig;
23
+ constructor(priceServiceConnection: PriceServiceConnection, pythContractAddress: string, grpcEndpoint: string, mnemonic: string, chainConfig?: Partial<InjectiveConfig>);
24
+ private injectiveAddress;
25
+ private signAndBroadcastMsg;
26
+ getPriceFeedUpdateObject(priceIds: string[]): Promise<any>;
27
+ updatePriceFeed(priceIds: string[], pubTimesToPush: number[]): Promise<void>;
28
+ }
29
+ export {};
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InjectivePricePusher = exports.InjectivePriceListener = void 0;
4
+ const interface_1 = require("../interface");
5
+ const sdk_ts_1 = require("@injectivelabs/sdk-ts");
6
+ const utils_1 = require("@injectivelabs/utils");
7
+ // this use price without leading 0x
8
+ class InjectivePriceListener extends interface_1.ChainPriceListener {
9
+ pythContractAddress;
10
+ grpcEndpoint;
11
+ constructor(pythContractAddress, grpcEndpoint, priceItems, config) {
12
+ super("Injective", config.pollingFrequency, priceItems);
13
+ this.pythContractAddress = pythContractAddress;
14
+ this.grpcEndpoint = grpcEndpoint;
15
+ }
16
+ async getOnChainPriceInfo(priceId) {
17
+ let priceQueryResponse;
18
+ try {
19
+ const api = new sdk_ts_1.ChainGrpcWasmApi(this.grpcEndpoint);
20
+ const { data } = await api.fetchSmartContractState(this.pythContractAddress, Buffer.from(`{"price_feed":{"id":"${priceId}"}}`).toString("base64"));
21
+ const json = Buffer.from(data, "base64").toString();
22
+ priceQueryResponse = JSON.parse(json);
23
+ }
24
+ catch (e) {
25
+ console.error(`Polling on-chain price for ${priceId} failed. Error:`);
26
+ console.error(e);
27
+ return undefined;
28
+ }
29
+ console.log(`Polled an Injective on chain price for feed ${this.priceIdToAlias.get(priceId)} (${priceId}).`);
30
+ return {
31
+ conf: priceQueryResponse.price_feed.price.conf,
32
+ price: priceQueryResponse.price_feed.price.price,
33
+ publishTime: priceQueryResponse.price_feed.price.publish_time,
34
+ };
35
+ }
36
+ }
37
+ exports.InjectivePriceListener = InjectivePriceListener;
38
+ class InjectivePricePusher {
39
+ priceServiceConnection;
40
+ pythContractAddress;
41
+ grpcEndpoint;
42
+ wallet;
43
+ chainConfig;
44
+ constructor(priceServiceConnection, pythContractAddress, grpcEndpoint, mnemonic, chainConfig) {
45
+ this.priceServiceConnection = priceServiceConnection;
46
+ this.pythContractAddress = pythContractAddress;
47
+ this.grpcEndpoint = grpcEndpoint;
48
+ this.wallet = sdk_ts_1.PrivateKey.fromMnemonic(mnemonic);
49
+ this.chainConfig = {
50
+ chainId: chainConfig?.chainId ?? "injective-888",
51
+ gasMultiplier: chainConfig?.gasMultiplier ?? 1.2,
52
+ gasPrice: chainConfig?.gasPrice ?? utils_1.DEFAULT_GAS_PRICE,
53
+ };
54
+ }
55
+ injectiveAddress() {
56
+ return this.wallet.toBech32();
57
+ }
58
+ async signAndBroadcastMsg(msg) {
59
+ const chainGrpcAuthApi = new sdk_ts_1.ChainGrpcAuthApi(this.grpcEndpoint);
60
+ const account = await chainGrpcAuthApi.fetchAccount(this.injectiveAddress());
61
+ const { txRaw: simulateTxRaw } = (0, sdk_ts_1.createTransactionFromMsg)({
62
+ sequence: account.baseAccount.sequence,
63
+ accountNumber: account.baseAccount.accountNumber,
64
+ message: msg,
65
+ chainId: this.chainConfig.chainId,
66
+ pubKey: this.wallet.toPublicKey().toBase64(),
67
+ });
68
+ const txService = new sdk_ts_1.TxGrpcClient(this.grpcEndpoint);
69
+ // simulation
70
+ const { gasInfo: { gasUsed }, } = await txService.simulate(simulateTxRaw);
71
+ // simulation returns us the approximate gas used
72
+ // gas passed with the transaction should be more than that
73
+ // in order for it to be successfully executed
74
+ // this multiplier takes care of that
75
+ const fee = {
76
+ amount: [
77
+ {
78
+ denom: "inj",
79
+ amount: (gasUsed *
80
+ this.chainConfig.gasPrice *
81
+ this.chainConfig.gasMultiplier).toFixed(),
82
+ },
83
+ ],
84
+ gas: (gasUsed * this.chainConfig.gasMultiplier).toFixed(),
85
+ };
86
+ const { signBytes, txRaw } = (0, sdk_ts_1.createTransactionFromMsg)({
87
+ sequence: account.baseAccount.sequence,
88
+ accountNumber: account.baseAccount.accountNumber,
89
+ message: msg,
90
+ chainId: this.chainConfig.chainId,
91
+ fee,
92
+ pubKey: this.wallet.toPublicKey().toBase64(),
93
+ });
94
+ const sig = await this.wallet.sign(Buffer.from(signBytes));
95
+ /** Append Signatures */
96
+ txRaw.setSignaturesList([sig]);
97
+ const txResponse = await txService.broadcast(txRaw);
98
+ return txResponse;
99
+ }
100
+ async getPriceFeedUpdateObject(priceIds) {
101
+ const vaas = await this.priceServiceConnection.getLatestVaas(priceIds);
102
+ return {
103
+ update_price_feeds: {
104
+ data: vaas,
105
+ },
106
+ };
107
+ }
108
+ async updatePriceFeed(priceIds, pubTimesToPush) {
109
+ if (priceIds.length === 0) {
110
+ return;
111
+ }
112
+ if (priceIds.length !== pubTimesToPush.length)
113
+ throw new Error("Invalid arguments");
114
+ let priceFeedUpdateObject;
115
+ try {
116
+ // get the latest VAAs for updatePriceFeed and then push them
117
+ priceFeedUpdateObject = await this.getPriceFeedUpdateObject(priceIds);
118
+ }
119
+ catch (e) {
120
+ console.error("Error fetching the latest vaas to push");
121
+ console.error(e);
122
+ return;
123
+ }
124
+ let updateFeeQueryResponse;
125
+ try {
126
+ const api = new sdk_ts_1.ChainGrpcWasmApi(this.grpcEndpoint);
127
+ const { data } = await api.fetchSmartContractState(this.pythContractAddress, Buffer.from(JSON.stringify({
128
+ get_update_fee: {
129
+ vaas: priceFeedUpdateObject.update_price_feeds.data,
130
+ },
131
+ })).toString("base64"));
132
+ const json = Buffer.from(data, "base64").toString();
133
+ updateFeeQueryResponse = JSON.parse(json);
134
+ }
135
+ catch (e) {
136
+ console.error("Error fetching update fee");
137
+ console.error(e);
138
+ return;
139
+ }
140
+ try {
141
+ const executeMsg = sdk_ts_1.MsgExecuteContract.fromJSON({
142
+ sender: this.injectiveAddress(),
143
+ contractAddress: this.pythContractAddress,
144
+ msg: priceFeedUpdateObject,
145
+ funds: [updateFeeQueryResponse],
146
+ });
147
+ const rs = await this.signAndBroadcastMsg(executeMsg);
148
+ if (rs.code !== 0)
149
+ throw new Error("Error: transaction failed");
150
+ console.log("Succesfully broadcasted txHash:", rs.txHash);
151
+ }
152
+ catch (e) {
153
+ if (e.message.match(/account inj[a-zA-Z0-9]+ not found/) !== null) {
154
+ console.error(e);
155
+ throw new Error("Please check the mnemonic");
156
+ }
157
+ if (e.message.match(/insufficient/) !== null &&
158
+ e.message.match(/funds/) !== null) {
159
+ console.error(e);
160
+ throw new Error("Insufficient funds");
161
+ }
162
+ console.error("Error executing messages");
163
+ console.log(e);
164
+ }
165
+ }
166
+ }
167
+ exports.InjectivePricePusher = InjectivePricePusher;
@@ -0,0 +1,31 @@
1
+ import { HexString, UnixTimestamp } from "@pythnetwork/price-service-client";
2
+ import { DurationInSeconds } from "./utils";
3
+ export type PriceItem = {
4
+ id: HexString;
5
+ alias: string;
6
+ };
7
+ export type PriceInfo = {
8
+ price: string;
9
+ conf: string;
10
+ publishTime: UnixTimestamp;
11
+ };
12
+ export interface IPriceListener {
13
+ start(): Promise<void>;
14
+ getLatestPriceInfo(priceId: string): PriceInfo | undefined;
15
+ }
16
+ export declare abstract class ChainPriceListener implements IPriceListener {
17
+ private chain;
18
+ private pollingFrequency;
19
+ protected priceItems: PriceItem[];
20
+ private latestPriceInfo;
21
+ protected priceIdToAlias: Map<HexString, string>;
22
+ constructor(chain: string, pollingFrequency: DurationInSeconds, priceItems: PriceItem[]);
23
+ start(): Promise<void>;
24
+ private pollPrices;
25
+ protected updateLatestPriceInfo(priceId: HexString, observedPrice: PriceInfo): void;
26
+ getLatestPriceInfo(priceId: string): PriceInfo | undefined;
27
+ abstract getOnChainPriceInfo(priceId: HexString): Promise<PriceInfo | undefined>;
28
+ }
29
+ export interface IPricePusher {
30
+ updatePriceFeed(priceIds: string[], pubTimesToPush: UnixTimestamp[]): Promise<void>;
31
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChainPriceListener = void 0;
4
+ class ChainPriceListener {
5
+ chain;
6
+ pollingFrequency;
7
+ priceItems;
8
+ latestPriceInfo;
9
+ priceIdToAlias;
10
+ constructor(chain, pollingFrequency, priceItems) {
11
+ this.chain = chain;
12
+ this.pollingFrequency = pollingFrequency;
13
+ this.priceItems = priceItems;
14
+ this.latestPriceInfo = new Map();
15
+ this.priceIdToAlias = new Map(priceItems.map(({ id, alias }) => [id, alias]));
16
+ }
17
+ async start() {
18
+ console.log(`Polling the prices on ${this.chain} every ${this.pollingFrequency} seconds...`);
19
+ setInterval(this.pollPrices.bind(this), this.pollingFrequency * 1000);
20
+ await this.pollPrices();
21
+ }
22
+ async pollPrices() {
23
+ for (const { id: priceId } of this.priceItems) {
24
+ const currentPriceInfo = await this.getOnChainPriceInfo(priceId);
25
+ if (currentPriceInfo !== undefined) {
26
+ this.updateLatestPriceInfo(priceId, currentPriceInfo);
27
+ }
28
+ }
29
+ }
30
+ updateLatestPriceInfo(priceId, observedPrice) {
31
+ const cachedLatestPriceInfo = this.getLatestPriceInfo(priceId);
32
+ // Ignore the observed price if the cache already has newer
33
+ // price. This could happen because we are using polling and
34
+ // subscription at the same time.
35
+ if (cachedLatestPriceInfo !== undefined &&
36
+ cachedLatestPriceInfo.publishTime > observedPrice.publishTime) {
37
+ return;
38
+ }
39
+ this.latestPriceInfo.set(priceId, observedPrice);
40
+ }
41
+ // Should return undefined only when the price does not exist.
42
+ getLatestPriceInfo(priceId) {
43
+ return this.latestPriceInfo.get(priceId);
44
+ }
45
+ }
46
+ exports.ChainPriceListener = ChainPriceListener;
@@ -0,0 +1,19 @@
1
+ import { Options } from "yargs";
2
+ export declare const priceServiceEndpoint: {
3
+ "price-service-endpoint": Options;
4
+ };
5
+ export declare const pythContractAddress: {
6
+ "pyth-contract-address": Options;
7
+ };
8
+ export declare const priceConfigFile: {
9
+ "price-config-file": Options;
10
+ };
11
+ export declare const pollingFrequency: {
12
+ "polling-frequency": Options;
13
+ };
14
+ export declare const pushingFrequency: {
15
+ "pushing-frequency": Options;
16
+ };
17
+ export declare const mnemonicFile: {
18
+ "mnemonic-file": Options;
19
+ };