@pythnetwork/price-pusher 10.3.1 → 11.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +28 -7
  2. package/dist/aptos/aptos.cjs +7 -7
  3. package/dist/aptos/aptos.d.ts +1 -1
  4. package/dist/aptos/balance-tracker.cjs +7 -7
  5. package/dist/aptos/balance-tracker.d.ts +1 -1
  6. package/dist/aptos/command.cjs +20 -17
  7. package/dist/aptos/command.d.ts +3 -2
  8. package/dist/common.cjs +0 -3
  9. package/dist/common.d.ts +0 -4
  10. package/dist/controller.cjs +12 -7
  11. package/dist/controller.d.ts +1 -1
  12. package/dist/evm/balance-tracker.cjs +4 -4
  13. package/dist/evm/balance-tracker.d.ts +2 -2
  14. package/dist/evm/command.cjs +89 -40
  15. package/dist/evm/command.d.ts +12 -6
  16. package/dist/evm/evm.cjs +162 -58
  17. package/dist/evm/evm.d.ts +13 -8
  18. package/dist/evm/gas-price.cjs +114 -0
  19. package/dist/evm/gas-price.d.ts +33 -0
  20. package/dist/evm/pyth-contract.cjs +2 -2
  21. package/dist/evm/super-wallet.cjs +4 -4
  22. package/dist/evm/super-wallet.d.ts +1 -1
  23. package/dist/fuel/command.cjs +17 -14
  24. package/dist/fuel/command.d.ts +3 -2
  25. package/dist/fuel/fuel.d.ts +2 -2
  26. package/dist/injective/command.cjs +26 -23
  27. package/dist/injective/command.d.ts +5 -4
  28. package/dist/injective/injective.cjs +11 -11
  29. package/dist/injective/injective.d.ts +2 -3
  30. package/dist/interface.d.ts +1 -1
  31. package/dist/metrics.cjs +57 -35
  32. package/dist/metrics.d.ts +10 -5
  33. package/dist/near/command.cjs +22 -19
  34. package/dist/near/command.d.ts +5 -4
  35. package/dist/near/near.cjs +17 -16
  36. package/dist/near/near.d.ts +1 -1
  37. package/dist/options.cjs +36 -26
  38. package/dist/options.d.ts +3 -0
  39. package/dist/price-config.cjs +15 -15
  40. package/dist/pyth-price-listener.cjs +5 -3
  41. package/dist/pyth-price-listener.d.ts +2 -3
  42. package/dist/solana/balance-tracker.cjs +5 -5
  43. package/dist/solana/balance-tracker.d.ts +2 -2
  44. package/dist/solana/command.cjs +75 -60
  45. package/dist/solana/command.d.ts +13 -10
  46. package/dist/solana/solana.cjs +5 -5
  47. package/dist/solana/solana.d.ts +4 -4
  48. package/dist/sui/balance-tracker.cjs +6 -6
  49. package/dist/sui/balance-tracker.d.ts +5 -5
  50. package/dist/sui/command.cjs +84 -47
  51. package/dist/sui/command.d.ts +10 -6
  52. package/dist/sui/sui.cjs +159 -106
  53. package/dist/sui/sui.d.ts +31 -14
  54. package/dist/ton/command.cjs +17 -14
  55. package/dist/ton/command.d.ts +3 -2
  56. package/dist/ton/ton.d.ts +2 -3
  57. package/dist/utils.d.ts +1 -2
  58. package/package.json +127 -131
package/dist/evm/evm.cjs CHANGED
@@ -17,8 +17,16 @@ _export(exports, {
17
17
  }
18
18
  });
19
19
  const _viem = require("viem");
20
- const _utils = require("../utils.cjs");
21
20
  const _interface = require("../interface.cjs");
