@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.
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Per-record decision functions — the **pure** logic that turns one
3
+ * upstream observation (a block or a mempool snapshot) plus one
4
+ * tracked record's current state into the events that should be
5
+ * emitted and the state patch the orchestrator should apply.
6
+ *
7
+ * These functions are extracted from `tracker.ts`'s `onBlock` /
8
+ * `onMempool` so each branch of the per-record state machine is
9
+ * testable with literal fixture inputs — no stub source, no async
10
+ * orchestration, no shared mutable closure. This is the same
11
+ * primitive-vs-orchestrator split that `oracle.ts` (`reducePollInputs`
12
+ * pure / poll loop stateful) and `chain-source` (math pure / source
13
+ * stateful) already follow.
14
+ *
15
+ * Inputs are immutable; outputs are immutable. The caller in
16
+ * `tracker.ts` applies the returned `statusPatch` and `identityPatch`
17
+ * to its mutable `TrackedRecord`, then emits the returned `events`
18
+ * via its event-bus + store-audit-log machinery.
19
+ */
20
+ import type { EventSource, RawTx } from '@valve-tech/chain-source';
21
+ import { type At, type Hash, type TxEvent, type TxStatus } from './events.js';
22
+ /**
23
+ * Read-only projection of `TrackedRecord` that the decision functions
24
+ * consume. The orchestrator passes its mutable record through this
25
+ * shape so the pure layer cannot accidentally mutate state.
26
+ */
27
+ export interface ReadonlyTrackedRecord {
28
+ hash: Hash;
29
+ status: TxStatus;
30
+ identity: {
31
+ from: string;
32
+ nonce: string;
33
+ } | null;
34
+ inLastMempoolSnapshot: boolean;
35
+ unseenThresholdBlocks: number;
36
+ }
37
+ /**
38
+ * Cached `(from, nonce)` identity of the tracked tx. Used for
39
+ * replacement detection.
40
+ */
41
+ export interface IdentityPatch {
42
+ from: string;
43
+ nonce: string;
44
+ }
45
+ /**
46
+ * Result of a decision function: events to emit and patches to apply
47
+ * to the tracked record. The patch shapes are deliberately narrow —
48
+ * only the fields the function decided to change. The orchestrator
49
+ * merges them into its mutable record.
50
+ */
51
+ export interface ObservationResult {
52
+ events: TxEvent[];
53
+ statusPatch: Partial<TxStatus>;
54
+ identityPatch: IdentityPatch | null;
55
+ /** Set when the per-mempool-tick "in-snapshot?" flag should change. */
56
+ inMempoolPatch: boolean | null;
57
+ }
58
+ /**
59
+ * Inputs to `decideBlockObservation`. The orchestrator builds a
60
+ * `txHashSet` once per block and passes it for O(1) "did this hash
61
+ * appear?" lookups across every tracked record.
62
+ */
63
+ export interface BlockObservationInput {
64
+ record: ReadonlyTrackedRecord;
65
+ blockHash: Hash;
66
+ blockNumber: bigint;
67
+ txHashSet: ReadonlySet<Hash>;
68
+ txs: ReadonlyArray<RawTx>;
69
+ chainId: number;
70
+ eventSource: EventSource;
71
+ envelope: At;
72
+ /**
73
+ * The previous canonical tip's block number, or `null` if this is
74
+ * the first block the tracker has seen. Used to gate the
75
+ * "confirmation bump" path — we don't bump confirmations on the
76
+ * very first block we observe.
77
+ */
78
+ previousTipNumber: bigint | null;
79
+ }
80
+ /**
81
+ * Per-record decision for one new canonical block. Returns the events
82
+ * to emit and the state patch to apply. Mutually-exclusive paths,
83
+ * evaluated in order:
84
+ *
85
+ * 1. Hash is in this block → fresh inclusion (emit `seen-in-block`
86
+ * with `confirmations: 1`) OR same-block re-observation (no
87
+ * emit; `lastSeenInBlock` is already current).
88
+ * 2. Hash NOT in this block but was previously included → bump
89
+ * `confirmations` on the cached observation, emit a fresh
90
+ * `seen-in-block` carrying the new count.
91
+ * 3. Hash NOT in this block, no prior inclusion, but identity is
92
+ * cached AND a different hash with the same `(from, nonce)` is
93
+ * in this block → emit `replaced-by` with the replacement's
94
+ * block number.
95
+ * 4. Truly unseen → bump the unseen-block streak; emit
96
+ * `unseen-for-N-blocks` when the streak crosses the
97
+ * subscription's threshold. Does NOT emit on the first block
98
+ * after subscription (no `firstObservedAtBlock` yet).
99
+ */
100
+ export declare const decideBlockObservation: (input: BlockObservationInput) => ObservationResult;
101
+ /**
102
+ * Inputs to `decideMempoolObservation`. The orchestrator builds the
103
+ * hash-keyed snapshot index once per mempool tick.
104
+ */
105
+ export interface MempoolObservationInput {
106
+ record: ReadonlyTrackedRecord;
107
+ presence: {
108
+ bucket: 'pending' | 'queued';
109
+ tx: RawTx;
110
+ } | null;
111
+ /**
112
+ * The replacement candidate found in the snapshot for this record's
113
+ * `(from, nonce)` identity, or `null` if none. Computed by the
114
+ * orchestrator once per record so this function stays pure on
115
+ * inputs (no closure over the snapshot).
116
+ */
117
+ replacementInMempool: RawTx | null;
118
+ chainId: number;
119
+ eventSource: EventSource;
120
+ envelope: At;
121
+ /**
122
+ * The current canonical-tip block number the orchestrator is using
123
+ * for `firstObservedAtBlock` / `lastObservedAtBlock` book-keeping.
124
+ * Falls back to `0n` when no tip has been observed yet.
125
+ */
126
+ tipBlockNumber: bigint;
127
+ }
128
+ /**
129
+ * Per-record decision for one mempool snapshot. Three independent
130
+ * outputs that may all fire on the same call:
131
+ *
132
+ * - **Presence transition** — emit `seen-in-mempool` on first
133
+ * observation or bucket change; emit `left-mempool` when a
134
+ * previously-seen hash is absent from this snapshot.
135
+ * - **Replacement** — emit `replaced-by` (with `null` block) when
136
+ * the orchestrator's pre-computed `replacementInMempool` is set
137
+ * AND the record hasn't already recorded a replacement.
138
+ */
139
+ export declare const decideMempoolObservation: (input: MempoolObservationInput) => ObservationResult;
140
+ /**
141
+ * Find a tx in `txs` whose `(from, nonce)` matches `identity` but
142
+ * whose hash differs from `originalHash` — the replacement candidate
143
+ * for the original tracked tx. Compares senders case-insensitively
144
+ * since upstreams disagree on checksum form.
145
+ */
146
+ export declare const findReplacementInBlock: (identity: {
147
+ from: string;
148
+ nonce: string;
149
+ }, originalHash: Hash, txs: ReadonlyArray<RawTx>) => RawTx | null;
150
+ /**
151
+ * Cache the tx's `(from, nonce)` as the record's identity if it's
152
+ * not already cached AND the tx carries both fields. Returns the
153
+ * patch to apply (or `null` when no change).
154
+ */
155
+ export declare const cacheIdentity: (current: {
156
+ from: string;
157
+ nonce: string;
158
+ } | null, tx: RawTx) => IdentityPatch | null;
159
+ //# sourceMappingURL=observations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observations.d.ts","sourceRoot":"","sources":["../src/observations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAElE,OAAO,EAML,KAAK,EAAE,EACP,KAAK,IAAI,EACT,KAAK,OAAO,EACZ,KAAK,QAAQ,EACd,MAAM,aAAa,CAAA;AAEpB;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,IAAI,CAAA;IACV,MAAM,EAAE,QAAQ,CAAA;IAChB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAChD,qBAAqB,EAAE,OAAO,CAAA;IAC9B,qBAAqB,EAAE,MAAM,CAAA;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,OAAO,EAAE,CAAA;IACjB,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC9B,aAAa,EAAE,aAAa,GAAG,IAAI,CAAA;IACnC,uEAAuE;IACvE,cAAc,EAAE,OAAO,GAAG,IAAI,CAAA;CAC/B;AAaD;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,qBAAqB,CAAA;IAC7B,SAAS,EAAE,IAAI,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,WAAW,CAAC,IAAI,CAAC,CAAA;IAC5B,GAAG,EAAE,aAAa,CAAC,KAAK,CAAC,CAAA;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,WAAW,CAAA;IACxB,QAAQ,EAAE,EAAE,CAAA;IACZ;;;;;OAKG;IACH,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO,qBAAqB,KAC3B,iBA6IF,CAAA;AAMD;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE;QAAE,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;QAAC,EAAE,EAAE,KAAK,CAAA;KAAE,GAAG,IAAI,CAAA;IAC5D;;;;;OAKG;IACH,oBAAoB,EAAE,KAAK,GAAG,IAAI,CAAA;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,WAAW,CAAA;IACxB,QAAQ,EAAE,EAAE,CAAA;IACZ;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,GACnC,OAAO,uBAAuB,KAC7B,iBAqFF,CAAA;AAMD;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,UAAU;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EACzC,cAAc,IAAI,EAClB,KAAK,aAAa,CAAC,KAAK,CAAC,KACxB,KAAK,GAAG,IAUV,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,GACxB,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC/C,IAAI,KAAK,KACR,aAAa,GAAG,IAIlB,CAAA"}
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Per-record decision functions — the **pure** logic that turns one
3
+ * upstream observation (a block or a mempool snapshot) plus one
4
+ * tracked record's current state into the events that should be
5
+ * emitted and the state patch the orchestrator should apply.
6
+ *
7
+ * These functions are extracted from `tracker.ts`'s `onBlock` /
8
+ * `onMempool` so each branch of the per-record state machine is
9
+ * testable with literal fixture inputs — no stub source, no async
10
+ * orchestration, no shared mutable closure. This is the same
11
+ * primitive-vs-orchestrator split that `oracle.ts` (`reducePollInputs`
12
+ * pure / poll loop stateful) and `chain-source` (math pure / source
13
+ * stateful) already follow.
14
+ *
15
+ * Inputs are immutable; outputs are immutable. The caller in
16
+ * `tracker.ts` applies the returned `statusPatch` and `identityPatch`
17
+ * to its mutable `TrackedRecord`, then emits the returned `events`
18
+ * via its event-bus + store-audit-log machinery.
19
+ */
20
+ import { buildLeftMempool, buildReplacedBy, buildSeenInBlock, buildSeenInMempool, buildUnseenForNBlocks, } from './events.js';
21
+ const EMPTY_RESULT = {
22
+ events: [],
23
+ statusPatch: {},
24
+ identityPatch: null,
25
+ inMempoolPatch: null,
26
+ };
27
+ /**
28
+ * Per-record decision for one new canonical block. Returns the events
29
+ * to emit and the state patch to apply. Mutually-exclusive paths,
30
+ * evaluated in order:
31
+ *
32
+ * 1. Hash is in this block → fresh inclusion (emit `seen-in-block`
33
+ * with `confirmations: 1`) OR same-block re-observation (no
34
+ * emit; `lastSeenInBlock` is already current).
35
+ * 2. Hash NOT in this block but was previously included → bump
36
+ * `confirmations` on the cached observation, emit a fresh
37
+ * `seen-in-block` carrying the new count.
38
+ * 3. Hash NOT in this block, no prior inclusion, but identity is
39
+ * cached AND a different hash with the same `(from, nonce)` is
40
+ * in this block → emit `replaced-by` with the replacement's
41
+ * block number.
42
+ * 4. Truly unseen → bump the unseen-block streak; emit
43
+ * `unseen-for-N-blocks` when the streak crosses the
44
+ * subscription's threshold. Does NOT emit on the first block
45
+ * after subscription (no `firstObservedAtBlock` yet).
46
+ */
47
+ export const decideBlockObservation = (input) => {
48
+ const { record, blockHash, blockNumber, txHashSet, txs, chainId, eventSource, envelope, previousTipNumber, } = input;
49
+ const wasSeenInThisBlock = txHashSet.has(record.hash);
50
+ if (wasSeenInThisBlock) {
51
+ const tx = txs.find((t) => t.hash === record.hash);
52
+ if (!tx) {
53
+ // Defensive: hash was in the set but the find missed. Should
54
+ // be unreachable since the set is built from the same tx list.
55
+ return EMPTY_RESULT;
56
+ }
57
+ const transactionIndex = txs.indexOf(tx);
58
+ const isFreshInclusion = record.status.lastSeenInBlock?.blockHash !== blockHash;
59
+ const confirmations = isFreshInclusion
60
+ ? 1
61
+ : record.status.lastSeenInBlock.confirmations;
62
+ const lastSeenInBlock = {
63
+ blockHash,
64
+ blockNumber,
65
+ transactionIndex,
66
+ confirmations,
67
+ source: eventSource,
68
+ };
69
+ const events = isFreshInclusion
70
+ ? [
71
+ buildSeenInBlock({
72
+ hash: record.hash,
73
+ chainId,
74
+ source: eventSource,
75
+ at: envelope,
76
+ blockHash,
77
+ blockNumber,
78
+ transactionIndex,
79
+ confirmations,
80
+ }),
81
+ ]
82
+ : [];
83
+ return {
84
+ events,
85
+ statusPatch: {
86
+ lastSeenInBlock,
87
+ unseenStreak: 0,
88
+ firstObservedAtBlock: record.status.firstObservedAtBlock ?? blockNumber,
89
+ lastObservedAtBlock: blockNumber,
90
+ },
91
+ identityPatch: cacheIdentity(record.identity, tx),
92
+ inMempoolPatch: null,
93
+ };
94
+ }
95
+ // Path 2: not in this block, but previously observed → confirmation bump
96
+ if (record.status.lastSeenInBlock && previousTipNumber !== null) {
97
+ const bumped = record.status.lastSeenInBlock.confirmations + 1;
98
+ const updated = {
99
+ ...record.status.lastSeenInBlock,
100
+ confirmations: bumped,
101
+ };
102
+ return {
103
+ events: [
104
+ buildSeenInBlock({
105
+ hash: record.hash,
106
+ chainId,
107
+ source: eventSource,
108
+ at: envelope,
109
+ blockHash: updated.blockHash,
110
+ blockNumber: updated.blockNumber,
111
+ transactionIndex: updated.transactionIndex,
112
+ confirmations: bumped,
113
+ }),
114
+ ],
115
+ statusPatch: {
116
+ lastSeenInBlock: updated,
117
+ lastObservedAtBlock: blockNumber,
118
+ },
119
+ identityPatch: null,
120
+ inMempoolPatch: null,
121
+ };
122
+ }
123
+ // Path 3: replacement detection
124
+ if (record.identity) {
125
+ const replacement = findReplacementInBlock(record.identity, record.hash, txs);
126
+ if (replacement && replacement.hash) {
127
+ return {
128
+ events: [
129
+ buildReplacedBy({
130
+ hash: record.hash,
131
+ chainId,
132
+ source: eventSource,
133
+ at: envelope,
134
+ replacementHash: replacement.hash,
135
+ replacementBlockNumber: blockNumber,
136
+ }),
137
+ ],
138
+ statusPatch: {
139
+ replacedBy: { hash: replacement.hash, blockNumber },
140
+ lastObservedAtBlock: blockNumber,
141
+ },
142
+ identityPatch: null,
143
+ inMempoolPatch: null,
144
+ };
145
+ }
146
+ }
147
+ // Path 4: truly unseen — only counts when there's a prior
148
+ // observation to count from.
149
+ if (record.status.firstObservedAtBlock === null) {
150
+ return EMPTY_RESULT;
151
+ }
152
+ const nextStreak = record.status.unseenStreak + 1;
153
+ const events = nextStreak === record.unseenThresholdBlocks
154
+ ? [
155
+ buildUnseenForNBlocks({
156
+ hash: record.hash,
157
+ chainId,
158
+ source: eventSource,
159
+ at: envelope,
160
+ blocks: nextStreak,
161
+ }),
162
+ ]
163
+ : [];
164
+ return {
165
+ events,
166
+ statusPatch: { unseenStreak: nextStreak },
167
+ identityPatch: null,
168
+ inMempoolPatch: null,
169
+ };
170
+ };
171
+ /**
172
+ * Per-record decision for one mempool snapshot. Three independent
173
+ * outputs that may all fire on the same call:
174
+ *
175
+ * - **Presence transition** — emit `seen-in-mempool` on first
176
+ * observation or bucket change; emit `left-mempool` when a
177
+ * previously-seen hash is absent from this snapshot.
178
+ * - **Replacement** — emit `replaced-by` (with `null` block) when
179
+ * the orchestrator's pre-computed `replacementInMempool` is set
180
+ * AND the record hasn't already recorded a replacement.
181
+ */
182
+ export const decideMempoolObservation = (input) => {
183
+ const { record, presence, replacementInMempool, chainId, eventSource, envelope, tipBlockNumber, } = input;
184
+ const events = [];
185
+ let statusPatch = {};
186
+ let identityPatch = null;
187
+ let inMempoolPatch = null;
188
+ if (presence) {
189
+ identityPatch = cacheIdentity(record.identity, presence.tx);
190
+ const isFreshOrBucketChange = !record.inLastMempoolSnapshot ||
191
+ record.status.lastSeenInMempool?.bucket !== presence.bucket;
192
+ statusPatch = {
193
+ lastSeenInMempool: {
194
+ bucket: presence.bucket,
195
+ tx: presence.tx,
196
+ at: envelope,
197
+ source: eventSource,
198
+ },
199
+ unseenStreak: 0,
200
+ firstObservedAtBlock: record.status.firstObservedAtBlock ?? tipBlockNumber,
201
+ lastObservedAtBlock: tipBlockNumber,
202
+ };
203
+ inMempoolPatch = true;
204
+ if (isFreshOrBucketChange) {
205
+ events.push(buildSeenInMempool({
206
+ hash: record.hash,
207
+ chainId,
208
+ source: eventSource,
209
+ at: envelope,
210
+ bucket: presence.bucket,
211
+ tx: presence.tx,
212
+ }));
213
+ }
214
+ }
215
+ else if (record.inLastMempoolSnapshot) {
216
+ inMempoolPatch = false;
217
+ events.push(buildLeftMempool({
218
+ hash: record.hash,
219
+ chainId,
220
+ source: eventSource,
221
+ at: envelope,
222
+ }));
223
+ }
224
+ // Replacement detection runs independently. The orchestrator skips
225
+ // it when the record already carries a replacement; we mirror that
226
+ // here so the function is self-contained.
227
+ if (record.identity &&
228
+ !record.status.replacedBy &&
229
+ replacementInMempool &&
230
+ replacementInMempool.hash &&
231
+ replacementInMempool.hash !== record.hash) {
232
+ statusPatch = {
233
+ ...statusPatch,
234
+ replacedBy: { hash: replacementInMempool.hash, blockNumber: null },
235
+ };
236
+ events.push(buildReplacedBy({
237
+ hash: record.hash,
238
+ chainId,
239
+ source: eventSource,
240
+ at: envelope,
241
+ replacementHash: replacementInMempool.hash,
242
+ replacementBlockNumber: null,
243
+ }));
244
+ }
245
+ return { events, statusPatch, identityPatch, inMempoolPatch };
246
+ };
247
+ // -------------------------------------------------------------
248
+ // Helpers (also pure)
249
+ // -------------------------------------------------------------
250
+ /**
251
+ * Find a tx in `txs` whose `(from, nonce)` matches `identity` but
252
+ * whose hash differs from `originalHash` — the replacement candidate
253
+ * for the original tracked tx. Compares senders case-insensitively
254
+ * since upstreams disagree on checksum form.
255
+ */
256
+ export const findReplacementInBlock = (identity, originalHash, txs) => {
257
+ const targetFrom = identity.from.toLowerCase();
258
+ for (const tx of txs) {
259
+ if (tx.from?.toLowerCase() !== targetFrom)
260
+ continue;
261
+ if (tx.nonce !== identity.nonce)
262
+ continue;
263
+ if (!tx.hash)
264
+ continue;
265
+ if (tx.hash === originalHash)
266
+ continue;
267
+ return tx;
268
+ }
269
+ return null;
270
+ };
271
+ /**
272
+ * Cache the tx's `(from, nonce)` as the record's identity if it's
273
+ * not already cached AND the tx carries both fields. Returns the
274
+ * patch to apply (or `null` when no change).
275
+ */
276
+ export const cacheIdentity = (current, tx) => {
277
+ if (current)
278
+ return null;
279
+ if (!tx.from || !tx.nonce)
280
+ return null;
281
+ return { from: tx.from, nonce: tx.nonce };
282
+ };
283
+ //# sourceMappingURL=observations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observations.js","sourceRoot":"","sources":["../src/observations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Reorg detector — pure function over a recent-block ring.
3
+ *
4
+ * Per `docs/tx-tracker-spec.md` §12. The tracker keeps a small ring
5
+ * of recently-observed canonical blocks (`BlockSample` below). On
6
+ * every new tip, it walks the ring backward up to `reorgDepthBlocks`
7
+ * (default 12) and asks: "is the canonical chain at any of those
8
+ * heights different from what we recorded?" Any divergence becomes
9
+ * a `vanished-from-block` candidate the tracker translates per
10
+ * affected hash.
11
+ *
12
+ * Bounded depth (§12.2): a fixed 12-block window keeps the per-tick
13
+ * work O(depth) regardless of how anomalous the upstream's history
14
+ * is. Deeper reorgs are vanishingly rare on chains the package
15
+ * targets; "find the common ancestor" would let one bad reorg make
16
+ * a tick arbitrarily long.
17
+ *
18
+ * Pure: no I/O, no clock, no mutation of input. The tracker (in
19
+ * `tracker.ts`) calls `appendBlock` to fold each newly-observed
20
+ * canonical block into the ring, and `detectDivergences` against
21
+ * a candidate canonical sequence (typically the source's most
22
+ * recent emit + the on-demand block fetches the tracker did to
23
+ * walk the chain backward).
24
+ */
25
+ import type { Hash } from './events.js';
26
+ /**
27
+ * One canonical-block coordinate the tracker has observed. The
28
+ * tracker also records the `transactions` it saw on that block
29
+ * (just the hashes, not the full RawTx) so it can quickly answer
30
+ * "was tx X in block N?" without a fresh RPC.
31
+ */
32
+ export interface BlockSample {
33
+ number: bigint;
34
+ hash: Hash;
35
+ parentHash: Hash | null;
36
+ /** Tx hashes observed in this block at the time it was canonical. */
37
+ transactionHashes: ReadonlySet<Hash>;
38
+ }
39
+ /**
40
+ * One divergence the detector found between the tracker's recorded
41
+ * ring and a freshly-observed canonical chain. The tracker
42
+ * translates this into per-hash `vanished-from-block` events for
43
+ * every tracked hash whose `seen-in-block` referenced the previous
44
+ * (now-stale) hash at this height.
45
+ */
46
+ export interface BlockDivergence {
47
+ blockNumber: bigint;
48
+ /** Hash the ring previously had at this height. */
49
+ previousBlockHash: Hash;
50
+ /**
51
+ * Hash the canonical chain has at this height now. The detector
52
+ * only emits divergences for heights where the caller passed an
53
+ * explicit canonical block, so this is always a real hash.
54
+ */
55
+ canonicalBlockHash: Hash;
56
+ /**
57
+ * Tx hashes the ring saw on the previously-canonical block at
58
+ * this height. Tracker uses this to scope which tracked hashes
59
+ * are affected.
60
+ */
61
+ vanishedTransactionHashes: ReadonlySet<Hash>;
62
+ }
63
+ /**
64
+ * Append a freshly-observed canonical block into a bounded ring
65
+ * keyed by block number, capped at `capacityBlocks` entries. Returns
66
+ * a new ring (input is not mutated). The newest entry overwrites
67
+ * any previous entry at the same number — a same-height reorg
68
+ * replaces the stale block with the new canonical one.
69
+ */
70
+ export declare const appendBlock: (ring: ReadonlyArray<BlockSample>, block: BlockSample, capacityBlocks: number) => BlockSample[];
71
+ /**
72
+ * Compare the tracker's `ring` (already-recorded canonical blocks)
73
+ * against a freshly-observed `canonical` sequence (the tracker's
74
+ * latest observation, plus any walk-back probes it performed). At
75
+ * each height present in BOTH sides, if the hashes disagree the
76
+ * detector returns a `BlockDivergence`.
77
+ *
78
+ * **Heights present only in `ring` are skipped** — the detector
79
+ * treats "no canonical entry at this height" as "no information,"
80
+ * not "vanished." A real same-height reorg requires the caller to
81
+ * explicitly pass the new canonical block at that height; gapping
82
+ * (e.g. caller skipped a height) is not a divergence signal.
83
+ *
84
+ * If you need to detect "ring's tip is no longer in canonical" you
85
+ * pass the canonical chain that explicitly covers that height; with
86
+ * a partial canonical sequence the detector stays conservative.
87
+ *
88
+ * `depthBlocks` caps how far back the comparison runs. The tracker
89
+ * rarely cares about divergences beyond `reorgDepthBlocks` because
90
+ * any tracked tx that deep would already be considered finalized by
91
+ * downstream consumer policy.
92
+ *
93
+ * Returns an empty array on a clean chain extension (no divergences).
94
+ */
95
+ export declare const detectDivergences: (input: {
96
+ ring: ReadonlyArray<BlockSample>;
97
+ canonical: ReadonlyArray<BlockSample>;
98
+ depthBlocks: number;
99
+ }) => BlockDivergence[];
100
+ /**
101
+ * Default reorg-detection depth in blocks. Conservative — even
102
+ * Ethereum's worst recent reorgs are under 7 blocks, and unbounded
103
+ * walks would let a single anomalous reorg make the tick arbitrarily
104
+ * long (§12.2). Tunable per-tracker via
105
+ * `CreateTxTrackerOptions.reorgDepthBlocks`.
106
+ */
107
+ export declare const defaultReorgDepthBlocks = 12;
108
+ //# sourceMappingURL=reorg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reorg.d.ts","sourceRoot":"","sources":["../src/reorg.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAEvC;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,IAAI,CAAA;IACV,UAAU,EAAE,IAAI,GAAG,IAAI,CAAA;IACvB,qEAAqE;IACrE,iBAAiB,EAAE,WAAW,CAAC,IAAI,CAAC,CAAA;CACrC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,mDAAmD;IACnD,iBAAiB,EAAE,IAAI,CAAA;IACvB;;;;OAIG;IACH,kBAAkB,EAAE,IAAI,CAAA;IACxB;;;;OAIG;IACH,yBAAyB,EAAE,WAAW,CAAC,IAAI,CAAC,CAAA;CAC7C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GACtB,MAAM,aAAa,CAAC,WAAW,CAAC,EAChC,OAAO,WAAW,EAClB,gBAAgB,MAAM,KACrB,WAAW,EAeb,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO;IACvC,IAAI,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IAChC,SAAS,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IACrC,WAAW,EAAE,MAAM,CAAA;CACpB,KAAG,eAAe,EA4ClB,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAA"}