Sui pusher updates (#914)

* split into ptbs

* update sui logic

* fix stuff

* revert

* use Map
This commit is contained in:
Jayant Krishnamurthy 2023-06-23 09:20:01 -07:00 committed by GitHub
parent 69a0a9ec98
commit 742c37ed88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 105 additions and 27 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@pythnetwork/price-pusher", "name": "@pythnetwork/price-pusher",
"version": "5.3.2", "version": "5.4.0",
"description": "Pyth Price Pusher", "description": "Pyth Price Pusher",
"homepage": "https://pyth.network", "homepage": "https://pyth.network",
"main": "lib/index.js", "main": "lib/index.js",

View File

@ -58,6 +58,13 @@ export default {
type: "string", type: "string",
required: true, required: true,
} as Options, } as Options,
"max-vaas-per-ptb": {
description:
"Maximum number of VAAs that can be included in a single PTB.",
type: "number",
required: true,
default: 1,
} as Options,
...options.priceConfigFile, ...options.priceConfigFile,
...options.priceServiceEndpoint, ...options.priceServiceEndpoint,
...options.mnemonicFile, ...options.mnemonicFile,
@ -77,6 +84,7 @@ export default {
wormholePackageId, wormholePackageId,
wormholeStateId, wormholeStateId,
priceFeedToPriceInfoObjectTableId, priceFeedToPriceInfoObjectTableId,
maxVaasPerPtb,
} = argv; } = argv;
const priceConfigs = readPriceConfigFile(priceConfigFile); const priceConfigs = readPriceConfigFile(priceConfigFile);
@ -91,6 +99,9 @@ export default {
debug: () => undefined, debug: () => undefined,
trace: () => undefined, trace: () => undefined,
}, },
priceFeedRequestConfig: {
binary: true,
},
} }
); );
const mnemonic = fs.readFileSync(mnemonicFile, "utf-8").trim(); const mnemonic = fs.readFileSync(mnemonicFile, "utf-8").trim();
@ -116,6 +127,7 @@ export default {
wormholePackageId, wormholePackageId,
wormholeStateId, wormholeStateId,
priceFeedToPriceInfoObjectTableId, priceFeedToPriceInfoObjectTableId,
maxVaasPerPtb,
endpoint, endpoint,
mnemonic mnemonic
); );

View File

