@valve-tech/gas-oracle 0.6.0 → 0.8.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/CHANGELOG.md +54 -0
  2. package/README.md +115 -16
  3. package/dist/block-position.d.ts +3 -3
  4. package/dist/block-position.d.ts.map +1 -1
  5. package/dist/block-position.js +32 -20
  6. package/dist/block-position.js.map +1 -1
  7. package/dist/classify-tip.d.ts +33 -0
  8. package/dist/classify-tip.d.ts.map +1 -0
  9. package/dist/classify-tip.js +52 -0
  10. package/dist/classify-tip.js.map +1 -0
  11. package/dist/inclusion-labels.d.ts +30 -0
  12. package/dist/inclusion-labels.d.ts.map +1 -0
  13. package/dist/inclusion-labels.js +35 -0
  14. package/dist/inclusion-labels.js.map +1 -0
  15. package/dist/index.d.ts +7 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +10 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/math.d.ts +1 -1
  20. package/dist/math.d.ts.map +1 -1
  21. package/dist/math.js +20 -18
  22. package/dist/math.js.map +1 -1
  23. package/dist/oracle.d.ts +2 -2
  24. package/dist/oracle.d.ts.map +1 -1
  25. package/dist/oracle.js +27 -11
  26. package/dist/oracle.js.map +1 -1
  27. package/dist/presets.d.ts +36 -0
  28. package/dist/presets.d.ts.map +1 -0
  29. package/dist/presets.js +27 -0
  30. package/dist/presets.js.map +1 -0
  31. package/dist/replacement.d.ts +80 -0
  32. package/dist/replacement.d.ts.map +1 -0
  33. package/dist/replacement.js +114 -0
  34. package/dist/replacement.js.map +1 -0
  35. package/dist/types.d.ts +49 -5
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js +57 -1
  38. package/dist/types.js.map +1 -1
  39. package/dist/viem-transport.d.ts +1 -1
  40. package/dist/viem-transport.d.ts.map +1 -1
  41. package/dist/viem-transport.js +11 -5
  42. package/dist/viem-transport.js.map +1 -1
  43. package/package.json +2 -2
  44. package/skills/gas-oracle-integration/SKILL.md +159 -13
package/CHANGELOG.md CHANGED
@@ -5,6 +5,60 @@ All notable changes to `@valve-tech/gas-oracle` are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [0.8.0] — 2026-05-06
9
+
10
+ Consumes `@valve-tech/chain-source@0.8.0`'s WS-aware `ChainSource` transparently. Adds upstream helpers (replacement / classifyTip / inclusion labels), chain presets (PulseChain), const-namespace exports, and a bigint migration of public numeric fields.
11
+
12
+ ### Added
13
+
14
+ - **Replacement helpers** (`replacement.ts`): `minimumReplacementFee(current, txType)`, `bumpForReplacement(currentGas, targetGas)`, `recommendBumpTier(snapshot, stuckTx, options?)` with three named strategies (`BumpStrategy.cheapestThatLands` / `oneStepFasterThanRecommended` / `instant`). Outpace correction via optional `stuckTx.identifier` reads `snapshot.mempoolSamples` to find the tip needed to outpace the stuck tx in the live distribution, on top of the EIP-1559 +10% protocol floor (verified against geth/reth/PulseChain go-pulse sources).
15
+ - **`classifyTip(snapshot, tipWei)`** (`classify-tip.ts`): inverse of `tipForBlockPosition`. Returns `{ tier, requiredForNextTier, percentile, rank, gasFromTop }`.
16
+ - **Inclusion labels** (`inclusion-labels.ts`): `defaultInclusionLabels` (Record<TierName, string>) and `inclusionLabel(tier, overrides?)` for locale/branded copy without forking.
17
+ - **Chain presets** (`presets.ts`): `chainPresets.pulsechain` (chainId: 369, priorityModel: PriorityModel.flat) plus `presetForChainId(chainId)` runtime lookup.
18
+ - **Const-namespace exports**: `PriorityModel`, `TierName`, `Trend`, `TxType` are now exported as both values (const namespaces) and types. Use `PriorityModel.flat` instead of `'flat'` etc. `TIER_LADDER` (canonical slow→instant ordering) also exported from `types.ts`.
19
+ - **`mempoolSamples: TipSample[]`** on `GasOracleState`. Producer-local — wire publishers should strip before serializing (same convention as `ring`).
20
+
21
+ ### Changed
22
+
23
+ - **`priorityModel` default flips from `'flat'` to `PriorityModel.eip1559`**. Most chains honor the EIP-2718 type byte and the EIP-1559 fee-market; the default is now correct for the majority case. PulseChain (chain 369) becomes the explicit exception — set `priorityModel: PriorityModel.flat` (or use `...chainPresets.pulsechain`).
24
+ - **BigInt migration of public numeric fields**: `MempoolStats.pendingCount`, `MempoolStats.queuedCount`, `BlockPositionQuery.rank`, `BlockPositionQuery.percentile`, `BlockPositionResult.rank`, `CreateGasOracleOptions.pollIntervalMs` are now `bigint`. Identifier-like fields (`chainId`, EIP-2718 type bytes) stay `number`.
25
+
26
+ ### Migration notes
27
+
28
+ - Examples that previously omitted `priorityModel` silently get `PriorityModel.eip1559` after upgrade. Verify against your target chain.
29
+ - Math operations on the migrated bigint fields need `n` literals (`0n`, `1n`, etc.) and integer-floor division semantics.
30
+ - `pollIntervalMs: 5000` becomes `pollIntervalMs: 5000n`.
31
+
32
+ ## [0.7.0] — 2026-05-06
33
+
34
+ ### Changed
35
+
36
+ - **`skills/gas-oracle-integration/SKILL.md`** — appended a "Tx
37
+ tracking — composing with `@valve-tech/tx-tracker`" section that
38
+ redirects per-tx tracking questions to the sibling package and
39
+ documents the shared-`ChainSource` composition pattern. The skill
40
+ `description` trigger phrases now also catch composition questions,
41
+ but per-tx work explicitly defers to the tx-tracker skill. Ships
42
+ in the npm tarball.
43
+ - **Coverage hardening pre-1.0.** Test suite up from 189 → 209 tests
44
+ (+20). New coverage: `sampleGasFees` one-shot snapshot path
45
+ (success + null + error-routing variants), `tipForBlockPosition`
46
+ with full mempool data (EIP-1559 + legacy + 0-headroom + no-gas
47
+ + no-fee branches in the mempool→TipSample translation),
48
+ `formatTier` for tx types 0/1/2/3 plus the no-type fallback —
49
+ with and without blob fees in tier state, `keepMempoolSnapshot:
50
+ true` retention, `pauseWhenIdle` re-subscribe inside the stale
51
+ window keeping the loop alive, `fetchHeadBlockNumber` happy /
52
+ null-from-RPC / throw / un-decodable variants. Eliminated dead
53
+ defensive arms in `math.ts` / `block-position.ts` sort
54
+ comparators (equal-key arms unreachable since duplicates are
55
+ filtered upstream) and in `oracle.ts` `attachToSource` (stale-
56
+ timer clear is unreachable because every detach path that sets
57
+ the timer also keeps `unsubBlocks !== null`, which makes
58
+ `attachToSource` early-return before the clear).
59
+ - Coverage went **91.91% / 84.75% / 93.18% / 93.9%** stmts /
60
+ branches / funcs / lines → **98.26% / 93.47% / 100% / 99.8%**.
61
+
8
62
  ## [0.6.0] — 2026-05-05
