Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce transaction construction time #919

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

110 changes: 97 additions & 13 deletions price_pusher/src/sui/sui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
RawSigner,
TransactionBlock,
SUI_CLOCK_OBJECT_ID,
SharedObjectRef,
getSharedObjectInitialVersion,
} from "@mysten/sui.js";

export class SuiPriceListener extends ChainPriceListener {
Expand Down Expand Up @@ -43,7 +45,7 @@ export class SuiPriceListener extends ChainPriceListener {

// Fetching the price info object for the above priceInfoObjectId
const priceInfoObject = await provider.getObject({
id: priceInfoObjectId,
id: priceInfoObjectId.objectId,
options: { showContent: true },
});

Expand Down Expand Up @@ -81,12 +83,20 @@ export class SuiPriceListener extends ChainPriceListener {
}
}

// Gas price is cached for one minute to balance minimal fetching and risk of stale prices
// across epoch boundaries.
const GAS_PRICE_CACHE_DURATION = 60 * 1000;

export class SuiPricePusher implements IPricePusher {
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;

private gasPriceCache?: { price: bigint; expiry: number };
private pythStateReference?: SharedObjectRef;
private wormholeStateReference?: SharedObjectRef;

constructor(
private priceServiceConnection: PriceServiceConnection,
private pythPackageId: string,
Expand All @@ -105,6 +115,48 @@ export class SuiPricePusher implements IPricePusher {
this.isAwaitingTx = false;
}

async getGasPrice() {
if (this.gasPriceCache && this.gasPriceCache.expiry > Date.now()) {
return this.gasPriceCache.price;
}

const price = await this.signer.provider.getReferenceGasPrice();
this.gasPriceCache = {
price,
expiry: Date.now() + GAS_PRICE_CACHE_DURATION,
};

return price;
}

async resolveSharedReferences() {
if (!this.pythStateReference || !this.wormholeStateReference) {
const [pythStateObject, wormholeStateObject] =
await this.signer.provider.multiGetObjects({
ids: [this.pythStateId, this.wormholeStateId],
options: { showOwner: true },
});

this.pythStateReference = {
objectId: this.pythStateId,
initialSharedVersion: getSharedObjectInitialVersion(pythStateObject)!,
mutable: false,
};

this.wormholeStateReference = {
objectId: this.wormholeStateId,
initialSharedVersion:
getSharedObjectInitialVersion(wormholeStateObject)!,
mutable: false,
};
}

return {
pythStateReference: this.pythStateReference,
wormholeStateReference: this.wormholeStateReference,
};
}

async updatePriceFeed(
priceIds: string[],
pubTimesToPush: number[]
Expand Down Expand Up @@ -173,6 +225,9 @@ export class SuiPricePusher implements IPricePusher {
vaas: string[],
priceIds: string[]
): Promise<TransactionBlock | undefined> {
const { pythStateReference, wormholeStateReference } =
await this.resolveSharedReferences();

const tx = new TransactionBlock();
// Parse our batch price attestation VAA bytes using Wormhole.
// Check out the Wormhole cross-chain bridge and generic messaging protocol here:
Expand All @@ -182,9 +237,13 @@ export class SuiPricePusher implements IPricePusher {
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),
tx.sharedObjectRef(wormholeStateReference),
tx.pure(new Uint8Array(Buffer.from(vaa, "base64"))),
tx.sharedObjectRef({
objectId: SUI_CLOCK_OBJECT_ID,
initialSharedVersion: 1,
mutable: false,
}),
],
});
verified_vaas = verified_vaas.concat(verified_vaa);
Expand All @@ -195,12 +254,16 @@ export class SuiPricePusher implements IPricePusher {
let [price_updates_hot_potato] = tx.moveCall({
target: `${this.pythPackageId}::pyth::create_price_infos_hot_potato`,
arguments: [
tx.object(this.pythStateId),
tx.sharedObjectRef(pythStateReference),
tx.makeMoveVec({
type: `${this.wormholePackageId}::vaa::VAA`,
objects: verified_vaas,
}),
tx.object(SUI_CLOCK_OBJECT_ID),
tx.sharedObjectRef({
objectId: SUI_CLOCK_OBJECT_ID,
initialSharedVersion: 1,
mutable: false,
}),
],
});

Expand All @@ -224,11 +287,15 @@ export class SuiPricePusher implements IPricePusher {
[price_updates_hot_potato] = tx.moveCall({
target: `${this.pythPackageId}::pyth::update_single_price_feed`,
arguments: [
tx.object(this.pythStateId),
tx.sharedObjectRef(pythStateReference),
price_updates_hot_potato,
tx.object(priceInfoObjectId),
tx.sharedObjectRef(priceInfoObjectId),
coin,
tx.object(SUI_CLOCK_OBJECT_ID),
tx.sharedObjectRef({
objectId: SUI_CLOCK_OBJECT_ID,
initialSharedVersion: 1,
mutable: false,
}),
],
});
}
Expand All @@ -246,8 +313,11 @@ export class SuiPricePusher implements IPricePusher {

/** Send every transaction in txs sequentially, returning when all transactions have completed. */
private async sendTransactionBlocks(txs: TransactionBlock[]): Promise<void> {
const gasPrice = await this.getGasPrice();

for (const tx of txs) {
try {
tx.setGasPrice(gasPrice);
const result = await this.signer.signAndExecuteTransactionBlock({
transactionBlock: tx,
options: {
Expand All @@ -259,6 +329,11 @@ export class SuiPricePusher implements IPricePusher {
},
});

// In the event of a transaction failure, remove the gas price cache just in case it was a gas-price-related issue:
if (result.effects?.status.status === "failure") {
this.gasPriceCache = undefined;
}

console.log(
"Successfully updated price with transaction digest ",
result.digest
Expand All @@ -277,7 +352,7 @@ export class SuiPricePusher implements IPricePusher {

// 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 } = {};
const CACHE: { [priceId: string]: SharedObjectRef } = {};

// For given priceid, this method will fetch the price info object id
// where the price information for the corresponding price feed is stored
Expand All @@ -286,7 +361,7 @@ async function priceIdToPriceInfoObjectId(
pythPackageId: string,
priceFeedToPriceInfoObjectTableId: string,
priceId: string
) {
): Promise<SharedObjectRef> {
// Check if this was fetched before.
if (CACHE[priceId] !== undefined) return CACHE[priceId];

Expand All @@ -313,8 +388,17 @@ async function priceIdToPriceInfoObjectId(
// This ID points to the price info object for the given price id stored on chain
const priceInfoObjectId = storedObjectID.data.content.fields.value;

const object = await provider.getObject({
id: priceInfoObjectId,
options: { showOwner: true },
});

// cache the price info object id
CACHE[priceId] = priceInfoObjectId;
CACHE[priceId] = {
objectId: priceInfoObjectId,
initialSharedVersion: getSharedObjectInitialVersion(object)!,
mutable: true,
};

return priceInfoObjectId;
return CACHE[priceId];
}