@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.
- package/README.md +28 -7
- package/dist/aptos/aptos.cjs +7 -7
- package/dist/aptos/aptos.d.ts +1 -1
- package/dist/aptos/balance-tracker.cjs +7 -7
- package/dist/aptos/balance-tracker.d.ts +1 -1
- package/dist/aptos/command.cjs +20 -17
- package/dist/aptos/command.d.ts +3 -2
- package/dist/common.cjs +0 -3
- package/dist/common.d.ts +0 -4
- package/dist/controller.cjs +12 -7
- package/dist/controller.d.ts +1 -1
- package/dist/evm/balance-tracker.cjs +4 -4
- package/dist/evm/balance-tracker.d.ts +2 -2
- package/dist/evm/command.cjs +89 -40
- package/dist/evm/command.d.ts +12 -6
- package/dist/evm/evm.cjs +162 -58
- package/dist/evm/evm.d.ts +13 -8
- package/dist/evm/gas-price.cjs +114 -0
- package/dist/evm/gas-price.d.ts +33 -0
- package/dist/evm/pyth-contract.cjs +2 -2
- package/dist/evm/super-wallet.cjs +4 -4
- package/dist/evm/super-wallet.d.ts +1 -1
- package/dist/fuel/command.cjs +17 -14
- package/dist/fuel/command.d.ts +3 -2
- package/dist/fuel/fuel.d.ts +2 -2
- package/dist/injective/command.cjs +26 -23
- package/dist/injective/command.d.ts +5 -4
- package/dist/injective/injective.cjs +11 -11
- package/dist/injective/injective.d.ts +2 -3
- package/dist/interface.d.ts +1 -1
- package/dist/metrics.cjs +57 -35
- package/dist/metrics.d.ts +10 -5
- package/dist/near/command.cjs +22 -19
- package/dist/near/command.d.ts +5 -4
- package/dist/near/near.cjs +17 -16
- package/dist/near/near.d.ts +1 -1
- package/dist/options.cjs +36 -26
- package/dist/options.d.ts +3 -0
- package/dist/price-config.cjs +15 -15
- package/dist/pyth-price-listener.cjs +5 -3
- package/dist/pyth-price-listener.d.ts +2 -3
- package/dist/solana/balance-tracker.cjs +5 -5
- package/dist/solana/balance-tracker.d.ts +2 -2
- package/dist/solana/command.cjs +75 -60
- package/dist/solana/command.d.ts +13 -10
- package/dist/solana/solana.cjs +5 -5
- package/dist/solana/solana.d.ts +4 -4
- package/dist/sui/balance-tracker.cjs +6 -6
- package/dist/sui/balance-tracker.d.ts +5 -5
- package/dist/sui/command.cjs +84 -47
- package/dist/sui/command.d.ts +10 -6
- package/dist/sui/sui.cjs +159 -106
- package/dist/sui/sui.d.ts +31 -14
- package/dist/ton/command.cjs +17 -14
- package/dist/ton/command.d.ts +3 -2
- package/dist/ton/ton.d.ts +2 -3
- package/dist/utils.d.ts +1 -2
- 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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
93
|
-
|
|
101
|
+
receiptWaitTimeoutMs;
|
|
102
|
+
receiptPollIntervalMs;
|
|
94
103
|
pusherAddress;
|
|
95
104
|
lastPushAttempt;
|
|
96
|
-
|
|
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.
|
|
106
|
-
this.
|
|
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
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
// support
|
|
136
|
-
let gasPrice =
|
|
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
|
|
175
|
+
gasPriceToOverride = (0, _gasprice.scaleGasPrice)(this.lastPushAttempt.gasPrice, this.overrideGasPriceMultiplier);
|
|
148
176
|
}
|
|
149
177
|
}
|
|
150
|
-
if (gasPriceToOverride !== undefined
|
|
151
|
-
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
169
|
-
|
|
198
|
+
gas: this.gasLimit === undefined ? undefined : BigInt(Math.ceil(this.gasLimit)),
|
|
199
|
+
...(0, _gasprice.gasPriceToTxParams)(gasPrice),
|
|
170
200
|
nonce: txNonce,
|
|
171
|
-
|
|
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.
|
|
207
|
+
this.logger.debug({
|
|
178
208
|
hash
|
|
179
209
|
}, "Price update sent");
|
|
180
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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 {
|
|
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
|
|
32
|
-
private
|
|
31
|
+
private receiptWaitTimeoutMs;
|
|
32
|
+
private receiptPollIntervalMs;
|
|
33
33
|
private pusherAddress;
|
|
34
34
|
private lastPushAttempt;
|
|
35
|
-
|
|
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
|
-
|
|
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,
|
|
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>;
|
package/dist/fuel/command.cjs
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
71
|
+
required: true,
|
|
72
|
+
type: "string"
|
|
75
73
|
},
|
|
76
74
|
"private-key-file": {
|
|
77
75
|
description: "Path to the private key file",
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
required: true,
|
|
77
|
+
type: "string"
|
|
80
78
|
},
|
|
81
79
|
"pyth-contract-address": {
|
|
82
80
|
description: "Pyth contract address on Fuel",
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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);
|