@valve-tech/chain-source 0.10.1 → 0.11.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/AGENTS.md ADDED
@@ -0,0 +1,271 @@
1
+ # AGENTS.md
2
+
3
+ Terse reference for AI agents (Claude Code, Cursor, Aider, etc.) integrating
4
+ `@valve-tech/chain-source`. 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
+ Canonical EVM chain-observation primitive. Owns the upstream poll cycle,
11
+ the per-method capability probe, and a typed multi-subscriber pub/sub
12
+ for new blocks and mempool snapshots. Plus on-demand RPC passthroughs
13
+ (block, fee history, receipt, tx by hash, fresh mempool snapshot).
14
+
15
+ Designed as the **shared foundation** for derived views of chain state.
16
+ Both `@valve-tech/gas-oracle` (gas-tier reducer) and
17
+ `@valve-tech/tx-tracker` (per-tx state machine) consume a `ChainSource`
18
+ rather than re-implementing their own poll loops — one upstream RPC
19
+ stream feeds every consumer that attaches.
20
+
21
+ Browser/mobile-safe: no Node-only imports (`events`, `fs`, etc.).
22
+ `viem ^2.0.0` is the only peer dependency.
23
+
24
+ See `docs/tx-tracker-spec.md` §3 for the full design contract.
25
+
26
+ ## Public API
27
+
28
+ All exports live under `src/index.ts`. Single subpath; no sub-exports.
29
+
30
+ ```ts
31
+ import {
32
+ createChainSource, // primary constructor
33
+ Subscriptions, // typed pub/sub primitive (browser-safe)
34
+ normalizeMempool, // pure helper — txpool_content → NormalizedMempool
35
+ probeCapabilities, // standalone capability probe (used internally)
36
+ // low-level transport helpers (also used internally)
37
+ safeRequest,
38
+ fetchBlock,
39
+ fetchHeadBlockNumber,
40
+ fetchFeeHistory,
41
+ fetchTxPool,
42
+ fetchReceipt,
43
+ fetchTransaction,
44
+ zeroHash,
45
+ // types
46
+ type ChainSource,
47
+ type CreateChainSourceOptions,
48
+ type BlockResult,
49
+ type Capabilities,
50
+ type EventSource,
51
+ type FeeHistoryResult,
52
+ type NormalizedMempool,
53
+ type PollOptions,
54
+ type RawTx,
55
+ type TransactionReceipt,
56
+ } from '@valve-tech/chain-source'
57
+ ```
58
+
59
+ ## Five types you must know
60
+
61
+ | Type | What it is |
62
+ |---|---|
63
+ | `CreateChainSourceOptions` | Constructor config. Required: `client` (a viem `PublicClient`). Tuneables: `pollIntervalMs` (default `10_000`), `poll: { mempool?, feeHistory? }`, `onError`. |
64
+ | `ChainSource` | The instance. Lifecycle (`start` / `stop` / `pollOnce` / `ready`), two subscribe streams, five on-demand fetchers, plus `capabilities()`. |
65
+ | `BlockResult` | Wire-shape block: hex-encoded numbers (`number`, `timestamp`, `baseFeePerGas`, `gasLimit`, `gasUsed`, optional blob fields), full `transactions: RawTx[]`. Consumers decode the fields they need. |
66
+ | `NormalizedMempool` | `{ pending, queued }` two-level map: `sender (lowercase) → nonce (decimal string) → RawTx`. Always pre-normalized; consumers do O(1) two-key lookups, no case/format folding needed. |
67
+ | `Capabilities` | The probe result. Per-method, not per-transport — see the matrix below. |
68
+
69
+ ## The capability matrix
70
+
71
+ Probed eagerly at construction, cached on the source, exposed via
72
+ `source.capabilities()`. The toolkit's "no silent downgrade" rule (spec
73
+ §2.2) is enforced *here* — every event a downstream emits carries an
74
+ `EventSource` discriminator chosen against this matrix.
75
+
76
+ ```ts
77
+ interface Capabilities {
78
+ newHeads: 'subscription' | 'poll-only' | 'unavailable'
79
+ newPendingTransactions: 'subscription' | 'poll-only' | 'unavailable'
80
+ txpoolContent: 'available' | 'gated'
81
+ receiptByHash: 'available' | 'unavailable'
82
+ reprobeOnReconnect: boolean
83
+ }
84
+ ```
85
+
86
+ Read `'subscription'` as "push path is live", `'poll-only'` as
87
+ "falling back to the interval timer", `'unavailable'` as "no path —
88
+ this signal is silent on this RPC". The probe runs one opportunistic
89
+ `eth_subscribe('newHeads')` round-trip to distinguish "subscribe is on
90
+ the transport" from "subscribe actually works on this provider" — some
91
+ viem transports wrap-but-can't-subscribe and the structural-only check
92
+ would lie.
93
+
94
+ `reprobeOnReconnect` is `true` for WS transports that signal reconnect.
95
+ HTTP has no persistent connection so it's always `false`.
96
+
97
+ ## The conservative probing window
98
+
99
+ For a brief window between `createChainSource()` and the eager probe
100
+ landing, `capabilities()` returns a `PROBING_DEFAULT` snapshot with
101
+ every signal set to the most defensive value (`unavailable` / `gated`).
102
+ This is intentional: a consumer that reads capabilities in this window
103
+ gets the safest answer (no path, fall back to the most defensive flow).
104
+
105
+ Callers that need a guaranteed-completed probe:
106
+
107
+ ```ts
108
+ await source.ready()
109
+ const caps = source.capabilities() // real values, not the defensive default
110
+ ```
111
+
112
+ ## `EventSource` discriminator
113
+
114
+ Reserved vocabulary for downstream events. The source itself doesn't
115
+ author editorial events (no `confirmed` / `failed` / `dropped` here —
116
+ those live in `@valve-tech/tx-tracker`); but events that downstreams
117
+ build from chain-source observations carry a `source` discriminator
118
+ chosen from this union:
119
+
120
+ ```ts
121
+ type EventSource =
122
+ | 'subscription' // arrived via eth_subscribe (push)
123
+ | 'block-poll' // arrived via the source's eth_getBlockByNumber tick
124
+ | 'mempool-snapshot' // arrived via the source's txpool_content tick
125
+ | 'receipt-poll' // tx-tracker fallback per-hash receipt poll
126
+ ```
127
+
128
+ Consumers that need hard guarantees on the freshness/authority of an
129
+ observation filter to `'subscription'`.
130
+
131
+ ## Two integration shapes (pick one)
132
+
133
+ ### 1. Standalone (you're building your own derived view)
134
+
135
+ ```ts
136
+ import { createPublicClient, http } from 'viem'
137
+ import { mainnet } from 'viem/chains'
138
+ import { createChainSource } from '@valve-tech/chain-source'
139
+
140
+ const client = createPublicClient({ chain: mainnet, transport: http() })
141
+ const source = createChainSource({ client })
142
+
143
+ const unsubBlocks = source.subscribeBlocks((block) => {
144
+ // BlockResult — hex strings; decode the fields you care about
145
+ console.log('block', BigInt(block.number), block.transactions.length, 'txs')
146
+ })
147
+
148
+ const unsubMempool = source.subscribeMempool((snapshot) => {
149
+ // NormalizedMempool — pre-lowercased addresses, pre-decimalized nonces
150
+ console.log('senders', Object.keys(snapshot.pending).length)
151
+ })
152
+
153
+ source.start()
154
+
155
+ // On-demand:
156
+ const receipt = await source.getReceipt('0xabc...')
157
+ const tx = await source.getTransaction('0xabc...')
158
+ const fees = await source.getFeeHistory(20, [25, 50, 75])
159
+
160
+ // Teardown — preserves the subscriber registry across restarts.
161
+ source.stop()
162
+ unsubBlocks()
163
+ unsubMempool()
164
+ ```
165
+
166
+ ### 2. Shared with sibling derived views (multi-subscriber)
167
+
168
+ ```ts
169
+ import { createChainSource } from '@valve-tech/chain-source'
170
+ import { createGasOracle } from '@valve-tech/gas-oracle'
171
+ import { createTxTracker } from '@valve-tech/tx-tracker'
172
+
173
+ const source = createChainSource({ client })
174
+ const oracle = createGasOracle({ source, chainId: 1 })
175
+ const tracker = createTxTracker({ source, chainId: 1 })
176
+
177
+ source.start(); oracle.start(); tracker.start()
178
+ // ↑ ONE shared poll cycle. Two derived views. No double-polling.
179
+ ```
180
+
181
+ Each lifecycle is independent — `oracle.stop()` stops the oracle, not
182
+ the source. The owner of the source (whoever called
183
+ `createChainSource`) calls `source.stop()`.
184
+
185
+ ## Pitfalls (read these)
186
+
187
+ 1. **Don't call `source.start()` per request.** Module-scope the
188
+ source so it starts once at process boot. A hot-path
189
+ `createChainSource() → start() → subscribe → wait → stop` cycle
190
+ pays the capability probe cost on every request and wastes the
191
+ shared-fan-out design.
192
+
193
+ 2. **`subscribeBlocks` / `subscribeMempool` callbacks must be cheap.**
194
+ The fan-out runs synchronously over a snapshot of the subscriber
195
+ set. Per-subscriber throws are swallowed (see `Subscriptions`),
196
+ but a subscriber that blocks (synchronous heavy work) blocks every
197
+ subscriber registered after it for that emit. Push expensive work
198
+ into a microtask / worker if needed.
199
+
200
+ 3. **Re-subscribing the same callback reference is a no-op.** The
201
+ backing set deduplicates by reference. If you really want
202
+ "deliver twice", register two distinct closures.
203
+
204
+ 4. **`subscribeMempool` snapshots are NOT deduped.** Mempool entries
205
+ come and go between blocks even on a static head — every
206
+ successful tick emits. Block events ARE deduped (by hash, not
207
+ number; same-height reorgs surface as fresh observations).
208
+
209
+ 5. **`getMempoolSnapshot()` returns `null` when `txpool_content` is
210
+ gated.** Most public RPCs gate this method. Check
211
+ `source.capabilities().txpoolContent` before assuming a snapshot
212
+ exists. For continuous mempool access, prefer `subscribeMempool` —
213
+ that path reuses the source's poll cycle rather than firing a
214
+ fresh RPC per call.
215
+
216
+ 6. **`capabilities()` returns the defensive default before
217
+ `await source.ready()`.** If your code branches on capabilities at
218
+ construction time, await ready first or you'll always take the
219
+ most-defensive branch.
220
+
221
+ 7. **Stopping the source resets dedup state.** A start → stop → start
222
+ pattern re-emits the current head + a fresh mempool snapshot on
223
+ first re-tick (deliberate — a paused-then-resumed consumer should
224
+ see a current snapshot, not wait for the next chain block). It does
225
+ NOT clear the subscriber registry.
226
+
227
+ 8. **Wire-format note.** Numeric fields on `BlockResult` /
228
+ `FeeHistoryResult` / `TxPoolContent` arrive as **hex strings** —
229
+ that's what the upstream JSON-RPC returns. The source decodes only
230
+ the bits IT needs (block number for the head-probe gate); your
231
+ consumer code decodes the rest at the point of use. `JSON.stringify`
232
+ on a state object containing decoded `bigint` values will throw —
233
+ hex-encode at the wire boundary if you persist.
234
+
235
+ ## On-demand RPC helpers — source vs. transport
236
+
237
+ The package exports both `source.getBlock(...)` (instance method) and
238
+ `fetchBlock(client, ...)` (free function). Same underlying call.
239
+
240
+ - Use the instance methods (`source.getBlock`, `source.getReceipt`,
241
+ etc.) when you have a source and want errors to flow through its
242
+ `onError` sink.
243
+ - Use the free functions (`fetchBlock`, `fetchReceipt`, etc.) when
244
+ you need a one-shot RPC without spinning up a source — e.g. inside
245
+ a script, a test, or a function that already has a `PublicClient`
246
+ in scope.
247
+
248
+ `safeRequest(client, method, params, onError?)` is the underlying
249
+ "never throws, returns null on error" wrapper used everywhere in the
250
+ package — use it directly for any custom RPC method that should follow
251
+ the same posture.
252
+
253
+ ## Skills (for AI agents)
254
+
255
+ `skills/` ships in the npm tarball. If you're an AI agent working in a
256
+ project that has installed this package, look in
257
+ `node_modules/@valve-tech/chain-source/skills/chain-source-integration/SKILL.md`
258
+ for trigger conditions, anti-pattern flags, and recipes for picking
259
+ the right integration shape.
260
+
261
+ ## Verifying provenance
262
+
263
+ Every published version ships with SLSA provenance attestation:
264
+
265
+ ```bash
266
+ npm view @valve-tech/chain-source@latest --json | jq .dist.attestations
267
+ npm audit signatures
268
+ ```
269
+
270
+ The attestation links the published tarball to the GitHub Actions
271
+ workflow run that built it.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/).
8
8
 
