@crewhaus/tool-token 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 +39 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +43 -15
- package/dist/lib/boot.d.ts +67 -0
- package/dist/lib/boot.js +97 -0
- package/dist/lib/chain.d.ts +2 -2
- package/dist/lib/chain.js +35 -4
- package/dist/lib/uri.js +1 -1
- package/package.json +15 -9
- package/dist/fixtures.d.ts +0 -117
- package/dist/fixtures.js +0 -222
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
|
@@ -116,6 +116,16 @@ own `getEthBalance` to stay in that snapshot. `batch: false` reads them
|
|
|
116
116
|
one at a time for chains with no Multicall3 deployment, and says in the answer
|
|
117
117
|
that the result is not a snapshot.
|
|
118
118
|
|
|
119
|
+
**Which Multicall3 is the operator's choice.** A batch is one `eth_call`, and
|
|
120
|
+
every answer in it — decimals, balances, the block number — is whatever that
|
|
121
|
+
contract returns, so a contract that fakes `aggregate3` can make a decimals
|
|
122
|
+
mismatch read as verified. Batches go to the canonical deployment
|
|
123
|
+
(`0xcA11bde05977b3631167028862bE2a173976CA11`) unless `tool_config.token.multicall3`
|
|
124
|
+
names another for the chain; a call's `multicall3Address` is accepted only when
|
|
125
|
+
it is that same address, and anything else is refused before any read. Every
|
|
126
|
+
batched answer says who served it: `aggregator: { address, source }`, with
|
|
127
|
+
`source` either `canonical` or `config`.
|
|
128
|
+
|
|
119
129
|
A sub-call inside that batch is a contract call like any other, and one that
|
|
120
130
|
does not answer is **not a zero**. A `getEthBalance` that came back empty —
|
|
121
131
|
an aggregator without the helper, a sub-call out of gas — is reported as
|
|
@@ -136,7 +146,34 @@ not promised by this paragraph.
|
|
|
136
146
|
## No dialling code at all
|
|
137
147
|
|
|
138
148
|
There is no RPC client and no HTTP client in this package. Both boundaries are
|
|
139
|
-
injected
|
|
149
|
+
injected, and a compiled bundle, `crewhaus run` and `crewhaus eval` bind them at
|
|
150
|
+
boot from the spec:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
chains: # the chain reader
|
|
154
|
+
- id: "1"
|
|
155
|
+
kind: evm
|
|
156
|
+
rpcUrls: [$ETH_RPC_URL] # read from the environment, never compiled in
|
|
157
|
+
finality: { kind: finalized }
|
|
158
|
+
|
|
159
|
+
tool_config:
|
|
160
|
+
token:
|
|
161
|
+
metadata_origins: [https://ipfs.io] # the only origins a tokenURI is read from
|
|
162
|
+
multicall3: # only where the canonical deployment is absent
|
|
163
|
+
"324": "0xF9cda624FBC7e059355ce98a31693d299FACd963"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Without a `chains` block, a chain read refuses and names the block to write.
|
|
167
|
+
These tools name a chain by its EIP-155 id, so declare it under that id in
|
|
168
|
+
decimal (`id: "1"`, `id: "8453"`); a chain declared as `mainnet` is refused
|
|
169
|
+
with the ids the spec does declare.
|
|
170
|
+
Without `metadata_origins`, metadata is not fetched, and the answer says why.
|
|
171
|
+
The metadata fetch goes through `@crewhaus/tool-http`'s gate: https only, the
|
|
172
|
+
SSRF refusal, no redirects, a byte cap. The caller's own `allowedHosts` and
|
|
173
|
+
`ipfsGateway` still decide what is attempted; this list decides what can be
|
|
174
|
+
dialled, and a model cannot widen it.
|
|
175
|
+
|
|
176
|
+
A host that builds its own wiring can still call the seams directly:
|
|
140
177
|
|
|
141
178
|
```ts
|
|
142
179
|
import { _setChainReader, chainReaderFromAdapters, _setMetadataFetch } from "@crewhaus/tool-token";
|
|
@@ -168,5 +205,6 @@ A tool that cannot say no is not finished. These say no, with the reason:
|
|
|
168
205
|
- an `ipfs://` URI that resolves outside the gateway it was given
|
|
169
206
|
- an `https://` metadata host nobody allow-listed, and any `http://` URI
|
|
170
207
|
- a metadata document over the byte cap — refused rather than parsed as a prefix
|
|
208
|
+
- a `multicall3Address` other than the canonical deployment or the one the operator configured
|
|
171
209
|
- a Multicall3 address that answers with something other than results
|
|
172
210
|
- a block tag that is not one, and a token id that is not a `uint256`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RegisteredTool } from "@crewhaus/tool-catalog";
|
|
2
2
|
export { type ChainRead, type ChainReadMethod, type ChainReader, READ_METHODS, TokenError, _setChainReader, chainReaderFromAdapters, hasChainReader, } from "./lib/chain";
|
|
3
|
+
export { type Aggregator, type TokenConfigInput, bindTokenChains, registerTokenConfig, } from "./lib/boot";
|
|
3
4
|
export { type MetadataFetch, type MetadataResponse, type UriPlan, DEFAULT_MAX_METADATA_BYTES, _setMetadataFetch, planUri, } from "./lib/uri";
|
|
4
5
|
export { type Candidate, type TokenList, type TokenListEntry, confusableSkeleton, findCandidates, textConcerns, } from "./lib/lists";
|
|
5
6
|
export declare const tokenResolve: RegisteredTool;
|
package/dist/index.js
CHANGED
|
@@ -30,20 +30,25 @@
|
|
|
30
30
|
*
|
|
31
31
|
* There is no RPC client and no HTTP client in this package. Chain reads
|
|
32
32
|
* leave through `_setChainReader` and metadata fetches through
|
|
33
|
-
* `_setMetadataFetch
|
|
33
|
+
* `_setMetadataFetch`. A bundle binds them at boot from the spec —
|
|
34
|
+
* `bindTokenChains` from its `chains` block, `registerTokenConfig` from
|
|
35
|
+
* `tool_config.token.metadata_origins` (`lib/boot.ts`) — and every test drives
|
|
34
36
|
* them from recorded answers, so the suite cannot reach the network even by
|
|
35
37
|
* accident.
|
|
36
38
|
*/
|
|
37
39
|
import { createHash } from "node:crypto";
|
|
40
|
+
import { CHAINS_BLOCK_EXAMPLE } from "@crewhaus/chain-adapter-base";
|
|
38
41
|
import { buildTool } from "@crewhaus/tool-builder";
|
|
39
42
|
import { MULTICALL3_ADDRESS } from "@crewhaus/tool-onchain";
|
|
40
43
|
import { z } from "zod";
|
|
44
|
+
import { resolveAggregator } from "./lib/boot";
|
|
41
45
|
import { TokenError, hasChainReader, nativeBalanceCall, readCalls, readHasCode, readNativeBalance, } from "./lib/chain";
|
|
42
46
|
import { INTERFACE_ID, SELECTOR, addressWord, bytes4Word, callData, decodeAddressValue, decodeBool, decodeStringish, decodeUint, padTokenIdHex, uintWord, } from "./lib/erc";
|
|
43
47
|
import { ADDRESS_SHAPE, confusableSkeleton, findCandidates, fingerprintLists, textConcerns, } from "./lib/lists";
|
|
44
48
|
import { MAX_FORMATTABLE_DECIMALS, checkAddress, formatUnits } from "./lib/onchain";
|
|
45
49
|
import { DEFAULT_MAX_METADATA_BYTES, decodeDataUri, fetchDocument, planUri, referencedUrls, } from "./lib/uri";
|
|
46
50
|
export { READ_METHODS, TokenError, _setChainReader, chainReaderFromAdapters, hasChainReader, } from "./lib/chain";
|
|
51
|
+
export { bindTokenChains, registerTokenConfig, } from "./lib/boot";
|
|
47
52
|
export { DEFAULT_MAX_METADATA_BYTES, _setMetadataFetch, planUri, } from "./lib/uri";
|
|
48
53
|
export { confusableSkeleton, findCandidates, textConcerns, } from "./lib/lists";
|
|
49
54
|
const json = (value) => JSON.stringify(value);
|
|
@@ -69,7 +74,7 @@ const batchField = z
|
|
|
69
74
|
const multicallField = z
|
|
70
75
|
.string()
|
|
71
76
|
.optional()
|
|
72
|
-
.describe(`
|
|
77
|
+
.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`);
|
|
73
78
|
// ---------------------------------------------------------------------------
|
|
74
79
|
// shared helpers
|
|
75
80
|
// ---------------------------------------------------------------------------
|
|
@@ -298,6 +303,7 @@ const CONCERNING_FLAGS = new Set([
|
|
|
298
303
|
]);
|
|
299
304
|
export const tokenResolve = buildTool({
|
|
300
305
|
name: "TokenResolve",
|
|
306
|
+
operativeArgs: [{ field: "query", kind: "text", within: "chainId" }],
|
|
301
307
|
description: "Turn a token symbol, name or address into ONE checksummed address, confirmed against the contract itself — or refuse and show you why. Use it before any transfer, balance read or approval, because a symbol is not an identifier: nothing stops a second contract calling itself USDC with six decimals and a convincing name, and reputable token lists carry different addresses for the same ticker. When a query could mean more than one address this returns EVERY candidate with resolved:false rather than the first hit or a most-likely, because picking between them is a decision with a wrong answer that costs money. Symbols that differ only by a Cyrillic lookalike or a zero-width space are pulled into the same candidate set on purpose, so an impostor a literal match would miss becomes an ambiguity you have to look at. The contract's own decimals and symbol are compared against what the lists claim, and a decimals mismatch is a refusal rather than a footnote.",
|
|
302
308
|
inputSchema: z
|
|
303
309
|
.object({
|
|
@@ -342,7 +348,7 @@ export const tokenResolve = buildTool({
|
|
|
342
348
|
// through whatever the runtime bound to the seam.
|
|
343
349
|
scope: "external",
|
|
344
350
|
ioCapability: "network",
|
|
345
|
-
execute: async (input) => {
|
|
351
|
+
execute: async (input, ctx) => {
|
|
346
352
|
const query = input.query.trim();
|
|
347
353
|
const lists = (input.lists ?? []);
|
|
348
354
|
const policy = input.policy ?? "listed-only";
|
|
@@ -413,12 +419,17 @@ export const tokenResolve = buildTool({
|
|
|
413
419
|
const wantsOnchain = input.confirmOnchain ?? true;
|
|
414
420
|
if (wantsOnchain && !hasChainReader()) {
|
|
415
421
|
if (input.confirmOnchain === true) {
|
|
416
|
-
throw new TokenError(
|
|
422
|
+
throw new TokenError(`confirmOnchain was asked for but no chain is configured. Declare one in the spec — ${CHAINS_BLOCK_EXAMPLE} — or pass confirmOnchain:false to answer from the lists alone`);
|
|
417
423
|
}
|
|
418
424
|
}
|
|
419
425
|
const confirm = wantsOnchain && hasChainReader();
|
|
420
426
|
const facts = new Map();
|
|
421
427
|
let blockNumber = null;
|
|
428
|
+
// Resolved before any read: an aggregator the operator did not name is
|
|
429
|
+
// refused, not asked.
|
|
430
|
+
const aggregator = confirm && (input.batch ?? true)
|
|
431
|
+
? resolveAggregator(input.chainId, input.multicall3Address, ctx?.toolConfig)
|
|
432
|
+
: null;
|
|
422
433
|
if (confirm) {
|
|
423
434
|
const calls = [];
|
|
424
435
|
for (const [i, candidate] of candidates.entries()) {
|
|
@@ -428,10 +439,8 @@ export const tokenResolve = buildTool({
|
|
|
428
439
|
chainId: input.chainId,
|
|
429
440
|
calls,
|
|
430
441
|
blockTag,
|
|
431
|
-
batch:
|
|
432
|
-
...(
|
|
433
|
-
? { multicall3Address: input.multicall3Address }
|
|
434
|
-
: {}),
|
|
442
|
+
batch: aggregator !== null,
|
|
443
|
+
...(aggregator !== null ? { multicall3Address: aggregator.address } : {}),
|
|
435
444
|
});
|
|
436
445
|
blockNumber = read.blockNumber;
|
|
437
446
|
for (const [i, candidate] of candidates.entries()) {
|
|
@@ -453,7 +462,14 @@ export const tokenResolve = buildTool({
|
|
|
453
462
|
flags: flagsFor(candidate, onchain),
|
|
454
463
|
});
|
|
455
464
|
}
|
|
456
|
-
const shared = {
|
|
465
|
+
const shared = {
|
|
466
|
+
...base,
|
|
467
|
+
blockNumber,
|
|
468
|
+
confirmedOnchain: confirm,
|
|
469
|
+
// Who answered the batched reads, since every one of them is its word.
|
|
470
|
+
aggregator,
|
|
471
|
+
candidates: flagged,
|
|
472
|
+
};
|
|
457
473
|
if (flagged.length > 1) {
|
|
458
474
|
return json({
|
|
459
475
|
...shared,
|
|
@@ -521,6 +537,7 @@ export const tokenResolve = buildTool({
|
|
|
521
537
|
// ---------------------------------------------------------------------------
|
|
522
538
|
export const erc20Balance = buildTool({
|
|
523
539
|
name: "Erc20Balance",
|
|
540
|
+
operativeArgs: [{ field: "token", kind: "id", within: "chainId" }],
|
|
524
541
|
description: "Read ERC-20 balances, and optionally an allowance, for one or many accounts at one block — with the token's decimals taken from the CONTRACT, never from a list and never defaulted. Use it instead of a raw eth_call: every amount comes back both as exact base units and as a decimal string, so a uint256 never becomes a JS number and eighteen digits of precision never quietly become fifteen. A token whose decimals() reverts is reported as unknown and its balances stay in base units rather than being scaled by an assumed 18, which is how a six-decimal transfer becomes a trillion-fold one. Tokens that answer symbol() with a bytes32 instead of a string — MKR and other 2017-era contracts — are decoded rather than crashed on, and an address with no code at it is refused instead of reporting everyone's balance as zero. Pass \"native\" as the token to read the chain's own currency in the same snapshot. It takes a contract ADDRESS, not a symbol: use TokenResolve first.",
|
|
525
542
|
inputSchema: z
|
|
526
543
|
.object({
|
|
@@ -551,10 +568,13 @@ export const erc20Balance = buildTool({
|
|
|
551
568
|
concurrencySafe: true,
|
|
552
569
|
scope: "external",
|
|
553
570
|
ioCapability: "network",
|
|
554
|
-
execute: async (input) => {
|
|
571
|
+
execute: async (input, ctx) => {
|
|
555
572
|
const blockTag = input.blockTag ?? "latest";
|
|
556
573
|
const batch = input.batch ?? true;
|
|
557
|
-
const
|
|
574
|
+
const aggregator = batch
|
|
575
|
+
? resolveAggregator(input.chainId, input.multicall3Address, ctx?.toolConfig)
|
|
576
|
+
: null;
|
|
577
|
+
const multicall = aggregator?.address ?? MULTICALL3_ADDRESS;
|
|
558
578
|
const isNative = input.token.trim().toLowerCase() === "native";
|
|
559
579
|
if (!isNative && !ADDRESS_SHAPE.test(input.token.trim())) {
|
|
560
580
|
throw new TokenError(`Erc20Balance takes a token contract address, not "${input.token}". A symbol is not an identifier — several contracts can claim the same one — so resolve it with TokenResolve first, which returns every candidate when a symbol is ambiguous rather than picking one.`);
|
|
@@ -627,6 +647,7 @@ export const erc20Balance = buildTool({
|
|
|
627
647
|
blockTag,
|
|
628
648
|
blockNumber,
|
|
629
649
|
batched: batch,
|
|
650
|
+
aggregator,
|
|
630
651
|
token: { kind: "native", address: null, decimals },
|
|
631
652
|
balances,
|
|
632
653
|
warnings,
|
|
@@ -701,6 +722,7 @@ export const erc20Balance = buildTool({
|
|
|
701
722
|
blockTag,
|
|
702
723
|
blockNumber: read.blockNumber,
|
|
703
724
|
batched: read.batched,
|
|
725
|
+
aggregator,
|
|
704
726
|
token: {
|
|
705
727
|
kind: "erc20",
|
|
706
728
|
address: token,
|
|
@@ -728,6 +750,10 @@ const INTERFACE_KEYS = {
|
|
|
728
750
|
};
|
|
729
751
|
export const erc721TokenInfo = buildTool({
|
|
730
752
|
name: "Erc721TokenInfo",
|
|
753
|
+
operativeArgs: [
|
|
754
|
+
{ field: "contract", kind: "id", within: "chainId" },
|
|
755
|
+
{ field: "ipfsGateway", kind: "url" },
|
|
756
|
+
],
|
|
731
757
|
description: "Read one NFT: which standard the contract actually implements, the collection's name and symbol, who owns the token (or an ERC-1155 holder's balance of it), and its tokenURI. Use it to check what an NFT is before buying, listing or transferring it. Metadata is read ONLY when it costs no trust: a data: URI is decoded in-process, an ipfs: URI is fetched through a gateway YOU named, and an https: URI only from a host YOU allow-listed — a URL that came out of contract data is not a reason to dial it, and following one is a server-side request forgery with extra steps. The answer lists every URI it read and every one it skipped, with the reason. ERC-1155's {id} placeholder is substituted with the 64-hex-digit zero-padded form the spec requires and almost everyone gets wrong, ERC-1155 has no ownerOf so ownership is answered as a balance or not at all rather than as a misleading null, and a contract that claims to support the invalid interface id is reported as one whose supportsInterface answers mean nothing.",
|
|
732
758
|
inputSchema: z
|
|
733
759
|
.object({
|
|
@@ -792,6 +818,9 @@ export const erc721TokenInfo = buildTool({
|
|
|
792
818
|
catch {
|
|
793
819
|
throw new TokenError(`"${input.tokenId}" is not a token id; give a decimal or 0x hex uint256 as a string`);
|
|
794
820
|
}
|
|
821
|
+
const aggregator = (input.batch ?? true)
|
|
822
|
+
? resolveAggregator(input.chainId, input.multicall3Address, ctx?.toolConfig)
|
|
823
|
+
: null;
|
|
795
824
|
const code = (input.checkCode ?? true) ? await readHasCode(input.chainId, contract, blockTag) : null;
|
|
796
825
|
if (code !== null && !code.hasCode) {
|
|
797
826
|
throw new TokenError(`there is no contract at ${contract} at block ${blockTag} — every call to it answers 0x, so an NFT read against it would report no owner and no URI rather than saying this is not a contract`);
|
|
@@ -826,10 +855,8 @@ export const erc721TokenInfo = buildTool({
|
|
|
826
855
|
chainId: input.chainId,
|
|
827
856
|
calls,
|
|
828
857
|
blockTag,
|
|
829
|
-
batch:
|
|
830
|
-
...(
|
|
831
|
-
? { multicall3Address: input.multicall3Address }
|
|
832
|
-
: {}),
|
|
858
|
+
batch: aggregator !== null,
|
|
859
|
+
...(aggregator !== null ? { multicall3Address: aggregator.address } : {}),
|
|
833
860
|
});
|
|
834
861
|
const says = (key) => {
|
|
835
862
|
const outcome = read.outcomes.get(key);
|
|
@@ -1013,6 +1040,7 @@ export const erc721TokenInfo = buildTool({
|
|
|
1013
1040
|
blockTag,
|
|
1014
1041
|
blockNumber: read.blockNumber,
|
|
1015
1042
|
batched: read.batched,
|
|
1043
|
+
aggregator,
|
|
1016
1044
|
standard,
|
|
1017
1045
|
standardSource,
|
|
1018
1046
|
interfaces,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two boot bindings this package's seams get in a compiled bundle,
|
|
3
|
+
* `crewhaus run` and `crewhaus eval` — each from a spec block, each failing
|
|
4
|
+
* closed without one.
|
|
5
|
+
*
|
|
6
|
+
* - {@link bindTokenChains}: the chain reader, from the spec's `chains`
|
|
7
|
+
* block, through the same adapters `tool-evm` reads with.
|
|
8
|
+
* - {@link registerTokenConfig}: the metadata fetcher, from
|
|
9
|
+
* `tool_config.token.metadata_origins`, the operator's list of origins a
|
|
10
|
+
* contract's `tokenURI` may be read from. The caller's own `allowedHosts`
|
|
11
|
+
* and `ipfsGateway` still decide what is attempted; this list decides
|
|
12
|
+
* what can be dialled, and a model cannot widen it. And the Multicall3
|
|
13
|
+
* deployments in `tool_config.token.multicall3`, below.
|
|
14
|
+
*
|
|
15
|
+
* Still no dialling code here: the fetch goes through `@crewhaus/tool-http`'s
|
|
16
|
+
* gate (allow-list, SSRF, no redirects, byte cap), and the chain read through
|
|
17
|
+
* the adapter.
|
|
18
|
+
*/
|
|
19
|
+
import type { ChainAdapterConfig } from "@crewhaus/chain-adapter-base";
|
|
20
|
+
/** Bind the chain reader from the spec's `chains` block. */
|
|
21
|
+
export declare function bindTokenChains(config: {
|
|
22
|
+
readonly chains: ReadonlyArray<ChainAdapterConfig>;
|
|
23
|
+
}): void;
|
|
24
|
+
/** The spec's `tool_config.token` block. */
|
|
25
|
+
export type TokenConfigInput = {
|
|
26
|
+
readonly metadata_origins?: ReadonlyArray<string>;
|
|
27
|
+
readonly metadataOrigins?: ReadonlyArray<string>;
|
|
28
|
+
/** chain id → the Multicall3 deployment batches on that chain go to. */
|
|
29
|
+
readonly multicall3?: Readonly<Record<string, string>>;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Apply `tool_config.token`: the Multicall3 deployments in `multicall3`, and
|
|
33
|
+
* the metadata fetcher from `metadata_origins`. Only https origins are
|
|
34
|
+
* accepted: a metadata document read over plain http is whatever the network
|
|
35
|
+
* says it is. A block without the list binds no fetcher.
|
|
36
|
+
*/
|
|
37
|
+
export declare function registerTokenConfig(input: TokenConfigInput): void;
|
|
38
|
+
/**
|
|
39
|
+
* Which contract answers a batched read — decided by the operator, never by
|
|
40
|
+
* a call.
|
|
41
|
+
*
|
|
42
|
+
* A batch is ONE `eth_call` to Multicall3, and every sub-result — decimals,
|
|
43
|
+
* symbol, balances, allowances, `ownerOf`, the block number — is whatever
|
|
44
|
+
* that contract returns. Any contract can implement `aggregate3` and return
|
|
45
|
+
* rows it made up. So a model-supplied aggregator address was a way to make
|
|
46
|
+
* TokenResolve report a decimals mismatch as `verified: true`, and
|
|
47
|
+
* Erc20Balance report balances nobody holds.
|
|
48
|
+
*
|
|
49
|
+
* The aggregator is the canonical deployment unless the operator names
|
|
50
|
+
* another for that chain in `tool_config.token.multicall3` (a chain whose
|
|
51
|
+
* Multicall3 is elsewhere, such as zkSync Era). A call may still pass
|
|
52
|
+
* `multicall3Address`, which older callers do, but only the address that
|
|
53
|
+
* would be used anyway is accepted; anything else is refused before a read.
|
|
54
|
+
*/
|
|
55
|
+
/** Who served a batch, as every batched answer reports it. */
|
|
56
|
+
export type Aggregator = {
|
|
57
|
+
/** EIP-55. */
|
|
58
|
+
readonly address: string;
|
|
59
|
+
/** `canonical`: the deterministic deploy. `config`: the operator's tool_config. */
|
|
60
|
+
readonly source: "canonical" | "config";
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The aggregator a batch on `chainId` goes to, and where that came from. A
|
|
64
|
+
* `requested` address other than that one is refused: which contract answers
|
|
65
|
+
* every read is not a per-call choice.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveAggregator(chainId: number, requested: string | undefined, toolConfig: unknown): Aggregator;
|
package/dist/lib/boot.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createEvmAdapters } from "@crewhaus/chain-adapter-evm";
|
|
2
|
+
import { guardedGet } from "@crewhaus/tool-http";
|
|
3
|
+
import { MULTICALL3_ADDRESS, parseMulticallMap } from "@crewhaus/tool-onchain";
|
|
4
|
+
import { TokenError, _setChainReader, chainReaderFromAdapters } from "./chain";
|
|
5
|
+
import { _setMetadataFetch } from "./uri";
|
|
6
|
+
/**
|
|
7
|
+
* The operator's Multicall3 deployments by chain id, from
|
|
8
|
+
* `tool_config.token.multicall3`. Only {@link registerTokenConfig} writes it.
|
|
9
|
+
*/
|
|
10
|
+
let configured = new Map();
|
|
11
|
+
/** A metadata document is small; a slow host should not hold a call open for long. */
|
|
12
|
+
const METADATA_TIMEOUT_MS = 10_000;
|
|
13
|
+
/** Bind the chain reader from the spec's `chains` block. */
|
|
14
|
+
export function bindTokenChains(config) {
|
|
15
|
+
const adapters = createEvmAdapters(config.chains);
|
|
16
|
+
_setChainReader(chainReaderFromAdapters((chainId) => adapters.get(chainId), [...adapters.keys()]));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Apply `tool_config.token`: the Multicall3 deployments in `multicall3`, and
|
|
20
|
+
* the metadata fetcher from `metadata_origins`. Only https origins are
|
|
21
|
+
* accepted: a metadata document read over plain http is whatever the network
|
|
22
|
+
* says it is. A block without the list binds no fetcher.
|
|
23
|
+
*/
|
|
24
|
+
export function registerTokenConfig(input) {
|
|
25
|
+
const block = (input ?? {});
|
|
26
|
+
configured = parseTokenMulticall(block["multicall3"]);
|
|
27
|
+
if (Object.hasOwn(block, "metadata_origins") && Object.hasOwn(block, "metadataOrigins")) {
|
|
28
|
+
throw new TokenError("tool_config.token sets both metadata_origins and metadataOrigins. Write the list once, as metadata_origins.");
|
|
29
|
+
}
|
|
30
|
+
const raw = block["metadata_origins"] ?? block["metadataOrigins"];
|
|
31
|
+
if (raw === undefined)
|
|
32
|
+
return;
|
|
33
|
+
if (!Array.isArray(raw) || raw.some((o) => typeof o !== "string")) {
|
|
34
|
+
throw new TokenError('tool_config.token.metadata_origins must be a list of https origins, for example ["https://ipfs.io"].');
|
|
35
|
+
}
|
|
36
|
+
const origins = [];
|
|
37
|
+
for (const origin of raw) {
|
|
38
|
+
let url;
|
|
39
|
+
try {
|
|
40
|
+
url = new URL(origin);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new TokenError(`tool_config.token.metadata_origins has "${origin}", which is not an origin. Write it as https://host.`);
|
|
44
|
+
}
|
|
45
|
+
if (url.protocol !== "https:") {
|
|
46
|
+
throw new TokenError(`tool_config.token.metadata_origins has "${url.protocol}//${url.host}", which is not https. Metadata is read over https only.`);
|
|
47
|
+
}
|
|
48
|
+
origins.push(url.origin);
|
|
49
|
+
}
|
|
50
|
+
_setMetadataFetch(async ({ url, maxBytes, signal }) => {
|
|
51
|
+
// One byte over the cap, so a document that fills it exactly is told
|
|
52
|
+
// apart from one that was cut — `fetchDocument` refuses the latter.
|
|
53
|
+
const got = await guardedGet(url, {
|
|
54
|
+
allowedOrigins: origins,
|
|
55
|
+
maxBytes: maxBytes + 1,
|
|
56
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
57
|
+
...(signal !== undefined ? { signal } : {}),
|
|
58
|
+
});
|
|
59
|
+
return { status: got.status, contentType: got.contentType, bytes: got.bytes };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Parse `tool_config.token.multicall3`, refusing a malformed entry by its key. */
|
|
63
|
+
function parseTokenMulticall(input) {
|
|
64
|
+
try {
|
|
65
|
+
return parseMulticallMap(input, "tool_config.token.multicall3");
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
throw new TokenError(err.message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The map one call runs under: the serving candidate's own `tool_config`
|
|
73
|
+
* block when it has one (it replaces the boot registration, as every
|
|
74
|
+
* tool_config block does), else the boot registration.
|
|
75
|
+
*/
|
|
76
|
+
function mapFor(toolConfig) {
|
|
77
|
+
if (typeof toolConfig === "object" && toolConfig !== null && !Array.isArray(toolConfig)) {
|
|
78
|
+
return parseTokenMulticall(toolConfig["multicall3"]);
|
|
79
|
+
}
|
|
80
|
+
return configured;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The aggregator a batch on `chainId` goes to, and where that came from. A
|
|
84
|
+
* `requested` address other than that one is refused: which contract answers
|
|
85
|
+
* every read is not a per-call choice.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveAggregator(chainId, requested, toolConfig) {
|
|
88
|
+
const override = mapFor(toolConfig).get(String(chainId));
|
|
89
|
+
const aggregator = override === undefined
|
|
90
|
+
? { address: MULTICALL3_ADDRESS, source: "canonical" }
|
|
91
|
+
: { address: override, source: "config" };
|
|
92
|
+
if (requested !== undefined &&
|
|
93
|
+
requested.trim().toLowerCase() !== aggregator.address.toLowerCase()) {
|
|
94
|
+
throw new TokenError(`multicall3Address ${JSON.stringify(requested.slice(0, 64))} is not the Multicall3 chain ${chainId} reads through (${aggregator.address}). The aggregator answers every read in a batch — decimals, balances, the block number — so which contract that is belongs to the operator: name it in tool_config.token.multicall3 for chain ${chainId}. Or pass batch:false to read each call directly.`);
|
|
95
|
+
}
|
|
96
|
+
return aggregator;
|
|
97
|
+
}
|
package/dist/lib/chain.d.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* blocks. `getBlockNumber()` rides along in the same batch so the answer says
|
|
23
23
|
* which block it is a snapshot of.
|
|
24
24
|
*/
|
|
25
|
-
import type
|
|
25
|
+
import { type ChainAdapter } from "@crewhaus/chain-adapter-base";
|
|
26
26
|
import { CrewhausError } from "@crewhaus/errors";
|
|
27
27
|
import { type RevertReason } from "@crewhaus/tool-onchain";
|
|
28
28
|
/** A refusal this package makes: a bad input, a missing seam, an unreadable answer. */
|
|
@@ -59,7 +59,7 @@ export declare function assertReadMethod(method: string): asserts method is Chai
|
|
|
59
59
|
* bundle has. The adapter applies its own read-only allowlist underneath, so
|
|
60
60
|
* a write method would be refused twice.
|
|
61
61
|
*/
|
|
62
|
-
export declare function chainReaderFromAdapters(resolve: (chainId: string) => ChainAdapter | undefined): ChainReader;
|
|
62
|
+
export declare function chainReaderFromAdapters(resolve: (chainId: string) => ChainAdapter | undefined, declared?: ReadonlyArray<string>): ChainReader;
|
|
63
63
|
/** One call in a read, keyed so results are never matched up by position here. */
|
|
64
64
|
export type BatchCall = {
|
|
65
65
|
/** The caller's own label. Results come back in a map under it. */
|
package/dist/lib/chain.js
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one way this package learns anything from a chain — and the reason it
|
|
3
|
+
* is the only way.
|
|
4
|
+
*
|
|
5
|
+
* **There is no dialling code in `@crewhaus/tool-token`.** No URL, no fetch,
|
|
6
|
+
* no socket. Every chain read leaves through `_setChainReader`, which the
|
|
7
|
+
* runtime binds at boot and every test in this package drives from recorded
|
|
8
|
+
* answers. Two things follow, and both are the point:
|
|
9
|
+
*
|
|
10
|
+
* 1. A test cannot reach a public RPC endpoint even by accident. There is
|
|
11
|
+
* nothing under the seam to reach it with. A suite that dialled a real
|
|
12
|
+
* node would fail on a CI runner with no egress and flake on somebody
|
|
13
|
+
* else's rate limit.
|
|
14
|
+
* 2. The set of JSON-RPC methods this package can emit is three, they are
|
|
15
|
+
* listed below, and {@link assertReadMethod} refuses anything else
|
|
16
|
+
* before it reaches the reader. `eth_sendTransaction` and
|
|
17
|
+
* `eth_sendRawTransaction` are not reachable from any code path here;
|
|
18
|
+
* `index.test.ts` asserts it.
|
|
19
|
+
*
|
|
20
|
+
* Reads are batched through Multicall3 by default, because a hundred balances
|
|
21
|
+
* read one at a time are a hundred balances from up to a hundred different
|
|
22
|
+
* blocks. `getBlockNumber()` rides along in the same batch so the answer says
|
|
23
|
+
* which block it is a snapshot of.
|
|
24
|
+
*/
|
|
25
|
+
import { CHAINS_BLOCK_EXAMPLE } from "@crewhaus/chain-adapter-base";
|
|
1
26
|
import { CrewhausError } from "@crewhaus/errors";
|
|
2
27
|
import { MULTICALL3_ADDRESS, decodeAggregate3, encodeAggregate3, } from "@crewhaus/tool-onchain";
|
|
3
28
|
import { SELECTOR, addressWord, callData, decodeUint } from "./erc";
|
|
@@ -15,7 +40,7 @@ export class TokenError extends CrewhausError {
|
|
|
15
40
|
*/
|
|
16
41
|
export const READ_METHODS = Object.freeze(["eth_call", "eth_getBalance", "eth_getCode"]);
|
|
17
42
|
const unbound = async (read) => {
|
|
18
|
-
throw new TokenError(`no chain
|
|
43
|
+
throw new TokenError(`no chain is configured, so chain ${read.chainId} cannot be read. Declare it in the spec — ${CHAINS_BLOCK_EXAMPLE}.`);
|
|
19
44
|
};
|
|
20
45
|
let reader = unbound;
|
|
21
46
|
/**
|
|
@@ -41,12 +66,18 @@ export function assertReadMethod(method) {
|
|
|
41
66
|
* bundle has. The adapter applies its own read-only allowlist underneath, so
|
|
42
67
|
* a write method would be refused twice.
|
|
43
68
|
*/
|
|
44
|
-
export function chainReaderFromAdapters(resolve) {
|
|
69
|
+
export function chainReaderFromAdapters(resolve, declared) {
|
|
45
70
|
return async (read) => {
|
|
46
71
|
assertReadMethod(read.method);
|
|
47
72
|
const adapter = resolve(String(read.chainId));
|
|
48
73
|
if (adapter === undefined) {
|
|
49
|
-
|
|
74
|
+
// These tools name a chain by its EIP-155 id, and look it up as that
|
|
75
|
+
// number written in decimal. A chain declared as "mainnet" is there,
|
|
76
|
+
// just not under the id asked for — so say which ids ARE declared.
|
|
77
|
+
const known = declared === undefined || declared.length === 0
|
|
78
|
+
? ""
|
|
79
|
+
: ` The spec declares ${declared.map((id) => `"${id}"`).join(", ")}; the token tools look a chain up by its EIP-155 chain id in decimal, so declare it as id: "${read.chainId}".`;
|
|
80
|
+
throw new TokenError(`no chain is declared with id "${read.chainId}", so chain ${read.chainId} cannot be read.${known === "" ? " Declare it in the spec's chains[] block." : known}`);
|
|
50
81
|
}
|
|
51
82
|
return adapter.rpcRead(read.method, read.params);
|
|
52
83
|
};
|
|
@@ -143,7 +174,7 @@ export async function readCalls(opts) {
|
|
|
143
174
|
rows = decodeAggregate3(asHex(answer, "the Multicall3 batch"), request.callCount);
|
|
144
175
|
}
|
|
145
176
|
catch (err) {
|
|
146
|
-
throw new TokenError(`the Multicall3 batch at ${request.to} did not answer with results: ${err.message}. If this chain
|
|
177
|
+
throw new TokenError(`the Multicall3 batch at ${request.to} did not answer with results: ${err.message}. If this chain's Multicall3 is deployed elsewhere, the operator names it in tool_config.token.multicall3; or pass batch:false to read the calls one at a time.`);
|
|
147
178
|
}
|
|
148
179
|
for (const [i, call] of packed.entries()) {
|
|
149
180
|
const row = rows[i];
|
package/dist/lib/uri.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { Buffer } from "node:buffer";
|
|
29
29
|
const unbound = async (req) => {
|
|
30
|
-
throw new Error(`no metadata
|
|
30
|
+
throw new Error(`no metadata origin is allowed, so ${req.url} was not read. Allow the origin in the spec — tool_config.token: { metadata_origins: [https://ipfs.io] }.`);
|
|
31
31
|
};
|
|
32
32
|
let metadataFetch = unbound;
|
|
33
33
|
/** Bind the metadata fetcher. `undefined` restores the unbound state. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/tool-token",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic token and address resolution: ERC-20 and ERC-721 reads with impostor flagging",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,19 +8,22 @@
|
|
|
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-
|
|
23
|
-
"@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-http": "0.8.0",
|
|
25
|
+
"@crewhaus/tool-onchain": "0.8.0",
|
|
26
|
+
"@crewhaus/tool-text": "0.8.0",
|
|
24
27
|
"zod": "^3.23.8"
|
|
25
28
|
},
|
|
26
29
|
"devDependencies": {
|
|
@@ -44,5 +47,8 @@
|
|
|
44
47
|
"publishConfig": {
|
|
45
48
|
"access": "public"
|
|
46
49
|
},
|
|
47
|
-
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
50
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"],
|
|
51
|
+
"engines": {
|
|
52
|
+
"bun": ">=1.3.14"
|
|
53
|
+
}
|
|
48
54
|
}
|
package/dist/fixtures.d.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import type { ChainRead, ChainReader } from "./lib/chain";
|
|
2
|
-
/** A 32-byte word from an unsigned integer. */
|
|
3
|
-
export declare const word: (value: bigint) => string;
|
|
4
|
-
export declare const encodeUint: (value: bigint) => string;
|
|
5
|
-
export declare const encodeAddress: (address: string) => string;
|
|
6
|
-
export declare const encodeBool: (value: boolean) => string;
|
|
7
|
-
/** A dynamic `string` return: offset, length, then the bytes, padded. */
|
|
8
|
-
export declare function encodeString(text: string): string;
|
|
9
|
-
/** The pre-standard `bytes32` symbol: the text, left-aligned, NUL-padded. */
|
|
10
|
-
export declare function encodeBytes32String(text: string): string;
|
|
11
|
-
/** `Error(string)` revert data — the selector is the one Solidity emits. */
|
|
12
|
-
export declare function revertWith(message: string): Promise<string>;
|
|
13
|
-
/** What one contract answers for one call. A revert is an answer, not a throw. */
|
|
14
|
-
export type CallAnswer = {
|
|
15
|
-
readonly ok: boolean;
|
|
16
|
-
readonly data: string;
|
|
17
|
-
};
|
|
18
|
-
export declare const answers: (data: string) => CallAnswer;
|
|
19
|
-
export declare const reverts: (revertData: string) => CallAnswer;
|
|
20
|
-
/** What a contract without that function returns: nothing at all. */
|
|
21
|
-
export declare const NOTHING: CallAnswer;
|
|
22
|
-
export type ContractHandler = (selector: string, args: string) => CallAnswer;
|
|
23
|
-
export declare const USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
|
|
24
|
-
export declare const IMPOSTOR_USDC = "0x1111111111111111111111111111111111111111";
|
|
25
|
-
export declare const CYRILLIC_USDC = "0x2222222222222222222222222222222222222222";
|
|
26
|
-
export declare const MKR = "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2";
|
|
27
|
-
export declare const BROKEN = "0x3333333333333333333333333333333333333333";
|
|
28
|
-
export declare const NOT_A_CONTRACT = "0x4444444444444444444444444444444444444444";
|
|
29
|
-
export declare const APE_NFT = "0x5555555555555555555555555555555555555555";
|
|
30
|
-
export declare const GAME_1155 = "0x6666666666666666666666666666666666666666";
|
|
31
|
-
export declare const ONCHAIN_NFT = "0x7777777777777777777777777777777777777777";
|
|
32
|
-
export declare const LIAR_165 = "0x8888888888888888888888888888888888888888";
|
|
33
|
-
export declare const ALICE = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
|
|
34
|
-
export declare const BOB = "0x388C818CA8B9251b393131C08a736A67ccB19297";
|
|
35
|
-
export declare const SPENDER = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45";
|
|
36
|
-
/** A plain ERC-20 whose metadata is all well-formed strings. */
|
|
37
|
-
export declare function erc20(opts: {
|
|
38
|
-
decimals: bigint;
|
|
39
|
-
symbol: string;
|
|
40
|
-
name: string;
|
|
41
|
-
totalSupply: bigint;
|
|
42
|
-
balances?: Record<string, bigint>;
|
|
43
|
-
allowances?: Record<string, bigint>;
|
|
44
|
-
bytes32Metadata?: boolean;
|
|
45
|
-
}): ContractHandler;
|
|
46
|
-
/** An ERC-721 that declares itself properly through ERC-165. */
|
|
47
|
-
export declare function erc721(opts: {
|
|
48
|
-
name: string;
|
|
49
|
-
symbol: string;
|
|
50
|
-
owners: Record<string, string>;
|
|
51
|
-
tokenUris: Record<string, string>;
|
|
52
|
-
missingTokenRevert: string;
|
|
53
|
-
interfaceIds?: ReadonlyArray<string>;
|
|
54
|
-
answersEverything?: boolean;
|
|
55
|
-
}): ContractHandler;
|
|
56
|
-
/** An ERC-1155, which has no ownerOf and whose uri carries the {id} template. */
|
|
57
|
-
export declare function erc1155(opts: {
|
|
58
|
-
uriTemplate: string;
|
|
59
|
-
balances: Record<string, bigint>;
|
|
60
|
-
interfaceIds?: ReadonlyArray<string>;
|
|
61
|
-
}): ContractHandler;
|
|
62
|
-
export type ChainStub = {
|
|
63
|
-
readonly reader: ChainReader;
|
|
64
|
-
/** Every read that left the package, in order, for assertions about what was asked. */
|
|
65
|
-
readonly reads: ChainRead[];
|
|
66
|
-
};
|
|
67
|
-
export declare function chainStub(opts: {
|
|
68
|
-
readonly contracts: Record<string, ContractHandler>;
|
|
69
|
-
readonly nativeBalances?: Record<string, bigint>;
|
|
70
|
-
readonly code?: Record<string, string>;
|
|
71
|
-
readonly blockNumber?: bigint;
|
|
72
|
-
readonly multicall3Address?: string;
|
|
73
|
-
/** Answer the batch call itself with this instead, to test an unusable answer. */
|
|
74
|
-
readonly brokenBatchAnswer?: string;
|
|
75
|
-
/**
|
|
76
|
-
* A Multicall3-shaped contract that implements `aggregate3` and nothing
|
|
77
|
-
* else, so `getEthBalance` and `getBlockNumber` reach its fallback and
|
|
78
|
-
* answer with no data. Real: `multicall3Address` is a caller-supplied
|
|
79
|
-
* address, and an aggregator that is not the canonical deployment has no
|
|
80
|
-
* obligation to carry the helpers.
|
|
81
|
-
*/
|
|
82
|
-
readonly withoutMulticallHelpers?: boolean;
|
|
83
|
-
}): ChainStub;
|
|
84
|
-
export declare const UNISWAP_LIST: {
|
|
85
|
-
id: string;
|
|
86
|
-
name: string;
|
|
87
|
-
tokens: {
|
|
88
|
-
chainId: number;
|
|
89
|
-
address: string;
|
|
90
|
-
symbol: string;
|
|
91
|
-
name: string;
|
|
92
|
-
decimals: number;
|
|
93
|
-
}[];
|
|
94
|
-
};
|
|
95
|
-
/** A second reputable list that happens to carry a different address for USDC. */
|
|
96
|
-
export declare const RIVAL_LIST: {
|
|
97
|
-
id: string;
|
|
98
|
-
name: string;
|
|
99
|
-
tokens: {
|
|
100
|
-
chainId: number;
|
|
101
|
-
address: string;
|
|
102
|
-
symbol: string;
|
|
103
|
-
name: string;
|
|
104
|
-
decimals: number;
|
|
105
|
-
}[];
|
|
106
|
-
};
|
|
107
|
-
/** The Cyrillic \u0421 impostor. It is not the same string, and it looks identical. */
|
|
108
|
-
export declare const CYRILLIC_LIST: {
|
|
109
|
-
id: string;
|
|
110
|
-
tokens: {
|
|
111
|
-
chainId: number;
|
|
112
|
-
address: string;
|
|
113
|
-
symbol: string;
|
|
114
|
-
name: string;
|
|
115
|
-
decimals: number;
|
|
116
|
-
}[];
|
|
117
|
-
};
|
package/dist/fixtures.js
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Recorded chain answers, and a reader stub that serves them.
|
|
3
|
-
*
|
|
4
|
-
* Every test in this package drives `_setChainReader` with one of these. None
|
|
5
|
-
* of them opens a socket, because there is nothing under the seam to open one
|
|
6
|
-
* with — a suite that reached a public RPC endpoint would fail on a CI runner
|
|
7
|
-
* with no egress and flake on somebody else's rate limit.
|
|
8
|
-
*
|
|
9
|
-
* The tokens here are the ones that break naive readers, and they are
|
|
10
|
-
* modelled on real contracts rather than invented:
|
|
11
|
-
*
|
|
12
|
-
* - a six-decimal stablecoin, the ordinary case;
|
|
13
|
-
* - a `bytes32` symbol, which is what MKR and other 2017-era tokens return
|
|
14
|
-
* because the ABI was not settled when they deployed;
|
|
15
|
-
* - a token whose `decimals()` reverts, which must come back as unknown and
|
|
16
|
-
* never as 18;
|
|
17
|
-
* - two impostors claiming a listed ticker, one of them with a Cyrillic \u0421;
|
|
18
|
-
* - an address with no code at all.
|
|
19
|
-
*
|
|
20
|
-
* The Multicall3 stub does a real ABI round trip through `tool-onchain`'s
|
|
21
|
-
* codec rather than a hand-written fake of one: it decodes the batch this
|
|
22
|
-
* package encoded and encodes the `(bool,bytes)[]` the package decodes. A
|
|
23
|
-
* fixture that faked the encoding would pass against a broken encoder.
|
|
24
|
-
*/
|
|
25
|
-
import { MULTICALL3_ADDRESS, abiDecode, abiEncodeCall } from "@crewhaus/tool-onchain";
|
|
26
|
-
import { SELECTOR } from "./lib/erc";
|
|
27
|
-
// ─── encoding helpers ───────────────────────────────────────────────────────
|
|
28
|
-
const hexBody = (value) => value.replace(/^0x/, "");
|
|
29
|
-
/** A 32-byte word from an unsigned integer. */
|
|
30
|
-
export const word = (value) => value.toString(16).padStart(64, "0");
|
|
31
|
-
export const encodeUint = (value) => `0x${word(value)}`;
|
|
32
|
-
export const encodeAddress = (address) => `0x${hexBody(address).toLowerCase().padStart(64, "0")}`;
|
|
33
|
-
export const encodeBool = (value) => encodeUint(value ? 1n : 0n);
|
|
34
|
-
/** A dynamic `string` return: offset, length, then the bytes, padded. */
|
|
35
|
-
export function encodeString(text) {
|
|
36
|
-
const bytes = new TextEncoder().encode(text);
|
|
37
|
-
let data = "";
|
|
38
|
-
for (const b of bytes)
|
|
39
|
-
data += b.toString(16).padStart(2, "0");
|
|
40
|
-
const padded = data.padEnd(Math.ceil(bytes.length / 32) * 64, "0");
|
|
41
|
-
return `0x${word(32n)}${word(BigInt(bytes.length))}${padded}`;
|
|
42
|
-
}
|
|
43
|
-
/** The pre-standard `bytes32` symbol: the text, left-aligned, NUL-padded. */
|
|
44
|
-
export function encodeBytes32String(text) {
|
|
45
|
-
const bytes = new TextEncoder().encode(text);
|
|
46
|
-
if (bytes.length > 32)
|
|
47
|
-
throw new Error(`"${text}" does not fit in a bytes32`);
|
|
48
|
-
let data = "";
|
|
49
|
-
for (const b of bytes)
|
|
50
|
-
data += b.toString(16).padStart(2, "0");
|
|
51
|
-
return `0x${data.padEnd(64, "0")}`;
|
|
52
|
-
}
|
|
53
|
-
const strip = (call) => `0x${call.slice(10)}`;
|
|
54
|
-
async function encodeCall(signature, args) {
|
|
55
|
-
const out = await abiEncodeCall.execute({ signature, args });
|
|
56
|
-
if (typeof out !== "string")
|
|
57
|
-
throw new Error("AbiEncodeCall did not return a string");
|
|
58
|
-
return JSON.parse(out).data;
|
|
59
|
-
}
|
|
60
|
-
/** `Error(string)` revert data — the selector is the one Solidity emits. */
|
|
61
|
-
export async function revertWith(message) {
|
|
62
|
-
return encodeCall("Error(string)", [message]);
|
|
63
|
-
}
|
|
64
|
-
export const answers = (data) => ({ ok: true, data });
|
|
65
|
-
export const reverts = (revertData) => ({ ok: false, data: revertData });
|
|
66
|
-
/** What a contract without that function returns: nothing at all. */
|
|
67
|
-
export const NOTHING = { ok: true, data: "0x" };
|
|
68
|
-
export const USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
|
|
69
|
-
export const IMPOSTOR_USDC = "0x1111111111111111111111111111111111111111";
|
|
70
|
-
export const CYRILLIC_USDC = "0x2222222222222222222222222222222222222222";
|
|
71
|
-
export const MKR = "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2";
|
|
72
|
-
export const BROKEN = "0x3333333333333333333333333333333333333333";
|
|
73
|
-
export const NOT_A_CONTRACT = "0x4444444444444444444444444444444444444444";
|
|
74
|
-
export const APE_NFT = "0x5555555555555555555555555555555555555555";
|
|
75
|
-
export const GAME_1155 = "0x6666666666666666666666666666666666666666";
|
|
76
|
-
export const ONCHAIN_NFT = "0x7777777777777777777777777777777777777777";
|
|
77
|
-
export const LIAR_165 = "0x8888888888888888888888888888888888888888";
|
|
78
|
-
export const ALICE = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
|
|
79
|
-
export const BOB = "0x388C818CA8B9251b393131C08a736A67ccB19297";
|
|
80
|
-
export const SPENDER = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45";
|
|
81
|
-
const addressArg = (args, index = 0) => `0x${args.slice(index * 64 + 24, index * 64 + 64)}`.toLowerCase();
|
|
82
|
-
const uintArg = (args, index = 0) => BigInt(`0x${args.slice(index * 64, index * 64 + 64) || "0"}`);
|
|
83
|
-
/** A plain ERC-20 whose metadata is all well-formed strings. */
|
|
84
|
-
export function erc20(opts) {
|
|
85
|
-
const text = opts.bytes32Metadata ? encodeBytes32String : encodeString;
|
|
86
|
-
return (selector, args) => {
|
|
87
|
-
switch (selector) {
|
|
88
|
-
case SELECTOR.decimals:
|
|
89
|
-
return answers(encodeUint(opts.decimals));
|
|
90
|
-
case SELECTOR.symbol:
|
|
91
|
-
return answers(text(opts.symbol));
|
|
92
|
-
case SELECTOR.name:
|
|
93
|
-
return answers(text(opts.name));
|
|
94
|
-
case SELECTOR.totalSupply:
|
|
95
|
-
return answers(encodeUint(opts.totalSupply));
|
|
96
|
-
case SELECTOR.balanceOf:
|
|
97
|
-
return answers(encodeUint(opts.balances?.[addressArg(args)] ?? 0n));
|
|
98
|
-
case SELECTOR.allowance:
|
|
99
|
-
return answers(encodeUint(opts.allowances?.[`${addressArg(args, 0)}:${addressArg(args, 1)}`] ?? 0n));
|
|
100
|
-
default:
|
|
101
|
-
return NOTHING;
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
/** An ERC-721 that declares itself properly through ERC-165. */
|
|
106
|
-
export function erc721(opts) {
|
|
107
|
-
const supported = new Set(opts.interfaceIds ?? ["01ffc9a7", "80ac58cd", "5b5e139f"]);
|
|
108
|
-
return (selector, args) => {
|
|
109
|
-
if (selector === SELECTOR.supportsInterface) {
|
|
110
|
-
const id = args.slice(0, 8).toLowerCase();
|
|
111
|
-
return answers(encodeBool(opts.answersEverything === true || supported.has(id)));
|
|
112
|
-
}
|
|
113
|
-
if (selector === SELECTOR.name)
|
|
114
|
-
return answers(encodeString(opts.name));
|
|
115
|
-
if (selector === SELECTOR.symbol)
|
|
116
|
-
return answers(encodeString(opts.symbol));
|
|
117
|
-
if (selector === SELECTOR.ownerOf) {
|
|
118
|
-
const owner = opts.owners[uintArg(args).toString()];
|
|
119
|
-
return owner === undefined ? reverts(opts.missingTokenRevert) : answers(encodeAddress(owner));
|
|
120
|
-
}
|
|
121
|
-
if (selector === SELECTOR.tokenURI) {
|
|
122
|
-
const uri = opts.tokenUris[uintArg(args).toString()];
|
|
123
|
-
return uri === undefined ? reverts(opts.missingTokenRevert) : answers(encodeString(uri));
|
|
124
|
-
}
|
|
125
|
-
return NOTHING;
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
/** An ERC-1155, which has no ownerOf and whose uri carries the {id} template. */
|
|
129
|
-
export function erc1155(opts) {
|
|
130
|
-
const supported = new Set(opts.interfaceIds ?? ["01ffc9a7", "d9b67a26", "0e89341c"]);
|
|
131
|
-
return (selector, args) => {
|
|
132
|
-
if (selector === SELECTOR.supportsInterface) {
|
|
133
|
-
return answers(encodeBool(supported.has(args.slice(0, 8).toLowerCase())));
|
|
134
|
-
}
|
|
135
|
-
if (selector === SELECTOR.uri)
|
|
136
|
-
return answers(encodeString(opts.uriTemplate));
|
|
137
|
-
if (selector === SELECTOR.balanceOf1155) {
|
|
138
|
-
return answers(encodeUint(opts.balances[`${addressArg(args, 0)}:${uintArg(args, 1)}`] ?? 0n));
|
|
139
|
-
}
|
|
140
|
-
return NOTHING;
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
export function chainStub(opts) {
|
|
144
|
-
const reads = [];
|
|
145
|
-
const multicall = (opts.multicall3Address ?? MULTICALL3_ADDRESS).toLowerCase();
|
|
146
|
-
const handlerFor = (address) => opts.contracts[address.toLowerCase()];
|
|
147
|
-
const dispatch = (target, data) => {
|
|
148
|
-
const selector = data.slice(0, 10).toLowerCase();
|
|
149
|
-
const args = data.slice(10);
|
|
150
|
-
if (target.toLowerCase() === multicall) {
|
|
151
|
-
if (opts.withoutMulticallHelpers === true)
|
|
152
|
-
return NOTHING;
|
|
153
|
-
if (selector === SELECTOR.getBlockNumber)
|
|
154
|
-
return answers(encodeUint(opts.blockNumber ?? 0n));
|
|
155
|
-
if (selector === SELECTOR.getEthBalance) {
|
|
156
|
-
return answers(encodeUint(opts.nativeBalances?.[addressArg(args)] ?? 0n));
|
|
157
|
-
}
|
|
158
|
-
return NOTHING;
|
|
159
|
-
}
|
|
160
|
-
const handler = handlerFor(target);
|
|
161
|
-
// An address with no code answers every call with nothing. That is the
|
|
162
|
-
// whole point of the NOT_A_CONTRACT fixture.
|
|
163
|
-
return handler === undefined ? NOTHING : handler(selector, args);
|
|
164
|
-
};
|
|
165
|
-
const reader = async (read) => {
|
|
166
|
-
reads.push(read);
|
|
167
|
-
if (read.method === "eth_getCode") {
|
|
168
|
-
const address = String(read.params[0]).toLowerCase();
|
|
169
|
-
if (opts.code?.[address] !== undefined)
|
|
170
|
-
return opts.code[address];
|
|
171
|
-
return handlerFor(address) === undefined ? "0x" : "0x60806040";
|
|
172
|
-
}
|
|
173
|
-
if (read.method === "eth_getBalance") {
|
|
174
|
-
return encodeUint(opts.nativeBalances?.[String(read.params[0]).toLowerCase()] ?? 0n);
|
|
175
|
-
}
|
|
176
|
-
const call = read.params[0];
|
|
177
|
-
if (call.to.toLowerCase() !== multicall || !call.data.startsWith("0x82ad56cb")) {
|
|
178
|
-
const answer = dispatch(call.to, call.data);
|
|
179
|
-
// An unbatched eth_call cannot return a revert as data: the node errors.
|
|
180
|
-
if (!answer.ok)
|
|
181
|
-
throw new Error(`execution reverted (${answer.data})`);
|
|
182
|
-
return answer.data;
|
|
183
|
-
}
|
|
184
|
-
if (opts.brokenBatchAnswer !== undefined)
|
|
185
|
-
return opts.brokenBatchAnswer;
|
|
186
|
-
const decoded = await abiDecode.execute({
|
|
187
|
-
data: strip(call.data),
|
|
188
|
-
types: ["(address,bool,bytes)[]"],
|
|
189
|
-
});
|
|
190
|
-
if (typeof decoded !== "string")
|
|
191
|
-
throw new Error("AbiDecode did not return a string");
|
|
192
|
-
const [batch] = JSON.parse(decoded).values;
|
|
193
|
-
const results = batch.map(([to, , callData]) => {
|
|
194
|
-
const answer = dispatch(to, callData);
|
|
195
|
-
return [answer.ok, answer.data];
|
|
196
|
-
});
|
|
197
|
-
return strip(await encodeCall("f((bool,bytes)[])", [results]));
|
|
198
|
-
};
|
|
199
|
-
return { reader, reads };
|
|
200
|
-
}
|
|
201
|
-
// ─── token lists ────────────────────────────────────────────────────────────
|
|
202
|
-
export const UNISWAP_LIST = {
|
|
203
|
-
id: "uniswap-default",
|
|
204
|
-
name: "Uniswap Labs Default",
|
|
205
|
-
tokens: [
|
|
206
|
-
{ chainId: 1, address: USDC, symbol: "USDC", name: "USD Coin", decimals: 6 },
|
|
207
|
-
{ chainId: 1, address: MKR, symbol: "MKR", name: "Maker", decimals: 18 },
|
|
208
|
-
],
|
|
209
|
-
};
|
|
210
|
-
/** A second reputable list that happens to carry a different address for USDC. */
|
|
211
|
-
export const RIVAL_LIST = {
|
|
212
|
-
id: "rival-list",
|
|
213
|
-
name: "Somebody Else's List",
|
|
214
|
-
tokens: [{ chainId: 1, address: IMPOSTOR_USDC, symbol: "USDC", name: "USD Coin", decimals: 6 }],
|
|
215
|
-
};
|
|
216
|
-
/** The Cyrillic \u0421 impostor. It is not the same string, and it looks identical. */
|
|
217
|
-
export const CYRILLIC_LIST = {
|
|
218
|
-
id: "shady-list",
|
|
219
|
-
tokens: [
|
|
220
|
-
{ chainId: 1, address: CYRILLIC_USDC, symbol: "USD\u0421", name: "USD Coin", decimals: 6 },
|
|
221
|
-
],
|
|
222
|
-
};
|