@rhinestone/service-sdk 0.2.11 → 0.2.13
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/dist/chains.d.ts +6 -1
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +444 -47
- package/dist/tipClamp.d.ts +21 -0
- package/dist/tipClamp.d.ts.map +1 -0
- package/dist/tipClamp.js +30 -0
- package/dist/viemDefaults.d.ts +20 -0
- package/dist/viemDefaults.d.ts.map +1 -0
- package/dist/viemDefaults.js +22 -0
- package/package.json +1 -1
package/dist/chains.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FeeValuesEIP1559, PublicClient } from 'viem';
|
|
2
2
|
/** Client methods required to estimate gas fees. */
|
|
3
|
-
export type FeeEstimationClient = Pick<PublicClient, 'estimateFeesPerGas' | 'getFeeHistory'
|
|
3
|
+
export type FeeEstimationClient = Pick<PublicClient, 'estimateFeesPerGas' | 'getFeeHistory'> & Partial<Pick<PublicClient, 'chain'>>;
|
|
4
4
|
/** Options for {@link estimateGasFees}. */
|
|
5
5
|
export type EstimateGasFeesOptions = {
|
|
6
6
|
/**
|
|
@@ -16,6 +16,11 @@ export type EstimateGasFeesOptions = {
|
|
|
16
16
|
/**
|
|
17
17
|
* Estimates gas fees for a chain and returns EIP-1559-shaped fee values.
|
|
18
18
|
*
|
|
19
|
+
* `maxFeePerGas` is built from the NEXT block's base fee (not the last mined
|
|
20
|
+
* one) times a per-chain multiplier, plus the priority fee. The orchestrator
|
|
21
|
+
* bills this number, so unused headroom is charged to the user — see
|
|
22
|
+
* BASE_FEE_MULTIPLIER_PCT.
|
|
23
|
+
*
|
|
19
24
|
* Pass `{ mode: 'legacy' }` to source the estimate from the RPC's legacy gas
|
|
20
25
|
* price. The price is used for both EIP-1559 fee caps so it remains the
|
|
21
26
|
* effective-price ceiling. On a no-priority local fork where the base fee
|
package/dist/chains.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chains.d.ts","sourceRoot":"","sources":["../src/chains.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"chains.d.ts","sourceRoot":"","sources":["../src/chains.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAmB,YAAY,EAAE,MAAM,MAAM,CAAA;AAmB3E,oDAAoD;AAMpD,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,YAAY,EACZ,oBAAoB,GAAG,eAAe,CACvC,GACC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAA;AAEtC,2CAA2C;AAC3C,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAA;CAChB,CAAA;AAyUD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,gBAAgB,CAAC,CA2C3B"}
|
package/dist/chains.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.estimateGasFees = estimateGasFees;
|
|
4
|
+
const api_1 = require("@opentelemetry/api");
|
|
4
5
|
const chains_1 = require("viem/chains");
|
|
6
|
+
const tipClamp_1 = require("./tipClamp");
|
|
7
|
+
const viemDefaults_1 = require("./viemDefaults");
|
|
5
8
|
// Chains that price fees from a windowed eth_feeHistory instead of viem's
|
|
6
9
|
// default estimator. Two membership rationales:
|
|
7
10
|
// - volatile-base L1s (mainnet, sepolia): windowing the next-block base fee
|
|
@@ -20,35 +23,90 @@ const HISTORICAL_FEE_CHAINS = new Set([
|
|
|
20
23
|
]);
|
|
21
24
|
const DEFAULT_FEE_CONFIG = {
|
|
22
25
|
historyBlocks: 20, // Geth FullNodeGPO window
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
// Matches the viem-path default rather than the old 2.0x. Every chain on the
|
|
27
|
+
// historical path overrides this today (and Plasma's base fee is 0, so it is
|
|
28
|
+
// inert there), but leaving 2.0x here would silently hand the next chain added
|
|
29
|
+
// to HISTORICAL_FEE_CHAINS exactly the over-charge this PR removes.
|
|
30
|
+
baseFeeMultiplierPct: 105n,
|
|
31
|
+
tipPercentile: 50, // calm-market per-block tip; per-chain blocks override it
|
|
25
32
|
congestedTipPercentile: 75, // escalate to p75 under load (not p90 — ~45x cost)
|
|
26
33
|
congestionRatio: 0.5, // "gasUsedRatio trends > 0.5"
|
|
27
34
|
windowTipPercentile: 50, // nearest-rank p50 across the window's per-block tips
|
|
28
35
|
// 0.1 gwei. Enough to clear the zero-tip stall (the tip is the only builder
|
|
29
|
-
// incentive — base fee is burned)
|
|
30
|
-
//
|
|
36
|
+
// incentive — base fee is burned) without over-buffering. Chains that fill
|
|
37
|
+
// often enough to measure set their own floor from realized data; this is the
|
|
38
|
+
// fallback for one that does not.
|
|
31
39
|
tipFloorWei: 100000000n,
|
|
32
40
|
};
|
|
33
41
|
// Per-chain overrides for the windowed feeHistory path; any omitted field falls
|
|
34
42
|
// back to DEFAULT_FEE_CONFIG.
|
|
35
43
|
const FEE_CONFIG_OVERRIDES = {
|
|
36
|
-
// Testnet
|
|
44
|
+
// Testnet: no tip floor — testnet inclusion is
|
|
45
|
+
// flaky and the tip is self-consistent (we bid it, we pay it, so it does not
|
|
46
|
+
// skew charged-vs-actual). The base multiplier is the part that was
|
|
47
|
+
// over-charging: billing 2.0x base while paying 1.0x put sepolia at A/C 0.70,
|
|
48
|
+
// the same defect as every other chain. Its measured base dispersion is 1.33x,
|
|
49
|
+
// so 1.30x sits just under the observed max rather than on the mean. The tip came
|
|
50
|
+
// down from p75 for the same reason: a 7d block replay puts sepolia's p75 at
|
|
51
|
+
// 1.0 gwei against a 0.0375 market p50, which alone held it at A/C 0.50.
|
|
37
52
|
[chains_1.sepolia.id]: {
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
baseFeeMultiplierPct: 130n,
|
|
54
|
+
tipPercentile: 50,
|
|
55
|
+
congestedTipPercentile: 75,
|
|
40
56
|
tipFloorWei: 0n,
|
|
41
57
|
},
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
58
|
+
// Mainnet's reward distribution is inflated by MEV-boost blocks — searchers
|
|
59
|
+
// pay enormous priority fees, so the feeHistory p50 prices our plain fills
|
|
60
|
+
// against searcher competition: 7d prod shows charged tip 0.152 gwei vs
|
|
61
|
+
// 0.011 realized (13.8x). Sample p25 and floor at the realized clearing
|
|
62
|
+
// price (the floor still clears the zero-tip stall — the tip is the only
|
|
63
|
+
// builder incentive on L1). Base is separate, and mainnet is the one chain
|
|
64
|
+
// where quote-time base is a poor predictor of fill-time base: 7d prod shows a
|
|
65
|
+
// charged base of 0.2199 at the old 2.0x (so a 0.110 base at quote) against a
|
|
66
|
+
// 0.1647 realized base — a measured 1.50x drift between quote and fill. The
|
|
67
|
+
// multiplier has to cover that drift, not the next block: 1.60x sits above the
|
|
68
|
+
// 1.50x mean so the cap still clears at fill time, and lands at A/C 0.95.
|
|
69
|
+
[chains_1.mainnet.id]: {
|
|
70
|
+
baseFeeMultiplierPct: 160n,
|
|
71
|
+
tipPercentile: 25,
|
|
72
|
+
congestedTipPercentile: 50,
|
|
73
|
+
tipFloorWei: 11000000n, // 0.011 gwei — the realized clearing tip; ~2x the 0.0054 market p50
|
|
74
|
+
},
|
|
75
|
+
// Plasma is on the historical path for its broken tip oracle, not a volatile
|
|
76
|
+
// base (its base fee is 0). Its reward distribution is a step function, not a
|
|
77
|
+
// gradient — probed live 2026-07-24 over 40 blocks:
|
|
78
|
+
// p1..p10 = 0 p15..p50 = 0.005 gwei
|
|
79
|
+
// so every percentile from 15 up reports the same 0.005, while real fills
|
|
80
|
+
// clear near 0.0014 (third-party solvers are included on-chain at that price
|
|
81
|
+
// ~10.7k times a day, so it demonstrably clears). Sampling anywhere above p10
|
|
82
|
+
// therefore prices at 0.005 and the floor never binds — the tip must be read
|
|
83
|
+
// at p10, where the window median is genuinely 0, so the floor below sets the
|
|
84
|
+
// price.
|
|
50
85
|
[chains_1.plasma.id]: {
|
|
51
|
-
|
|
86
|
+
// Explicit, not inherited. Plasma's base fee is ~0 almost always, so this
|
|
87
|
+
// multiplier is free — but "almost" matters: a 7d block replay puts 0.4% of
|
|
88
|
+
// quotes under the base fee one block later at 1.05x, and 1.5% ten blocks
|
|
89
|
+
// later, against 0% at 2.0x. On ~15k bundles/day priced through this chain
|
|
90
|
+
// that is real. Inheriting the shared default would silently re-introduce it.
|
|
91
|
+
baseFeeMultiplierPct: 200n,
|
|
92
|
+
tipPercentile: 10,
|
|
93
|
+
congestedTipPercentile: 25,
|
|
94
|
+
// 0.0014 gwei — where fills on this chain ACTUALLY clear, re-measured from
|
|
95
|
+
// prod on 2026-07-27 once the previous 0.0017 had deployed and could be
|
|
96
|
+
// checked against reality (it billed 1.25x the clearing price).
|
|
97
|
+
//
|
|
98
|
+
// Measured from the solver that fills ~10.7k plasma bundles/day: it is
|
|
99
|
+
// included at 0.00103 (7d median 0.001364). Inclusion at ~0.001 is therefore
|
|
100
|
+
// demonstrated tens of thousands of times a day. NOTE the on-chain reward
|
|
101
|
+
// percentiles disagree and must not be used here — being gas-weighted they
|
|
102
|
+
// read p25..p90 = 0.005, which is what a minority of gas-heavy transactions
|
|
103
|
+
// pay, not the bar for inclusion.
|
|
104
|
+
//
|
|
105
|
+
// Lowering it cannot hurt includability: plasma's base is ~0, so the cap is
|
|
106
|
+
// `base * 2.0 + floor` and the floor never gates it — the 7d replay reports
|
|
107
|
+
// a 0.00% unincludable rate for every floor from 0.0017 down to 0.0008, at
|
|
108
|
+
// every lag from 1 to 50 blocks. The floor only sets what we CHARGE.
|
|
109
|
+
tipFloorWei: 1400000n,
|
|
52
110
|
},
|
|
53
111
|
};
|
|
54
112
|
// Chains where viem's default base-fee estimate is fine but its tip oracle
|
|
@@ -61,21 +119,182 @@ const TIP_FLOOR_WEI = {
|
|
|
61
119
|
[chains_1.base.id]: 3000000n, // 0.003 gwei — oracle ~0.001 (network ~p25); real fills ~0.003-0.0036 (probed 2026-07-08)
|
|
62
120
|
[chains_1.bsc.id]: 50000000n, // 0.05 gwei — post-Maxwell chain-min; oracle intermittently returns 0 through some upstreams, and with BSC's zero base fee that zeroes the ENTIRE bid (probed 2026-07-13)
|
|
63
121
|
[chains_1.polygon.id]: 45000000000n, // 45 gwei — Bor min 30 gwei x1.5; real p50 tip ~200 gwei
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
122
|
+
// 100 wei. Gnosis operates at a scale where gwei is the wrong unit to think
|
|
123
|
+
// in: its median base fee is 118 WEI (p10 24, p90 278k), and included-tx tips
|
|
124
|
+
// are 0 at p50 and p75. The old 1.0 gwei floor was ~8,500,000x the median base
|
|
125
|
+
// fee; an earlier revision of this PR cut it to 100_000 wei, which was still
|
|
126
|
+
// 850x the base and left the floor as the dominant term in the whole quote —
|
|
127
|
+
// it alone pinned Gnosis at A/C 0.00 no matter what the base multiplier did.
|
|
128
|
+
//
|
|
129
|
+
// 21 wei — viem's own oracle reading on this chain, so the floor effectively
|
|
130
|
+
// never binds and simply guards a zero-reporting upstream. Note the BSC
|
|
131
|
+
// failure mode (a 0 oracle zeroing the ENTIRE bid) cannot occur here: Gnosis'
|
|
132
|
+
// base fee is nonzero, so `base * mult` alone keeps the bid includable.
|
|
133
|
+
//
|
|
134
|
+
// Deliberately NOT sized for congestion. Gnosis does have busy episodes
|
|
135
|
+
// needing ~263,000 wei, but no static floor is the right tool — it would
|
|
136
|
+
// over-charge the calm majority without covering the tail. If that tail ever
|
|
137
|
+
// matters (it does not at 3 fills/wk), the fix is moving Gnosis to
|
|
138
|
+
// HISTORICAL_FEE_CHAINS for the windowed path's congestion escalation, not a
|
|
139
|
+
// bigger floor.
|
|
140
|
+
[chains_1.gnosis.id]: 21n,
|
|
141
|
+
// Left at its existing value deliberately. HyperEVM over-charges the tip by
|
|
142
|
+
// ~16x (0.158 charged vs 0.00953 realized) but under-charges the base (0.189
|
|
143
|
+
// charged vs 0.354 realized), and the two errors cancel to an overall A/C of
|
|
144
|
+
// 1.05. Correcting only the tip would turn that balance into a ~1.65
|
|
145
|
+
// under-charge, so the tip stays and the base carries the correction instead
|
|
146
|
+
// — see BASE_FEE_MULTIPLIER_PCT_OVERRIDES. Revisit with a fill-side base probe.
|
|
147
|
+
[chains_1.hyperEvm.id]: 150000000n, // 0.15 gwei
|
|
148
|
+
// 3 gwei — feeHistory p50 2.0 / p75 3.0 of included txs, over 2,100 blocks
|
|
149
|
+
// spanning 5.1d (2026-07-24). This contradicts the previous comment's
|
|
150
|
+
// "competitive tip p50/mode ~22 gwei" (2026-06-25); both claim a p50 and they
|
|
151
|
+
// differ 7x. The old figure is not reproducible against current chain data.
|
|
152
|
+
// Monad rides the shared 1.05x base multiplier with no cushion, so an error
|
|
153
|
+
// here surfaces directly; the lag replay puts it at 0% under-capped at every
|
|
154
|
+
// lag tested.
|
|
155
|
+
[chains_1.monad.id]: 3000000000n,
|
|
67
156
|
};
|
|
68
157
|
// Chains whose real tip drifts above the static floor in busy markets — also
|
|
69
158
|
// window the feeHistory tip and take the max with the floor. Polygon: real tip
|
|
70
159
|
// runs ~6.7x its oracle. Base: realized tip drifts above the floor while the
|
|
71
160
|
// oracle stays pinned — see TIP_FLOOR_WEI for the measured values. Both have
|
|
72
|
-
// stable base fees,
|
|
161
|
+
// stable base fees, but the base still gets the shared next-block treatment.
|
|
73
162
|
const WINDOWED_TIP_CHAINS = new Set([chains_1.polygon.id, chains_1.base.id]);
|
|
163
|
+
// Upper bound on the priority fee, per chain.
|
|
164
|
+
//
|
|
165
|
+
// The counterpart to TIP_FLOOR_WEI: a floor can only ever RAISE a tip, so there
|
|
166
|
+
// was previously no way to correct a chain whose eth_maxPriorityFeePerGas
|
|
167
|
+
// OVER-reports. Several do, by a wide margin — the oracle quotes a price far
|
|
168
|
+
// above what transactions on that chain actually pay to get included.
|
|
169
|
+
//
|
|
170
|
+
// Each value is set from the strongest evidence that chain has. Where we fill
|
|
171
|
+
// often enough for our own realized tip to mean something (Optimism, Soneium,
|
|
172
|
+
// BSC) the cap sits at that realized price. Where we do not (Sonic, Avalanche:
|
|
173
|
+
// 3-5 fills/wk, too few to be anything but noise — Gnosis' realized price over
|
|
174
|
+
// 3 fills came out BELOW its own base fee, which is impossible) it is set from
|
|
175
|
+
// eth_feeHistory rewards, the tips included transactions actually paid, at
|
|
176
|
+
// roughly the market p75/p90 — demonstrably enough to be included, never the
|
|
177
|
+
// median.
|
|
178
|
+
const TIP_CAP_WEI = {
|
|
179
|
+
// Arbitrum, Unichain and Katana are deliberately absent: their charged tip
|
|
180
|
+
// already matches realized. Unichain and Katana sit at A/C 0.94/0.91, so a cap
|
|
181
|
+
// there would under-charge; Arbitrum's oracle returns 0 outright (38,387
|
|
182
|
+
// samples over 7d, all zero — it has no priority-fee auction), so a cap there
|
|
183
|
+
// would simply be a no-op.
|
|
184
|
+
[chains_1.optimism.id]: 400000n, // 0.0004 gwei — realized tip 0.00038 vs a flat 0.001 oracle quote
|
|
185
|
+
[chains_1.soneium.id]: 700000n, // 0.0007 gwei — realized tip 0.000706 across 1198 fills/wk (so this clamps a hair under realized), vs a flat 0.001 oracle quote
|
|
186
|
+
[chains_1.sonic.id]: 100000n, // 0.0001 gwei — deliberately 5x the 0.00002 market p90, since 3 fills/wk is too thin to trust; still a ~5000x cut from the 0.525 the oracle drove
|
|
187
|
+
[chains_1.avalanche.id]: 1000000n, // 0.001 gwei — market p75/p90; charged tip was 0.0117
|
|
188
|
+
[chains_1.bsc.id]: 55000000n, // 0.055 gwei — realized tip 0.0542, market p50 0.052; the oracle drifts to 0.0629 above the 0.05 chain-min
|
|
189
|
+
};
|
|
74
190
|
const WINDOWED_TIP_HISTORY_BLOCKS = 20;
|
|
191
|
+
// Stays at p50 for both windowed chains. Polygon looked like a candidate for a
|
|
192
|
+
// lower percentile, but its market p25 (167 gwei, probed 2026-07-24) is ABOVE
|
|
193
|
+
// what we already charge (~154) while our own fills clear at ~85 — our fills
|
|
194
|
+
// price below the entire included-tx distribution, which no percentile of that
|
|
195
|
+
// distribution can express.
|
|
75
196
|
const WINDOWED_TIP_PERCENTILE = 50; // p50 per block, nearest-rank across the window
|
|
197
|
+
// Base-fee headroom applied on the viem estimator path, as a percentage.
|
|
198
|
+
//
|
|
199
|
+
// This is pure CAP headroom against a base fee that rises between quote and
|
|
200
|
+
// submission: on-chain you only ever pay `base + tip`, so unused headroom is
|
|
201
|
+
// never spent — but the orchestrator BILLS this number (and sizes gas-refund
|
|
202
|
+
// ceilings and bridged native reserves from it), so over-buffering is charged
|
|
203
|
+
// to the user.
|
|
204
|
+
//
|
|
205
|
+
// It is applied to the NEXT block's base fee, which eth_feeHistory reports
|
|
206
|
+
// directly (the historical path has always used it). That matters: viem's own
|
|
207
|
+
// estimate scales the LATEST MINED block's base by 1.2x, and since EIP-1559
|
|
208
|
+
// allows the base to grow at most 12.5% per block, viem's 1.2x is really
|
|
209
|
+
// "one worst-case block (1.125x) plus ~6% margin" — not 20% of slack. Pricing
|
|
210
|
+
// off the next block's actual base instead of that worst case is where the
|
|
211
|
+
// over-charge disappears: 7d of prod fills show the stable-base chains on this
|
|
212
|
+
// path billing exactly 1.20x what they pay (Base 0.00602 charged vs 0.00501
|
|
213
|
+
// realized; Arbitrum 0.02412 vs 0.02008), because their blocks are not full
|
|
214
|
+
// and the worst case never materialises.
|
|
215
|
+
//
|
|
216
|
+
// 1.05x of the real next-block base keeps genuine headroom while removing that
|
|
217
|
+
// systematic over-charge. If the base outruns it, the relayer's tx queue
|
|
218
|
+
// re-prices and bumps unbounded (relayer/src/tx_queue/nonce-slot.ts, reprice /
|
|
219
|
+
// bumpedFees) — a latency cost, not an inclusion failure.
|
|
220
|
+
//
|
|
221
|
+
// That unbounded bump is a phase-1 posture, not a permanent property: the
|
|
222
|
+
// relayer omits `feeCeiling` and `gasPriceCap` "isn't wired from the intent
|
|
223
|
+
// yet" (relayer/src/tx_queue/config.ts). Once it is, `exceedsCap` flips the
|
|
224
|
+
// slot to `flipToCancelTx` and the fill is ABANDONED rather than delayed — and
|
|
225
|
+
// since that cap derives from the quoted maxFeePerGas, shrinking headroom
|
|
226
|
+
// shrinks the abandon threshold with it. Revisit this multiplier then.
|
|
227
|
+
const BASE_FEE_MULTIPLIER_PCT = 105n;
|
|
228
|
+
// Per-chain base-fee multiplier for the viem paths. Only HyperEVM needs one: its
|
|
229
|
+
// realized base runs ~2.25x the base quoted to us (0.354 vs 0.157, 7d prod),
|
|
230
|
+
// which no next-block reading can see. It is also the one chain whose two errors
|
|
231
|
+
// currently cancel — the tip over-charges ~16x and the base under-charges, netting
|
|
232
|
+
// A/C 1.05 — so dropping it to the shared 1.05 would unbalance it to 1.12. The
|
|
233
|
+
// tip is deliberately left alone for the same reason; 1.30x restores A/C ~1.00.
|
|
234
|
+
// Sized from a 7d block replay measuring, per chain, the share of quotes whose
|
|
235
|
+
// cap falls below the base fee of the block they would land in at a 1-5 block
|
|
236
|
+
// lag. The shared 1.05x holds every chain at 0% except these two.
|
|
237
|
+
const BASE_FEE_MULTIPLIER_PCT_OVERRIDES = {
|
|
238
|
+
[chains_1.hyperEvm.id]: 130n,
|
|
239
|
+
// Gnosis' tip is ~0 in calm markets (market p50 AND p75 are both zero), so
|
|
240
|
+
// its A/C is bounded almost entirely by this multiplier — we bill the cap, so
|
|
241
|
+
// A/C ~= 1/mult. That makes the multiplier the only real lever, and the
|
|
242
|
+
// question is how little headroom its base movement actually needs.
|
|
243
|
+
//
|
|
244
|
+
// Measured over 7d: 1.15x leaves 0% of quotes under-capped at one block, 1.7%
|
|
245
|
+
// at five and 4.3% at ten, at A/C 0.98. The shared 1.05x would be 14%/28%,
|
|
246
|
+
// which is why Gnosis needs an override at all.
|
|
247
|
+
//
|
|
248
|
+
// Be clear that this IS a trade, not a free win. main is 0% under-capped at
|
|
249
|
+
// every lag — but only because its 1.0 gwei tip floor (8,500,000x this chain's
|
|
250
|
+
// median base fee) accidentally supplied enormous cap headroom while pinning
|
|
251
|
+
// A/C at 0.00. Removing that floor is what surfaces the exposure. What we are
|
|
252
|
+
// buying: A/C 0.00 -> 0.98. What we are paying: a relayer re-price on roughly
|
|
253
|
+
// 1.7% of quotes, on a chain filling ~3x/week — about one bump every seven
|
|
254
|
+
// months, and a re-price is latency, not a lost fill.
|
|
255
|
+
//
|
|
256
|
+
// 1.30x is the conservative alternative: A/C 0.88 at 0.1%/0.2%. Prefer it if
|
|
257
|
+
// Gnosis volume ever becomes non-trivial.
|
|
258
|
+
[chains_1.gnosis.id]: 115n,
|
|
259
|
+
// 0.9% under-capped at one block and 8.1% at five, against 0% today. 1.15x
|
|
260
|
+
// returns it to 0% through a ten-block lag while still improving A/C from
|
|
261
|
+
// 0.70 to 0.86 — better than today on both axes, which 1.05x is not.
|
|
262
|
+
[chains_1.avalanche.id]: 115n,
|
|
263
|
+
};
|
|
264
|
+
const baseFeeMultiplierPct = (chainId) => BASE_FEE_MULTIPLIER_PCT_OVERRIDES[chainId] ?? BASE_FEE_MULTIPLIER_PCT;
|
|
265
|
+
/**
|
|
266
|
+
* Next-block base fee, as reported by eth_feeHistory (last entry).
|
|
267
|
+
*
|
|
268
|
+
* Returns `null` — never 0n — when the RPC omits it or returns an empty array:
|
|
269
|
+
* a 0 base would silently price the cap at the tip alone, which under-prices
|
|
270
|
+
* on every chain with a nonzero base. `null` routes the caller to viem's own
|
|
271
|
+
* (strictly more conservative) cap instead.
|
|
272
|
+
*/
|
|
273
|
+
async function nextBlockBaseFee(client, chainId) {
|
|
274
|
+
const feeHistory = await client.getFeeHistory({
|
|
275
|
+
blockCount: 1,
|
|
276
|
+
rewardPercentiles: [],
|
|
277
|
+
});
|
|
278
|
+
const baseFee = feeHistory.baseFeePerGas?.at(-1);
|
|
279
|
+
if (baseFee === undefined) {
|
|
280
|
+
// Recorded HERE rather than at the caller's `.catch`, because this is a
|
|
281
|
+
// SUCCESSFUL response carrying no base fee — it never throws, so the catch
|
|
282
|
+
// does not see it. That is the same failure class the windowed path records
|
|
283
|
+
// as `feehistory_missing_base`, and leaving it out would make the one case
|
|
284
|
+
// viem's transport retries cannot absorb the one case with no signal.
|
|
285
|
+
recordDegradedEstimate(chainId, 'feehistory_missing_base');
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return baseFee;
|
|
289
|
+
}
|
|
76
290
|
/**
|
|
77
291
|
* Estimates gas fees for a chain and returns EIP-1559-shaped fee values.
|
|
78
292
|
*
|
|
293
|
+
* `maxFeePerGas` is built from the NEXT block's base fee (not the last mined
|
|
294
|
+
* one) times a per-chain multiplier, plus the priority fee. The orchestrator
|
|
295
|
+
* bills this number, so unused headroom is charged to the user — see
|
|
296
|
+
* BASE_FEE_MULTIPLIER_PCT.
|
|
297
|
+
*
|
|
79
298
|
* Pass `{ mode: 'legacy' }` to source the estimate from the RPC's legacy gas
|
|
80
299
|
* price. The price is used for both EIP-1559 fee caps so it remains the
|
|
81
300
|
* effective-price ceiling. On a no-priority local fork where the base fee
|
|
@@ -96,47 +315,151 @@ async function estimateGasFees(client, chainId, options) {
|
|
|
96
315
|
}
|
|
97
316
|
if (HISTORICAL_FEE_CHAINS.has(chainId)) {
|
|
98
317
|
const config = { ...DEFAULT_FEE_CONFIG, ...FEE_CONFIG_OVERRIDES[chainId] };
|
|
99
|
-
return estimateFeeHistoryGasFees(client, config);
|
|
318
|
+
return estimateFeeHistoryGasFees(client, chainId, config);
|
|
100
319
|
}
|
|
101
320
|
const tipFloorWei = TIP_FLOOR_WEI[chainId];
|
|
102
321
|
if (tipFloorWei !== undefined) {
|
|
103
322
|
return estimateViemFeesWithTipFloor(client, chainId, tipFloorWei);
|
|
104
323
|
}
|
|
105
|
-
|
|
324
|
+
const [fees, baseFee] = await Promise.all([
|
|
325
|
+
client.estimateFeesPerGas(),
|
|
326
|
+
nextBlockBaseFee(client, chainId).catch(() => {
|
|
327
|
+
recordDegradedEstimate(chainId, 'feehistory_error');
|
|
328
|
+
return null;
|
|
329
|
+
}),
|
|
330
|
+
]);
|
|
331
|
+
const oracleTip = fees.maxPriorityFeePerGas;
|
|
332
|
+
// The cap applies here too: the chains whose oracle over-reports (Optimism,
|
|
333
|
+
// Soneium, Sonic, Avalanche) have no tip floor, so this is their only path.
|
|
334
|
+
const tip = (0, tipClamp_1.clampTip)(oracleTip, undefined, TIP_CAP_WEI[chainId]);
|
|
335
|
+
return {
|
|
336
|
+
maxFeePerGas: cappedMaxFeePerGas(client, fees, baseFee, chainId, tip, oracleTip),
|
|
337
|
+
maxPriorityFeePerGas: tip,
|
|
338
|
+
};
|
|
106
339
|
}
|
|
107
|
-
// viem's
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
340
|
+
// viem's oracle tip, lifted to an absolute floor — and, for windowed-tip chains,
|
|
341
|
+
// to the windowed feeHistory tip — then clamped down to any TIP_CAP_WEI. The tip
|
|
342
|
+
// is therefore raised OR lowered here; the cap applies first and the floor
|
|
343
|
+
// last, so a floor is never undercut by a cap.
|
|
344
|
+
//
|
|
345
|
+
// maxFeePerGas is rebuilt from the next block's base fee rather than adjusted
|
|
346
|
+
// off viem's, falling back to viem's own cap plus the tip delta when the base
|
|
347
|
+
// read fails.
|
|
112
348
|
async function estimateViemFeesWithTipFloor(client, chainId, tipFloorWei) {
|
|
113
|
-
|
|
349
|
+
// Windowed chains get their tip AND the next-block base from one 20-block
|
|
350
|
+
// feeHistory response: that response already carries `baseFeePerGas` with the
|
|
351
|
+
// next block's base as its last entry, so a second blockCount:1 call would
|
|
352
|
+
// re-fetch a number we are holding. Everyone else reads the base alone.
|
|
353
|
+
//
|
|
354
|
+
// Best-effort throughout: a feeHistory outage must never turn into a quote
|
|
355
|
+
// outage. On failure the tip falls back to max(oracle, floor) — exactly the
|
|
356
|
+
// pre-overlay pricing — and the cap to viem's own, which is strictly more
|
|
357
|
+
// conservative, never less.
|
|
358
|
+
const [fees, history] = await Promise.all([
|
|
359
|
+
client.estimateFeesPerGas(),
|
|
360
|
+
(WINDOWED_TIP_CHAINS.has(chainId)
|
|
361
|
+
? windowedFeeHistoryTip(client, chainId)
|
|
362
|
+
: nextBlockBaseFee(client, chainId).then((baseFee) => ({
|
|
363
|
+
tip: 0n,
|
|
364
|
+
baseFee,
|
|
365
|
+
}))).catch(() => {
|
|
366
|
+
recordDegradedEstimate(chainId, 'feehistory_error');
|
|
367
|
+
return { tip: 0n, baseFee: null };
|
|
368
|
+
}),
|
|
369
|
+
]);
|
|
114
370
|
const oracleTip = fees.maxPriorityFeePerGas;
|
|
115
371
|
let tip = oracleTip;
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// max(oracle, floor), which is exactly the pre-overlay pricing.
|
|
120
|
-
const windowedTip = await windowedFeeHistoryTip(client).catch(() => 0n);
|
|
121
|
-
if (windowedTip > tip)
|
|
122
|
-
tip = windowedTip;
|
|
123
|
-
}
|
|
124
|
-
if (tipFloorWei > tip)
|
|
125
|
-
tip = tipFloorWei;
|
|
372
|
+
if (history.tip > tip)
|
|
373
|
+
tip = history.tip;
|
|
374
|
+
tip = (0, tipClamp_1.clampTip)(tip, tipFloorWei, TIP_CAP_WEI[chainId]);
|
|
126
375
|
return {
|
|
127
|
-
maxFeePerGas: fees.
|
|
376
|
+
maxFeePerGas: cappedMaxFeePerGas(client, fees, history.baseFee, chainId, tip, oracleTip),
|
|
128
377
|
maxPriorityFeePerGas: tip,
|
|
129
378
|
};
|
|
130
379
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
380
|
+
/**
|
|
381
|
+
* The base-fee multiplier viem ACTUALLY applied, as a percentage, read from the
|
|
382
|
+
* client's own chain config exactly as viem reads it
|
|
383
|
+
* (`chain?.fees?.baseFeeMultiplier ?? 1.2`). Used to invert viem's cap on the
|
|
384
|
+
* degraded path, so the reconstruction cannot drift from the viem the CONSUMER
|
|
385
|
+
* is running (viem is a `^2` peer range, so it need not match the version CI
|
|
386
|
+
* pins) or from a consumer that sets a custom multiplier on its chain.
|
|
387
|
+
*
|
|
388
|
+
* Falls back to {@link VIEM_DEFAULT_BASE_FEE_MULTIPLIER_PCT} only when the value
|
|
389
|
+
* is not a plain number — i.e. absent, or viem's function form, which viem
|
|
390
|
+
* evaluates with a block/request context this path does not have. No supported
|
|
391
|
+
* chain uses the function form, and that fallback is exactly today's behaviour,
|
|
392
|
+
* so it is a no-op regression-wise.
|
|
393
|
+
*/
|
|
394
|
+
function viemBaseFeeMultiplierPct(client) {
|
|
395
|
+
const m = client.chain?.fees?.baseFeeMultiplier;
|
|
396
|
+
// Number.isFinite, not just `>= 1`: Infinity passes `>= 1` and would make
|
|
397
|
+
// BigInt(Math.round(m * 100)) throw, crashing the estimate on a degraded path.
|
|
398
|
+
// (NaN is already excluded by `>= 1` being false, but this states it.) An
|
|
399
|
+
// invalid multiplier falls back to the pinned default rather than crashing.
|
|
400
|
+
if (typeof m === 'number' && Number.isFinite(m) && m >= 1) {
|
|
401
|
+
return BigInt(Math.round(m * 100));
|
|
402
|
+
}
|
|
403
|
+
return viemDefaults_1.VIEM_DEFAULT_BASE_FEE_MULTIPLIER_PCT;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Cap = nextBlockBaseFee * multiplier + tip.
|
|
407
|
+
*
|
|
408
|
+
* When the base fee could not be read, falls back to viem's own cap — but only
|
|
409
|
+
* where that is genuinely more conservative. viem builds its cap as
|
|
410
|
+
* `latestBase * 1.2 + tip`, so it is more conservative ONLY for chains whose
|
|
411
|
+
* multiplier is at or below 1.2x. For the ones above it (Gnosis 2.0x, HyperEVM
|
|
412
|
+
* 1.30x) viem's cap is LOWER than intended — measured at 62% and 93% of the
|
|
413
|
+
* healthy cap — which on Gnosis reopens exactly the under-capping its 2.0x
|
|
414
|
+
* override exists to close, precisely when the RPC is unhealthy and base fees
|
|
415
|
+
* move. For those chains we recover viem's implied base and re-apply our own
|
|
416
|
+
* multiplier, taking whichever cap is higher.
|
|
417
|
+
*/
|
|
418
|
+
function cappedMaxFeePerGas(client, fees, baseFee, chainId, tip, oracleTip) {
|
|
419
|
+
const pct = baseFeeMultiplierPct(chainId);
|
|
420
|
+
if (baseFee === null) {
|
|
421
|
+
const viemPct = viemBaseFeeMultiplierPct(client);
|
|
422
|
+
const viemCap = fees.maxFeePerGas + (tip - oracleTip);
|
|
423
|
+
let cap = viemCap;
|
|
424
|
+
if (pct > viemPct) {
|
|
425
|
+
const impliedBase = ((fees.maxFeePerGas - oracleTip) * 100n) / viemPct;
|
|
426
|
+
const scaled = (impliedBase * pct) / 100n + tip;
|
|
427
|
+
if (scaled > cap)
|
|
428
|
+
cap = scaled;
|
|
429
|
+
}
|
|
430
|
+
// Never return below the tip itself. viem's default guarantees
|
|
431
|
+
// maxFeePerGas >= oracleTip, so `viemCap` is non-negative and this cannot
|
|
432
|
+
// bind today — but the guarantee is viem's, not ours, and an estimator that
|
|
433
|
+
// broke it would otherwise make BOTH arms negative (a `maxFeePerGas <=
|
|
434
|
+
// oracleTip` precondition would not help: it routes to viemCap, which is
|
|
435
|
+
// exactly the negative value).
|
|
436
|
+
return cap > tip ? cap : tip;
|
|
437
|
+
}
|
|
438
|
+
return (baseFee * pct) / 100n + tip;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Nearest-rank p50 across the window's per-block p50 tips — used to track a busy
|
|
442
|
+
* market that the static oracle/floor lags — plus the next block's base fee,
|
|
443
|
+
* which the same response already carries as the last `baseFeePerGas` entry.
|
|
444
|
+
*
|
|
445
|
+
* `baseFee` is `null`, never `0n`, when that array is missing or empty, for the
|
|
446
|
+
* same reason as {@link nextBlockBaseFee}: a 0 base would price the cap at the
|
|
447
|
+
* tip alone.
|
|
448
|
+
*/
|
|
449
|
+
async function windowedFeeHistoryTip(client, chainId) {
|
|
134
450
|
const feeHistory = await client.getFeeHistory({
|
|
135
451
|
blockCount: WINDOWED_TIP_HISTORY_BLOCKS,
|
|
136
452
|
rewardPercentiles: [WINDOWED_TIP_PERCENTILE],
|
|
137
453
|
});
|
|
138
454
|
const perBlockTips = (feeHistory.reward ?? []).map((r) => r?.[0] ?? 0n);
|
|
139
|
-
|
|
455
|
+
const baseFee = feeHistory.baseFeePerGas?.at(-1);
|
|
456
|
+
if (baseFee === undefined) {
|
|
457
|
+
recordDegradedEstimate(chainId, 'feehistory_missing_base');
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
tip: percentileBigInt(perBlockTips, WINDOWED_TIP_PERCENTILE),
|
|
461
|
+
baseFee: baseFee ?? null,
|
|
462
|
+
};
|
|
140
463
|
}
|
|
141
464
|
// Windowed eth_feeHistory estimator:
|
|
142
465
|
// tip = `windowTipPercentile` across the last N blocks' per-block
|
|
@@ -144,8 +467,59 @@ async function windowedFeeHistoryTip(client) {
|
|
|
144
467
|
// (or `congestedTipPercentile` when the window's mean
|
|
145
468
|
// gasUsedRatio exceeds `congestionRatio`), floored at
|
|
146
469
|
// `tipFloorWei`.
|
|
147
|
-
// maxFeePerGas = nextBlockBaseFee *
|
|
148
|
-
|
|
470
|
+
// maxFeePerGas = nextBlockBaseFee * baseFeeMultiplierPct / 100 + tip
|
|
471
|
+
/**
|
|
472
|
+
* Records that a quote was produced by a DEGRADED path — eth_feeHistory failed
|
|
473
|
+
* or answered without a base fee, so the cap came from viem rather than from the
|
|
474
|
+
* next block's actual base.
|
|
475
|
+
*
|
|
476
|
+
* This exists because the degradation is otherwise invisible. Every fallback
|
|
477
|
+
* here is deliberately silent at the call site (a feeHistory outage must never
|
|
478
|
+
* become a quote outage), which means a broken upstream would quietly revert
|
|
479
|
+
* these chains to viem's 1.2x-of-latest pricing — the exact over-charge this
|
|
480
|
+
* module exists to remove — and nothing would say so. The estimate would still
|
|
481
|
+
* look healthy; it would just be wrong.
|
|
482
|
+
*
|
|
483
|
+
* Written to the active span rather than a logger: this package cannot reach
|
|
484
|
+
* the orchestrator's logger, and the orchestrator already wraps these calls in
|
|
485
|
+
* `withSpan('getGasFeeComponents')`, so the attributes land in Tempo without any
|
|
486
|
+
* wiring. `@opentelemetry/api` is a no-op when no SDK is registered, so this
|
|
487
|
+
* costs nothing for consumers who do not instrument.
|
|
488
|
+
*/
|
|
489
|
+
function recordDegradedEstimate(chainId, reason) {
|
|
490
|
+
const span = api_1.trace.getActiveSpan();
|
|
491
|
+
if (!span)
|
|
492
|
+
return;
|
|
493
|
+
span.setAttribute('gas.estimate.degraded', true);
|
|
494
|
+
span.setAttribute('gas.estimate.degraded_reason', reason);
|
|
495
|
+
span.setAttribute('gas.estimate.chain_id', chainId);
|
|
496
|
+
span.addEvent('gas.estimate.degraded', { reason, 'chain.id': chainId });
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Cap for the windowed estimator when eth_feeHistory answered without a base
|
|
500
|
+
* fee. Recovers the base implied by viem's own estimate and re-applies this
|
|
501
|
+
* chain's multiplier, never returning less than viem's cap or the tip itself.
|
|
502
|
+
*
|
|
503
|
+
* Deliberately does NOT adopt viem's tip — see the call site.
|
|
504
|
+
*/
|
|
505
|
+
async function viemFallbackCap(client, config, priorityFee) {
|
|
506
|
+
const fees = await client.estimateFeesPerGas();
|
|
507
|
+
const oracleTip = fees.maxPriorityFeePerGas;
|
|
508
|
+
const viemCap = fees.maxFeePerGas + (priorityFee - oracleTip);
|
|
509
|
+
let cap = viemCap;
|
|
510
|
+
if (fees.maxFeePerGas > oracleTip) {
|
|
511
|
+
// Same reconstruction as cappedMaxFeePerGas: invert by the multiplier viem
|
|
512
|
+
// ACTUALLY applied for this consumer, not the pinned default — viem is a
|
|
513
|
+
// peer range and a consumer chain may set its own baseFeeMultiplier.
|
|
514
|
+
const viemPct = viemBaseFeeMultiplierPct(client);
|
|
515
|
+
const impliedBase = ((fees.maxFeePerGas - oracleTip) * 100n) / viemPct;
|
|
516
|
+
const scaled = (impliedBase * config.baseFeeMultiplierPct) / 100n;
|
|
517
|
+
if (scaled + priorityFee > cap)
|
|
518
|
+
cap = scaled + priorityFee;
|
|
519
|
+
}
|
|
520
|
+
return cap > priorityFee ? cap : priorityFee;
|
|
521
|
+
}
|
|
522
|
+
async function estimateFeeHistoryGasFees(client, chainId, config) {
|
|
149
523
|
const feeHistory = await client.getFeeHistory({
|
|
150
524
|
blockCount: config.historyBlocks,
|
|
151
525
|
rewardPercentiles: [config.tipPercentile, config.congestedTipPercentile],
|
|
@@ -161,8 +535,31 @@ async function estimateFeeHistoryGasFees(client, config) {
|
|
|
161
535
|
const priorityFee = windowedTip > config.tipFloorWei ? windowedTip : config.tipFloorWei;
|
|
162
536
|
// baseFeePerGas has historyBlocks + 1 entries; the last is the *next* block's
|
|
163
537
|
// base fee (the one our tx lands in), not the latest mined block's.
|
|
164
|
-
|
|
165
|
-
|
|
538
|
+
//
|
|
539
|
+
// Optional chaining, not a bare `.at()`: an absent array would otherwise throw
|
|
540
|
+
// a TypeError here rather than reaching the fallback below.
|
|
541
|
+
const baseFee = feeHistory.baseFeePerGas?.at(-1);
|
|
542
|
+
if (baseFee === undefined) {
|
|
543
|
+
// The RPC answered without telling us the base fee. Defaulting to 0n would
|
|
544
|
+
// price the cap at the tip alone — on mainnet an unincludable bid AND a ~20x
|
|
545
|
+
// under-charge, silently — so that is not an option. Throwing is not a good
|
|
546
|
+
// one either: this is a successful HTTP 200 carrying bad data, so viem's
|
|
547
|
+
// transport retries never fire, and the chains on this path include Plasma
|
|
548
|
+
// at ~69k fills/wk. So fall back for the BASE only.
|
|
549
|
+
//
|
|
550
|
+
// Keeping `priorityFee` is the point. These chains are on the windowed path
|
|
551
|
+
// precisely because their tip oracle is untrustworthy (Plasma's reports
|
|
552
|
+
// 0.005 gwei against a measured 0.0014), so falling back to viem wholesale
|
|
553
|
+
// would trade a hard failure for a silent ~3.5x overcharge on the chain
|
|
554
|
+
// carrying the most fills through our pricing. The tip we already computed
|
|
555
|
+
// is the calibrated one.
|
|
556
|
+
recordDegradedEstimate(chainId, 'feehistory_missing_base');
|
|
557
|
+
return {
|
|
558
|
+
maxFeePerGas: await viemFallbackCap(client, config, priorityFee),
|
|
559
|
+
maxPriorityFeePerGas: priorityFee,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
const maxFeePerGas = (baseFee * config.baseFeeMultiplierPct) / 100n + priorityFee;
|
|
166
563
|
return { maxFeePerGas, maxPriorityFeePerGas: priorityFee };
|
|
167
564
|
}
|
|
168
565
|
// Nearest-rank percentile over a bigint sample. Empty sample → 0n.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies a chain's tip cap and floor, in the order that makes the FLOOR win.
|
|
3
|
+
*
|
|
4
|
+
* The cap is measured from what included transactions actually pay, so it is
|
|
5
|
+
* the tighter evidence of what a chain requires. The floor is a safety guard,
|
|
6
|
+
* and on BSC it is the chain's post-Maxwell protocol MINIMUM — a bid below it
|
|
7
|
+
* is rejected outright. So the cap applies first and the floor last, making
|
|
8
|
+
* `result >= floor` hold for any cap value, including a cap below the floor.
|
|
9
|
+
*
|
|
10
|
+
* No chain has a cap below its floor today (BSC is the only chain in both
|
|
11
|
+
* tables: cap 0.055, floor 0.05), which is exactly why this ordering has to be
|
|
12
|
+
* pinned by a test rather than by the current data.
|
|
13
|
+
*
|
|
14
|
+
* This lives in its own module rather than in `chains.ts` because `./chains` is
|
|
15
|
+
* a public package export — anything exported from there becomes SDK API, and
|
|
16
|
+
* consumers must not be able to depend on cap/floor ordering internals. This
|
|
17
|
+
* module has no `exports` subpath, so it is importable by the test and not by
|
|
18
|
+
* consumers.
|
|
19
|
+
*/
|
|
20
|
+
export declare function clampTip(tip: bigint, floorWei: bigint | undefined, capWei: bigint | undefined): bigint;
|
|
21
|
+
//# sourceMappingURL=tipClamp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tipClamp.d.ts","sourceRoot":"","sources":["../src/tipClamp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,MAAM,CAKR"}
|
package/dist/tipClamp.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clampTip = clampTip;
|
|
4
|
+
/**
|
|
5
|
+
* Applies a chain's tip cap and floor, in the order that makes the FLOOR win.
|
|
6
|
+
*
|
|
7
|
+
* The cap is measured from what included transactions actually pay, so it is
|
|
8
|
+
* the tighter evidence of what a chain requires. The floor is a safety guard,
|
|
9
|
+
* and on BSC it is the chain's post-Maxwell protocol MINIMUM — a bid below it
|
|
10
|
+
* is rejected outright. So the cap applies first and the floor last, making
|
|
11
|
+
* `result >= floor` hold for any cap value, including a cap below the floor.
|
|
12
|
+
*
|
|
13
|
+
* No chain has a cap below its floor today (BSC is the only chain in both
|
|
14
|
+
* tables: cap 0.055, floor 0.05), which is exactly why this ordering has to be
|
|
15
|
+
* pinned by a test rather than by the current data.
|
|
16
|
+
*
|
|
17
|
+
* This lives in its own module rather than in `chains.ts` because `./chains` is
|
|
18
|
+
* a public package export — anything exported from there becomes SDK API, and
|
|
19
|
+
* consumers must not be able to depend on cap/floor ordering internals. This
|
|
20
|
+
* module has no `exports` subpath, so it is importable by the test and not by
|
|
21
|
+
* consumers.
|
|
22
|
+
*/
|
|
23
|
+
function clampTip(tip, floorWei, capWei) {
|
|
24
|
+
let clamped = tip;
|
|
25
|
+
if (capWei !== undefined && capWei < clamped)
|
|
26
|
+
clamped = capWei;
|
|
27
|
+
if (floorWei !== undefined && floorWei > clamped)
|
|
28
|
+
clamped = floorWei;
|
|
29
|
+
return clamped;
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* viem's default base-fee multiplier, as a percentage.
|
|
3
|
+
*
|
|
4
|
+
* viem builds its EIP-1559 estimate as `base * baseFeeMultiplier + tip`, with
|
|
5
|
+
* `baseFeeMultiplier` defaulting to 1.2 (actions/public/estimateFeesPerGas.js:
|
|
6
|
+
* `chain?.fees?.baseFeeMultiplier ?? 1.2`).
|
|
7
|
+
*
|
|
8
|
+
* We depend on that value in exactly one place: the degraded path in
|
|
9
|
+
* `cappedMaxFeePerGas`, which runs viem's formula BACKWARDS to recover the base
|
|
10
|
+
* fee when eth_feeHistory could not supply one. If viem ever changes this
|
|
11
|
+
* default, that division silently mis-scales the two chains whose own
|
|
12
|
+
* multiplier exceeds it (Gnosis, HyperEVM) — no error, just a wrong number.
|
|
13
|
+
*
|
|
14
|
+
* Lives in its own module, rather than in `chains.ts`, so the test that pins it
|
|
15
|
+
* against viem's real behaviour can import the same constant the code uses
|
|
16
|
+
* instead of a copy that could drift. `./chains` is a public package export;
|
|
17
|
+
* this module has no `exports` subpath, so it stays internal.
|
|
18
|
+
*/
|
|
19
|
+
export declare const VIEM_DEFAULT_BASE_FEE_MULTIPLIER_PCT = 120n;
|
|
20
|
+
//# sourceMappingURL=viemDefaults.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viemDefaults.d.ts","sourceRoot":"","sources":["../src/viemDefaults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oCAAoC,OAAO,CAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VIEM_DEFAULT_BASE_FEE_MULTIPLIER_PCT = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* viem's default base-fee multiplier, as a percentage.
|
|
6
|
+
*
|
|
7
|
+
* viem builds its EIP-1559 estimate as `base * baseFeeMultiplier + tip`, with
|
|
8
|
+
* `baseFeeMultiplier` defaulting to 1.2 (actions/public/estimateFeesPerGas.js:
|
|
9
|
+
* `chain?.fees?.baseFeeMultiplier ?? 1.2`).
|
|
10
|
+
*
|
|
11
|
+
* We depend on that value in exactly one place: the degraded path in
|
|
12
|
+
* `cappedMaxFeePerGas`, which runs viem's formula BACKWARDS to recover the base
|
|
13
|
+
* fee when eth_feeHistory could not supply one. If viem ever changes this
|
|
14
|
+
* default, that division silently mis-scales the two chains whose own
|
|
15
|
+
* multiplier exceeds it (Gnosis, HyperEVM) — no error, just a wrong number.
|
|
16
|
+
*
|
|
17
|
+
* Lives in its own module, rather than in `chains.ts`, so the test that pins it
|
|
18
|
+
* against viem's real behaviour can import the same constant the code uses
|
|
19
|
+
* instead of a copy that could drift. `./chains` is a public package export;
|
|
20
|
+
* this module has no `exports` subpath, so it stays internal.
|
|
21
|
+
*/
|
|
22
|
+
exports.VIEM_DEFAULT_BASE_FEE_MULTIPLIER_PCT = 120n;
|