@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.
- package/LICENSE +21 -0
- package/README.md +270 -0
- package/dist/block-position.d.ts +97 -0
- package/dist/block-position.d.ts.map +1 -0
- package/dist/block-position.js +131 -0
- package/dist/block-position.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/math.d.ts +148 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +343 -0
- package/dist/math.js.map +1 -0
- package/dist/mempool.d.ts +89 -0
- package/dist/mempool.d.ts.map +1 -0
- package/dist/mempool.js +108 -0
- package/dist/mempool.js.map +1 -0
- package/dist/oracle.d.ts +139 -0
- package/dist/oracle.d.ts.map +1 -0
- package/dist/oracle.js +208 -0
- package/dist/oracle.js.map +1 -0
- package/dist/samples.d.ts +36 -0
- package/dist/samples.d.ts.map +1 -0
- package/dist/samples.js +107 -0
- package/dist/samples.js.map +1 -0
- package/dist/transport.d.ts +75 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +72 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/viem-actions.d.ts +77 -0
- package/dist/viem-actions.d.ts.map +1 -0
- package/dist/viem-actions.js +118 -0
- package/dist/viem-actions.js.map +1 -0
- package/dist/viem-transport.d.ts +85 -0
- package/dist/viem-transport.d.ts.map +1 -0
- package/dist/viem-transport.js +165 -0
- package/dist/viem-transport.js.map +1 -0
- package/package.json +81 -0
- 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
|
package/dist/math.js.map
ADDED
|
@@ -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"}
|