@valve-tech/chain-source 0.10.0 → 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 +271 -0
- package/CHANGELOG.md +18 -0
- package/dist/types.d.ts +7 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -6
- package/dist/types.js.map +1 -1
- package/package.json +3 -1
- package/skills/chain-source-integration/SKILL.md +257 -0
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,24 @@ 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
|
+
|
|
19
|
+
## [0.10.1] — 2026-05-08
|
|
20
|
+
|
|
21
|
+
Synchronized release — no changes to this package. Republished at
|
|
22
|
+
0.10.1 alongside the rest of the toolkit; v0.10.0 only got
|
|
23
|
+
trueblocks-sdk publishing wrong (missing `repository` field tripped
|
|
24
|
+
provenance validation), so the rest of the line had to bump to
|
|
25
|
+
re-sync.
|
|
26
|
+
|
|
9
27
|
## [0.10.0] — 2026-05-08
|
|
10
28
|
|
|
11
29
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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)`
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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.
|
|
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.
|