@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Valve Tech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # @valve-tech/gas-oracle
2
+
3
+ Multi-tier gas-fee oracle for EVM chains. Pass it a viem `PublicClient`
4
+ and it polls block + mempool data, computes `slow` / `standard` /
5
+ `fast` / `instant` tier recommendations, and serves them via an
6
+ in-memory cache. Includes a configurable downside-decay cap, a chain-
7
+ aware EIP-1559 priority cutoff, and EIP-4844 blob-fee handling.
8
+
9
+ Zero runtime dependencies. `viem` is the only peer dependency.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ yarn add @valve-tech/gas-oracle viem
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { createPublicClient, http, parseEther } from 'viem'
21
+ import { mainnet } from 'viem/chains'
22
+ import { createGasOracle } from '@valve-tech/gas-oracle'
23
+
24
+ const client = createPublicClient({ chain: mainnet, transport: http() })
25
+
26
+ const oracle = createGasOracle({
27
+ client,
28
+ chainId: 1,
29
+ priorityFeeDecayCap: parseEther('0.125'), // 12.5%/block, EIP-1559 parity
30
+ priorityModel: 'eip1559', // 'flat' for extractive networks
31
+ })
32
+
33
+ oracle.subscribe((state) => {
34
+ console.log('fast tier:', state.tiers.fast.maxPriorityFeePerGas)
35
+ })
36
+
37
+ oracle.start()
38
+
39
+ // Sub-millisecond read, no RPC roundtrip:
40
+ const tier = oracle.getState()?.tiers.standard
41
+ ```
42
+
43
+ ## Tier semantics
44
+
45
+ Each tier is one `TierRecommendation`:
46
+
47
+ ```ts
48
+ interface TierRecommendation {
49
+ maxPriorityFeePerGas: bigint
50
+ maxFeePerGas: bigint // bufferedBaseFee + maxPriorityFeePerGas
51
+ gasPrice: bigint // baseFee + maxPriorityFeePerGas (legacy)
52
+ maxFeePerBlobGas: bigint | null // null on chains without EIP-4844
53
+ }
54
+ ```
55
+
56
+ Tier mapping in the gas-weighted percentile distribution:
57
+
58
+ | Tier | Percentile | Use for |
59
+ |------------|------------|--------------------------------------------------------------|
60
+ | `slow` | p10 | Background / non-time-sensitive ops (claims, batched writes) |
61
+ | `standard` | p50 | Default for most user actions |
62
+ | `fast` | p75 | Trades, swaps, anything competing with bots |
63
+ | `instant` | p90 | Auctions, MEV-adjacent, opt-out-of-mempool deals |
64
+
65
+ `slow` always reads from the full distribution (legacy + 1559) so legacy
66
+ senders can still find the lane they actually live in. Under
67
+ `priorityModel: 'eip1559'`, the paying-lane tiers (`standard`/`fast`/
68
+ `instant`) draw from type-2+ samples only — legacy spam can't suppress
69
+ them.
70
+
71
+ ## Configuration
72
+
73
+ ### `priorityFeeDecayCap`
74
+
75
+ How fast the published priority-fee tip is allowed to drop, expressed
76
+ wad (1e18 = 100%). Use `parseEther` for the human-readable form:
77
+
78
+ ```ts
79
+ priorityFeeDecayCap: parseEther('0.125') // 12.5%/block (EIP-1559 parity)
80
+ priorityFeeDecayCap: parseEther('0.05') // 5%/block (smoother)
81
+ priorityFeeDecayCap: parseEther('0') // no decay allowed (sticky floor)
82
+ priorityFeeDecayCap: parseEther('1') // full collapse after one block
83
+ priorityFeeDecayCap: null // uncapped — track raw mempool
84
+ ```
85
+
86
+ Validated at construction; out-of-range values throw. Upside is always
87
+ unclamped — real spikes propagate immediately.
88
+
89
+ ### `priorityModel`
90
+
91
+ Where the chain's inclusion logic draws its priority cutoff in the
92
+ EIP-2718 type space:
93
+
94
+ - `'flat'` — every tx contributes equally to the gas-weighted
95
+ distribution. Right for extractive validators (PulseChain, etc.)
96
+ that ignore the type byte and just maximize fee per gas.
97
+ - `'eip1559'` — type 2+ samples drive the paying-lane tiers (standard/
98
+ fast/instant); `slow` still draws from the full distribution. Right
99
+ for chains that honor EIP-1559 ordering (Ethereum, most L2s).
100
+
101
+ Default `'flat'` (most conservative — never under-counts spam).
102
+
103
+ ### `baseFeeLivenessBlocks`
104
+
105
+ How many blocks the published recommendation should survive in the
106
+ worst case. The buffered base fee underpinning `maxFeePerGas` becomes
107
+ `baseFee × (9/8)^N` (the EIP-1559 worst-case rise compounded over N
108
+ blocks), so a tx submitted with the snapshot still lands within `N`
109
+ blocks even if every intervening block is full.
110
+
111
+ ```ts
112
+ baseFeeLivenessBlocks: 1 // default; one block of headroom (= old behavior)
113
+ baseFeeLivenessBlocks: 6 // ~1.5 minutes on Ethereum, ~2 minutes on PulseChain
114
+ baseFeeLivenessBlocks: 30 // generous cushion for slow human approvals
115
+ ```
116
+
117
+ `falling` markets stay at 1× regardless of N (base fee will continue
118
+ to drop, headroom is wasted).
119
+
120
+ ### `poll`
121
+
122
+ Producer-side toggles for upstream RPC calls:
123
+
124
+ ```ts
125
+ poll: {
126
+ feeHistory: true, // eth_feeHistory; powers trend detection
127
+ mempool: true, // txpool_content; powers pending-pressure signal
128
+ }
129
+ ```
130
+
131
+ Both default `true`. Setting either to `false` skips that RPC entirely
132
+ each cycle. Useful when the upstream provider gates the method (many
133
+ public RPCs return 405 on `txpool_content`) or when you want a
134
+ minimum-RPC-budget oracle. `eth_getBlockByNumber` is not toggleable.
135
+
136
+ ### `keepMempoolSnapshot`
137
+
138
+ When `true`, the oracle retains the latest normalized mempool snapshot
139
+ and exposes it via `oracle.getMempoolSnapshot()`. The snapshot powers
140
+ `findInMempool` / `tipForBlockPosition({ kind: 'aheadOf' })`-style
141
+ lookups without a second RPC roundtrip. Memory cost is the size of one
142
+ `txpool_content` payload (5–15MB on busy ETH mainnet); leave off in
143
+ browser/mobile contexts. Default `false`.
144
+
145
+ ## Mempool inspection
146
+
147
+ Two ways into the same data: pure helpers that take a normalized pool
148
+ (if you already have one) or oracle-backed actions (if you're already
149
+ running a `GasOracle`).
150
+
151
+ ```ts
152
+ import {
153
+ normalizeMempool,
154
+ findByHash,
155
+ findByAddressNonce,
156
+ findInMempool,
157
+ } from '@valve-tech/gas-oracle'
158
+
159
+ // Normalize once at ingest — case-folds sender addresses and
160
+ // decimalizes nonce keys. All lookups expect the normalized form.
161
+ const pool = normalizeMempool(rawPoolFromTxpoolContent)
162
+
163
+ findByHash(pool, '0xdeadbeef…') // MempoolHit | null
164
+ findByAddressNonce(pool, '0xabc…', 5) // MempoolHit | null
165
+ findInMempool(pool, { hash: '0xdeadbeef…' }) // discriminated form
166
+ findInMempool(pool, { address: '0xabc…', nonce: 5n })
167
+ ```
168
+
169
+ `MempoolHit` carries the matched `tx`, the `bucket` (`'pending'` /
170
+ `'queued'`), and the canonicalized `address` + `nonce`.
171
+
172
+ ## Block-position calculations
173
+
174
+ Compute the priority fee required to land at a target position in the
175
+ next block. The query is a discriminated union — each `kind` carries
176
+ exactly the fields it needs:
177
+
178
+ ```ts
179
+ import { tipForBlockPosition } from '@valve-tech/gas-oracle'
180
+
181
+ // Absolute targeting
182
+ tipForBlockPosition(samples, { kind: 'rank', rank: 0 }) // top of block
183
+ tipForBlockPosition(samples, { kind: 'percentile', percentile: 5 }) // top 5%
184
+ tipForBlockPosition(samples, { kind: 'gasFromTop', gas: 1_000_000n }) // first 1M gas
185
+
186
+ // Relative targeting — beat or undercut a specific tx
187
+ tipForBlockPosition(samples, { kind: 'aheadOf', tx: { hash: '0xabc…' } })
188
+ tipForBlockPosition(samples, { kind: 'behind', tx: { address: '0xabc…', nonce: 5 } })
189
+ ```
190
+
191
+ Returns `{ requiredTip, pivot, rank, gasFromTop }`. `requiredTip` is
192
+ the *minimum* tip — pivot.tip + 1 wei to outbid, or pivot.tip - 1 wei
193
+ to undercut. Add your own buffer for finality.
194
+
195
+ `samples` is typically the merged ring + mempool tip distribution —
196
+ the same union `computeTiers` reads. The viem-actions extension
197
+ exposes `client.tipForBlockPosition(query)` which assembles this
198
+ distribution for you from the oracle's state.
199
+
200
+ ## viem integration
201
+
202
+ ### Subpath: `@valve-tech/gas-oracle/viem-actions`
203
+
204
+ Extension surface for callers who want explicit access to tier shapes:
205
+
206
+ ```ts
207
+ import { gasOracleActions } from '@valve-tech/gas-oracle/viem-actions'
208
+
209
+ const client = createPublicClient({ chain: mainnet, transport: http() })
210
+ .extend(gasOracleActions({
211
+ chainId: 1,
212
+ priorityFeeDecayCap: parseEther('0.125'),
213
+ priorityModel: 'eip1559',
214
+ }))
215
+
216
+ await client.getGasTiers() // full snapshot
217
+ await client.getGasTier('fast') // one tier
218
+ await client.findTxInMempool({ hash: '0xabc…' }) // mempool lookup
219
+ await client.tipForBlockPosition({ // position targeting
220
+ kind: 'aheadOf',
221
+ tx: { address: '0xabc…', nonce: 5 },
222
+ })
223
+ client.stopGasOracle() // shutdown hook
224
+ ```
225
+
226
+ ### Subpath: `@valve-tech/gas-oracle/viem-transport`
227
+
228
+ Drop-in replacement for callers who want viem's existing API to *just
229
+ work better* — `useFeeData`, `walletClient.sendTransaction({...})`,
230
+ `estimateMaxPriorityFeePerGas`, and so on:
231
+
232
+ ```ts
233
+ import { withGasOracle } from '@valve-tech/gas-oracle/viem-transport'
234
+
235
+ const transport = withGasOracle(http(rpcUrl), {
236
+ chainId: 1,
237
+ priorityFeeDecayCap: parseEther('0.125'),
238
+ priorityModel: 'eip1559',
239
+ intercept: {
240
+ eth_gasFeeEstimate: true, // additive (default on)
241
+ eth_maxPriorityFeePerGas: 'fast', // tier required for standard methods
242
+ },
243
+ })
244
+
245
+ const client = createPublicClient({ chain: mainnet, transport })
246
+ ```
247
+
248
+ Default `intercept` is `{ eth_gasFeeEstimate: true }` only — the
249
+ additive method that returns multi-tier shape. Standard methods
250
+ (`eth_gasPrice`, `eth_maxPriorityFeePerGas`) pass through to upstream
251
+ unless explicitly opted in **with a tier name**. Boolean opt-in on the
252
+ standard methods is intentionally not accepted: a default tier choice
253
+ would silently make the method's number depend on the package version,
254
+ and that's the silently-pick-a-percentile foot-gun this design is
255
+ careful to avoid.
256
+
257
+ `eth_feeHistory` is intentionally NOT in the intercept config —
258
+ synthesizing a historical-percentile array from oracle state is its
259
+ own design problem. Always passes through to upstream.
260
+
261
+ ## Wire format
262
+
263
+ Every fee field is a `bigint`. Callers serializing across HTTP / Redis
264
+ / WebSocket should hex-encode (`'0x' + n.toString(16)`) since JSON has
265
+ no native bigint and `JSON.stringify` will throw on raw bigint values.
266
+ The package keeps the canonical numeric form internally.
267
+
268
+ ## License
269
+
270
+ MIT
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Block-position calculator.
3
+ *
4
+ * Validators include txs in *gas-weighted tip-descending order* — high
5
+ * tip-per-gas at the top of the block, low tip-per-gas at the bottom.
6
+ * "Position in the block" therefore has two natural axes:
7
+ *
8
+ * 1. **Rank** — count from the top. Position 0 is the highest-tip tx.
9
+ * 2. **Gas offset** — accumulated gas consumed by all higher-tip txs.
10
+ * Position is "I want my tx mined within the first 1M gas of the
11
+ * block" → walk samples in tip-desc order, sum gas, find the
12
+ * pivot whose cumulative cross 1M.
13
+ *
14
+ * Both axes resolve to the same machinery — pick a pivot in the
15
+ * sorted-by-tip-desc sample list, return the tip you'd need to pay to
16
+ * outbid it.
17
+ *
18
+ * `tipForBlockPosition` returns the *minimum* tip to land at the
19
+ * requested position; callers should add their own buffer for finality.
20
+ * Equality at the same tip is validator-policy-dependent and not
21
+ * guaranteed, so "outbid by 1 wei" is the honest minimum.
22
+ */
23
+ import type { TipSample } from './types.js';
24
+ import type { TxIdentifier } from './mempool.js';
25
+ /**
26
+ * What position to target in the gas-weighted, tip-descending order
27
+ * of a block. Discriminated by `kind`:
28
+ *
29
+ * - `'rank'` — `rank` is 0-indexed from the top (highest tip).
30
+ * `rank: 0` → land at the very top; `rank: 4` →
31
+ * land in 5th place.
32
+ * - `'percentile'` — `percentile` is `[0, 100]` from the top.
33
+ * `percentile: 5` → I want to be in the top 5%.
34
+ * - `'gasFromTop'` — `gas` is a bigint of cumulative gas from the
35
+ * block top. `gas: 1_000_000n` → land within the
36
+ * first 1M gas of the block.
37
+ * - `'aheadOf'` — beat out a specific tx in the distribution.
38
+ * Useful for MEV-style "I just need to outrank
39
+ * this swap" targeting.
40
+ * - `'behind'` — be just behind a specific tx (still mine in the
41
+ * same block, but pay less). Useful for piggybacking
42
+ * on a high-tip tx that's already going to displace
43
+ * pressure.
44
+ *
45
+ * Discriminated-union over flat-options-with-mode: per project style,
46
+ * fields whose presence depends on the kind belong inside the kind's
47
+ * variant, not flattened with `?`-marks.
48
+ */
49
+ export type BlockPositionQuery = {
50
+ kind: 'rank';
51
+ rank: number;
52
+ } | {
53
+ kind: 'percentile';
54
+ percentile: number;
55
+ } | {
56
+ kind: 'gasFromTop';
57
+ gas: bigint;
58
+ } | {
59
+ kind: 'aheadOf';
60
+ tx: TxIdentifier;
61
+ } | {
62
+ kind: 'behind';
63
+ tx: TxIdentifier;
64
+ };
65
+ export interface BlockPositionResult {
66
+ /**
67
+ * Minimum tip-per-gas to definitively land at the requested position.
68
+ * For `aheadOf` and `gasFromTop`/`rank`/`percentile` queries, this is
69
+ * `pivot.tip + 1n` (one wei above the tx at the boundary). For
70
+ * `behind`, this is `max(pivot.tip - 1n, 0n)`. Returns `0n` when the
71
+ * distribution is empty or the position is below everyone.
72
+ */
73
+ requiredTip: bigint;
74
+ /**
75
+ * The sample at the boundary — the tx you're outbidding (or
76
+ * undercutting). `null` when the requested position is outside the
77
+ * distribution (e.g. `rank: 1000` in a 50-tx block) or the
78
+ * distribution is empty.
79
+ */
80
+ pivot: TipSample | null;
81
+ /** Approximate rank of the resolved position, 0-indexed from top. */
82
+ rank: number;
83
+ /** Approximate gas-from-top of the resolved position. */
84
+ gasFromTop: bigint;
85
+ }
86
+ /**
87
+ * Compute the tip required to land at the requested position in the
88
+ * next block, given a sample distribution (typically the merged ring
89
+ * + mempool samples that `computeTiers` consumes, but any
90
+ * `TipSample[]` works — pass just block samples for "would I have
91
+ * landed in the last block?" hindsight).
92
+ *
93
+ * Pure: no I/O, no oracle dependency, no wall-clock. Test by feeding
94
+ * fixture samples and asserting the returned shape.
95
+ */
96
+ export declare const tipForBlockPosition: (samples: TipSample[], query: BlockPositionQuery) => BlockPositionResult;
97
+ //# sourceMappingURL=block-position.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-position.d.ts","sourceRoot":"","sources":["../src/block-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,YAAY,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,YAAY,CAAA;CAAE,CAAA;AAExC,MAAM,WAAW,mBAAmB;IAClC;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;IACvB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAA;CACnB;AA+CD;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAC9B,SAAS,SAAS,EAAE,EACpB,OAAO,kBAAkB,KACxB,mBA0DF,CAAA"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Block-position calculator.
3
+ *
4
+ * Validators include txs in *gas-weighted tip-descending order* — high
5
+ * tip-per-gas at the top of the block, low tip-per-gas at the bottom.
6
+ * "Position in the block" therefore has two natural axes:
7
+ *
8
+ * 1. **Rank** — count from the top. Position 0 is the highest-tip tx.
9
+ * 2. **Gas offset** — accumulated gas consumed by all higher-tip txs.
10
+ * Position is "I want my tx mined within the first 1M gas of the
11
+ * block" → walk samples in tip-desc order, sum gas, find the
12
+ * pivot whose cumulative cross 1M.
13
+ *
14
+ * Both axes resolve to the same machinery — pick a pivot in the
15
+ * sorted-by-tip-desc sample list, return the tip you'd need to pay to
16
+ * outbid it.
17
+ *
18
+ * `tipForBlockPosition` returns the *minimum* tip to land at the
19
+ * requested position; callers should add their own buffer for finality.
20
+ * Equality at the same tip is validator-policy-dependent and not
21
+ * guaranteed, so "outbid by 1 wei" is the honest minimum.
22
+ */
23
+ const sortByTipDesc = (samples) => [...samples].sort((a, b) => (a.tip > b.tip ? -1 : a.tip < b.tip ? 1 : 0));
24
+ const matchesIdentifier = (sample, id) => {
25
+ if ('hash' in id) {
26
+ return (typeof sample.hash === 'string' &&
27
+ sample.hash.toLowerCase() === id.hash.toLowerCase());
28
+ }
29
+ if (sample.address === undefined || sample.nonce === undefined)
30
+ return false;
31
+ if (sample.address.toLowerCase() !== id.address.toLowerCase())
32
+ return false;
33
+ return sample.nonce === BigInt(id.nonce).toString();
34
+ };
35
+ /**
36
+ * Walk samples in tip-desc order accumulating gas; return the index
37
+ * (0-based from top) where cumulative gas first crosses `targetGas`.
38
+ * Returns -1 when the target exceeds the sum of all gas (the position
39
+ * is below the whole distribution).
40
+ */
41
+ const indexAtGasOffset = (sorted, targetGas) => {
42
+ if (targetGas <= 0n)
43
+ return 0;
44
+ let cumulative = 0n;
45
+ for (let i = 0; i < sorted.length; i += 1) {
46
+ cumulative += sorted[i].gas;
47
+ if (cumulative > targetGas)
48
+ return i;
49
+ }
50
+ return -1;
51
+ };
52
+ const sumGasUpTo = (sorted, indexExclusive) => {
53
+ let g = 0n;
54
+ const upper = Math.min(indexExclusive, sorted.length);
55
+ for (let i = 0; i < upper; i += 1)
56
+ g += sorted[i].gas;
57
+ return g;
58
+ };
59
+ const empty = () => ({
60
+ requiredTip: 0n,
61
+ pivot: null,
62
+ rank: 0,
63
+ gasFromTop: 0n,
64
+ });
65
+ /**
66
+ * Compute the tip required to land at the requested position in the
67
+ * next block, given a sample distribution (typically the merged ring
68
+ * + mempool samples that `computeTiers` consumes, but any
69
+ * `TipSample[]` works — pass just block samples for "would I have
70
+ * landed in the last block?" hindsight).
71
+ *
72
+ * Pure: no I/O, no oracle dependency, no wall-clock. Test by feeding
73
+ * fixture samples and asserting the returned shape.
74
+ */
75
+ export const tipForBlockPosition = (samples, query) => {
76
+ if (samples.length === 0)
77
+ return empty();
78
+ const sorted = sortByTipDesc(samples);
79
+ let pivotIndex;
80
+ let beatPivot; // true = outbid (tip+1), false = undercut (tip-1)
81
+ switch (query.kind) {
82
+ case 'rank': {
83
+ pivotIndex = query.rank;
84
+ beatPivot = true;
85
+ break;
86
+ }
87
+ case 'percentile': {
88
+ // 0% = top of block (highest tip); 100% = bottom. Clamp to [0, 100].
89
+ const pct = Math.min(100, Math.max(0, query.percentile));
90
+ pivotIndex = Math.floor((sorted.length * pct) / 100);
91
+ // Edge: percentile=100 lands at length, which is "below everything"
92
+ if (pivotIndex >= sorted.length)
93
+ pivotIndex = sorted.length - 1;
94
+ beatPivot = true;
95
+ break;
96
+ }
97
+ case 'gasFromTop': {
98
+ pivotIndex = indexAtGasOffset(sorted, query.gas);
99
+ beatPivot = true;
100
+ break;
101
+ }
102
+ case 'aheadOf':
103
+ case 'behind': {
104
+ pivotIndex = sorted.findIndex((s) => matchesIdentifier(s, query.tx));
105
+ beatPivot = query.kind === 'aheadOf';
106
+ break;
107
+ }
108
+ }
109
+ if (pivotIndex < 0 || pivotIndex >= sorted.length) {
110
+ // Position is below everyone (or pivot not found) — pay nothing in priority
111
+ return {
112
+ requiredTip: 0n,
113
+ pivot: null,
114
+ rank: sorted.length,
115
+ gasFromTop: sumGasUpTo(sorted, sorted.length),
116
+ };
117
+ }
118
+ const pivot = sorted[pivotIndex];
119
+ const requiredTip = beatPivot
120
+ ? pivot.tip + 1n
121
+ : pivot.tip > 0n
122
+ ? pivot.tip - 1n
123
+ : 0n;
124
+ return {
125
+ requiredTip,
126
+ pivot,
127
+ rank: pivotIndex,
128
+ gasFromTop: sumGasUpTo(sorted, pivotIndex),
129
+ };
130
+ };
131
+ //# sourceMappingURL=block-position.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-position.js","sourceRoot":"","sources":["../src/block-position.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @valve-tech/gas-oracle — public API.
3
+ *
4
+ * Multi-tier gas-fee oracle for EVM chains. Pass it a viem PublicClient
5
+ * and it polls block + mempool data, computes slow/standard/fast/instant
6
+ * tier recommendations, and exposes them via a sub-millisecond in-memory
7
+ * read. Includes EIP-1559-style 12.5%/block downside cap so quiet blocks
8
+ * don't drop the published number off a cliff, and EIP-4844 blob fee
9
+ * for chains that support it.
10
+ *
11
+ * Zero runtime dependencies. viem is the only peer dependency, used to
12
+ * issue the underlying RPC calls (`eth_feeHistory`,
13
+ * `eth_getBlockByNumber`, `txpool_content`).
14
+ */
15
+ export { createGasOracle, reducePollInputs, type CreateGasOracleOptions, type GasOracle, } from './oracle.js';
16
+ export { effectiveTip, computePercentiles, detectTrend, cappedTip, computeTiers, computeBlobBaseFee, flattenTxPool, gasWeightedPercentiles, sortedTips, DEFAULT_PRIORITY_FEE_DECAY_CAP, DEFAULT_BASE_FEE_LIVENESS_BLOCKS, } from './math.js';
17
+ export { blockToSample, mempoolToSamples, } from './samples.js';
18
+ export { fetchOracleInputs, type FeeHistoryResult, type BlockResult, type TxPoolContent, type OraclePollInputs, } from './transport.js';
19
+ export type { RawTx, TipPercentiles, TierRecommendation, TierName, Trend, MempoolStats, BlobStats, BlockSample, GasOracleState, TipSample, PriorityModel, PollOptions, } from './types.js';
20
+ export { normalizeMempool, findByHash, findByAddressNonce, findInMempool, } from './mempool.js';
21
+ export type { NormalizedMempool, TxIdentifier, MempoolBucket, MempoolHit, } from './mempool.js';
22
+ export { tipForBlockPosition } from './block-position.js';
23
+ export type { BlockPositionQuery, BlockPositionResult, } from './block-position.js';
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,KAAK,sBAAsB,EAC3B,KAAK,SAAS,GACf,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,WAAW,EACX,SAAS,EACT,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,sBAAsB,EACtB,UAAU,EACV,8BAA8B,EAC9B,gCAAgC,GACjC,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,aAAa,EACb,gBAAgB,GACjB,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA;AAEvB,YAAY,EACV,KAAK,EACL,cAAc,EACd,kBAAkB,EAClB,QAAQ,EACR,KAAK,EACL,YAAY,EACZ,SAAS,EACT,WAAW,EACX,cAAc,EACd,SAAS,EACT,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAA;AAGnB,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,aAAa,GACd,MAAM,cAAc,CAAA;AACrB,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,UAAU,GACX,MAAM,cAAc,CAAA;AAGrB,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,YAAY,EACV,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,qBAAqB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @valve-tech/gas-oracle — public API.
3
+ *
4
+ * Multi-tier gas-fee oracle for EVM chains. Pass it a viem PublicClient
5
+ * and it polls block + mempool data, computes slow/standard/fast/instant
6
+ * tier recommendations, and exposes them via a sub-millisecond in-memory
7
+ * read. Includes EIP-1559-style 12.5%/block downside cap so quiet blocks
8
+ * don't drop the published number off a cliff, and EIP-4844 blob fee
9
+ * for chains that support it.
10
+ *
11
+ * Zero runtime dependencies. viem is the only peer dependency, used to
12
+ * issue the underlying RPC calls (`eth_feeHistory`,
13
+ * `eth_getBlockByNumber`, `txpool_content`).
14
+ */
15
+ export { createGasOracle, reducePollInputs, } from './oracle.js';
16
+ export { effectiveTip, computePercentiles, detectTrend, cappedTip, computeTiers, computeBlobBaseFee, flattenTxPool, gasWeightedPercentiles, sortedTips, DEFAULT_PRIORITY_FEE_DECAY_CAP, DEFAULT_BASE_FEE_LIVENESS_BLOCKS, } from './math.js';
17
+ export { blockToSample, mempoolToSamples, } from './samples.js';
18
+ export { fetchOracleInputs, } from './transport.js';
19
+ // Mempool inspection
20
+ export { normalizeMempool, findByHash, findByAddressNonce, findInMempool, } from './mempool.js';
21
+ // Block-position calculations
22
+ export { tipForBlockPosition } from './block-position.js';
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,eAAe,EACf,gBAAgB,GAGjB,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,WAAW,EACX,SAAS,EACT,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,sBAAsB,EACtB,UAAU,EACV,8BAA8B,EAC9B,gCAAgC,GACjC,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,aAAa,EACb,gBAAgB,GACjB,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,iBAAiB,GAKlB,MAAM,gBAAgB,CAAA;AAiBvB,qBAAqB;AACrB,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,aAAa,GACd,MAAM,cAAc,CAAA;AAQrB,8BAA8B;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA"}