9
63
 
10
64
  ### Added
package/README.md CHANGED
@@ -28,7 +28,7 @@ yarn add @valve-tech/gas-oracle viem
28
28
  ```ts
29
29
  import { createPublicClient, http, parseEther } from 'viem'
30
30
  import { mainnet } from 'viem/chains'
31
- import { createGasOracle } from '@valve-tech/gas-oracle'
31
+ import { createGasOracle, PriorityModel } from '@valve-tech/gas-oracle'
32
32
 
33
33
  const client = createPublicClient({ chain: mainnet, transport: http() })
34
34
 
@@ -36,7 +36,7 @@ const oracle = createGasOracle({
36
36
  client,
37
37
  chainId: 1,
38
38
  priorityFeeDecayCap: parseEther('0.125'), // 12.5%/block, EIP-1559 parity
39
- priorityModel: 'eip1559', // 'flat' for chains whose validators charge tips instead of burning them
39
+ // priorityModel defaults to PriorityModel.eip1559 explicit only if you need to override
40
40
  })
41
41
 
42
42
  oracle.subscribe((state) => {
@@ -73,7 +73,7 @@ Tier mapping in the gas-weighted percentile distribution:
73
73
 
74
74
  `slow` always reads from the full distribution (legacy + 1559) so legacy
75
75
  senders can still find the lane they actually live in. Under
76
- `priorityModel: 'eip1559'`, the paying-lane tiers (`standard`/`fast`/
76
+ `PriorityModel.eip1559`, the paying-lane tiers (`standard`/`fast`/
77
77
  `instant`) draw from type-2+ samples only — legacy spam can't suppress
78
78
  them.
79
79
 
@@ -95,19 +95,32 @@ priorityFeeDecayCap: null // uncapped — track raw mempool
95
95
  Validated at construction; out-of-range values throw. Upside is always
96
96
  unclamped — real spikes propagate immediately.
97
97
 
98
- ### `priorityModel`
98
+ ### Choosing `priorityModel`
99
99
 
100
- Where the chain's inclusion logic draws its priority cutoff in the
101
- EIP-2718 type space:
100
+ The default is `PriorityModel.eip1559` and is correct for every chain whose validators honor the EIP-2718 type byte and the EIP-1559 fee-market shape. **You should only override this if you've verified your target chain's validators are extractive** that is, they ignore the type byte and maximize fee per gas regardless of tx envelope.
102
101
 
103
- - `'flat'` every tx contributes equally to the gas-weighted
102
+ The canonical example is **PulseChain (chain 369)**: extractive validators mean the percentile math has to draw from the full tx distribution (`PriorityModel.flat`) instead of filtering to type-2+ samples. Setting the wrong model here silently under-prices, and your tx stalls.
103
+
104
+ For chains we know about, the `chainPresets` entry-point handles this for you:
105
+
106
+ ```ts
107
+ import { createGasOracle, chainPresets } from '@valve-tech/gas-oracle'
108
+
109
+ const oracle = createGasOracle({
110
+ client,
111
+ ...chainPresets.pulsechain, // chainId: 369, priorityModel: PriorityModel.flat
112
+ })
113
+ ```
114
+
115
+ Mechanics:
116
+
117
+ - `PriorityModel.flat` — every tx contributes equally to the gas-weighted
104
118
  distribution. Right for extractive validators (PulseChain, etc.)
105
119
  that ignore the type byte and just maximize fee per gas.
106
- - `'eip1559'` — type 2+ samples drive the paying-lane tiers (standard/
107
- fast/instant); `slow` still draws from the full distribution. Right
108
- for chains that honor EIP-1559 ordering (Ethereum, most L2s).
109
-
110
- Default `'flat'` (most conservative — never under-counts spam).
120
+ - `PriorityModel.eip1559` — type 2+ samples drive the paying-lane tiers
121
+ (standard/fast/instant); `slow` still draws from the full
122
+ distribution. Right for chains that honor EIP-1559 ordering
123
+ (Ethereum, most L2s).
111
124
 
112
125
  ### `baseFeeLivenessBlocks`
113
126
 
@@ -240,12 +253,12 @@ For callers who need a single fee snapshot without a long-lived
240
253
  oracle (typical tx-submit flow):
241
254
 
242
255
  ```ts
