@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
@@ -0,0 +1,108 @@
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
+ const normalizeSubpool = (sub) => {
23
+ if (!sub)
24
+ return {};
25
+ const out = {};
26
+ for (const [address, byNonce] of Object.entries(sub)) {
27
+ const normalizedByNonce = {};
28
+ for (const [nonce, tx] of Object.entries(byNonce)) {
29
+ normalizedByNonce[BigInt(nonce).toString()] = tx;
30
+ }
31
+ out[address.toLowerCase()] = normalizedByNonce;
32
+ }
33
+ return out;
34
+ };
35
+ /**
36
+ * Run the upstream `txpool_content` payload through one normalization
37
+ * pass: lowercase every sender address, decimal-ify every nonce key.
38
+ * Idempotent — re-normalizing a NormalizedMempool returns an
39
+ * equivalent NormalizedMempool. Pass `null`/`undefined` to get an
40
+ * empty NormalizedMempool back (lookup helpers handle null too, but
41
+ * always-having-a-shape simplifies callers that store the snapshot).
42
+ */
43
+ export const normalizeMempool = (pool) => ({
44
+ pending: normalizeSubpool(pool?.pending),
45
+ queued: normalizeSubpool(pool?.queued),
46
+ });
47
+ const searchSubpool = (subpool, bucket, predicate) => {
48
+ if (!subpool)
49
+ return null;
50
+ for (const [address, byNonce] of Object.entries(subpool)) {
51
+ for (const [nonce, tx] of Object.entries(byNonce)) {
52
+ if (predicate(tx)) {
53
+ return { tx, bucket, address, nonce };
54
+ }
55
+ }
56
+ }
57
+ return null;
58
+ };
59
+ const eq = (a, b) => typeof a === 'string' && a.toLowerCase() === b.toLowerCase();
60
+ /**
61
+ * Find a tx in `pending` or `queued` by hash. Searches `pending` first
62
+ * — the common "is my tx going to mine?" question is most often
63
+ * answered there. Hash comparison is case-insensitive (tx.hash on the
64
+ * stored RawTx may still be checksum-mixed-case from upstream; we
65
+ * don't rewrite per-tx fields during pool normalization, only the
66
+ * outer keys).
67
+ */
68
+ export const findByHash = (pool, hash) => {
69
+ if (!pool || !hash)
70
+ return null;
71
+ const pending = searchSubpool(pool.pending, 'pending', (tx) => eq(tx.hash, hash));
72
+ if (pending)
73
+ return pending;
74
+ return searchSubpool(pool.queued, 'queued', (tx) => eq(tx.hash, hash));
75
+ };
76
+ /**
77
+ * Find a tx by sender address + nonce. Direct two-key lookup — no
78
+ * scan, no case-walk, because the pool is normalized at ingest.
79
+ * `nonce` accepts a number, a decimal string (`'5'`), a hex string
80
+ * (`'0x5'`), or a bigint. Returns `null` when the pool is null/empty,
81
+ * the address has no entries, or the address is present but the nonce
82
+ * isn't.
83
+ */
84
+ export const findByAddressNonce = (pool, address, nonce) => {
85
+ if (!pool || !address)
86
+ return null;
87
+ const lowerAddr = address.toLowerCase();
88
+ // BigInt() handles every accepted form: 5, 5n, '5', '0x5'. The .toString()
89
+ // is the canonical decimal form the normalized pool keys are written in.
90
+ const decimalNonce = BigInt(nonce).toString();
91
+ const probe = (subpool, bucket) => {
92
+ const tx = subpool?.[lowerAddr]?.[decimalNonce];
93
+ return tx ? { tx, bucket, address: lowerAddr, nonce: decimalNonce } : null;
94
+ };
95
+ return probe(pool.pending, 'pending') ?? probe(pool.queued, 'queued');
96
+ };
97
+ /**
98
+ * Single-call lookup that takes a discriminated `TxIdentifier`. Use
99
+ * when you don't statically know which dimension you're querying on
100
+ * (e.g. wiring up a UI that lets the user paste either a hash or an
101
+ * address + nonce).
102
+ */
103
+ export const findInMempool = (pool, id) => {
104
+ if ('hash' in id)
105
+ return findByHash(pool, id.hash);
106
+ return findByAddressNonce(pool, id.address, id.nonce);
107
+ };
108
+ //# sourceMappingURL=mempool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mempool.js","sourceRoot":"","sources":["../src/mempool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Gas oracle factory. Owns:
3
+ *
4
+ * - the polling lifecycle (start/stop interval timer)
5
+ * - in-memory state for one chain
6
+ * - a subscriber list so callers can mirror published state to Redis,
7
+ * a websocket, a metrics gauge, etc. — the package itself stays
8
+ * transport-agnostic and zero-deps beyond the viem peer.
9
+ *
10
+ * One oracle instance per chain. If you need many chains in one
11
+ * process, instantiate many oracles. They don't share any state.
12
+ */
13
+ import type { PublicClient } from 'viem';
14
+ import { type OraclePollInputs } from './transport.js';
15
+ import { type NormalizedMempool } from './mempool.js';
16
+ import type { GasOracleState, PollOptions, PriorityModel } from './types.js';
17
+ export interface CreateGasOracleOptions {
18
+ /** viem PublicClient pointed at the upstream RPC for this chain. */
19
+ client: PublicClient;
20
+ /** EVM chain ID. Echoed back in `state.chainId`; not validated. */
21
+ chainId: number;
22
+ /** Polling interval in ms. Default 10_000. */
23
+ pollIntervalMs?: number;
24
+ /**
25
+ * Optional error sink. Called for each sub-RPC that fails inside a
26
+ * poll cycle. Default behavior is to swallow — the oracle keeps running
27
+ * on partial data. Useful for routing failures into a debug logger or
28
+ * a metrics counter.
29
+ */
30
+ onError?: (method: string, err: unknown) => void;
31
+ /**
32
+ * Maximum fraction the published priority-fee tip may drop per block
33
+ * elapsed since the last publish. Wad-scaled bigint: 1e18 = 100%.
34
+ * Pass `parseEther('0.125')` for the default 12.5%/block (EIP-1559
35
+ * parity). `null` disables capping entirely. Validated at construction
36
+ * to fall in `[0n, WAD]` when not null.
37
+ */
38
+ priorityFeeDecayCap?: bigint | null;
39
+ /**
40
+ * Where the chain draws its priority cutoff in the EIP-2718 type space.
41
+ * `'flat'` (default) treats every tx equally — right for extractive
42
+ * networks. `'eip1559'` derives standard/fast/instant from type-2+
43
+ * samples only — right for chains that honor 1559 ordering.
44
+ */
45
+ priorityModel?: PriorityModel;
46
+ /**
47
+ * How many blocks the published recommendation should survive in the
48
+ * worst case. The buffered base fee underpinning `maxFeePerGas` becomes
49
+ * `baseFee * (9/8)^N` (the EIP-1559 worst-case rise compounded), so a
50
+ * tx submitted with the current snapshot still lands within `N` blocks
51
+ * even if every intervening block is full. Default `1` (one block of
52
+ * headroom — matches pre-v0.2 behavior). A wallet that wants its
53
+ * recommendation to last ~1.5 minutes on Ethereum (~6 blocks at 12s)
54
+ * sets this to `6`. Validated `>= 1`.
55
+ */
56
+ baseFeeLivenessBlocks?: number;
57
+ /**
58
+ * Producer-side toggles for upstream RPC calls. `feeHistory` and
59
+ * `mempool` default true; setting either to false skips that RPC
60
+ * entirely each cycle. `eth_getBlockByNumber` is not toggleable.
61
+ */
62
+ poll?: PollOptions;
63
+ /**
64
+ * When `true`, the oracle retains the latest normalized mempool
65
+ * snapshot and exposes it via `getMempoolSnapshot()`. The snapshot
66
+ * powers `findInMempool` / `tipForBlockPosition({ kind: 'aheadOf' })`-
67
+ * style lookups without a second RPC roundtrip. Memory cost is the
68
+ * size of one `txpool_content` payload (5–15MB on busy ETH mainnet);
69
+ * keep this off in browser/mobile contexts. Default `false`.
70
+ */
71
+ keepMempoolSnapshot?: boolean;
72
+ }
73
+ export interface GasOracle {
74
+ /** Begin the poll loop. Idempotent — calling twice is a no-op. */
75
+ start: () => void;
76
+ /** Stop polling and clear in-memory state. */
77
+ stop: () => void;
78
+ /** Latest known state, or null if no successful poll has completed yet. */
79
+ getState: () => GasOracleState | null;
80
+ /**
81
+ * Latest normalized mempool snapshot, or `null` if `keepMempoolSnapshot`
82
+ * is off, no poll has yet succeeded, or the most recent poll's
83
+ * `txpool_content` failed (provider gated, etc.). Always returns the
84
+ * normalized form — addresses lowercase, nonces decimalized — so
85
+ * callers can pass it directly into `findInMempool` /
86
+ * `tipForBlockPosition`.
87
+ */
88
+ getMempoolSnapshot: () => NormalizedMempool | null;
89
+ /**
90
+ * Run one poll cycle out-of-band and return the resulting state.
91
+ * Useful for tests, on-demand refreshes, or running without the
92
+ * interval timer (e.g., serverless). Resolves to null if the cycle
93
+ * couldn't produce state (no block fetched).
94
+ */
95
+ pollOnce: () => Promise<GasOracleState | null>;
96
+ /**
97
+ * Subscribe to state updates. The callback fires after every
98
+ * successful poll cycle with the new state. Returns an unsubscribe
99
+ * function. Subscribers fire synchronously; if a subscriber throws,
100
+ * the error is swallowed so one bad consumer can't take down the
101
+ * oracle's other listeners.
102
+ */
103
+ subscribe: (cb: (state: GasOracleState) => void) => () => void;
104
+ }
105
+ /**
106
+ * Reduce a poll cycle's RPC outputs into a new oracle state, using the
107
+ * previous state to anchor the cap-decay and the base-fee history.
108
+ *
109
+ * Pure: no I/O, no global state, no time. Test by feeding it fixture
110
+ * inputs and asserting the returned shape.
111
+ */
112
+ export declare const reducePollInputs: (input: {
113
+ inputs: OraclePollInputs;
114
+ chainId: number;
115
+ prev: GasOracleState | null;
116
+ priorityFeeDecayCap?: bigint | null;
117
+ priorityModel?: PriorityModel;
118
+ baseFeeLivenessBlocks?: number;
119
+ }) => GasOracleState | null;
120
+ /**
121
+ * Build a configured oracle. Nothing happens until you call `start()`.
122
+ *
123
+ * @example
124
+ * import { createPublicClient, http } from 'viem'
125
+ * import { mainnet } from 'viem/chains'
126
+ * import { createGasOracle } from '@valve-tech/gas-oracle'
127
+ *
128
+ * const client = createPublicClient({ chain: mainnet, transport: http() })
129
+ * const oracle = createGasOracle({ client, chainId: 1 })
130
+ * oracle.subscribe((state) => publishToRedis(state))
131
+ * oracle.start()
132
+ *
133
+ * // Later — sub-ms read, no RPC:
134
+ * const tier = oracle.getState()?.tiers.standard
135
+ */
136
+ export declare const createGasOracle: (options: CreateGasOracleOptions) => GasOracle;
137
+ export type { OraclePollInputs } from './transport.js';
138
+ export type { TierName } from './types.js';
139
+ //# sourceMappingURL=oracle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oracle.d.ts","sourceRoot":"","sources":["../src/oracle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAA;AAExC,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAOzE,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAEvE,OAAO,KAAK,EAEV,cAAc,EAEd,WAAW,EACX,aAAa,EACd,MAAM,YAAY,CAAA;AAMnB,MAAM,WAAW,sBAAsB;IACrC,oEAAoE;IACpE,MAAM,EAAE,YAAY,CAAA;IACpB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAA;IACf,8CAA8C;IAC9C,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IAChD;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B;;;;;;;;;OASG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B;;;;OAIG;IACH,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAED,MAAM,WAAW,SAAS;IACxB,kEAAkE;IAClE,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,8CAA8C;IAC9C,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,cAAc,GAAG,IAAI,CAAA;IACrC;;;;;;;OAOG;IACH,kBAAkB,EAAE,MAAM,iBAAiB,GAAG,IAAI,CAAA;IAClD;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAA;IAC9C;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,KAAK,MAAM,IAAI,CAAA;CAC/D;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO;IACtC,MAAM,EAAE,gBAAgB,CAAA;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,cAAc,GAAG,IAAI,CAAA;IAC3B,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAA;CAC/B,KAAG,cAAc,GAAG,IA6FpB,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,sBAAsB,KAAG,SAsFjE,CAAA;AAID,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACtD,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA"}
package/dist/oracle.js ADDED
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Gas oracle factory. Owns:
3
+ *
4
+ * - the polling lifecycle (start/stop interval timer)
5
+ * - in-memory state for one chain
6
+ * - a subscriber list so callers can mirror published state to Redis,
7
+ * a websocket, a metrics gauge, etc. — the package itself stays
8
+ * transport-agnostic and zero-deps beyond the viem peer.
9
+ *
10
+ * One oracle instance per chain. If you need many chains in one
11
+ * process, instantiate many oracles. They don't share any state.
12
+ */
13
+ import { fetchOracleInputs } from './transport.js';
14
+ import { computeBlobBaseFee, computeTiers, detectTrend, flattenTxPool, } from './math.js';
15
+ import { normalizeMempool } from './mempool.js';
16
+ import { blockToSample, mempoolToSamples } from './samples.js';
17
+ const DEFAULT_POLL_INTERVAL_MS = 10000;
18
+ const TREND_WINDOW = 5;
19
+ const WAD = 1000000000000000000n;
20
+ /**
21
+ * Reduce a poll cycle's RPC outputs into a new oracle state, using the
22
+ * previous state to anchor the cap-decay and the base-fee history.
23
+ *
24
+ * Pure: no I/O, no global state, no time. Test by feeding it fixture
25
+ * inputs and asserting the returned shape.
26
+ */
27
+ export const reducePollInputs = (input) => {
28
+ const { block, feeHistory, txPool } = input.inputs;
29
+ if (!block)
30
+ return null;
31
+ const blockNumber = BigInt(block.number);
32
+ const timestamp = BigInt(block.timestamp);
33
+ const baseFee = BigInt(block.baseFeePerGas);
34
+ const blockGasLimit = BigInt(block.gasLimit);
35
+ // Base-fee history: prefer feeHistory's window, fall back to a
36
+ // single-element array so detectTrend has something to chew on.
37
+ const baseFeeHistory = feeHistory
38
+ ? feeHistory.baseFeePerGas.map((hex) => BigInt(hex))
39
+ : [baseFee];
40
+ const baseFeeTrend = detectTrend(baseFeeHistory.slice(-TREND_WINDOW));
41
+ // Build the rolling ring. The full 20-block append/bridgeGap/clear
42
+ // lifecycle is deferred (spec §7-§9); for now we keep window = 1 by
43
+ // populating ring as a single-element array from the current block.
44
+ // Forward-compatible: callers can read `state.ring` as a list of
45
+ // BlockSamples without caring how many entries are in it.
46
+ const blockSample = blockToSample(block);
47
+ const ring = [blockSample];
48
+ const ringSamples = blockSample.tips;
49
+ // Mempool — best-effort signal.
50
+ let mempool = {
51
+ pendingCount: 0,
52
+ queuedCount: 0,
53
+ pendingGasDemand: 0n,
54
+ blockGasLimit,
55
+ };
56
+ let mempoolSamples = mempoolToSamples(txPool, baseFee);
57
+ if (txPool) {
58
+ const pendingTxs = flattenTxPool(txPool.pending);
59
+ const queuedTxs = flattenTxPool(txPool.queued);
60
+ mempool = {
61
+ pendingCount: pendingTxs.length,
62
+ queuedCount: queuedTxs.length,
63
+ pendingGasDemand: pendingTxs.reduce((sum, tx) => sum + (tx.gas ? BigInt(tx.gas) : 0n), 0n),
64
+ blockGasLimit,
65
+ };
66
+ }
67
+ else {
68
+ mempoolSamples = [];
69
+ }
70
+ // EIP-4844 blob — only on chains that expose excessBlobGas.
71
+ let blob = null;
72
+ if (block.excessBlobGas !== undefined) {
73
+ const excessBlobGas = BigInt(block.excessBlobGas);
74
+ const blobGasUsed = BigInt(block.blobGasUsed ?? '0x0');
75
+ const blobBaseFee = computeBlobBaseFee(excessBlobGas);
76
+ const prevBlobHistory = input.prev?.blob ? [input.prev.blob.blobBaseFee, blobBaseFee] : [blobBaseFee];
77
+ blob = {
78
+ blobBaseFee,
79
+ excessBlobGas,
80
+ blobGasUsed,
81
+ blobBaseFeeTrend: detectTrend(prevBlobHistory),
82
+ };
83
+ }
84
+ const blobInput = blob ? { blobBaseFee: blob.blobBaseFee, trend: blob.blobBaseFeeTrend } : null;
85
+ const { tiers, publishedTips } = computeTiers({
86
+ ringSamples,
87
+ mempoolSamples,
88
+ baseFee,
89
+ baseFeeTrend,
90
+ blob: blobInput,
91
+ blockNumber,
92
+ lastPublishedTips: input.prev?.lastPublishedTips,
93
+ lastPublishedBlockNumber: input.prev?.lastPublishedBlockNumber,
94
+ priorityFeeDecayCap: input.priorityFeeDecayCap,
95
+ priorityModel: input.priorityModel,
96
+ baseFeeLivenessBlocks: input.baseFeeLivenessBlocks,
97
+ });
98
+ return {
99
+ chainId: input.chainId,
100
+ blockNumber,
101
+ timestamp,
102
+ baseFee,
103
+ baseFeeTrend,
104
+ baseFeeHistory,
105
+ mempool,
106
+ blob,
107
+ tiers,
108
+ ring,
109
+ lastPublishedTips: publishedTips,
110
+ lastPublishedBlockNumber: blockNumber,
111
+ };
112
+ };
113
+ /**
114
+ * Build a configured oracle. Nothing happens until you call `start()`.
115
+ *
116
+ * @example
117
+ * import { createPublicClient, http } from 'viem'
118
+ * import { mainnet } from 'viem/chains'
119
+ * import { createGasOracle } from '@valve-tech/gas-oracle'
120
+ *
121
+ * const client = createPublicClient({ chain: mainnet, transport: http() })
122
+ * const oracle = createGasOracle({ client, chainId: 1 })
123
+ * oracle.subscribe((state) => publishToRedis(state))
124
+ * oracle.start()
125
+ *
126
+ * // Later — sub-ms read, no RPC:
127
+ * const tier = oracle.getState()?.tiers.standard
128
+ */
129
+ export const createGasOracle = (options) => {
130
+ // Validate at the boundary so misconfigured callers fail fast rather
131
+ // than producing silently-wrong tier numbers later.
132
+ if (options.priorityFeeDecayCap !== undefined &&
133
+ options.priorityFeeDecayCap !== null) {
134
+ const cap = options.priorityFeeDecayCap;
135
+ if (cap < 0n || cap > WAD) {
136
+ throw new Error(`priorityFeeDecayCap must be in [0n, ${WAD}] (wad-scale; null = uncapped); got ${cap}`);
137
+ }
138
+ }
139
+ if (options.baseFeeLivenessBlocks !== undefined &&
140
+ (!Number.isInteger(options.baseFeeLivenessBlocks) || options.baseFeeLivenessBlocks < 1)) {
141
+ throw new Error(`baseFeeLivenessBlocks must be a positive integer; got ${options.baseFeeLivenessBlocks}`);
142
+ }
143
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
144
+ const retainMempool = options.keepMempoolSnapshot === true;
145
+ let state = null;
146
+ let mempoolSnapshot = null;
147
+ let timer = null;
148
+ const subscribers = new Set();
149
+ const notify = (next) => {
150
+ for (const cb of subscribers) {
151
+ try {
152
+ cb(next);
153
+ }
154
+ catch { /* swallow per-subscriber errors */ }
155
+ }
156
+ };
157
+ const cycle = async () => {
158
+ const inputs = await fetchOracleInputs(options.client, {
159
+ onError: options.onError,
160
+ poll: options.poll,
161
+ });
162
+ const next = reducePollInputs({
163
+ inputs,
164
+ chainId: options.chainId,
165
+ prev: state,
166
+ priorityFeeDecayCap: options.priorityFeeDecayCap,
167
+ priorityModel: options.priorityModel,
168
+ baseFeeLivenessBlocks: options.baseFeeLivenessBlocks,
169
+ });
170
+ if (next) {
171
+ state = next;
172
+ if (retainMempool) {
173
+ // Re-normalize each cycle. `inputs.txPool` is null when the
174
+ // mempool RPC is gated/disabled — store an empty snapshot
175
+ // rather than leaving stale data from a prior cycle around.
176
+ mempoolSnapshot = normalizeMempool(inputs.txPool);
177
+ }
178
+ notify(next);
179
+ }
180
+ return next;
181
+ };
182
+ return {
183
+ start: () => {
184
+ if (timer !== null)
185
+ return;
186
+ // Fire-and-forget the first cycle so callers don't block on RPC
187
+ // latency. The interval picks up from there.
188
+ void cycle();
189
+ timer = setInterval(() => { void cycle(); }, pollIntervalMs);
190
+ },
191
+ stop: () => {
192
+ if (timer !== null) {
193
+ clearInterval(timer);
194
+ timer = null;
195
+ }
196
+ state = null;
197
+ mempoolSnapshot = null;
198
+ },
199
+ getState: () => state,
200
+ getMempoolSnapshot: () => mempoolSnapshot,
201
+ pollOnce: () => cycle(),
202
+ subscribe: (cb) => {
203
+ subscribers.add(cb);
204
+ return () => { subscribers.delete(cb); };
205
+ },
206
+ };
207
+ };
208
+ //# sourceMappingURL=oracle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oracle.js","sourceRoot":"","sources":["../src/oracle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Adapters from upstream RPC result shapes to the gas-oracle sample
3
+ * model. Pure functions: take a typed RPC payload and emit either a
4
+ * `BlockSample` (one block of state for the ring buffer) or an array
5
+ * of `{ tip, gas }` mempool samples.
6
+ *
7
+ * Kept separate from `math.ts` (which stays numeric) and from
8
+ * `transport.ts` (which stays I/O) so tests can fixture the RPC
9
+ * shapes without touching either neighbor.
10
+ */
11
+ import type { BlockResult, TxPoolContent } from './transport.js';
12
+ import type { BlockSample, TipSample } from './types.js';
13
+ /**
14
+ * Convert a full block (from `eth_getBlockByNumber(_, true)`) into a
15
+ * `BlockSample`. Per-tx tips + gas are computed once at ingest and
16
+ * never re-derived. Identifier fields (hash/from/nonce) ride along so
17
+ * the block-position helpers can locate a specific tx in the
18
+ * distribution. Txs missing the `gas` field are filtered out — we
19
+ * can't gas-weight a sample without the weight.
20
+ */
21
+ export declare const blockToSample: (block: BlockResult) => BlockSample;
22
+ /**
23
+ * Flatten the `pending` half of a `txpool_content` payload into the
24
+ * sample shape. Queued txs are excluded (consistent with existing
25
+ * behavior — they can't be mined at current nonce).
26
+ *
27
+ * The pool's outer key is the canonical sender address (lowercased
28
+ * here, since the pool may not yet be normalized). Outer key is
29
+ * preferred over `tx.from` because some upstream serializers omit
30
+ * the per-tx `from` for mempool entries to save bytes.
31
+ *
32
+ * Txs missing `gas` are filtered out for the same reason as in
33
+ * `blockToSample`: gas-weighting requires the weight.
34
+ */
35
+ export declare const mempoolToSamples: (pool: TxPoolContent | null | undefined, currentBaseFee: bigint) => TipSample[];
36
+ //# sourceMappingURL=samples.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"samples.d.ts","sourceRoot":"","sources":["../src/samples.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAChE,OAAO,KAAK,EAAE,WAAW,EAAS,SAAS,EAAE,MAAM,YAAY,CAAA;AA8B/D;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,WAAW,KAAG,WAsBlD,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,GAC3B,MAAM,aAAa,GAAG,IAAI,GAAG,SAAS,EACtC,gBAAgB,MAAM,KACrB,SAAS,EAkBX,CAAA"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Adapters from upstream RPC result shapes to the gas-oracle sample
3
+ * model. Pure functions: take a typed RPC payload and emit either a
4
+ * `BlockSample` (one block of state for the ring buffer) or an array
5
+ * of `{ tip, gas }` mempool samples.
6
+ *
7
+ * Kept separate from `math.ts` (which stays numeric) and from
8
+ * `transport.ts` (which stays I/O) so tests can fixture the RPC
9
+ * shapes without touching either neighbor.
10
+ */
11
+ import { effectiveTip } from './math.js';
12
+ /**
13
+ * Decode the EIP-2718 type byte from its hex-string wire form.
14
+ * Returns `undefined` for missing or malformed inputs so the
15
+ * `priorityModel: 'eip1559'` filter under-counts rather than mis-buckets
16
+ * a malformed tx into the priority lane. `Number()` correctly handles
17
+ * the `0x`-prefixed forms most clients emit (`'0x'`, `'0x0'`, `'0x2'`).
18
+ */
19
+ const decodeTxType = (raw) => {
20
+ if (raw === undefined)
21
+ return undefined;
22
+ // Empty string and `'0x'` both coerce to 0 via Number — that's the
23
+ // legacy/type-0 case and the right answer.
24
+ const n = raw === '0x' || raw === '' ? 0 : Number(raw);
25
+ return Number.isFinite(n) ? n : undefined;
26
+ };
27
+ /**
28
+ * Decimalize a hex-or-decimal nonce, when present. Returns undefined
29
+ * for missing input so the field stays optional through to consumers.
30
+ */
31
+ const decodeNonce = (raw) => {
32
+ if (raw === undefined)
33
+ return undefined;
34
+ try {
35
+ return BigInt(raw).toString();
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ };
41
+ /**
42
+ * Convert a full block (from `eth_getBlockByNumber(_, true)`) into a
43
+ * `BlockSample`. Per-tx tips + gas are computed once at ingest and
44
+ * never re-derived. Identifier fields (hash/from/nonce) ride along so
45
+ * the block-position helpers can locate a specific tx in the
46
+ * distribution. Txs missing the `gas` field are filtered out — we
47
+ * can't gas-weight a sample without the weight.
48
+ */
49
+ export const blockToSample = (block) => {
50
+ const baseFee = BigInt(block.baseFeePerGas);
51
+ const tips = [];
52
+ for (const tx of block.transactions) {
53
+ if (tx.gas === undefined)
54
+ continue;
55
+ tips.push({
56
+ tip: effectiveTip(tx, baseFee),
57
+ gas: BigInt(tx.gas),
58
+ txType: decodeTxType(tx.type),
59
+ hash: tx.hash,
60
+ address: tx.from?.toLowerCase(),
61
+ nonce: decodeNonce(tx.nonce),
62
+ });
63
+ }
64
+ return {
65
+ number: BigInt(block.number),
66
+ hash: block.hash ?? '',
67
+ parentHash: block.parentHash ?? '',
68
+ baseFee,
69
+ gasUsed: BigInt(block.gasUsed),
70
+ tips,
71
+ };
72
+ };
73
+ /**
74
+ * Flatten the `pending` half of a `txpool_content` payload into the
75
+ * sample shape. Queued txs are excluded (consistent with existing
76
+ * behavior — they can't be mined at current nonce).
77
+ *
78
+ * The pool's outer key is the canonical sender address (lowercased
79
+ * here, since the pool may not yet be normalized). Outer key is
80
+ * preferred over `tx.from` because some upstream serializers omit
81
+ * the per-tx `from` for mempool entries to save bytes.
82
+ *
83
+ * Txs missing `gas` are filtered out for the same reason as in
84
+ * `blockToSample`: gas-weighting requires the weight.
85
+ */
86
+ export const mempoolToSamples = (pool, currentBaseFee) => {
87
+ if (!pool)
88
+ return [];
89
+ const samples = [];
90
+ for (const [address, byNonce] of Object.entries(pool.pending ?? {})) {
91
+ const lowerAddr = address.toLowerCase();
92
+ for (const [nonce, tx] of Object.entries(byNonce)) {
93
+ if (tx.gas === undefined)
94
+ continue;
95
+ samples.push({
96
+ tip: effectiveTip(tx, currentBaseFee),
97
+ gas: BigInt(tx.gas),
98
+ txType: decodeTxType(tx.type),
99
+ hash: tx.hash,
100
+ address: lowerAddr,
101
+ nonce: decodeNonce(nonce) ?? decodeNonce(tx.nonce),
102
+ });
103
+ }
104
+ }
105
+ return samples;
106
+ };
107
+ //# sourceMappingURL=samples.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"samples.js","sourceRoot":"","sources":["../src/samples.ts"],"names":[],"mappings":"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"}