@ -83,6 +83,10 @@ export class SuiPriceListener extends ChainPriceListener {
export class SuiPricePusher implements IPricePusher { export class SuiPricePusher implements IPricePusher {
private readonly signer: RawSigner; private readonly signer: RawSigner;
// Sui transactions can error if they're sent concurrently. This flag tracks whether an update is in-flight,
// so we can skip sending another update at the same time.
private isAwaitingTx: boolean;
constructor( constructor(
private priceServiceConnection: PriceServiceConnection, private priceServiceConnection: PriceServiceConnection,
private pythPackageId: string, private pythPackageId: string,
@ -90,6 +94,7 @@ export class SuiPricePusher implements IPricePusher {
private wormholePackageId: string, private wormholePackageId: string,
private wormholeStateId: string, private wormholeStateId: string,
private priceFeedToPriceInfoObjectTableId: string, private priceFeedToPriceInfoObjectTableId: string,
private maxVaasPerPtb: number,
endpoint: string, endpoint: string,
mnemonic: string mnemonic: string
) { ) {
@ -97,6 +102,7 @@ export class SuiPricePusher implements IPricePusher {
Ed25519Keypair.deriveKeypair(mnemonic), Ed25519Keypair.deriveKeypair(mnemonic),
new JsonRpcProvider(new Connection({ fullnode: endpoint })) new JsonRpcProvider(new Connection({ fullnode: endpoint }))
); );
this.isAwaitingTx = false;
} }
async updatePriceFeed( async updatePriceFeed(
@ -110,10 +116,64 @@ export class SuiPricePusher implements IPricePusher {
if (priceIds.length !== pubTimesToPush.length) if (priceIds.length !== pubTimesToPush.length)
throw new Error("Invalid arguments"); throw new Error("Invalid arguments");
if (this.isAwaitingTx) {
console.log(
"Skipping update: previous price update transaction(s) have not completed."
);
return;
}
const priceFeeds = await this.priceServiceConnection.getLatestPriceFeeds(
priceIds
);
if (priceFeeds === undefined) {
console.log("Failed to fetch price updates. Skipping push.");
return;
}
const vaaToPriceFeedIds: Map<string, string[]> = new Map();
for (const priceFeed of priceFeeds) {
// The ! will succeed as long as the priceServiceConnection is configured to return binary vaa data (which it is).
const vaa = priceFeed.getVAA()!;
if (!vaaToPriceFeedIds.has(vaa)) {
vaaToPriceFeedIds.set(vaa, []);
}
vaaToPriceFeedIds.get(vaa)!.push(priceFeed.id);
}
const txs = [];
let currentBatchVaas = [];
let currentBatchPriceFeedIds = [];
for (const [vaa, priceFeedIds] of Object.entries(vaaToPriceFeedIds)) {
currentBatchVaas.push(vaa);
currentBatchPriceFeedIds.push(...priceFeedIds);
if (currentBatchVaas.length >= this.maxVaasPerPtb) {
const tx = await this.createPriceUpdateTransaction(
currentBatchVaas,
currentBatchPriceFeedIds
);
if (tx !== undefined) {
txs.push(tx);
}
currentBatchVaas = [];
currentBatchPriceFeedIds = [];
}
}
try {
this.isAwaitingTx = true;
await this.sendTransactionBlocks(txs);
} finally {
this.isAwaitingTx = false;
}
}
private async createPriceUpdateTransaction(
vaas: string[],
priceIds: string[]
): Promise<TransactionBlock | undefined> {
const tx = new TransactionBlock(); const tx = new TransactionBlock();
const vaas = await this.priceServiceConnection.getLatestVaas(priceIds);
// Parse our batch price attestation VAA bytes using Wormhole. // Parse our batch price attestation VAA bytes using Wormhole.
// Check out the Wormhole cross-chain bridge and generic messaging protocol here: // Check out the Wormhole cross-chain bridge and generic messaging protocol here:
// https://github.com/wormhole-foundation/wormhole // https://github.com/wormhole-foundation/wormhole
@ -158,7 +218,7 @@ export class SuiPricePusher implements IPricePusher {
} catch (e) { } catch (e) {
console.log("Error fetching price info object id for ", priceId); console.log("Error fetching price info object id for ", priceId);
console.error(e); console.error(e);
return; return undefined;
} }
const coin = tx.splitCoins(tx.gas, [tx.pure(1)]); const coin = tx.splitCoins(tx.gas, [tx.pure(1)]);
[price_updates_hot_potato] = tx.moveCall({ [price_updates_hot_potato] = tx.moveCall({
@ -181,30 +241,36 @@ export class SuiPricePusher implements IPricePusher {
typeArguments: [`${this.pythPackageId}::price_info::PriceInfo`], typeArguments: [`${this.pythPackageId}::price_info::PriceInfo`],
}); });
try { return tx;
const result = await this.signer.signAndExecuteTransactionBlock({ }
transactionBlock: tx,
options: {
showInput: true,
showEffects: true,
showEvents: true,
showObjectChanges: true,
showBalanceChanges: true,
},
});
console.log( /** Send every transaction in txs sequentially, returning when all transactions have completed. */
"Successfully updated price with transaction digest ", private async sendTransactionBlocks(txs: TransactionBlock[]): Promise<void> {
result.digest for (const tx of txs) {
); try {
} catch (e) { const result = await this.signer.signAndExecuteTransactionBlock({
console.log("Error when signAndExecuteTransactionBlock"); transactionBlock: tx,
if (String(e).includes("GasBalanceTooLow")) { options: {
console.log("Insufficient Gas Amount. Please top up your account"); showInput: true,
process.exit(); 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);
} }
console.error(e);
return;
} }
} }
} }