|
7 | 7 | import { AptosAccount, AptosClient, TxnBuilderTypes } from "aptos";
|
8 | 8 | import { DurationInSeconds } from "../utils";
|
9 | 9 | import { PriceServiceConnection } from "@pythnetwork/price-service-client";
|
10 |
| -import { PushAttempt } from "../common"; |
11 | 10 |
|
12 | 11 | export class AptosPriceListener extends ChainPriceListener {
|
13 | 12 | constructor(
|
@@ -68,17 +67,23 @@ export class AptosPriceListener extends ChainPriceListener {
|
68 | 67 | }
|
69 | 68 | }
|
70 | 69 |
|
| 70 | +// Derivation path for aptos accounts |
| 71 | +export const APTOS_ACCOUNT_HD_PATH = "m/44'/637'/0'/0'/0'"; |
71 | 72 | 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; |
73 | 77 |
|
74 |
| - private readonly accountHDPath = "m/44'/637'/0'/0'/0'"; |
75 | 78 | constructor(
|
76 | 79 | private priceServiceConnection: PriceServiceConnection,
|
77 | 80 | private pythContractAddress: string,
|
78 | 81 | private endpoint: string,
|
79 | 82 | private mnemonic: string,
|
80 | 83 | private overrideGasPriceMultiplier: number
|
81 |
| - ) {} |
| 84 | + ) { |
| 85 | + this.sequenceNumberLocked = false; |
| 86 | + } |
82 | 87 |
|
83 | 88 | /**
|
84 | 89 | * 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 {
|
118 | 123 |
|
119 | 124 | try {
|
120 | 125 | const account = AptosAccount.fromDerivePath(
|
121 |
| - this.accountHDPath, |
| 126 | + APTOS_ACCOUNT_HD_PATH, |
122 | 127 | this.mnemonic
|
123 | 128 | );
|
124 | 129 | const client = new AptosClient(this.endpoint);
|
125 | 130 |
|
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 | + } |
171 | 145 | );
|
172 | 146 |
|
173 |
| - const signedTx = await client.signTransaction(account, rawTxWithFee); |
| 147 | + const signedTx = await client.signTransaction(account, rawTx); |
174 | 148 | const pendingTx = await client.submitTransaction(signedTx);
|
175 | 149 |
|
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); |
183 | 151 | return;
|
184 | 152 | } catch (e: any) {
|
185 | 153 | console.error("Error executing messages");
|
186 | 154 | console.log(e);
|
| 155 | + |
| 156 | + // Reset the sequence number to re-sync it (in case that was the issue) |
| 157 | + this.lastSequenceNumber = undefined; |
| 158 | + |
187 | 159 | return;
|
188 | 160 | }
|
189 | 161 | }
|
| 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 | + } |
190 | 197 | }
|
0 commit comments