@valve-tech/gas-oracle 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +270 -0
  3. package/dist/block-position.d.ts +97 -0
  4. package/dist/block-position.d.ts.map +1 -0
  5. package/dist/block-position.js +131 -0
  6. package/dist/block-position.js.map +1 -0
  7. package/dist/index.d.ts +24 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +23 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/math.d.ts +148 -0
  12. package/dist/math.d.ts.map +1 -0
  13. package/dist/math.js +343 -0
  14. package/dist/math.js.map +1 -0
  15. package/dist/mempool.d.ts +89 -0
  16. package/dist/mempool.d.ts.map +1 -0
  17. package/dist/mempool.js +108 -0
  18. package/dist/mempool.js.map +1 -0
  19. package/dist/oracle.d.ts +139 -0
  20. package/dist/oracle.d.ts.map +1 -0
  21. package/dist/oracle.js +208 -0
  22. package/dist/oracle.js.map +1 -0
  23. package/dist/samples.d.ts +36 -0
  24. package/dist/samples.d.ts.map +1 -0
  25. package/dist/samples.js +107 -0
  26. package/dist/samples.js.map +1 -0
  27. package/dist/transport.d.ts +75 -0
  28. package/dist/transport.d.ts.map +1 -0
  29. package/dist/transport.js +72 -0
  30. package/dist/transport.js.map +1 -0
  31. package/dist/types.d.ts +168 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +11 -0
  34. package/dist/types.js.map +1 -0
  35. package/dist/viem-actions.d.ts +77 -0
  36. package/dist/viem-actions.d.ts.map +1 -0
  37. package/dist/viem-actions.js +118 -0
  38. package/dist/viem-actions.js.map +1 -0
  39. package/dist/viem-transport.d.ts +85 -0
  40. package/dist/viem-transport.d.ts.map +1 -0
  41. package/dist/viem-transport.js +165 -0
  42. package/dist/viem-transport.js.map +1 -0
  43. package/package.json +81 -0
  44. package/src/index.ts +84 -0
