@pythnetwork/price-pusher 10.4.0 → 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 +25 -6
- package/dist/common.cjs +0 -3
- package/dist/common.d.ts +0 -4
- package/dist/controller.cjs +10 -5
- package/dist/evm/command.cjs +49 -3
- package/dist/evm/command.d.ts +5 -0
- package/dist/evm/evm.cjs +155 -51
- package/dist/evm/evm.d.ts +11 -5
- package/dist/evm/gas-price.cjs +114 -0
- package/dist/evm/gas-price.d.ts +33 -0
- package/dist/metrics.cjs +46 -24
- package/dist/metrics.d.ts +10 -5
- package/dist/price-config.cjs +2 -2
- package/dist/pyth-price-listener.cjs +3 -1
- package/dist/solana/solana.cjs +5 -5
- package/dist/sui/balance-tracker.cjs +2 -2
- package/dist/sui/balance-tracker.d.ts +4 -4
- package/dist/sui/command.cjs +44 -10
- package/dist/sui/command.d.ts +3 -0
- package/dist/sui/sui.cjs +149 -96
- package/dist/sui/sui.d.ts +29 -12
- package/package.json +14 -7
package/README.md
CHANGED
|
@@ -104,8 +104,19 @@ pnpm run start evm --endpoint wss://example-rpc.com \
|
|
|
104
104
|
[--override-gas-price-multiplier 1.1] \
|
|
105
105
|
[--override-gas-price-multiplier-cap 5] \
|
|
106
106
|
[--gas-limit 1000000] \
|
|
107
|
+
[--gas-pricing-strategy eip1559] \
|
|
108
|
+
[--base-fee-multiplier 1.2] \
|
|
109
|
+
[--priority-fee-multiplier 1] \
|
|
107
110
|
[--gas-price 160000000]
|
|
108
111
|
|
|
112
|
+
# The EVM pusher prices transactions with the `eip1559` strategy by default. It
|
|
113
|
+
# computes `maxFeePerGas` from the chain base fee (padded by `--base-fee-multiplier`
|
|
114
|
+
# to leave headroom for the base fee growing between blocks) plus an estimated
|
|
115
|
+
# priority fee (scaled by `--priority-fee-multiplier`). This avoids the deprecated
|
|
116
|
+
# `eth_gasPrice` RPC, which some RPCs mishandle. For chains that do not support
|
|
117
|
+
# EIP-1559 transactions, pass `--gas-pricing-strategy legacy`, which falls back to
|
|
118
|
+
# the single `gasPrice` model and where `--gas-price` / `--custom-gas-station` apply.
|
|
119
|
+
|
|
109
120
|
# For Injective
|
|
110
121
|
pnpm run start injective --grpc-endpoint https://grpc-endpoint.com \
|
|
111
122
|
--pyth-contract-address inj1z60tg0... --price-service-endpoint "https://example-hermes-rpc.com" \
|
|
@@ -129,19 +140,27 @@ pnpm run start aptos --endpoint https://fullnode.testnet.aptoslabs.com/v1 \
|
|
|
129
140
|
|
|
130
141
|
# For Sui
|
|
131
142
|
pnpm run start sui \
|
|
132
|
-
--endpoint https://
|
|
133
|
-
--
|
|
134
|
-
--pyth-
|
|
135
|
-
--
|
|
136
|
-
--wormhole-
|
|
137
|
-
--
|
|
143
|
+
--endpoint https://fullnode.testnet.sui.io:443 \
|
|
144
|
+
--network testnet \
|
|
145
|
+
--pyth-package-id 0xabf837e98c26087cba0883c0a7a28326b1fa3c5e1e2c5abdb486f9e8f594c837 \
|
|
146
|
+
--pyth-state-id 0x243759059f4c3111179da5878c12f68d612c21a8d54d85edc86164bb18be1c7c \
|
|
147
|
+
--wormhole-package-id 0x21473617f3565d704aa67be73ea41243e9e34a42d434c31f8182c67ba01ccf49 \
|
|
148
|
+
--wormhole-state-id 0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790 \
|
|
149
|
+
--price-feed-to-price-info-object-table-id 0xcb858b77d8068c6c8c0d8a4ddfba95053268e4a31f8ecc49adccc4ec1570d3a7 \
|
|
138
150
|
--price-service-endpoint https://example-hermes-rpc.com \
|
|
139
151
|
--mnemonic-file ./mnemonic \
|
|
140
152
|
--price-config-file ./price-config.beta.sample.yaml \
|
|
153
|
+
[--endpoint-type json-rpc] \
|
|
141
154
|
[--pushing-frequency 10] \
|
|
142
155
|
[--polling-frequency 5] \
|
|
143
156
|
[--num-gas-objects 30]
|
|
144
157
|
|
|
158
|
+
# `--endpoint-type` selects the Sui RPC transport: `json-rpc` (default) or `grpc`.
|
|
159
|
+
# Sui Foundation is deprecating JSON-RPC (public endpoints off July 2026, removed
|
|
160
|
+
# from full nodes by mid-Oct 2026), so set `--endpoint-type grpc` and point
|
|
161
|
+
# `--endpoint` at a gRPC-web base URL to migrate. `grpc` uses @mysten/sui's
|
|
162
|
+
# experimental SuiGrpcClient.
|
|
163
|
+
|
|
145
164
|
# For Near
|
|
146
165
|
pnpm run start near \
|
|
147
166
|
--node-url https://rpc.testnet.near.org \
|
package/dist/common.cjs
CHANGED
package/dist/common.d.ts
CHANGED
package/dist/controller.cjs
CHANGED
|
@@ -28,6 +28,9 @@ class Controller {
|
|
|
28
28
|
this.metrics = config.metrics;
|
|
29
29
|
// Set the number of price feeds if metrics are enabled
|
|
30
30
|
this.metrics?.setPriceFeedsTotal(this.priceConfigs.length);
|
|
31
|
+
// Report unhealthy on /live if no loop iteration completes within a few
|
|
32
|
+
// pushing cycles (min 60s), so the k8s livenessProbe restarts a hung pod.
|
|
33
|
+
this.metrics?.setLivenessThresholdSeconds(Math.max(this.pushingFrequency * 5, 60));
|
|
31
34
|
}
|
|
32
35
|
async start() {
|
|
33
36
|
// start the listeners
|
|
@@ -49,13 +52,13 @@ class Controller {
|
|
|
49
52
|
const targetLatestPrice = this.targetPriceListener.getLatestPriceInfo(priceId);
|
|
50
53
|
const sourceLatestPrice = this.sourcePriceListener.getLatestPriceInfo(priceId);
|
|
51
54
|
if (this.metrics && targetLatestPrice && sourceLatestPrice) {
|
|
52
|
-
this.metrics.updateTimestamps(
|
|
53
|
-
this.metrics.updatePriceValues(
|
|
55
|
+
this.metrics.updateTimestamps(alias, targetLatestPrice.publishTime, sourceLatestPrice.publishTime, priceConfig.timeDifference);
|
|
56
|
+
this.metrics.updatePriceValues(alias, sourceLatestPrice.price, targetLatestPrice.price);
|
|
54
57
|
}
|
|
55
58
|
const priceShouldUpdate = (0, _priceconfig.shouldUpdate)(priceConfig, sourceLatestPrice, targetLatestPrice, this.logger);
|
|
56
59
|
// Record update condition in metrics
|
|
57
60
|
if (this.metrics) {
|
|
58
|
-
this.metrics.recordUpdateCondition(
|
|
61
|
+
this.metrics.recordUpdateCondition(alias, priceShouldUpdate);
|
|
59
62
|
}
|
|
60
63
|
if (priceShouldUpdate == _priceconfig.UpdateCondition.YES) {
|
|
61
64
|
pushThresholdMet = true;
|
|
@@ -80,7 +83,7 @@ class Controller {
|
|
|
80
83
|
if (this.metrics) {
|
|
81
84
|
for (const config of pricesToPush){
|
|
82
85
|
const triggerValue = (0, _priceconfig.shouldUpdate)(config, this.sourcePriceListener.getLatestPriceInfo(config.id), this.targetPriceListener.getLatestPriceInfo(config.id), this.logger) === _priceconfig.UpdateCondition.YES ? "yes" : "early";
|
|
83
|
-
this.metrics.recordPriceUpdate(config.
|
|
86
|
+
this.metrics.recordPriceUpdate(config.alias, triggerValue);
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
89
|
} catch (error) {
|
|
@@ -92,13 +95,15 @@ class Controller {
|
|
|
92
95
|
if (this.metrics) {
|
|
93
96
|
for (const config of pricesToPush){
|
|
94
97
|
const triggerValue = (0, _priceconfig.shouldUpdate)(config, this.sourcePriceListener.getLatestPriceInfo(config.id), this.targetPriceListener.getLatestPriceInfo(config.id), this.logger) === _priceconfig.UpdateCondition.YES ? "yes" : "early";
|
|
95
|
-
this.metrics.recordPriceUpdateError(config.
|
|
98
|
+
this.metrics.recordPriceUpdateError(config.alias, triggerValue);
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
} else {
|
|
100
103
|
this.logger.info("None of the checks were triggered. No push needed.");
|
|
101
104
|
}
|
|
105
|
+
// Liveness heartbeat: mark this loop iteration as completed.
|
|
106
|
+
this.metrics?.recordLoopIteration();
|
|
102
107
|
await (0, _utils.sleep)(this.pushingFrequency * 1000);
|
|
103
108
|
}
|
|
104
109
|
}
|
package/dist/evm/command.cjs
CHANGED
|
@@ -70,6 +70,12 @@ function _interop_require_wildcard(obj, nodeInterop) {
|
|
|
70
70
|
}
|
|
71
71
|
const _default = {
|
|
72
72
|
builder: {
|
|
73
|
+
"base-fee-multiplier": {
|
|
74
|
+
default: 1.2,
|
|
75
|
+
description: "[eip1559 strategy only] Multiplier applied to the chain base fee when computing " + "maxFeePerGas. Values above 1 leave headroom for the base fee to grow between blocks. " + "Default to 1.2",
|
|
76
|
+
required: false,
|
|
77
|
+
type: "number"
|
|
78
|
+
},
|
|
73
79
|
"custom-gas-station": {
|
|
74
80
|
description: "If using a custom gas station, chainId of custom gas station to use",
|
|
75
81
|
required: false,
|
|
@@ -86,10 +92,19 @@ const _default = {
|
|
|
86
92
|
type: "number"
|
|
87
93
|
},
|
|
88
94
|
"gas-price": {
|
|
89
|
-
description: "Override the gas price that would be received from the RPC",
|
|
95
|
+
description: "[legacy strategy only] Override the gas price (in wei) that would be received from the RPC",
|
|
90
96
|
required: false,
|
|
91
97
|
type: "number"
|
|
92
98
|
},
|
|
99
|
+
"gas-pricing-strategy": {
|
|
100
|
+
choices: [
|
|
101
|
+
"eip1559",
|
|
102
|
+
"legacy"
|
|
103
|
+
],
|
|
104
|
+
default: "eip1559",
|
|
105
|
+
description: "How to price transactions. 'eip1559' (default and recommended) sources the " + "gas from the chain base fee and a priority fee, which is more robust as it " + "avoids the deprecated eth_gasPrice RPC. Use 'legacy' for chains that do not " + "support eip1559 transactions.",
|
|
106
|
+
required: false
|
|
107
|
+
},
|
|
93
108
|
"override-gas-price-multiplier": {
|
|
94
109
|
default: 1.1,
|
|
95
110
|
description: "Multiply the previous gas price by this number if the transaction is not landing to override. " + "Please note that the gas price can grow exponentially on consecutive failures; " + "to set a cap on the multiplier, use the `override-gas-price-multiplier-cap` option." + "Default to 1.1",
|
|
@@ -102,6 +117,24 @@ const _default = {
|
|
|
102
117
|
required: false,
|
|
103
118
|
type: "number"
|
|
104
119
|
},
|
|
120
|
+
"priority-fee-multiplier": {
|
|
121
|
+
default: 1,
|
|
122
|
+
description: "[eip1559 strategy only] Multiplier applied to the priority fee (tip) estimated from " + "the RPC. Default to 1",
|
|
123
|
+
required: false,
|
|
124
|
+
type: "number"
|
|
125
|
+
},
|
|
126
|
+
"receipt-poll-interval": {
|
|
127
|
+
default: 2,
|
|
128
|
+
description: "How often (in seconds) to poll for a sent transaction's receipt while waiting for " + "it to land. Default to 2",
|
|
129
|
+
required: false,
|
|
130
|
+
type: "number"
|
|
131
|
+
},
|
|
132
|
+
"receipt-wait-timeout": {
|
|
133
|
+
default: 60,
|
|
134
|
+
description: "Maximum time (in seconds) to poll for a sent transaction's receipt before giving " + "up. Bounds the wait so a transaction that never lands cannot be polled forever. " + "Default to 60",
|
|
135
|
+
required: false,
|
|
136
|
+
type: "number"
|
|
137
|
+
},
|
|
105
138
|
"tx-speed": {
|
|
106
139
|
choices: [
|
|
107
140
|
"slow",
|
|
@@ -133,7 +166,7 @@ const _default = {
|
|
|
133
166
|
describe: "run price pusher for evm",
|
|
134
167
|
handler: async function(argv) {
|
|
135
168
|
// FIXME: type checks for this
|
|
136
|
-
const { endpoint, priceConfigFile, priceServiceEndpoint, hermesAccessToken, mnemonicFile, pythContractAddress, pushingFrequency, pollingFrequency, customGasStation, txSpeed, overrideGasPriceMultiplier, overrideGasPriceMultiplierCap, gasLimit, gasPrice, updateFeeMultiplier, logLevel, controllerLogLevel, enableMetrics, metricsPort } = argv;
|
|
169
|
+
const { endpoint, priceConfigFile, priceServiceEndpoint, hermesAccessToken, mnemonicFile, pythContractAddress, pushingFrequency, pollingFrequency, customGasStation, txSpeed, overrideGasPriceMultiplier, overrideGasPriceMultiplierCap, gasLimit, gasPricingStrategy, gasPrice, baseFeeMultiplier, priorityFeeMultiplier, updateFeeMultiplier, receiptPollInterval, receiptWaitTimeout, logLevel, controllerLogLevel, enableMetrics, metricsPort } = argv;
|
|
137
170
|
const logger = (0, _pino.default)({
|
|
138
171
|
level: logLevel
|
|
139
172
|
});
|
|
@@ -179,9 +212,22 @@ const _default = {
|
|
|
179
212
|
const gasStation = (0, _customgasstation.getCustomGasStation)(logger.child({
|
|
180
213
|
module: "CustomGasStation"
|
|
181
214
|
}), customGasStation, txSpeed);
|
|
215
|
+
if (gasPricingStrategy === "eip1559" && (gasStation || gasPrice)) {
|
|
216
|
+
logger.warn("`--custom-gas-station`/`--tx-speed` and `--gas-price` only apply to the 'legacy' " + "gas pricing strategy and will be ignored. Use `--base-fee-multiplier` and " + "`--priority-fee-multiplier` to tune the 'eip1559' strategy.");
|
|
217
|
+
}
|
|
218
|
+
const gasPriceConfig = {
|
|
219
|
+
baseFeeMultiplier,
|
|
220
|
+
customGasStation: gasStation,
|
|
221
|
+
gasPrice,
|
|
222
|
+
priorityFeeMultiplier,
|
|
223
|
+
strategy: gasPricingStrategy
|
|
224
|
+
};
|
|
182
225
|
const evmPusher = new _evm.EvmPricePusher(hermesClient, client, pythContract, logger.child({
|
|
183
226
|
module: "EvmPricePusher"
|
|
184
|
-
}), overrideGasPriceMultiplier, overrideGasPriceMultiplierCap, updateFeeMultiplier,
|
|
227
|
+
}), overrideGasPriceMultiplier, overrideGasPriceMultiplierCap, updateFeeMultiplier, gasPriceConfig, gasLimit, // CLI values are in seconds; the pusher expects milliseconds. Defaults
|
|
228
|
+
// (60s / 2s) reproduce the pre-flag behaviour, so existing deployments
|
|
229
|
+
// that don't set these flags are unaffected.
|
|
230
|
+
receiptWaitTimeout * 1000, receiptPollInterval * 1000);
|
|
185
231
|
const controller = new _controller.Controller(priceConfigs, pythListener, evmListener, evmPusher, logger.child({
|
|
186
232
|
module: "Controller"
|
|
187
233
|
}, {
|
package/dist/evm/command.d.ts
CHANGED
|
@@ -12,12 +12,17 @@ declare const _default: {
|
|
|
12
12
|
"hermes-access-token": Options;
|
|
13
13
|
"price-service-endpoint": Options;
|
|
14
14
|
"price-config-file": Options;
|
|
15
|
+
"base-fee-multiplier": Options;
|
|
15
16
|
"custom-gas-station": Options;
|
|
16
17
|
endpoint: Options;
|
|
17
18
|
"gas-limit": Options;
|
|
18
19
|
"gas-price": Options;
|
|
20
|
+
"gas-pricing-strategy": Options;
|
|
19
21
|
"override-gas-price-multiplier": Options;
|
|
20
22
|
"override-gas-price-multiplier-cap": Options;
|
|
23
|
+
"priority-fee-multiplier": Options;
|
|
24
|
+
"receipt-poll-interval": Options;
|
|
25
|
+
"receipt-wait-timeout": Options;
|
|
21
26
|
"tx-speed": Options;
|
|
22
27
|
"update-fee-multiplier": Options;
|
|
23
28
|
};
|
package/dist/evm/evm.cjs
CHANGED
|
@@ -19,6 +19,14 @@ _export(exports, {
|
|
|
19
19
|
const _viem = require("viem");
|
|
20
20
|
const _interface = require("../interface.cjs");
|
|
21
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;
|
|
@@ -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,14 +172,16 @@ 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
|
|
@@ -166,7 +196,7 @@ class EvmPricePusher {
|
|
|
166
196
|
pubTimesToPushParam
|
|
167
197
|
], {
|
|
168
198
|
gas: this.gasLimit === undefined ? undefined : BigInt(Math.ceil(this.gasLimit)),
|
|
169
|
-
|
|
199
|
+
...(0, _gasprice.gasPriceToTxParams)(gasPrice),
|
|
170
200
|
nonce: txNonce,
|
|
171
201
|
value: updateFee
|
|
172
202
|
});
|
|
@@ -174,17 +204,17 @@ class EvmPricePusher {
|
|
|
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
|
@@ -3,7 +3,7 @@ import type { Logger } from "pino";
|
|
|
3
3
|
import type { IPricePusher, PriceInfo, PriceItem } from "../interface.js";
|
|
4
4
|
import { ChainPriceListener } from "../interface.js";
|
|
5
5
|
import type { DurationInSeconds } from "../utils.js";
|
|
6
|
-
import type {
|
|
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
9
|
export declare class EvmPriceListener extends ChainPriceListener {
|
|
@@ -26,13 +26,19 @@ export declare class EvmPricePusher implements IPricePusher {
|
|
|
26
26
|
private overrideGasPriceMultiplier;
|
|
27
27
|
private overrideGasPriceMultiplierCap;
|
|
28
28
|
private updateFeeMultiplier;
|
|
29
|
+
private gasPriceConfig;
|
|
29
30
|
private gasLimit?;
|
|
30
|
-
private
|
|
31
|
-
private
|
|
31
|
+
private receiptWaitTimeoutMs;
|
|
32
|
+
private receiptPollIntervalMs;
|
|
32
33
|
private pusherAddress;
|
|
33
34
|
private lastPushAttempt;
|
|
34
|
-
|
|
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);
|
|
35
38
|
updatePriceFeed(priceIds: string[], pubTimesToPush: UnixTimestamp[]): Promise<void>;
|
|
36
|
-
|
|
39
|
+
dispose(): void;
|
|
40
|
+
private trackTransactionReceipt;
|
|
41
|
+
private pollTransactionReceipt;
|
|
42
|
+
private interruptibleSleep;
|
|
37
43
|
private getPriceFeedsUpdateData;
|
|
38
44
|
}
|