@pythnetwork/price-pusher
Version:
Pyth Price Pusher
181 lines (180 loc) • 8.36 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AptosPricePusher = exports.APTOS_ACCOUNT_HD_PATH = exports.AptosPriceListener = void 0;
const interface_1 = require("../interface");
const aptos_1 = require("aptos");
class AptosPriceListener extends interface_1.ChainPriceListener {
pythModule;
endpoint;
logger;
constructor(pythModule, endpoint, priceItems, logger, config) {
super(config.pollingFrequency, priceItems);
this.pythModule = pythModule;
this.endpoint = endpoint;
this.logger = logger;
}
async getOnChainPriceInfo(priceId) {
const client = new aptos_1.AptosClient(this.endpoint);
const res = await client.getAccountResource(this.pythModule, `${this.pythModule}::state::LatestPriceInfo`);
try {
// This depends upon the pyth contract storage on Aptos and should not be undefined.
// If undefined, there has been some change and we would need to update accordingly.
const handle = res.data.info.handle;
const priceItemRes = await client.getTableItem(handle, {
key_type: `${this.pythModule}::price_identifier::PriceIdentifier`,
value_type: `${this.pythModule}::price_info::PriceInfo`,
key: {
bytes: priceId,
},
});
const multiplier = priceItemRes.price_feed.price.price.negative === true ? -1 : 1;
const price = multiplier * Number(priceItemRes.price_feed.price.price.magnitude);
this.logger.debug(`Polled an Aptos on-chain price for feed ${this.priceIdToAlias.get(priceId)} (${priceId}).`);
return {
price: price.toString(),
conf: priceItemRes.price_feed.price.conf,
publishTime: Number(priceItemRes.price_feed.price.timestamp),
};
}
catch (err) {
this.logger.error(err, `Polling Aptos on-chain price for ${priceId} failed.`);
return undefined;
}
}
}
exports.AptosPriceListener = AptosPriceListener;
// Derivation path for aptos accounts
exports.APTOS_ACCOUNT_HD_PATH = "m/44'/637'/0'/0'/0'";
/**
* The `AptosPricePusher` is designed for high-throughput of price updates.
* Achieving this property requires sacrificing some nice-to-have features of other
* pusher implementations that can reduce cost when running multiple pushers. Specifically,
* this implementation does not use `update_price_feeds_if_necssary` and simulate the transaction
* before submission.
*
* If multiple instances of this pusher are running in parallel, both of them will
* land all of their pushed updates on-chain.
*/
class AptosPricePusher {
hermesClient;
logger;
pythContractAddress;
endpoint;
mnemonic;
overrideGasPriceMultiplier;
// The last sequence number that has a transaction submitted.
lastSequenceNumber;
// If true, we are trying to fetch the most recent sequence number from the blockchain.
sequenceNumberLocked;
constructor(hermesClient, logger, pythContractAddress, endpoint, mnemonic, overrideGasPriceMultiplier) {
this.hermesClient = hermesClient;
this.logger = logger;
this.pythContractAddress = pythContractAddress;
this.endpoint = endpoint;
this.mnemonic = mnemonic;
this.overrideGasPriceMultiplier = overrideGasPriceMultiplier;
this.sequenceNumberLocked = false;
}
/**
* Gets price update data which then can be submitted to the Pyth contract to update the prices.
* 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)
*
* @param priceIds Array of hex-encoded price ids.
* @returns Array of price update data.
*/
async getPriceFeedsUpdateData(priceIds) {
const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
encoding: "base64",
});
return response.binary.data.map((data) => Array.from(Buffer.from(data, "base64")));
}
async updatePriceFeed(priceIds, pubTimesToPush) {
if (priceIds.length === 0) {
return;
}
if (priceIds.length !== pubTimesToPush.length)
throw new Error("Invalid arguments");
let priceFeedUpdateData;
try {
// get the latest VAAs for updatePriceFeed and then push them
priceFeedUpdateData = await this.getPriceFeedsUpdateData(priceIds);
}
catch (err) {
this.logger.error(err, "Error fetching the latest vaas to push.");
return;
}
const account = aptos_1.AptosAccount.fromDerivePath(exports.APTOS_ACCOUNT_HD_PATH, this.mnemonic);
const client = new aptos_1.AptosClient(this.endpoint);
const sequenceNumber = await this.tryGetNextSequenceNumber(client, account);
const rawTx = await client.generateTransaction(account.address(), {
function: `${this.pythContractAddress}::pyth::update_price_feeds_with_funder`,
type_arguments: [],
arguments: [priceFeedUpdateData],
}, {
sequence_number: sequenceNumber.toFixed(),
});
try {
const signedTx = await client.signTransaction(account, rawTx);
const pendingTx = await client.submitTransaction(signedTx);
this.logger.debug({ hash: pendingTx.hash }, "Successfully broadcasted tx.");
// Sometimes broadcasted txs don't make it on-chain and they cause our sequence number
// to go out of sync. Missing transactions are rare and we don't want this check to block
// the next price update. So we use spawn a promise without awaiting on it to wait for the
// transaction to be confirmed and if it fails, it resets the sequence number and return.
this.waitForTransactionConfirmation(client, pendingTx.hash);
return;
}
catch (err) {
this.logger.error(err, "Error executing messages");
// Reset the sequence number to re-sync it (in case that was the issue)
this.lastSequenceNumber = undefined;
return;
}
}
// Wait for the transaction to be confirmed. If it fails, reset the sequence number.
async waitForTransactionConfirmation(client, txHash) {
try {
await client.waitForTransaction(txHash, {
checkSuccess: true,
timeoutSecs: 10,
});
this.logger.info({ hash: txHash }, `Transaction confirmed.`);
}
catch (err) {
this.logger.error({ err, hash: txHash }, `Transaction failed to confirm.`);
this.lastSequenceNumber = undefined;
}
}
// Try to get the next sequence number for account. This function uses a local cache
// to predict the next sequence number if possible; if not, it fetches the number from
// the blockchain itself (and caches it for later).
async tryGetNextSequenceNumber(client, account) {
if (this.lastSequenceNumber !== undefined) {
this.lastSequenceNumber += 1;
return this.lastSequenceNumber;
}
else {
// Fetch from the blockchain if we don't have the local cache.
// Note that this is locked so that only 1 fetch occurs regardless of how many updates
// happen during that fetch.
if (!this.sequenceNumberLocked) {
try {
this.sequenceNumberLocked = true;
this.lastSequenceNumber = Number((await client.getAccount(account.address())).sequence_number);
this.logger.debug(`Fetched account sequence number: ${this.lastSequenceNumber}`);
return this.lastSequenceNumber;
}
catch (e) {
throw new Error("Failed to retrieve sequence number" + e);
}
finally {
this.sequenceNumberLocked = false;
}
}
else {
throw new Error("Waiting for sequence number in another thread.");
}
}
}
}
exports.AptosPricePusher = AptosPricePusher;