243
- import { sampleGasFees } from '@valve-tech/gas-oracle'
256
+ import { sampleGasFees, PriorityModel } from '@valve-tech/gas-oracle'
244
257
 
245
258
  const snapshot = await sampleGasFees({
246
259
  client,
247
260
  chainId: 1,
248
- priorityModel: 'eip1559',
261
+ priorityModel: PriorityModel.eip1559,
249
262
  })
250
263
  const tip = snapshot?.tiers.fast.maxPriorityFeePerGas
251
264
  ```
@@ -309,6 +322,90 @@ the same union `computeTiers` reads. The viem-actions extension
309
322
  exposes `client.tipForBlockPosition(query)` which assembles this
310
323
  distribution for you from the oracle's state.
311
324
 
325
+ ## Replacement workflow
326
+
327
+ When a tx gets stuck and you need to bump it past the EIP-1559 protocol
328
+ replacement floor (and optionally past the live mempool distribution):
329
+
330
+ ```ts
331
+ import {
332
+ createGasOracle,
333
+ recommendBumpTier,
334
+ bumpForReplacement,
335
+ BumpStrategy,
336
+ } from '@valve-tech/gas-oracle'
337
+
338
+ // 1. Pick a tier to bump to:
339
+ const tier = recommendBumpTier(
340
+ state,
341
+ { priorityTip: stuckTx.maxPriorityFeePerGas, identifier: { hash: stuckTx.hash } },
342
+ { strategy: BumpStrategy.cheapestThatLands },
343
+ )
344
+
345
+ if (tier === null) {
346
+ // Stuck tx is already paying above the top of the tier ladder, or
347
+ // the snapshot has no tip data. Caller's call: hold, or push instant.
348
+ return
349
+ }
350
+
351
+ // 2. Compute the gas object that satisfies both the protocol floor and the target tier:
352
+ const target = state.tiers[tier]
353
+ const gas = bumpForReplacement(
354
+ { maxFeePerGas: stuckTx.maxFeePerGas, maxPriorityFeePerGas: stuckTx.maxPriorityFeePerGas },
355
+ { maxFeePerGas: target.maxFeePerGas, maxPriorityFeePerGas: target.maxPriorityFeePerGas },
356
+ )
357
+
358
+ // 3. Send the replacement
359
+ walletClient.sendTransaction({ ...stuckTx, ...gas })
360
+ ```
361
+
362
+ ## Tip classification
363
+
364
+ Inverse of `tipForBlockPosition`: given a tip, find where it lands in the
365
+ live distribution and which named tier it falls in.
366
+
367
+ ```ts
368
+ import { classifyTip } from '@valve-tech/gas-oracle'
369
+
370
+ const result = classifyTip(state, myTip)
371
+ // result.tier — TierName | null (null if below slow)
372
+ // result.requiredForNextTier — bigint floor of next tier above (null at instant)
373
+ // result.percentile — bigint 0-100 (0 = top, 100 = bottom)
374
+ // result.rank — bigint 0-indexed from top
375
+ // result.gasFromTop — bigint accumulated gas above this tip
376
+ ```
377
+
378
+ ## UI labels
379
+
380
+ ```ts
381
+ import { defaultInclusionLabels, inclusionLabel, TierName } from '@valve-tech/gas-oracle'
382
+
383
+ defaultInclusionLabels[TierName.standard] // 'Next block'
384
+
385
+ // Locale / branded copy via partial overrides — no fork:
386
+ const es = { [TierName.standard]: 'Próximo bloque' }
387
+ inclusionLabel(TierName.standard, es) // 'Próximo bloque'
388
+ inclusionLabel(TierName.slow, es) // falls back to default English
389
+ ```
390
+
391
+ ## Chain presets
392
+
393
+ ```ts
394
+ import { createGasOracle, chainPresets, presetForChainId } from '@valve-tech/gas-oracle'
395
+
396
+ // Static — direct preset access
397
+ createGasOracle({ client, ...chainPresets.pulsechain })
398
+
399
+ // Dynamic — runtime lookup by chainId
400
+ const preset = presetForChainId(chainId)
401
+ createGasOracle({ client, chainId, ...preset })
402
+ ```
403
+
404
+ PulseChain (chain 369) is the only entry shipped today. Adding more
405
+ requires verifying the chain's actual validator behavior against
406
+ block-level data; the default (eip1559) is correct for every chain we
407
+ haven't proven otherwise.
408
+
312
409
  ## viem integration
313
410
 
314
411
  ### Subpath: `@valve-tech/gas-oracle/viem-actions`
@@ -317,12 +414,13 @@ Extension surface for callers who want explicit access to tier shapes:
317
414
 
318
415
  ```ts
