@valve-tech/tx-tracker 0.6.0 → 0.8.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 +140 -0
- package/README.md +13 -6
- package/dist/events.d.ts +309 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +132 -0
- package/dist/events.js.map +1 -0
- package/dist/group-events.d.ts +82 -0
- package/dist/group-events.d.ts.map +1 -0
- package/dist/group-events.js +47 -0
- package/dist/group-events.js.map +1 -0
- package/dist/group.d.ts +31 -0
- package/dist/group.d.ts.map +1 -0
- package/dist/group.js +196 -0
- package/dist/group.js.map +1 -0
- package/dist/index.d.ts +57 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +52 -1
- package/dist/index.js.map +1 -1
- package/dist/observations.d.ts +169 -0
- package/dist/observations.d.ts.map +1 -0
- package/dist/observations.js +287 -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/replace-transaction.d.ts +46 -0
- package/dist/replace-transaction.d.ts.map +1 -0
- package/dist/replace-transaction.js +47 -0
- package/dist/replace-transaction.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 +211 -0
- package/dist/tracker.d.ts.map +1 -0
- package/dist/tracker.js +1004 -0
- package/dist/tracker.js.map +1 -0
- package/dist/wait-for-pending.d.ts +41 -0
- package/dist/wait-for-pending.d.ts.map +1 -0
- package/dist/wait-for-pending.js +71 -0
- package/dist/wait-for-pending.js.map +1 -0
- package/dist/wait-for-transaction.d.ts +55 -0
- package/dist/wait-for-transaction.d.ts.map +1 -0
- package/dist/wait-for-transaction.js +72 -0
- package/dist/wait-for-transaction.js.map +1 -0
- package/dist/watch-transaction.d.ts +57 -0
- package/dist/watch-transaction.d.ts.map +1 -0
- package/dist/watch-transaction.js +76 -0
- package/dist/watch-transaction.js.map +1 -0
- package/package.json +6 -1
- package/skills/tx-tracker-integration/SKILL.md +198 -0
|
@@ -0,0 +1,287 @@
|
|
|
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, prefetchedReceipts, } = 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 receipt = isFreshInclusion
|
|
70
|
+
? prefetchedReceipts?.get(record.hash)
|
|
71
|
+
: undefined;
|
|
72
|
+
const events = isFreshInclusion
|
|
73
|
+
? [
|
|
74
|
+
buildSeenInBlock({
|
|
75
|
+
hash: record.hash,
|
|
76
|
+
chainId,
|
|
77
|
+
source: eventSource,
|
|
78
|
+
at: envelope,
|
|
79
|
+
blockHash,
|
|
80
|
+
blockNumber,
|
|
81
|
+
transactionIndex,
|
|
82
|
+
confirmations,
|
|
83
|
+
...(receipt !== undefined ? { receipt } : {}),
|
|
84
|
+
}),
|
|
85
|
+
]
|
|
86
|
+
: [];
|
|
87
|
+
return {
|
|
88
|
+
events,
|
|
89
|
+
statusPatch: {
|
|
90
|
+
lastSeenInBlock,
|
|
91
|
+
unseenStreak: 0,
|
|
92
|
+
firstObservedAtBlock: record.status.firstObservedAtBlock ?? blockNumber,
|
|
93
|
+
lastObservedAtBlock: blockNumber,
|
|
94
|
+
},
|
|
95
|
+
identityPatch: cacheIdentity(record.identity, tx),
|
|
96
|
+
inMempoolPatch: null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Path 2: not in this block, but previously observed → confirmation bump
|
|
100
|
+
if (record.status.lastSeenInBlock && previousTipNumber !== null) {
|
|
101
|
+
const bumped = record.status.lastSeenInBlock.confirmations + 1;
|
|
102
|
+
const updated = {
|
|
103
|
+
...record.status.lastSeenInBlock,
|
|
104
|
+
confirmations: bumped,
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
events: [
|
|
108
|
+
buildSeenInBlock({
|
|
109
|
+
hash: record.hash,
|
|
110
|
+
chainId,
|
|
111
|
+
source: eventSource,
|
|
112
|
+
at: envelope,
|
|
113
|
+
blockHash: updated.blockHash,
|
|
114
|
+
blockNumber: updated.blockNumber,
|
|
115
|
+
transactionIndex: updated.transactionIndex,
|
|
116
|
+
confirmations: bumped,
|
|
117
|
+
}),
|
|
118
|
+
],
|
|
119
|
+
statusPatch: {
|
|
120
|
+
lastSeenInBlock: updated,
|
|
121
|
+
lastObservedAtBlock: blockNumber,
|
|
122
|
+
},
|
|
123
|
+
identityPatch: null,
|
|
124
|
+
inMempoolPatch: null,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Path 3: replacement detection
|
|
128
|
+
if (record.identity) {
|
|
129
|
+
const replacement = findReplacementInBlock(record.identity, record.hash, txs);
|
|
130
|
+
if (replacement && replacement.hash) {
|
|
131
|
+
return {
|
|
132
|
+
events: [
|
|
133
|
+
buildReplacedBy({
|
|
134
|
+
hash: record.hash,
|
|
135
|
+
chainId,
|
|
136
|
+
source: eventSource,
|
|
137
|
+
at: envelope,
|
|
138
|
+
replacementHash: replacement.hash,
|
|
139
|
+
replacementBlockNumber: blockNumber,
|
|
140
|
+
}),
|
|
141
|
+
],
|
|
142
|
+
statusPatch: {
|
|
143
|
+
replacedBy: { hash: replacement.hash, blockNumber },
|
|
144
|
+
lastObservedAtBlock: blockNumber,
|
|
145
|
+
},
|
|
146
|
+
identityPatch: null,
|
|
147
|
+
inMempoolPatch: null,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Path 4: truly unseen — only counts when there's a prior
|
|
152
|
+
// observation to count from.
|
|
153
|
+
if (record.status.firstObservedAtBlock === null) {
|
|
154
|
+
return EMPTY_RESULT;
|
|
155
|
+
}
|
|
156
|
+
const nextStreak = record.status.unseenStreak + 1;
|
|
157
|
+
const events = nextStreak === record.unseenThresholdBlocks
|
|
158
|
+
? [
|
|
159
|
+
buildUnseenForNBlocks({
|
|
160
|
+
hash: record.hash,
|
|
161
|
+
chainId,
|
|
162
|
+
source: eventSource,
|
|
163
|
+
at: envelope,
|
|
164
|
+
blocks: nextStreak,
|
|
165
|
+
}),
|
|
166
|
+
]
|
|
167
|
+
: [];
|
|
168
|
+
return {
|
|
169
|
+
events,
|
|
170
|
+
statusPatch: { unseenStreak: nextStreak },
|
|
171
|
+
identityPatch: null,
|
|
172
|
+
inMempoolPatch: null,
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Per-record decision for one mempool snapshot. Three independent
|
|
177
|
+
* outputs that may all fire on the same call:
|
|
178
|
+
*
|
|
179
|
+
* - **Presence transition** — emit `seen-in-mempool` on first
|
|
180
|
+
* observation or bucket change; emit `left-mempool` when a
|
|
181
|
+
* previously-seen hash is absent from this snapshot.
|
|
182
|
+
* - **Replacement** — emit `replaced-by` (with `null` block) when
|
|
183
|
+
* the orchestrator's pre-computed `replacementInMempool` is set
|
|
184
|
+
* AND the record hasn't already recorded a replacement.
|
|
185
|
+
*/
|
|
186
|
+
export const decideMempoolObservation = (input) => {
|
|
187
|
+
const { record, presence, replacementInMempool, chainId, eventSource, envelope, tipBlockNumber, } = input;
|
|
188
|
+
const events = [];
|
|
189
|
+
let statusPatch = {};
|
|
190
|
+
let identityPatch = null;
|
|
191
|
+
let inMempoolPatch = null;
|
|
192
|
+
if (presence) {
|
|
193
|
+
identityPatch = cacheIdentity(record.identity, presence.tx);
|
|
194
|
+
const isFreshOrBucketChange = !record.inLastMempoolSnapshot ||
|
|
195
|
+
record.status.lastSeenInMempool?.bucket !== presence.bucket;
|
|
196
|
+
statusPatch = {
|
|
197
|
+
lastSeenInMempool: {
|
|
198
|
+
bucket: presence.bucket,
|
|
199
|
+
tx: presence.tx,
|
|
200
|
+
at: envelope,
|
|
201
|
+
source: eventSource,
|
|
202
|
+
},
|
|
203
|
+
unseenStreak: 0,
|
|
204
|
+
firstObservedAtBlock: record.status.firstObservedAtBlock ?? tipBlockNumber,
|
|
205
|
+
lastObservedAtBlock: tipBlockNumber,
|
|
206
|
+
};
|
|
207
|
+
inMempoolPatch = true;
|
|
208
|
+
if (isFreshOrBucketChange) {
|
|
209
|
+
events.push(buildSeenInMempool({
|
|
210
|
+
hash: record.hash,
|
|
211
|
+
chainId,
|
|
212
|
+
source: eventSource,
|
|
213
|
+
at: envelope,
|
|
214
|
+
bucket: presence.bucket,
|
|
215
|
+
tx: presence.tx,
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else if (record.inLastMempoolSnapshot) {
|
|
220
|
+
inMempoolPatch = false;
|
|
221
|
+
events.push(buildLeftMempool({
|
|
222
|
+
hash: record.hash,
|
|
223
|
+
chainId,
|
|
224
|
+
source: eventSource,
|
|
225
|
+
at: envelope,
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
// Replacement detection runs independently. The orchestrator skips
|
|
229
|
+
// it when the record already carries a replacement; we mirror that
|
|
230
|
+
// here so the function is self-contained.
|
|
231
|
+
if (record.identity &&
|
|
232
|
+
!record.status.replacedBy &&
|
|
233
|
+
replacementInMempool &&
|
|
234
|
+
replacementInMempool.hash &&
|
|
235
|
+
replacementInMempool.hash !== record.hash) {
|
|
236
|
+
statusPatch = {
|
|
237
|
+
...statusPatch,
|
|
238
|
+
replacedBy: { hash: replacementInMempool.hash, blockNumber: null },
|
|
239
|
+
};
|
|
240
|
+
events.push(buildReplacedBy({
|
|
241
|
+
hash: record.hash,
|
|
242
|
+
chainId,
|
|
243
|
+
source: eventSource,
|
|
244
|
+
at: envelope,
|
|
245
|
+
replacementHash: replacementInMempool.hash,
|
|
246
|
+
replacementBlockNumber: null,
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
return { events, statusPatch, identityPatch, inMempoolPatch };
|
|
250
|
+
};
|
|
251
|
+
// -------------------------------------------------------------
|
|
252
|
+
// Helpers (also pure)
|
|
253
|
+
// -------------------------------------------------------------
|
|
254
|
+
/**
|
|
255
|
+
* Find a tx in `txs` whose `(from, nonce)` matches `identity` but
|
|
256
|
+
* whose hash differs from `originalHash` — the replacement candidate
|
|
257
|
+
* for the original tracked tx. Compares senders case-insensitively
|
|
258
|
+
* since upstreams disagree on checksum form.
|
|
259
|
+
*/
|
|
260
|
+
export const findReplacementInBlock = (identity, originalHash, txs) => {
|
|
261
|
+
const targetFrom = identity.from.toLowerCase();
|
|
262
|
+
for (const tx of txs) {
|
|
263
|
+
if (tx.from?.toLowerCase() !== targetFrom)
|
|
264
|
+
continue;
|
|
265
|
+
if (tx.nonce !== identity.nonce)
|
|
266
|
+
continue;
|
|
267
|
+
if (!tx.hash)
|
|
268
|
+
continue;
|
|
269
|
+
if (tx.hash === originalHash)
|
|
270
|
+
continue;
|
|
271
|
+
return tx;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Cache the tx's `(from, nonce)` as the record's identity if it's
|
|
277
|
+
* not already cached AND the tx carries both fields. Returns the
|
|
278
|
+
* patch to apply (or `null` when no change).
|
|
279
|
+
*/
|
|
280
|
+
export const cacheIdentity = (current, tx) => {
|
|
281
|
+
if (current)
|
|
282
|
+
return null;
|
|
283
|
+
if (!tx.from || !tx.nonce)
|
|
284
|
+
return null;
|
|
285
|
+
return { from: tx.from, nonce: tx.nonce };
|
|
286
|
+
};
|
|
287
|
+
//# sourceMappingURL=observations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observations.js","sourceRoot":"","sources":["../src/observations.ts"],"names":[],"mappings":"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"}
|
package/dist/reorg.d.ts
ADDED
|
@@ -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"}
|
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,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `replaceTransaction` — same-nonce replacement primitive. Provex
|
|
3
|
+
* upstream item 5 (memory: upstream-candidates.md#L50). Caller
|
|
4
|
+
* provides `original` (the previously-submitted request, including
|
|
5
|
+
* calldata + nonce) and `newGas` (the bumped EIP-1559 fee params,
|
|
6
|
+
* computed via @valve-tech/gas-oracle's helper or the caller's own
|
|
7
|
+
* math).
|
|
8
|
+
*
|
|
9
|
+
* **tx-tracker MUST NOT import from @valve-tech/gas-oracle.** Sibling
|
|
10
|
+
* packages, kept independent. The bump-rule helper ships separately
|
|
11
|
+
* in gas-oracle. See project memory:
|
|
12
|
+
* transaction-verbs-package-placement.md.
|
|
13
|
+
*/
|
|
14
|
+
import type { Address, Hex, WalletClient } from 'viem';
|
|
15
|
+
export interface ReplaceTransactionOriginal {
|
|
16
|
+
to: Address;
|
|
17
|
+
nonce: number;
|
|
18
|
+
data?: Hex;
|
|
19
|
+
value?: bigint;
|
|
20
|
+
chainId?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ReplaceTransactionNewGas {
|
|
23
|
+
maxFeePerGas: bigint;
|
|
24
|
+
maxPriorityFeePerGas: bigint;
|
|
25
|
+
}
|
|
26
|
+
export interface ReplaceTransactionOptions {
|
|
27
|
+
original: ReplaceTransactionOriginal;
|
|
28
|
+
walletClient: WalletClient;
|
|
29
|
+
newGas: ReplaceTransactionNewGas;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Submit a same-nonce replacement transaction with bumped gas.
|
|
33
|
+
* Returns the new tx hash. Throws whatever the wallet client throws
|
|
34
|
+
* (no swallowing — caller decides retry / surface).
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* import { replaceTransaction } from '@valve-tech/tx-tracker'
|
|
38
|
+
*
|
|
39
|
+
* const newHash = await replaceTransaction({
|
|
40
|
+
* original: { to: '0xrecipient', nonce: 42, data: '0x...', value: 0n },
|
|
41
|
+
* walletClient,
|
|
42
|
+
* newGas: bumpForReplacement(currentGas), // from @valve-tech/gas-oracle
|
|
43
|
+
* })
|
|
44
|
+
*/
|
|
45
|
+
export declare const replaceTransaction: (options: ReplaceTransactionOptions) => Promise<Hex>;
|
|
46
|
+
//# sourceMappingURL=replace-transaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-transaction.d.ts","sourceRoot":"","sources":["../src/replace-transaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,MAAM,CAAA;AAEtD,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,OAAO,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,GAAG,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,MAAM,CAAA;IACpB,oBAAoB,EAAE,MAAM,CAAA;CAC7B;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,0BAA0B,CAAA;IACpC,YAAY,EAAE,YAAY,CAAA;IAC1B,MAAM,EAAE,wBAAwB,CAAA;CACjC;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,yBAAyB,KACjC,OAAO,CAAC,GAAG,CAkBb,CAAA"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `replaceTransaction` — same-nonce replacement primitive. Provex
|
|
3
|
+
* upstream item 5 (memory: upstream-candidates.md#L50). Caller
|
|
4
|
+
* provides `original` (the previously-submitted request, including
|
|
5
|
+
* calldata + nonce) and `newGas` (the bumped EIP-1559 fee params,
|
|
6
|
+
* computed via @valve-tech/gas-oracle's helper or the caller's own
|
|
7
|
+
* math).
|
|
8
|
+
*
|
|
9
|
+
* **tx-tracker MUST NOT import from @valve-tech/gas-oracle.** Sibling
|
|
10
|
+
* packages, kept independent. The bump-rule helper ships separately
|
|
11
|
+
* in gas-oracle. See project memory:
|
|
12
|
+
* transaction-verbs-package-placement.md.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Submit a same-nonce replacement transaction with bumped gas.
|
|
16
|
+
* Returns the new tx hash. Throws whatever the wallet client throws
|
|
17
|
+
* (no swallowing — caller decides retry / surface).
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* import { replaceTransaction } from '@valve-tech/tx-tracker'
|
|
21
|
+
*
|
|
22
|
+
* const newHash = await replaceTransaction({
|
|
23
|
+
* original: { to: '0xrecipient', nonce: 42, data: '0x...', value: 0n },
|
|
24
|
+
* walletClient,
|
|
25
|
+
* newGas: bumpForReplacement(currentGas), // from @valve-tech/gas-oracle
|
|
26
|
+
* })
|
|
27
|
+
*/
|
|
28
|
+
export const replaceTransaction = async (options) => {
|
|
29
|
+
const { original, walletClient, newGas } = options;
|
|
30
|
+
if (!walletClient.account) {
|
|
31
|
+
throw new Error('replaceTransaction: walletClient must have an account');
|
|
32
|
+
}
|
|
33
|
+
// viem's WalletClient.sendTransaction has multiple overloads with strict
|
|
34
|
+
// type-narrowing on chain/account. Cast is necessary to thread the request
|
|
35
|
+
// through the type system without consumers having to specify all generic params.
|
|
36
|
+
return walletClient.sendTransaction({
|
|
37
|
+
account: walletClient.account,
|
|
38
|
+
chain: null,
|
|
39
|
+
to: original.to,
|
|
40
|
+
data: original.data,
|
|
41
|
+
value: original.value,
|
|
42
|
+
nonce: original.nonce,
|
|
43
|
+
maxFeePerGas: newGas.maxFeePerGas,
|
|
44
|
+
maxPriorityFeePerGas: newGas.maxPriorityFeePerGas,
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=replace-transaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-transaction.js","sourceRoot":"","sources":["../src/replace-transaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAuBH;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,OAAkC,EACpB,EAAE;IAChB,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAClD,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAA;IAC1E,CAAC;IACD,yEAAyE;IACzE,2EAA2E;IAC3E,kFAAkF;IAClF,OAAO,YAAY,CAAC,eAAe,CAAC;QAClC,OAAO,EAAE,YAAY,CAAC,OAAO;QAC7B,KAAK,EAAE,IAAI;QACX,EAAE,EAAE,QAAQ,CAAC,EAAE;QACf,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,oBAAoB,EAAE,MAAM,CAAC,oBAAoB;KACI,CAAC,CAAA;AAC1D,CAAC,CAAA"}
|