9
+ ## [0.11.0] — 2026-05-11
10
+
11
+ ### Changed
12
+
13
+ - Doc comment in `src/types.ts` no longer describes the
14
+ gas-oracle import migration as "future" — the migration shipped
15
+ in this same release. Comment now states that this package is the
16
+ canonical owner and gas-oracle re-exports from `index.ts`. No
17
+ type-shape change.
18
+
9
19
  ## [0.10.1] — 2026-05-08
10
20
 
11
21
  Synchronized release — no changes to this package. Republished at
package/dist/types.d.ts CHANGED
@@ -11,12 +11,13 @@
11
11
  * on a state with `bigint` will throw; persistence layers hex-encode
12
12
  * at their wire boundary, see `docs/tx-tracker-spec.md` §2.5.
13
13
  *
14
- * The shapes here are **functionally identical** to the equivalent
15
- * types currently in `@valve-tech/gas-oracle/src/transport.ts` and
16
- * `mempool.ts`. That is intentional a future PR migrates gas-oracle
17
- * to import from this package and removes its local copies. Keeping
18
- * them structurally identical now means the migration is a simple
19
- * import swap, not a type-shape change.
14
+ * This package is the canonical owner of the wire-shape types
15
+ * (`RawTx`, `BlockResult`, `FeeHistoryResult`, `TxPoolContent`,
16
+ * `NormalizedMempool`) and the poll-cycle toggle (`PollOptions`).
17
+ * `@valve-tech/gas-oracle` and `@valve-tech/tx-tracker` import them
18
+ * from here; gas-oracle re-exports them from its own `index.ts` so
19
+ * downstream consumers using the gas-oracle package don't have to
20
+ * add a second import to type a fixture or a stored snapshot.
20
21
  */
