Skip to content

Commit 52ae0b8

Browse files
authored
Update aptos pusher to support high TPS (#937)
1 parent 387f172 commit 52ae0b8

File tree

3 files changed

+77
-60
lines changed

3 files changed

+77
-60
lines changed

price_pusher/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/price-pusher",
3-
"version": "5.4.2",
3+
"version": "5.4.3",
44
"description": "Pyth Price Pusher",
55
"homepage": "https://pyth.network",
66
"main": "lib/index.js",

price_pusher/src/aptos/aptos.ts

Lines changed: 65 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
import { AptosAccount, AptosClient, TxnBuilderTypes } from "aptos";
88
import { DurationInSeconds } from "../utils";
99
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
10-
import { PushAttempt } from "../common";
1110

1211
export class AptosPriceListener extends ChainPriceListener {
1312
constructor(
@@ -68,17 +67,23 @@ export class AptosPriceListener extends ChainPriceListener {
6867
}
6968
}
7069

70+
// Derivation path for aptos accounts
71+
export const APTOS_ACCOUNT_HD_PATH = "m/44'/637'/0'/0'/0'";
7172
export class AptosPricePusher implements IPricePusher {
72-
private lastPushAttempt: PushAttempt | undefined;
73+
// The last sequence number that has a transaction submitted.
74+
private lastSequenceNumber: number | undefined;
75+
// If true, we are trying to fetch the most recent sequence number from the blockchain.
76+
private sequenceNumberLocked: boolean;
7377

74-
private readonly accountHDPath = "m/44'/637'/0'/0'/0'";
7578
constructor(
7679
private priceServiceConnection: PriceServiceConnection,
7780
private pythContractAddress: string,
7881
private endpoint: string,
7982
private mnemonic: string,
8083
private overrideGasPriceMultiplier: number
81-
) {}
84+
) {
85+
this.sequenceNumberLocked = false;
86+
}
8287

8388
/**
8489
* Gets price update data which then can be submitted to the Pyth contract to update the prices.
@@ -118,73 +123,75 @@ export class AptosPricePusher implements IPricePusher {
118123

119124
try {
120125
const account = AptosAccount.fromDerivePath(
121-
this.accountHDPath,
126+
APTOS_ACCOUNT_HD_PATH,
122127
this.mnemonic
123128
);
124129
const client = new AptosClient(this.endpoint);
125130

126-
const rawTx = await client.generateTransaction(account.address(), {
127-
function: `${this.pythContractAddress}::pyth::update_price_feeds_if_fresh_with_funder`,
128-
type_arguments: [],
129-
arguments: [
130-
priceFeedUpdateData,
131-
priceIds.map((priceId) => Buffer.from(priceId, "hex")),
132-
pubTimesToPush,
133-
],
134-
});
135-
136-
const simulation = await client.simulateTransaction(account, rawTx, {
137-
estimateGasUnitPrice: true,
138-
estimateMaxGasAmount: true,
139-
estimatePrioritizedGasUnitPrice: true,
140-
});
141-
142-
// Transactions on Aptos can be prioritized by paying a higher gas unit price.
143-
// We are storing the gas unit price paid for the last transaction.
144-
// If that transaction is not added to the block, we are increasing the gas unit price
145-
// by multiplying the old gas unit price with `this.overrideGasPriceMultiplier`.
146-
// After which we are sending a transaction with the same sequence number as the last
147-
// transaction. Since they have the same sequence number only one of them will be added to
148-
// the block and we won't be paying fees twice.
149-
let gasUnitPrice = Number(simulation[0].gas_unit_price);
150-
if (
151-
this.lastPushAttempt !== undefined &&
152-
Number(simulation[0].sequence_number) === this.lastPushAttempt.nonce
153-
) {
154-
const newGasUnitPrice = Number(
155-
this.lastPushAttempt.gasPrice * this.overrideGasPriceMultiplier
156-
);
157-
if (gasUnitPrice < newGasUnitPrice) gasUnitPrice = newGasUnitPrice;
158-
}
159-
160-
const gasUsed = Number(simulation[0].gas_used) * 1.5;
161-
const maxGasAmount = Number(gasUnitPrice * gasUsed);
162-
163-
const rawTxWithFee = new TxnBuilderTypes.RawTransaction(
164-
rawTx.sender,
165-
rawTx.sequence_number,
166-
rawTx.payload,
167-
BigInt(maxGasAmount.toFixed()),
168-
BigInt(gasUnitPrice.toFixed()),
169-
rawTx.expiration_timestamp_secs,
170-
rawTx.chain_id
131+
const sequenceNumber = await this.tryGetNextSequenceNumber(
132+
client,
133+
account
134+
);
135+
const rawTx = await client.generateTransaction(
136+
account.address(),
137+
{
138+
function: `${this.pythContractAddress}::pyth::update_price_feeds_with_funder`,
139+
type_arguments: [],
140+
arguments: [priceFeedUpdateData],
141+
},
142+
{
143+
sequence_number: sequenceNumber.toFixed(),
144+
}
171145
);
172146

173-
const signedTx = await client.signTransaction(account, rawTxWithFee);
147+
const signedTx = await client.signTransaction(account, rawTx);
174148
const pendingTx = await client.submitTransaction(signedTx);
175149

176-
console.log("Succesfully broadcasted txHash:", pendingTx.hash);
177-
178-
// Update lastAttempt
179-
this.lastPushAttempt = {
180-
nonce: Number(pendingTx.sequence_number),
181-
gasPrice: gasUnitPrice,
182-
};
150+
console.log("Successfully broadcasted txHash:", pendingTx.hash);
183151
return;
184152
} catch (e: any) {
185153
console.error("Error executing messages");
186154
console.log(e);
155+
156+
// Reset the sequence number to re-sync it (in case that was the issue)
157+
this.lastSequenceNumber = undefined;
158+
187159
return;
188160
}
189161
}
162+
163+
// Try to get the next sequence number for account. This function uses a local cache
164+
// to predict the next sequence number if possible; if not, it fetches the number from
165+
// the blockchain itself (and caches it for later).
166+
private async tryGetNextSequenceNumber(
167+
client: AptosClient,
168+
account: AptosAccount
169+
): Promise<number> {
170+
if (this.lastSequenceNumber !== undefined) {
171+
this.lastSequenceNumber += 1;
172+
return this.lastSequenceNumber;
173+
} else {
174+
// Fetch from the blockchain if we don't have the local cache.
175+
// Note that this is locked so that only 1 fetch occurs regardless of how many updates
176+
// happen during that fetch.
177+
if (!this.sequenceNumberLocked) {
178+
try {
179+
this.sequenceNumberLocked = true;
180+
this.lastSequenceNumber = Number(
181+
(await client.getAccount(account.address())).sequence_number
182+
);
183+
console.log(
184+
`Fetched account sequence number: ${this.lastSequenceNumber}`
185+
);
186+
return this.lastSequenceNumber;
187+
} catch (e: any) {
188+
throw new Error("Failed to retrieve sequence number");
189+
} finally {
190+
this.sequenceNumberLocked = false;
191+
}
192+
} else {
193+
throw new Error("Waiting for sequence number in another thread.");
194+
}
195+
}
196+
}
190197
}

price_pusher/src/aptos/command.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import fs from "fs";
55
import { PythPriceListener } from "../pyth-price-listener";
66
import { Controller } from "../controller";
77
import { Options } from "yargs";
8-
import { AptosPriceListener, AptosPricePusher } from "./aptos";
8+
import {
9+
AptosPriceListener,
10+
AptosPricePusher,
11+
APTOS_ACCOUNT_HD_PATH,
12+
} from "./aptos";
13+
import { AptosAccount } from "aptos";
914

1015
export default {
1116
command: "aptos",
@@ -61,6 +66,11 @@ export default {
6166
}
6267
);
6368
const mnemonic = fs.readFileSync(mnemonicFile, "utf-8").trim();
69+
const account = AptosAccount.fromDerivePath(
70+
APTOS_ACCOUNT_HD_PATH,
71+
mnemonic
72+
);
73+
console.log(`Pushing from account address: ${account.address()}`);
6474

6575
const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
6676

0 commit comments

Comments
 (0)