@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 +264 -0
- package/dist/fixtures.d.ts +120 -0
- package/dist/fixtures.js +205 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1231 -0
- package/dist/lib/batch.d.ts +13 -0
- package/dist/lib/batch.js +55 -0
- package/dist/lib/codec.d.ts +49 -0
- package/dist/lib/codec.js +113 -0
- package/dist/lib/fees.d.ts +61 -0
- package/dist/lib/fees.js +113 -0
- package/dist/lib/hex.d.ts +56 -0
- package/dist/lib/hex.js +118 -0
- package/dist/lib/proxy.d.ts +99 -0
- package/dist/lib/proxy.js +152 -0
- package/dist/lib/rpc.d.ts +110 -0
- package/dist/lib/rpc.js +190 -0
- package/dist/lib/simulate.d.ts +68 -0
- package/dist/lib/simulate.js +154 -0
- package/package.json +47 -0
|
@@ -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;
|
package/dist/lib/rpc.js
ADDED
|
@@ -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>;
|