@crewhaus/tool-chainread 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/README.md ADDED
@@ -0,0 +1,396 @@
1
+ # @crewhaus/tool-chainread
2
+
3
+ What the chain actually recorded, read over a public RPC endpoint you name.
4
+
5
+ | Tool | Answers |
6
+ |---|---|
7
+ | `EvmGetBlock` | what is in this block — by number, by tag, or by hash |
8
+ | `EvmBlockAtTimestamp` | which block was the chain at, at this moment |
9
+ | `EvmRpcHealth` | what is this endpoint, and what can it serve |
10
+ | `EvmNonceStatus` | is this account clear to send, or is something stuck |
11
+ | `EvmWaitForReceipt` | did it land, did it revert, or is it still pending |
12
+ | `EvmTransactionSummary` | what did this transaction actually move |
13
+ | `EvmEventScan` | every log matching this filter over this range |
14
+ | `OnchainTransactionsSync` | this address's history, as the rows a reconciler already reads |
15
+
16
+ `@crewhaus/tool-onchain` next door does the arithmetic offline — calldata, digests,
17
+ checksums, token units. This package is the half that dials.
18
+
19
+ ## Nothing here signs or sends
20
+
21
+ No schema accepts a private key, a mnemonic, a keystore or a signed payload, and no
22
+ code path can reach `eth_sendRawTransaction`. That is not a promise about the
23
+ call sites: **every** method name goes through `assertReadOnlyMethod` from
24
+ `@crewhaus/chain-adapter-base` — the same allow-list the chain adapters use — before a
25
+ socket is opened, so a write method throws inside the transport whatever asks for it.
26
+ Both halves are asserted in `index.test.ts`: the schemas are walked for key-shaped
27
+ field names, and the stub endpoint's own request log is checked for anything that
28
+ sends.
29
+
30
+ ## Three outcomes, not two
31
+
32
+ `EvmWaitForReceipt` exists because callers conflate three things that need three
33
+ different next moves:
34
+
35
+ | Outcome | What happened | What you do |
36
+ |---|---|---|
37
+ | `mined` + `status: "success"` | it landed and did what it said | carry on |
38
+ | `mined` + `status: "reverted"` | it landed and the call failed | fix the call — **do not** resend |
39
+ | `deadline` | still unmined when the clock ran out | wait longer, or speed it up |
40
+
41
+ A reverted transaction **has a receipt**, with `status: 0x0`. A tool that reports "no
42
+ receipt" for it tells the caller to broadcast again, and a failed transaction becomes
43
+ two. There is a fourth distinction inside the third: `knownToEndpoint` says whether
44
+ this endpoint is holding the transaction in its mempool or has never heard of it,
45
+ which is the difference between "slow" and "it never propagated, or it went to a
46
+ different chain".
47
+
48
+ A receipt that arrived but is not yet as deep as you asked for still reports as
49
+ `mined`, with `settled: false` — the clock running out does not un-mine anything.
50
+ `finality: "safe" | "finalized"` uses the chain's own tag instead of counting blocks;
51
+ an endpoint that does not serve that tag is a refusal, not a quiet fallback to
52
+ counting, because counting is not finality on a chain that reorganises in bursts.
53
+
54
+ There is one thing it deliberately does not do: explain a revert. Replaying the call
55
+ to recover the revert reason needs the state as it was at mining time, which needs an
56
+ archive node, and replaying it against the head instead produces a "reason" that is
57
+ sometimes right and always confident. Use `EvmRpcHealth` to find out whether your
58
+ endpoint keeps archive state, then replay it yourself if it does.
59
+
60
+ ## The invariant `EvmBlockAtTimestamp` returns
61
+
62
+ > The returned block is the **last** block whose timestamp is `<= target`, and the
63
+ > block after it has a timestamp `> target`.
64
+
65
+ Half of that — "a block at or before the timestamp" — is satisfied by the genesis
66
+ block, which is why the second half is part of the contract. The result carries a
67
+ `certificate` with all three numbers so the invariant can be checked without another
68
+ call, and the block after is returned as `next`.
69
+
70
+ The boundaries are where a plausible wrong answer is indistinguishable from the right
71
+ one, so each one has a defined behaviour:
72
+
73
+ - **Before the first block.** Refused, naming the earliest block and its timestamp.
74
+ Answering "block 0" would send a caller to read state from before the chain existed.
75
+ - **At or past the head.** Returns the head with `atHead: true`, `next: null` and a
76
+ note that the answer will change as the chain advances. There is no block after the
77
+ head to prove a boundary with, and pretending otherwise is the lie.
78
+ - **Several blocks sharing one timestamp.** Returns the highest of them. Ties resolve
79
+ upward, because a caller reading state at the answer wants the state after
80
+ everything that happened at that instant.
81
+ - **Timestamps that go backwards.** Refused, naming both blocks. A binary search over
82
+ a non-monotonic sequence has more than one right answer, and every sample the search
83
+ takes is compared against every other so the assumption is checked rather than
84
+ assumed.
85
+ - **Milliseconds.** Refused by name. `Date.now()` pasted in would otherwise resolve to
86
+ "at or past the head" and answer with the head block, every time, looking entirely
87
+ ordinary.
88
+
89
+ The search is plain bisection: about 25 probes over a twenty-million-block chain, and
90
+ `fromBlock`/`toBlock` cut both the bracket and the probe count. Interpolating from an
91
+ average block time would converge faster on a chain with a steady block time and badly
92
+ on one that changed — Ethereum before and after the Merge is two different chains by
93
+ that measure — and the failure mode of a bad estimate is a wrong bracket, so the
94
+ faster thing is not the safer thing here.
95
+
96
+ ## `EvmEventScan` refuses rather than under-reporting
97
+
98
+ Public endpoints cap log queries and disagree about how. Three behaviours, three
99
+ answers:
100
+
101
+ 1. **An error naming a smaller range.** The suggested range is parsed out of the error
102
+ and used directly, clamped so a suggestion can only ever narrow the request — an
103
+ endpoint answering "try 10000 blocks" to a 500-block query is describing its own
104
+ limit, not this request. Only the first 512 characters of the message are read, and
105
+ the parser's quantifiers are bounded: this string is the one field in the exchange a
106
+ provider fills in freely, inside a body allowed sixteen megabytes, and an unbounded
107
+ digit run costs quadratic time to fail to match. When nothing usable is in the
108
+ message, the span is halved.
109
+ The decision to narrow is made on the *shape* of the failure (a JSON-RPC error, an
110
+ oversized body, a timeout), never by matching error text: providers phrase the same
111
+ cap five ways. A 401, 403 or 429 never narrows, because retrying a rate limit in
112
+ halves turns one rejected request into sixteen.
113
+ 2. **An error about the result count.** Same path.
114
+ 3. **Silent truncation.** The endpoint returns the first N logs of a range that held
115
+ more, with a 200 and a well-formed array and nothing saying it is short. This is
116
+ the one that matters, because a harness that scans for `Transfer` and finds nothing
117
+ concludes the transfer did not happen.
118
+
119
+ Truncation is caught by proof, not by a heuristic about round numbers. A chunk whose
120
+ count reaches `suspectAt` (default 1000) is split and re-queried; if the halves
121
+ together hold more logs than the whole did, the whole was truncated. The recovered set
122
+ is returned with `paging.silentTruncations` and a warning saying the endpoint will do
123
+ it again — and the working span is narrowed so the rest of the scan does not
124
+ rediscover the same cap chunk after chunk.
125
+
126
+ When the split cannot go further because the chunk is a single block, there is nothing
127
+ left to prove it with, and the scan **refuses**. So does a result larger than `maxLogs`
128
+ — slicing it here would produce exactly the indistinguishable-from-complete set the
129
+ rest of this is about. Three more refusals: a log from outside the range that was
130
+ asked for, a log marked `removed`, and the same block height coming back with two
131
+ different hashes, which is the chain reorganising mid-scan.
132
+
133
+ `complete: true` is the only value that field ever has. The alternative is an error.
134
+
135
+ The range is resolved to numbers before the first query, so `toBlock: "latest"` is
136
+ pinned once; a scan whose upper bound keeps moving is not one anybody can call
137
+ complete. `confirmations` keeps the whole range behind the head by that many blocks.
138
+
139
+ ## `OnchainTransactionsSync` borrows the row shape rather than inventing one
140
+
141
+ The rows it returns are `@crewhaus/tool-money`'s `Transaction` — the same shape
142
+ `StatementParse` produces and `LedgerReconcile` consumes — imported, not
143
+ re-declared. A second declaration of it is a shape that drifts the first time a
144
+ field is added, and an onchain history that cannot be matched against a bank
145
+ statement is the whole point of the tool gone.
146
+
147
+ That shape has one field this package would never have written: `amountMinor` is
148
+ a JS `number`. A uint256 is not, and `Number.MAX_SAFE_INTEGER` is about nine
149
+ thousandths of one coin in wei — so a plain payment does not fit in the field at
150
+ all. Nothing is rounded to make it. A movement that cannot be written exactly is
151
+ **parked**, in an `unrepresentable` list, with the raw amount and the reason:
152
+
153
+ | Reason | What it means |
154
+ |---|---|
155
+ | `exceedsSafeInteger` | the amount is past what the row's number field holds. Name the asset's `decimals` and the ledger's `minorUnitDecimals` and it scales into range |
156
+ | `belowMinorUnit` | 1.234567 USDC against a ledger kept in cents. No rounding of it is the amount that moved |
157
+ | `timestampOutOfRange` | the block's timestamp is past what a calendar date can express, so the row has no date |
158
+ | `undecodable` | a log carrying the Transfer topic without a Transfer's topics |
159
+
160
+ The exact uint256 also travels beside every row it *did* fit, in `detail`, keyed
161
+ by the row's id. A reconciler reads `rows`; an auditor reads `detail`.
162
+
163
+ ### Three sources, and the ones there are not
164
+
165
+ A public endpoint has no index by address, so a history has to be reconstructed:
166
+
167
+ 1. **Logged token transfers**, from `eth_getLogs` with the address in the from
168
+ or the to topic position — two scans, merged and deduped. Paged by the same
169
+ `scanLogs` that `EvmEventScan` uses, so the silent-truncation proof, the
170
+ reorg check and the refusal-rather-than-a-prefix behaviour are the tested
171
+ ones and not a second implementation of them.
172
+ 2. **Native value and gas**, by hydrating every block in the range and picking
173
+ out the transactions this address was a party to. That is one request per
174
+ block, so the range is bounded by `maxHydratedBlocks` and a range past it is
175
+ a **refusal** — returning the token rows alone and calling it a history would
176
+ leave every coin payment out of a statement that says it is complete.
177
+ 3. Nothing else, and the output says so rather than the docs alone:
178
+ **internal native transfers** need a trace, **ERC-1155** puts its parties in
179
+ topic positions this filter does not look at and its batch amounts in data no
180
+ filter reaches, and **token metadata** is never read off a token contract —
181
+ an airdrop whose `decimals()` returns 2 turns dust into a five-figure row, so
182
+ decimals arrive from the caller or the raw base units are used unchanged.
183
+
184
+ `tokens` is an allow-list, and it is applied twice: in the `eth_getLogs` filter,
185
+ so an airdrop is never fetched, and again against the logs that come back. A
186
+ filter is a request, not a proof — an endpoint that ignores it answers with every
187
+ token that ever touched the wallet, and those rows would land in a
188
+ reconciliation under a `coverage` line saying they were never scanned for.
189
+
190
+ The block path has its own version of the truncation trap. An endpoint that
191
+ ignores `fullTransactions: true` answers with a list of hashes; filtering hashes
192
+ on `from` matches nothing, and that empty result is indistinguishable from a
193
+ block this wallet was never in. It is refused by name.
194
+
195
+ ### What the blocks held, checked against the nonce
196
+
197
+ A block is one response and cannot be re-asked in halves, so the split-and-
198
+ compare proof the log scan uses has nothing to work with here. The account's own
199
+ nonce is the oracle instead: it rises once per transaction sent, so its rise
200
+ across the range is how many of them those blocks must contain.
201
+
202
+ Fewer is a **refusal** — each one missed is a fee row and possibly a payment.
203
+ More is not: a rollup's system and deposit transactions appear in blocks without
204
+ raising an ordinary nonce, and nothing is missing in that direction. A pruned
205
+ endpoint that will not serve a historical nonce leaves the check unrun, and
206
+ `sentProof` says `unproved` with the reason rather than reporting a completeness
207
+ nothing established. What the address *received* has no such oracle at all, and
208
+ `coverage.nativeTransfers` says so in the output.
209
+
210
+ ### The things that double-count
211
+
212
+ **Gas is charged once per transaction.** A wallet can appear in a dozen logs of
213
+ one swap, and a fee attributed per log charges the gas a dozen times — the
214
+ reconciliation is then off by exactly that. Fee rows come only from the block
215
+ path, keyed on the transaction, so it cannot happen.
216
+
217
+ **A reverted transaction moved nothing.** Its value transfer is discarded with
218
+ the rest of its state changes and only the fee is charged, so a receipt is read
219
+ for every matching transaction — for the status as much as for the fee. A mined
220
+ transaction whose receipt this endpoint does not have is a refusal, because
221
+ booking the movement either way is a guess.
222
+
223
+ **A transfer to oneself is not a row.** It changes no balance; one row would be
224
+ a debit that never happened.
225
+
226
+ ### The cursor names where it stopped
227
+
228
+ `cursor.nextFromBlock` is the pinned upper bound plus one — not the head at the
229
+ end of the run, which has moved, and a cursor set from it skips every block
230
+ mined while the sync ran. `confirmations` keeps the whole range behind the head.
231
+
232
+ Dates are UTC calendar dates, because a block timestamp is UTC and nothing on
233
+ the chain knows the ledger's timezone. `balanceMinor` is always `null`: there is
234
+ no opening balance here to run one from, and a computed one is a number a
235
+ reconciliation would trust.
236
+
237
+ ## `EvmRpcHealth` says unknown when it means unknown
238
+
239
+ Archive support is decided by reading a historic balance and interpreting what comes
240
+ back, and the interesting case is the third one:
241
+
242
+ - the read succeeds → `yes`
243
+ - it fails with a pruning error (`missing trie node`, `state is not available`, …) → `no`
244
+ - anything else → `unknown`, with the error as the evidence
245
+
246
+ "We could not tell" and "it does not" lead to different decisions, and only one of them
247
+ means find another endpoint. The probe block must be more than 128 blocks behind the
248
+ head, because that is how much state a plain full node keeps: a successful read inside
249
+ that window distinguishes nothing, and a short-lived devnet is entirely inside it —
250
+ also reported as `unknown`, saying so.
251
+
252
+ Head age comes from an injected clock, and a head stamped in the *future* is reported
253
+ as `clockSkewSuspected` rather than as a very fresh chain. `compareWith` probes several
254
+ endpoints and flags the one thing that is never survivable: two endpoints reporting
255
+ different chain ids, which is a misconfiguration that otherwise shows up as data that
256
+ cannot be reconciled.
257
+
258
+ No latency is measured. One sample from behind a load balancer says nothing, and a
259
+ verdict that depends on it is a verdict that changes between two identical runs.
260
+
261
+ ## `EvmNonceStatus` will not guess
262
+
263
+ `eth_getTransactionCount(address, "pending")` is not a standard. Plenty of endpoints do
264
+ not track a public mempool and answer it with the mined count, so `pending == latest`
265
+ means either "nothing is queued" or "this endpoint cannot see what is queued" — and a
266
+ confident `clear` from the second is how a harness broadcasts a duplicate.
267
+
268
+ Hand it the transaction hashes you broadcast and it can decide instead of guessing:
269
+
270
+ - a transaction this endpoint admits is unmined, on an account whose pending nonce has
271
+ not moved, proves the pending view is not mempool-aware → `mempoolVisible: false`,
272
+ verdict `unknown`
273
+ - a transaction waiting at a nonce above the pending nonce proves a hole at the pending
274
+ nonce, which nothing after can be mined past → verdict `gap`
275
+ - a hash the endpoint has forgotten, at a nonce the account has already passed, was
276
+ replaced or superseded by something else (pass the `nonce` you sent it with, or the
277
+ two are indistinguishable)
278
+ - a pending nonce *below* the latest nonce cannot be true of one node — two sequential
279
+ calls were answered by different machines — so both numbers are reported as unusable
280
+ rather than subtracted
281
+
282
+ With no hashes supplied, `gapDetection` says so: a gapped transaction sits in a node's
283
+ queued pool and is invisible to both nonce counts.
284
+
285
+ ## `EvmTransactionSummary` says what it cannot see
286
+
287
+ A receipt contains logs and nothing else. Native value moved by a **contract** during
288
+ the call leaves no log — it is only visible in a trace, and `debug_traceTransaction` is
289
+ neither on a public endpoint's free tier nor on this package's read-only allow-list. So
290
+ `netDeltas.complete` is `false` and `completeness.internalNativeTransfers` is
291
+ `"excluded"`, in the output rather than only here. A net delta that claims to be
292
+ complete and is not is worse than no net delta: it reconciles.
293
+
294
+ A **reverted** transaction moved nothing. Its value transfer is discarded with the rest
295
+ of its state changes and only the fee is charged, so `netDeltas.nativeWei` is `0` and
296
+ `netDeltas.reverted` is `true`. A receipt with neither status nor revert — a
297
+ pre-Byzantium one, carrying a state root instead — does not record whether the call
298
+ reverted at all: the value is counted as moved, and `completeWhy` says that this is a
299
+ convention rather than a reading.
300
+
301
+ Fees are computed from the fields the receipt actually has, not from a chain-id table
302
+ that goes stale every time a rollup ships. An OP-stack receipt carries `l1Fee`, which is
303
+ charged *separately* from gas — leaving it out understates a Base transaction by most of
304
+ its cost. An Arbitrum receipt carries `gasUsedForL1`, whose gas is *already inside*
305
+ `gasUsed` — adding anything would double-count. The model that was applied travels with
306
+ the number.
307
+
308
+ Token amounts are raw base units. Resolving symbol and decimals costs an `eth_call` per
309
+ token and a token contract is free to report whatever it likes; `TokenUnits` in
310
+ `@crewhaus/tool-onchain` will format them against decimals you trust.
311
+
312
+ ## The endpoint is vetted before the socket opens
313
+
314
+ Every other networked package here builds its URLs from constants, so a model never
315
+ picks a host. This one cannot — reading the chain you name means you name the RPC URL —
316
+ and an unguarded `fetch(input.rpcUrl)` is a server-side request forgery primitive with a
317
+ model steering. `http://169.254.169.254/` is the cloud metadata service.
318
+
319
+ So: the scheme must be http(s), credentials in the URL are refused, the host is checked
320
+ against the loopback, private, link-local, carrier-grade-NAT and multicast ranges —
321
+ through every `inet_aton` spelling, because `0177.0.0.1`, `0x7f000001` and `2130706433`
322
+ are all `127.0.0.1` — the name is resolved, the resolved address is checked too, and the
323
+ connection is pinned to the address that was vetted so a rebinding resolver cannot hand
324
+ back a different one at connect time. Error messages carry the origin only: provider
325
+ keys live in the path.
326
+
327
+ IPv6 is classified by expanding the address to its eight groups and doing arithmetic on
328
+ them, never by matching its text. One address has many spellings and the one a check is
329
+ written against is rarely the one that arrives: `http://[::ffff:169.254.169.254]/` never
330
+ reaches a guard in that form, because the URL parser re-serialises the embedded quad and
331
+ hands over `::ffff:a9fe:a9fe`. Every transition mechanism that carries an IPv4 address —
332
+ IPv4-mapped, IPv4-translated, NAT64's `64:ff9b::/96` (which is exactly what a DNS64
333
+ resolver answers for an IPv4-only name), 6to4's `2002::/16`, the deprecated
334
+ IPv4-compatible form — is resolved to the IPv4 it carries and judged on that.
335
+
336
+ A local anvil or hardhat node is the obvious casualty, so an **operator** can permit it:
337
+
338
+ ```ts
339
+ import { setRpcEndpointPolicy } from "@crewhaus/tool-chainread";
340
+
341
+ setRpcEndpointPolicy({ allowPrivateHosts: true }); // a local devnet
342
+ setRpcEndpointPolicy({ allowedOrigins: ["https://mainnet.base.org"] }); // stricter still
343
+ ```
344
+
345
+ Bound at boot by the runtime, exactly like `setEvmAdapterResolver` in `tool-evm`, and
346
+ deliberately **not** a field in any tool's input schema: a gate a model can open for
347
+ itself is not a gate. `EvmRpcHealth` reports the policy it is operating under.
348
+
349
+ This is a narrower guard than `@crewhaus/tool-fetch`'s `assertNotSsrf`, which is the
350
+ repo's canonical one and is not a dependency of this package. It covers the same ground
351
+ for the one shape of request made here — a single origin, no redirects followed, no
352
+ credentials attached — and a redirect is refused rather than followed, because only the
353
+ first URL is ours.
354
+
355
+ ## Quantities
356
+
357
+ Every chain quantity crosses this boundary as a **decimal string**: wei, token amounts,
358
+ nonces, gas, block numbers, timestamps. A uint256 does not fit in a double, and reading
359
+ a balance as a number silently loses its low bits. Block numbers get the same treatment
360
+ even though today's heads fit — they are compared, subtracted and used as search bounds
361
+ all over this package, and one accidental `Number(...)` in that chain is a bug that only
362
+ shows up on a chain nobody tested against.
363
+
364
+ Inputs accept a decimal string, a `0x` hex string or a safe integer. A number past
365
+ `Number.MAX_SAFE_INTEGER` is refused rather than rounded: by then it has already lost
366
+ its low digits, and accepting it would be accepting a value the caller can no longer
367
+ see is wrong.
368
+
369
+ ## Testing
370
+
371
+ No test in this package opens a socket or sends a DNS query. Three seams, all exported
372
+ beside the tools:
373
+
374
+ ```ts
375
+ import { _setClock, _setDnsLookup, _setFetch, virtualClock } from "@crewhaus/tool-chainread";
376
+ import { makeChain, rpcStub } from "./fixtures"; // src/fixtures.ts, beside the tools
377
+
378
+ _setDnsLookup(async () => ({ address: "93.184.216.34", family: 4 }));
379
+ _setFetch(rpcStub(makeChain({ blocks: 500 }), { silentLogCap: 10 }).fetch);
380
+ _setClock(virtualClock());
381
+ ```
382
+
383
+ `rpcStub` (in `src/fixtures.ts`) serves a recorded chain and takes the misbehaviours as knobs: a block-span cap
384
+ that errors with a suggested range, a result cap that errors, a result cap that
385
+ truncates silently, pruned archive state, an endpoint with no mempool view, no finality
386
+ tags, an HTTP rate limit, an unreachable host. Every request is recorded, so a test can
387
+ assert what a tool actually asked for — that a scan halved its range, that a search spent
388
+ the probes it claimed, that a poll stopped when it said it did.
389
+
390
+ `virtualClock` makes the deadline paths instant and deterministic: `EvmWaitForReceipt`'s
391
+ fifteen-minute wait costs microseconds, and the assertion is on the poll count and the
392
+ reason rather than on elapsed time.
393
+
394
+ Each seam restores its production implementation when passed `undefined`, and a suite
395
+ that sets one must restore it — the next file in the same bun process inherits it
396
+ otherwise.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * A recorded chain, and an RPC endpoint that serves it.
3
+ *
4
+ * No test in this package opens a socket. The stub below answers the ten
5
+ * JSON-RPC methods these tools use, from data built here, and its knobs are the
6
+ * ways a real public endpoint misbehaves: a block-span cap that errors with a
7
+ * suggested range, a result cap that errors, a result cap that SILENTLY
8
+ * truncates, a pruned archive, a mempool the endpoint cannot see, and an HTTP
9
+ * layer that rate-limits. Those knobs are the fixtures that matter — the happy
10
+ * path is the easy half.
11
+ *
12
+ * Every request is recorded in `requests`, so a test can assert what a tool
13
+ * actually asked for: that a scan halved its range, that a search spent the
14
+ * probes it claimed, that a poll stopped when it said it did.
15
+ */
16
+ import type { RpcFetch } from "./lib/rpc";
17
+ /** A deterministic 32-byte hash from a label, so fixtures are readable in a diff. */
18
+ export declare function hash32(label: string): string;
19
+ /** A deterministic 20-byte address from a label. */
20
+ export declare function address20(label: string): string;
21
+ export declare const TOKEN: string;
22
+ export declare const ALICE: string;
23
+ export declare const BOB: string;
24
+ export declare const ROUTER: string;
25
+ export declare const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
26
+ export declare const APPROVAL_TOPIC = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925";
27
+ export declare function topicFor(address: string): string;
28
+ export declare function word(value: bigint): string;
29
+ export type FixtureBlock = {
30
+ readonly number: bigint;
31
+ readonly timestamp: bigint;
32
+ readonly hash: string;
33
+ readonly baseFeePerGas: bigint | null;
34
+ };
35
+ export type FixtureLog = {
36
+ readonly blockNumber: bigint;
37
+ readonly logIndex: number;
38
+ readonly address: string;
39
+ readonly topics: ReadonlyArray<string>;
40
+ readonly data: string;
41
+ readonly transactionHash: string;
42
+ };
43
+ export type FixtureTx = {
44
+ readonly hash: string;
45
+ readonly from: string;
46
+ readonly to: string | null;
47
+ readonly value: bigint;
48
+ readonly nonce: bigint;
49
+ readonly input: string;
50
+ readonly blockNumber: bigint | null;
51
+ };
52
+ export type FixtureReceipt = {
53
+ readonly hash: string;
54
+ readonly blockNumber: bigint;
55
+ readonly status: "0x1" | "0x0" | null;
56
+ readonly gasUsed: bigint;
57
+ readonly effectiveGasPrice: bigint | null;
58
+ readonly l1Fee?: bigint;
59
+ readonly gasUsedForL1?: bigint;
60
+ readonly root?: string;
61
+ /** Set only by a contract creation, which is the one case a transaction has no `to`. */
62
+ readonly contractAddress?: string;
63
+ /** The transaction's place in its block, as the chain numbers it. Default 0. */
64
+ readonly transactionIndex?: number;
65
+ readonly logs: ReadonlyArray<FixtureLog>;
66
+ };
67
+ export type Chain = {
68
+ readonly chainId: bigint;
69
+ blocks: FixtureBlock[];
70
+ logs: FixtureLog[];
71
+ txs: Map<string, FixtureTx>;
72
+ receipts: Map<string, FixtureReceipt>;
73
+ /** address → { latest, pending } nonce. */
74
+ nonces: Map<string, {
75
+ latest: bigint;
76
+ pending: bigint;
77
+ }>;
78
+ head: bigint;
79
+ /** Tagged blocks the chain serves; a chain that serves neither is the pre-Merge case. */
80
+ safe: bigint | null;
81
+ finalized: bigint | null;
82
+ };
83
+ export type ChainOptions = {
84
+ readonly chainId?: bigint;
85
+ readonly blocks?: number;
86
+ readonly startTimestamp?: bigint;
87
+ readonly blockTime?: bigint;
88
+ readonly baseFee?: bigint | null;
89
+ };
90
+ export declare function makeChain(options?: ChainOptions): Chain;
91
+ /** Add `count` ERC-20 Transfer logs to one block, from ALICE to BOB. */
92
+ export declare function addTransferLogs(chain: Chain, blockNumber: bigint, count: number, token?: string): void;
93
+ export declare function blockOf(chain: Chain, number: bigint): FixtureBlock | undefined;
94
+ export type StubOptions = {
95
+ /** Error when a getLogs span exceeds this, naming a range that would have worked. */
96
+ readonly maxRangeBlocks?: bigint;
97
+ /** Error when more than this many logs match — the "query returned more than N results" family. */
98
+ readonly maxResults?: number;
99
+ /** Return the first N matches and say nothing. This is the one that needs catching. */
100
+ readonly silentLogCap?: number;
101
+ /** `no` prunes historic state; `error` fails the probe for an unrelated reason. */
102
+ readonly archive?: "yes" | "no" | "error";
103
+ /** When false, the pending nonce equals the latest one however full the mempool is. */
104
+ readonly mempoolAware?: boolean;
105
+ /** Answer every request with this HTTP status instead. */
106
+ readonly httpStatus?: number;
107
+ /** Throw from the dialer, as an unreachable host does. */
108
+ readonly unreachable?: boolean;
109
+ /** Serve neither `safe` nor `finalized`, as a pre-Merge or minimal endpoint does. */
110
+ readonly noFinalityTags?: boolean;
111
+ };
112
+ export type RpcStub = {
113
+ readonly fetch: RpcFetch;
114
+ /** Every request, in order: `{ method, params }`. */
115
+ readonly requests: Array<{
116
+ readonly method: string;
117
+ readonly params: ReadonlyArray<unknown>;
118
+ }>;
119
+ /** Requests for one method, for the assertions that count probes or polls. */
120
+ count(method: string): number;
121
+ };
122
+ /** Build an endpoint that serves `chain`, misbehaving in whatever way `options` says. */
123
+ export declare function rpcStub(chain: Chain, options?: StubOptions): RpcStub;