@valve-tech/gas-oracle 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +270 -0
- package/dist/block-position.d.ts +97 -0
- package/dist/block-position.d.ts.map +1 -0
- package/dist/block-position.js +131 -0
- package/dist/block-position.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/math.d.ts +148 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +343 -0
- package/dist/math.js.map +1 -0
- package/dist/mempool.d.ts +89 -0
- package/dist/mempool.d.ts.map +1 -0
- package/dist/mempool.js +108 -0
- package/dist/mempool.js.map +1 -0
- package/dist/oracle.d.ts +139 -0
- package/dist/oracle.d.ts.map +1 -0
- package/dist/oracle.js +208 -0
- package/dist/oracle.js.map +1 -0
- package/dist/samples.d.ts +36 -0
- package/dist/samples.d.ts.map +1 -0
- package/dist/samples.js +107 -0
- package/dist/samples.js.map +1 -0
- package/dist/transport.d.ts +75 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +72 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/viem-actions.d.ts +77 -0
- package/dist/viem-actions.d.ts.map +1 -0
- package/dist/viem-actions.js +118 -0
- package/dist/viem-actions.js.map +1 -0
- package/dist/viem-transport.d.ts +85 -0
- package/dist/viem-transport.d.ts.map +1 -0
- package/dist/viem-transport.js +165 -0
- package/dist/viem-transport.js.map +1 -0
- package/package.json +81 -0
- package/src/index.ts +84 -0
package/dist/mempool.js
ADDED
|
@@ -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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"}
|
package/dist/oracle.d.ts
ADDED
|
@@ -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"}
|
package/dist/samples.js
ADDED
|
@@ -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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"}
|