@valve-tech/tx-tracker 0.6.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 +237 -0
- package/CHANGELOG.md +125 -0
- package/README.md +13 -6
- package/dist/events.d.ts +299 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +131 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +46 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -1
- package/dist/index.js.map +1 -1
- package/dist/observations.d.ts +159 -0
- package/dist/observations.d.ts.map +1 -0
- package/dist/observations.js +283 -0
- package/dist/observations.js.map +1 -0
- package/dist/reorg.d.ts +108 -0
- package/dist/reorg.d.ts.map +1 -0
- package/dist/reorg.js +125 -0
- package/dist/reorg.js.map +1 -0
- package/dist/selectors.d.ts +78 -0
- package/dist/selectors.d.ts.map +1 -0
- package/dist/selectors.js +119 -0
- package/dist/selectors.js.map +1 -0
- package/dist/store.d.ts +166 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +110 -0
- package/dist/store.js.map +1 -0
- package/dist/tracker.d.ts +165 -0
- package/dist/tracker.d.ts.map +1 -0
- package/dist/tracker.js +823 -0
- package/dist/tracker.js.map +1 -0
- package/package.json +6 -1
- package/skills/tx-tracker-integration/SKILL.md +168 -0
package/dist/reorg.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
/**
|
|
26
|
+
* Append a freshly-observed canonical block into a bounded ring
|
|
27
|
+
* keyed by block number, capped at `capacityBlocks` entries. Returns
|
|
28
|
+
* a new ring (input is not mutated). The newest entry overwrites
|
|
29
|
+
* any previous entry at the same number — a same-height reorg
|
|
30
|
+
* replaces the stale block with the new canonical one.
|
|
31
|
+
*/
|
|
32
|
+
export const appendBlock = (ring, block, capacityBlocks) => {
|
|
33
|
+
const next = ring.filter((b) => b.number !== block.number);
|
|
34
|
+
next.push(block);
|
|
35
|
+
// Sort ascending by block number. The ring is small (~depth + a
|
|
36
|
+
// few) so the cost is negligible. The same-number case is
|
|
37
|
+
// unreachable here — `filter` above strips any prior entry at
|
|
38
|
+
// `block.number` before we push, so the comparator never sees
|
|
39
|
+
// equal keys; we return -1 in that arm only to satisfy bigint
|
|
40
|
+
// sort's contract.
|
|
41
|
+
/* c8 ignore next */
|
|
42
|
+
next.sort((a, b) => (a.number < b.number ? -1 : 1));
|
|
43
|
+
if (next.length > capacityBlocks) {
|
|
44
|
+
next.splice(0, next.length - capacityBlocks);
|
|
45
|
+
}
|
|
46
|
+
return next;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Compare the tracker's `ring` (already-recorded canonical blocks)
|
|
50
|
+
* against a freshly-observed `canonical` sequence (the tracker's
|
|
51
|
+
* latest observation, plus any walk-back probes it performed). At
|
|
52
|
+
* each height present in BOTH sides, if the hashes disagree the
|
|
53
|
+
* detector returns a `BlockDivergence`.
|
|
54
|
+
*
|
|
55
|
+
* **Heights present only in `ring` are skipped** — the detector
|
|
56
|
+
* treats "no canonical entry at this height" as "no information,"
|
|
57
|
+
* not "vanished." A real same-height reorg requires the caller to
|
|
58
|
+
* explicitly pass the new canonical block at that height; gapping
|
|
59
|
+
* (e.g. caller skipped a height) is not a divergence signal.
|
|
60
|
+
*
|
|
61
|
+
* If you need to detect "ring's tip is no longer in canonical" you
|
|
62
|
+
* pass the canonical chain that explicitly covers that height; with
|
|
63
|
+
* a partial canonical sequence the detector stays conservative.
|
|
64
|
+
*
|
|
65
|
+
* `depthBlocks` caps how far back the comparison runs. The tracker
|
|
66
|
+
* rarely cares about divergences beyond `reorgDepthBlocks` because
|
|
67
|
+
* any tracked tx that deep would already be considered finalized by
|
|
68
|
+
* downstream consumer policy.
|
|
69
|
+
*
|
|
70
|
+
* Returns an empty array on a clean chain extension (no divergences).
|
|
71
|
+
*/
|
|
72
|
+
export const detectDivergences = (input) => {
|
|
73
|
+
const { ring, canonical, depthBlocks } = input;
|
|
74
|
+
if (ring.length === 0 || canonical.length === 0)
|
|
75
|
+
return [];
|
|
76
|
+
// Index the canonical sequence by number for O(1) lookup. The
|
|
77
|
+
// canonical sequence is small (≤ depthBlocks), so map construction
|
|
78
|
+
// is cheap.
|
|
79
|
+
const canonicalByNumber = new Map();
|
|
80
|
+
for (const block of canonical) {
|
|
81
|
+
canonicalByNumber.set(block.number, block);
|
|
82
|
+
}
|
|
83
|
+
// The "tip" is the highest-numbered block in either side. Compare
|
|
84
|
+
// back from there for `depthBlocks` heights (inclusive of the tip).
|
|
85
|
+
// The early `length === 0` guard above means `ring[length-1]` and
|
|
86
|
+
// `canonical[length-1]` are always defined here — the `!` reflects
|
|
87
|
+
// that invariant rather than papering over a real nullable.
|
|
88
|
+
const ringTip = ring[ring.length - 1].number;
|
|
89
|
+
const canonicalTip = canonical[canonical.length - 1].number;
|
|
90
|
+
const tip = ringTip > canonicalTip ? ringTip : canonicalTip;
|
|
91
|
+
const lowestComparedNumber = tip - BigInt(depthBlocks - 1);
|
|
92
|
+
const divergences = [];
|
|
93
|
+
for (const sampled of ring) {
|
|
94
|
+
if (sampled.number < lowestComparedNumber)
|
|
95
|
+
continue;
|
|
96
|
+
const canonicalAtHeight = canonicalByNumber.get(sampled.number) ?? null;
|
|
97
|
+
if (!canonicalAtHeight)
|
|
98
|
+
continue; // no canonical info at this height
|
|
99
|
+
if (canonicalAtHeight.hash === sampled.hash)
|
|
100
|
+
continue; // unchanged
|
|
101
|
+
divergences.push({
|
|
102
|
+
blockNumber: sampled.number,
|
|
103
|
+
previousBlockHash: sampled.hash,
|
|
104
|
+
canonicalBlockHash: canonicalAtHeight.hash,
|
|
105
|
+
vanishedTransactionHashes: sampled.transactionHashes,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Sort ascending by number so the tracker emits vanished events
|
|
109
|
+
// in chain order — easier on consumers piping to a single sink.
|
|
110
|
+
// Divergences are unique by `blockNumber` (one entry per ring
|
|
111
|
+
// height), so the comparator never sees equal keys; we return -1
|
|
112
|
+
// in that arm only to satisfy the sort contract.
|
|
113
|
+
/* c8 ignore next */
|
|
114
|
+
divergences.sort((a, b) => (a.blockNumber < b.blockNumber ? -1 : 1));
|
|
115
|
+
return divergences;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Default reorg-detection depth in blocks. Conservative — even
|
|
119
|
+
* Ethereum's worst recent reorgs are under 7 blocks, and unbounded
|
|
120
|
+
* walks would let a single anomalous reorg make the tick arbitrarily
|
|
121
|
+
* long (§12.2). Tunable per-tracker via
|
|
122
|
+
* `CreateTxTrackerOptions.reorgDepthBlocks`.
|
|
123
|
+
*/
|
|
124
|
+
export const defaultReorgDepthBlocks = 12;
|
|
125
|
+
//# sourceMappingURL=reorg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reorg.js","sourceRoot":"","sources":["../src/reorg.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk-subscription matchers — pure functions over `RawTx`.
|
|
3
|
+
*
|
|
4
|
+
* Per `docs/tx-tracker-spec.md` §11. Indexers want "all txs from
|
|
5
|
+
* these senders" or "all txs touching this contract"; the tracker
|
|
6
|
+
* supports `from` / `to` / `predicate` selectors. The matchers
|
|
7
|
+
* themselves are pure (no I/O, no cached state) — `tracker.ts`
|
|
8
|
+
* iterates the source's mempool snapshots and the canonical block's
|
|
9
|
+
* `transactions` array per tick and asks each registered selector
|
|
10
|
+
* whether each tx matches.
|
|
11
|
+
*
|
|
12
|
+
* `from` and `to` selectors normalize addresses to lowercase ASCII
|
|
13
|
+
* before comparison because upstream RPCs are inconsistent about
|
|
14
|
+
* checksum vs lowercase. Mempool snapshots are already lowercased
|
|
15
|
+
* by `chain-source`'s `normalizeMempool`, but the same matcher is
|
|
16
|
+
* called against block-tx records (where the upstream may emit
|
|
17
|
+
* checksum form), so the selector normalizes its own
|
|
18
|
+
* pre-stored target once and compares lowercase-vs-lowercase.
|
|
19
|
+
*
|
|
20
|
+
* Predicate selectors are caller-defined. The tracker calls them
|
|
21
|
+
* O(N) per tick per matched-tx; spec §11.3 documents that slow
|
|
22
|
+
* predicates degrade the tick.
|
|
23
|
+
*/
|
|
24
|
+
import type { RawTx } from '@valve-tech/chain-source';
|
|
25
|
+
import type { Hash } from './events.js';
|
|
26
|
+
import type { BulkSelector } from './store.js';
|
|
27
|
+
/**
|
|
28
|
+
* One match the bulk subscription is about to emit. The tracker
|
|
29
|
+
* builds the surrounding `TxMatchEvent` envelope (`source`,
|
|
30
|
+
* `at`, etc.) before pushing to the consumer; this shape is just
|
|
31
|
+
* the matcher's payload.
|
|
32
|
+
*/
|
|
33
|
+
export interface BulkMatchPayload {
|
|
34
|
+
hash: Hash;
|
|
35
|
+
matchedBy: 'from' | 'to' | 'predicate';
|
|
36
|
+
selector: BulkSelector;
|
|
37
|
+
tx: RawTx;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One compiled selector — `selector` is the original consumer-facing
|
|
41
|
+
* shape, `match` is the cached pure function the tracker calls per
|
|
42
|
+
* tx. The tracker compiles each registered selector once at
|
|
43
|
+
* registration time so the per-tick fanout pays only the function
|
|
44
|
+
* call, not a dispatch on `selector.kind`.
|
|
45
|
+
*/
|
|
46
|
+
export interface CompiledSelector {
|
|
47
|
+
selector: BulkSelector;
|
|
48
|
+
match: (tx: RawTx) => boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Compile a `BulkSelector` into its pure matcher. `from` / `to`
|
|
52
|
+
* cache the lowercased address once; `predicate` returns the
|
|
53
|
+
* caller's function as-is.
|
|
54
|
+
*
|
|
55
|
+
* Throws on a malformed selector (`from` / `to` without an address,
|
|
56
|
+
* `predicate` without a function) — caught at registration time
|
|
57
|
+
* rather than at the per-tick callsite, where the failure mode would
|
|
58
|
+
* be silent zero matches.
|
|
59
|
+
*/
|
|
60
|
+
export declare const compileSelector: (selector: BulkSelector) => CompiledSelector;
|
|
61
|
+
/**
|
|
62
|
+
* Iterate `txs` against every compiled selector and yield one
|
|
63
|
+
* `BulkMatchPayload` per (tx, selector) match. Used by the tracker's
|
|
64
|
+
* per-tick fan-out over both `block.transactions` (after a new tip
|
|
65
|
+
* lands) and the source's mempool snapshot deltas.
|
|
66
|
+
*
|
|
67
|
+
* The tracker, not this helper, decides whether to auto-track each
|
|
68
|
+
* matched hash — `BulkTrackOptions.autoTrackMatched` (default true)
|
|
69
|
+
* lives in `tracker.ts`.
|
|
70
|
+
*/
|
|
71
|
+
export declare const matchAll: (txs: ReadonlyArray<RawTx>, compiled: ReadonlyArray<CompiledSelector>) => BulkMatchPayload[];
|
|
72
|
+
/**
|
|
73
|
+
* Default per-tracker bulk-subscription cap (§11.3). Higher fan-out
|
|
74
|
+
* is technically allowed but indicates the consumer should be
|
|
75
|
+
* running an indexer-shaped store rather than the in-memory default.
|
|
76
|
+
*/
|
|
77
|
+
export declare const defaultMaxBulkSubscriptions = 16;
|
|
78
|
+
//# sourceMappingURL=selectors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectors.d.ts","sourceRoot":"","sources":["../src/selectors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAErD,OAAO,KAAK,EAAW,IAAI,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,IAAI,CAAA;IACV,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,CAAA;IACtC,QAAQ,EAAE,YAAY,CAAA;IACtB,EAAE,EAAE,KAAK,CAAA;CACV;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,YAAY,CAAA;IACtB,KAAK,EAAE,CAAC,EAAE,EAAE,KAAK,KAAK,OAAO,CAAA;CAC9B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,YAAY,KAAG,gBAwCxD,CAAA;AAeD;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GACnB,KAAK,aAAa,CAAC,KAAK,CAAC,EACzB,UAAU,aAAa,CAAC,gBAAgB,CAAC,KACxC,gBAAgB,EAelB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAA"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk-subscription matchers — pure functions over `RawTx`.
|
|
3
|
+
*
|
|
4
|
+
* Per `docs/tx-tracker-spec.md` §11. Indexers want "all txs from
|
|
5
|
+
* these senders" or "all txs touching this contract"; the tracker
|
|
6
|
+
* supports `from` / `to` / `predicate` selectors. The matchers
|
|
7
|
+
* themselves are pure (no I/O, no cached state) — `tracker.ts`
|
|
8
|
+
* iterates the source's mempool snapshots and the canonical block's
|
|
9
|
+
* `transactions` array per tick and asks each registered selector
|
|
10
|
+
* whether each tx matches.
|
|
11
|
+
*
|
|
12
|
+
* `from` and `to` selectors normalize addresses to lowercase ASCII
|
|
13
|
+
* before comparison because upstream RPCs are inconsistent about
|
|
14
|
+
* checksum vs lowercase. Mempool snapshots are already lowercased
|
|
15
|
+
* by `chain-source`'s `normalizeMempool`, but the same matcher is
|
|
16
|
+
* called against block-tx records (where the upstream may emit
|
|
17
|
+
* checksum form), so the selector normalizes its own
|
|
18
|
+
* pre-stored target once and compares lowercase-vs-lowercase.
|
|
19
|
+
*
|
|
20
|
+
* Predicate selectors are caller-defined. The tracker calls them
|
|
21
|
+
* O(N) per tick per matched-tx; spec §11.3 documents that slow
|
|
22
|
+
* predicates degrade the tick.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Compile a `BulkSelector` into its pure matcher. `from` / `to`
|
|
26
|
+
* cache the lowercased address once; `predicate` returns the
|
|
27
|
+
* caller's function as-is.
|
|
28
|
+
*
|
|
29
|
+
* Throws on a malformed selector (`from` / `to` without an address,
|
|
30
|
+
* `predicate` without a function) — caught at registration time
|
|
31
|
+
* rather than at the per-tick callsite, where the failure mode would
|
|
32
|
+
* be silent zero matches.
|
|
33
|
+
*/
|
|
34
|
+
export const compileSelector = (selector) => {
|
|
35
|
+
switch (selector.kind) {
|
|
36
|
+
case 'from': {
|
|
37
|
+
if (!selector.address) {
|
|
38
|
+
throw new Error('compileSelector: "from" selector requires an address');
|
|
39
|
+
}
|
|
40
|
+
const target = selector.address.toLowerCase();
|
|
41
|
+
return {
|
|
42
|
+
selector,
|
|
43
|
+
match: (tx) => (tx.from ? tx.from.toLowerCase() === target : false),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
case 'to': {
|
|
47
|
+
if (!selector.address) {
|
|
48
|
+
throw new Error('compileSelector: "to" selector requires an address');
|
|
49
|
+
}
|
|
50
|
+
const target = selector.address.toLowerCase();
|
|
51
|
+
return {
|
|
52
|
+
selector,
|
|
53
|
+
match: (tx) =>
|
|
54
|
+
// RawTx in chain-source's wire shape doesn't carry `to`
|
|
55
|
+
// explicitly (it's loose for portability across
|
|
56
|
+
// mempool/block payloads); the matcher reads the field
|
|
57
|
+
// off the structurally-typed object so block-side txs
|
|
58
|
+
// (which DO carry `to`) match correctly. Mempool snapshots
|
|
59
|
+
// also carry `to`; both are read off the same field name
|
|
60
|
+
// upstream clients use.
|
|
61
|
+
extractTo(tx) ? extractTo(tx)?.toLowerCase() === target : false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
case 'predicate': {
|
|
65
|
+
if (typeof selector.match !== 'function') {
|
|
66
|
+
throw new Error('compileSelector: "predicate" selector requires a match function');
|
|
67
|
+
}
|
|
68
|
+
const fn = selector.match;
|
|
69
|
+
return { selector, match: (tx) => fn(tx) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Read `tx.to` off a RawTx without losing type-safety. The
|
|
75
|
+
* `chain-source` `RawTx` is intentionally loose (no `to` field) —
|
|
76
|
+
* mempool / block payloads carry `to` even though it's not in the
|
|
77
|
+
* narrow type, and `to`-selectors must compare against it. Safe
|
|
78
|
+
* cast through `Record<string, unknown>` keeps lint clean and
|
|
79
|
+
* avoids `any`.
|
|
80
|
+
*/
|
|
81
|
+
const extractTo = (tx) => {
|
|
82
|
+
const value = tx.to;
|
|
83
|
+
return typeof value === 'string' ? value : undefined;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Iterate `txs` against every compiled selector and yield one
|
|
87
|
+
* `BulkMatchPayload` per (tx, selector) match. Used by the tracker's
|
|
88
|
+
* per-tick fan-out over both `block.transactions` (after a new tip
|
|
89
|
+
* lands) and the source's mempool snapshot deltas.
|
|
90
|
+
*
|
|
91
|
+
* The tracker, not this helper, decides whether to auto-track each
|
|
92
|
+
* matched hash — `BulkTrackOptions.autoTrackMatched` (default true)
|
|
93
|
+
* lives in `tracker.ts`.
|
|
94
|
+
*/
|
|
95
|
+
export const matchAll = (txs, compiled) => {
|
|
96
|
+
const matches = [];
|
|
97
|
+
for (const tx of txs) {
|
|
98
|
+
if (!tx.hash)
|
|
99
|
+
continue; // can't bulk-track an unhashed tx
|
|
100
|
+
for (const cs of compiled) {
|
|
101
|
+
if (!cs.match(tx))
|
|
102
|
+
continue;
|
|
103
|
+
matches.push({
|
|
104
|
+
hash: tx.hash,
|
|
105
|
+
matchedBy: cs.selector.kind,
|
|
106
|
+
selector: cs.selector,
|
|
107
|
+
tx,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return matches;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Default per-tracker bulk-subscription cap (§11.3). Higher fan-out
|
|
115
|
+
* is technically allowed but indicates the consumer should be
|
|
116
|
+
* running an indexer-shaped store rather than the in-memory default.
|
|
117
|
+
*/
|
|
118
|
+
export const defaultMaxBulkSubscriptions = 16;
|
|
119
|
+
//# sourceMappingURL=selectors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectors.js","sourceRoot":"","sources":["../src/selectors.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TxTrackerStore` — persistence surface for the tracker, plus the
|
|
3
|
+
* default `createInMemoryStore` implementation.
|
|
4
|
+
*
|
|
5
|
+
* Per `docs/tx-tracker-spec.md` §9 + §10. Indexers and relays cannot
|
|
6
|
+
* lose tracked hashes across a process restart; wallets are fine
|
|
7
|
+
* in-memory. The store interface lets either case plug in.
|
|
8
|
+
*
|
|
9
|
+
* Two responsibilities:
|
|
10
|
+
*
|
|
11
|
+
* - **Record store** — `put` / `get` / `delete` / `listDurable`
|
|
12
|
+
* for `TrackedTxRecord` (one per `(chainId, hash)`).
|
|
13
|
+
* - **Per-hash audit log** — `appendEvent` / `readEventLog?` over
|
|
14
|
+
* `TxEvent[]`. Default implementation is a bounded ring keyed
|
|
15
|
+
* by `(chainId, hash)`.
|
|
16
|
+
*
|
|
17
|
+
* The interface is **async-shaped** so durable stores (Redis, SQLite,
|
|
18
|
+
* JSON-on-disk) can implement it directly. The in-memory default
|
|
19
|
+
* resolves synchronously under the hood — `Promise.resolve` wrappers
|
|
20
|
+
* are cheap.
|
|
21
|
+
*
|
|
22
|
+
* **Wire format** (§9.1, §2.5): `TrackedTxRecord` carries `bigint`
|
|
23
|
+
* fields (`firstSeenBlockNumber`, `lastObservedBlockNumber`,
|
|
24
|
+
* `retentionExpiresAtBlockNumber`) and `TxStatus` carries `bigint`
|
|
25
|
+
* inside its nested `at` / `lastSeenIn*` shapes. The in-memory store
|
|
26
|
+
* keeps them as `bigint` end-to-end. Durable store implementers MUST
|
|
27
|
+
* hex-encode (`'0x' + n.toString(16)`) on write and decode on read;
|
|
28
|
+
* the package never calls `JSON.stringify` on a record itself.
|
|
29
|
+
*/
|
|
30
|
+
import type { TxEvent, TxStatus, Hash, Address } from './events.js';
|
|
31
|
+
/**
|
|
32
|
+
* One persisted record per tracked `(chainId, hash)`. `subscriptions`
|
|
33
|
+
* carries every persisted (durable) subscription that referenced this
|
|
34
|
+
* hash so `listDurable` can rehydrate them after restart.
|
|
35
|
+
*/
|
|
36
|
+
export interface TrackedTxRecord {
|
|
37
|
+
chainId: number;
|
|
38
|
+
hash: Hash;
|
|
39
|
+
/** Cached current status, kept in sync with every emit. */
|
|
40
|
+
status: TxStatus;
|
|
41
|
+
/** Block number of the very first observation. */
|
|
42
|
+
firstSeenBlockNumber: bigint;
|
|
43
|
+
/** Block number of the most recent observation (any kind). */
|
|
44
|
+
lastObservedBlockNumber: bigint;
|
|
45
|
+
/**
|
|
46
|
+
* Block number after which the record is GC-eligible. Set when the
|
|
47
|
+
* record reaches a terminal observation (`seen-in-block` reaching
|
|
48
|
+
* the consumer's confirmation threshold, `unseen-for-N-blocks`,
|
|
49
|
+
* `replaced-by`, etc.) plus `retentionBlocks`.
|
|
50
|
+
*/
|
|
51
|
+
retentionExpiresAtBlockNumber: bigint;
|
|
52
|
+
/** Persisted subscriptions referencing this hash. */
|
|
53
|
+
subscriptions: PersistedSubscription[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Per-hash and per-bulk subscription metadata. Predicate selectors
|
|
57
|
+
* are non-serializable (closures cannot survive a process boundary)
|
|
58
|
+
* — the tracker silently demotes them to non-durable and surfaces a
|
|
59
|
+
* warning at registration. Per spec §13.2 only `'from'` / `'to'`
|
|
60
|
+
* bulk selectors and per-hash selectors persist meaningfully.
|
|
61
|
+
*/
|
|
62
|
+
export interface PersistedSubscription {
|
|
63
|
+
/** Stable identifier. The tracker assigns this on registration. */
|
|
64
|
+
id: string;
|
|
65
|
+
/** Whether the consumer asked for durable persistence. */
|
|
66
|
+
durable: boolean;
|
|
67
|
+
selector: HashSelector | BulkSelector;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Per-hash selector — one tracked hash, ergonomic for wallet UIs.
|
|
71
|
+
*/
|
|
72
|
+
export interface HashSelector {
|
|
73
|
+
kind: 'hash';
|
|
74
|
+
hash: Hash;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Bulk selector — `'from' | 'to' | 'predicate'`. `predicate`
|
|
78
|
+
* carries a non-serializable function reference; durable persistence
|
|
79
|
+
* records `kind: 'predicate'` without the function and the tracker
|
|
80
|
+
* cannot rehydrate it on restart.
|
|
81
|
+
*/
|
|
82
|
+
export interface BulkSelector {
|
|
83
|
+
kind: 'from' | 'to' | 'predicate';
|
|
84
|
+
/** For 'from' / 'to'. */
|
|
85
|
+
address?: Address;
|
|
86
|
+
/** For 'predicate'. Function references do not persist meaningfully. */
|
|
87
|
+
match?: (tx: import('@valve-tech/chain-source').RawTx) => boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Persistence surface. Indexers and relays cannot lose tracked hashes
|
|
91
|
+
* across restart; wallets are fine in-memory. `readEventLog` is
|
|
92
|
+
* optional — durable stores that don't keep a log can omit it.
|
|
93
|
+
*/
|
|
94
|
+
export interface TxTrackerStore {
|
|
95
|
+
/**
|
|
96
|
+
* Persist (or update) a tracked-tx record. Idempotent on
|
|
97
|
+
* `(chainId, hash)`.
|
|
98
|
+
*/
|
|
99
|
+
put(record: TrackedTxRecord): Promise<void>;
|
|
100
|
+
/** Read the latest record for a hash. Returns null if absent. */
|
|
101
|
+
get(chainId: number, hash: Hash): Promise<TrackedTxRecord | null>;
|
|
102
|
+
/** Remove a hash. Called when the retention window expires. */
|
|
103
|
+
delete(chainId: number, hash: Hash): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* List records that carry at least one durable subscription. Called
|
|
106
|
+
* once at `tracker.start()` so durable per-hash subscriptions can
|
|
107
|
+
* be re-registered against the source after a restart.
|
|
108
|
+
*/
|
|
109
|
+
listDurable(chainId: number): Promise<TrackedTxRecord[]>;
|
|
110
|
+
/**
|
|
111
|
+
* Append an event to the per-hash audit log. Indexers replay this
|
|
112
|
+
* log on restart; wallets can wrap a no-op store implementation.
|
|
113
|
+
* Failures here are routed through the tracker's `onError` and
|
|
114
|
+
* never block live emit (per spec Appendix A).
|
|
115
|
+
*/
|
|
116
|
+
appendEvent(chainId: number, hash: Hash, event: TxEvent): Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* Read the per-hash audit log, optionally constrained to events
|
|
119
|
+
* with `at.blockNumber >= since`. Optional — implementations
|
|
120
|
+
* without a log return `undefined` on the type and get pruned
|
|
121
|
+
* from the catch-up code path.
|
|
122
|
+
*/
|
|
123
|
+
readEventLog?(chainId: number, hash: Hash, since?: bigint): Promise<TxEvent[]>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Tunable knobs for the default in-memory store.
|
|
127
|
+
*
|
|
128
|
+
* - `retentionBlocks` (default 64): how many blocks past the last
|
|
129
|
+
* terminal observation a record stays in the store. After this
|
|
130
|
+
* window passes, the record is GC'd. Block-units, not seconds —
|
|
131
|
+
* reorg safety is a block-depth invariant, not a time invariant
|
|
132
|
+
* (spec §10.1).
|
|
133
|
+
*
|
|
134
|
+
* - `eventLogCapacity` (default 256): bounded ring buffer cap on
|
|
135
|
+
* the per-hash audit log. Older events are dropped when the cap
|
|
136
|
+
* is exceeded; the latest status is always retained.
|
|
137
|
+
*/
|
|
138
|
+
export interface InMemoryStoreOptions {
|
|
139
|
+
retentionBlocks?: number;
|
|
140
|
+
eventLogCapacity?: number;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Default in-memory store. Synchronous internals wrapped in
|
|
144
|
+
* `Promise.resolve`; safe for any consumer that doesn't need
|
|
145
|
+
* cross-process durability.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* import { createInMemoryStore } from '@valve-tech/tx-tracker'
|
|
149
|
+
*
|
|
150
|
+
* const store = createInMemoryStore({ retentionBlocks: 32 })
|
|
151
|
+
* const tracker = createTxTracker({ source, chainId: 1, store })
|
|
152
|
+
*/
|
|
153
|
+
export declare const createInMemoryStore: (options?: InMemoryStoreOptions) => TxTrackerStore;
|
|
154
|
+
/**
|
|
155
|
+
* Compute the retention-expiry block for a terminal observation.
|
|
156
|
+
* Pure helper exposed so the tracker (which decides when to call
|
|
157
|
+
* `store.put` with an updated `retentionExpiresAtBlockNumber`) and
|
|
158
|
+
* the store implementations stay in sync on the calculation.
|
|
159
|
+
*/
|
|
160
|
+
export declare const computeRetentionExpiry: (terminalBlockNumber: bigint, retentionBlocks?: number) => bigint;
|
|
161
|
+
/**
|
|
162
|
+
* Default retention window in blocks — exposed so the tracker can
|
|
163
|
+
* use the same default if no store-level override is present.
|
|
164
|
+
*/
|
|
165
|
+
export declare const defaultRetentionBlocks = 64;
|
|
166
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAEnE;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,IAAI,CAAA;IACV,2DAA2D;IAC3D,MAAM,EAAE,QAAQ,CAAA;IAChB,kDAAkD;IAClD,oBAAoB,EAAE,MAAM,CAAA;IAC5B,8DAA8D;IAC9D,uBAAuB,EAAE,MAAM,CAAA;IAC/B;;;;;OAKG;IACH,6BAA6B,EAAE,MAAM,CAAA;IACrC,qDAAqD;IACrD,aAAa,EAAE,qBAAqB,EAAE,CAAA;CACvC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,mEAAmE;IACnE,EAAE,EAAE,MAAM,CAAA;IACV,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,YAAY,GAAG,YAAY,CAAA;CACtC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,IAAI,CAAA;CACX;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,CAAA;IACjC,yBAAyB;IACzB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,wEAAwE;IACxE,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,OAAO,0BAA0B,EAAE,KAAK,KAAK,OAAO,CAAA;CAClE;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,GAAG,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE3C,iEAAiE;IACjE,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAA;IAEjE,+DAA+D;IAC/D,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAElD;;;;OAIG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAA;IAExD;;;;;OAKG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEvE;;;;;OAKG;IACH,YAAY,CAAC,CACX,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,IAAI,EACV,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,oBAAoB;IACnC,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAcD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,GAC9B,UAAS,oBAAyB,KACjC,cAsDF,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,qBAAqB,MAAM,EAC3B,kBAAiB,MAAiC,KACjD,MAAuD,CAAA;AAE1D;;;GAGG;AACH,eAAO,MAAM,sBAAsB,KAA2B,CAAA"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TxTrackerStore` — persistence surface for the tracker, plus the
|
|
3
|
+
* default `createInMemoryStore` implementation.
|
|
4
|
+
*
|
|
5
|
+
* Per `docs/tx-tracker-spec.md` §9 + §10. Indexers and relays cannot
|
|
6
|
+
* lose tracked hashes across a process restart; wallets are fine
|
|
7
|
+
* in-memory. The store interface lets either case plug in.
|
|
8
|
+
*
|
|
9
|
+
* Two responsibilities:
|
|
10
|
+
*
|
|
11
|
+
* - **Record store** — `put` / `get` / `delete` / `listDurable`
|
|
12
|
+
* for `TrackedTxRecord` (one per `(chainId, hash)`).
|
|
13
|
+
* - **Per-hash audit log** — `appendEvent` / `readEventLog?` over
|
|
14
|
+
* `TxEvent[]`. Default implementation is a bounded ring keyed
|
|
15
|
+
* by `(chainId, hash)`.
|
|
16
|
+
*
|
|
17
|
+
* The interface is **async-shaped** so durable stores (Redis, SQLite,
|
|
18
|
+
* JSON-on-disk) can implement it directly. The in-memory default
|
|
19
|
+
* resolves synchronously under the hood — `Promise.resolve` wrappers
|
|
20
|
+
* are cheap.
|
|
21
|
+
*
|
|
22
|
+
* **Wire format** (§9.1, §2.5): `TrackedTxRecord` carries `bigint`
|
|
23
|
+
* fields (`firstSeenBlockNumber`, `lastObservedBlockNumber`,
|
|
24
|
+
* `retentionExpiresAtBlockNumber`) and `TxStatus` carries `bigint`
|
|
25
|
+
* inside its nested `at` / `lastSeenIn*` shapes. The in-memory store
|
|
26
|
+
* keeps them as `bigint` end-to-end. Durable store implementers MUST
|
|
27
|
+
* hex-encode (`'0x' + n.toString(16)`) on write and decode on read;
|
|
28
|
+
* the package never calls `JSON.stringify` on a record itself.
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_RETENTION_BLOCKS = 64;
|
|
31
|
+
const DEFAULT_EVENT_LOG_CAPACITY = 256;
|
|
32
|
+
/**
|
|
33
|
+
* Composite key for the in-memory map. Keying by `(chainId, hash)`
|
|
34
|
+
* lets a single store back multi-chain trackers if a future caller
|
|
35
|
+
* wants that — today the tracker is single-chain, but the store
|
|
36
|
+
* type is already chain-aware so no migration is needed later.
|
|
37
|
+
*/
|
|
38
|
+
const recordKey = (chainId, hash) => `${chainId}:${hash}`;
|
|
39
|
+
/**
|
|
40
|
+
* Default in-memory store. Synchronous internals wrapped in
|
|
41
|
+
* `Promise.resolve`; safe for any consumer that doesn't need
|
|
42
|
+
* cross-process durability.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* import { createInMemoryStore } from '@valve-tech/tx-tracker'
|
|
46
|
+
*
|
|
47
|
+
* const store = createInMemoryStore({ retentionBlocks: 32 })
|
|
48
|
+
* const tracker = createTxTracker({ source, chainId: 1, store })
|
|
49
|
+
*/
|
|
50
|
+
export const createInMemoryStore = (options = {}) => {
|
|
51
|
+
const eventLogCapacity = options.eventLogCapacity ?? DEFAULT_EVENT_LOG_CAPACITY;
|
|
52
|
+
const records = new Map();
|
|
53
|
+
const eventLogs = new Map();
|
|
54
|
+
return {
|
|
55
|
+
put: (record) => {
|
|
56
|
+
records.set(recordKey(record.chainId, record.hash), record);
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
},
|
|
59
|
+
get: (chainId, hash) => Promise.resolve(records.get(recordKey(chainId, hash)) ?? null),
|
|
60
|
+
delete: (chainId, hash) => {
|
|
61
|
+
const key = recordKey(chainId, hash);
|
|
62
|
+
records.delete(key);
|
|
63
|
+
eventLogs.delete(key);
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
},
|
|
66
|
+
listDurable: (chainId) => {
|
|
67
|
+
const result = [];
|
|
68
|
+
for (const record of records.values()) {
|
|
69
|
+
if (record.chainId !== chainId)
|
|
70
|
+
continue;
|
|
71
|
+
if (record.subscriptions.some((sub) => sub.durable)) {
|
|
72
|
+
result.push(record);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return Promise.resolve(result);
|
|
76
|
+
},
|
|
77
|
+
appendEvent: (chainId, hash, event) => {
|
|
78
|
+
const key = recordKey(chainId, hash);
|
|
79
|
+
const log = eventLogs.get(key) ?? [];
|
|
80
|
+
log.push(event);
|
|
81
|
+
// Drop oldest when capacity exceeded. Latest entries are the
|
|
82
|
+
// ones consumers care about for catch-up; keeping a strict
|
|
83
|
+
// ring rather than unbounded growth caps memory.
|
|
84
|
+
if (log.length > eventLogCapacity) {
|
|
85
|
+
log.splice(0, log.length - eventLogCapacity);
|
|
86
|
+
}
|
|
87
|
+
eventLogs.set(key, log);
|
|
88
|
+
return Promise.resolve();
|
|
89
|
+
},
|
|
90
|
+
readEventLog: (chainId, hash, since) => {
|
|
91
|
+
const log = eventLogs.get(recordKey(chainId, hash)) ?? [];
|
|
92
|
+
if (since === undefined)
|
|
93
|
+
return Promise.resolve([...log]);
|
|
94
|
+
return Promise.resolve(log.filter((e) => e.at.blockNumber >= since));
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Compute the retention-expiry block for a terminal observation.
|
|
100
|
+
* Pure helper exposed so the tracker (which decides when to call
|
|
101
|
+
* `store.put` with an updated `retentionExpiresAtBlockNumber`) and
|
|
102
|
+
* the store implementations stay in sync on the calculation.
|
|
103
|
+
*/
|
|
104
|
+
export const computeRetentionExpiry = (terminalBlockNumber, retentionBlocks = DEFAULT_RETENTION_BLOCKS) => terminalBlockNumber + BigInt(retentionBlocks);
|
|
105
|
+
/**
|
|
106
|
+
* Default retention window in blocks — exposed so the tracker can
|
|
107
|
+
* use the same default if no store-level override is present.
|
|
108
|
+
*/
|
|
109
|
+
export const defaultRetentionBlocks = DEFAULT_RETENTION_BLOCKS;
|
|
110
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|