21
22
  /**
22
23
  * Minimal tx shape extracted from `eth_getBlockByNumber(latest, true)`
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,KAAK;IACpB,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,CAAA;IACnB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,KAAK,EAAE,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;IAC9C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;CAC9C;AAED;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,aAAa,CAAA;AAE7C;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,GACnB,cAAc,GACd,YAAY,GACZ,kBAAkB,GAClB,cAAc,CAAA;AAElB;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,QAAQ,EAAE,cAAc,GAAG,WAAW,GAAG,aAAa,CAAA;IACtD,4EAA4E;IAC5E,sBAAsB,EAAE,cAAc,GAAG,WAAW,GAAG,aAAa,CAAA;IACpE,oEAAoE;IACpE,aAAa,EAAE,WAAW,GAAG,OAAO,CAAA;IACpC,qEAAqE;IACrE,aAAa,EAAE,WAAW,GAAG,aAAa,CAAA;IAC1C,qEAAqE;IACrE,kBAAkB,EAAE,OAAO,CAAA;CAC5B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,KAAK;IACpB,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,CAAA;IACnB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,KAAK,EAAE,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;IAC9C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;CAC9C;AAED;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,aAAa,CAAA;AAE7C;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,GACnB,cAAc,GACd,YAAY,GACZ,kBAAkB,GAClB,cAAc,CAAA;AAElB;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,QAAQ,EAAE,cAAc,GAAG,WAAW,GAAG,aAAa,CAAA;IACtD,4EAA4E;IAC5E,sBAAsB,EAAE,cAAc,GAAG,WAAW,GAAG,aAAa,CAAA;IACpE,oEAAoE;IACpE,aAAa,EAAE,WAAW,GAAG,OAAO,CAAA;IACpC,qEAAqE;IACrE,aAAa,EAAE,WAAW,GAAG,aAAa,CAAA;IAC1C,qEAAqE;IACrE,kBAAkB,EAAE,OAAO,CAAA;CAC5B"}
package/dist/types.js CHANGED
@@ -11,12 +11,13 @@
11
11
  * on a state with `bigint` will throw; persistence layers hex-encode
12
12
  * at their wire boundary, see `docs/tx-tracker-spec.md` §2.5.
13
13
  *
14
- * The shapes here are **functionally identical** to the equivalent
15
- * types currently in `@valve-tech/gas-oracle/src/transport.ts` and
16
- * `mempool.ts`. That is intentional a future PR migrates gas-oracle
17
- * to import from this package and removes its local copies. Keeping
18
- * them structurally identical now means the migration is a simple
19
- * import swap, not a type-shape change.
14
+ * This package is the canonical owner of the wire-shape types
15
+ * (`RawTx`, `BlockResult`, `FeeHistoryResult`, `TxPoolContent`,
16
+ * `NormalizedMempool`) and the poll-cycle toggle (`PollOptions`).
17
+ * `@valve-tech/gas-oracle` and `@valve-tech/tx-tracker` import them
18
+ * from here; gas-oracle re-exports them from its own `index.ts` so
19
+ * downstream consumers using the gas-oracle package don't have to
20
+ * add a second import to type a fixture or a stored snapshot.
20
21
  */
