@crewhaus/tool-chaincall 0.7.0 → 0.8.1
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/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CrewHaus Factory
|
|
2
|
+
Copyright 2026 Max Meier
|
|
3
|
+
|
|
4
|
+
This product includes software developed by Max Meier
|
|
5
|
+
(operating as StudioMax — https://studiomax.io) and contributors.
|
|
6
|
+
|
|
7
|
+
The CrewHaus name and logo are trademarks of Max Meier. See
|
|
8
|
+
https://github.com/crewhaus/factory/blob/main/TRADEMARK.md for the
|
|
9
|
+
use-of-marks policy.
|
package/README.md
CHANGED
|
@@ -46,6 +46,32 @@ SUCCEEDS in the EVM and returns nothing. Decode that as `uint256` and you get
|
|
|
46
46
|
zero, which reads as a balance. Every such row is marked `emptyReturn` and
|
|
47
47
|
left undecoded, and the result says how many there were.
|
|
48
48
|
|
|
49
|
+
## Who answers a batch
|
|
50
|
+
|
|
51
|
+
A batch is one `eth_call` to Multicall3, and every row in it — each call's
|
|
52
|
+
return data, whether it succeeded, the block number that pins it — is what
|
|
53
|
+
that contract returns. Any contract can implement `aggregate3` and make rows
|
|
54
|
+
up, so which one answers is the operator's decision, not a call's: batches go
|
|
55
|
+
to the canonical deployment (`0xcA11bde05977b3631167028862bE2a173976CA11`)
|
|
56
|
+
unless `tool_config.chaincall.multicall3` names another for the chain. A
|
|
57
|
+
call's `multicall3Address` is accepted only when it is that same address, and
|
|
58
|
+
anything else is refused before any read. `EvmMulticall`, `ContractInspect`
|
|
59
|
+
(when it batched) and a balance-tracking `EvmSimulateBundle` say who answered:
|
|
60
|
+
`aggregator: { address, source }`, with `source` either `canonical` or
|
|
61
|
+
`config`.
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
tool_config:
|
|
65
|
+
chaincall:
|
|
66
|
+
multicall3: # only where the canonical deployment is absent
|
|
67
|
+
"324": "0xF9cda624FBC7e059355ce98a31693d299FACd963"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`EvmSimulateBundle` runs under `stateOverrides` the caller chose, so its
|
|
71
|
+
results describe the chain as overridden; it says so in `limitations`, and
|
|
72
|
+
names the aggregator when an override rewrote the one that read the tracked
|
|
73
|
+
balances.
|
|
74
|
+
|
|
49
75
|
## Verified, and claimed
|
|
50
76
|
|
|
51
77
|
`ContractInspect` splits its answer in two, because they are different kinds
|
|
@@ -97,10 +123,14 @@ is not the same thing as one.
|
|
|
97
123
|
A probe that **reverts** is an answer: a contract that is not ERC-165 reverts
|
|
98
124
|
on `supportsInterface`, one that is not a proxy reverts on `implementation()`.
|
|
99
125
|
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.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
126
|
+
pruned the block — is not, and the two are counted separately. With several
|
|
127
|
+
nodes in the chain's `rpcUrls`, a probe counts as reverted when every node that
|
|
128
|
+
answered said so; a node that could not be reached counts neither way. A
|
|
129
|
+
`quorum` of two or more nodes stops waiting once no answer can win, so there a
|
|
130
|
+
probe that fails on every node counts as unanswered. When *every* probe fails
|
|
131
|
+
that way the whole read is **refused**, because the report it would otherwise
|
|
132
|
+
produce ("not a proxy, not ERC-165") is a set of claims about a contract nobody
|
|
133
|
+
managed to ask.
|
|
104
134
|
|
|
105
135
|
The same line is held one probe at a time. When it is the `0xffffffff`
|
|
106
136
|
sentinel that the node would not answer, the claims are still dropped — but
|
|
@@ -138,7 +168,10 @@ providers use instead of it mean the method is unimplemented. A **timeout**,
|
|
|
138
168
|
a **rejected parameter**, a **rate limit** and a **revert** do not — degrading on
|
|
139
169
|
any of those would turn a transient fault into a permanently weaker answer
|
|
140
170
|
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.
|
|
171
|
+
so, and no `eth_call` is attempted after it. With several nodes, every one
|
|
172
|
+
must say it lacks the method: one that rejected the bundle, could not be
|
|
173
|
+
reached or was not heard may have it. A `quorum` of two or more nodes stops
|
|
174
|
+
waiting once no answer can win, so it never falls back.
|
|
142
175
|
|
|
143
176
|
`allowFallback: false` refuses instead of answering the lesser question.
|
|
144
177
|
|
|
@@ -174,7 +207,8 @@ one:
|
|
|
174
207
|
- a block with no `baseFeePerGas` is a **legacy** chain, priced from
|
|
175
208
|
`eth_gasPrice`, with no 1559 projection at all;
|
|
176
209
|
- a node without `eth_feeHistory` is reported as such — no percentiles, rather
|
|
177
|
-
than percentiles from somewhere else
|
|
210
|
+
than percentiles from somewhere else. With several nodes, every one must lack
|
|
211
|
+
it, as for the simulation fallback above;
|
|
178
212
|
- a base fee that is **identical in every sampled block** is flagged. Several
|
|
179
213
|
chains hold it at a floor and price congestion elsewhere; that is not a
|
|
180
214
|
quiet market;
|
|
@@ -220,10 +254,32 @@ A tool that cannot say no is not finished. These say no, with the reason:
|
|
|
220
254
|
- **balance tracking without a chained simulation**;
|
|
221
255
|
- **fee percentiles that are not strictly ascending**, which `eth_feeHistory`
|
|
222
256
|
requires, and a **block the node does not have**;
|
|
257
|
+
- **a `multicall3Address` other than the canonical deployment or the one the
|
|
258
|
+
operator configured** for the chain, before anything is dialled;
|
|
259
|
+
- **a node that answers with any redirect**, even to its own address: a chain
|
|
260
|
+
read never follows one, and the error says where it pointed. Under
|
|
261
|
+
`fallback`, the next node named is asked instead. Point `rpcUrls` at the
|
|
262
|
+
address that answers directly;
|
|
223
263
|
- **an address that is not 20 hex bytes**, before anything is dialled. A
|
|
224
264
|
lowercase address is answered, with `checksumVerified: false` attached,
|
|
225
265
|
because EIP-55 can only verify a checksum that is there.
|
|
226
266
|
|
|
267
|
+
## Permission rules
|
|
268
|
+
|
|
269
|
+
A rule's argument for these tools is `<chainId>/<address>`, the chain first
|
|
270
|
+
and written as your spec's `chains[].id` names it:
|
|
271
|
+
`EvmMulticall(8453/0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)` is one contract
|
|
272
|
+
on a chain declared `id: "8453"`, `EvmMulticall(base-mainnet/0x8335…)` the same
|
|
273
|
+
contract on one declared `id: base-mainnet`, and `ContractInspect(**0x8335…)`
|
|
274
|
+
that address on any chain.
|
|
275
|
+
|
|
276
|
+
An allow must be written that way: `*` does not cross the `/`, so
|
|
277
|
+
`alwaysAllow ContractInspect(*)` or `ContractInspect(0x8335…)` grants nothing
|
|
278
|
+
(write `**` for "any"). A deny or ask also fires on the address alone and on
|
|
279
|
+
the chain alone, so `alwaysDeny ContractInspect(0x8335…)` denies that contract
|
|
280
|
+
on every chain. A deny or ask ignores the letter case of the hex: an EIP-55
|
|
281
|
+
address and its lower-case spelling are one account.
|
|
282
|
+
|
|
227
283
|
## Numbers
|
|
228
284
|
|
|
229
285
|
No chain quantity is ever a JS number. Wei, balances, gas and block numbers
|
|
@@ -238,10 +294,22 @@ an integer nor the value that was computed.
|
|
|
238
294
|
|
|
239
295
|
## Wiring
|
|
240
296
|
|
|
241
|
-
The transport is bound once at boot, per chain, from the spec's `chains
|
|
242
|
-
block — the same
|
|
297
|
+
The transport is bound once at boot, per chain, from the spec's `chains`
|
|
298
|
+
block — the same block `@crewhaus/tool-evm` reads. A compiled bundle,
|
|
299
|
+
`crewhaus run` and `crewhaus eval` call `bindChainCallChains` with it; without
|
|
300
|
+
one, every call refuses and names the block to write. No caller supplies a URL
|
|
243
301
|
anywhere in this package, and there is no endpoint list inside it.
|
|
244
302
|
|
|
303
|
+
```yaml
|
|
304
|
+
chains:
|
|
305
|
+
- id: "8453"
|
|
306
|
+
kind: evm
|
|
307
|
+
rpcUrls: [$BASE_RPC_URL] # read from the environment, never compiled in
|
|
308
|
+
finality: { kind: finalized }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
A host that builds its own adapters binds them directly:
|
|
312
|
+
|
|
245
313
|
```ts
|
|
246
314
|
import { setChainRpcResolver, chainRpcFromAdapter } from "@crewhaus/tool-chaincall";
|
|
247
315
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RegisteredTool } from "@crewhaus/tool-catalog";
|
|
2
|
-
export { ChainCallError, type ChainRpc, type ChainRpcResolver, _setRpc, chainRpcFromAdapter, setChainRpcResolver, } from "./lib/rpc";
|
|
2
|
+
export { ChainCallError, type ChainRpc, type ChainRpcResolver, _setRpc, bindChainCallChains, chainRpcFromAdapter, setChainRpcResolver, } from "./lib/rpc";
|
|
3
3
|
export { MULTICALL3_ADDRESS } from "@crewhaus/tool-onchain";
|
|
4
|
+
export { type Aggregator, type ChaincallConfigInput, registerChaincallConfig, } from "./lib/aggregator";
|
|
4
5
|
export { EIP1822_SLOT, EIP1967_ADMIN_SLOT, EIP1967_BEACON_SLOT, EIP1967_IMPLEMENTATION_SLOT, ERC165_INVALID_ID, KNOWN_INTERFACES, } from "./lib/proxy";
|
|
5
6
|
export declare const evmMulticall: RegisteredTool;
|
|
6
7
|
export declare const contractInspect: RegisteredTool;
|
package/dist/index.js
CHANGED
|
@@ -38,15 +38,17 @@
|
|
|
38
38
|
import { buildTool } from "@crewhaus/tool-builder";
|
|
39
39
|
import { MULTICALL3_ADDRESS, decodeAggregate3, decodeRevertData, encodeAggregate3, } from "@crewhaus/tool-onchain";
|
|
40
40
|
import { z } from "zod";
|
|
41
|
+
import { resolveAggregator } from "./lib/aggregator";
|
|
41
42
|
import { chunk, runAggregate3 } from "./lib/batch";
|
|
42
43
|
import { SELECTORS, balanceOfData, checkAddress, decodeValues, encodeCallData, getEthBalanceData, supportsInterfaceData, } from "./lib/codec";
|
|
43
44
|
import { changeBps, nextBaseFee, summarisePercentile } from "./lib/fees";
|
|
44
45
|
import { ZERO_ADDRESS, data as asData, blockParam, byteLength, isZeroWord, optionalQuantity, quantity, toQuantity, wordAt, wordToAddress, wordToBigint, wordToBool, } from "./lib/hex";
|
|
45
46
|
import { DIAMOND_LOUPE_ID, EIP1822_SLOT, EIP1967_ADMIN_SLOT, EIP1967_BEACON_SLOT, EIP1967_IMPLEMENTATION_SLOT, ERC165_INVALID_ID, KNOWN_INTERFACES, minimalProxyTarget, reconcileProxy, } from "./lib/proxy";
|
|
46
|
-
import { ChainCallError, isAbort, isMethodUnsupported,
|
|
47
|
+
import { ChainCallError, firstRefusal, isAbort, isMethodUnsupported, nodeAnswers, resolveRpc, rpcErrorText, rpcRead, startDeadline, } from "./lib/rpc";
|
|
47
48
|
import { FALLBACK_LIMITATIONS, parseSimulateV1, revertFromCallError, toWireCall, unchainedLimitation, } from "./lib/simulate";
|
|
48
|
-
export { ChainCallError, _setRpc, chainRpcFromAdapter, setChainRpcResolver, } from "./lib/rpc";
|
|
49
|
+
export { ChainCallError, _setRpc, bindChainCallChains, chainRpcFromAdapter, setChainRpcResolver, } from "./lib/rpc";
|
|
49
50
|
export { MULTICALL3_ADDRESS } from "@crewhaus/tool-onchain";
|
|
51
|
+
export { registerChaincallConfig, } from "./lib/aggregator";
|
|
50
52
|
export { EIP1822_SLOT, EIP1967_ADMIN_SLOT, EIP1967_BEACON_SLOT, EIP1967_IMPLEMENTATION_SLOT, ERC165_INVALID_ID, KNOWN_INTERFACES, } from "./lib/proxy";
|
|
51
53
|
const json = (value) => JSON.stringify(value);
|
|
52
54
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -97,7 +99,7 @@ const multicallAddressField = z
|
|
|
97
99
|
.string()
|
|
98
100
|
.regex(/^0x[0-9a-fA-F]{40}$/)
|
|
99
101
|
.optional()
|
|
100
|
-
.describe(`
|
|
102
|
+
.describe(`leave it out: batches go to the canonical Multicall3 (${MULTICALL3_ADDRESS}) or the one the operator configured for the chain, and any other address is refused`);
|
|
101
103
|
/** The block parameter, and whether the caller pinned it themselves. */
|
|
102
104
|
function blockOf(input) {
|
|
103
105
|
return {
|
|
@@ -147,6 +149,7 @@ async function calldataFor(call, index) {
|
|
|
147
149
|
}
|
|
148
150
|
export const evmMulticall = buildTool({
|
|
149
151
|
name: "EvmMulticall",
|
|
152
|
+
operativeArgs: [{ field: "calls.target", kind: "id", within: "chainId" }],
|
|
150
153
|
description: "Run many view calls against a chain in one request through Multicall3, returning one decoded row per call at a SINGLE block height. Use it for any question that reads more than one contract — a hundred balances, a token's whole metadata, a pool's reserves plus its fee — because a hundred separate reads land at a hundred different block heights and the answers do not add up. A sub-call that reverts is a row with its revert reason, not a failed request, so one bad token does not lose the other ninety-nine. When a batch is too large for one request it is split, and the block is PINNED from the first batch so every row still answers at the same height. A call that succeeds with empty return data is flagged rather than decoded: in the EVM, calling an address with no code succeeds and returns nothing, which is how a missing contract becomes a zero balance. It reads only, and nothing here signs or submits anything.",
|
|
151
154
|
inputSchema: z
|
|
152
155
|
.object({
|
|
@@ -170,7 +173,10 @@ export const evmMulticall = buildTool({
|
|
|
170
173
|
const deadline = startDeadline(input.timeoutMs ?? DEFAULT_TIMEOUT_MS, ctx?.signal);
|
|
171
174
|
try {
|
|
172
175
|
const rpc = resolveRpc(input.chainId, "EvmMulticall");
|
|
173
|
-
|
|
176
|
+
// Resolved before any read: an aggregator the operator did not name
|
|
177
|
+
// is refused, not asked.
|
|
178
|
+
const aggregator = resolveAggregator(input.chainId, input.multicall3Address, ctx?.toolConfig, "EvmMulticall");
|
|
179
|
+
const multicall3 = aggregator.address;
|
|
174
180
|
const { param, pinnedByCaller } = blockOf(input);
|
|
175
181
|
const calls = [];
|
|
176
182
|
for (const [index, call] of input.calls.entries()) {
|
|
@@ -270,6 +276,8 @@ export const evmMulticall = buildTool({
|
|
|
270
276
|
return json({
|
|
271
277
|
chainId: input.chainId,
|
|
272
278
|
multicall3,
|
|
279
|
+
// Who answered every row, since each one is its word.
|
|
280
|
+
aggregator,
|
|
273
281
|
block: {
|
|
274
282
|
requested: param,
|
|
275
283
|
number: pinned === undefined ? null : pinned.toString(),
|
|
@@ -306,10 +314,14 @@ export const evmMulticall = buildTool({
|
|
|
306
314
|
function looksLikeRevert(err) {
|
|
307
315
|
if (revertFromCallError(err) !== undefined)
|
|
308
316
|
return true;
|
|
309
|
-
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
317
|
+
// Every node that answered must have said it reverted, and one must have
|
|
318
|
+
// answered: a node that could not be reached taught nothing either way, and
|
|
319
|
+
// one that answered anything else contradicts the revert. Geth answers a
|
|
320
|
+
// reverted eth_call with code 3; nodes that do not attach revert data still
|
|
321
|
+
// say so in the message.
|
|
322
|
+
const answers = nodeAnswers(err).flatMap(({ answer }) => (answer === undefined ? [] : [answer]));
|
|
323
|
+
return (answers.length > 0 &&
|
|
324
|
+
answers.every(({ code, message }) => code === 3 || /revert|out of gas|invalid opcode/i.test(message ?? "")));
|
|
313
325
|
}
|
|
314
326
|
async function runViews(rpc, chainId, views, multicall3, block, signal) {
|
|
315
327
|
try {
|
|
@@ -382,7 +394,8 @@ function addressResult(row, what) {
|
|
|
382
394
|
}
|
|
383
395
|
export const contractInspect = buildTool({
|
|
384
396
|
name: "ContractInspect",
|
|
385
|
-
|
|
397
|
+
operativeArgs: [{ field: "address", kind: "id", within: "chainId" }],
|
|
398
|
+
description: "Ask an address what it is before calling it: whether there is code there, how big it is, whether it is a proxy and what it delegates to, and which ERC-165 interfaces it claims. Use it whenever an address arrives from somewhere you did not write: a proxy's ABI is the implementation's, and calling the wrong ABI produces calldata a node accepts and a contract misreads. The answer separates what was VERIFIED from what the contract CLAIMS: a proxy's implementation slot is storage, read with eth_getStorageAt, while supportsInterface is the contract answering about itself, and a contract can lie. A contract claiming the reserved 0xffffffff interface id is reported as non-compliant and its claims are dropped. A proxy whose mechanisms disagree, or a diamond with no single implementation, comes back unresolved with the reason instead of a guessed address. Read-only: it never signs, sends or deploys.",
|
|
386
399
|
inputSchema: z
|
|
387
400
|
.object({
|
|
388
401
|
chainId: chainIdField,
|
|
@@ -407,9 +420,10 @@ export const contractInspect = buildTool({
|
|
|
407
420
|
const deadline = startDeadline(input.timeoutMs ?? DEFAULT_TIMEOUT_MS, ctx?.signal);
|
|
408
421
|
try {
|
|
409
422
|
const rpc = resolveRpc(input.chainId, "ContractInspect");
|
|
423
|
+
const aggregator = resolveAggregator(input.chainId, input.multicall3Address, ctx?.toolConfig, "ContractInspect");
|
|
410
424
|
const target = await checkAddress(input.address, "address");
|
|
411
425
|
const { param } = blockOf(input);
|
|
412
|
-
const multicall3 =
|
|
426
|
+
const multicall3 = aggregator.address;
|
|
413
427
|
const caveats = [];
|
|
414
428
|
const code = asData(await rpcRead(rpc, input.chainId, "eth_getCode", [target.address, param], deadline.signal), "eth_getCode");
|
|
415
429
|
const codeSize = byteLength(code);
|
|
@@ -558,6 +572,8 @@ export const contractInspect = buildTool({
|
|
|
558
572
|
address: target.address,
|
|
559
573
|
checksumVerified: target.checksumVerified,
|
|
560
574
|
block: { requested: param },
|
|
575
|
+
// Who answered the batched view calls, when they were batched.
|
|
576
|
+
aggregator: mode === "multicall3" ? aggregator : null,
|
|
561
577
|
verified: {
|
|
562
578
|
isContract: true,
|
|
563
579
|
codeSize,
|
|
@@ -668,7 +684,8 @@ function readProbeBlob(returnData, count) {
|
|
|
668
684
|
}
|
|
669
685
|
export const evmSimulateBundle = buildTool({
|
|
670
686
|
name: "EvmSimulateBundle",
|
|
671
|
-
|
|
687
|
+
operativeArgs: [{ field: "calls.to", kind: "id", within: "chainId" }],
|
|
688
|
+
description: "Simulate an ordered sequence of calls against a block and report what each would do (status, gas, return data, revert reason, logs) without submitting anything. Use it to check a plan before approving it: an approve then a swap, a multi-step position change, a governance execution. State CHAINS, so each call sees what the one before it did, and that needs eth_simulateV1. When the endpoint lacks it, this falls back to independent eth_calls, says so in `mode`, sets `chained: false`, and OMITS logs and balance deltas rather than report a half-true effects summary a policy gate would trust. A timeout or a rejected parameter is never degraded into that fallback. Optional balance tracking brackets the bundle with Multicall3 balance reads, so native and ERC-20 deltas come from the chain's own accounting rather than from summing Transfer logs. It simulates only: no key is accepted, nothing is signed, and nothing is broadcast.",
|
|
672
689
|
inputSchema: z
|
|
673
690
|
.object({
|
|
674
691
|
chainId: chainIdField,
|
|
@@ -719,7 +736,12 @@ export const evmSimulateBundle = buildTool({
|
|
|
719
736
|
try {
|
|
720
737
|
const rpc = resolveRpc(input.chainId, "EvmSimulateBundle");
|
|
721
738
|
const { param } = blockOf(input);
|
|
722
|
-
|
|
739
|
+
// Multicall3 reads the tracked balances, and nothing else here; without
|
|
740
|
+
// trackBalances a multicall3Address is ignored, as it always was.
|
|
741
|
+
const aggregator = input.trackBalances === undefined
|
|
742
|
+
? undefined
|
|
743
|
+
: resolveAggregator(input.chainId, input.multicall3Address, ctx?.toolConfig, "EvmSimulateBundle");
|
|
744
|
+
const multicall3 = aggregator?.address ?? MULTICALL3_ADDRESS;
|
|
723
745
|
const traceTransfers = input.traceTransfers ?? true;
|
|
724
746
|
const userCalls = input.calls.map((call, index) => toWireCall(call, index));
|
|
725
747
|
let probes = [];
|
|
@@ -760,7 +782,11 @@ export const evmSimulateBundle = buildTool({
|
|
|
760
782
|
throw new ChainCallError("EvmSimulateBundle: the call was cancelled — the deadline elapsed, or the run was cancelled — before the node answered eth_simulateV1. This is NOT treated as an unimplemented method, so no eth_call fallback was attempted; raise timeoutMs or retry.", err);
|
|
761
783
|
}
|
|
762
784
|
if (!isMethodUnsupported(err, deadline.signal)) {
|
|
763
|
-
|
|
785
|
+
const refusal = firstRefusal(err);
|
|
786
|
+
const one = nodeAnswers(err).length === 1;
|
|
787
|
+
throw new ChainCallError(refusal !== undefined
|
|
788
|
+
? `EvmSimulateBundle: ${one ? "the node" : "a node"} refused this bundle — ${refusal}. ${one ? "The node" : "That node"} implements eth_simulateV1, so this is an answer about the bundle and not a reason to fall back to eth_call.`
|
|
789
|
+
: `EvmSimulateBundle: no node simulated this bundle — ${rpcErrorText(err)}. The eth_call fallback is used only when every node says it lacks eth_simulateV1, and not every node did, so it was not used. Retry, or check the nodes in the chains block.`, err);
|
|
764
790
|
}
|
|
765
791
|
if (input.allowFallback === false) {
|
|
766
792
|
throw new ChainCallError(`EvmSimulateBundle: this endpoint does not implement eth_simulateV1 (${rpcErrorText(err)}) and allowFallback is false. The eth_call fallback cannot chain state between calls, so it answers a different question; re-run with allowFallback, or point at an endpoint that implements eth_simulateV1.`, err);
|
|
@@ -775,6 +801,14 @@ export const evmSimulateBundle = buildTool({
|
|
|
775
801
|
const inner = tracking ? block.calls.slice(1, -1) : block.calls;
|
|
776
802
|
const calls = inner.map((call, index) => ({ ...call, index }));
|
|
777
803
|
const limitations = [];
|
|
804
|
+
const overridden = Object.keys(input.stateOverrides ?? {});
|
|
805
|
+
if (overridden.length > 0) {
|
|
806
|
+
const aggregatorOverridden = aggregator !== undefined &&
|
|
807
|
+
overridden.some((a) => a.toLowerCase() === aggregator.address.toLowerCase());
|
|
808
|
+
limitations.push(`stateOverrides rewrote ${overridden.length} address(es) for this simulation, so every result describes the chain as overridden, not as it is${aggregatorOverridden
|
|
809
|
+
? ` — including the Multicall3 (${aggregator.address}) that read the tracked balances, so balanceChanges are whatever the override's code returns`
|
|
810
|
+
: ""}.`);
|
|
811
|
+
}
|
|
778
812
|
if (traceTransfers) {
|
|
779
813
|
limitations.push("traceTransfers is on, so native value movements appear as synthetic ERC-20-shaped Transfer logs emitted by the zero address. They are not events any contract actually emitted.");
|
|
780
814
|
}
|
|
@@ -801,7 +835,7 @@ export const evmSimulateBundle = buildTool({
|
|
|
801
835
|
callCount: calls.length,
|
|
802
836
|
reverted: calls.filter((c) => c.status === "reverted").length,
|
|
803
837
|
calls,
|
|
804
|
-
...(balanceChanges !== undefined ? { balanceChanges } : {}),
|
|
838
|
+
...(balanceChanges !== undefined ? { balanceChanges, aggregator } : {}),
|
|
805
839
|
...(limitations.length > 0 ? { limitations } : {}),
|
|
806
840
|
});
|
|
807
841
|
}
|
|
@@ -958,7 +992,8 @@ async function resolveFallbackBlock(rpc, chainId, block, signal) {
|
|
|
958
992
|
const OP_GAS_PRICE_ORACLE = "0x420000000000000000000000000000000000000F";
|
|
959
993
|
export const gasMarketRead = buildTool({
|
|
960
994
|
name: "GasMarketRead",
|
|
961
|
-
|
|
995
|
+
operativeArgs: [{ field: "chainId", kind: "id" }],
|
|
996
|
+
description: "Read a chain's fee market: the current base fee, priority-fee percentiles from eth_feeHistory over a window of blocks, and the next block's base fee by the EIP-1559 rule. Use it to decide when to send and what to pay, instead of a model recalling a gwei figure. It reports which mechanism the chain runs rather than assuming one: a chain with no baseFeePerGas is reported as legacy, a node without eth_feeHistory is reported as such rather than filled in, and a base fee that never moves is flagged rather than trended. The projection uses the block's integer gasUsed and gasLimit, never feeHistory's floating-point ratio, and is compared with the node's own next-block figure, so a non-standard elasticity shows as a disagreement. It does not price rollup L1 data fees, and says so: on an OP-stack chain it checks for the GasPriceOracle predeploy and warns that the unreported cost is often the larger one.",
|
|
962
997
|
inputSchema: z
|
|
963
998
|
.object({
|
|
964
999
|
chainId: chainIdField,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Who served a batch, as every batched answer reports it. */
|
|
2
|
+
export type Aggregator = {
|
|
3
|
+
/** EIP-55. */
|
|
4
|
+
readonly address: string;
|
|
5
|
+
/** `canonical`: the deterministic deploy. `config`: the operator's tool_config. */
|
|
6
|
+
readonly source: "canonical" | "config";
|
|
7
|
+
};
|
|
8
|
+
/** The spec's `tool_config.chaincall` block. */
|
|
9
|
+
export type ChaincallConfigInput = {
|
|
10
|
+
/** chain id → the Multicall3 deployment batches on that chain go to. */
|
|
11
|
+
readonly multicall3?: Readonly<Record<string, string>>;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Apply `tool_config.chaincall` at boot: the Multicall3 deployments in
|
|
15
|
+
* `multicall3`, each held to the address check (shape and EIP-55 checksum).
|
|
16
|
+
* A block without it leaves every chain on the canonical deployment.
|
|
17
|
+
*/
|
|
18
|
+
export declare function registerChaincallConfig(input: ChaincallConfigInput): void;
|
|
19
|
+
/**
|
|
20
|
+
* The aggregator a batch on `chainId` goes to, and where that came from. A
|
|
21
|
+
* `requested` address other than that one is refused: which contract answers
|
|
22
|
+
* every row is not a per-call choice.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveAggregator(chainId: string, requested: string | undefined, toolConfig: unknown, toolName: string): Aggregator;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Multicall3 answers a batch — decided by the operator, never by a call.
|
|
3
|
+
*
|
|
4
|
+
* A batch is ONE `eth_call` to Multicall3, and every row in it — each view
|
|
5
|
+
* call's return data, whether it succeeded, the block number the batch pins —
|
|
6
|
+
* is whatever that contract returns. Any contract can implement `aggregate3`
|
|
7
|
+
* and return rows it made up. So a model-supplied `multicall3Address` was a
|
|
8
|
+
* way to make EvmMulticall report a balance nobody holds as `success: true`,
|
|
9
|
+
* ContractInspect report interfaces a contract never claimed, and
|
|
10
|
+
* EvmSimulateBundle's balance tracking report deltas that never happened.
|
|
11
|
+
*
|
|
12
|
+
* The aggregator is the canonical deployment unless the operator names
|
|
13
|
+
* another for that chain in `tool_config.chaincall.multicall3` (a chain whose
|
|
14
|
+
* Multicall3 is elsewhere, such as zkSync Era). A call may still pass
|
|
15
|
+
* `multicall3Address`, which older callers do, but only the address that
|
|
16
|
+
* would be used anyway is accepted; anything else is refused before a read.
|
|
17
|
+
* This is `@crewhaus/tool-token`'s rule, applied to this package's batches.
|
|
18
|
+
*/
|
|
19
|
+
import { MULTICALL3_ADDRESS, parseMulticallMap } from "@crewhaus/tool-onchain";
|
|
20
|
+
import { ChainCallError } from "./rpc";
|
|
21
|
+
/**
|
|
22
|
+
* The operator's Multicall3 deployments by chain id, from
|
|
23
|
+
* `tool_config.chaincall.multicall3`. Only {@link registerChaincallConfig}
|
|
24
|
+
* writes it.
|
|
25
|
+
*/
|
|
26
|
+
let configured = new Map();
|
|
27
|
+
function parseChaincallMulticall(input) {
|
|
28
|
+
try {
|
|
29
|
+
return parseMulticallMap(input, "tool_config.chaincall.multicall3");
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
throw new ChainCallError(err.message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Apply `tool_config.chaincall` at boot: the Multicall3 deployments in
|
|
37
|
+
* `multicall3`, each held to the address check (shape and EIP-55 checksum).
|
|
38
|
+
* A block without it leaves every chain on the canonical deployment.
|
|
39
|
+
*/
|
|
40
|
+
export function registerChaincallConfig(input) {
|
|
41
|
+
const block = (input ?? {});
|
|
42
|
+
configured = parseChaincallMulticall(block["multicall3"]);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The map one call runs under: the serving candidate's own `tool_config`
|
|
46
|
+
* block when it has one (it replaces the boot registration, as every
|
|
47
|
+
* tool_config block does), else the boot registration.
|
|
48
|
+
*/
|
|
49
|
+
function mapFor(toolConfig) {
|
|
50
|
+
if (typeof toolConfig === "object" && toolConfig !== null && !Array.isArray(toolConfig)) {
|
|
51
|
+
return parseChaincallMulticall(toolConfig["multicall3"]);
|
|
52
|
+
}
|
|
53
|
+
return configured;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The aggregator a batch on `chainId` goes to, and where that came from. A
|
|
57
|
+
* `requested` address other than that one is refused: which contract answers
|
|
58
|
+
* every row is not a per-call choice.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveAggregator(chainId, requested, toolConfig, toolName) {
|
|
61
|
+
const override = mapFor(toolConfig).get(chainId);
|
|
62
|
+
const aggregator = override === undefined
|
|
63
|
+
? { address: MULTICALL3_ADDRESS, source: "canonical" }
|
|
64
|
+
: { address: override, source: "config" };
|
|
65
|
+
if (requested !== undefined &&
|
|
66
|
+
requested.trim().toLowerCase() !== aggregator.address.toLowerCase()) {
|
|
67
|
+
throw new ChainCallError(`${toolName}: multicall3Address ${JSON.stringify(requested.slice(0, 64))} is not the Multicall3 chain "${chainId}" reads through (${aggregator.address}). The aggregator answers every row of a batch — return data, success, the block number — so which contract that is belongs to the operator: name it in tool_config.chaincall.multicall3 for chain "${chainId}", and leave multicall3Address out.`);
|
|
68
|
+
}
|
|
69
|
+
return aggregator;
|
|
70
|
+
}
|