@valve-tech/gas-oracle 0.2.3 → 0.2.5
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/AGENTS.md +216 -0
- package/CHANGELOG.md +113 -0
- package/README.md +75 -0
- package/package.json +10 -1
- package/skills/gas-oracle-integration/SKILL.md +98 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Terse reference for AI agents (Claude Code, Cursor, Aider, etc.) integrating
|
|
4
|
+
`@valve-tech/gas-oracle`. The full README is for humans; this file is for
|
|
5
|
+
agents that need to ground their work in the package's actual surface
|
|
6
|
+
quickly.
|
|
7
|
+
|
|
8
|
+
## What this package does
|
|
9
|
+
|
|
10
|
+
Multi-tier gas-fee oracle for EVM chains. Polls block + mempool data,
|
|
11
|
+
computes `slow` / `standard` / `fast` / `instant` priority-fee
|
|
12
|
+
recommendations from a gas-weighted percentile distribution, and serves
|
|
13
|
+
them via an in-memory cache. Pass it a viem `PublicClient` and call
|
|
14
|
+
`oracle.getState()` for sub-millisecond reads.
|
|
15
|
+
|
|
16
|
+
Zero runtime dependencies. `viem ^2.0.0` is the only peer dependency.
|
|
17
|
+
|
|
18
|
+
## Public API
|
|
19
|
+
|
|
20
|
+
All exports live under `src/index.ts`. Default subpath is the canonical
|
|
21
|
+
oracle constructor; two named subpaths add viem integration.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import {
|
|
25
|
+
createGasOracle, // primary constructor
|
|
26
|
+
normalizeMempool, // pure helper
|
|
27
|
+
findByHash, findByAddressNonce, findInMempool, // mempool lookups
|
|
28
|
+
tipForBlockPosition, // discriminated block-position query
|
|
29
|
+
// types
|
|
30
|
+
type GasOracle,
|
|
31
|
+
type CreateCreateGasOracleOptions,
|
|
32
|
+
type GasGasOracleState,
|
|
33
|
+
type TierRecommendation,
|
|
34
|
+
type RawTx,
|
|
35
|
+
type NormalizedMempool,
|
|
36
|
+
type MempoolHit,
|
|
37
|
+
type TxIdentifier,
|
|
38
|
+
type BlockPositionQuery,
|
|
39
|
+
type BlockPositionResult,
|
|
40
|
+
} from '@valve-tech/gas-oracle'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { gasOracleActions } from '@valve-tech/gas-oracle/viem-actions'
|
|
45
|
+
// client.extend(gasOracleActions({ oracle }))
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { withGasOracle } from '@valve-tech/gas-oracle/viem-transport'
|
|
50
|
+
// withGasOracle(transport, { oracle, intercept: ['eth_gasFeeEstimate', ...] })
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Five types you must know
|
|
54
|
+
|
|
55
|
+
| Type | What it is |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `CreateGasOracleOptions` | Constructor config. Required: `client`, `chainId`. Tuneables: `priorityFeeDecayCap`, `priorityModel`, `baseFeeLivenessBlocks`, `poll`, `keepMempoolSnapshot`. |
|
|
58
|
+
| `GasOracleState` | What `oracle.getState()` returns. Holds the four-tier `tiers` map plus latest `baseFee`, `bufferedBaseFee`, mempool snapshot if enabled. |
|
|
59
|
+
| `TierRecommendation` | Per-tier struct: `{ maxPriorityFeePerGas, maxFeePerGas, gasPrice, maxFeePerBlobGas }`. All `bigint`. |
|
|
60
|
+
| `RawTx` | One mempool entry: `{ hash, from, to, nonce, value, maxPriorityFeePerGas, maxFeePerGas, gas, ... }`. Nullable fields for fields the chain may not expose. |
|
|
61
|
+
| `BlockPositionQuery` | Discriminated union — five mutually-exclusive shapes (see below). |
|
|
62
|
+
|
|
63
|
+
## The discriminated `BlockPositionQuery`
|
|
64
|
+
|
|
65
|
+
This is the API surface most likely to confuse — it's deliberately shaped
|
|
66
|
+
to make impossible queries unrepresentable. The `kind` field discriminates;
|
|
67
|
+
TypeScript will narrow the rest of the fields per branch.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
type BlockPositionQuery =
|
|
71
|
+
| { kind: 'rank'; rank: number } // "tip to land in top N"
|
|
72
|
+
| { kind: 'percentile'; percentile: number } // "tip to land at p_X"
|
|
73
|
+
| { kind: 'gasFromTop'; gas: bigint } // "tip to land within G gas of leader"
|
|
74
|
+
| { kind: 'aheadOf'; tx: TxIdentifier } // "tip to leapfrog this tx"
|
|
75
|
+
| { kind: 'behind'; tx: TxIdentifier } // "tip to slip in just behind this tx"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`TxIdentifier` is `{ hash: string } | { address: string; nonce: number | bigint | string }`.
|
|
79
|
+
Both forms resolve against the latest mempool snapshot. The result is a
|
|
80
|
+
`BlockPositionResult`:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
{
|
|
84
|
+
requiredTip: bigint // min tip-per-gas to land at the position
|
|
85
|
+
pivot: TipSample | null // the boundary sample, or null if out of range
|
|
86
|
+
rank: number // approximate 0-indexed rank from top
|
|
87
|
+
gasFromTop: bigint // approximate gas-from-top
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Configuration patterns by chain class
|
|
92
|
+
|
|
93
|
+
| Chain class | `priorityModel` | `baseFeeLivenessBlocks` | Notes |
|
|
94
|
+
|---|---|---|---|
|
|
95
|
+
| Ethereum mainnet, Base, Arbitrum, OP | `'eip1559'` | 6 | Validators burn base fee; tip is the only revenue. |
|
|
96
|
+
| PulseChain (mainnet + testnet) | `'flat'` | 6 | Validators charge tips instead of burning. Type-2 vs legacy distinction is meaningless on flat-fee chains. |
|
|
97
|
+
| Anything where you're unsure | `'flat'` | 6 | More conservative — never under-counts spam. |
|
|
98
|
+
|
|
99
|
+
`priorityFeeDecayCap`: default is `WAD / 8` (12.5%/block) which matches
|
|
100
|
+
EIP-1559's natural decay. Tighten to `WAD / 20` for very low-volatility
|
|
101
|
+
chains; loosen to `null` for chains where you want the published tip to
|
|
102
|
+
free-fall during quiet windows.
|
|
103
|
+
|
|
104
|
+
## Three integration shapes (pick one)
|
|
105
|
+
|
|
106
|
+
### 1. Direct (no viem wrapper)
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const oracle = createGasOracle({ client, chainId: 1, priorityModel: 'eip1559' })
|
|
110
|
+
oracle.start()
|
|
111
|
+
const tier = oracle.getState()?.tiers.fast
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 2. viem-actions extension (recommended for app code)
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const client = createPublicClient({ chain: mainnet, transport: http() })
|
|
118
|
+
.extend(gasOracleActions({ chainId: 1, priorityModel: 'eip1559' }))
|
|
119
|
+
|
|
120
|
+
await client.getGasTiers() // GasOracleState
|
|
121
|
+
await client.getGasTier('fast') // TierRecommendation
|
|
122
|
+
await client.findTxInMempool({ hash: '0x...' }) // MempoolHit | null
|
|
123
|
+
await client.tipForBlockPosition({ kind: 'rank', rank: 50 }) // BlockPositionResult
|
|
124
|
+
client.stopGasOracle() // teardown
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`gasOracleActions(options)` accepts the same options as `createGasOracle`
|
|
128
|
+
(minus `client`, which it gets from the viem client at extension time)
|
|
129
|
+
plus `lifecycle: 'eager' | 'lazy'`. Default `'eager'` starts polling on
|
|
130
|
+
extension; `'lazy'` defers until the first read.
|
|
131
|
+
|
|
132
|
+
### 3. viem-transport interception (drop-in for existing wagmi/viem code)
|
|
133
|
+
|
|
134
|
+
Default intercepts only `eth_gasFeeEstimate` (a Valve-specific multi-tier
|
|
135
|
+
RPC extension). Standard methods stay unintercepted unless opted in:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const transport = withGasOracle(http(), {
|
|
139
|
+
chainId: 1,
|
|
140
|
+
priorityModel: 'eip1559',
|
|
141
|
+
intercept: {
|
|
142
|
+
eth_gasFeeEstimate: true, // default
|
|
143
|
+
eth_gasPrice: 'standard', // tier-required opt-in (no boolean default)
|
|
144
|
+
eth_maxPriorityFeePerGas: 'fast',
|
|
145
|
+
},
|
|
146
|
+
lifecycle: 'lazy', // optional: defer poll until first intercept
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Tearing down (e.g. test/HMR): cast back to GasOracleTransport
|
|
150
|
+
;(transport as GasOracleTransport).stopGasOracle()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`eth_gasPrice` and `eth_maxPriorityFeePerGas` reject a boolean for the
|
|
154
|
+
intercept config — a tier choice is required so that the returned number
|
|
155
|
+
doesn't silently depend on the package version's default.
|
|
156
|
+
|
|
157
|
+
## Pitfalls (read these)
|
|
158
|
+
|
|
159
|
+
1. **Don't poll `oracle.getState()` in a tight loop.** It's O(1) but you're
|
|
160
|
+
wasting CPU. Subscribe via `oracle.subscribe(callback)` for change
|
|
161
|
+
notifications and read state inside the callback.
|
|
162
|
+
|
|
163
|
+
2. **One oracle per chain, ever.** Module-scope the oracle in your code so
|
|
164
|
+
it's started once. Construct + `start()` per request and you're paying
|
|
165
|
+
the warmup cost (~3-5 blocks of `eth_feeHistory` polling) every time.
|
|
166
|
+
|
|
167
|
+
3. **`oracle.start()` returns immediately but tiers aren't populated until
|
|
168
|
+
the first poll completes.** `oracle.getState()` returns `null` during
|
|
169
|
+
warmup. Either handle the null case at the call site, or call
|
|
170
|
+
`await oracle.pollOnce()` once after `start()` to force a synchronous
|
|
171
|
+
first poll.
|
|
172
|
+
|
|
173
|
+
4. **Mempool snapshot is opt-in.** `keepMempoolSnapshot: false` (the
|
|
174
|
+
default) means `findByHash`/`findInMempool`/`tipForBlockPosition` queries
|
|
175
|
+
that take a `TxIdentifier` will throw. Set `true` if you need them.
|
|
176
|
+
|
|
177
|
+
5. **`findInMempool({ hash })` resolution costs a snapshot scan.** Cache
|
|
178
|
+
the result if you're calling it repeatedly for the same hash —
|
|
179
|
+
memoize or use a local map.
|
|
180
|
+
|
|
181
|
+
6. **PulseChain RPCs may not honor `txpool_content`.** If
|
|
182
|
+
`oracle.getMempoolSnapshot()` returns `[]` consistently on a chain you
|
|
183
|
+
know has live txs, the upstream RPC is rejecting the namespace.
|
|
184
|
+
Verify with `curl -X POST <rpc> -H 'content-type: application/json' \
|
|
185
|
+
--data '{"jsonrpc":"2.0","method":"txpool_content","params":[],"id":1}'`.
|
|
186
|
+
|
|
187
|
+
## Examples
|
|
188
|
+
|
|
189
|
+
Runnable scripts in `examples/`:
|
|
190
|
+
|
|
191
|
+
- `examples/01-basic-tiers.ts` — minimal `createGasOracle` + read tier
|
|
192
|
+
- `examples/02-mempool-snapshot.ts` — `keepMempoolSnapshot: true` + `findByHash`
|
|
193
|
+
- `examples/03-block-position.ts` — all five `tipForBlockPosition` query forms
|
|
194
|
+
- `examples/04-viem-actions.ts` — `client.extend(gasOracleActions(...))`
|
|
195
|
+
- `examples/05-viem-transport.ts` — `withGasOracle(transport, ...)` interception
|
|
196
|
+
|
|
197
|
+
Run any of them with `yarn tsx examples/01-basic-tiers.ts`.
|
|
198
|
+
|
|
199
|
+
## Skills (for AI agents)
|
|
200
|
+
|
|
201
|
+
`skills/` directory ships in the npm tarball. If you're an AI agent working
|
|
202
|
+
in a project that has installed this package, look in
|
|
203
|
+
`node_modules/@valve-tech/gas-oracle/skills/SKILL.md` for trigger conditions
|
|
204
|
+
and integration recipes that go deeper than this file.
|
|
205
|
+
|
|
206
|
+
## Verifying provenance
|
|
207
|
+
|
|
208
|
+
v0.2.3+ ships with SLSA provenance attestation:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npm view @valve-tech/gas-oracle@latest --json | jq .dist.attestations
|
|
212
|
+
npm audit signatures
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The attestation links the published tarball to the GitHub Actions workflow
|
|
216
|
+
run that built it.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@valve-tech/gas-oracle` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [0.2.5] — 2026-05-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- README **RPC transport modes** section covering all four caller-side
|
|
12
|
+
configurations the package supports: HTTP-only, WS-only, both (via viem's
|
|
13
|
+
`fallback`), and "neither" (driving the pure `reducePollInputs` reducer
|
|
14
|
+
with pre-fetched `OraclePollInputs` — no live `PublicClient` needed).
|
|
15
|
+
- `examples/06-reducer-only.ts` exercising the offline path end-to-end with
|
|
16
|
+
synthetic fixture inputs, surfacing the `fetchOracleInputs` /
|
|
17
|
+
`reducePollInputs` export split that enables it.
|
|
18
|
+
|
|
19
|
+
### Notes
|
|
20
|
+
- Documentation-only release. No API changes; behavior identical to v0.2.4.
|
|
21
|
+
- Picking WS today buys nothing functional over HTTP — the oracle never
|
|
22
|
+
opens a subscription. The functional case for WS arrives when
|
|
23
|
+
subscription-using features (e.g., tx-tracking via `newHeads` /
|
|
24
|
+
`newPendingTransactions`) land. Choose WS now only if upstream is cheaper
|
|
25
|
+
or lower-latency on it.
|
|
26
|
+
|
|
27
|
+
## [0.2.4] — 2026-05-02
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- `CHANGELOG.md` (this file).
|
|
31
|
+
- `AGENTS.md` at repo root — terse, AI-first companion to README. Lists the
|
|
32
|
+
public API, the discriminated query shape for `tipForBlockPosition`, and
|
|
33
|
+
pitfalls.
|
|
34
|
+
- `examples/` directory with 5 runnable scripts covering basic tier reads,
|
|
35
|
+
mempool snapshots, block-position queries, and both viem subpaths.
|
|
36
|
+
- `skills/` directory shipped in the npm tarball — Claude Code / Cursor / etc.
|
|
37
|
+
agents consuming `node_modules/@valve-tech/gas-oracle/skills/` get grounded
|
|
38
|
+
context about when and how to use the package.
|
|
39
|
+
- README badges (npm version, types-included, SLSA provenance).
|
|
40
|
+
- ESLint configuration with `@typescript-eslint/no-explicit-any` enforced as
|
|
41
|
+
an error. The codebase was incidentally `any`-free; this makes the rule a
|
|
42
|
+
hard constraint.
|
|
43
|
+
- `lint` script wired into the CI workflow.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- `files` field in `package.json` now includes `CHANGELOG.md`, `AGENTS.md`,
|
|
47
|
+
and `skills/` so consumers get the docs and skill files in their
|
|
48
|
+
`node_modules/`.
|
|
49
|
+
|
|
50
|
+
## [0.2.3] — 2026-05-02
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
- First release published via npm trusted-publisher OIDC. SLSA provenance
|
|
54
|
+
attestation now ships with the tarball; consumers can verify with
|
|
55
|
+
`npm audit signatures`.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
- Aligned the release workflow with the known-working pattern used by other
|
|
59
|
+
OIDC-publishing repos: removed the `environment:` block from the publish
|
|
60
|
+
job, pinned `npm` to `11.5.1` before install. Without this, the OIDC PUT
|
|
61
|
+
to npm 404'd despite the trusted-publisher record being correctly
|
|
62
|
+
configured. See repo commit `de6c5bb` for the diagnostic.
|
|
63
|
+
|
|
64
|
+
## [0.2.2] — *unpublished*
|
|
65
|
+
|
|
66
|
+
Tagged but never published. OIDC publish failed; abandoned in favor of
|
|
67
|
+
v0.2.3 which carries the workflow fix.
|
|
68
|
+
|
|
69
|
+
## [0.2.1] — 2026-05-02
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
- Top-level `main`, `types`, and `exports` now point at `dist/*` instead of
|
|
73
|
+
`src/*.ts`. The `publishConfig` override pattern that previously rewrote
|
|
74
|
+
these fields at publish time is deprecated in npm 11 and didn't apply
|
|
75
|
+
correctly during the v0.2.0 manual publish — that release shipped a
|
|
76
|
+
tarball whose `package.json` pointed at non-existent `src/` paths.
|
|
77
|
+
- Added `prepare: yarn build` to scripts so consumers using workspace
|
|
78
|
+
symlinks or `git+` installs get a built `dist/` automatically.
|
|
79
|
+
|
|
80
|
+
### Removed
|
|
81
|
+
- The `publishConfig` block (deprecated; replaced by aligning top-level
|
|
82
|
+
fields with the published shape).
|
|
83
|
+
|
|
84
|
+
## [0.2.0] — 2026-05-02
|
|
85
|
+
|
|
86
|
+
First public release. Tarball had a packaging bug — see [0.2.1] for the
|
|
87
|
+
fix. Consumers should install `@valve-tech/gas-oracle@^0.2.1` or later.
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
- `priorityFeeDecayCap: bigint | null` config (wad; null = uncapped;
|
|
91
|
+
default `WAD/8` = 12.5%/block, EIP-1559 parity).
|
|
92
|
+
- `priorityModel: 'flat' | 'eip1559'` for chains whose validators charge
|
|
93
|
+
tips instead of burning them (`flat`) versus chains that honor
|
|
94
|
+
EIP-1559 ordering (`eip1559`).
|
|
95
|
+
- `baseFeeLivenessBlocks: number` — compounded 9/8 buffer over N blocks
|
|
96
|
+
so `maxFeePerGas` survives sustained worst-case base-fee growth.
|
|
97
|
+
- `poll: { feeHistory?, mempool? }` toggles for chains that don't expose
|
|
98
|
+
one or both endpoints.
|
|
99
|
+
- `keepMempoolSnapshot: boolean` + `oracle.getMempoolSnapshot()` for
|
|
100
|
+
Phase B stuck-tx detection.
|
|
101
|
+
- Pure helpers: `normalizeMempool`, `findByHash`, `findByAddressNonce`,
|
|
102
|
+
`findInMempool`.
|
|
103
|
+
- `tipForBlockPosition` — discriminated query over `rank` / `percentile`
|
|
104
|
+
/ `gasFromTop` and `aheadOf` / `behind` a `TxIdentifier`.
|
|
105
|
+
- `@valve-tech/gas-oracle/viem-actions` subpath for
|
|
106
|
+
`client.extend(gasOracleActions(...))` integration.
|
|
107
|
+
- `@valve-tech/gas-oracle/viem-transport` subpath for `withGasOracle(transport, ...)`
|
|
108
|
+
drop-in interception.
|
|
109
|
+
|
|
110
|
+
[0.2.4]: https://github.com/valve-tech/gas-oracle/releases/tag/v0.2.4
|
|
111
|
+
[0.2.3]: https://github.com/valve-tech/gas-oracle/releases/tag/v0.2.3
|
|
112
|
+
[0.2.1]: https://github.com/valve-tech/gas-oracle/releases/tag/v0.2.1
|
|
113
|
+
[0.2.0]: https://github.com/valve-tech/gas-oracle/releases/tag/v0.2.0
|
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @valve-tech/gas-oracle
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@valve-tech/gas-oracle)
|
|
4
|
+
[](https://www.npmjs.com/package/@valve-tech/gas-oracle)
|
|
5
|
+
[](https://docs.npmjs.com/generating-provenance-statements)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
3
8
|
Multi-tier gas-fee oracle for EVM chains. Pass it a viem `PublicClient`
|
|
4
9
|
and it polls block + mempool data, computes `slow` / `standard` /
|
|
5
10
|
`fast` / `instant` tier recommendations, and serves them via an
|
|
@@ -8,6 +13,10 @@ aware EIP-1559 priority cutoff, and EIP-4844 blob-fee handling.
|
|
|
8
13
|
|
|
9
14
|
Zero runtime dependencies. `viem` is the only peer dependency.
|
|
10
15
|
|
|
16
|
+
> **AI agents:** see [`AGENTS.md`](AGENTS.md) for a terse, AI-first reference,
|
|
17
|
+
> and [`skills/`](skills/) for Claude Code / Cursor skill files shipped in
|
|
18
|
+
> `node_modules/`.
|
|
19
|
+
|
|
11
20
|
## Install
|
|
12
21
|
|
|
13
22
|
```bash
|
|
@@ -258,6 +267,72 @@ careful to avoid.
|
|
|
258
267
|
synthesizing a historical-percentile array from oracle state is its
|
|
259
268
|
own design problem. Always passes through to upstream.
|
|
260
269
|
|
|
270
|
+
## RPC transport modes
|
|
271
|
+
|
|
272
|
+
The package only ever calls `client.request({ method, params })` and
|
|
273
|
+
never opens a subscription. That makes it transport-agnostic — any
|
|
274
|
+
viem `Transport` works, and the four caller-side configurations below
|
|
275
|
+
all run unchanged:
|
|
276
|
+
|
|
277
|
+
### HTTP only
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { http } from 'viem'
|
|
281
|
+
|
|
282
|
+
const client = createPublicClient({ chain: mainnet, transport: http(rpcUrl) })
|
|
283
|
+
const oracle = createGasOracle({ client, chainId: 1 })
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### WebSocket only
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { webSocket } from 'viem'
|
|
290
|
+
|
|
291
|
+
const client = createPublicClient({ chain: mainnet, transport: webSocket(wsUrl) })
|
|
292
|
+
const oracle = createGasOracle({ client, chainId: 1 })
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
WS works because the three RPCs the oracle issues (`eth_feeHistory`,
|
|
296
|
+
`eth_getBlockByNumber`, `txpool_content`) are all request/response;
|
|
297
|
+
viem's `webSocket` transport implements the same `request` interface
|
|
298
|
+
as `http`. **Picking WS today buys nothing functional over HTTP** —
|
|
299
|
+
the oracle still polls on its `pollIntervalMs`. The functional case
|
|
300
|
+
for WS arrives when subscription-using features land (tx-tracking
|
|
301
|
+
`newHeads` / `newPendingTransactions`); choose WS now only if your
|
|
302
|
+
upstream is cheaper or lower-latency on it.
|
|
303
|
+
|
|
304
|
+
### Both — `fallback` for resilience
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
import { fallback, http, webSocket } from 'viem'
|
|
308
|
+
|
|
309
|
+
const transport = fallback([webSocket(wsUrl), http(rpcUrl)])
|
|
310
|
+
const client = createPublicClient({ chain: mainnet, transport })
|
|
311
|
+
const oracle = createGasOracle({ client, chainId: 1 })
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
viem handles failover transparently — if the WS drops, requests fall
|
|
315
|
+
to HTTP without the oracle noticing.
|
|
316
|
+
|
|
317
|
+
### Neither — pure reducer, no live RPC
|
|
318
|
+
|
|
319
|
+
The oracle's I/O surface (`fetchOracleInputs`) and its math
|
|
320
|
+
(`reducePollInputs`) are exported as separate top-level entries. That
|
|
321
|
+
split is what enables the offline path: drive the reducer with
|
|
322
|
+
`OraclePollInputs` from any source — fixture file, snapshot store,
|
|
323
|
+
Kafka log, another service's API — and never touch a `PublicClient`.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { reducePollInputs, type OraclePollInputs } from '@valve-tech/gas-oracle'
|
|
327
|
+
|
|
328
|
+
const inputs: OraclePollInputs = await loadFromYourQueue()
|
|
329
|
+
const state = reducePollInputs({ inputs, chainId: 1, prev: priorState })
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Use cases: serverless / edge handlers, backtest harnesses replaying
|
|
333
|
+
historical RPC payloads, tests asserting state shape from fixtures.
|
|
334
|
+
See `examples/06-reducer-only.ts`.
|
|
335
|
+
|
|
261
336
|
## Wire format
|
|
262
337
|
|
|
263
338
|
Every fee field is a `bigint`. Callers serializing across HTTP / Redis
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valve-tech/gas-oracle",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Multi-tier gas-fee oracle for EVM chains. Computes slow/standard/fast/instant tier recommendations from block-included tips, mempool pending tips, and base-fee trend, with a configurable downside-decay cap and a chain-aware EIP-1559 priority cutoff. viem-native — pass it a PublicClient and it does the rest. Ships viem-actions and viem-transport subpaths for drop-in client extension and transport-wrapping.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/valve-tech/gas-oracle#readme",
|
|
@@ -42,12 +42,17 @@
|
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"dist",
|
|
45
|
+
"skills",
|
|
45
46
|
"README.md",
|
|
47
|
+
"AGENTS.md",
|
|
48
|
+
"CHANGELOG.md",
|
|
46
49
|
"LICENSE"
|
|
47
50
|
],
|
|
48
51
|
"scripts": {
|
|
49
52
|
"build": "tsc -p .",
|
|
50
53
|
"typecheck": "tsc -p . --noEmit",
|
|
54
|
+
"typecheck:examples": "tsc -p examples",
|
|
55
|
+
"lint": "eslint src",
|
|
51
56
|
"test": "vitest run",
|
|
52
57
|
"test:watch": "vitest",
|
|
53
58
|
"prepare": "yarn build"
|
|
@@ -57,7 +62,11 @@
|
|
|
57
62
|
},
|
|
58
63
|
"devDependencies": {
|
|
59
64
|
"@types/node": "^22.0.0",
|
|
65
|
+
"@typescript-eslint/eslint-plugin": "^8.59.1",
|
|
66
|
+
"@typescript-eslint/parser": "^8.59.1",
|
|
67
|
+
"eslint": "^10.3.0",
|
|
60
68
|
"typescript": "^6.0.2",
|
|
69
|
+
"typescript-eslint": "^8.59.1",
|
|
61
70
|
"viem": "^2.21.0",
|
|
62
71
|
"vitest": "^4.1.4"
|
|
63
72
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gas-oracle-integration
|
|
3
|
+
description: Integrate `@valve-tech/gas-oracle` into an EVM dapp or backend. Use when the user wants gas-tier recommendations (`slow` / `standard` / `fast` / `instant`), needs to set `maxPriorityFeePerGas` and `maxFeePerGas` for a transaction, hits stuck-tx detection requirements, or asks "how do I price a transaction" against a viem `PublicClient`. Also use when seeing imports from `@valve-tech/gas-oracle` and the user asks for help configuring it per chain (Ethereum, Base, Arbitrum, OP, PulseChain), or asks about `priorityFeeDecayCap`, `priorityModel`, `tipForBlockPosition`, viem-actions, or viem-transport.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Integrating `@valve-tech/gas-oracle`
|
|
7
|
+
|
|
8
|
+
Multi-tier gas-fee oracle for EVM chains. This skill is for AI agents
|
|
9
|
+
working in a project that imports the package — it grounds you in the
|
|
10
|
+
right configuration choices for the user's chain and the right
|
|
11
|
+
integration shape for their codebase.
|
|
12
|
+
|
|
13
|
+
## Decision tree: which integration to use
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Is the user already passing a viem PublicClient around?
|
|
17
|
+
├── Yes — use viem-actions (`client.extend(gasOracleActions(...))` or
|
|
18
|
+
│ the direct invocation `gasOracleActions(opts)(client)`).
|
|
19
|
+
│ Most ergonomic for app code.
|
|
20
|
+
└── No — does the user have wagmi/viem code that already calls
|
|
21
|
+
`client.getGasPrice()` / `eth_maxPriorityFeePerGas`?
|
|
22
|
+
├── Yes — use viem-transport (`withGasOracle(transport, ...)`)
|
|
23
|
+
│ to intercept those methods at the RPC layer. Drop-in.
|
|
24
|
+
│ No call-site changes.
|
|
25
|
+
└── No — use the direct constructor `createGasOracle(opts)`.
|
|
26
|
+
Simplest. Read tiers via `oracle.getState()?.tiers`.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Per-chain config (always required)
|
|
30
|
+
|
|
31
|
+
| Chain | `chainId` | `priorityModel` | `baseFeeLivenessBlocks` | Notes |
|
|
32
|
+
|---|---|---|---|---|
|
|
33
|
+
| Ethereum mainnet | 1 | `'eip1559'` | 6 | Validators burn base fee. |
|
|
34
|
+
| Base | 8453 | `'eip1559'` | 6 | Same as ETH. |
|
|
35
|
+
| Arbitrum One | 42161 | `'eip1559'` | 6 | |
|
|
36
|
+
| Optimism | 10 | `'eip1559'` | 6 | |
|
|
37
|
+
| PulseChain mainnet | 369 | `'flat'` | 6 | Validators charge tips. |
|
|
38
|
+
| PulseChain testnet v4 | 943 | `'flat'` | 6 | |
|
|
39
|
+
| Unknown / unsure | — | `'flat'` | 6 | Conservative; never under-counts. |
|
|
40
|
+
|
|
41
|
+
`priorityFeeDecayCap`: leave at default (`WAD/8` = 12.5%/block, EIP-1559
|
|
42
|
+
parity) unless you have a specific reason to tighten/loosen.
|
|
43
|
+
|
|
44
|
+
## Anti-patterns to flag
|
|
45
|
+
|
|
46
|
+
When reviewing user code, watch for these and suggest fixes:
|
|
47
|
+
|
|
48
|
+
1. **Multiple oracles per chain in the same process.** Construct once,
|
|
49
|
+
module-scope it. Each oracle runs a poll interval and holds state.
|
|
50
|
+
Two oracles for chain 1 = double the RPC traffic, no benefit.
|
|
51
|
+
|
|
52
|
+
2. **`oracle.getState()` in a hot path that runs every render / every
|
|
53
|
+
request.** It's O(1) but you're wasting cache lines. Either subscribe
|
|
54
|
+
via `oracle.subscribe(cb)` and store the latest state in a module
|
|
55
|
+
variable, or cache the result yourself with a short TTL.
|
|
56
|
+
|
|
57
|
+
3. **Reading `oracle.getState()` immediately after `oracle.start()`
|
|
58
|
+
without handling null.** First poll hasn't completed yet; tiers will
|
|
59
|
+
be missing. Fix: `await oracle.pollOnce()` after `start()` to seed
|
|
60
|
+
state synchronously, then it's safe to call `getState()`.
|
|
61
|
+
|
|
62
|
+
4. **Using `priorityModel: 'eip1559'` on PulseChain or other tip-charging
|
|
63
|
+
chains.** Cuts the distribution to type-2+ samples only, but
|
|
64
|
+
PulseChain validators don't honor the type byte — they sort by tip
|
|
65
|
+
regardless. Result: under-published tier values, your tx loses to
|
|
66
|
+
legacy spam.
|
|
67
|
+
|
|
68
|
+
5. **`keepMempoolSnapshot: true` on a chain whose RPC gates
|
|
69
|
+
`txpool_content`** (most public RPCs). Wastes a poll cycle's RPC
|
|
70
|
+
budget on a request that always errors. Set `false` until you have
|
|
71
|
+
a node you operate.
|
|
72
|
+
|
|
73
|
+
6. **Calling `findTxInMempool` with a hash that's been confirmed.**
|
|
74
|
+
Confirmed txs are NOT in the mempool snapshot (it's pending+queued
|
|
75
|
+
only). Check `eth_getTransactionByHash` instead.
|
|
76
|
+
|
|
77
|
+
## How to recognize this package in the user's code
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// Direct constructor
|
|
81
|
+
import { createGasOracle } from '@valve-tech/gas-oracle'
|
|
82
|
+
|
|
83
|
+
// viem-actions extension
|
|
84
|
+
import { gasOracleActions } from '@valve-tech/gas-oracle/viem-actions'
|
|
85
|
+
|
|
86
|
+
// viem-transport interception
|
|
87
|
+
import { withGasOracle } from '@valve-tech/gas-oracle/viem-transport'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`package.json` will show `"@valve-tech/gas-oracle": "^0.2.x"` in dependencies.
|
|
91
|
+
|
|
92
|
+
## Where to find more
|
|
93
|
+
|
|
94
|
+
- Full API + types: `node_modules/@valve-tech/gas-oracle/AGENTS.md`
|
|
95
|
+
- Runnable examples: `node_modules/@valve-tech/gas-oracle/examples/`
|
|
96
|
+
- Human-facing docs: `node_modules/@valve-tech/gas-oracle/README.md`
|
|
97
|
+
- Source (when types alone aren't enough): `node_modules/@valve-tech/gas-oracle/dist/`
|
|
98
|
+
(compiled JS + .d.ts) — sources aren't shipped, only built output.
|