21
+ const _utils = require("../utils.cjs");
22
+ const _gasprice = require("./gas-price.cjs");
23
+ // Some RPC providers surface the same underlying failure through different viem
24
+ // error classes. For example "replacement transaction underpriced" comes back as
25
+ // an `InternalRpcError` (code -32000) on most chains, but Soneium/Alchemy returns
26
+ // it as an `InvalidInputRpcError` (code -32602, "Missing or invalid parameters").
27
+ // Matching on the error text across the whole cause chain (rather than on a
28
+ // specific class) keeps the detection robust across providers.
29
+ const errorChainIncludes = (error, needle)=>error.walk((e)=>e instanceof _viem.BaseError && e.details.includes(needle) || e instanceof Error && e.message.includes(needle)) !== null;
22
30
  class EvmPriceListener extends _interface.ChainPriceListener {
23
31
  pythContract;
24
32
  watchEvents;
@@ -44,8 +52,8 @@ class EvmPriceListener extends _interface.ChainPriceListener {
44
52
  this.pythContract.watchEvent.PriceFeedUpdate({
45
53
  id: this.priceItems.map((item)=>(0, _utils.addLeading0x)(item.id))
46
54
  }, {
47
- strict: true,
48
- onLogs: this.onPriceFeedUpdate.bind(this)
55
+ onLogs: this.onPriceFeedUpdate.bind(this),
56
+ strict: true
49
57
  });
50
58
  }
51
59
  onPriceFeedUpdate(logs) {
@@ -88,12 +96,31 @@ class EvmPricePusher {
88
96
  overrideGasPriceMultiplier;
89
97
  overrideGasPriceMultiplierCap;
90
98
  updateFeeMultiplier;
99
+ gasPriceConfig;
91
100
  gasLimit;
92
- customGasStation;
93
- gasPrice;
101
+ receiptWaitTimeoutMs;
102
+ receiptPollIntervalMs;
94
103
  pusherAddress;
95
104
  lastPushAttempt;
96
- constructor(hermesClient, client, pythContract, logger, overrideGasPriceMultiplier, overrideGasPriceMultiplierCap, updateFeeMultiplier, gasLimit, customGasStation, gasPrice){
105
+ // Bounded, per-nonce receipt trackers. Each poll self-terminates on landing or
106
+ // at `receiptWaitTimeoutMs`. Same-nonce gas escalations ADD a tracker rather
107
+ // than replacing the previous one: the original and the repriced tx compete
108
+ // for the nonce and EITHER can land (a miner may include the old tx over the
109
+ // replacement), so all competing hashes stay watched and whichever lands is
110
+ // logged. A nonce advance means the previous nonce already resolved, so its
111
+ // now-doomed stragglers are aborted. This replaces viem's
112
+ // `waitForTransactionReceipt`, whose timeout (on the pinned version) rejects
113
+ // without tearing down its internal `eth_getTransactionByHash` / block-number
114
+ // poll — so a fire-and-forget wait for a tx that never becomes findable leaked
115
+ // a detached poller that ran for the pod's whole lifetime, one per push. Here
116
+ // the count is bounded: at most one nonce is tracked at a time, and each of
117
+ // its trackers self-terminates at the deadline.
118
+ receiptTrackers = new Set();
119
+ trackedNonce;
120
+ constructor(hermesClient, client, pythContract, logger, overrideGasPriceMultiplier, overrideGasPriceMultiplierCap, updateFeeMultiplier, gasPriceConfig, gasLimit, // Upper bound on how long to poll for a tx receipt before giving up. Keeps a
121
+ // tx that never lands from polling forever. Defaults to ~2 push cycles.
122
+ receiptWaitTimeoutMs = 60_000, // How often to poll `eth_getTransactionReceipt` while waiting.
123
+ receiptPollIntervalMs = 2000){
97
124
  this.hermesClient = hermesClient;
98
125
  this.client = client;
99
126
  this.pythContract = pythContract;
@@ -101,9 +128,10 @@ class EvmPricePusher {
101
128
  this.overrideGasPriceMultiplier = overrideGasPriceMultiplier;
102
129
  this.overrideGasPriceMultiplierCap = overrideGasPriceMultiplierCap;
103
130
  this.updateFeeMultiplier = updateFeeMultiplier;
131
+ this.gasPriceConfig = gasPriceConfig;
104
132
  this.gasLimit = gasLimit;
105
- this.customGasStation = customGasStation;
106
- this.gasPrice = gasPrice;
133
+ this.receiptWaitTimeoutMs = receiptWaitTimeoutMs;
134
+ this.receiptPollIntervalMs = receiptPollIntervalMs;
107
135
  }
108
136
  // The pubTimes are passed here to use the values that triggered the push.
109
137
  // This is an optimization to avoid getting a newer value (as an update comes)
@@ -129,11 +157,11 @@ class EvmPricePusher {
129
157
  this.logger.error(error, "An unidentified error has occured when getting the update fee.");
130
158
  throw error;
131
159
  }
132
- // Gas price in networks with transaction type eip1559 represents the
133
- // addition of baseFee and priorityFee required to land the transaction. We
134
- // are using this to remain compatible with the networks that doesn't
135
- // support this transaction type.
136
- let gasPrice = this.gasPrice ?? Number(await (this.customGasStation?.getCustomGasPrice() ?? this.client.getGasPrice()));
160
+ // Fetch a fresh gas price. With the eip1559 strategy this is sourced from
161
+ // the chain base fee and priority fee (avoiding the deprecated eth_gasPrice
162
+ // RPC); with the legacy strategy it falls back to the single gasPrice model
163
+ // for chains that don't support eip1559 transactions.
164
+ let gasPrice = await (0, _gasprice.getGasPrice)(this.client, this.gasPriceConfig, this.logger);
137
165
  // Try to re-use the same nonce and increase the gas if the last tx is not landed yet.
138
166
  this.pusherAddress ??= this.client.account.address;
139
167
  const lastExecutedNonce = await this.client.getTransactionCount({
@@ -144,20 +172,22 @@ class EvmPricePusher {
144
172
  if (this.lastPushAttempt.nonce <= lastExecutedNonce) {
145
173
  this.lastPushAttempt = undefined;
146
174
  } else {
147
- gasPriceToOverride = this.lastPushAttempt.gasPrice * this.overrideGasPriceMultiplier;
175
+ gasPriceToOverride = (0, _gasprice.scaleGasPrice)(this.lastPushAttempt.gasPrice, this.overrideGasPriceMultiplier);
148
176
  }
149
177
  }
150
- if (gasPriceToOverride !== undefined && gasPriceToOverride > Number(gasPrice)) {
151
- gasPrice = Math.min(gasPriceToOverride, gasPrice * this.overrideGasPriceMultiplierCap);
178
+ if (gasPriceToOverride !== undefined) {
179
+ // Bump every fee component to override the stuck transaction, capping the
180
+ // bump relative to the fresh gas price returned by the RPC.
181
+ gasPrice = (0, _gasprice.escalateGasPrice)(gasPrice, gasPriceToOverride, this.overrideGasPriceMultiplierCap);
152
182
  }
153
183
  const txNonce = lastExecutedNonce + 1;
154
- this.logger.debug(`Using gas price: ${gasPrice} and nonce: ${txNonce}`);
184
+ this.logger.debug(`Using ${(0, _gasprice.describeGasPrice)(gasPrice)} and nonce: ${txNonce}`);
155
185
  const pubTimesToPushParam = pubTimesToPush.map(BigInt);
156
186
  const priceIdsWith0x = priceIds.map((priceId)=>(0, _utils.addLeading0x)(priceId));
157
187
  // Update lastAttempt
158
188
  this.lastPushAttempt = {
159
- nonce: txNonce,
160
- gasPrice: gasPrice
189
+ gasPrice: gasPrice,
190
+ nonce: txNonce
161
191
  };
162
192
  try {
163
193
  const { request } = await this.pythContract.simulate.updatePriceFeedsIfNecessary([
@@ -165,26 +195,26 @@ class EvmPricePusher {
165
195
  priceIdsWith0x,
166
196
  pubTimesToPushParam
167
197
  ], {
168
- value: updateFee,
169
- gasPrice: BigInt(Math.ceil(gasPrice)),
198
+ gas: this.gasLimit === undefined ? undefined : BigInt(Math.ceil(this.gasLimit)),
199
+ ...(0, _gasprice.gasPriceToTxParams)(gasPrice),
170
200
  nonce: txNonce,
171
- gas: this.gasLimit === undefined ? undefined : BigInt(Math.ceil(this.gasLimit))
201
+ value: updateFee
172
202
  });
173
203
  this.logger.debug({
174
204
  request
175
205
  }, "Simulated request successfully");
176
206
  const hash = await this.client.writeContract(request);
177
- this.logger.info({
207
+ this.logger.debug({
178
208
  hash
179
209
  }, "Price update sent");
180
- void this.waitForTransactionReceipt(hash);
210
+ this.trackTransactionReceipt(hash, txNonce);
181
211
  } catch (error) {
182
212
  this.logger.debug({
183
213
  err: error
184
214
  }, "Simulating or sending transactions failed.");
185
215
  if (error instanceof _viem.BaseError) {
186
216
  if (error.walk((e)=>e instanceof _viem.ContractFunctionRevertedError && e.data?.errorName === "NoFreshUpdate")) {
187
- this.logger.info("Simulation reverted because none of the updates are fresh. This is an expected behaviour to save gas. Skipping this push.");
217
+ this.logger.debug("Simulation reverted because none of the updates are fresh. This is an expected behaviour to save gas. Skipping this push.");
188
218
  return;
189
219
  }
190
220
  if (error.walk((e)=>e instanceof _viem.InsufficientFundsError)) {
@@ -193,12 +223,12 @@ class EvmPricePusher {
193
223
  }, "Wallet doesn't have enough balance. In rare cases, there might be issues with gas price " + "calculation in the RPC.");
194
224
  throw error;
195
225
  }
196
- if (error.walk((e)=>e instanceof _viem.FeeCapTooLowError) || error.walk((e)=>e instanceof _viem.InternalRpcError && e.details.includes("replacement transaction underpriced"))) {
226
+ if (error.walk((e)=>e instanceof _viem.FeeCapTooLowError) || errorChainIncludes(error, "replacement transaction underpriced")) {
197
227
  this.logger.warn("The gas price of the transaction is too low or there is an existing transaction with higher gas with the same nonce. " + "The price will be increased in the next push. Skipping this push. " + "If this keeps happening or transactions are not landing you need to increase the override gas price " + "multiplier and the cap to increase the likelihood of the transaction landing on-chain.");
198
228
  return;
199
229
  }
200
230
  if (error.walk((e)=>e instanceof _viem.TransactionExecutionError && (e.details.includes("nonce too low") || e.message.includes("Nonce provided for the transaction")))) {
201
- this.logger.info("The nonce is incorrect. This is an expected behaviour in high frequency or multi-instance setup. Skipping this push.");
231
+ this.logger.debug("The nonce is incorrect. This is an expected behaviour in high frequency or multi-instance setup. Skipping this push.");
202
232
  return;
203
233
  }
204
234
  // Sometimes the contract function execution fails in simulation and this error is thrown.
@@ -211,7 +241,7 @@ class EvmPricePusher {
211
241
  // We normally crash on unknown failures but we believe that this type of error is safe to skip. The other reason is that
212
242
  // wometimes we see a TransactionExecutionError because of the nonce without any details and it is not catchable.
213
243
  if (error.walk((e)=>e instanceof _viem.TransactionExecutionError)) {
214
- this.logger.error({
244
+ this.logger.warn({
215
245
  err: error
216
246
  }, "Transaction execution failed. This is an expected behaviour in high frequency or multi-instance setup. " + "Please review this error and file an issue if it is a bug. Skipping this push.");
217
247
  return;
@@ -219,7 +249,7 @@ class EvmPricePusher {
219
249
  // The following errors are part of the legacy code and might not work as expected.
220
250
  // We are keeping them in case they help with handling what is not covered above.
221
251
  if (error.message.includes("the tx doesn't have the correct nonce.") || error.message.includes("nonce too low") || error.message.includes("invalid nonce")) {
222
- this.logger.info("The nonce is incorrect (are multiple users using this account?). Skipping this push.");
252
+ this.logger.debug("The nonce is incorrect (are multiple users using this account?). Skipping this push.");
223
253
  return;
224
254
  }
225
255
  if (error.message.includes("max fee per gas less than block base fee")) {
@@ -235,7 +265,7 @@ class EvmPricePusher {
235
265
  throw new Error("Please top up the wallet");
236
266
  }
237
267
  if (error.message.includes("could not replace existing tx")) {
238
- this.logger.error("A transaction with the same nonce has been mined and this one is no longer needed. Skipping this push.");
268
+ this.logger.debug("A transaction with the same nonce has been mined and this one is no longer needed. Skipping this push.");
239
269
  return;
240
270
  }
241
271
  }
@@ -246,37 +276,111 @@ class EvmPricePusher {
246
276
  throw error;
247
277
  }
248
278
  }
249
- async waitForTransactionReceipt(hash) {
250
- try {
251
- const receipt = await this.client.waitForTransactionReceipt({
252
- hash: hash
253
- });
254
- switch(receipt.status){
255
- case "success":
256
- {
257
- this.logger.debug({
258
- hash,
259
- receipt
260
- }, "Price update successful");
261
- this.logger.info({
262
- hash
263
- }, "Price update successful");
264
- break;
265
- }
266
- default:
267
- {
268
- this.logger.info({
269
- hash,
270
- receipt
271
- }, "Price update did not succeed or its transaction did not land. " + "This is an expected behaviour in high frequency or multi-instance setup.");
272
- }
279
+ // Stop all in-flight receipt trackers (graceful shutdown / tests). Safe to
280
+ // call at any time; leaves no pending poll behind.
281
+ dispose() {
282
+ for (const controller of this.receiptTrackers){
283
+ controller.abort();
284
+ }
285
+ this.receiptTrackers.clear();
286
+ this.trackedNonce = undefined;
287
+ }
288
+ // Start a receipt tracker for a freshly-sent tx. Fire-and-forget by design —
289
+ // the receipt is observational (it only produces the "Price update successful"
290
+ // log the Grafana Tx-Hash panel scrapes); the controller never blocks on it,
291
+ // and the re-send/nonce/gas decision is driven by a fresh `getTransactionCount`
292
+ // each cycle, not by the receipt.
293
+ trackTransactionReceipt(hash, nonce) {
294
+ // A new nonce means the previous nonce already resolved (a tx for it landed,
295
+ // advancing the on-chain count), so abort the stragglers still polling the
296
+ // old nonce's hashes. They each do one final lookup on abort, so the hash
297
+ // that actually landed is still logged before they stop. Same-nonce
298
+ // escalations fall through and ADD a tracker so every competing hash for the
299
+ // live nonce stays watched.
300
+ if (this.trackedNonce !== undefined && nonce !== this.trackedNonce) {
301
+ for (const controller of this.receiptTrackers){
302
+ controller.abort();
273
303
  }
274
- } catch (error) {
275
- this.logger.warn({
276
- err: error
277
- }, "Failed to get transaction receipt");
304
+ this.receiptTrackers.clear();
305
+ }
306
+ this.trackedNonce = nonce;
307
+ const controller = new AbortController();
308
+ this.receiptTrackers.add(controller);
309
+ void this.pollTransactionReceipt(hash, controller).finally(()=>{
310
+ this.receiptTrackers.delete(controller);
311
+ });
312
+ }
313
+ // Poll `eth_getTransactionReceipt` (one call per interval) until the tx lands,
314
+ // the tracker is superseded, or the deadline elapses. Unlike viem's
315
+ // `waitForTransactionReceipt` this issues no `eth_getTransactionByHash` and no
316
+ // per-block full-block fetch, and its teardown is guaranteed by the bound +
317
+ // AbortController — so an un-landing tx cannot leak a poller.
318
+ async pollTransactionReceipt(hash, controller) {
319
+ const deadline = Date.now() + this.receiptWaitTimeoutMs;
320
+ for(;;){
321
+ let receipt;
322
+ try {
323
+ receipt = await this.client.getTransactionReceipt({
324
+ hash
325
+ });
326
+ } catch {
327
+ // viem throws TransactionReceiptNotFoundError while the tx is unmined.
328
+ // Treat any lookup failure as "not landed yet" and keep polling.
329
+ receipt = undefined;
330
+ }
331
+ // A fetched receipt means the tx actually landed — it is never stale, so
332
+ // log it even if this tracker was superseded (otherwise the Grafana panel
333
+ // would miss the hash of a tx that landed just as the next push started).
334
+ if (receipt !== undefined) {
335
+ if (receipt.status === "success") {
336
+ // Keep one non-debug hash line per landed tx: the bundled Grafana
337
+ // "Tx Hash" panel scrapes this message from Loki and extracts {{.hash}}.
338
+ this.logger.info({
339
+ hash
340
+ }, "Price update successful");
341
+ } else {
342
+ this.logger.debug({
343
+ hash,
344
+ receipt
345
+ }, "Price update did not succeed or its transaction did not land. " + "This is an expected behaviour in high frequency or multi-instance setup.");
346
+ }
347
+ return;
348
+ }
349
+ // Not landed yet: stop if this tracker was superseded, or if we ran out
350
+ // of time (only these bounded paths end the loop, so it cannot leak).
351
+ if (controller.signal.aborted) {
352
+ return;
353
+ }
354
+ if (Date.now() >= deadline) {
355
+ this.logger.debug({
356
+ hash
357
+ }, "Gave up waiting for the transaction receipt within the timeout. " + "This is expected when a tx does not land; the next push re-derives " + "the nonce from the chain and replaces it if needed.");
358
+ return;
359
+ }
360
+ await this.interruptibleSleep(this.receiptPollIntervalMs, controller.signal);
278
361
  }
279
362
  }
363
+ // Sleep that resolves early if the signal aborts, so a superseded tracker
364
+ // stops promptly instead of waiting out its poll interval.
365
+ interruptibleSleep(ms, signal) {
366
+ return new Promise((resolve)=>{
367
+ if (signal.aborted) {
368
+ resolve();
369
+ return;
370
+ }
371
+ const onAbort = ()=>{
372
+ clearTimeout(timer);
373
+ resolve();
374
+ };
375
+ const timer = setTimeout(()=>{
376
+ signal.removeEventListener("abort", onAbort);
377
+ resolve();
378
+ }, ms);
379
+ signal.addEventListener("abort", onAbort, {
380
+ once: true
381
+ });
382
+ });
383
+ }
280
384
  async getPriceFeedsUpdateData(priceIds) {
281
385
  const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
282
386
  encoding: "hex",
package/dist/evm/evm.d.ts CHANGED
@@ -1,12 +1,11 @@
1
- import type { HexString, UnixTimestamp } from "@pythnetwork/hermes-client";
2
- import { HermesClient } from "@pythnetwork/hermes-client";
1
+ import type { HermesClient, HexString, UnixTimestamp } from "@pythnetwork/hermes-client";
3
2
  import type { Logger } from "pino";
4
3
  import type { IPricePusher, PriceInfo, PriceItem } from "../interface.js";
4
+ import { ChainPriceListener } from "../interface.js";
5
5
  import type { DurationInSeconds } from "../utils.js";
6
- import { CustomGasStation } from "./custom-gas-station";
6
+ import type { GasPriceConfig } from "./gas-price.js";
7
7
  import type { PythContract } from "./pyth-contract.js";
8
8
  import type { SuperWalletClient } from "./super-wallet.js";
9
- import { ChainPriceListener } from "../interface.js";
10
9
  export declare class EvmPriceListener extends ChainPriceListener {
11
10
  private pythContract;
12
11
  private watchEvents;
@@ -27,13 +26,19 @@ export declare class EvmPricePusher implements IPricePusher {
27
26
  private overrideGasPriceMultiplier;
28
27
  private overrideGasPriceMultiplierCap;
29
28
  private updateFeeMultiplier;
29
+ private gasPriceConfig;
30
30
  private gasLimit?;
31
- private customGasStation?;
32
- private gasPrice?;
31
+ private receiptWaitTimeoutMs;
32
+ private receiptPollIntervalMs;
33
33
  private pusherAddress;
34
34
  private lastPushAttempt;
35
- constructor(hermesClient: HermesClient, client: SuperWalletClient, pythContract: PythContract, logger: Logger, overrideGasPriceMultiplier: number, overrideGasPriceMultiplierCap: number, updateFeeMultiplier: number, gasLimit?: number | undefined, customGasStation?: CustomGasStation | undefined, gasPrice?: number | undefined);
35
+ private receiptTrackers;
36
+ private trackedNonce;
37
+ constructor(hermesClient: HermesClient, client: SuperWalletClient, pythContract: PythContract, logger: Logger, overrideGasPriceMultiplier: number, overrideGasPriceMultiplierCap: number, updateFeeMultiplier: number, gasPriceConfig: GasPriceConfig, gasLimit?: number | undefined, receiptWaitTimeoutMs?: number, receiptPollIntervalMs?: number);
36
38
  updatePriceFeed(priceIds: string[], pubTimesToPush: UnixTimestamp[]): Promise<void>;
37
- private waitForTransactionReceipt;
39
+ dispose(): void;
40
+ private trackTransactionReceipt;
41
+ private pollTransactionReceipt;
42
+ private interruptibleSleep;
38
43
  private getPriceFeedsUpdateData;
39
44
  }
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: Object.getOwnPropertyDescriptor(all, name).get
9
+ });
10
+ }
11
+ _export(exports, {
12
+ get describeGasPrice () {
13
+ return describeGasPrice;
14
+ },
15
+ get escalateGasPrice () {
16
+ return escalateGasPrice;
17
+ },
18
+ get gasPriceStrategies () {
19
+ return gasPriceStrategies;
20
+ },
21
+ get gasPriceToTxParams () {
22
+ return gasPriceToTxParams;
23
+ },
24
+ get getGasPrice () {
25
+ return getGasPrice;
26
+ },
27
+ get scaleGasPrice () {
28
+ return scaleGasPrice;
29
+ }
30
+ });
31
+ const gasPriceStrategies = [
32
+ "eip1559",
33
+ "legacy"
34
+ ];
35
+ const getGasPrice = async (client, config, logger)=>{
36
+ if (config.strategy === "legacy") {
37
+ const gasPrice = config.gasPrice !== undefined ? BigInt(Math.ceil(config.gasPrice)) : await config.customGasStation?.getCustomGasPrice() || await client.getGasPrice();
38
+ return {
39
+ gasPrice,
40
+ strategy: "legacy"
41
+ };
42
+ }
43
+ // Source the fee from the chain base fee and an estimated priority fee
44
+ // instead of the deprecated eth_gasPrice RPC (which some RPCs mishandle).
45
+ // We pad the base fee to leave headroom for increases between blocks and
46
+ // apply a multiplier to the priority fee, mirroring the gas model in Fortuna.
47
+ const block = await client.getBlock({
48
+ blockTag: "latest"
49
+ });
50
+ if (block.baseFeePerGas == null) {
51
+ throw new Error("The chain does not report a base fee per gas, so it does not support " + "eip1559 transactions. Re-run with `--gas-pricing-strategy legacy`.");
52
+ }
53
+ const priorityFee = scaleBigInt(await client.estimateMaxPriorityFeePerGas(), config.priorityFeeMultiplier);
54
+ const maxFeePerGas = scaleBigInt(block.baseFeePerGas, config.baseFeeMultiplier) + priorityFee;
55
+ logger.debug(`Estimated baseFeePerGas=${block.baseFeePerGas} priorityFee=${priorityFee}`);
56
+ return {
57
+ maxFeePerGas,
58
+ maxPriorityFeePerGas: priorityFee,
59
+ strategy: "eip1559"
60
+ };
61
+ };
62
+ const scaleGasPrice = (gasPrice, factor)=>{
63
+ if (gasPrice.strategy === "legacy") {
64
+ return {
65
+ gasPrice: scaleBigInt(gasPrice.gasPrice, factor),
66
+ strategy: "legacy"
67
+ };
68
+ }
69
+ return {
70
+ maxFeePerGas: scaleBigInt(gasPrice.maxFeePerGas, factor),
71
+ maxPriorityFeePerGas: scaleBigInt(gasPrice.maxPriorityFeePerGas, factor),
72
+ strategy: "eip1559"
73
+ };
74
+ };
75
+ const escalateGasPrice = (fresh, escalated, cap)=>{
76
+ // The strategy is fixed for the lifetime of the pusher, so fresh and escalated
77
+ // always share it. The guards keep the discriminated union narrow.
78
+ if (fresh.strategy === "legacy" && escalated.strategy === "legacy") {
79
+ return {
80
+ gasPrice: capComponent(fresh.gasPrice, escalated.gasPrice, cap),
81
+ strategy: "legacy"
82
+ };
83
+ }
84
+ if (fresh.strategy === "eip1559" && escalated.strategy === "eip1559") {
85
+ // Cap the total spend via maxFeePerGas. The priority fee (tip) only needs
86
+ // to be raised enough to outbid the stuck tx; it is bounded by the capped
87
+ // maxFeePerGas (the EIP-1559 invariant priority <= maxFee), NOT by the fresh
88
+ // tip scaled by `cap` — when the market tip drops, that would stop the tip
89
+ // from escalating and leave the replacement underpriced.
90
+ const maxFeePerGas = capComponent(fresh.maxFeePerGas, escalated.maxFeePerGas, cap);
91
+ const bumpedPriority = maxBigInt(escalated.maxPriorityFeePerGas, fresh.maxPriorityFeePerGas);
92
+ return {
93
+ maxFeePerGas,
94
+ maxPriorityFeePerGas: minBigInt(bumpedPriority, maxFeePerGas),
95
+ strategy: "eip1559"
96
+ };
97
+ }
98
+ return fresh;
99
+ };
100
+ // Raise `fresh` to at least `escalated`, but never above `fresh * cap`.
101
+ const capComponent = (fresh, escalated, cap)=>minBigInt(maxBigInt(escalated, fresh), scaleBigInt(fresh, cap));
102
+ const maxBigInt = (a, b)=>a > b ? a : b;
103
+ const minBigInt = (a, b)=>a < b ? a : b;
104
+ const gasPriceToTxParams = (gasPrice)=>gasPrice.strategy === "legacy" ? {
105
+ gasPrice: gasPrice.gasPrice
106
+ } : {
107
+ maxFeePerGas: gasPrice.maxFeePerGas,
108
+ maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas
109
+ };
110
+ const describeGasPrice = (gasPrice)=>gasPrice.strategy === "legacy" ? `gasPrice=${gasPrice.gasPrice}` : `maxFeePerGas=${gasPrice.maxFeePerGas} maxPriorityFeePerGas=${gasPrice.maxPriorityFeePerGas}`;
111
+ // Scale a bigint by a floating point factor using fixed-point arithmetic to
112
+ // avoid precision loss on large wei values.
113
+ const SCALE_PRECISION = 1_000_000_000n;
114
+ const scaleBigInt = (value, factor)=>value * BigInt(Math.round(factor * Number(SCALE_PRECISION))) / SCALE_PRECISION;
@@ -0,0 +1,33 @@
1
+ import type { Logger } from "pino";
2
+ import type { CustomGasStation } from "./custom-gas-station.js";
3
+ import type { SuperWalletClient } from "./super-wallet.js";
4
+ export declare const gasPriceStrategies: readonly ["eip1559", "legacy"];
5
+ export type GasPriceStrategy = (typeof gasPriceStrategies)[number];
6
+ export type GasPrice = {
7
+ strategy: "legacy";
8
+ gasPrice: bigint;
9
+ } | {
10
+ strategy: "eip1559";
11
+ maxFeePerGas: bigint;
12
+ maxPriorityFeePerGas: bigint;
13
+ };
14
+ export type GasPriceConfig = {
15
+ strategy: GasPriceStrategy;
16
+ baseFeeMultiplier: number;
17
+ priorityFeeMultiplier: number;
18
+ gasPrice?: number | undefined;
19
+ customGasStation?: CustomGasStation | undefined;
20
+ };
21
+ export declare const getGasPrice: (client: SuperWalletClient, config: GasPriceConfig, logger: Logger) => Promise<GasPrice>;
22
+ export declare const scaleGasPrice: (gasPrice: GasPrice, factor: number) => GasPrice;
23
+ export declare const escalateGasPrice: (fresh: GasPrice, escalated: GasPrice, cap: number) => GasPrice;
24
+ export declare const gasPriceToTxParams: (gasPrice: GasPrice) => {
25
+ gasPrice: bigint;
26
+ maxFeePerGas?: never;
27
+ maxPriorityFeePerGas?: never;
28
+ } | {
29
+ maxFeePerGas: bigint;
30
+ maxPriorityFeePerGas: bigint;
31
+ gasPrice?: never;
32
+ };
33
+ export declare const describeGasPrice: (gasPrice: GasPrice) => string;
@@ -11,7 +11,7 @@ Object.defineProperty(exports, "createPythContract", {
11
11
  const _viem = require("viem");
12
12
  const _pythabi = require("./pyth-abi.cjs");
13
13
  const createPythContract = (client, address)=>(0, _viem.getContract)({
14
- client,
15
14
  abi: _pythabi.PythAbi,
16
- address
15
+ address,
16
+ client
17
17
  });
@@ -56,9 +56,9 @@ function _interop_require_wildcard(obj, nodeInterop) {
56
56
  const UNKNOWN_CHAIN_CONFIG = {
57
57
  name: "Unknown",
58
58
  nativeCurrency: {
59
+ decimals: 18,
59
60
  name: "Unknown",
60
- symbol: "Unknown",
61
- decimals: 18
61
+ symbol: "Unknown"
62
62
  },
63
63
  rpcUrls: {
64
64
  default: {
@@ -83,8 +83,8 @@ const createClient = async (endpoint, mnemonic)=>{
83
83
  transport
84
84
  }).getChainId();
85
85
  return (0, _viem.createWalletClient)({
86
- transport,
87
86
  account: (0, _accounts.mnemonicToAccount)(mnemonic),
88
- chain: getChainById(chainId)
87
+ chain: getChainById(chainId),
88
+ transport
89
89
  }).extend(_viem.publicActions);
90
90
  };
@@ -1,3 +1,3 @@
1
- import type { Account, Chain, Client, RpcSchema, WalletActions, PublicActions, Transport } from "viem";
1
+ import type { Account, Chain, Client, PublicActions, RpcSchema, Transport, WalletActions } from "viem";
2
2
  export type SuperWalletClient = Client<Transport, Chain, Account, RpcSchema, PublicActions<Transport, Chain, Account> & WalletActions<Chain, Account>>;
3
3
  export declare const createClient: (endpoint: string, mnemonic: string) => Promise<SuperWalletClient>;
@@ -12,12 +12,12 @@ const _nodefs = /*#__PURE__*/ _interop_require_default(require("node:fs"));
12
12
  const _hermesclient = require("@pythnetwork/hermes-client");
13
13
  const _fuels = require("fuels");
14
14
  const _pino = /*#__PURE__*/ _interop_require_default(require("pino"));
15
+ const _controller = require("../controller.cjs");
15
16
  const _options = /*#__PURE__*/ _interop_require_wildcard(require("../options.cjs"));
16
17
  const _priceconfig = require("../price-config.cjs");
17
18
  const _pythpricelistener = require("../pyth-price-listener.cjs");
18
- const _fuel = require("./fuel.cjs");
19
- const _controller = require("../controller.cjs");
20
19
  const _utils = require("../utils.cjs");
20
+ const _fuel = require("./fuel.cjs");
21
21
  function _interop_require_default(obj) {
22
22
  return obj && obj.__esModule ? obj : {
23
23
  default: obj
@@ -65,41 +65,44 @@ function _interop_require_wildcard(obj, nodeInterop) {
65
65
  return newObj;
66
66
  }
67
67
  const _default = {
68
- command: "fuel",
69
- describe: "run price pusher for Fuel",
70
68
  builder: {
71
69
  endpoint: {
72
70
  description: "Fuel RPC API endpoint",
73
- type: "string",
74
- required: true
71
+ required: true,
72
+ type: "string"
75
73
  },
76
74
  "private-key-file": {
77
75
  description: "Path to the private key file",
78
- type: "string",
79
- required: true
76
+ required: true,
77
+ type: "string"
80
78
  },
81
79
  "pyth-contract-address": {
82
80
  description: "Pyth contract address on Fuel",
83
- type: "string",
84
- required: true
81
+ required: true,
82
+ type: "string"
85
83
  },
86
84
  ..._options.priceConfigFile,
87
85
  ..._options.priceServiceEndpoint,
86
+ ..._options.hermesAccessToken,
88
87
  ..._options.pushingFrequency,
89
88
  ..._options.pollingFrequency,
90
89
  ..._options.logLevel,
91
90
  ..._options.controllerLogLevel
92
91
  },
92
+ command: "fuel",
93
+ describe: "run price pusher for Fuel",
93
94
  handler: async function(argv) {
94
- const { endpoint, privateKeyFile, pythContractAddress, priceConfigFile, priceServiceEndpoint, pushingFrequency, pollingFrequency, logLevel, controllerLogLevel } = argv;
95
+ const { endpoint, privateKeyFile, pythContractAddress, priceConfigFile, priceServiceEndpoint, hermesAccessToken, pushingFrequency, pollingFrequency, logLevel, controllerLogLevel } = argv;
95
96
  const logger = (0, _pino.default)({
96
97
  level: logLevel
97
98
  });
98
99
  const priceConfigs = (0, _priceconfig.readPriceConfigFile)(priceConfigFile);
99
- const hermesClient = new _hermesclient.HermesClient(priceServiceEndpoint);
100
+ const hermesClient = new _hermesclient.HermesClient(priceServiceEndpoint, {
101
+ accessToken: hermesAccessToken
102
+ });
100
103
  let priceItems = priceConfigs.map(({ id, alias })=>({
101
- id,
102
- alias
104
+ alias,
105
+ id
103
106
  }));
104
107
  // Better to filter out invalid price items before creating the pyth listener
105
108
  const { existingPriceItems, invalidPriceItems } = await (0, _utils.filterInvalidPriceItems)(hermesClient, priceItems);