@crewhaus/tool-chaincall 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +76 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +50 -15
- package/dist/lib/aggregator.d.ts +24 -0
- package/dist/lib/aggregator.js +70 -0
- package/dist/lib/codec.d.ts +5 -1
- package/dist/lib/codec.js +23 -3
- package/dist/lib/rpc.d.ts +58 -16
- package/dist/lib/rpc.js +105 -24
- package/package.json +13 -8
- package/dist/fixtures.d.ts +0 -120
- package/dist/fixtures.js +0 -205
package/dist/lib/codec.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/** Encode a call from a signature and its arguments. Throws on a bad type. */
|
|
2
2
|
export declare function encodeCallData(signature: string, args: ReadonlyArray<unknown>, what: string): Promise<string>;
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Decode return data against a list of ABI types. An empty list decodes to
|
|
5
|
+
* no values, as it always has: EvmMulticall accepts `outputs: []`, and
|
|
6
|
+
* AbiDecode's own schema (which wants at least one type) is not asked.
|
|
7
|
+
*/
|
|
4
8
|
export declare function decodeValues(types: ReadonlyArray<string>, hex: string, what: string): Promise<unknown[]>;
|
|
5
9
|
export type CheckedAddress = {
|
|
6
10
|
/** EIP-55 mixed case, which is what goes into any output. */
|
package/dist/lib/codec.js
CHANGED
|
@@ -20,9 +20,23 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { abiDecode, abiEncodeCall, addressCheck } from "@crewhaus/tool-onchain";
|
|
22
22
|
import { ChainCallError } from "./rpc";
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Run one of `tool-onchain`'s tools and read its JSON result.
|
|
25
|
+
*
|
|
26
|
+
* The input goes through the tool's own schema first, as it would from a
|
|
27
|
+
* model. Calling `execute` directly skips the executor that normally parses
|
|
28
|
+
* it, and with it the limits the schema carries — `AbiDecode`'s cap on the
|
|
29
|
+
* size of the data it decodes among them — while the data here comes from
|
|
30
|
+
* a node answering for a contract somebody else deployed.
|
|
31
|
+
*/
|
|
24
32
|
async function toolJson(tool, input, what) {
|
|
25
|
-
const
|
|
33
|
+
const parsed = tool.inputSchema.safeParse(input);
|
|
34
|
+
if (!parsed.success) {
|
|
35
|
+
const issue = parsed.error.issues[0];
|
|
36
|
+
const path = issue === undefined || issue.path.length === 0 ? "" : ` (${issue.path.join(".")})`;
|
|
37
|
+
throw new ChainCallError(`${what}: ${tool.name} refused the input${path}: ${issue?.message ?? "invalid"}`);
|
|
38
|
+
}
|
|
39
|
+
const result = await tool.execute(parsed.data);
|
|
26
40
|
if (typeof result !== "string") {
|
|
27
41
|
throw new ChainCallError(`${what}: ${tool.name} returned non-text content`);
|
|
28
42
|
}
|
|
@@ -36,8 +50,14 @@ export async function encodeCallData(signature, args, what) {
|
|
|
36
50
|
throw new ChainCallError(`${what}: could not encode the call`);
|
|
37
51
|
return encoded;
|
|
38
52
|
}
|
|
39
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Decode return data against a list of ABI types. An empty list decodes to
|
|
55
|
+
* no values, as it always has: EvmMulticall accepts `outputs: []`, and
|
|
56
|
+
* AbiDecode's own schema (which wants at least one type) is not asked.
|
|
57
|
+
*/
|
|
40
58
|
export async function decodeValues(types, hex, what) {
|
|
59
|
+
if (types.length === 0)
|
|
60
|
+
return [];
|
|
41
61
|
const out = await toolJson(abiDecode, { data: hex, types }, what);
|
|
42
62
|
const values = out["values"];
|
|
43
63
|
if (!Array.isArray(values))
|
package/dist/lib/rpc.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* does — this package holds no endpoint list of its own and there is no
|
|
17
17
|
* caller-supplied URL anywhere in it.
|
|
18
18
|
*/
|
|
19
|
-
import type { ChainAdapter } from "@crewhaus/chain-adapter-base";
|
|
19
|
+
import type { ChainAdapter, ChainAdapterConfig } from "@crewhaus/chain-adapter-base";
|
|
20
20
|
import { CrewhausError } from "@crewhaus/errors";
|
|
21
21
|
/** A refusal raised by this package: a bad input, a broken promise, a limit. */
|
|
22
22
|
export declare class ChainCallError extends CrewhausError {
|
|
@@ -30,6 +30,12 @@ export declare class ChainCallError extends CrewhausError {
|
|
|
30
30
|
*/
|
|
31
31
|
export type ChainRpc = (method: string, params: ReadonlyArray<unknown>, opts?: {
|
|
32
32
|
readonly signal?: AbortSignal;
|
|
33
|
+
/**
|
|
34
|
+
* What is left of the tool's own deadline, in ms. A transport with a
|
|
35
|
+
* deadline of its own (the chain adapter's defaults to 30 s) uses this
|
|
36
|
+
* instead, so a `timeoutMs` above that default is not silently cut to it.
|
|
37
|
+
*/
|
|
38
|
+
readonly timeoutMs?: number;
|
|
33
39
|
}) => Promise<unknown>;
|
|
34
40
|
/** Boot-time binding: chain id in, transport out. */
|
|
35
41
|
export type ChainRpcResolver = (chainId: string) => ChainRpc | undefined;
|
|
@@ -41,9 +47,19 @@ export declare function setChainRpcResolver(fn: ChainRpcResolver | undefined): v
|
|
|
41
47
|
/**
|
|
42
48
|
* Adapt a `ChainAdapter` into the seam, so the runtime can hand these tools
|
|
43
49
|
* the same adapters `tool-evm` already gets. `rpcRead` keeps its own
|
|
44
|
-
* allowlist and boundary classification; this does not replace either.
|
|
50
|
+
* allowlist and boundary classification; this does not replace either. The
|
|
51
|
+
* call's signal goes through, so a cancelled read closes its request instead
|
|
52
|
+
* of leaving it in flight.
|
|
45
53
|
*/
|
|
46
54
|
export declare function chainRpcFromAdapter(adapter: ChainAdapter): ChainRpc;
|
|
55
|
+
/**
|
|
56
|
+
* Bind the transports from the spec's `chains` block — what every generated
|
|
57
|
+
* bundle, `crewhaus run` and `crewhaus eval` call at boot when a spec lists
|
|
58
|
+
* one of these tools. Each chain gets the adapter `tool-evm` gets.
|
|
59
|
+
*/
|
|
60
|
+
export declare function bindChainCallChains(config: {
|
|
61
|
+
readonly chains: ReadonlyArray<ChainAdapterConfig>;
|
|
62
|
+
}): void;
|
|
47
63
|
/**
|
|
48
64
|
* Test-only injection point, the convention every networked package here
|
|
49
65
|
* follows: one transport for every chain id. `undefined` unbinds, and a
|
|
@@ -62,12 +78,12 @@ export declare function resolveRpc(chainId: string, toolName: string): ChainRpc;
|
|
|
62
78
|
* write-class method reaching this line is a defect in this package and
|
|
63
79
|
* should arrive at the log looking like one.
|
|
64
80
|
*
|
|
65
|
-
* The signal is both passed down AND raced against.
|
|
66
|
-
*
|
|
67
|
-
* `timeoutMs` that bounds nothing is a promise this
|
|
68
|
-
* making. Racing means the TOOL returns on time
|
|
69
|
-
* may still be in flight, which is the honest
|
|
70
|
-
* side does not accept a signal.
|
|
81
|
+
* The signal is both passed down AND raced against. The chain adapter
|
|
82
|
+
* honours it (see `chainRpcFromAdapter`), but a transport bound some other
|
|
83
|
+
* way may not — and a `timeoutMs` that bounds nothing is a promise this
|
|
84
|
+
* package should not be making. Racing means the TOOL returns on time even
|
|
85
|
+
* then; the request underneath may still be in flight, which is the honest
|
|
86
|
+
* limit of a seam whose other side does not accept a signal.
|
|
71
87
|
*/
|
|
72
88
|
export declare function rpcRead(rpc: ChainRpc, chainId: string, method: string, params: ReadonlyArray<unknown>, signal?: AbortSignal): Promise<unknown>;
|
|
73
89
|
/** The JSON-RPC error object, as much of it as a thrown value tends to carry. */
|
|
@@ -82,19 +98,50 @@ export type RpcErrorShape = {
|
|
|
82
98
|
* `.data` attached, some an `Error` wrapping one under `.cause` or `.error`.
|
|
83
99
|
*/
|
|
84
100
|
export declare function rpcError(err: unknown): RpcErrorShape;
|
|
85
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* True when the failure is a cancellation or a deadline — the caller's, the
|
|
103
|
+
* runtime's, or the transport's own (a chain adapter's timeout carries
|
|
104
|
+
* `timedOut`) — rather than anything the node said.
|
|
105
|
+
*/
|
|
86
106
|
export declare function isAbort(err: unknown, signal?: AbortSignal): boolean;
|
|
87
107
|
/**
|
|
88
|
-
*
|
|
108
|
+
* What each node the read was sent to said, one entry per node.
|
|
109
|
+
*
|
|
110
|
+
* The chain adapter reports a read that failed on every node as one error
|
|
111
|
+
* whose words end with the last node's reason; which node failed last is a
|
|
112
|
+
* matter of timing, so nothing here decides on those words. It also carries
|
|
113
|
+
* `attempts`, each node's own failure, and each one that the node answered
|
|
114
|
+
* with a JSON-RPC error carries that error as `rpc`. `answer` is that error,
|
|
115
|
+
* or undefined for a node that answered nothing a JSON-RPC client can read:
|
|
116
|
+
* it could not be reached, answered an HTTP error or a redirect, or the
|
|
117
|
+
* adapter refused what it sent. An adapter error without `attempts` (a quorum
|
|
118
|
+
* lost before every voter was heard, or no node at all) is one entry with no
|
|
119
|
+
* answer. Anything else, a transport bound some other way, is read as the
|
|
120
|
+
* node's own answer, as it always was.
|
|
121
|
+
*/
|
|
122
|
+
export declare function nodeAnswers(err: unknown): ReadonlyArray<{
|
|
123
|
+
readonly answer: RpcErrorShape | undefined;
|
|
124
|
+
readonly text: string;
|
|
125
|
+
}>;
|
|
126
|
+
/**
|
|
127
|
+
* -32601 and the strings providers use instead of it, from EVERY node.
|
|
89
128
|
*
|
|
90
129
|
* This is the one predicate `EvmSimulateBundle` degrades on, so it has to be
|
|
91
130
|
* narrow in both directions. A node that has never heard of `eth_simulateV1`
|
|
92
131
|
* is a reason to answer a lesser question; a node that timed out, or that
|
|
93
132
|
* rejected the parameters, or whose simulation reverted, is NOT — degrading
|
|
94
133
|
* on those would turn a transient fault into a quietly weaker answer that
|
|
95
|
-
* still looks like an answer.
|
|
134
|
+
* still looks like an answer. So with several nodes every one must have said
|
|
135
|
+
* so: one that rejected the parameters, could not be reached, or was not
|
|
136
|
+
* heard may implement the method.
|
|
96
137
|
*/
|
|
97
138
|
export declare function isMethodUnsupported(err: unknown, signal?: AbortSignal): boolean;
|
|
139
|
+
/**
|
|
140
|
+
* The first node, in the order the chain names them, that answered with an
|
|
141
|
+
* error other than "no such method": a node that implements the method and
|
|
142
|
+
* refused this request. Undefined when no node did.
|
|
143
|
+
*/
|
|
144
|
+
export declare function firstRefusal(err: unknown): string | undefined;
|
|
98
145
|
/** A one-line description of a node failure, for a refusal message. */
|
|
99
146
|
export declare function rpcErrorText(err: unknown): string;
|
|
100
147
|
export type Deadline = {
|
|
@@ -102,9 +149,4 @@ export type Deadline = {
|
|
|
102
149
|
/** Clear the timer. Always call it, or the process keeps a handle alive. */
|
|
103
150
|
cancel(): void;
|
|
104
151
|
};
|
|
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
152
|
export declare function startDeadline(ms: number, outer?: AbortSignal): Deadline;
|
package/dist/lib/rpc.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { assertReadOnlyMethod } from "@crewhaus/chain-adapter-base";
|
|
1
|
+
import { CHAINS_BLOCK_EXAMPLE, assertReadOnlyMethod } from "@crewhaus/chain-adapter-base";
|
|
2
|
+
import { createEvmAdapters } from "@crewhaus/chain-adapter-evm";
|
|
2
3
|
import { CrewhausError } from "@crewhaus/errors";
|
|
3
4
|
/** A refusal raised by this package: a bad input, a broken promise, a limit. */
|
|
4
5
|
export class ChainCallError extends CrewhausError {
|
|
@@ -18,10 +19,27 @@ export function setChainRpcResolver(fn) {
|
|
|
18
19
|
/**
|
|
19
20
|
* Adapt a `ChainAdapter` into the seam, so the runtime can hand these tools
|
|
20
21
|
* the same adapters `tool-evm` already gets. `rpcRead` keeps its own
|
|
21
|
-
* allowlist and boundary classification; this does not replace either.
|
|
22
|
+
* allowlist and boundary classification; this does not replace either. The
|
|
23
|
+
* call's signal goes through, so a cancelled read closes its request instead
|
|
24
|
+
* of leaving it in flight.
|
|
22
25
|
*/
|
|
23
26
|
export function chainRpcFromAdapter(adapter) {
|
|
24
|
-
return (method, params) => adapter.rpcRead(method, params
|
|
27
|
+
return (method, params, opts) => adapter.rpcRead(method, params, {
|
|
28
|
+
...(opts?.signal === undefined ? {} : { signal: opts.signal }),
|
|
29
|
+
...(opts?.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Bind the transports from the spec's `chains` block — what every generated
|
|
34
|
+
* bundle, `crewhaus run` and `crewhaus eval` call at boot when a spec lists
|
|
35
|
+
* one of these tools. Each chain gets the adapter `tool-evm` gets.
|
|
36
|
+
*/
|
|
37
|
+
export function bindChainCallChains(config) {
|
|
38
|
+
const adapters = createEvmAdapters(config.chains);
|
|
39
|
+
setChainRpcResolver((chainId) => {
|
|
40
|
+
const adapter = adapters.get(chainId);
|
|
41
|
+
return adapter === undefined ? undefined : chainRpcFromAdapter(adapter);
|
|
42
|
+
});
|
|
25
43
|
}
|
|
26
44
|
/**
|
|
27
45
|
* Test-only injection point, the convention every networked package here
|
|
@@ -35,7 +53,7 @@ export function _setRpc(fn) {
|
|
|
35
53
|
/** Find the transport for a chain, or say what the operator has to wire. */
|
|
36
54
|
export function resolveRpc(chainId, toolName) {
|
|
37
55
|
if (resolver === undefined) {
|
|
38
|
-
throw new ChainCallError(`${toolName}: no chain
|
|
56
|
+
throw new ChainCallError(`${toolName}: no chain is configured. Declare one in the spec — ${CHAINS_BLOCK_EXAMPLE}.`);
|
|
39
57
|
}
|
|
40
58
|
const rpc = resolver(chainId);
|
|
41
59
|
if (rpc === undefined) {
|
|
@@ -58,12 +76,12 @@ function abortError(signal) {
|
|
|
58
76
|
* write-class method reaching this line is a defect in this package and
|
|
59
77
|
* should arrive at the log looking like one.
|
|
60
78
|
*
|
|
61
|
-
* The signal is both passed down AND raced against.
|
|
62
|
-
*
|
|
63
|
-
* `timeoutMs` that bounds nothing is a promise this
|
|
64
|
-
* making. Racing means the TOOL returns on time
|
|
65
|
-
* may still be in flight, which is the honest
|
|
66
|
-
* side does not accept a signal.
|
|
79
|
+
* The signal is both passed down AND raced against. The chain adapter
|
|
80
|
+
* honours it (see `chainRpcFromAdapter`), but a transport bound some other
|
|
81
|
+
* way may not — and a `timeoutMs` that bounds nothing is a promise this
|
|
82
|
+
* package should not be making. Racing means the TOOL returns on time even
|
|
83
|
+
* then; the request underneath may still be in flight, which is the honest
|
|
84
|
+
* limit of a seam whose other side does not accept a signal.
|
|
67
85
|
*/
|
|
68
86
|
export async function rpcRead(rpc, chainId, method, params, signal) {
|
|
69
87
|
assertReadOnlyMethod(chainId, method);
|
|
@@ -71,7 +89,13 @@ export async function rpcRead(rpc, chainId, method, params, signal) {
|
|
|
71
89
|
// reason a node sees one more request.
|
|
72
90
|
if (signal?.aborted === true)
|
|
73
91
|
throw abortError(signal);
|
|
74
|
-
const
|
|
92
|
+
const deadlineAt = signal === undefined ? undefined : DEADLINES.get(signal);
|
|
93
|
+
const call = rpc(method, params, {
|
|
94
|
+
...(signal === undefined ? {} : { signal }),
|
|
95
|
+
// The rest of the tool's budget, so the transport's own deadline is not
|
|
96
|
+
// shorter than the one the caller asked for.
|
|
97
|
+
...(deadlineAt === undefined ? {} : { timeoutMs: Math.max(1, deadlineAt - Date.now()) }),
|
|
98
|
+
});
|
|
75
99
|
if (signal === undefined)
|
|
76
100
|
return call;
|
|
77
101
|
let onAbort;
|
|
@@ -124,27 +148,55 @@ export function rpcError(err) {
|
|
|
124
148
|
const message = record?.["message"];
|
|
125
149
|
return typeof message === "string" ? { message } : { message: String(err) };
|
|
126
150
|
}
|
|
127
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* True when the failure is a cancellation or a deadline — the caller's, the
|
|
153
|
+
* runtime's, or the transport's own (a chain adapter's timeout carries
|
|
154
|
+
* `timedOut`) — rather than anything the node said.
|
|
155
|
+
*/
|
|
128
156
|
export function isAbort(err, signal) {
|
|
129
157
|
if (signal?.aborted === true)
|
|
130
158
|
return true;
|
|
131
159
|
const record = asRecord(err);
|
|
132
|
-
return record?.["name"] === "AbortError" ||
|
|
160
|
+
return (record?.["name"] === "AbortError" ||
|
|
161
|
+
record?.["name"] === "TimeoutError" ||
|
|
162
|
+
record?.["timedOut"] === true);
|
|
133
163
|
}
|
|
134
164
|
/**
|
|
135
|
-
*
|
|
165
|
+
* What each node the read was sent to said, one entry per node.
|
|
136
166
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
167
|
+
* The chain adapter reports a read that failed on every node as one error
|
|
168
|
+
* whose words end with the last node's reason; which node failed last is a
|
|
169
|
+
* matter of timing, so nothing here decides on those words. It also carries
|
|
170
|
+
* `attempts`, each node's own failure, and each one that the node answered
|
|
171
|
+
* with a JSON-RPC error carries that error as `rpc`. `answer` is that error,
|
|
172
|
+
* or undefined for a node that answered nothing a JSON-RPC client can read:
|
|
173
|
+
* it could not be reached, answered an HTTP error or a redirect, or the
|
|
174
|
+
* adapter refused what it sent. An adapter error without `attempts` (a quorum
|
|
175
|
+
* lost before every voter was heard, or no node at all) is one entry with no
|
|
176
|
+
* answer. Anything else, a transport bound some other way, is read as the
|
|
177
|
+
* node's own answer, as it always was.
|
|
143
178
|
*/
|
|
144
|
-
export function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
179
|
+
export function nodeAnswers(err) {
|
|
180
|
+
const record = asRecord(err);
|
|
181
|
+
if (record?.["name"] !== "ChainAdapterError") {
|
|
182
|
+
return [{ answer: rpcError(err), text: rpcErrorText(err) }];
|
|
183
|
+
}
|
|
184
|
+
const attempts = record["attempts"];
|
|
185
|
+
const nodes = Array.isArray(attempts) && attempts.length > 0 ? attempts : [err];
|
|
186
|
+
return nodes.map((node) => {
|
|
187
|
+
const rpc = asRecord(asRecord(node)?.["rpc"]);
|
|
188
|
+
const code = rpc?.["code"];
|
|
189
|
+
const message = rpc?.["message"];
|
|
190
|
+
if (typeof code !== "number") {
|
|
191
|
+
const said = asRecord(node)?.["message"];
|
|
192
|
+
return { answer: undefined, text: typeof said === "string" ? said : String(node) };
|
|
193
|
+
}
|
|
194
|
+
const words = typeof message === "string" ? message : "";
|
|
195
|
+
return { answer: { code, message: words }, text: `${words} (code ${code})` };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/** -32601 and the strings providers use instead of it, in one node's answer. */
|
|
199
|
+
function saysUnsupported({ code, message }) {
|
|
148
200
|
// -32601 is "method not found". -32004 is the "method not supported" code
|
|
149
201
|
// several providers return instead; Geth and Erigon both use -32601.
|
|
150
202
|
if (code === -32601 || code === -32004)
|
|
@@ -159,6 +211,32 @@ export function isMethodUnsupported(err, signal) {
|
|
|
159
211
|
/\bunknown method\b/.test(text) ||
|
|
160
212
|
/\bdoes not exist\/is not available\b/.test(text));
|
|
161
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* -32601 and the strings providers use instead of it, from EVERY node.
|
|
216
|
+
*
|
|
217
|
+
* This is the one predicate `EvmSimulateBundle` degrades on, so it has to be
|
|
218
|
+
* narrow in both directions. A node that has never heard of `eth_simulateV1`
|
|
219
|
+
* is a reason to answer a lesser question; a node that timed out, or that
|
|
220
|
+
* rejected the parameters, or whose simulation reverted, is NOT — degrading
|
|
221
|
+
* on those would turn a transient fault into a quietly weaker answer that
|
|
222
|
+
* still looks like an answer. So with several nodes every one must have said
|
|
223
|
+
* so: one that rejected the parameters, could not be reached, or was not
|
|
224
|
+
* heard may implement the method.
|
|
225
|
+
*/
|
|
226
|
+
export function isMethodUnsupported(err, signal) {
|
|
227
|
+
if (isAbort(err, signal))
|
|
228
|
+
return false;
|
|
229
|
+
return nodeAnswers(err).every(({ answer }) => answer !== undefined && saysUnsupported(answer));
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The first node, in the order the chain names them, that answered with an
|
|
233
|
+
* error other than "no such method": a node that implements the method and
|
|
234
|
+
* refused this request. Undefined when no node did.
|
|
235
|
+
*/
|
|
236
|
+
export function firstRefusal(err) {
|
|
237
|
+
return nodeAnswers(err).find(({ answer }) => answer !== undefined && !saysUnsupported(answer))
|
|
238
|
+
?.text;
|
|
239
|
+
}
|
|
162
240
|
/** A one-line description of a node failure, for a refusal message. */
|
|
163
241
|
export function rpcErrorText(err) {
|
|
164
242
|
const { code, message } = rpcError(err);
|
|
@@ -170,8 +248,11 @@ export function rpcErrorText(err) {
|
|
|
170
248
|
* here opens one before its first byte and cancels it in a `finally`: a tool
|
|
171
249
|
* that fans out over several round trips can hang in several places.
|
|
172
250
|
*/
|
|
251
|
+
/** When each deadline's signal runs out (epoch ms), for `rpcRead` to pass down. */
|
|
252
|
+
const DEADLINES = new WeakMap();
|
|
173
253
|
export function startDeadline(ms, outer) {
|
|
174
254
|
const ctrl = new AbortController();
|
|
255
|
+
DEADLINES.set(ctrl.signal, Date.now() + ms);
|
|
175
256
|
const timer = setTimeout(() => ctrl.abort(new Error(`deadline of ${ms}ms elapsed`)), ms);
|
|
176
257
|
const onOuter = () => ctrl.abort(outer?.reason);
|
|
177
258
|
if (outer !== undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/tool-chaincall",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic EVM contract reads: batched calls, contract introspection, gas market and simulation",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,18 +8,20 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
12
13
|
}
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"test": "bun test src"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"@crewhaus/chain-adapter-base": "0.
|
|
19
|
-
"@crewhaus/
|
|
20
|
-
"@crewhaus/
|
|
21
|
-
"@crewhaus/tool-
|
|
22
|
-
"@crewhaus/tool-
|
|
19
|
+
"@crewhaus/chain-adapter-base": "0.8.0",
|
|
20
|
+
"@crewhaus/chain-adapter-evm": "0.8.0",
|
|
21
|
+
"@crewhaus/errors": "0.8.0",
|
|
22
|
+
"@crewhaus/tool-builder": "0.8.0",
|
|
23
|
+
"@crewhaus/tool-catalog": "0.8.0",
|
|
24
|
+
"@crewhaus/tool-onchain": "0.8.0",
|
|
23
25
|
"zod": "^3.23.8"
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
@@ -43,5 +45,8 @@
|
|
|
43
45
|
"publishConfig": {
|
|
44
46
|
"access": "public"
|
|
45
47
|
},
|
|
46
|
-
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
48
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"],
|
|
49
|
+
"engines": {
|
|
50
|
+
"bun": ">=1.3.14"
|
|
51
|
+
}
|
|
47
52
|
}
|
package/dist/fixtures.d.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
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
|
-
}>;
|