@valve-tech/tx-tracker 0.5.0 → 0.7.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,237 @@
1
+ # AGENTS.md
2
+
3
+ Terse reference for AI agents (Claude Code, Cursor, Aider, etc.) integrating
4
+ `@valve-tech/tx-tracker`. 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
+ Per-tx state machine for EVM chains. Consumes a `ChainSource`'s block +
11
+ mempool stream and emits **neutral observations** (`seen-in-mempool`,
12
+ `seen-in-block`, `replaced-by`, `vanished-from-block`,
13
+ `unseen-for-N-blocks`, `signal-degraded`, `signal-recovered`,
14
+ `stopped`). Three consumption shapes (callback / async iterator /
15
+ snapshot) over one push-based core. Per-method capability disclosure
16
+ keeps the no-silent-downgrade rule.
17
+
18
+ Sibling to `@valve-tech/gas-oracle` — both consume the same
19
+ `ChainSource`, neither depends on the other. One upstream RPC poll
20
+ cycle can feed both.
21
+
22
+ `viem ^2.0.0` is the only external peer. `@valve-tech/chain-source`
23
+ is a runtime dependency.
24
+
25
+ ## Public API
26
+
27
+ All exports live under `src/index.ts` (single subpath; no sub-exports).
28
+
29
+ ```ts
30
+ import {
31
+ createTxTracker, // primary constructor
32
+ createInMemoryStore, // default TxTrackerStore impl
33
+ computeRetentionExpiry, // pure helper
34
+ defaultRetentionBlocks, // 64
35
+ defaultReorgDepthBlocks, // 12
36
+ defaultMaxBulkSubscriptions, // 16
37
+ // pure detectors / matchers
38
+ appendBlock,
39
+ detectDivergences,
40
+ compileSelector,
41
+ matchAll,
42
+ // event builders (mostly internal — exported for store implementers)
43
+ buildStarted, buildSeenInMempool, buildLeftMempool,
44
+ buildSeenInBlock, buildVanishedFromBlock, buildReplacedBy,
45
+ buildUnseenForNBlocks, buildSignalDegraded, buildSignalRecovered,
46
+ buildStopped, buildInitialStatus,
47
+ // types
48
+ type TxTracker,
49
+ type CreateTxTrackerOptions,
50
+ type TrackOptions,
51
+ type BulkTrackOptions,
52
+ type TxMatchEvent,
53
+ type TxSubscription,
54
+ type LostSignalPolicy,
55
+ type TxEvent,
56
+ type TxEventStarted, type TxEventSeenInMempool, type TxEventLeftMempool,
57
+ type TxEventSeenInBlock, type TxEventVanishedFromBlock,
58
+ type TxEventReplacedBy, type TxEventUnseenForNBlocks,
59
+ type TxEventSignalDegraded, type TxEventSignalRecovered,
60
+ type TxEventStopped,
61
+ type TxStatus,
62
+ type Address, type Hash, type At, type Envelope,
63
+ // store types
64
+ type TxTrackerStore,
65
+ type TrackedTxRecord,
66
+ type PersistedSubscription,
67
+ type HashSelector,
68
+ type BulkSelector,
69
+ type InMemoryStoreOptions,
70
+ // reorg
71
+ type BlockSample,
72
+ type BlockDivergence,
73
+ // selectors
74
+ type CompiledSelector,
75
+ type BulkMatchPayload,
76
+ } from '@valve-tech/tx-tracker'
77
+ ```
78
+
79
+ ## Five types you must know
80
+
81
+ | Type | What it is |
82
+ |---|---|
83
+ | `CreateTxTrackerOptions` | Constructor config. Required: `source`, `chainId`. Tuneables: `store`, `lostSignalPolicy`, `reorgDepthBlocks`, `unseenThresholdBlocks`, `maxBulkSubscriptions`, `onError`, `lifecycle`. |
84
+ | `TxEvent` | Discriminated union of 10 variants (see below). Every variant carries `{ hash, chainId, source, at: { blockNumber, timestamp } }`. |
85
+ | `TxStatus` | Cached snapshot returned by `getTxStatus(hash)`. Carries the **last observation** (`lastSeenInBlock`, `lastSeenInMempool`, `replacedBy`, `vanishedAt`) plus housekeeping (`unseenStreak`, `firstObservedAtBlock`, `lastObservedAtBlock`, `capabilities`). |
86
+ | `TxTrackerStore` | Persistence surface. `put` / `get` / `delete` / `listDurable` / `appendEvent` / `readEventLog?`. Default: `createInMemoryStore`. |
87
+ | `BulkSelector` | `{ kind: 'from' \| 'to' \| 'predicate', address?, match? }`. From / to lowercase the address once at compile time. Predicate runs O(N) per tx per tick. |
88
+
89
+ ## The discriminated `TxEvent`
90
+
91
+ Every event carries the same envelope; the `kind` field discriminates
92
+ the variant-specific payload.
93
+
94
+ ```ts
95
+ type TxEvent =
96
+ | { kind: 'started'; capabilities: Capabilities }
97
+ | { kind: 'seen-in-mempool'; bucket: 'pending' | 'queued'; tx: RawTx }
98
+ | { kind: 'left-mempool' }
99
+ | { kind: 'seen-in-block'; blockHash; blockNumber; transactionIndex; confirmations }
100
+ | { kind: 'vanished-from-block'; previousBlockHash; canonicalBlockHash; blockNumber }
101
+ | { kind: 'replaced-by'; replacementHash; replacementBlockNumber: bigint | null }
102
+ | { kind: 'unseen-for-N-blocks'; blocks: number }
103
+ | { kind: 'signal-degraded'; capabilityLost: keyof Capabilities; fallbackSource }
104
+ | { kind: 'signal-recovered'; capabilityRestored: keyof Capabilities }
105
+ | { kind: 'stopped'; reason: 'unsubscribed' | 'retention-expired' | 'tracker-stopped' }
106
+ ```
107
+
108
+ The envelope on every variant:
109
+
110
+ ```ts
111
+ {
112
+ hash: Hash
113
+ chainId: number
114
+ source: 'subscription' | 'block-poll' | 'mempool-snapshot' | 'receipt-poll'
115
+ at: { blockNumber: bigint; timestamp: bigint }
116
+ }
117
+ ```
118
+
119
+ ## Three consumption shapes (consistent across all three)
120
+
121
+ ```ts
122
+ // 1. Snapshot — sub-millisecond, returns null if not tracked
123
+ const status = tracker.getTxStatus(hash)
124
+
125
+ // 2. Callback — returns an unsubscribe handle
126
+ const unsub = tracker.subscribe(hash, (event) => { /* ... */ })
127
+
128
+ // 3. Async iterator — recommended for new code
129
+ for await (const event of tracker.track(hash)) {
130
+ if (event.kind === 'seen-in-block' && event.confirmations >= 6) break
131
+ }
132
+ ```
133
+
134
+ All three back onto the same internal `Subscriptions<TxEvent>` per
135
+ hash, so they see consistent state.
136
+
137
+ ## Bulk subscriptions (indexer-style)
138
+
139
+ ```ts
140
+ const sub = tracker.trackFromAddress(treasuryAddress, { durable: true })
141
+ // raw match stream:
142
+ for await (const m of sub.events()) { /* m: TxMatchEvent */ }
143
+ // per-hash event stream (auto-tracked by default):
144
+ sub.subscribe((event) => { /* TxEvent */ })
145
+ sub.stop() // does NOT stop already-auto-tracked per-hash subs
146
+ ```
147
+
148
+ `trackFromAddress` / `trackToAddress` / `trackPredicate`. Capped at
149
+ `maxBulkSubscriptions: 16` by default.
150
+
151
+ ## Composing with gas-oracle
152
+
153
+ One `ChainSource` shared across both — one upstream RPC poll cycle:
154
+
155
+ ```ts
156
+ import { createChainSource } from '@valve-tech/chain-source'
157
+ import { createGasOracle } from '@valve-tech/gas-oracle'
158
+ import { createTxTracker } from '@valve-tech/tx-tracker'
159
+
160
+ const source = createChainSource({ client })
161
+ const oracle = createGasOracle({ source, chainId: 1 })
162
+ const tracker = createTxTracker({ source, chainId: 1 })
163
+
164
+ source.start(); oracle.start(); tracker.start()
165
+ ```
166
+
167
+ Each surface owns its own lifecycle — `oracle.stop()` does not stop
168
+ the source or the tracker.
169
+
170
+ ## Configuration patterns
171
+
172
+ | Setting | Default | Tune up for | Tune down for |
173
+ |---|---|---|---|
174
+ | `reorgDepthBlocks` | 12 | Weak-finality chains (PoW, small validator sets) | High-finality chains; only care about shallow reorgs |
175
+ | `unseenThresholdBlocks` | 30 | Slow chains (Ethereum: ~6 min) | Fast L2s |
176
+ | `lostSignalPolicy` | `'emit-uncertain'` | (default — loud is correct) | `'silent'` for wallets that don't want capability-churn UI flicker |
177
+ | `createInMemoryStore({ retentionBlocks })` | 64 | Indexers replaying long windows | Wallet UIs |
178
+ | `createInMemoryStore({ eventLogCapacity })` | 256 | Heavy catch-up on restart | Memory-constrained mobile / edge |
179
+
180
+ `reorgDepthBlocks` and retention are in **block-units, not seconds** —
181
+ reorg safety is a depth invariant. Spec §10.1.
182
+
183
+ ## Wire format
184
+
185
+ All numeric fields are `bigint` (block numbers, fees, timestamps).
186
+ `JSON.stringify(event)` will throw without hex-encoding at the wire
187
+ boundary. Durable store implementers MUST hex-encode (`'0x' + n.toString(16)`)
188
+ on write and decode on read. The default in-memory store keeps `bigint`
189
+ end-to-end.
190
+
191
+ ## Capability disclosure
192
+
193
+ `tracker.capabilities()` forwards the source's snapshot:
194
+
195
+ ```ts
196
+ {
197
+ newHeads: 'subscription' | 'poll-only' | 'unavailable'
198
+ newPendingTransactions: 'subscription' | 'poll-only' | 'unavailable'
199
+ txpoolContent: 'available' | 'gated'
200
+ receiptByHash: 'available' | 'unavailable'
201
+ reprobeOnReconnect: boolean
202
+ }
203
+ ```
204
+
205
+ When capabilities change mid-tracking, the tracker emits
206
+ `signal-degraded` / `signal-recovered` per affected key. Consumers that
207
+ need hard inclusion guarantees filter to `event.source === 'subscription'`.
208
+
209
+ ## Examples
210
+
211
+ - `examples/07-tx-tracker.ts` — minimal tracker, no oracle (async iterator)
212
+ - `examples/08-tx-tracker-with-oracle.ts` — shared `ChainSource` between gas-oracle + tracker
213
+ - `examples/09-bulk-from-address.ts` — indexer-style bulk subscription
214
+
215
+ (Examples live under `node_modules/@valve-tech/gas-oracle/examples/` —
216
+ the toolkit's examples directory is hosted by gas-oracle.) Run with
217
+ `yarn tsx examples/07-tx-tracker.ts`.
218
+
219
+ ## Skills (for AI agents)
220
+
221
+ `skills/` directory ships in the npm tarball. If you're an AI agent
222
+ working in a project that has installed this package, look in
223
+ `node_modules/@valve-tech/tx-tracker/skills/tx-tracker-integration/SKILL.md`
224
+ for trigger conditions and integration recipes that go deeper than this
225
+ file.
226
+
227
+ ## Verifying provenance
228
+
229
+ v0.6.0+ ships with SLSA provenance attestation:
230
+
231
+ ```bash
232
+ npm view @valve-tech/tx-tracker@latest --json | jq .dist.attestations
233
+ npm audit signatures
234
+ ```
235
+
236
+ The attestation links the published tarball to the GitHub Actions
237
+ workflow run that built it.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,144 @@ 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.7.0] — 2026-05-06
10
+
11
+ > **The implementation lands.** This is the first release of
12
+ > `@valve-tech/tx-tracker` with a real public surface. Prior versions
13
+ > (v0.0.1 → v0.6.0) were stubs reserving the npm name. The full
14
+ > design contract is at `docs/tx-tracker-spec.md` in the
15
+ > `valve-tech/evm-toolkit` repo.
16
+
17
+ ### Added
18
+
19
+ - **Per-tx state machine** (`createTxTracker`) consuming a
20
+ `ChainSource` for upstream block + mempool signals. Three
21
+ consumption shapes over one push-based core: `getTxStatus(hash)`
22
+ for the cached snapshot, `subscribe(hash, cb)` for callback-style,
23
+ and `track(hash)` for the async-iterator shape — all three back
24
+ onto the same internal stream so they see consistent state.
25
+ - **`TxEvent` discriminated union** (spec §6) with neutral
26
+ observation kinds: `started`, `seen-in-mempool`, `left-mempool`,
27
+ `seen-in-block`, `vanished-from-block`, `replaced-by`,
28
+ `unseen-for-N-blocks`, `signal-degraded`, `signal-recovered`,
29
+ `stopped`. Every event carries an envelope (`hash`, `chainId`,
30
+ `source`, `at: { blockNumber, timestamp }`) so consumers can
31
+ apply policy (`'confirmed'`, `'stuck'`, etc.) in their own UX
32
+ voice without the tracker prejudging.
33
+ - **`TxTrackerStore` interface + `createInMemoryStore` default**
34
+ (spec §9, §10). Block-unit retention (`retentionBlocks: 64` by
35
+ default — reorg safety is a depth invariant, not a wall-clock
36
+ invariant), bounded per-hash audit log (`eventLogCapacity: 256`
37
+ by default) for catch-up replay.
38
+ - **Reorg detector** (`detectDivergences`, spec §12) — pure function
39
+ over `BlockSample[]` that flags same-height different-hash
40
+ divergences within `reorgDepthBlocks` (default 12). Ring is
41
+ conservative about heights with no canonical entry — a partial
42
+ canonical sequence does not nuke unrelated ring entries.
43
+ - **Bulk subscriptions** (spec §11): `trackFromAddress`,
44
+ `trackToAddress`, `trackPredicate`. Auto-tracks matched hashes
45
+ by default (`autoTrackMatched: true`) so the per-hash event
46
+ stream is available too. Capped at `maxBulkSubscriptions: 16`.
47
+ - **Capability disclosure** — `tracker.capabilities()` forwards the
48
+ source's snapshot. `signal-degraded` / `signal-recovered` events
49
+ fire on every tracked hash when source-level capability
50
+ transitions cross authority boundaries.
51
+ - **Replacement detection** — caches `(from, nonce)` on first
52
+ observation and emits `replaced-by` when a different hash with
53
+ the same identity appears (mempool: `replacementBlockNumber: null`;
54
+ block: filled-in block number).
55
+ - **`subscribeAll(cb)`** — global stream of every event the tracker
56
+ emits, useful for indexers piping to a single sink.
57
+ - **`AGENTS.md`** + **`skills/tx-tracker-integration/SKILL.md`** for AI
58
+ agents working in downstream projects that import the package. Both
59
+ ship in the npm tarball; the SKILL.md trigger phrases catch
60
+ "track this transaction," "watch tx hash," "stuck transaction," and
61
+ composition questions with `@valve-tech/gas-oracle`.
62
+
63
+ ### Changed
64
+
65
+ - **Coverage hardening pre-1.0.** Eliminated dead defensive branches
66
+ in `reorg.ts` (sort-comparator equal-key arms unreachable after
67
+ the dedup `filter`; `?? 0n` defaults unreachable after the empty-
68
+ array early return). Tightened `capabilityRank`'s input type from
69
+ `string` to a `CapabilityValue` union literal so the switch is
70
+ exhaustive without a default arm.
71
+ - **Test suite up from 75 → 95 tests** (+20). New coverage:
72
+ `trackToAddress`, per-subscription `lostSignalPolicy` overrides,
73
+ durable-subscription persistence (with stub stores), predicate-
74
+ selector + `durable: true` warning, store.appendEvent / store.put
75
+ failure routing through `onError`, bad-block-number handling,
76
+ async iterator queue-vs-waiter ordering and early-break cleanup,
77
+ bulk async iterator drain via `sub.stop()`, multi-sub-on-same-hash
78
+ cleanup semantics, idempotent `stop` / `unsub` / `sub.stop`,
79
+ reorg handler skipping records without `lastSeenInBlock`,
80
+ `findReplacement` raw-nonce fallback when `BigInt()` throws,
81
+ `lifecycle: 'lazy'` accepts-the-option contract.
82
+ - Coverage went **89.23% / 78.59% / 92.13% / 91.84%** stmts / branches
83
+ / funcs / lines → **96.13% / 88.93% / 98.87% / 97.61%**, then to
84
+ **97.22% / 92.7% / 98.9% / 98.12%** after the per-record decision
85
+ logic was extracted into pure functions (see "Refactor" below).
86
+
87
+ ### Refactor
88
+
89
+ - **Per-record decision logic extracted from `tracker.ts` into a new
90
+ pure module `observations.ts`.** The previous shape — two giant
91
+ closures inside `onBlock` / `onMempool` mutating shared state and
92
+ emitting events as a side effect — was a pile of conditionals that
93
+ could only be tested by spinning up the full state machine through
94
+ a stub source. Now `decideBlockObservation` and
95
+ `decideMempoolObservation` are pure functions: literal inputs in,
96
+ `{ events, statusPatch, identityPatch, inMempoolPatch }` out. The
97
+ orchestrator in `tracker.ts` shrank to "compute envelope, loop
98
+ records, call decision fn, merge patch, emit events." Same shape
99
+ as the rest of the toolkit (`reducePollInputs` pure / poll loop
100
+ stateful in gas-oracle; math pure / source stateful in chain-source).
101
+ - **`observations.ts` lands at 100% statements / 100% branches**
102
+ (67/67 stmts, 55/55 branches) covered by 33 fixture-driven unit
103
+ tests in `observations.test.ts`. Each per-record decision arm
104
+ has a dedicated test with literal inputs — no async, no stubs,
105
+ no shared state.
106
+ - `tracker.ts` shrank from 374 statements → 344 (the extracted
107
+ code is gone) and is now mostly orchestration; its branch
108
+ coverage rose from 86.69% → 88.75%.
109
+ - `findReplacement` (closure-based) replaced with pure
110
+ `findReplacementInMempool(snapshot, identity, originalHash)`.
111
+ `cacheIdentityFromTx` (mutation-based) replaced with pure
112
+ `cacheIdentity(current, tx)` returning a patch.
113
+ - **No behavior change.** All 95 pre-refactor integration tests
114
+ continue to pass unchanged; the refactor is internal-only and
115
+ the public API surface is identical.
116
+ - Tracker test suite: 95 → 133 (+38: 33 from `observations.test.ts`
117
+ plus 5 new tracker integration tests covering reorg height-mismatch
118
+ skip and async-iterator multi-waiter drain paths).
119
+
120
+ ### Notes
121
+
122
+ - Implements spec §5–§12 minus the `'receipt-poll-fallback'`
123
+ lostSignalPolicy strategy (the type is accepted; the runtime
124
+ falls back to `'emit-uncertain'` and a follow-up PR adds the
125
+ per-block receipt fetch path).
126
+ - Predicate bulk selectors are silently non-durable per spec §13.2
127
+ (closures don't survive a process boundary). The tracker logs a
128
+ warning via `onError` when a `predicate` selector is registered
129
+ with `durable: true` and persists everything else about the
130
+ selector.
131
+ - `gas-oracle` and `tx-tracker` remain siblings — neither imports
132
+ the other; both consume `@valve-tech/chain-source` directly.
133
+
134
+ ## [0.6.0] — 2026-05-05
135
+
136
+ ### Notes
137
+
138
+ - Synchronized release — no functional changes to this package
139
+ (still a stub on npm). Bumped in lockstep with
140
+ `@valve-tech/chain-source@0.6.0` (block-stream dedup + head-probe
141
+ gating in the source tick) and `@valve-tech/gas-oracle@0.6.0`
142
+ (now consumes ChainSource via `source?: ChainSource`). The
143
+ tx-tracker implementation track lands in a future minor — this
144
+ version exists to keep the synced version line consistent across
145
+ the toolkit.
146
+
9
147
  ## [0.5.0] — 2026-05-05
10
148
 
11
149
  ### Notes
package/README.md CHANGED
@@ -1,10 +1,5 @@
1
1
  # @valve-tech/tx-tracker
2
2
 
3
- > **Status: stub (v0.0.1).** This package is a name reservation. The
4
- > implementation lands in v0.1.0. See
5
- > [`docs/tx-tracker-spec.md`](https://github.com/valve-tech/evm-toolkit/blob/main/docs/tx-tracker-spec.md)
6
- > for the full design contract.
7
-
8
3
  Per-tx state machine for EVM chains. Emits **neutral observations** —
9
4
  `seen-in-mempool`, `seen-in-block`, `replaced-by`, `vanished-from-block`,
10
5
  `unseen-for-N-blocks`, `signal-degraded`, `signal-recovered`, `stopped` —
@@ -12,8 +7,11 @@ so wallet UIs, indexers, and relays can write their own interpretations
12
7
  on top. The package itself never says "confirmed" or "stuck"; it gives
13
8
  you the data to decide.
14
9
 
10
+ See
11
+ [`docs/tx-tracker-spec.md`](https://github.com/valve-tech/evm-toolkit/blob/main/docs/tx-tracker-spec.md)
12
+ for the full design contract.
13
+
15
14
  ```ts
16
- // v0.1.0+ shape (not yet implemented):
17
15
  import { createChainSource } from '@valve-tech/chain-source'
18
16
  import { createTxTracker } from '@valve-tech/tx-tracker'
19
17
 
@@ -51,6 +49,15 @@ adapters (callback / async iterator / snapshot).
51
49
  yarn add @valve-tech/tx-tracker @valve-tech/chain-source viem
52
50
  ```
53
51
 
52
+ ## For AI agents
53
+
54
+ This package ships an [`AGENTS.md`](AGENTS.md) reference and a
55
+ [`skills/`](skills/) directory for Claude Code / Cursor skill files
56
+ shipped in the npm tarball. After install, both are reachable at:
57
+
58
+ - `node_modules/@valve-tech/tx-tracker/AGENTS.md`
59
+ - `node_modules/@valve-tech/tx-tracker/skills/tx-tracker-integration/SKILL.md`
60
+
54
61
  ## License
55
62
 
56
63
  MIT