package/dist/math.d.ts ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Pure math primitives for the gas oracle. No I/O, no state — every
3
+ * function is referentially transparent and exhaustively unit-testable.
4
+ *
5
+ * The math is the load-bearing wall: tier recommendations are only as
6
+ * trustworthy as `effectiveTip` and `cappedTip`, so every edge case
7
+ * (legacy txs, EIP-1559 with zero headroom, cold-start cap, post-EIP-4844
8
+ * blob fee) is handled here rather than at higher layers.
9
+ */
10
+ import type { PriorityModel, RawTx, TierRecommendation, TierName, TipPercentiles, TipSample, Trend } from './types.js';
11
+ /** Default priority-fee decay cap = 12.5% / block (EIP-1559 parity). */
12
+ export declare const DEFAULT_PRIORITY_FEE_DECAY_CAP: bigint;
13
+ /**
14
+ * Effective per-gas tip a validator sees for one transaction.
15
+ *
16
+ * EIP-1559 (has both maxPriority + maxFee):
17
+ * tip = min(maxPriorityFeePerGas, maxFeePerGas - baseFee)
18
+ * Legacy (gasPrice only):
19
+ * tip = gasPrice - baseFee (clamped at 0)
20
+ *
21
+ * Returns 0n when fee fields are missing or the math would go negative.
22
+ */
23
+ export declare const effectiveTip: (tx: RawTx, baseFee: bigint) => bigint;
24
+ /**
25
+ * p10/p25/p50/p75/p90 from a pre-sorted ascending bigint array. Empty
26
+ * input returns all-zeros so callers can treat absence-of-data the same
27
+ * way as a quiet block.
28
+ */
29
+ export declare const computePercentiles: (sorted: bigint[]) => TipPercentiles;
30
+ /**
31
+ * Sort a tx array by effective tip (ascending) and return the bigints.
32
+ * Convenience wrapper so callers don't have to remember the comparator.
33
+ */
34
+ export declare const sortedTips: (txs: RawTx[], baseFee: bigint) => bigint[];
35
+ /**
36
+ * Classify a base-fee history as rising, falling, or stable. Threshold
37
+ * is ±10% comparing first to last value. Single-element or empty input
38
+ * returns 'stable'.
39
+ */
40
+ export declare const detectTrend: (history: bigint[]) => Trend;
41
+ /**
42
+ * Clamp a tip from below by the previously-published tip, decayed at
43
+ * `decayCap` per block of elapsed time. Upside is unclamped (a real
44
+ * spike propagates immediately).
45
+ *
46
+ * `decayCap` is expressed wad: `parseEther('0.125')` is 12.5% (EIP-1559
47
+ * parity). `null` disables capping entirely — every cycle publishes the
48
+ * raw tip with no anchor. `0n` means no decay allowed (last published
49
+ * is the floor in perpetuity until upside breaks it). `WAD` (1e18)
50
+ * means full collapse permitted after one block (effectively no floor).
51
+ *
52
+ * Pure integer math:
53
+ * retention factor per block = (WAD - decayCap) / WAD
54
+ * floor = lastPublished * retention^nBlocks
55
+ *
56
+ * Cold start (no history) returns `rawTip` verbatim. Same-block
57
+ * duplicate polls hold the previous value (n ≤ 0 case).
58
+ */
59
+ export declare const cappedTip: (rawTip: bigint, nowBlockNumber: bigint, lastPublished: bigint | undefined, lastPublishedBlockNumber: bigint | undefined, decayCap?: bigint | null) => bigint;
60
+ /**
61
+ * Gas-weighted percentile of a tip distribution. Each sample is one
62
+ * tx's effective tip + its declared gas; gas is the weight. Sort by tip
63
+ * ascending, walk cumulative gas, return the tip of the first sample
64
+ * whose running gas crosses `totalGas * p / 100`.
65
+ *
66
+ * Why gas-weight rather than count-weight? A 21k-gas spam tx and a
67
+ * 2M-gas swap exert very different pressure on validator inclusion
68
+ * choices; treating them as one count each understates the paying
69
+ * lane's share. The unit miners actually maximize is total tip * gas,
70
+ * so the gas-weighted distribution is the one our tier recommendations
71
+ * should reflect.
72
+ *
73
+ * Edge cases:
74
+ * - Empty samples: every requested percentile returns 0n.
75
+ * - totalGas === 0n (only zero-gas samples): same as empty, every
76
+ * percentile is 0n.
77
+ * - Single sample: every percentile is that sample's tip.
78
+ *
79
+ * Bigint-only. No floating-point math; the gas target uses integer
80
+ * division and is monotone in `p`.
81
+ */
82
+ export declare const gasWeightedPercentiles: (samples: TipSample[], percentiles: number[]) => Record<number, bigint>;
83
+ /** Default liveness window — one block ahead, matching pre-v0.2 behavior. */
84
+ export declare const DEFAULT_BASE_FEE_LIVENESS_BLOCKS = 1;
85
+ /**
86
+ * Concatenate ring + mempool samples into one gas-weighted distribution
87
+ * and read the four tier percentiles from it. Each tier's raw tip is
88
+ * then clamped from below by the dynamic decay cap (`cappedTip`) so a
89
+ * single quiet block can't drop the published number off a cliff.
90
+ *
91
+ * Why one merged distribution rather than `max(blockTier, mempoolTier)`?
92
+ * The competitive environment a customer's tx has to clear is the
93
+ * union of paying-lane txs that just landed and paying-lane txs queued
94
+ * for the next block. Taking percentiles from each source separately
95
+ * and `max()`-ing them double-counts spam-lane bias on whichever side
96
+ * happens to have less gas-weight in the paying lane on a given tick;
97
+ * gas-weighting the union resolves that without per-source magic.
98
+ *
99
+ * `priorityModel: 'eip1559'` further filters the paying-lane tiers
100
+ * (standard/fast/instant) down to type-2+ samples so legacy spam can't
101
+ * suppress the recommendation on chains that honor the 1559 cutoff.
102
+ * `slow` keeps drawing from the full union — it's the lane legacy txs
103
+ * actually live in, and excluding them would mis-report it.
104
+ *
105
+ * `maxFeePerGas` = bufferedBaseFee + cappedTip. `gasPrice` (legacy
106
+ * fallback) = baseFee + cappedTip. `maxFeePerBlobGas` is null when the
107
+ * chain doesn't expose `excessBlobGas`.
108
+ *
109
+ * Returns the tier objects (for the published snapshot) and the
110
+ * cappedTip values keyed by tier name (for the producer to persist as
111
+ * `lastPublishedTips` for the next cycle's cap anchor).
112
+ */
113
+ export declare const computeTiers: (input: {
114
+ ringSamples: TipSample[];
115
+ mempoolSamples: TipSample[];
116
+ baseFee: bigint;
117
+ baseFeeTrend: Trend;
118
+ blob: {
119
+ blobBaseFee: bigint;
120
+ trend: Trend;
121
+ } | null;
122
+ blockNumber: bigint;
123
+ lastPublishedTips: Record<TierName, bigint> | undefined;
124
+ lastPublishedBlockNumber: bigint | undefined;
125
+ priorityFeeDecayCap?: bigint | null;
126
+ priorityModel?: PriorityModel;
127
+ baseFeeLivenessBlocks?: number;
128
+ }) => {
129
+ tiers: Record<TierName, TierRecommendation>;
130
+ publishedTips: Record<TierName, bigint>;
131
+ };
132
+ /**
133
+ * EIP-4844 blob base fee from `excessBlobGas`. Implements the
134
+ * fake_exponential approximation: `output = factor * e^(num/den)` via
135
+ * iterative summation until the term goes to zero. Integer-only.
136
+ *
137
+ * MIN_BLOB_BASE_FEE = 1
138
+ * BLOB_BASE_FEE_UPDATE_FRACTION = 3338477
139
+ */
140
+ export declare const computeBlobBaseFee: (excessBlobGas: bigint) => bigint;
141
+ /**
142
+ * Geth/Reth `txpool_content` returns `pending` and `queued`, each keyed
143
+ * by sender → nonce → tx. This flattens to a tx array; queued is
144
+ * usually ignored for tip math (those txs aren't competing for the
145
+ * next block).
146
+ */
147
+ export declare const flattenTxPool: (pool: Record<string, Record<string, RawTx>> | undefined | null) => RawTx[];
148
+ //# sourceMappingURL=math.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"math.d.ts","sourceRoot":"","sources":["../src/math.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,KAAK,EACL,kBAAkB,EAClB,QAAQ,EACR,cAAc,EACd,SAAS,EACT,KAAK,EACN,MAAM,YAAY,CAAA;AAcnB,wEAAwE;AACxE,eAAO,MAAM,8BAA8B,QAAW,CAAA;AAEtD;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,GAAI,IAAI,KAAK,EAAE,SAAS,MAAM,KAAG,MAezD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,EAAE,KAAG,cASrD,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,KAAK,EAAE,EAAE,SAAS,MAAM,KAAG,MAAM,EAGhB,CAAA;AAEjD;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,MAAM,EAAE,KAAG,KAS/C,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,SAAS,GACpB,QAAQ,MAAM,EACd,gBAAgB,MAAM,EACtB,eAAe,MAAM,GAAG,SAAS,EACjC,0BAA0B,MAAM,GAAG,SAAS,EAC5C,WAAU,MAAM,GAAG,IAAqC,KACvD,MAgBF,CAAA;AAeD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,sBAAsB,GACjC,SAAS,SAAS,EAAE,EACpB,aAAa,MAAM,EAAE,KACpB,MAAM,CAAC,MAAM,EAAE,MAAM,CAkCvB,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,gCAAgC,IAAI,CAAA;AAqDjD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO;IAClC,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,cAAc,EAAE,SAAS,EAAE,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,KAAK,CAAA;IACnB,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,GAAG,IAAI,CAAA;IAClD,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,SAAS,CAAA;IACvD,wBAAwB,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5C,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAA;CAC/B,KAAG;IACF,KAAK,EAAE,MAAM,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAA;IAC3C,aAAa,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;CA4DxC,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAAI,eAAe,MAAM,KAAG,MAY1D,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,GAAG,SAAS,GAAG,IAAI,KAC7D,KAAK,EAOP,CAAA"}
package/dist/math.js ADDED
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Pure math primitives for the gas oracle. No I/O, no state — every
3
+ * function is referentially transparent and exhaustively unit-testable.
4
+ *
5
+ * The math is the load-bearing wall: tier recommendations are only as
6
+ * trustworthy as `effectiveTip` and `cappedTip`, so every edge case
7
+ * (legacy txs, EIP-1559 with zero headroom, cold-start cap, post-EIP-4844
8
+ * blob fee) is handled here rather than at higher layers.
9
+ */
10
+ const TREND_RISING_THRESHOLD_PCT = 10n;
11
+ const TREND_FALLING_THRESHOLD_PCT = -10n;
12
+ /**
13
+ * 1e18, the EVM-native fixed-point scale. All caller-facing fractional
14
+ * config knobs are bigints expressed against this scale — i.e.
15
+ * `parseEther('0.125')` for 12.5%. Internal-only; the public surface
16
+ * teaches callers to construct values via viem's `parseEther`, not via
17
+ * a valve-specific WAD export.
18
+ */
19
+ const WAD = 1000000000000000000n;
20
+ /** Default priority-fee decay cap = 12.5% / block (EIP-1559 parity). */
21
+ export const DEFAULT_PRIORITY_FEE_DECAY_CAP = WAD / 8n;
22
+ /**
23
+ * Effective per-gas tip a validator sees for one transaction.
24
+ *
25
+ * EIP-1559 (has both maxPriority + maxFee):
26
+ * tip = min(maxPriorityFeePerGas, maxFeePerGas - baseFee)
27
+ * Legacy (gasPrice only):
28
+ * tip = gasPrice - baseFee (clamped at 0)
29
+ *
30
+ * Returns 0n when fee fields are missing or the math would go negative.
31
+ */
32
+ export const effectiveTip = (tx, baseFee) => {
33
+ if (tx.maxFeePerGas && tx.maxPriorityFeePerGas) {
34
+ const maxFee = BigInt(tx.maxFeePerGas);
35
+ const maxPriority = BigInt(tx.maxPriorityFeePerGas);
36
+ const headroom = maxFee - baseFee;
37
+ if (headroom <= 0n)
38
+ return 0n;
39
+ return maxPriority < headroom ? maxPriority : headroom;
40
+ }
41
+ if (tx.gasPrice) {
42
+ const price = BigInt(tx.gasPrice);
43
+ return price > baseFee ? price - baseFee : 0n;
44
+ }
45
+ return 0n;
46
+ };
47
+ /**
48
+ * p10/p25/p50/p75/p90 from a pre-sorted ascending bigint array. Empty
49
+ * input returns all-zeros so callers can treat absence-of-data the same
50
+ * way as a quiet block.
51
+ */
52
+ export const computePercentiles = (sorted) => {
53
+ if (sorted.length === 0) {
54
+ return { p10: 0n, p25: 0n, p50: 0n, p75: 0n, p90: 0n };
55
+ }
56
+ const at = (pct) => {
57
+ const idx = Math.min(Math.floor(sorted.length * pct), sorted.length - 1);
58
+ return sorted[idx];
59
+ };
60
+ return { p10: at(0.1), p25: at(0.25), p50: at(0.5), p75: at(0.75), p90: at(0.9) };
61
+ };
62
+ /**
63
+ * Sort a tx array by effective tip (ascending) and return the bigints.
64
+ * Convenience wrapper so callers don't have to remember the comparator.
65
+ */
66
+ export const sortedTips = (txs, baseFee) => txs
67
+ .map((tx) => effectiveTip(tx, baseFee))
68
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
69
+ /**
70
+ * Classify a base-fee history as rising, falling, or stable. Threshold
71
+ * is ±10% comparing first to last value. Single-element or empty input
72
+ * returns 'stable'.
73
+ */
74
+ export const detectTrend = (history) => {
75
+ if (history.length < 2)
76
+ return 'stable';
77
+ const first = history[0];
78
+ const last = history[history.length - 1];
79
+ if (first === 0n)
80
+ return last > 0n ? 'rising' : 'stable';
81
+ const change = ((last - first) * 100n) / first;
82
+ if (change > TREND_RISING_THRESHOLD_PCT)
83
+ return 'rising';
84
+ if (change < TREND_FALLING_THRESHOLD_PCT)
85
+ return 'falling';
86
+ return 'stable';
87
+ };
88
+ /**
89
+ * Clamp a tip from below by the previously-published tip, decayed at
90
+ * `decayCap` per block of elapsed time. Upside is unclamped (a real
91
+ * spike propagates immediately).
92
+ *
93
+ * `decayCap` is expressed wad: `parseEther('0.125')` is 12.5% (EIP-1559
94
+ * parity). `null` disables capping entirely — every cycle publishes the
95
+ * raw tip with no anchor. `0n` means no decay allowed (last published
96
+ * is the floor in perpetuity until upside breaks it). `WAD` (1e18)
97
+ * means full collapse permitted after one block (effectively no floor).
98
+ *
99
+ * Pure integer math:
100
+ * retention factor per block = (WAD - decayCap) / WAD
101
+ * floor = lastPublished * retention^nBlocks
102
+ *
103
+ * Cold start (no history) returns `rawTip` verbatim. Same-block
104
+ * duplicate polls hold the previous value (n ≤ 0 case).
105
+ */
106
+ export const cappedTip = (rawTip, nowBlockNumber, lastPublished, lastPublishedBlockNumber, decayCap = DEFAULT_PRIORITY_FEE_DECAY_CAP) => {
107
+ if (decayCap === null)
108
+ return rawTip;
109
+ if (lastPublished === undefined || lastPublishedBlockNumber === undefined) {
110
+ return rawTip;
111
+ }
112
+ const nBlocks = nowBlockNumber - lastPublishedBlockNumber;
113
+ if (nBlocks <= 0n) {
114
+ return rawTip > lastPublished ? rawTip : lastPublished;
115
+ }
116
+ const retain = WAD - decayCap;
117
+ let factor = WAD;
118
+ for (let i = 0n; i < nBlocks; i += 1n) {
119
+ factor = (factor * retain) / WAD;
120
+ }
121
+ const downsideFloor = (lastPublished * factor) / WAD;
122
+ return rawTip > downsideFloor ? rawTip : downsideFloor;
123
+ };
124
+ /**
125
+ * Tier → percentile target, in the [0, 100] integer space used by
126
+ * `gasWeightedPercentiles`. `standard` is p50 (the median) so the
127
+ * recommended tip lands inside the paying lane on bimodal chains rather
128
+ * than chasing the spam lane at p25.
129
+ */
130
+ const TIER_PERCENTILE = [
131
+ ['slow', 10],
132
+ ['standard', 50],
133
+ ['fast', 75],
134
+ ['instant', 90],
135
+ ];
136
+ /**
137
+ * Gas-weighted percentile of a tip distribution. Each sample is one
138
+ * tx's effective tip + its declared gas; gas is the weight. Sort by tip
139
+ * ascending, walk cumulative gas, return the tip of the first sample
140
+ * whose running gas crosses `totalGas * p / 100`.
141
+ *
142
+ * Why gas-weight rather than count-weight? A 21k-gas spam tx and a
143
+ * 2M-gas swap exert very different pressure on validator inclusion
144
+ * choices; treating them as one count each understates the paying
145
+ * lane's share. The unit miners actually maximize is total tip * gas,
146
+ * so the gas-weighted distribution is the one our tier recommendations
147
+ * should reflect.
148
+ *
149
+ * Edge cases:
150
+ * - Empty samples: every requested percentile returns 0n.
151
+ * - totalGas === 0n (only zero-gas samples): same as empty, every
152
+ * percentile is 0n.
153
+ * - Single sample: every percentile is that sample's tip.
154
+ *
155
+ * Bigint-only. No floating-point math; the gas target uses integer
156
+ * division and is monotone in `p`.
157
+ */
158
+ export const gasWeightedPercentiles = (samples, percentiles) => {
159
+ const out = {};
160
+ if (samples.length === 0) {
161
+ for (const p of percentiles)
162
+ out[p] = 0n;
163
+ return out;
164
+ }
165
+ // Don't mutate the caller's array.
166
+ const sorted = [...samples].sort((a, b) => a.tip < b.tip ? -1 : a.tip > b.tip ? 1 : 0);
167
+ let totalGas = 0n;
168
+ for (const s of sorted)
169
+ totalGas += s.gas;
170
+ if (totalGas === 0n) {
171
+ for (const p of percentiles)
172
+ out[p] = 0n;
173
+ return out;
174
+ }
175
+ for (const p of percentiles) {
176
+ const target = (totalGas * BigInt(p)) / 100n;
177
+ let cumulative = 0n;
178
+ let chosen = sorted[sorted.length - 1].tip;
179
+ for (const s of sorted) {
180
+ cumulative += s.gas;
181
+ if (cumulative >= target) {
182
+ chosen = s.tip;
183
+ break;
184
+ }
185
+ }
186
+ out[p] = chosen;
187
+ }
188
+ return out;
189
+ };
190
+ /** Default liveness window — one block ahead, matching pre-v0.2 behavior. */
191
+ export const DEFAULT_BASE_FEE_LIVENESS_BLOCKS = 1;
192
+ /**
193
+ * Buffer multiplier applied to base fee when computing `maxFeePerGas`.
194
+ *
195
+ * EIP-1559 lets base fee change by up to ±12.5% per block, so the
196
+ * worst-case upper-bound trajectory over `N` blocks is `(9/8)^N`.
197
+ * Callers who want their published recommendation to remain includable
198
+ * for longer than one block (e.g. wallets sending into a UI where the
199
+ * user might take 60+ seconds to confirm) raise `livenessBlocks` to
200
+ * compound headroom proportionally.
201
+ *
202
+ * - Rising markets: `(9/8)^N * 10/9` — adds an 11% extra margin on
203
+ * top of the worst-case rise (preserves the old
204
+ * 1.25:1.125 ratio at N=1).
205
+ * - Stable markets: `(9/8)^N` — exact worst-case EIP-1559 trajectory.
206
+ * - Falling markets: `1×` regardless of N. Base fee will continue to
207
+ * drop, headroom is wasted.
208
+ *
209
+ * At `N=1` this matches the pre-v0.2 hardcoded values (1.25 / 1.125 / 1).
210
+ */
211
+ const baseFeeBufferMultiplier = (trend, livenessBlocks) => {
212
+ if (trend === 'falling')
213
+ return { num: 1n, den: 1n };
214
+ let num = 1n;
215
+ let den = 1n;
216
+ for (let i = 0; i < livenessBlocks; i += 1) {
217
+ num *= 9n;
218
+ den *= 8n;
219
+ }
220
+ if (trend === 'rising') {
221
+ num *= 10n;
222
+ den *= 9n;
223
+ }
224
+ return { num, den };
225
+ };
226
+ /**
227
+ * `priorityModel` cutoff: which tx types count as "paying lane."
228
+ *
229
+ * A sample is in the paying lane iff its `txType >= EIP1559_TYPE_CUTOFF`
230
+ * (≥ 2). Samples without a captured `txType` are excluded from the
231
+ * paying-lane filter — better to under-count than to mis-bucket a
232
+ * legacy tx into the priority lane and pull the recommendation up.
233
+ */
234
+ const EIP1559_TYPE_CUTOFF = 2;
235
+ const isPayingLaneSample = (sample) => sample.txType !== undefined && sample.txType >= EIP1559_TYPE_CUTOFF;
236
+ /**
237
+ * Concatenate ring + mempool samples into one gas-weighted distribution
238
+ * and read the four tier percentiles from it. Each tier's raw tip is
239
+ * then clamped from below by the dynamic decay cap (`cappedTip`) so a
240
+ * single quiet block can't drop the published number off a cliff.
241
+ *
242
+ * Why one merged distribution rather than `max(blockTier, mempoolTier)`?
243
+ * The competitive environment a customer's tx has to clear is the
244
+ * union of paying-lane txs that just landed and paying-lane txs queued
245
+ * for the next block. Taking percentiles from each source separately
246
+ * and `max()`-ing them double-counts spam-lane bias on whichever side
247
+ * happens to have less gas-weight in the paying lane on a given tick;
248
+ * gas-weighting the union resolves that without per-source magic.
249
+ *
250
+ * `priorityModel: 'eip1559'` further filters the paying-lane tiers
251
+ * (standard/fast/instant) down to type-2+ samples so legacy spam can't
252
+ * suppress the recommendation on chains that honor the 1559 cutoff.
253
+ * `slow` keeps drawing from the full union — it's the lane legacy txs
254
+ * actually live in, and excluding them would mis-report it.
255
+ *
256
+ * `maxFeePerGas` = bufferedBaseFee + cappedTip. `gasPrice` (legacy
257
+ * fallback) = baseFee + cappedTip. `maxFeePerBlobGas` is null when the
258
+ * chain doesn't expose `excessBlobGas`.
259
+ *
260
+ * Returns the tier objects (for the published snapshot) and the
261
+ * cappedTip values keyed by tier name (for the producer to persist as
262
+ * `lastPublishedTips` for the next cycle's cap anchor).
263
+ */
264
+ export const computeTiers = (input) => {
265
+ const livenessBlocks = input.baseFeeLivenessBlocks ?? DEFAULT_BASE_FEE_LIVENESS_BLOCKS;
266
+ const { num: baseNum, den: baseDen } = baseFeeBufferMultiplier(input.baseFeeTrend, livenessBlocks);
267
+ const bufferedBase = (input.baseFee * baseNum) / baseDen;
268
+ const blobFee = (() => {
269
+ if (!input.blob)
270
+ return null;
271
+ const { num, den } = baseFeeBufferMultiplier(input.blob.trend, livenessBlocks);
272
+ return (input.blob.blobBaseFee * num) / den;
273
+ })();
274
+ const decayCap = input.priorityFeeDecayCap === undefined
275
+ ? DEFAULT_PRIORITY_FEE_DECAY_CAP
276
+ : input.priorityFeeDecayCap;
277
+ const priorityModel = input.priorityModel ?? 'flat';
278
+ const combined = [...input.ringSamples, ...input.mempoolSamples];
279
+ const targets = TIER_PERCENTILE.map(([, p]) => p);
280
+ // Full-distribution percentiles always exist — `slow` reads from them
281
+ // regardless of model, so legacy txs find their lane.
282
+ const fullPercentiles = gasWeightedPercentiles(combined, targets);
283
+ // Paying-lane percentiles only computed when the model demands them;
284
+ // saves one pass on the much-more-common 'flat' path.
285
+ const payingLanePercentiles = priorityModel === 'eip1559'
286
+ ? gasWeightedPercentiles(combined.filter(isPayingLaneSample), targets)
287
+ : null;
288
+ const tiers = {};
289
+ const publishedTips = {};
290
+ for (const [name, percentileTarget] of TIER_PERCENTILE) {
291
+ const useFiltered = payingLanePercentiles !== null && name !== 'slow';
292
+ const source = useFiltered ? payingLanePercentiles : fullPercentiles;
293
+ const rawTip = source[percentileTarget];
294
+ const tip = cappedTip(rawTip, input.blockNumber, input.lastPublishedTips?.[name], input.lastPublishedBlockNumber, decayCap);
295
+ publishedTips[name] = tip;
296
+ tiers[name] = {
297
+ maxPriorityFeePerGas: tip,
298
+ maxFeePerGas: bufferedBase + tip,
299
+ gasPrice: input.baseFee + tip,
300
+ maxFeePerBlobGas: blobFee,
301
+ };
302
+ }
303
+ return { tiers, publishedTips };
304
+ };
305
+ /**
306
+ * EIP-4844 blob base fee from `excessBlobGas`. Implements the
307
+ * fake_exponential approximation: `output = factor * e^(num/den)` via
308
+ * iterative summation until the term goes to zero. Integer-only.
309
+ *
310
+ * MIN_BLOB_BASE_FEE = 1
311
+ * BLOB_BASE_FEE_UPDATE_FRACTION = 3338477
312
+ */
313
+ export const computeBlobBaseFee = (excessBlobGas) => {
314
+ const BLOB_BASE_FEE_UPDATE_FRACTION = 3338477n;
315
+ if (excessBlobGas === 0n)
316
+ return 1n;
317
+ let output = 0n;
318
+ let accum = BLOB_BASE_FEE_UPDATE_FRACTION;
319
+ let i = 1n;
320
+ while (accum > 0n) {
321
+ output += accum;
322
+ accum = (accum * excessBlobGas) / (BLOB_BASE_FEE_UPDATE_FRACTION * i);
323
+ i += 1n;
324
+ }
325
+ return output / BLOB_BASE_FEE_UPDATE_FRACTION;
326
+ };
327
+ /**
328
+ * Geth/Reth `txpool_content` returns `pending` and `queued`, each keyed
329
+ * by sender → nonce → tx. This flattens to a tx array; queued is
330
+ * usually ignored for tip math (those txs aren't competing for the
331
+ * next block).
332
+ */
333
+ export const flattenTxPool = (pool) => {
334
+ if (!pool)
335
+ return [];
336
+ const txs = [];
337
+ for (const byNonce of Object.values(pool)) {
338
+ for (const tx of Object.values(byNonce))
339
+ txs.push(tx);
340
+ }
341
+ return txs;
342
+ };
343
+ //# sourceMappingURL=math.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"math.js","sourceRoot":"","sources":["../src/math.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Pure mempool-inspection helpers.
3
+ *
4
+ * `txpool_content` returns a two-level map: sender → nonce → RawTx,
5
+ * split into `pending` (next-in-line for inclusion) and `queued` (gap
6
+ * txs that can't mine yet). These helpers find a specific tx in either
7
+ * sub-pool.
8
+ *
9
+ * **Normalize once.** Upstream clients serialize sender addresses
10
+ * inconsistently — geth/reth use EIP-55 checksum, others lowercase,
11
+ * some checksum a few hex chars and not others. Doing the case-fold
12
+ * (and the hex/decimal nonce coercion) on every lookup wastes work
13
+ * and invites case-mismatch bugs. Instead, callers run
14
+ * `normalizeMempool(pool)` ONCE at ingest, then pass the normalized
15
+ * form to every subsequent lookup. Lookups then become direct O(1)
16
+ * map accesses with no string-case fallback walks.
17
+ *
18
+ * The `NormalizedMempool` type signals that contract structurally —
19
+ * functionally identical to `TxPoolContent`, but the type alias names
20
+ * the invariant.
21
+ */
22
+ import type { TxPoolContent } from './transport.js';
23
+ import type { RawTx } from './types.js';
24
+ /**
25
+ * `TxPoolContent` after a single normalization pass — every sender
26
+ * address key is lowercase ASCII, every nonce key is a decimal string.
27
+ * All lookup helpers expect this form; pass raw `TxPoolContent` through
28
+ * `normalizeMempool` first.
29
+ */
30
+ export type NormalizedMempool = TxPoolContent;
31
+ /**
32
+ * Discriminated identifier for a single tx — either by its hash, or by
33
+ * sender + nonce. Re-used across mempool lookups (`findInMempool`) and
34
+ * block-position queries (`tipForBlockPosition`) so callers think in
35
+ * one consistent shape.
36
+ */
37
+ export type TxIdentifier = {
38
+ hash: string;
39
+ } | {
40
+ address: string;
41
+ nonce: number | bigint | string;
42
+ };
43
+ /** Which sub-pool a hit was found in. */
44
+ export type MempoolBucket = 'pending' | 'queued';
45
+ export interface MempoolHit {
46
+ /** The tx as returned by `txpool_content`. */
47
+ tx: RawTx;
48
+ /** Which sub-pool the tx was found in. */
49
+ bucket: MempoolBucket;
50
+ /** Sender address, lowercased. */
51
+ address: string;
52
+ /** Tx nonce as a decimal string. */
53
+ nonce: string;
54
+ }
55
+ /**
56
+ * Run the upstream `txpool_content` payload through one normalization
57
+ * pass: lowercase every sender address, decimal-ify every nonce key.
58
+ * Idempotent — re-normalizing a NormalizedMempool returns an
59
+ * equivalent NormalizedMempool. Pass `null`/`undefined` to get an
60
+ * empty NormalizedMempool back (lookup helpers handle null too, but
61
+ * always-having-a-shape simplifies callers that store the snapshot).
62
+ */
63
+ export declare const normalizeMempool: (pool: TxPoolContent | null | undefined) => NormalizedMempool;
64
+ /**
65
+ * Find a tx in `pending` or `queued` by hash. Searches `pending` first
66
+ * — the common "is my tx going to mine?" question is most often
67
+ * answered there. Hash comparison is case-insensitive (tx.hash on the
68
+ * stored RawTx may still be checksum-mixed-case from upstream; we
69
+ * don't rewrite per-tx fields during pool normalization, only the
70
+ * outer keys).
71
+ */
72
+ export declare const findByHash: (pool: NormalizedMempool | null | undefined, hash: string) => MempoolHit | null;
73
+ /**
74
+ * Find a tx by sender address + nonce. Direct two-key lookup — no
75
+ * scan, no case-walk, because the pool is normalized at ingest.
76
+ * `nonce` accepts a number, a decimal string (`'5'`), a hex string
77
+ * (`'0x5'`), or a bigint. Returns `null` when the pool is null/empty,
78
+ * the address has no entries, or the address is present but the nonce
79
+ * isn't.
80
+ */
81
+ export declare const findByAddressNonce: (pool: NormalizedMempool | null | undefined, address: string, nonce: number | bigint | string) => MempoolHit | null;
82
+ /**
83
+ * Single-call lookup that takes a discriminated `TxIdentifier`. Use
84
+ * when you don't statically know which dimension you're querying on
85
+ * (e.g. wiring up a UI that lets the user paste either a hash or an
86
+ * address + nonce).
87
+ */
88
+ export declare const findInMempool: (pool: NormalizedMempool | null | undefined, id: TxIdentifier) => MempoolHit | null;
89
+ //# sourceMappingURL=mempool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mempool.d.ts","sourceRoot":"","sources":["../src/mempool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEvC;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,aAAa,CAAA;AAE7C;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;CAAE,CAAA;AAExD,yCAAyC;AACzC,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEhD,MAAM,WAAW,UAAU;IACzB,8CAA8C;IAC9C,EAAE,EAAE,KAAK,CAAA;IACT,0CAA0C;IAC1C,MAAM,EAAE,aAAa,CAAA;IACrB,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAA;CACd;AAiBD;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAC3B,MAAM,aAAa,GAAG,IAAI,GAAG,SAAS,KACrC,iBAGD,CAAA;AAqBF;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GACrB,MAAM,iBAAiB,GAAG,IAAI,GAAG,SAAS,EAC1C,MAAM,MAAM,KACX,UAAU,GAAG,IAKf,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,iBAAiB,GAAG,IAAI,GAAG,SAAS,EAC1C,SAAS,MAAM,EACf,OAAO,MAAM,GAAG,MAAM,GAAG,MAAM,KAC9B,UAAU,GAAG,IAiBf,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,iBAAiB,GAAG,IAAI,GAAG,SAAS,EAC1C,IAAI,YAAY,KACf,UAAU,GAAG,IAGf,CAAA"}