21
22
  export {};
22
23
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valve-tech/chain-source",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "Canonical EVM chain-observation primitive: a unified push-or-poll source for new blocks, mempool snapshots, on-demand receipt + tx lookups, and capability disclosure (HTTP / WS / per-method gating). Used as the shared foundation by @valve-tech/gas-oracle and @valve-tech/tx-tracker; consumable directly by anyone building their own derived view on chain state. viem-native. Part of the valve-tech/evm-toolkit synchronized release line — implementation lands in subsequent 0.3.x releases per docs/tx-tracker-spec.md.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/valve-tech/evm-toolkit/tree/main/packages/chain-source#readme",
@@ -32,7 +32,9 @@
32
32
  },
33
33
  "files": [
34
34
  "dist",
35
+ "skills",
35
36
  "README.md",
37
+ "AGENTS.md",
36
38
  "CHANGELOG.md",
37
39
  "LICENSE"
38
40
  ],
@@ -0,0 +1,257 @@
1
+ ---
2
+ name: chain-source-integration
3
+ description: Integrate `@valve-tech/chain-source` — the canonical EVM chain-observation primitive — into a dapp, indexer, watcher, or backend. Use when the user is wiring up a multi-subscriber poll/push source for new blocks or mempool snapshots, deciding whether to subscribe vs poll, asking about RPC capability disclosure (HTTP-only vs WS, `txpool_content` gating, `eth_getTransactionReceipt` fallback), sharing one upstream RPC stream between `@valve-tech/gas-oracle` and `@valve-tech/tx-tracker`, or sees imports from `@valve-tech/chain-source`. Also fires for questions about `subscribeBlocks` / `subscribeMempool`, the conservative probing-window default, the `EventSource` discriminator (`'subscription'` vs `'block-poll'` vs `'mempool-snapshot'` vs `'receipt-poll'`), or "why am I getting null from `getMempoolSnapshot()`?". Skip when the user only wants per-tx state (delegate to tx-tracker skill) or only wants gas-tier recommendations (delegate to gas-oracle skill); both are derived views built on top of this package and have their own integration skills.
4
+ ---
5
+
6
+ # Integrating `@valve-tech/chain-source`
7
+
8
+ Canonical EVM chain-observation primitive: a unified push-or-poll
9
+ source for new blocks, mempool snapshots, on-demand receipt + tx
10
+ lookups, and explicit per-method capability disclosure. This skill is
11
+ for AI agents working in a project that imports the package — it
12
+ grounds you in the right integration shape and the per-RPC capability
13
+ realities you'll hit in production.
14
+
15
+ ## Decision tree: which integration to use
16
+
17
+ ```
18
+ Is the user composing this WITH @valve-tech/gas-oracle or
19
+ @valve-tech/tx-tracker (or both)?
20
+ ├── Yes — construct ONE shared ChainSource, pass `source` (not `client`)
21
+ │ to each sibling. One upstream RPC poll cycle, multiple
22
+ │ derived views. Module-scope the source.
23
+ └── No — does the user need their own derived view of chain state
24
+ (custom indexer, alerting, analytics, anything that needs
25
+ the block + mempool stream)?
26
+ ├── Yes — use `createChainSource` directly. Subscribe via
27
+ │ `subscribeBlocks` / `subscribeMempool`.
28
+ └── No — they probably want one of the sibling packages
29
+ instead. Don't ship chain-source as a dependency just
30
+ to call `getReceipt` once — viem's `client.getTransactionReceipt`
31
+ already does that.
32
+ ```
33
+
34
+ ## How to recognize this package in the user's code
35
+
36
+ ```ts
37
+ import {
38
+ createChainSource, // primary constructor
39
+ Subscriptions, // pub/sub primitive (rare to import)
40
+ normalizeMempool, // pure helper
41
+ } from '@valve-tech/chain-source'
42
+ ```
43
+
44
+ `package.json` will show `"@valve-tech/chain-source": "^0.10.x"` — and
45
+ typically also one or both of `@valve-tech/gas-oracle` /
46
+ `@valve-tech/tx-tracker` if they're using the shared-source shape.
47
+
48
+ ## The capability matrix — what each state means for your code
49
+
50
+ `source.capabilities()` returns this snapshot. Branch on it for any
51
+ code path whose correctness depends on the underlying RPC's surface.
52
+
53
+ | Field | States | What to do |
54
+ |---|---|---|
55
+ | `newHeads` | `subscription` / `poll-only` / `unavailable` | If `subscription`, you'll get push events. If `poll-only`, you're on the interval timer (default 10s). `unavailable` means the transport has no `subscribe` at all (HTTP-only). |
56
+ | `newPendingTransactions` | `subscription` / `poll-only` / `unavailable` | Same shape. `poll-only` here means mempool falls back to the `txpool_content` tick. `unavailable` means there's no mempool path at all on this provider — `subscribeMempool` will never fire and `getMempoolSnapshot()` returns `null`. |
57
+ | `txpoolContent` | `available` / `gated` | Most public RPCs gate this. If `gated`, mempool is silent unless WS push is available. |
58
+ | `receiptByHash` | `available` / `unavailable` | `unavailable` is rare but real on some L2s/sidechains. tx-tracker's receipt-poll fallback path becomes a no-op. |
59
+ | `reprobeOnReconnect` | boolean | `true` for WS transports that signal reconnect; `false` for HTTP. Informational. |
60
+
61
+ Always read capabilities AFTER `await source.ready()` if you're
62
+ branching at construction time — before that, you get a defensive
63
+ default with everything `unavailable` / `gated`.
64
+
65
+ ## Per-RPC capability profiles (what to expect)
66
+
67
+ | RPC class | `newHeads` | `newPendingTxs` | `txpool_content` | Notes |
68
+ |---|---|---|---|---|
69
+ | Self-hosted geth/reth (HTTP+WS) | `subscription` | `subscription` | `available` | Full surface; the default-everything-works case. |
70
+ | Self-hosted geth/reth (HTTP only) | `unavailable` | `poll-only` | `available` | No push; poll cycle drives everything. |
71
+ | Alchemy / Infura / QuickNode (WS) | `subscription` | `subscription` (Alchemy) / `unavailable` (some) | `gated` | Push-based blocks; mempool depends on plan tier. |
72
+ | Public RPC aggregators (LlamaNodes, Ankr, etc., HTTP) | `unavailable` | `unavailable` | `gated` | Block-poll only; mempool is silent. Set `poll: { mempool: false }` to skip the doomed RPC. |
73
+ | PulseChain RPCs (typical) | varies by node | varies | often `gated` | Verify `txpool_content` per node — public PulseChain RPCs frequently gate it. |
74
+
75
+ If `txpoolContent === 'gated'` AND `newPendingTransactions === 'unavailable'`,
76
+ mempool data is structurally unobtainable on this RPC. Flag that to the
77
+ user — don't write code that silently does nothing.
78
+
79
+ ## Anti-patterns to flag
80
+
81
+ When reviewing user code, watch for these and suggest fixes:
82
+
83
+ 1. **Constructing a `ChainSource` per request / per render.** The
84
+ capability probe runs eagerly at construction; the poll loop runs
85
+ on a timer. Both are wasted if you tear it down 100ms later.
86
+ Module-scope it.
87
+
88
+ 2. **Constructing a separate `ChainSource` for the gas-oracle AND
89
+ for the tx-tracker.** That's two independent poll cycles for the
90
+ same chain — double the RPC traffic for no benefit. Construct one
91
+ source, pass it to both:
92
+ ```ts
93
+ const source = createChainSource({ client })
94
+ const oracle = createGasOracle({ source, chainId: 1 })
95
+ const tracker = createTxTracker({ source, chainId: 1 })
96
+ ```
97
+
98
+ 3. **Reading `source.capabilities()` without `await source.ready()`
99
+ first** — getting the defensive default and branching on it. Code
100
+ ends up perma-stuck on the most-defensive path even on a fully-
101
+ capable provider. Either `await ready()` or wait for the first
102
+ subscribe event before branching.
103
+
104
+ 4. **Ignoring `source.capabilities().txpoolContent === 'gated'`** then
105
+ wiring up `subscribeMempool` and being confused about why nothing
106
+ fires. If the upstream gates the method AND there's no WS push
107
+ path for `newPendingTransactions`, the mempool stream will never
108
+ emit — either set `poll: { mempool: false }` to skip the doomed
109
+ RPC or surface the gap to the user.
110
+
111
+ 5. **Calling `source.start()` but never `subscribeBlocks` / never
112
+ `subscribeMempool`.** The poll cycle runs, the RPCs fire, but
113
+ nothing is consuming the events — pure waste. Either subscribe or
114
+ use the on-demand methods (`getBlock`, `getReceipt`, etc.) which
115
+ don't need `start()`.
116
+
117
+ 6. **Calling `source.subscribeMempool` and treating the snapshots as
118
+ diffs.** They're not — every successful mempool tick emits the
119
+ FULL pending+queued snapshot. If you need diffs, hold the previous
120
+ snapshot in your subscriber and compute the delta yourself.
121
+
122
+ 7. **Persisting `BlockResult` / `NormalizedMempool` via
123
+ `JSON.stringify`** without hex-encoding bigints first. The
124
+ wire-shape types are hex strings already, but anything you
125
+ decoded to bigint will throw. The toolkit deliberately keeps
126
+ bigint as the canonical numeric form internally; encoding at the
127
+ wire boundary is the consumer's job.
128
+
129
+ 8. **Subscribing the same callback reference twice expecting two
130
+ deliveries.** The backing set deduplicates by reference. If you
131
+ really want two deliveries, register two distinct closures.
132
+
133
+ ## Standalone usage — minimal
134
+
135
+ ```ts
136
+ import { createPublicClient, http } from 'viem'
137
+ import { mainnet } from 'viem/chains'
138
+ import { createChainSource } from '@valve-tech/chain-source'
139
+
140
+ const client = createPublicClient({ chain: mainnet, transport: http() })
141
+ const source = createChainSource({
142
+ client,
143
+ pollIntervalMs: 12_000, // optional; default 10_000
144
+ poll: { mempool: false }, // skip doomed txpool_content
145
+ onError: (method, err) => console.warn(method, err),
146
+ })
147
+
148
+ await source.ready() // wait for capability probe before branching
149
+
150
+ if (source.capabilities().newHeads === 'unavailable') {
151
+ console.log('poll-only mode — no WS subscribe path')
152
+ }
153
+
154
+ const unsub = source.subscribeBlocks((block) => {
155
+ console.log('new block', BigInt(block.number))
156
+ })
157
+
158
+ source.start()
159
+ // ... later
160
+ unsub()
161
+ source.stop()
162
+ ```
163
+
164
+ ## Composing with gas-oracle and tx-tracker
165
+
166
+ When the user is using two or more derived views, the canonical shape
167
+ is one shared source:
168
+
169
+ ```ts
170
+ import { createPublicClient, http } from 'viem'
171
+ import { mainnet } from 'viem/chains'
172
+ import { createChainSource } from '@valve-tech/chain-source'
173
+ import { createGasOracle } from '@valve-tech/gas-oracle'
174
+ import { createTxTracker } from '@valve-tech/tx-tracker'
175
+
176
+ const client = createPublicClient({ chain: mainnet, transport: http() })
177
+ const source = createChainSource({ client })
178
+ const oracle = createGasOracle({ source, chainId: 1 })
179
+ const tracker = createTxTracker({ source, chainId: 1 })
180
+
181
+ source.start(); oracle.start(); tracker.start()
182
+ ```
183
+
184
+ `ChainSource` owns the upstream poll cycle. The oracle reads it for
185
+ tier reduction; the tracker reads it for per-tx observations. **One
186
+ upstream RPC poll cycle, two derived views** (per spec §3.1). Each
187
+ surface owns its own lifecycle — `oracle.stop()` does not stop the
188
+ source or the tracker. The owner of the source (whoever called
189
+ `createChainSource`) calls `source.stop()`.
190
+
191
+ For per-tx tracking work (subscribe to a hash, watch for replacement,
192
+ detect drops), redirect to the tx-tracker integration skill at
193
+ `node_modules/@valve-tech/tx-tracker/skills/tx-tracker-integration/SKILL.md` —
194
+ chain-source itself is intentionally **stateless about per-tx
195
+ anything**.
196
+
197
+ For gas-tier work (priority-fee tiers, replacement bumps, block
198
+ position queries), redirect to
199
+ `node_modules/@valve-tech/gas-oracle/skills/gas-oracle-integration/SKILL.md` —
200
+ chain-source is intentionally **stateless about gas math**.
201
+
202
+ ## On-demand RPCs without a running source
203
+
204
+ When you only need a one-shot RPC (a script, a test, a utility
205
+ function that already has a `PublicClient`), the package exports
206
+ free-function transport helpers — same calls as the source's instance
207
+ methods, no construction or lifecycle overhead:
208
+
209
+ ```ts
210
+ import {
211
+ fetchBlock,
212
+ fetchReceipt,
213
+ fetchTransaction,
214
+ fetchFeeHistory,
215
+ fetchTxPool,
216
+ safeRequest,
217
+ } from '@valve-tech/chain-source'
218
+
219
+ const block = await fetchBlock(client, 'latest')
220
+ const receipt = await fetchReceipt(client, '0xabc...')
221
+ const tx = await fetchTransaction(client, '0xabc...')
222
+
223
+ // Custom method with the same "never throws, returns null" posture:
224
+ const custom = await safeRequest(client, 'eth_chainId', [])
225
+ ```
226
+
227
+ These are the same primitives the source uses internally. Each takes
228
+ an optional `onError(err)` sink as the last arg.
229
+
230
+ ## The `EventSource` discriminator
231
+
232
+ When a downstream view (tx-tracker, your own derived view) emits
233
+ events built from chain-source observations, those events should
234
+ carry an `EventSource` discriminator chosen against the capability
235
+ matrix. This is the toolkit's "no silent downgrade" rule made
236
+ observable:
237
+
238
+ ```ts
239
+ type EventSource =
240
+ | 'subscription' // arrived via eth_subscribe (push)
241
+ | 'block-poll' // arrived via the source's eth_getBlockByNumber tick
242
+ | 'mempool-snapshot' // arrived via the source's txpool_content tick
243
+ | 'receipt-poll' // tx-tracker fallback per-hash receipt poll
244
+ ```
245
+
246
+ When you build your own derived view, follow the same pattern —
247
+ attach a `source` field to your emitted events so consumers can filter
248
+ to `'subscription'` for hard-guarantee freshness.
249
+
250
+ ## Where to find more
251
+
252
+ - Full API + types: `node_modules/@valve-tech/chain-source/AGENTS.md`
253
+ - Human-facing docs: `node_modules/@valve-tech/chain-source/README.md`
254
+ - Source (when types alone aren't enough): `node_modules/@valve-tech/chain-source/dist/`
255
+ (compiled JS + .d.ts) — sources aren't shipped, only built output.
256
+ - Design contract: the published-on-GitHub `docs/tx-tracker-spec.md`
257
+ §3 covers the source's role in the toolkit's broader architecture.