319
416
  import { gasOracleActions } from '@valve-tech/gas-oracle/viem-actions'
417
+ import { PriorityModel } from '@valve-tech/gas-oracle'
320
418
 
321
419
  const client = createPublicClient({ chain: mainnet, transport: http() })
322
420
  .extend(gasOracleActions({
323
421
  chainId: 1,
324
422
  priorityFeeDecayCap: parseEther('0.125'),
325
- priorityModel: 'eip1559',
423
+ priorityModel: PriorityModel.eip1559,
326
424
  }))
327
425
 
328
426
  await client.getGasTiers() // full snapshot
@@ -343,11 +441,12 @@ work better* — `useFeeData`, `walletClient.sendTransaction({...})`,
343
441
 
344
442
  ```ts
345
443
  import { withGasOracle } from '@valve-tech/gas-oracle/viem-transport'
444
+ import { PriorityModel } from '@valve-tech/gas-oracle'
346
445
 
347
446
  const transport = withGasOracle(http(rpcUrl), {
348
447
  chainId: 1,
349
448
  priorityFeeDecayCap: parseEther('0.125'),
350
- priorityModel: 'eip1559',
449
+ priorityModel: PriorityModel.eip1559,
351
450
  intercept: {
352
451
  eth_gasFeeEstimate: true, // additive (default on)
353
452
  eth_maxPriorityFeePerGas: 'fast', // tier required for standard methods
@@ -48,10 +48,10 @@ import type { TxIdentifier } from './mempool.js';
48
48
  */
49
49
  export type BlockPositionQuery = {
50
50
  kind: 'rank';
51
- rank: number;
51
+ rank: bigint;
52
52
  } | {
53
53
  kind: 'percentile';
54
- percentile: number;
54
+ percentile: bigint;
55
55
  } | {
56
56
  kind: 'gasFromTop';
57
57
  gas: bigint;
@@ -79,7 +79,7 @@ export interface BlockPositionResult {
79
79
  */
80
80
  pivot: TipSample | null;
81
81
  /** Approximate rank of the resolved position, 0-indexed from top. */
82
- rank: number;
82
+ rank: bigint;
83
83
  /** Approximate gas-from-top of the resolved position. */
84
84
  gasFromTop: bigint;
85
85
  }
@@ -1 +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"}
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;AAoDD;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAC9B,SAAS,SAAS,EAAE,EACpB,OAAO,kBAAkB,KACxB,mBAiEF,CAAA"}
@@ -20,7 +20,11 @@
20
20
  * Equality at the same tip is validator-policy-dependent and not
21
21
  * guaranteed, so "outbid by 1 wei" is the honest minimum.
22
22
  */
23
- const sortByTipDesc = (samples) => [...samples].sort((a, b) => (a.tip > b.tip ? -1 : a.tip < b.tip ? 1 : 0));
23
+ const sortByTipDesc = (samples) =>
24
+ // Equal-tip arm folded into the descending side — order between
25
+ // equal-tip samples is unspecified and consumers shouldn't rely on
26
+ // it (mempool ordering is provider-dependent anyway).
27
+ [...samples].sort((a, b) => (a.tip > b.tip ? -1 : 1));
24
28
  const matchesIdentifier = (sample, id) => {
25
29
  if ('hash' in id) {
26
30
  return (typeof sample.hash === 'string' &&
@@ -35,31 +39,33 @@ const matchesIdentifier = (sample, id) => {
35
39
  /**
36
40
  * Walk samples in tip-desc order accumulating gas; return the index
37
41
  * (0-based from top) where cumulative gas first crosses `targetGas`.
38
- * Returns -1 when the target exceeds the sum of all gas (the position
42
+ * Returns -1n when the target exceeds the sum of all gas (the position
39
43
  * is below the whole distribution).
40
44
  */
41
45
  const indexAtGasOffset = (sorted, targetGas) => {
42
46
  if (targetGas <= 0n)
43
- return 0;
47
+ return 0n;
44
48
  let cumulative = 0n;
45
- for (let i = 0; i < sorted.length; i += 1) {
46
- cumulative += sorted[i].gas;
49
+ const len = BigInt(sorted.length);
50
+ for (let i = 0n; i < len; i += 1n) {
51
+ cumulative += sorted[Number(i)].gas;
47
52
  if (cumulative > targetGas)
48
53
  return i;
49
54
  }
50
- return -1;
55
+ return -1n;
51
56
  };
52
57
  const sumGasUpTo = (sorted, indexExclusive) => {
53
58
  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;
59
+ const len = BigInt(sorted.length);
60
+ const upper = indexExclusive < len ? indexExclusive : len;
61
+ for (let i = 0n; i < upper; i += 1n)
62
+ g += sorted[Number(i)].gas;
57
63
  return g;
58
64
  };
59
65
  const empty = () => ({
60
66
  requiredTip: 0n,
61
67
  pivot: null,
62
- rank: 0,
68
+ rank: 0n,
63
69
  gasFromTop: 0n,
64
70
  });
65
71
  /**
@@ -76,6 +82,7 @@ export const tipForBlockPosition = (samples, query) => {
76
82
  if (samples.length === 0)
77
83
  return empty();
78
84
  const sorted = sortByTipDesc(samples);
85
+ const len = BigInt(sorted.length);
79
86
  let pivotIndex;
80
87
  let beatPivot; // true = outbid (tip+1), false = undercut (tip-1)
81
88
  switch (query.kind) {
@@ -85,12 +92,16 @@ export const tipForBlockPosition = (samples, query) => {
85
92
  break;
86
93
  }
87
94
  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);
95
+ // 0% = top of block (highest tip); 100% = bottom. Clamp to [0n, 100n].
96
+ const pct = query.percentile < 0n
97
+ ? 0n
98
+ : query.percentile > 100n
99
+ ? 100n
100
+ : query.percentile;
101
+ pivotIndex = (len * pct) / 100n;
91
102
  // Edge: percentile=100 lands at length, which is "below everything"
92
- if (pivotIndex >= sorted.length)
93
- pivotIndex = sorted.length - 1;
103
+ if (pivotIndex >= len)
104
+ pivotIndex = len - 1n;
94
105
  beatPivot = true;
95
106
  break;
96
107
  }
@@ -101,21 +112,22 @@ export const tipForBlockPosition = (samples, query) => {
101
112
  }
102
113
  case 'aheadOf':
103
114
  case 'behind': {
104
- pivotIndex = sorted.findIndex((s) => matchesIdentifier(s, query.tx));
115
+ const found = sorted.findIndex((s) => matchesIdentifier(s, query.tx));
116
+ pivotIndex = found === -1 ? -1n : BigInt(found);
105
117
  beatPivot = query.kind === 'aheadOf';
106
118
  break;
107
119
  }
108
120
  }
109
- if (pivotIndex < 0 || pivotIndex >= sorted.length) {
121
+ if (pivotIndex < 0n || pivotIndex >= len) {
110
122
  // Position is below everyone (or pivot not found) — pay nothing in priority
111
123
  return {
112
124
  requiredTip: 0n,
113
125
  pivot: null,
114
- rank: sorted.length,
115
- gasFromTop: sumGasUpTo(sorted, sorted.length),
126
+ rank: len,
127
+ gasFromTop: sumGasUpTo(sorted, len),
116
128
  };
117
129
  }
118
- const pivot = sorted[pivotIndex];
130
+ const pivot = sorted[Number(pivotIndex)];
119
131
  const requiredTip = beatPivot
120
132
  ? pivot.tip + 1n
121
133
  : pivot.tip > 0n
@@ -1 +1 @@
1
- {"version":3,"file":"block-position.js","sourceRoot":"","sources":["../src/block-position.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"block-position.js","sourceRoot":"","sources":["../src/block-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AA0DH,MAAM,aAAa,GAAG,CAAC,OAAoB,EAAe,EAAE;AAC1D,gEAAgE;AAChE,mEAAmE;AACnE,sDAAsD;AACtD,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAEvD,MAAM,iBAAiB,GAAG,CAAC,MAAiB,EAAE,EAAgB,EAAW,EAAE;IACzE,IAAI,MAAM,IAAI,EAAE,EAAE,CAAC;QACjB,OAAO,CACL,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ;YAC/B,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CACpD,CAAA;IACH,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IAC5E,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE;QAAE,OAAO,KAAK,CAAA;IAC3E,OAAO,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAA;AACrD,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,gBAAgB,GAAG,CAAC,MAAmB,EAAE,SAAiB,EAAU,EAAE;IAC1E,IAAI,SAAS,IAAI,EAAE;QAAE,OAAO,EAAE,CAAA;IAC9B,IAAI,UAAU,GAAG,EAAE,CAAA;IACnB,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACjC,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;QAClC,UAAU,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA;QACnC,IAAI,UAAU,GAAG,SAAS;YAAE,OAAO,CAAC,CAAA;IACtC,CAAC;IACD,OAAO,CAAC,EAAE,CAAA;AACZ,CAAC,CAAA;AAED,MAAM,UAAU,GAAG,CAAC,MAAmB,EAAE,cAAsB,EAAU,EAAE;IACzE,IAAI,CAAC,GAAG,EAAE,CAAA;IACV,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACjC,MAAM,KAAK,GAAG,cAAc,GAAG,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAA;IACzD,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,EAAE;QAAE,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA;IAC/D,OAAO,CAAC,CAAA;AACV,CAAC,CAAA;AAED,MAAM,KAAK,GAAG,GAAwB,EAAE,CAAC,CAAC;IACxC,WAAW,EAAE,EAAE;IACf,KAAK,EAAE,IAAI;IACX,IAAI,EAAE,EAAE;IACR,UAAU,EAAE,EAAE;CACf,CAAC,CAAA;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,OAAoB,EACpB,KAAyB,EACJ,EAAE;IACvB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,EAAE,CAAA;IACxC,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,CAAA;IACrC,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAEjC,IAAI,UAAkB,CAAA;IACtB,IAAI,SAAkB,CAAA,CAAC,kDAAkD;IAEzE,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,UAAU,GAAG,KAAK,CAAC,IAAI,CAAA;YACvB,SAAS,GAAG,IAAI,CAAA;YAChB,MAAK;QACP,CAAC;QACD,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,uEAAuE;YACvE,MAAM,GAAG,GACP,KAAK,CAAC,UAAU,GAAG,EAAE;gBACnB,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI;oBACvB,CAAC,CAAC,IAAI;oBACN,CAAC,CAAC,KAAK,CAAC,UAAU,CAAA;YACxB,UAAU,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,CAAA;YAC/B,oEAAoE;YACpE,IAAI,UAAU,IAAI,GAAG;gBAAE,UAAU,GAAG,GAAG,GAAG,EAAE,CAAA;YAC5C,SAAS,GAAG,IAAI,CAAA;YAChB,MAAK;QACP,CAAC;QACD,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,UAAU,GAAG,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;YAChD,SAAS,GAAG,IAAI,CAAA;YAChB,MAAK;QACP,CAAC;QACD,KAAK,SAAS,CAAC;QACf,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;YACrE,UAAU,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YAC/C,SAAS,GAAG,KAAK,CAAC,IAAI,KAAK,SAAS,CAAA;YACpC,MAAK;QACP,CAAC;IACH,CAAC;IAED,IAAI,UAAU,GAAG,EAAE,IAAI,UAAU,IAAI,GAAG,EAAE,CAAC;QACzC,4EAA4E;QAC5E,OAAO;YACL,WAAW,EAAE,EAAE;YACf,KAAK,EAAE,IAAI;YACX,IAAI,EAAE,GAAG;YACT,UAAU,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC;SACpC,CAAA;IACH,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAA;IACxC,MAAM,WAAW,GAAG,SAAS;QAC3B,CAAC,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE;QAChB,CAAC,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE;YACd,CAAC,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE;YAChB,CAAC,CAAC,EAAE,CAAA;IAER,OAAO;QACL,WAAW;QACX,KAAK;QACL,IAAI,EAAE,UAAU;QAChB,UAAU,EAAE,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC;KAC3C,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Inverse of `tipForBlockPosition`. Given a tip and a snapshot, find
3
+ * where the tip would land in the live distribution and which named
4
+ * tier it falls in. Pure: no I/O, no oracle dependency, no wall-clock.
5
+ *
6
+ * Useful for "you priced at the Xth percentile" UI affordances and for
7
+ * post-hoc "why is my tx slow" diagnostics.
8
+ */
9
+ import { TierName, type GasOracleState } from './types.js';
10
+ export interface ClassifyTipResult {
11
+ /**
12
+ * Named tier the tip falls in. `null` when below `TierName.slow`'s
13
+ * `maxPriorityFeePerGas` floor.
14
+ */
15
+ tier: TierName | null;
16
+ /**
17
+ * `maxPriorityFeePerGas` floor of the next tier above `tier`. `null`
18
+ * when `tier === TierName.instant` (already at top).
19
+ */
20
+ requiredForNextTier: bigint | null;
21
+ /**
22
+ * Approximate percentile in the live distribution (block ring tips +
23
+ * mempool samples), 0n..100n. `0n` = top of block (highest tip);
24
+ * `100n` = bottom. `0n` when distribution empty.
25
+ */
26
+ percentile: bigint;
27
+ /** Approximate rank, 0n-indexed from top. `0n` when distribution empty. */
28
+ rank: bigint;
29
+ /** Accumulated gas above this tip's position. `0n` when empty. */
30
+ gasFromTop: bigint;
31
+ }
32
+ export declare const classifyTip: (snapshot: GasOracleState, tipWei: bigint) => ClassifyTipResult;
33
+ //# sourceMappingURL=classify-tip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify-tip.d.ts","sourceRoot":"","sources":["../src/classify-tip.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEL,QAAQ,EACR,KAAK,cAAc,EAEpB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAA;IACrB;;;OAGG;IACH,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAA;IACZ,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAA;CACnB;AA4BD,eAAO,MAAM,WAAW,GACtB,UAAU,cAAc,EACxB,QAAQ,MAAM,KACb,iBA0BF,CAAA"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Inverse of `tipForBlockPosition`. Given a tip and a snapshot, find
3
+ * where the tip would land in the live distribution and which named
4
+ * tier it falls in. Pure: no I/O, no oracle dependency, no wall-clock.
5
+ *
6
+ * Useful for "you priced at the Xth percentile" UI affordances and for
7
+ * post-hoc "why is my tx slow" diagnostics.
8
+ */
9
+ import { TIER_LADDER, } from './types.js';
10
+ const tierForTip = (tiers, tipWei) => {
11
+ for (let i = TIER_LADDER.length - 1; i >= 0; i -= 1) {
12
+ const tier = TIER_LADDER[i];
13
+ if (tipWei >= tiers[tier].maxPriorityFeePerGas)
14
+ return tier;
15
+ }
16
+ return null;
17
+ };
18
+ const requiredForNextTierAbove = (tiers, currentTier) => {
19
+ const currentIndex = currentTier ? TIER_LADDER.indexOf(currentTier) : -1;
20
+ const nextIndex = currentIndex + 1;
21
+ if (nextIndex >= TIER_LADDER.length)
22
+ return null;
23
+ return tiers[TIER_LADDER[nextIndex]].maxPriorityFeePerGas;
24
+ };
25
+ const collectDistribution = (snapshot) => [
26
+ ...snapshot.ring.flatMap((block) => block.tips),
27
+ ...snapshot.mempoolSamples,
28
+ ];
29
+ export const classifyTip = (snapshot, tipWei) => {
30
+ const tier = tierForTip(snapshot.tiers, tipWei);
31
+ const requiredForNextTier = requiredForNextTierAbove(snapshot.tiers, tier);
32
+ const samples = collectDistribution(snapshot);
33
+ if (samples.length === 0) {
34
+ return { tier, requiredForNextTier, percentile: 0n, rank: 0n, gasFromTop: 0n };
35
+ }
36
+ // Sort by tip desc, equal-tip arm folded into descending side
37
+ // (matches block-position.ts convention).
38
+ const sorted = [...samples].sort((a, b) => (a.tip > b.tip ? -1 : 1));
39
+ const firstWeakerIndexNum = sorted.findIndex((s) => s.tip <= tipWei);
40
+ const samplesLen = BigInt(sorted.length);
41
+ const rank = firstWeakerIndexNum === -1
42
+ ? samplesLen
43
+ : BigInt(firstWeakerIndexNum);
44
+ // Round-half-away-from-zero percentile, all bigint. samplesLen >= 1n
45
+ // is guaranteed by the early-return on empty samples above.
46
+ const percentile = (rank * 100n + samplesLen / 2n) / samplesLen;
47
+ let gasFromTop = 0n;
48
+ for (let i = 0n; i < rank; i += 1n)
49
+ gasFromTop += sorted[Number(i)].gas;
50
+ return { tier, requiredForNextTier, percentile, rank, gasFromTop };
51
+ };
52
+ //# sourceMappingURL=classify-tip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify-tip.js","sourceRoot":"","sources":["../src/classify-tip.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Default English UI copy mapping each tier to a user-facing inclusion
3
+ * label, plus a small helper that resolves a tier with optional partial
4
+ * overrides (locale or branded copy without forking the whole map).
5
+ *
6
+ * Conservative phrasing — labels describe relative position in the next
7
+ * block, not hard guarantees. Real inclusion is probabilistic.
8
+ */
9
+ import { TierName } from './types.js';
10
+ export declare const defaultInclusionLabels: Record<TierName, string>;
11
+ /**
12
+ * Resolve a tier to its inclusion label, falling back to
13
+ * `defaultInclusionLabels` for any tier not present in `overrides`.
14
+ *
15
+ * Locale / branded-copy pattern (no fork required):
16
+ *
17
+ * ```ts
18
+ * const es: Partial<Record<TierName, string>> = {
19
+ * [TierName.standard]: 'Próximo bloque',
20
+ * [TierName.fast]: 'Cabeza del próximo bloque',
21
+ * }
22
+ * inclusionLabel(TierName.standard, es) // 'Próximo bloque'
23
+ * inclusionLabel(TierName.slow, es) // falls back to default English
24
+ * ```
25
+ *
26
+ * Consumers can also fully replace the map by spreading:
27
+ * `const myLabels = { ...defaultInclusionLabels, ...partial }`.
28
+ */
29
+ export declare const inclusionLabel: (tier: TierName, overrides?: Partial<Record<TierName, string>>) => string;
30
+ //# sourceMappingURL=inclusion-labels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inclusion-labels.d.ts","sourceRoot":"","sources":["../src/inclusion-labels.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAErC,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAK3D,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,QAAQ,EACd,YAAY,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,KAC5C,MAA2D,CAAA"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Default English UI copy mapping each tier to a user-facing inclusion
3
+ * label, plus a small helper that resolves a tier with optional partial
4
+ * overrides (locale or branded copy without forking the whole map).
5
+ *
6
+ * Conservative phrasing — labels describe relative position in the next
7
+ * block, not hard guarantees. Real inclusion is probabilistic.
8
+ */
9
+ import { TierName } from './types.js';
10
+ export const defaultInclusionLabels = {
11
+ [TierName.slow]: 'Within a few blocks',
12
+ [TierName.standard]: 'Next block',
13
+ [TierName.fast]: 'Top of next block',
14
+ [TierName.instant]: 'Front of next block',
15
+ };
16
+ /**
17
+ * Resolve a tier to its inclusion label, falling back to
18
+ * `defaultInclusionLabels` for any tier not present in `overrides`.
19
+ *
20
+ * Locale / branded-copy pattern (no fork required):
21
+ *
22
+ * ```ts
23
+ * const es: Partial<Record<TierName, string>> = {
24
+ * [TierName.standard]: 'Próximo bloque',
25
+ * [TierName.fast]: 'Cabeza del próximo bloque',
26
+ * }
27
+ * inclusionLabel(TierName.standard, es) // 'Próximo bloque'
28
+ * inclusionLabel(TierName.slow, es) // falls back to default English
29
+ * ```
30
+ *
31
+ * Consumers can also fully replace the map by spreading:
32
+ * `const myLabels = { ...defaultInclusionLabels, ...partial }`.
33
+ */
34
+ export const inclusionLabel = (tier, overrides) => overrides?.[tier] ?? defaultInclusionLabels[tier];
35
+ //# sourceMappingURL=inclusion-labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inclusion-labels.js","sourceRoot":"","sources":["../src/inclusion-labels.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAErC,MAAM,CAAC,MAAM,sBAAsB,GAA6B;IAC9D,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,qBAAqB;IACtC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY;IACjC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,mBAAmB;IACpC,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB;CAC1C,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,IAAc,EACd,SAA6C,EACrC,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,IAAI,sBAAsB,CAAC,IAAI,CAAC,CAAA"}
package/dist/index.d.ts CHANGED
@@ -16,9 +16,15 @@ export { createGasOracle, reducePollInputs, sampleGasFees, type CreateGasOracleO
16
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
17
  export { blockToSample, mempoolToSamples, } from './samples.js';
18
18
  export { fetchOracleInputs, fetchHeadBlockNumber, 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';
19
+ export type { RawTx, TipPercentiles, TierRecommendation, MempoolStats, BlobStats, BlockSample, GasOracleState, TipSample, PollOptions, } from './types.js';
20
+ export { PriorityModel, TierName, TIER_LADDER, Trend, TxType } from './types.js';
20
21
  export { normalizeMempool, findByHash, findByAddressNonce, findInMempool, } from './mempool.js';
21
22
  export type { NormalizedMempool, TxIdentifier, MempoolBucket, MempoolHit, } from './mempool.js';
22
23
  export { tipForBlockPosition } from './block-position.js';
23
24
  export type { BlockPositionQuery, BlockPositionResult, } from './block-position.js';
25
+ export { minimumReplacementFee, bumpForReplacement, recommendBumpTier, BumpStrategy, ReplacementBumpPercent, } from './replacement.js';
26
+ export type { RecommendBumpTierOptions, ReplacementGas, } from './replacement.js';
27
+ export { classifyTip, type ClassifyTipResult } from './classify-tip.js';
28
+ export { defaultInclusionLabels, inclusionLabel } from './inclusion-labels.js';
29
+ export { chainPresets, presetForChainId, type ChainPreset, } from './presets.js';
24
30
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,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,oBAAoB,EACpB,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,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,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA;AAEvB,YAAY,EACV,KAAK,EACL,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,SAAS,EACT,WAAW,EACX,cAAc,EACd,SAAS,EACT,WAAW,GACZ,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAGhF,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;AAG5B,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,sBAAsB,GACvB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,wBAAwB,EACxB,cAAc,GACf,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAE,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAGvE,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAG9E,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,KAAK,WAAW,GACjB,MAAM,cAAc,CAAA"}