@crewhaus/tool-chaincall 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,264 @@
1
+ # @crewhaus/tool-chaincall
2
+
3
+ Ask a contract what it knows, and what a call would cost.
4
+
5
+ | Tool | Answers |
6
+ |---|---|
7
+ | `EvmMulticall` | many view calls, one request, one block height |
8
+ | `ContractInspect` | is there code here, is it a proxy, what does it claim |
9
+ | `EvmSimulateBundle` | what would this sequence of calls do |
10
+ | `GasMarketRead` | what the fee market is doing, and which fee market this is |
11
+
12
+ All four are read-only. No private key is accepted anywhere, nothing is
13
+ signed, and no transaction is submitted — `eth_sendRawTransaction` and
14
+ `eth_sendTransaction` are not reachable from this package, because every
15
+ method it dispatches passes `chain-adapter-base`'s read-only allowlist at a
16
+ single chokepoint. Simulating a transaction is a read: the node evaluates it
17
+ and throws the result away.
18
+
19
+ ## One block, or it is not a table
20
+
21
+ A hundred separate reads land at a hundred different block heights. The
22
+ answers look like a table and do not add up — a pool's reserves from one
23
+ block and its fee from another price a swap that was never available.
24
+
25
+ `EvmMulticall` packs the calls into a single `aggregate3` through
26
+ Multicall3 (the encoder is `@crewhaus/tool-onchain`'s, not a second copy), so
27
+ one `eth_call` answers all of them at one height. When a batch is too large
28
+ for one request it is split — and then the single-block promise needs
29
+ defending. The block is pinned by riding a Multicall3 self-call to
30
+ `getBlockNumber()` in the first batch: one extra slot instead of an extra
31
+ round trip, and, unlike asking `eth_blockNumber` first, the number it returns
32
+ is the block that batch actually executed at, so the head cannot move between
33
+ the two reads. Every later batch asks for that exact height.
34
+
35
+ If the block cannot be pinned and the batch has to be split, the call is
36
+ **refused**. Answering from two heights and presenting it as one table is the
37
+ bug this tool exists to prevent.
38
+
39
+ **A sub-call that reverts is a row**, with its reason decoded — `Error(string)`,
40
+ a `Panic` with the Solidity code's meaning, or the raw hex for a custom error
41
+ nobody can name. One bad token does not lose the other ninety-nine. What
42
+ throws is the *batch* failing, which means there are no rows at all.
43
+
44
+ **An empty return is flagged, never decoded.** Calling an address with no code
45
+ SUCCEEDS in the EVM and returns nothing. Decode that as `uint256` and you get
46
+ zero, which reads as a balance. Every such row is marked `emptyReturn` and
47
+ left undecoded, and the result says how many there were.
48
+
49
+ ## Verified, and claimed
50
+
51
+ `ContractInspect` splits its answer in two, because they are different kinds
52
+ of fact.
53
+
54
+ **`verified`** is read out of state. Code size from `eth_getCode`. The EIP-1967
55
+ implementation, admin and beacon slots, and the EIP-1822 `keccak256("PROXIABLE")`
56
+ slot, from `eth_getStorageAt`. The EIP-1167 minimal-proxy target out of the
57
+ runtime bytecode itself. A storage slot is not an opinion.
58
+
59
+ **`claimed`** is the contract answering questions about itself. `supportsInterface`
60
+ is self-reported: a contract can claim an interface it does not implement and
61
+ implement one it does not claim. So is `implementation()`, which on a proxy is
62
+ answered by the implementation through the delegate.
63
+
64
+ The compliance check is the part that is usually skipped. ERC-165 requires a
65
+ contract to answer **false** for the reserved id `0xffffffff`. A contract whose
66
+ fallback returns a non-zero word answers *true* to every `supportsInterface`
67
+ call ever made — so its claims carry no information, and a list of eleven
68
+ interfaces it "supports" is eleven wrong facts with nothing to spot them by.
69
+ When the sentinel fails, the claims are **dropped entirely**, not returned as
70
+ an empty list, and the reason is given.
71
+
72
+ Proxy detection has no single specification, so the tool reports signals and
73
+ then tries to reconcile them:
74
+
75
+ - two mechanisms naming **different** implementations → unresolved, both
76
+ addresses shown. Picking one sends the caller to the wrong ABI;
77
+ - a **beacon** whose `implementation()` will not answer → unresolved, naming
78
+ the beacon;
79
+ - an ERC-165 claim of **EIP-2535 DiamondLoupe** → unresolved, because a diamond
80
+ routes every selector to a different facet and there is no single
81
+ implementation. A claim is allowed to withdraw an address here, never to
82
+ supply one;
83
+ - a slot that is set but does **not** hold an address — an EIP-1822
84
+ implementation contract stores its UUID there — is reported as raw storage
85
+ rather than masked down to its low 20 bytes.
86
+
87
+ Only the canonical EIP-1167 bytecode matches. The optimized variants that push
88
+ a shorter address do not, and a near-match is reported as no match: those 45
89
+ bytes are the one place a target can be read with no second call, so a pattern
90
+ that is almost right is a target that is almost right.
91
+
92
+ The probes go out as one Multicall3 batch. Where Multicall3 is not deployed
93
+ the batch fails and they are made one at a time instead — `reads` says which
94
+ happened, because ten sequential round trips against a rate-limited endpoint
95
+ is not the same thing as one.
96
+
97
+ A probe that **reverts** is an answer: a contract that is not ERC-165 reverts
98
+ on `supportsInterface`, one that is not a proxy reverts on `implementation()`.
99
+ A probe the node could not answer at all — it is down, it is rate-limiting, it
100
+ pruned the block — is not, and the two are counted separately. When *every*
101
+ probe fails that way the whole read is **refused**, because the report it would
102
+ otherwise produce ("not a proxy, not ERC-165") is a set of claims about a
103
+ contract nobody managed to ask.
104
+
105
+ The same line is held one probe at a time. When it is the `0xffffffff`
106
+ sentinel that the node would not answer, the claims are still dropped — but
107
+ `claimed.erc165` carries `established: false` and says *why*, because "this
108
+ contract does not implement ERC-165" is a fact about the contract and nothing
109
+ here established it.
110
+
111
+ ## The honest-degradation case
112
+
113
+ A bundle asks what an ordered sequence of calls would do **with state chained**:
114
+ the approve, then the swap that sees it. That needs `eth_simulateV1`, and many
115
+ endpoints do not implement it.
116
+
117
+ `eth_call` cannot chain. Every call it makes starts from an unmodified block,
118
+ so the swap is evaluated against a world where the approval never happened.
119
+ That is a different question. So the fallback says so, in the result, in three
120
+ ways at once:
121
+
122
+ - `mode: "eth_call-fallback"` and `chained: false`;
123
+ - **no `logs` key on any call**, and **no `balanceChanges` key** — absent, not
124
+ empty. An empty log list reads as "this emitted no events", which is the
125
+ reading a policy gate acts on and is wrong about;
126
+ - a `limitations` list naming each thing the answer is missing.
127
+
128
+ The fallback also **pins the height once** before it starts: a block tag sent
129
+ once per `eth_call` is resolved once per `eth_call`, and a head that moves
130
+ mid-loop puts rows from two blocks into one table. One
131
+ `eth_getBlockByNumber` resolves the tag, `blockNumber` reports the height the
132
+ calls actually ran at, and a tag that cannot be resolved is reported as
133
+ unpinned — the limitation then says the calls may not share a block, rather
134
+ than asserting one they may not.
135
+
136
+ The degradation is narrow in both directions. `-32601` and the strings
137
+ providers use instead of it mean the method is unimplemented. A **timeout**,
138
+ a **rejected parameter**, a **rate limit** and a **revert** do not — degrading on
139
+ any of those would turn a transient fault into a permanently weaker answer
140
+ that still reads like an answer. A timed-out simulation is a refusal that says
141
+ so, and no `eth_call` is attempted after it.
142
+
143
+ `allowFallback: false` refuses instead of answering the lesser question.
144
+
145
+ ### Deltas from the chain's own accounting
146
+
147
+ `trackBalances` brackets the bundle with Multicall3 balance reads placed
148
+ *inside* the simulation, so before and after are both measured in the chained
149
+ state. Native balances come from `getEthBalance`, token balances from
150
+ `balanceOf` — not from summing `Transfer` logs, which miss fee-on-transfer and
151
+ rebasing tokens and miss native value moved inside internal frames.
152
+
153
+ ### Absence is not emptiness, in the primary path too
154
+
155
+ The same rule governs a successful `eth_simulateV1`. A node can implement the
156
+ method and still return no log list for a call, and no `gasUsed` figure. Those
157
+ rows come back with **no `logs` key and no `gasUsed` key** rather than `[]` and
158
+ `"0"`, and a `limitations` entry counts them: an empty list is the fact "this
159
+ call emitted nothing", and the two must not arrive looking the same. A call
160
+ result carrying **neither a status nor an error** is refused outright, because
161
+ an outcome the node never stated is not an outcome that held.
162
+
163
+ In fallback mode `trackBalances` is **refused**: there is no after-state to
164
+ difference against, and a plausible delta is worse than none. If the
165
+ bracketing read itself does not execute — Multicall3 not deployed — no deltas
166
+ are reported rather than partial ones, and a single token that cannot be read
167
+ is reported as unreadable rather than as zero.
168
+
169
+ ## Which fee market is this
170
+
171
+ Chains disagree. `GasMarketRead` reports the mechanism rather than assuming
172
+ one:
173
+
174
+ - a block with no `baseFeePerGas` is a **legacy** chain, priced from
175
+ `eth_gasPrice`, with no 1559 projection at all;
176
+ - a node without `eth_feeHistory` is reported as such — no percentiles, rather
177
+ than percentiles from somewhere else;
178
+ - a base fee that is **identical in every sampled block** is flagged. Several
179
+ chains hold it at a floor and price congestion elsewhere; that is not a
180
+ quiet market;
181
+ - an `atPercentile` the node did not answer is **named**, not substituted in
182
+ silence: `planned` carries both `requestedPercentile` and the
183
+ `atPercentile` it was actually priced at, with a caveat. Pricing a request
184
+ for the 90th at the 10th without saying so is how a caller under-tips.
185
+
186
+ The next block's base fee is computed by the EIP-1559 rule from the block's
187
+ **integer** `gasUsed` and `gasLimit` — never from `feeHistory`'s
188
+ `gasUsedRatio`, which is a JSON float that has already lost the low bits — and
189
+ then **compared against the node's own figure**. A disagreement is reported as
190
+ a disagreement: this chain's elasticity or change denominator is not the
191
+ vanilla one, and the node's number is the one to trust.
192
+
193
+ Blob fees are read from the node's tail entry and not recomputed, because
194
+ EIP-4844 prices blob gas with an exponential rather than the 1559 linear rule.
195
+
196
+ Every report carries the rollup caveat, and it is not a guess: the tool checks
197
+ whether the OP-stack `GasPriceOracle` predeploy has code at
198
+ `0x420…0F`. Where it does, the L1 data fee is a real cost — frequently the
199
+ larger share — that **this tool does not price**. It does not read the oracle
200
+ and does not implement the post-Ecotone blob-scalar formula, so every figure
201
+ it returns is execution gas.
202
+
203
+ ## What it refuses
204
+
205
+ A tool that cannot say no is not finished. These say no, with the reason:
206
+
207
+ - **a split batch whose block could not be pinned**, rather than reading from
208
+ two heights into one table;
209
+ - **a result blob whose row count does not match the calls sent** — from
210
+ Multicall3 or from `eth_simulateV1` — because results are positional and
211
+ nothing in them names the call they answer;
212
+ - **a call giving both `data` and `signature`**, which would encode to two
213
+ different things with no way to tell which was meant, and one giving
214
+ neither;
215
+ - **a proxy whose mechanisms disagree**, and a **diamond**, which get no
216
+ implementation address at all;
217
+ - **a simulation that timed out or was rejected**, which is never degraded
218
+ into a fallback, and **a simulated call whose outcome the node did not
219
+ state** — no status and no error — which is never read as a success;
220
+ - **balance tracking without a chained simulation**;
221
+ - **fee percentiles that are not strictly ascending**, which `eth_feeHistory`
222
+ requires, and a **block the node does not have**;
223
+ - **an address that is not 20 hex bytes**, before anything is dialled. A
224
+ lowercase address is answered, with `checksumVerified: false` attached,
225
+ because EIP-55 can only verify a checksum that is there.
226
+
227
+ ## Numbers
228
+
229
+ No chain quantity is ever a JS number. Wei, balances, gas and block numbers
230
+ are `bigint` in memory and **decimal strings** on the way out. A uint256 is 78
231
+ digits and a double carries about 15; the difference is lost silently, and it
232
+ is money.
233
+
234
+ That includes figures merely *derived* from wei. `baseFeeChangeBps` is a
235
+ decimal string too: a chain sitting at a one-wei base-fee floor that then
236
+ spikes produces a ratio past 2^53, and `Number()` renders it `1e+34` — neither
237
+ an integer nor the value that was computed.
238
+
239
+ ## Wiring
240
+
241
+ The transport is bound once at boot, per chain, from the spec's `chains[]`
242
+ block — the same shape `@crewhaus/tool-evm` uses. No caller supplies a URL
243
+ anywhere in this package, and there is no endpoint list inside it.
244
+
245
+ ```ts
246
+ import { setChainRpcResolver, chainRpcFromAdapter } from "@crewhaus/tool-chaincall";
247
+
248
+ setChainRpcResolver((chainId) => {
249
+ const adapter = adapters.get(chainId);
250
+ return adapter === undefined ? undefined : chainRpcFromAdapter(adapter);
251
+ });
252
+ ```
253
+
254
+ `_setRpc` is the test seam beside it: one transport for every chain id,
255
+ `undefined` to restore. Every test in this package drives it, and nothing in
256
+ the suite resolves a name or opens a socket.
257
+
258
+ `timeoutMs` bounds the **tool**, not the socket. `ChainAdapter.rpcRead` takes
259
+ no `AbortSignal`, so a transport built on one cannot be cancelled; the deadline
260
+ is raced against the call so the tool returns on time, and the request
261
+ underneath it may still be in flight. That is the honest limit of a seam whose
262
+ other side does not accept a signal — and it is better than a `timeoutMs` that
263
+ bounds nothing. A cancellation is never mistaken for something else: it is not
264
+ a missing Multicall3 deploy, and it is not a node that cannot simulate.
@@ -0,0 +1,120 @@
1
+ import type { ChainRpc } from "./lib/rpc";
2
+ /** A 32-byte word holding an unsigned integer. */
3
+ export declare function word(value: bigint | number): string;
4
+ /** A 32-byte word holding an address in its low 20 bytes. */
5
+ export declare function addressWord(address: string): string;
6
+ /** A 32-byte word that is not an address: the top bytes are dirty. */
7
+ export declare const NON_ADDRESS_WORD = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bec8";
8
+ export declare const ZERO_WORD: string;
9
+ /**
10
+ * ABI-encode what `aggregate3` returns, through the real encoder.
11
+ *
12
+ * The trick is to encode a call taking exactly that argument and drop the
13
+ * four-byte selector: the remaining bytes are the argument's encoding, which
14
+ * is what a node hands back.
15
+ */
16
+ export declare function aggregate3Return(rows: ReadonlyArray<readonly [boolean, string]>): Promise<string>;
17
+ /** `Error(string)` revert bytes for a given message, through the real encoder. */
18
+ export declare function errorStringRevert(message: string): Promise<string>;
19
+ /** The canonical EIP-1167 runtime bytecode delegating to `target`. */
20
+ export declare function minimalProxyCode(target: string): string;
21
+ /** A JSON-RPC error as a provider throws it: an Error carrying `code`. */
22
+ export declare function jsonRpcError(code: number, message: string, data?: unknown): Error;
23
+ /** What a node says about a method it has never heard of. */
24
+ export declare function methodNotFound(method: string): Error;
25
+ export type RpcHandler = (params: ReadonlyArray<unknown>, call: number) => unknown | Promise<unknown>;
26
+ export type RpcStub = {
27
+ readonly rpc: ChainRpc;
28
+ /** Every dispatch, in order. The "nothing sends" test reads this. */
29
+ readonly calls: Array<{
30
+ method: string;
31
+ params: ReadonlyArray<unknown>;
32
+ }>;
33
+ /** How many times one method was dispatched. */
34
+ count(method: string): number;
35
+ };
36
+ /**
37
+ * A transport built from a table of handlers.
38
+ *
39
+ * An unhandled method answers -32601, which is what a real node does and is
40
+ * also the one failure this package degrades on — so a test that forgets a
41
+ * handler exercises the degradation path loudly rather than hanging.
42
+ */
43
+ export declare function rpcStub(handlers: Readonly<Record<string, RpcHandler>>): RpcStub;
44
+ /** Serve a different blob to each successive `eth_call`, in order. */
45
+ export declare function sequentialCalls(blobs: ReadonlyArray<string>): RpcHandler;
46
+ export type InspectViews = {
47
+ /** What supportsInterface(0xffffffff) answers. A compliant contract: false. */
48
+ readonly sentinel?: boolean | "revert";
49
+ /** Interface ids the contract claims. */
50
+ readonly supports?: ReadonlyArray<string>;
51
+ /** What implementation() answers, when it answers. */
52
+ readonly implementationView?: string;
53
+ /** Whether proxiableUUID() answers at all. */
54
+ readonly proxiable?: boolean;
55
+ /** Present only when the beacon slot is set: the beacon's implementation(). */
56
+ readonly beaconImplementation?: string | "revert";
57
+ };
58
+ /**
59
+ * The view rows `ContractInspect` expects, in the order it builds them:
60
+ * the 0xffffffff sentinel, one row per probed interface id, implementation(),
61
+ * proxiableUUID(), and the beacon's implementation() when there is a beacon.
62
+ */
63
+ export declare function inspectViewRows(spec: InspectViews): Array<readonly [boolean, string]>;
64
+ export type SimulatedCall = {
65
+ readonly returnData?: string;
66
+ readonly gasUsed?: number;
67
+ readonly status?: 0 | 1;
68
+ readonly logs?: ReadonlyArray<{
69
+ address: string;
70
+ topics: string[];
71
+ data: string;
72
+ }>;
73
+ readonly error?: {
74
+ code: number;
75
+ message: string;
76
+ };
77
+ };
78
+ /** One block of `eth_simulateV1` output. */
79
+ export declare function simulateV1Result(blockNumber: number, calls: ReadonlyArray<SimulatedCall>): unknown;
80
+ /**
81
+ * One block of `eth_simulateV1` output with the call objects passed through
82
+ * VERBATIM.
83
+ *
84
+ * `simulateV1Result` fills in `status`, `gasUsed` and `logs` for every call,
85
+ * which makes the case worth testing — a node that leaves one of them out —
86
+ * inexpressible. A fixture that cannot express a shape hides every bug that
87
+ * shape causes.
88
+ */
89
+ export declare function simulateV1Raw(blockNumber: number, calls: ReadonlyArray<Record<string, unknown>>): unknown;
90
+ /** An ERC-20 Transfer log, the shape a simulation returns them in. */
91
+ export declare const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
92
+ export declare function transferLog(token: string, from: string, to: string, amount: bigint): {
93
+ address: string;
94
+ topics: string[];
95
+ data: string;
96
+ };
97
+ export type BlockFixture = {
98
+ readonly number: number;
99
+ readonly gasUsed: bigint;
100
+ readonly gasLimit: bigint;
101
+ readonly baseFeePerGas?: bigint;
102
+ };
103
+ export declare function blockResult(block: BlockFixture): unknown;
104
+ export type FeeHistoryFixture = {
105
+ readonly oldestBlock: number;
106
+ /** blockCount + 1 entries: the last is the NEXT block's base fee. */
107
+ readonly baseFeePerGas: ReadonlyArray<bigint>;
108
+ readonly reward?: ReadonlyArray<ReadonlyArray<bigint>>;
109
+ readonly baseFeePerBlobGas?: ReadonlyArray<bigint>;
110
+ };
111
+ export declare function feeHistoryResult(history: FeeHistoryFixture): unknown;
112
+ export declare const ADDR: Readonly<{
113
+ proxy: "0x1111111111111111111111111111111111111111";
114
+ implementation: "0x2222222222222222222222222222222222222222";
115
+ admin: "0x3333333333333333333333333333333333333333";
116
+ beacon: "0x4444444444444444444444444444444444444444";
117
+ other: "0x5555555555555555555555555555555555555555";
118
+ token: "0x6666666666666666666666666666666666666666";
119
+ wallet: "0x7777777777777777777777777777777777777777";
120
+ }>;
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Recorded node shapes, and a transport that serves them.
3
+ *
4
+ * Nothing in this package's suite opens a socket. Every test installs one of
5
+ * these through `_setRpc`, so the suite runs identically on a CI box with no
6
+ * egress and never depends on somebody else's rate limit or on a public
7
+ * endpoint being up.
8
+ *
9
+ * The return blobs are built with `@crewhaus/tool-onchain`'s real encoder
10
+ * rather than pasted as hex. A hand-typed `(bool,bytes)[]` with a plausible
11
+ * offset in it would make the decoder's own correctness untestable here: the
12
+ * fixture and the bug would agree.
13
+ */
14
+ import { abiEncodeCall } from "@crewhaus/tool-onchain";
15
+ import { KNOWN_INTERFACES } from "./lib/proxy";
16
+ /** A 32-byte word holding an unsigned integer. */
17
+ export function word(value) {
18
+ return `0x${BigInt(value).toString(16).padStart(64, "0")}`;
19
+ }
20
+ /** A 32-byte word holding an address in its low 20 bytes. */
21
+ export function addressWord(address) {
22
+ return `0x${address.replace(/^0x/, "").toLowerCase().padStart(64, "0")}`;
23
+ }
24
+ /** A 32-byte word that is not an address: the top bytes are dirty. */
25
+ export const NON_ADDRESS_WORD = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bec8";
26
+ export const ZERO_WORD = word(0);
27
+ /**
28
+ * ABI-encode what `aggregate3` returns, through the real encoder.
29
+ *
30
+ * The trick is to encode a call taking exactly that argument and drop the
31
+ * four-byte selector: the remaining bytes are the argument's encoding, which
32
+ * is what a node hands back.
33
+ */
34
+ export async function aggregate3Return(rows) {
35
+ const result = await abiEncodeCall.execute({
36
+ signature: "fixture((bool,bytes)[])",
37
+ args: [rows.map(([ok, data]) => [ok, data])],
38
+ });
39
+ if (typeof result !== "string")
40
+ throw new Error("the encoder returned non-text content");
41
+ const { data } = JSON.parse(result);
42
+ return `0x${data.slice(10)}`;
43
+ }
44
+ /** `Error(string)` revert bytes for a given message, through the real encoder. */
45
+ export async function errorStringRevert(message) {
46
+ const result = await abiEncodeCall.execute({ signature: "Error(string)", args: [message] });
47
+ if (typeof result !== "string")
48
+ throw new Error("the encoder returned non-text content");
49
+ return JSON.parse(result).data;
50
+ }
51
+ /** The canonical EIP-1167 runtime bytecode delegating to `target`. */
52
+ export function minimalProxyCode(target) {
53
+ return `0x363d3d373d3d3d363d73${target.replace(/^0x/, "").toLowerCase()}5af43d82803e903d91602b57fd5bf3`;
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // the transport stub
57
+ // ---------------------------------------------------------------------------
58
+ /** A JSON-RPC error as a provider throws it: an Error carrying `code`. */
59
+ export function jsonRpcError(code, message, data) {
60
+ const err = new Error(message);
61
+ err.code = code;
62
+ if (data !== undefined)
63
+ err.data = data;
64
+ return err;
65
+ }
66
+ /** What a node says about a method it has never heard of. */
67
+ export function methodNotFound(method) {
68
+ return jsonRpcError(-32601, `the method ${method} does not exist/is not available`);
69
+ }
70
+ /**
71
+ * A transport built from a table of handlers.
72
+ *
73
+ * An unhandled method answers -32601, which is what a real node does and is
74
+ * also the one failure this package degrades on — so a test that forgets a
75
+ * handler exercises the degradation path loudly rather than hanging.
76
+ */
77
+ export function rpcStub(handlers) {
78
+ const calls = [];
79
+ const perMethod = new Map();
80
+ const rpc = async (method, params) => {
81
+ calls.push({ method, params });
82
+ const seen = perMethod.get(method) ?? 0;
83
+ perMethod.set(method, seen + 1);
84
+ const handler = handlers[method];
85
+ if (handler === undefined)
86
+ throw methodNotFound(method);
87
+ return handler(params, seen);
88
+ };
89
+ return { rpc, calls, count: (method) => perMethod.get(method) ?? 0 };
90
+ }
91
+ /** Serve a different blob to each successive `eth_call`, in order. */
92
+ export function sequentialCalls(blobs) {
93
+ return (_params, index) => {
94
+ const blob = blobs[index];
95
+ if (blob === undefined)
96
+ throw new Error(`the stub has no eth_call blob #${index}`);
97
+ return blob;
98
+ };
99
+ }
100
+ /**
101
+ * The view rows `ContractInspect` expects, in the order it builds them:
102
+ * the 0xffffffff sentinel, one row per probed interface id, implementation(),
103
+ * proxiableUUID(), and the beacon's implementation() when there is a beacon.
104
+ */
105
+ export function inspectViewRows(spec) {
106
+ const claimed = new Set((spec.supports ?? []).map((id) => id.toLowerCase()));
107
+ const boolRow = (value) => [true, word(value ? 1 : 0)];
108
+ const revertRow = [false, "0x"];
109
+ const rows = [
110
+ spec.sentinel === "revert" ? revertRow : boolRow(spec.sentinel ?? false),
111
+ ...KNOWN_INTERFACES.map((i) => spec.sentinel === "revert" ? revertRow : boolRow(claimed.has(i.id.toLowerCase()))),
112
+ spec.implementationView === undefined
113
+ ? revertRow
114
+ : [true, addressWord(spec.implementationView)],
115
+ spec.proxiable === true ? [true, word(1)] : revertRow,
116
+ ];
117
+ if (spec.beaconImplementation !== undefined) {
118
+ rows.push(spec.beaconImplementation === "revert"
119
+ ? revertRow
120
+ : [true, addressWord(spec.beaconImplementation)]);
121
+ }
122
+ return rows;
123
+ }
124
+ /** One block of `eth_simulateV1` output. */
125
+ export function simulateV1Result(blockNumber, calls) {
126
+ return [
127
+ {
128
+ number: `0x${blockNumber.toString(16)}`,
129
+ gasLimit: "0x1c9c380",
130
+ gasUsed: "0x5208",
131
+ calls: calls.map((call) => ({
132
+ returnData: call.returnData ?? "0x",
133
+ gasUsed: `0x${(call.gasUsed ?? 21_000).toString(16)}`,
134
+ status: `0x${(call.status ?? 1).toString(16)}`,
135
+ logs: call.logs ?? [],
136
+ ...(call.error !== undefined ? { error: call.error } : {}),
137
+ })),
138
+ },
139
+ ];
140
+ }
141
+ /**
142
+ * One block of `eth_simulateV1` output with the call objects passed through
143
+ * VERBATIM.
144
+ *
145
+ * `simulateV1Result` fills in `status`, `gasUsed` and `logs` for every call,
146
+ * which makes the case worth testing — a node that leaves one of them out —
147
+ * inexpressible. A fixture that cannot express a shape hides every bug that
148
+ * shape causes.
149
+ */
150
+ export function simulateV1Raw(blockNumber, calls) {
151
+ return [
152
+ {
153
+ number: `0x${blockNumber.toString(16)}`,
154
+ gasLimit: "0x1c9c380",
155
+ gasUsed: "0x5208",
156
+ calls,
157
+ },
158
+ ];
159
+ }
160
+ /** An ERC-20 Transfer log, the shape a simulation returns them in. */
161
+ export const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
162
+ export function transferLog(token, from, to, amount) {
163
+ return {
164
+ address: token,
165
+ topics: [TRANSFER_TOPIC, addressWord(from), addressWord(to)],
166
+ data: word(amount),
167
+ };
168
+ }
169
+ export function blockResult(block) {
170
+ return {
171
+ number: `0x${block.number.toString(16)}`,
172
+ gasUsed: `0x${block.gasUsed.toString(16)}`,
173
+ gasLimit: `0x${block.gasLimit.toString(16)}`,
174
+ ...(block.baseFeePerGas === undefined
175
+ ? {}
176
+ : { baseFeePerGas: `0x${block.baseFeePerGas.toString(16)}` }),
177
+ hash: "0x00000000000000000000000000000000000000000000000000000000000000aa",
178
+ };
179
+ }
180
+ export function feeHistoryResult(history) {
181
+ const hex = (v) => `0x${v.toString(16)}`;
182
+ return {
183
+ oldestBlock: `0x${history.oldestBlock.toString(16)}`,
184
+ baseFeePerGas: history.baseFeePerGas.map(hex),
185
+ // A float on purpose: this is what the wire carries, and the projection
186
+ // must be proven not to use it.
187
+ gasUsedRatio: history.baseFeePerGas.slice(0, -1).map(() => 0.5123456789),
188
+ ...(history.reward === undefined ? {} : { reward: history.reward.map((row) => row.map(hex)) }),
189
+ ...(history.baseFeePerBlobGas === undefined
190
+ ? {}
191
+ : { baseFeePerBlobGas: history.baseFeePerBlobGas.map(hex) }),
192
+ };
193
+ }
194
+ // ---------------------------------------------------------------------------
195
+ // addresses used across the suite
196
+ // ---------------------------------------------------------------------------
197
+ export const ADDR = Object.freeze({
198
+ proxy: "0x1111111111111111111111111111111111111111",
199
+ implementation: "0x2222222222222222222222222222222222222222",
200
+ admin: "0x3333333333333333333333333333333333333333",
201
+ beacon: "0x4444444444444444444444444444444444444444",
202
+ other: "0x5555555555555555555555555555555555555555",
203
+ token: "0x6666666666666666666666666666666666666666",
204
+ wallet: "0x7777777777777777777777777777777777777777",
205
+ });
@@ -0,0 +1,10 @@
1
+ import type { RegisteredTool } from "@crewhaus/tool-catalog";
2
+ export { ChainCallError, type ChainRpc, type ChainRpcResolver, _setRpc, chainRpcFromAdapter, setChainRpcResolver, } from "./lib/rpc";
3
+ export { MULTICALL3_ADDRESS } from "@crewhaus/tool-onchain";
4
+ export { EIP1822_SLOT, EIP1967_ADMIN_SLOT, EIP1967_BEACON_SLOT, EIP1967_IMPLEMENTATION_SLOT, ERC165_INVALID_ID, KNOWN_INTERFACES, } from "./lib/proxy";
5
+ export declare const evmMulticall: RegisteredTool;
6
+ export declare const contractInspect: RegisteredTool;
7
+ export declare const evmSimulateBundle: RegisteredTool;
8
+ export declare const gasMarketRead: RegisteredTool;
9
+ /** Every tool this package registers, in the order a catalog should list them. */
10
+ export declare const CHAINCALL_TOOLS: ReadonlyArray<RegisteredTool>;