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