@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.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Telling a proxy from a contract, and a fact from a claim.
3
+ *
4
+ * Proxy detection has no single specification. Four different mechanisms are
5
+ * in wide use and they disagree about where the implementation address even
6
+ * lives, so this file reports SIGNALS and only then tries to reconcile them:
7
+ *
8
+ * - **EIP-1967** puts it in a fixed storage slot. That is state, read with
9
+ * `eth_getStorageAt`, and it is a fact about the contract.
10
+ * - **EIP-1967 beacon** puts a beacon address in a different fixed slot;
11
+ * the implementation is whatever that beacon's `implementation()`
12
+ * returns right now, which makes it a fact plus one hop.
13
+ * - **EIP-1167** carries the target inside the runtime bytecode. Also a
14
+ * fact, and the only one that does not need a second read.
15
+ * - **EIP-1822** uses `keccak256("PROXIABLE")` as the slot.
16
+ *
17
+ * And **EIP-2535 diamonds** have no single implementation at all: each
18
+ * selector routes to a different facet. There is no address to report, so
19
+ * none is invented — the answer is "unresolved, and here is why", because a
20
+ * wrong implementation address means the caller fetches the wrong ABI and
21
+ * every call it composes afterwards is wrong.
22
+ *
23
+ * ERC-165 is on the other side of the line. `supportsInterface` is the
24
+ * contract answering a question about itself, and a contract can lie or
25
+ * simply be wrong. It is never reported as verification.
26
+ */
27
+ /** `keccak256("eip1967.proxy.implementation") - 1`. */
28
+ export declare const EIP1967_IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
29
+ /** `keccak256("eip1967.proxy.admin") - 1`. */
30
+ export declare const EIP1967_ADMIN_SLOT = "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103";
31
+ /** `keccak256("eip1967.proxy.beacon") - 1`. */
32
+ export declare const EIP1967_BEACON_SLOT = "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50";
33
+ /** `keccak256("PROXIABLE")`, the EIP-1822 slot. */
34
+ export declare const EIP1822_SLOT = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bec8";
35
+ /**
36
+ * Read an EIP-1167 clone's target out of its bytecode, or null.
37
+ *
38
+ * Only the canonical form matches. The optimized "vanity" variants that
39
+ * shave bytes by pushing a shorter address do not, and a near-match is
40
+ * reported as no match rather than as a guess: these 45 bytes are the one
41
+ * place a proxy's target can be read with no second call, so a pattern that
42
+ * is almost right is a pattern whose address is almost right.
43
+ */
44
+ export declare function minimalProxyTarget(code: string): string | null;
45
+ /**
46
+ * Interface ids this package probes by default, with what each one means.
47
+ *
48
+ * The ids come from their EIPs. Three of them are single-function
49
+ * interfaces, so the id IS that function's selector and `lib.test.ts`
50
+ * recomputes them; the rest are XORs of several selectors and are carried as
51
+ * published constants.
52
+ */
53
+ export declare const KNOWN_INTERFACES: ReadonlyArray<{
54
+ readonly id: string;
55
+ readonly name: string;
56
+ /** The single function this id is the selector of, when there is one. */
57
+ readonly singleFunction?: string;
58
+ }>;
59
+ /** The DiamondLoupe id, which changes what "the implementation" even means. */
60
+ export declare const DIAMOND_LOUPE_ID = "0x48e2b093";
61
+ /**
62
+ * The reserved id every ERC-165 contract must answer FALSE for.
63
+ *
64
+ * This is the whole compliance test, and skipping it is how a claim list
65
+ * becomes fiction: a contract whose fallback returns a non-zero word answers
66
+ * `true` to every `supportsInterface` call ever made. Its claims then say
67
+ * nothing at all, and a list of eleven interfaces it "supports" is eleven
68
+ * wrong facts that a caller has no way to spot.
69
+ */
70
+ export declare const ERC165_INVALID_ID = "0xffffffff";
71
+ export type ProxySignal = {
72
+ readonly kind: "eip1967" | "eip1967-beacon" | "eip1167-minimal" | "eip1822" | "diamond";
73
+ /** The implementation this signal points at, when it points at one. */
74
+ readonly implementation: string | null;
75
+ /** Where it was read from, in enough detail to go and check. */
76
+ readonly source: string;
77
+ /** Whether the value came from storage/bytecode (fact) or a view call. */
78
+ readonly evidence: "storage" | "bytecode" | "call";
79
+ };
80
+ export type ProxyVerdict = {
81
+ readonly isProxy: boolean;
82
+ readonly resolved: boolean;
83
+ readonly kind: string;
84
+ readonly implementation: string | null;
85
+ readonly signals: ReadonlyArray<ProxySignal>;
86
+ /** Present whenever `resolved` is false, saying what to do instead. */
87
+ readonly unresolvedReason?: string;
88
+ };
89
+ /**
90
+ * Reconcile the signals into one answer, or decline to.
91
+ *
92
+ * Two signals naming two different implementations is the case this exists
93
+ * for. It happens — a clone of a proxy, a half-migrated upgrade, a contract
94
+ * that writes the 1967 slot for tooling while actually dispatching somewhere
95
+ * else — and picking one is how a caller ends up with an ABI that decodes
96
+ * the wrong contract's storage. So: one implementation, or none and a
97
+ * reason.
98
+ */
99
+ export declare function reconcileProxy(signals: ReadonlyArray<ProxySignal>): ProxyVerdict;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Telling a proxy from a contract, and a fact from a claim.
3
+ *
4
+ * Proxy detection has no single specification. Four different mechanisms are
5
+ * in wide use and they disagree about where the implementation address even
6
+ * lives, so this file reports SIGNALS and only then tries to reconcile them:
7
+ *
8
+ * - **EIP-1967** puts it in a fixed storage slot. That is state, read with
9
+ * `eth_getStorageAt`, and it is a fact about the contract.
10
+ * - **EIP-1967 beacon** puts a beacon address in a different fixed slot;
11
+ * the implementation is whatever that beacon's `implementation()`
12
+ * returns right now, which makes it a fact plus one hop.
13
+ * - **EIP-1167** carries the target inside the runtime bytecode. Also a
14
+ * fact, and the only one that does not need a second read.
15
+ * - **EIP-1822** uses `keccak256("PROXIABLE")` as the slot.
16
+ *
17
+ * And **EIP-2535 diamonds** have no single implementation at all: each
18
+ * selector routes to a different facet. There is no address to report, so
19
+ * none is invented — the answer is "unresolved, and here is why", because a
20
+ * wrong implementation address means the caller fetches the wrong ABI and
21
+ * every call it composes afterwards is wrong.
22
+ *
23
+ * ERC-165 is on the other side of the line. `supportsInterface` is the
24
+ * contract answering a question about itself, and a contract can lie or
25
+ * simply be wrong. It is never reported as verification.
26
+ */
27
+ /** `keccak256("eip1967.proxy.implementation") - 1`. */
28
+ export const EIP1967_IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
29
+ /** `keccak256("eip1967.proxy.admin") - 1`. */
30
+ export const EIP1967_ADMIN_SLOT = "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103";
31
+ /** `keccak256("eip1967.proxy.beacon") - 1`. */
32
+ export const EIP1967_BEACON_SLOT = "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50";
33
+ /** `keccak256("PROXIABLE")`, the EIP-1822 slot. */
34
+ export const EIP1822_SLOT = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bec8";
35
+ /**
36
+ * The EIP-1167 minimal proxy's runtime code, with the 20-byte target between
37
+ * the two halves. 45 bytes exactly.
38
+ */
39
+ const EIP1167_PREFIX = "363d3d373d3d3d363d73";
40
+ const EIP1167_SUFFIX = "5af43d82803e903d91602b57fd5bf3";
41
+ /**
42
+ * Read an EIP-1167 clone's target out of its bytecode, or null.
43
+ *
44
+ * Only the canonical form matches. The optimized "vanity" variants that
45
+ * shave bytes by pushing a shorter address do not, and a near-match is
46
+ * reported as no match rather than as a guess: these 45 bytes are the one
47
+ * place a proxy's target can be read with no second call, so a pattern that
48
+ * is almost right is a pattern whose address is almost right.
49
+ */
50
+ export function minimalProxyTarget(code) {
51
+ const body = code.replace(/^0x/, "").toLowerCase();
52
+ if (body.length !== EIP1167_PREFIX.length + 40 + EIP1167_SUFFIX.length)
53
+ return null;
54
+ if (!body.startsWith(EIP1167_PREFIX) || !body.endsWith(EIP1167_SUFFIX))
55
+ return null;
56
+ return `0x${body.slice(EIP1167_PREFIX.length, EIP1167_PREFIX.length + 40)}`;
57
+ }
58
+ /**
59
+ * Interface ids this package probes by default, with what each one means.
60
+ *
61
+ * The ids come from their EIPs. Three of them are single-function
62
+ * interfaces, so the id IS that function's selector and `lib.test.ts`
63
+ * recomputes them; the rest are XORs of several selectors and are carried as
64
+ * published constants.
65
+ */
66
+ export const KNOWN_INTERFACES = Object.freeze([
67
+ { id: "0x01ffc9a7", name: "ERC-165", singleFunction: "supportsInterface(bytes4)" },
68
+ { id: "0x80ac58cd", name: "ERC-721" },
69
+ { id: "0x5b5e139f", name: "ERC-721Metadata" },
70
+ { id: "0x780e9d63", name: "ERC-721Enumerable" },
71
+ { id: "0xd9b67a26", name: "ERC-1155" },
72
+ { id: "0x0e89341c", name: "ERC-1155MetadataURI", singleFunction: "uri(uint256)" },
73
+ {
74
+ id: "0x2a55205a",
75
+ name: "ERC-2981 (royalties)",
76
+ singleFunction: "royaltyInfo(uint256,uint256)",
77
+ },
78
+ { id: "0x49064906", name: "ERC-4906 (metadata update)" },
79
+ { id: "0x1626ba7e", name: "ERC-1271 (contract signatures)" },
80
+ { id: "0x7965db0b", name: "AccessControl" },
81
+ { id: "0x48e2b093", name: "EIP-2535 DiamondLoupe" },
82
+ ]);
83
+ /** The DiamondLoupe id, which changes what "the implementation" even means. */
84
+ export const DIAMOND_LOUPE_ID = "0x48e2b093";
85
+ /**
86
+ * The reserved id every ERC-165 contract must answer FALSE for.
87
+ *
88
+ * This is the whole compliance test, and skipping it is how a claim list
89
+ * becomes fiction: a contract whose fallback returns a non-zero word answers
90
+ * `true` to every `supportsInterface` call ever made. Its claims then say
91
+ * nothing at all, and a list of eleven interfaces it "supports" is eleven
92
+ * wrong facts that a caller has no way to spot.
93
+ */
94
+ export const ERC165_INVALID_ID = "0xffffffff";
95
+ /**
96
+ * Reconcile the signals into one answer, or decline to.
97
+ *
98
+ * Two signals naming two different implementations is the case this exists
99
+ * for. It happens — a clone of a proxy, a half-migrated upgrade, a contract
100
+ * that writes the 1967 slot for tooling while actually dispatching somewhere
101
+ * else — and picking one is how a caller ends up with an ABI that decodes
102
+ * the wrong contract's storage. So: one implementation, or none and a
103
+ * reason.
104
+ */
105
+ export function reconcileProxy(signals) {
106
+ if (signals.length === 0) {
107
+ return { isProxy: false, resolved: true, kind: "none", implementation: null, signals };
108
+ }
109
+ const diamond = signals.find((s) => s.kind === "diamond");
110
+ if (diamond !== undefined) {
111
+ return {
112
+ isProxy: true,
113
+ resolved: false,
114
+ kind: "diamond",
115
+ implementation: null,
116
+ signals,
117
+ unresolvedReason: "this contract CLAIMS the EIP-2535 DiamondLoupe interface (self-reported, via ERC-165), and a diamond routes every selector to a different facet — there is no single implementation to report. Enumerate the facets with facets() and inspect each one. A claim can only make this answer more cautious, never more confident, which is why it is allowed to withdraw an address but never to supply one.",
118
+ };
119
+ }
120
+ const addressed = signals.filter((s) => s.implementation !== null);
121
+ const distinct = [...new Set(addressed.map((s) => s.implementation.toLowerCase()))];
122
+ if (distinct.length === 0) {
123
+ const kinds = signals.map((s) => s.kind).join(", ");
124
+ return {
125
+ isProxy: true,
126
+ resolved: false,
127
+ kind: signals[0]?.kind ?? "unknown",
128
+ implementation: null,
129
+ signals,
130
+ unresolvedReason: `this contract looks like a proxy (${kinds}) but no implementation address could be read from it`,
131
+ };
132
+ }
133
+ if (distinct.length > 1) {
134
+ const detail = addressed.map((s) => `${s.implementation} (${s.source})`).join(" and ");
135
+ return {
136
+ isProxy: true,
137
+ resolved: false,
138
+ kind: "conflicting",
139
+ implementation: null,
140
+ signals,
141
+ unresolvedReason: `two proxy mechanisms on this contract name different implementations — ${detail}. Reporting either one would send you to the wrong ABI, so neither is reported.`,
142
+ };
143
+ }
144
+ const primary = addressed[0];
145
+ return {
146
+ isProxy: true,
147
+ resolved: true,
148
+ kind: primary.kind,
149
+ implementation: primary.implementation,
150
+ signals,
151
+ };
152
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The one place this package talks to a chain, and the gate it talks through.
3
+ *
4
+ * Every JSON-RPC method dispatched from anywhere in this package passes
5
+ * `assertReadOnlyMethod` from `@crewhaus/chain-adapter-base` FIRST. That
6
+ * allowlist is the repo's existing statement of what a read is, and putting
7
+ * it at this chokepoint means a bug three files away cannot dispatch
8
+ * `eth_sendRawTransaction` — it would have to get past a check it never
9
+ * sees. Nothing here signs, and there is nowhere to put a key.
10
+ *
11
+ * The seam is `ChainRpc`: one function, `(method, params) -> result`. It sits
12
+ * ABOVE the dialler on purpose, the same way `tool-registry`'s does, so a
13
+ * test that installs a stub never resolves a name, never opens a socket and
14
+ * never needs egress. The production binding is a resolver the runtime sets
15
+ * at boot from the spec's `chains[]` block, exactly as `@crewhaus/tool-evm`
16
+ * does — this package holds no endpoint list of its own and there is no
17
+ * caller-supplied URL anywhere in it.
18
+ */
19
+ import type { ChainAdapter } from "@crewhaus/chain-adapter-base";
20
+ import { CrewhausError } from "@crewhaus/errors";
21
+ /** A refusal raised by this package: a bad input, a broken promise, a limit. */
22
+ export declare class ChainCallError extends CrewhausError {
23
+ readonly name = "ChainCallError";
24
+ constructor(message: string, cause?: unknown);
25
+ }
26
+ /**
27
+ * A JSON-RPC read. `params` is the wire array; the return is the wire
28
+ * `result`, undecoded, because every caller here wants different bytes out
29
+ * of it.
30
+ */
31
+ export type ChainRpc = (method: string, params: ReadonlyArray<unknown>, opts?: {
32
+ readonly signal?: AbortSignal;
33
+ }) => Promise<unknown>;
34
+ /** Boot-time binding: chain id in, transport out. */
35
+ export type ChainRpcResolver = (chainId: string) => ChainRpc | undefined;
36
+ /**
37
+ * Bind the per-chain transports at boot. A generated daemon calls this once,
38
+ * wiring each declared chain to its adapter; `undefined` unbinds.
39
+ */
40
+ export declare function setChainRpcResolver(fn: ChainRpcResolver | undefined): void;
41
+ /**
42
+ * Adapt a `ChainAdapter` into the seam, so the runtime can hand these tools
43
+ * the same adapters `tool-evm` already gets. `rpcRead` keeps its own
44
+ * allowlist and boundary classification; this does not replace either.
45
+ */
46
+ export declare function chainRpcFromAdapter(adapter: ChainAdapter): ChainRpc;
47
+ /**
48
+ * Test-only injection point, the convention every networked package here
49
+ * follows: one transport for every chain id. `undefined` unbinds, and a
50
+ * suite that sets it must restore it, or the next file in the same bun
51
+ * process inherits a stub.
52
+ */
53
+ export declare function _setRpc(fn: ChainRpc | undefined): void;
54
+ /** Find the transport for a chain, or say what the operator has to wire. */
55
+ export declare function resolveRpc(chainId: string, toolName: string): ChainRpc;
56
+ /**
57
+ * Dispatch one read.
58
+ *
59
+ * The allowlist check is here rather than at each call site because a check
60
+ * you have to remember is a check that gets forgotten. `assertReadOnlyMethod`
61
+ * throws a `ChainAdapterError`, which is deliberately NOT rewrapped: a
62
+ * write-class method reaching this line is a defect in this package and
63
+ * should arrive at the log looking like one.
64
+ *
65
+ * The signal is both passed down AND raced against. `ChainAdapter.rpcRead`
66
+ * takes no signal, so a transport built on one cannot be cancelled — and a
67
+ * `timeoutMs` that bounds nothing is a promise this package should not be
68
+ * making. Racing means the TOOL returns on time; the request underneath it
69
+ * may still be in flight, which is the honest limit of a seam whose other
70
+ * side does not accept a signal.
71
+ */
72
+ export declare function rpcRead(rpc: ChainRpc, chainId: string, method: string, params: ReadonlyArray<unknown>, signal?: AbortSignal): Promise<unknown>;
73
+ /** The JSON-RPC error object, as much of it as a thrown value tends to carry. */
74
+ export type RpcErrorShape = {
75
+ readonly code?: number;
76
+ readonly message?: string;
77
+ readonly data?: unknown;
78
+ };
79
+ /**
80
+ * Pull the JSON-RPC error out of whatever the transport threw. Providers
81
+ * differ: some throw the error object, some an `Error` with `.code` and
82
+ * `.data` attached, some an `Error` wrapping one under `.cause` or `.error`.
83
+ */
84
+ export declare function rpcError(err: unknown): RpcErrorShape;
85
+ /** True when the failure is the caller's or the runtime's cancellation. */
86
+ export declare function isAbort(err: unknown, signal?: AbortSignal): boolean;
87
+ /**
88
+ * -32601 and the strings providers use instead of it.
89
+ *
90
+ * This is the one predicate `EvmSimulateBundle` degrades on, so it has to be
91
+ * narrow in both directions. A node that has never heard of `eth_simulateV1`
92
+ * is a reason to answer a lesser question; a node that timed out, or that
93
+ * rejected the parameters, or whose simulation reverted, is NOT — degrading
94
+ * on those would turn a transient fault into a quietly weaker answer that
95
+ * still looks like an answer.
96
+ */
97
+ export declare function isMethodUnsupported(err: unknown, signal?: AbortSignal): boolean;
98
+ /** A one-line description of a node failure, for a refusal message. */
99
+ export declare function rpcErrorText(err: unknown): string;
100
+ export type Deadline = {
101
+ readonly signal: AbortSignal;
102
+ /** Clear the timer. Always call it, or the process keeps a handle alive. */
103
+ cancel(): void;
104
+ };
105
+ /**
106
+ * A deadline that also honours the runtime's own cancellation. Every tool
107
+ * here opens one before its first byte and cancels it in a `finally`: a tool
108
+ * that fans out over several round trips can hang in several places.
109
+ */
110
+ export declare function startDeadline(ms: number, outer?: AbortSignal): Deadline;
@@ -0,0 +1,190 @@
1
+ import { assertReadOnlyMethod } from "@crewhaus/chain-adapter-base";
2
+ import { CrewhausError } from "@crewhaus/errors";
3
+ /** A refusal raised by this package: a bad input, a broken promise, a limit. */
4
+ export class ChainCallError extends CrewhausError {
5
+ name = "ChainCallError";
6
+ constructor(message, cause) {
7
+ super("tool", message, cause);
8
+ }
9
+ }
10
+ let resolver;
11
+ /**
12
+ * Bind the per-chain transports at boot. A generated daemon calls this once,
13
+ * wiring each declared chain to its adapter; `undefined` unbinds.
14
+ */
15
+ export function setChainRpcResolver(fn) {
16
+ resolver = fn;
17
+ }
18
+ /**
19
+ * Adapt a `ChainAdapter` into the seam, so the runtime can hand these tools
20
+ * the same adapters `tool-evm` already gets. `rpcRead` keeps its own
21
+ * allowlist and boundary classification; this does not replace either.
22
+ */
23
+ export function chainRpcFromAdapter(adapter) {
24
+ return (method, params) => adapter.rpcRead(method, params);
25
+ }
26
+ /**
27
+ * Test-only injection point, the convention every networked package here
28
+ * follows: one transport for every chain id. `undefined` unbinds, and a
29
+ * suite that sets it must restore it, or the next file in the same bun
30
+ * process inherits a stub.
31
+ */
32
+ export function _setRpc(fn) {
33
+ resolver = fn === undefined ? undefined : () => fn;
34
+ }
35
+ /** Find the transport for a chain, or say what the operator has to wire. */
36
+ export function resolveRpc(chainId, toolName) {
37
+ if (resolver === undefined) {
38
+ throw new ChainCallError(`${toolName}: no chain transport is bound — the runtime must call setChainRpcResolver() at boot with the chains from spec.chains[]`);
39
+ }
40
+ const rpc = resolver(chainId);
41
+ if (rpc === undefined) {
42
+ throw new ChainCallError(`${toolName}: no chain is configured for chainId "${chainId}" — declare it in spec.chains[]`);
43
+ }
44
+ return rpc;
45
+ }
46
+ /** The rejection a cancelled read produces. `isAbort` recognises it. */
47
+ function abortError(signal) {
48
+ const reason = signal.reason;
49
+ const message = reason instanceof Error ? reason.message : "the read was cancelled";
50
+ return Object.assign(new Error(message), { name: "AbortError" });
51
+ }
52
+ /**
53
+ * Dispatch one read.
54
+ *
55
+ * The allowlist check is here rather than at each call site because a check
56
+ * you have to remember is a check that gets forgotten. `assertReadOnlyMethod`
57
+ * throws a `ChainAdapterError`, which is deliberately NOT rewrapped: a
58
+ * write-class method reaching this line is a defect in this package and
59
+ * should arrive at the log looking like one.
60
+ *
61
+ * The signal is both passed down AND raced against. `ChainAdapter.rpcRead`
62
+ * takes no signal, so a transport built on one cannot be cancelled — and a
63
+ * `timeoutMs` that bounds nothing is a promise this package should not be
64
+ * making. Racing means the TOOL returns on time; the request underneath it
65
+ * may still be in flight, which is the honest limit of a seam whose other
66
+ * side does not accept a signal.
67
+ */
68
+ export async function rpcRead(rpc, chainId, method, params, signal) {
69
+ assertReadOnlyMethod(chainId, method);
70
+ // Checked before dispatch, not after: a cancelled run should not be the
71
+ // reason a node sees one more request.
72
+ if (signal?.aborted === true)
73
+ throw abortError(signal);
74
+ const call = rpc(method, params, signal === undefined ? {} : { signal });
75
+ if (signal === undefined)
76
+ return call;
77
+ let onAbort;
78
+ try {
79
+ return await Promise.race([
80
+ call,
81
+ new Promise((_, reject) => {
82
+ onAbort = () => reject(abortError(signal));
83
+ signal.addEventListener("abort", onAbort, { once: true });
84
+ }),
85
+ ]);
86
+ }
87
+ finally {
88
+ if (onAbort !== undefined)
89
+ signal.removeEventListener("abort", onAbort);
90
+ }
91
+ }
92
+ function asRecord(value) {
93
+ return typeof value === "object" && value !== null
94
+ ? value
95
+ : undefined;
96
+ }
97
+ /**
98
+ * Pull the JSON-RPC error out of whatever the transport threw. Providers
99
+ * differ: some throw the error object, some an `Error` with `.code` and
100
+ * `.data` attached, some an `Error` wrapping one under `.cause` or `.error`.
101
+ */
102
+ export function rpcError(err) {
103
+ const seen = new Set();
104
+ let node = err;
105
+ for (let depth = 0; depth < 4 && node !== undefined && node !== null; depth++) {
106
+ if (seen.has(node))
107
+ break;
108
+ seen.add(node);
109
+ const record = asRecord(node);
110
+ if (record === undefined)
111
+ break;
112
+ const code = record["code"];
113
+ const message = record["message"];
114
+ if (typeof code === "number") {
115
+ return {
116
+ code,
117
+ ...(typeof message === "string" ? { message } : {}),
118
+ ...(record["data"] !== undefined ? { data: record["data"] } : {}),
119
+ };
120
+ }
121
+ node = record["error"] ?? record["cause"];
122
+ }
123
+ const record = asRecord(err);
124
+ const message = record?.["message"];
125
+ return typeof message === "string" ? { message } : { message: String(err) };
126
+ }
127
+ /** True when the failure is the caller's or the runtime's cancellation. */
128
+ export function isAbort(err, signal) {
129
+ if (signal?.aborted === true)
130
+ return true;
131
+ const record = asRecord(err);
132
+ return record?.["name"] === "AbortError" || record?.["name"] === "TimeoutError";
133
+ }
134
+ /**
135
+ * -32601 and the strings providers use instead of it.
136
+ *
137
+ * This is the one predicate `EvmSimulateBundle` degrades on, so it has to be
138
+ * narrow in both directions. A node that has never heard of `eth_simulateV1`
139
+ * is a reason to answer a lesser question; a node that timed out, or that
140
+ * rejected the parameters, or whose simulation reverted, is NOT — degrading
141
+ * on those would turn a transient fault into a quietly weaker answer that
142
+ * still looks like an answer.
143
+ */
144
+ export function isMethodUnsupported(err, signal) {
145
+ if (isAbort(err, signal))
146
+ return false;
147
+ const { code, message } = rpcError(err);
148
+ // -32601 is "method not found". -32004 is the "method not supported" code
149
+ // several providers return instead; Geth and Erigon both use -32601.
150
+ if (code === -32601 || code === -32004)
151
+ return true;
152
+ if (code !== undefined && code !== -32000 && code !== -32601 && code !== -32004)
153
+ return false;
154
+ const text = (message ?? "").toLowerCase();
155
+ if (text.includes("revert") || text.includes("insufficient funds"))
156
+ return false;
157
+ return (/\bmethod\b[^.]{0,40}\bnot (?:found|supported|available|implemented|enabled)\b/.test(text) ||
158
+ /\bunsupported method\b/.test(text) ||
159
+ /\bunknown method\b/.test(text) ||
160
+ /\bdoes not exist\/is not available\b/.test(text));
161
+ }
162
+ /** A one-line description of a node failure, for a refusal message. */
163
+ export function rpcErrorText(err) {
164
+ const { code, message } = rpcError(err);
165
+ const text = message ?? String(err);
166
+ return code === undefined ? text : `${text} (code ${code})`;
167
+ }
168
+ /**
169
+ * A deadline that also honours the runtime's own cancellation. Every tool
170
+ * here opens one before its first byte and cancels it in a `finally`: a tool
171
+ * that fans out over several round trips can hang in several places.
172
+ */
173
+ export function startDeadline(ms, outer) {
174
+ const ctrl = new AbortController();
175
+ const timer = setTimeout(() => ctrl.abort(new Error(`deadline of ${ms}ms elapsed`)), ms);
176
+ const onOuter = () => ctrl.abort(outer?.reason);
177
+ if (outer !== undefined) {
178
+ if (outer.aborted)
179
+ ctrl.abort(outer.reason);
180
+ else
181
+ outer.addEventListener("abort", onOuter, { once: true });
182
+ }
183
+ return {
184
+ signal: ctrl.signal,
185
+ cancel: () => {
186
+ clearTimeout(timer);
187
+ outer?.removeEventListener("abort", onOuter);
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,68 @@
1
+ import type { RevertReason } from "@crewhaus/tool-onchain";
2
+ /** One call in a bundle. No signature, no nonce, no key: nothing is sent. */
3
+ export type SimCall = {
4
+ readonly from?: string;
5
+ readonly to: string;
6
+ readonly data?: string;
7
+ /** Native value in wei, as a decimal string. */
8
+ readonly value?: string;
9
+ /** Gas cap for this call, as a decimal string. */
10
+ readonly gas?: string;
11
+ };
12
+ export type SimLog = {
13
+ readonly address: string;
14
+ readonly topics: ReadonlyArray<string>;
15
+ readonly data: string;
16
+ };
17
+ export type SimCallResult = {
18
+ readonly index: number;
19
+ readonly status: "success" | "reverted";
20
+ /** Absent when the node returned no gas figure. Never defaulted to "0". */
21
+ readonly gasUsed?: string;
22
+ readonly returnData: string;
23
+ readonly revert?: RevertReason;
24
+ /**
25
+ * Present only when the node returned a log list for this call, and only in
26
+ * `eth_simulateV1` mode. An empty array means "this call emitted nothing";
27
+ * the key being ABSENT means the node did not report logs at all, which is
28
+ * a different fact and must not be collapsed into the first one.
29
+ */
30
+ readonly logs?: ReadonlyArray<SimLog>;
31
+ /** The node's own message for a call that failed before execution. */
32
+ readonly error?: string;
33
+ };
34
+ /** The wire object for one call, with absent fields omitted rather than nulled. */
35
+ export declare function toWireCall(call: SimCall, index: number): Record<string, unknown>;
36
+ export type SimulatedBlock = {
37
+ readonly blockNumber: string;
38
+ readonly calls: ReadonlyArray<SimCallResult>;
39
+ };
40
+ /**
41
+ * Read the one block `eth_simulateV1` returns for a one-block bundle.
42
+ *
43
+ * The call count is checked before anything is read out of it, for the same
44
+ * reason a Multicall3 blob's count is: results are positional, and a node
45
+ * that returned fewer than were sent would otherwise have every later result
46
+ * attributed to the wrong call.
47
+ */
48
+ export declare function parseSimulateV1(raw: unknown, expectedCalls: number): SimulatedBlock;
49
+ /**
50
+ * Read an `eth_call` refusal as a revert when it carries revert bytes.
51
+ *
52
+ * Geth-family nodes answer a reverted `eth_call` with error code 3 and the
53
+ * revert data in `error.data`. Everything else — a bad parameter, a node
54
+ * that cannot serve the block — has no such bytes, and is not a revert.
55
+ */
56
+ export declare function revertFromCallError(err: unknown): RevertReason | undefined;
57
+ /**
58
+ * The chaining limitation, worded for what the fallback actually did.
59
+ *
60
+ * "Every call was evaluated against the same unmodified block" is only true
61
+ * when the calls carried a block NUMBER. Sending a tag once per call lets the
62
+ * head move between them, which is the same silent inconsistency `EvmMulticall`
63
+ * refuses to produce — so when the height could not be pinned the sentence
64
+ * says that instead of asserting a block they may not share.
65
+ */
66
+ export declare function unchainedLimitation(pinnedBlock: string | null): string;
67
+ /** What a fallback result is missing, said in full rather than implied. */
68
+ export declare const FALLBACK_LIMITATIONS: ReadonlyArray<string>;