@crewhaus/tool-onchain 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 +26 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +15 -3
- package/dist/lib/abi.d.ts +49 -0
- package/dist/lib/abi.js +210 -39
- package/dist/lib/address.d.ts +18 -0
- package/dist/lib/address.js +44 -4
- package/dist/lib/multicall.d.ts +9 -0
- package/dist/lib/multicall.js +21 -7
- package/dist/lib/typed.d.ts +1 -1
- package/dist/lib/typed.js +141 -35
- package/package.json +11 -7
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
|
@@ -66,6 +66,27 @@ nothing on a chain. So:
|
|
|
66
66
|
percentage, because a percentage invites a decimal and a decimal invites a
|
|
67
67
|
float.
|
|
68
68
|
|
|
69
|
+
- **Data that decodes to far more than itself.** Return data comes from
|
|
70
|
+
whoever deployed the contract, and nothing in the ABI format stops two
|
|
71
|
+
offsets pointing at the same bytes: a few kilobytes whose heads all share
|
|
72
|
+
one tail decode to millions of values. `AbiDecode` (and every package that
|
|
73
|
+
decodes through it) refuses data that decodes to more than four times its
|
|
74
|
+
own size — no encoder writes such data — and checks an array's length
|
|
75
|
+
against the bytes that follow it before building anything. It also counts
|
|
76
|
+
the values it produces, tuples and arrays included, and stops at what the
|
|
77
|
+
data could hold for those types if each word were read once. An empty
|
|
78
|
+
tuple `()` is refused as a type to decode: no Solidity type is one, and it
|
|
79
|
+
reads no bytes, so a type string full of them multiplied every word of
|
|
80
|
+
data on its own. Type strings are capped at 8,192 characters and 32 levels
|
|
81
|
+
of nesting.
|
|
82
|
+
|
|
83
|
+
- **An address re-typed between two calls.** `AbiEncodeCall` and
|
|
84
|
+
`TypedDataHash` (message fields and `verifyingContract` alike) check every
|
|
85
|
+
address they encode the way `AddressCheck` does: 0x and 40 hex characters,
|
|
86
|
+
and a mixed-case address must pass its EIP-55 checksum. All-lowercase and
|
|
87
|
+
all-uppercase hex carry no checksum and are accepted, as ethers and viem
|
|
88
|
+
accept them.
|
|
89
|
+
|
|
69
90
|
An address with no checksum is reported as valid **and** as unverifiable,
|
|
70
91
|
rather than letting `valid: true` be read as "no typo". A position with no
|
|
71
92
|
debt has no health factor rather than an infinite one — `Infinity` would read
|
|
@@ -105,7 +126,11 @@ same as all of them, so every entry point takes it as an argument.
|
|
|
105
126
|
- **It does not sign or hold keys.**
|
|
106
127
|
- **It does not know what a contract does.** `AbiDecode` decodes the types
|
|
107
128
|
you name; naming the wrong ones produces confident nonsense, which is why
|
|
108
|
-
data that ends early is an error rather than a short answer.
|
|
129
|
+
data that ends early is an error rather than a short answer. So is a word
|
|
130
|
+
that is not the encoding of its type — a `uint8` holding 256, an `address`
|
|
131
|
+
or `bytes4` with non-zero padding, an `int8` that is not sign-extended —
|
|
132
|
+
which is how Solidity's own decoder treats it, and what reading a
|
|
133
|
+
`uint256` slot as a `uint8` looks like.
|
|
109
134
|
- **It does not fetch an ABI.** Signatures come from the caller.
|
|
110
135
|
- **It does not price anything.** `DefiMath` computes against a reference you
|
|
111
136
|
supply; where that reference came from is your problem.
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,18 @@ import type { RegisteredTool } from "@crewhaus/tool-catalog";
|
|
|
4
4
|
* registers no tool of its own: a batch read is only useful to something that
|
|
5
5
|
* can dial a chain, and nothing in this package dials anything.
|
|
6
6
|
*/
|
|
7
|
-
export { AGGREGATE3_RETURN_TYPE, AGGREGATE3_SELECTOR, AGGREGATE3_SIGNATURE, type Aggregate3Request, type Aggregate3Result, type Call3, ERROR_STRING_SELECTOR, MULTICALL3_ADDRESS, PANIC_REASONS, PANIC_SELECTOR, type RevertKind, type RevertReason, decodeAggregate3, decodeRevertData, encodeAggregate3, } from "./lib/multicall";
|
|
7
|
+
export { AGGREGATE3_RETURN_TYPE, AGGREGATE3_SELECTOR, AGGREGATE3_SIGNATURE, type Aggregate3Request, type Aggregate3Result, type Call3, ERROR_STRING_SELECTOR, MULTICALL3_ADDRESS, PANIC_REASONS, PANIC_SELECTOR, type RevertKind, type RevertReason, decodeAggregate3, decodeRevertData, encodeAggregate3, parseMulticallMap, } from "./lib/multicall";
|
|
8
|
+
/**
|
|
9
|
+
* The canonical form of a function signature, and its four-byte selector —
|
|
10
|
+
* the pair that tells two overloads of one name apart. `uint` is `uint256`
|
|
11
|
+
* here, as it is in the hash; a malformed signature throws, naming what is
|
|
12
|
+
* wrong. Library code for a package that builds tools from an ABI
|
|
13
|
+
* (`@crewhaus/tool-contract-gateway`); like everything here, it dials nothing.
|
|
14
|
+
*/
|
|
15
|
+
export declare function canonicalFunction(signature: string): {
|
|
16
|
+
readonly signature: string;
|
|
17
|
+
readonly selector: string;
|
|
18
|
+
};
|
|
8
19
|
export declare const abiEncodeCall: RegisteredTool;
|
|
9
20
|
export declare const abiDecode: RegisteredTool;
|
|
10
21
|
export declare const functionSelector: RegisteredTool;
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { buildTool } from "@crewhaus/tool-builder";
|
|
20
20
|
import { keccak256Hex } from "@crewhaus/tool-encode";
|
|
21
21
|
import { z } from "zod";
|
|
22
|
-
import { decodeData, encodeCall, parseSignature, selectorOf } from "./lib/abi";
|
|
22
|
+
import { canonicalSignature, decodeData, encodeCall, parseSignature, selectorOf, } from "./lib/abi";
|
|
23
23
|
import { formatUnits, parseUnits, validateAddress } from "./lib/address";
|
|
24
24
|
import { healthFactorBps, maximumIn, minimumOut, priceImpactBps, rescaleDecimals, shareBps, } from "./lib/defi";
|
|
25
25
|
import { personalSignHash, typedDataDigest } from "./lib/typed";
|
|
@@ -28,7 +28,19 @@ import { personalSignHash, typedDataDigest } from "./lib/typed";
|
|
|
28
28
|
* registers no tool of its own: a batch read is only useful to something that
|
|
29
29
|
* can dial a chain, and nothing in this package dials anything.
|
|
30
30
|
*/
|
|
31
|
-
export { AGGREGATE3_RETURN_TYPE, AGGREGATE3_SELECTOR, AGGREGATE3_SIGNATURE, ERROR_STRING_SELECTOR, MULTICALL3_ADDRESS, PANIC_REASONS, PANIC_SELECTOR, decodeAggregate3, decodeRevertData, encodeAggregate3, } from "./lib/multicall";
|
|
31
|
+
export { AGGREGATE3_RETURN_TYPE, AGGREGATE3_SELECTOR, AGGREGATE3_SIGNATURE, ERROR_STRING_SELECTOR, MULTICALL3_ADDRESS, PANIC_REASONS, PANIC_SELECTOR, decodeAggregate3, decodeRevertData, encodeAggregate3, parseMulticallMap, } from "./lib/multicall";
|
|
32
|
+
/**
|
|
33
|
+
* The canonical form of a function signature, and its four-byte selector —
|
|
34
|
+
* the pair that tells two overloads of one name apart. `uint` is `uint256`
|
|
35
|
+
* here, as it is in the hash; a malformed signature throws, naming what is
|
|
36
|
+
* wrong. Library code for a package that builds tools from an ABI
|
|
37
|
+
* (`@crewhaus/tool-contract-gateway`); like everything here, it dials nothing.
|
|
38
|
+
*/
|
|
39
|
+
export function canonicalFunction(signature) {
|
|
40
|
+
const { name, types } = parseSignature(signature);
|
|
41
|
+
const canonical = canonicalSignature(name, types);
|
|
42
|
+
return { signature: canonical, selector: `0x${selectorOf(canonical)}` };
|
|
43
|
+
}
|
|
32
44
|
const json = (value) => JSON.stringify(value);
|
|
33
45
|
const LIMITS = { args: 256, types: 64, hexChars: 2_000_000 };
|
|
34
46
|
/** Accepts a bigint-safe integer: a decimal or hex string, or a safe number. */
|
|
@@ -77,7 +89,7 @@ export const abiEncodeCall = buildTool({
|
|
|
77
89
|
});
|
|
78
90
|
export const abiDecode = buildTool({
|
|
79
91
|
name: "AbiDecode",
|
|
80
|
-
description: "Decode ABI-encoded hex — an eth_call result, a log's data, a transaction's arguments, revert data — into named, typed values. Use it to read a contract's answer without a model squinting at 32-byte words. Integers come back as decimal STRINGS so a uint256 survives JSON intact; addresses come back lowercase and hex-prefixed. Data that ends early is an error rather than a plausible short answer.",
|
|
92
|
+
description: "Decode ABI-encoded hex — an eth_call result, a log's data, a transaction's arguments, revert data — into named, typed values. Use it to read a contract's answer without a model squinting at 32-byte words. Integers come back as decimal STRINGS so a uint256 survives JSON intact; addresses come back lowercase and hex-prefixed. Data that ends early is an error rather than a plausible short answer, and so is a word that no encoder writes for its type — a uint8 holding 256, an address or bytes4 with non-zero padding, an int8 that is not sign-extended — because that is what naming the wrong types looks like.",
|
|
81
93
|
inputSchema: z.object({
|
|
82
94
|
data: z.string().max(LIMITS.hexChars).describe("0x hex, without a function selector"),
|
|
83
95
|
types: z.array(z.string()).min(1).max(LIMITS.types).describe('e.g. ["uint256", "address"]'),
|
package/dist/lib/abi.d.ts
CHANGED
|
@@ -11,7 +11,27 @@ export type AbiType = {
|
|
|
11
11
|
/** Canonical form, which is what the selector is computed over. */
|
|
12
12
|
readonly canonical: string;
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* The longest type string, and the deepest nesting of arrays and tuples,
|
|
16
|
+
* this parses. Each level rescans the text beneath it, so an unbounded one
|
|
17
|
+
* costs time in the square of its length; real ABI types are a few hundred
|
|
18
|
+
* characters and a handful of levels deep.
|
|
19
|
+
*/
|
|
20
|
+
export declare const MAX_TYPE_CHARS = 8192;
|
|
21
|
+
export declare const MAX_TYPE_DEPTH = 32;
|
|
14
22
|
export declare function parseType(raw: string): AbiType;
|
|
23
|
+
/**
|
|
24
|
+
* Hex to bytes, strictly: an odd number of digits or a non-hex character is
|
|
25
|
+
* refused, never truncated or skipped. The one hex decoder the coder and the
|
|
26
|
+
* EIP-712 hasher share, so they cannot disagree about what a value is.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hexToBytes(raw: string, what: string): Uint8Array;
|
|
29
|
+
/**
|
|
30
|
+
* A `bytes` or `bytesN` value: a hex string. A number is refused rather than
|
|
31
|
+
* read as its decimal digits reinterpreted as hex, which is how 4660 would
|
|
32
|
+
* become 0x4660.
|
|
33
|
+
*/
|
|
34
|
+
export declare function bytesArg(value: unknown, what: string): Uint8Array;
|
|
15
35
|
/** Head/tail encoding for a list of types, which is what a tuple is. */
|
|
16
36
|
export declare function encodeTuple(types: ReadonlyArray<AbiType>, values: ReadonlyArray<AbiValue>, what?: string): Uint8Array;
|
|
17
37
|
/** The canonical signature a selector is hashed over. */
|
|
@@ -25,5 +45,34 @@ export declare function parseSignature(signature: string): {
|
|
|
25
45
|
export declare function encodeCall(signature: string, args: ReadonlyArray<AbiValue>): string;
|
|
26
46
|
/** Decoded values use strings for integers, so a uint256 survives JSON. */
|
|
27
47
|
export type Decoded = string | boolean | Decoded[];
|
|
48
|
+
/**
|
|
49
|
+
* How far a decode may inflate its input.
|
|
50
|
+
*
|
|
51
|
+
* An ABI encoding points at its dynamic values with offsets, and nothing in
|
|
52
|
+
* the format stops two offsets pointing at the same bytes. No encoder writes
|
|
53
|
+
* that — the compilers, ethers and viem each write every tail once — but a
|
|
54
|
+
* hostile contract or a crafted blob can: n heads sharing one tail decode it
|
|
55
|
+
* n times, at every level of nesting, so a few kilobytes decode to millions
|
|
56
|
+
* of values, and one call pins a core and fills the heap.
|
|
57
|
+
*
|
|
58
|
+
* So a decode is metered twice:
|
|
59
|
+
*
|
|
60
|
+
* - Every word read and every word of string or bytes payload costs one
|
|
61
|
+
* unit, and a decode may spend {@link MAX_INFLATION} units per word of
|
|
62
|
+
* input (and at least {@link BUDGET_FLOOR}). An honest encoding reads each
|
|
63
|
+
* word about once and never comes near the limit.
|
|
64
|
+
* - Every value decoded — a tuple and an array included, not only the words
|
|
65
|
+
* in them — is counted, and a decode may produce at most `1 + n` values
|
|
66
|
+
* per word of input, where `n` is how deeply the types nest. A value either
|
|
67
|
+
* reads a word of its own or is a tuple or an array holding at least one
|
|
68
|
+
* value, so an encoding that reads each word once never produces more.
|
|
69
|
+
* Without this count a type string could multiply its data on its own:
|
|
70
|
+
* `((),(),…,uint256)[]` read one word per item and returned thousands of
|
|
71
|
+
* values for it.
|
|
72
|
+
*
|
|
73
|
+
* An empty tuple `()` is refused outright: no Solidity type is one, and it
|
|
74
|
+
* decodes from no bytes at all, so nothing in the data could back it.
|
|
75
|
+
*/
|
|
76
|
+
export declare const MAX_INFLATION = 4;
|
|
28
77
|
export declare function decodeTuple(types: ReadonlyArray<AbiType>, data: Uint8Array, base?: number, what?: string): Decoded[];
|
|
29
78
|
export declare function decodeData(typeList: ReadonlyArray<string>, hex: string): Decoded[];
|
package/dist/lib/abi.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* ether.
|
|
14
14
|
*/
|
|
15
15
|
import { keccak256, toHex } from "@crewhaus/tool-encode";
|
|
16
|
+
import { checkedAddress } from "./address";
|
|
16
17
|
const WORD = 32;
|
|
17
18
|
const INT_RE = /^(u?int)(\d*)$/;
|
|
18
19
|
const BYTES_RE = /^bytes(\d*)$/;
|
|
@@ -37,18 +38,41 @@ function splitTop(text) {
|
|
|
37
38
|
parts.push(current.trim());
|
|
38
39
|
return parts;
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The longest type string, and the deepest nesting of arrays and tuples,
|
|
43
|
+
* this parses. Each level rescans the text beneath it, so an unbounded one
|
|
44
|
+
* costs time in the square of its length; real ABI types are a few hundred
|
|
45
|
+
* characters and a handful of levels deep.
|
|
46
|
+
*/
|
|
47
|
+
export const MAX_TYPE_CHARS = 8192;
|
|
48
|
+
export const MAX_TYPE_DEPTH = 32;
|
|
40
49
|
export function parseType(raw) {
|
|
50
|
+
if (raw.length > MAX_TYPE_CHARS) {
|
|
51
|
+
throw new Error(`a type of ${raw.length} characters is longer than the ${MAX_TYPE_CHARS} this reads`);
|
|
52
|
+
}
|
|
53
|
+
return parseTypeAt(raw, 0);
|
|
54
|
+
}
|
|
55
|
+
function parseTypeAt(raw, depth) {
|
|
41
56
|
const text = raw.trim();
|
|
42
57
|
if (text === "")
|
|
43
58
|
throw new Error("an empty string is not an ABI type");
|
|
59
|
+
if (depth > MAX_TYPE_DEPTH) {
|
|
60
|
+
throw new Error(`the type nests arrays and tuples more than ${MAX_TYPE_DEPTH} levels deep`);
|
|
61
|
+
}
|
|
44
62
|
// Array suffix, innermost last: `uint256[2][]` is a dynamic array of
|
|
45
63
|
// fixed-length-2 arrays, so the LAST suffix is the outer type.
|
|
46
64
|
const arrayMatch = /^(.*)\[(\d*)\]$/.exec(text);
|
|
47
65
|
if (arrayMatch) {
|
|
48
|
-
const child =
|
|
66
|
+
const child = parseTypeAt(arrayMatch[1], depth + 1);
|
|
49
67
|
const length = arrayMatch[2] === "" ? -1 : Number.parseInt(arrayMatch[2], 10);
|
|
50
68
|
if (length === 0)
|
|
51
69
|
throw new Error(`"${text}" has a zero-length array, which cannot hold a value`);
|
|
70
|
+
// A fixed length is laid out inline, so its size must be a number this
|
|
71
|
+
// can count in: a type string is not a reason to believe in 2^64 items.
|
|
72
|
+
if (length !== -1 &&
|
|
73
|
+
!Number.isSafeInteger(length * (child.dynamic ? WORD : staticSize(child)))) {
|
|
74
|
+
throw new Error(`"${text}" is a fixed array too large to lay out`);
|
|
75
|
+
}
|
|
52
76
|
return {
|
|
53
77
|
base: "array",
|
|
54
78
|
bits: 0,
|
|
@@ -60,7 +84,7 @@ export function parseType(raw) {
|
|
|
60
84
|
};
|
|
61
85
|
}
|
|
62
86
|
if (text.startsWith("(") && text.endsWith(")")) {
|
|
63
|
-
const components = splitTop(text.slice(1, -1)).map(
|
|
87
|
+
const components = splitTop(text.slice(1, -1)).map((c) => parseTypeAt(c, depth + 1));
|
|
64
88
|
return {
|
|
65
89
|
base: "tuple",
|
|
66
90
|
bits: 0,
|
|
@@ -150,7 +174,12 @@ export function parseType(raw) {
|
|
|
150
174
|
}
|
|
151
175
|
throw new Error(`"${text}" is not an ABI type this understands`);
|
|
152
176
|
}
|
|
153
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Hex to bytes, strictly: an odd number of digits or a non-hex character is
|
|
179
|
+
* refused, never truncated or skipped. The one hex decoder the coder and the
|
|
180
|
+
* EIP-712 hasher share, so they cannot disagree about what a value is.
|
|
181
|
+
*/
|
|
182
|
+
export function hexToBytes(raw, what) {
|
|
154
183
|
const text = raw.startsWith("0x") || raw.startsWith("0X") ? raw.slice(2) : raw;
|
|
155
184
|
if (text.length % 2 !== 0)
|
|
156
185
|
throw new Error(`${what} has an odd number of hex digits`);
|
|
@@ -161,6 +190,17 @@ function hexToBytes(raw, what) {
|
|
|
161
190
|
out[i] = Number.parseInt(text.slice(i * 2, i * 2 + 2), 16);
|
|
162
191
|
return out;
|
|
163
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* A `bytes` or `bytesN` value: a hex string. A number is refused rather than
|
|
195
|
+
* read as its decimal digits reinterpreted as hex, which is how 4660 would
|
|
196
|
+
* become 0x4660.
|
|
197
|
+
*/
|
|
198
|
+
export function bytesArg(value, what) {
|
|
199
|
+
if (typeof value !== "string") {
|
|
200
|
+
throw new Error(`${what}: bytes are a 0x hex string, not a ${typeof value}`);
|
|
201
|
+
}
|
|
202
|
+
return hexToBytes(value, what);
|
|
203
|
+
}
|
|
164
204
|
/** Two's complement into a 32-byte word, big-endian. */
|
|
165
205
|
function wordFromBigInt(value, signed, bits, what) {
|
|
166
206
|
const limit = 1n << BigInt(bits);
|
|
@@ -255,18 +295,16 @@ function encodeValue(type, value, what) {
|
|
|
255
295
|
return type.dynamic ? { head: none, tail: body } : { head: body, tail: none };
|
|
256
296
|
}
|
|
257
297
|
if (type.base === "string" || type.base === "bytes") {
|
|
258
|
-
const bytes = type.base === "string"
|
|
259
|
-
? new TextEncoder().encode(String(value))
|
|
260
|
-
: hexToBytes(String(value), what);
|
|
298
|
+
const bytes = type.base === "string" ? new TextEncoder().encode(String(value)) : bytesArg(value, what);
|
|
261
299
|
return {
|
|
262
300
|
head: none,
|
|
263
301
|
tail: concat([wordFromBigInt(BigInt(bytes.length), false, 256, what), padRight(bytes)]),
|
|
264
302
|
};
|
|
265
303
|
}
|
|
266
304
|
if (type.base === "address") {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
305
|
+
// Shape AND checksum: a mixed-case address that fails EIP-55 has a wrong
|
|
306
|
+
// character in it, and encoding it anyway sends to that wrong address.
|
|
307
|
+
const bytes = hexToBytes(checkedAddress(value, what), what);
|
|
270
308
|
const word = new Uint8Array(WORD);
|
|
271
309
|
word.set(bytes, 12);
|
|
272
310
|
return { head: word, tail: none };
|
|
@@ -277,7 +315,7 @@ function encodeValue(type, value, what) {
|
|
|
277
315
|
return { head: wordFromBigInt(value ? 1n : 0n, false, 8, what), tail: none };
|
|
278
316
|
}
|
|
279
317
|
if (type.base === "bytesN") {
|
|
280
|
-
const bytes =
|
|
318
|
+
const bytes = bytesArg(value, what);
|
|
281
319
|
const size = type.bits / 8;
|
|
282
320
|
if (bytes.length !== size)
|
|
283
321
|
throw new Error(`${what}: bytes${size} needs ${size} bytes, got ${bytes.length}`);
|
|
@@ -324,6 +362,9 @@ export function selectorOf(signature) {
|
|
|
324
362
|
}
|
|
325
363
|
/** Split `transfer(address,uint256)` into its name and parsed argument types. */
|
|
326
364
|
export function parseSignature(signature) {
|
|
365
|
+
if (signature.length > MAX_TYPE_CHARS) {
|
|
366
|
+
throw new Error(`a signature of ${signature.length} characters is longer than the ${MAX_TYPE_CHARS} this reads`);
|
|
367
|
+
}
|
|
327
368
|
const open = signature.indexOf("(");
|
|
328
369
|
if (open === -1 || !signature.trim().endsWith(")")) {
|
|
329
370
|
throw new Error(`"${signature}" is not a function signature — expected name(type,type)`);
|
|
@@ -343,78 +384,208 @@ export function encodeCall(signature, args) {
|
|
|
343
384
|
const canonical = canonicalSignature(name, types);
|
|
344
385
|
return `0x${selectorOf(canonical)}${toHex(encodeTuple(types, args))}`;
|
|
345
386
|
}
|
|
346
|
-
|
|
387
|
+
/**
|
|
388
|
+
* How far a decode may inflate its input.
|
|
389
|
+
*
|
|
390
|
+
* An ABI encoding points at its dynamic values with offsets, and nothing in
|
|
391
|
+
* the format stops two offsets pointing at the same bytes. No encoder writes
|
|
392
|
+
* that — the compilers, ethers and viem each write every tail once — but a
|
|
393
|
+
* hostile contract or a crafted blob can: n heads sharing one tail decode it
|
|
394
|
+
* n times, at every level of nesting, so a few kilobytes decode to millions
|
|
395
|
+
* of values, and one call pins a core and fills the heap.
|
|
396
|
+
*
|
|
397
|
+
* So a decode is metered twice:
|
|
398
|
+
*
|
|
399
|
+
* - Every word read and every word of string or bytes payload costs one
|
|
400
|
+
* unit, and a decode may spend {@link MAX_INFLATION} units per word of
|
|
401
|
+
* input (and at least {@link BUDGET_FLOOR}). An honest encoding reads each
|
|
402
|
+
* word about once and never comes near the limit.
|
|
403
|
+
* - Every value decoded — a tuple and an array included, not only the words
|
|
404
|
+
* in them — is counted, and a decode may produce at most `1 + n` values
|
|
405
|
+
* per word of input, where `n` is how deeply the types nest. A value either
|
|
406
|
+
* reads a word of its own or is a tuple or an array holding at least one
|
|
407
|
+
* value, so an encoding that reads each word once never produces more.
|
|
408
|
+
* Without this count a type string could multiply its data on its own:
|
|
409
|
+
* `((),(),…,uint256)[]` read one word per item and returned thousands of
|
|
410
|
+
* values for it.
|
|
411
|
+
*
|
|
412
|
+
* An empty tuple `()` is refused outright: no Solidity type is one, and it
|
|
413
|
+
* decodes from no bytes at all, so nothing in the data could back it.
|
|
414
|
+
*/
|
|
415
|
+
export const MAX_INFLATION = 4;
|
|
416
|
+
const BUDGET_FLOOR = 1024;
|
|
417
|
+
/** How many tuples and arrays deep a type goes: 0 for a word, 1 for `uint256[]`. */
|
|
418
|
+
function nesting(type) {
|
|
419
|
+
if (type.base === "array")
|
|
420
|
+
return 1 + nesting(type.child);
|
|
421
|
+
if (type.base === "tuple") {
|
|
422
|
+
return 1 + type.components.reduce((deepest, c) => Math.max(deepest, nesting(c)), 0);
|
|
423
|
+
}
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
426
|
+
function budgetFor(data, types) {
|
|
427
|
+
const words = Math.ceil(data.length / WORD);
|
|
428
|
+
const perWord = 1 + types.reduce((deepest, t) => Math.max(deepest, nesting(t)), 0);
|
|
429
|
+
const maxValues = Math.max(BUDGET_FLOOR, words * perWord);
|
|
430
|
+
return {
|
|
431
|
+
remaining: Math.max(BUDGET_FLOOR, words * MAX_INFLATION),
|
|
432
|
+
values: maxValues,
|
|
433
|
+
maxValues,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function charge(budget, units, what) {
|
|
437
|
+
budget.remaining -= units;
|
|
438
|
+
if (budget.remaining < 0) {
|
|
439
|
+
throw new Error(`${what}: the data decodes to more than ${MAX_INFLATION} times its own size — its offsets point at the same bytes again and again, which no ABI encoder writes. Refusing to inflate it.`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/** One decoded value, counted against the decode's value budget. */
|
|
443
|
+
function chargeValue(budget, what) {
|
|
444
|
+
budget.values -= 1;
|
|
445
|
+
if (budget.values < 0) {
|
|
446
|
+
throw new Error(`${what}: the data decodes to more than ${budget.maxValues} values, more than its size can hold for these types when each word is read once — its offsets point at the same bytes again and again, or the types multiply it. Refusing to inflate it.`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function wordAt(data, offset, what, budget) {
|
|
347
450
|
if (offset + WORD > data.length) {
|
|
348
451
|
throw new Error(`${what}: the data ends before offset ${offset}, so it is truncated or mistyped`);
|
|
349
452
|
}
|
|
453
|
+
charge(budget, 1, what);
|
|
350
454
|
return data.subarray(offset, offset + WORD);
|
|
351
455
|
}
|
|
352
|
-
|
|
456
|
+
/** A word read as an unsigned 256-bit integer: an offset, a length, or a value to check. */
|
|
457
|
+
function uintFromWord(word) {
|
|
353
458
|
let value = 0n;
|
|
354
459
|
for (const byte of word)
|
|
355
460
|
value = (value << 8n) | BigInt(byte);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
/** How many bytes one element of this type takes where its parent lays it out. */
|
|
464
|
+
function headSize(type) {
|
|
465
|
+
return type.dynamic ? WORD : staticSize(type);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Decode `count` elements of one type laid out from `start` — an array's
|
|
469
|
+
* items. The data must be able to hold them BEFORE anything is decoded or
|
|
470
|
+
* allocated: a length is a claim the data makes about itself, and a fixed
|
|
471
|
+
* length is a claim the caller's type string makes.
|
|
472
|
+
*/
|
|
473
|
+
function decodeRepeated(count, child, data, start, what, budget) {
|
|
474
|
+
const each = headSize(child);
|
|
475
|
+
// A zero-size element (an empty tuple) takes no bytes, so no length the
|
|
476
|
+
// data claims for it is backed by anything.
|
|
477
|
+
if (each === 0 && count > 0) {
|
|
478
|
+
throw new Error(`${what}: an array of ${child.canonical} holds nothing the data can back`);
|
|
479
|
+
}
|
|
480
|
+
const need = count * each;
|
|
481
|
+
if (!Number.isSafeInteger(need) || start + need > data.length) {
|
|
482
|
+
throw new Error(`${what}: claims ${count} items, more than the data could hold`);
|
|
483
|
+
}
|
|
484
|
+
return decodeSequence(count, () => child, data, start, what, budget);
|
|
362
485
|
}
|
|
363
|
-
function decodeValue(type, data, offset, what) {
|
|
486
|
+
function decodeValue(type, data, offset, what, budget) {
|
|
487
|
+
chargeValue(budget, what);
|
|
364
488
|
if (type.base === "array") {
|
|
365
489
|
const child = type.child;
|
|
366
490
|
if (type.arrayLength === -1) {
|
|
367
|
-
const length = Number(
|
|
368
|
-
|
|
369
|
-
throw new Error(`${what}: claims ${length} items, more than the data could hold`);
|
|
370
|
-
}
|
|
371
|
-
return decodeTuple(new Array(length).fill(child), data, offset + WORD, what);
|
|
491
|
+
const length = Number(uintFromWord(wordAt(data, offset, what, budget)));
|
|
492
|
+
return decodeRepeated(length, child, data, offset + WORD, what, budget);
|
|
372
493
|
}
|
|
373
|
-
return
|
|
494
|
+
return decodeRepeated(type.arrayLength, child, data, offset, what, budget);
|
|
495
|
+
}
|
|
496
|
+
if (type.base === "tuple") {
|
|
497
|
+
const components = type.components;
|
|
498
|
+
if (components.length === 0) {
|
|
499
|
+
throw new Error(`${what}: () is an empty tuple — no Solidity type is one, and it decodes from no bytes, so nothing in the data can back it`);
|
|
500
|
+
}
|
|
501
|
+
return decodeSequence(components.length, (i) => components[i], data, offset, what, budget);
|
|
374
502
|
}
|
|
375
|
-
if (type.base === "tuple")
|
|
376
|
-
return decodeTuple(type.components, data, offset, what);
|
|
377
503
|
if (type.base === "string" || type.base === "bytes") {
|
|
378
|
-
const length = Number(
|
|
504
|
+
const length = Number(uintFromWord(wordAt(data, offset, what, budget)));
|
|
379
505
|
const start = offset + WORD;
|
|
380
506
|
if (start + length > data.length) {
|
|
381
507
|
throw new Error(`${what}: declares ${length} bytes, past the end of the data`);
|
|
382
508
|
}
|
|
509
|
+
charge(budget, Math.ceil(length / WORD), what);
|
|
383
510
|
const bytes = data.subarray(start, start + length);
|
|
384
511
|
return type.base === "string" ? new TextDecoder().decode(bytes) : `0x${toHex(bytes)}`;
|
|
385
512
|
}
|
|
386
|
-
|
|
387
|
-
|
|
513
|
+
return decodeStaticWord(type, wordAt(data, offset, what, budget), what);
|
|
514
|
+
}
|
|
515
|
+
/** True when every byte in the slice is zero. */
|
|
516
|
+
function allZero(bytes) {
|
|
517
|
+
return bytes.every((b) => b === 0);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* One static word, read as its type — and refused unless it is the ONE word
|
|
521
|
+
* an encoder writes for a value of that type.
|
|
522
|
+
*
|
|
523
|
+
* A 32-byte word holds more than an address, a uint8 or a bytes4 can, and the
|
|
524
|
+
* ABI says what the rest must be: zero padding, or a sign extension. Solidity's
|
|
525
|
+
* decoder reverts on a word that breaks that; reading it anyway returns a value
|
|
526
|
+
* the named type cannot hold — a uint8 of 2^256−1, an "address" read from the
|
|
527
|
+
* low bytes of a balance — which is what naming the wrong types looks like,
|
|
528
|
+
* and a plausible answer to a question nobody asked. So it is an error, as a
|
|
529
|
+
* bool word of 2 always was.
|
|
530
|
+
*/
|
|
531
|
+
function decodeStaticWord(type, word, what) {
|
|
532
|
+
const value = uintFromWord(word);
|
|
533
|
+
if (type.base === "address") {
|
|
534
|
+
if (!allZero(word.subarray(0, 12))) {
|
|
535
|
+
throw new Error(`${what}: an address word has non-zero upper bytes (0x${toHex(word.subarray(0, 12))}), so it is not an address — the data is mistyped or was not ABI-encoded`);
|
|
536
|
+
}
|
|
388
537
|
return `0x${toHex(word.subarray(12))}`;
|
|
538
|
+
}
|
|
389
539
|
if (type.base === "bool") {
|
|
390
|
-
const value = bigIntFromWord(word, false, 256);
|
|
391
540
|
if (value > 1n)
|
|
392
541
|
throw new Error(`${what}: a bool word holds ${value}, which is neither true nor false`);
|
|
393
542
|
return value === 1n;
|
|
394
543
|
}
|
|
395
|
-
if (type.base === "bytesN")
|
|
396
|
-
|
|
397
|
-
|
|
544
|
+
if (type.base === "bytesN") {
|
|
545
|
+
const size = type.bits / 8;
|
|
546
|
+
if (!allZero(word.subarray(size))) {
|
|
547
|
+
throw new Error(`${what}: a bytes${size} word has non-zero padding after its ${size} bytes — the data is mistyped or was not ABI-encoded`);
|
|
548
|
+
}
|
|
549
|
+
return `0x${toHex(word.subarray(0, size))}`;
|
|
550
|
+
}
|
|
551
|
+
if (type.base === "int") {
|
|
552
|
+
const signed = BigInt.asIntN(type.bits, value);
|
|
553
|
+
// A canonical intN word is the sign extension of its low N bits: every
|
|
554
|
+
// bit above them equals the sign bit.
|
|
555
|
+
if (BigInt.asUintN(256, signed) !== value) {
|
|
556
|
+
throw new Error(`${what}: the word is not a sign-extended int${type.bits} — its upper bits are neither all zero nor all one — so the data is mistyped or was not ABI-encoded`);
|
|
557
|
+
}
|
|
558
|
+
return signed.toString();
|
|
559
|
+
}
|
|
560
|
+
if (value >> BigInt(type.bits) !== 0n) {
|
|
561
|
+
throw new Error(`${what}: a uint${type.bits} word holds ${value}, which does not fit in ${type.bits} bits — the data is mistyped or was not ABI-encoded`);
|
|
562
|
+
}
|
|
563
|
+
return value.toString();
|
|
398
564
|
}
|
|
399
|
-
|
|
565
|
+
/** Head/tail decoding of `count` values laid out from `base`: a tuple, or an array's items. */
|
|
566
|
+
function decodeSequence(count, typeAt, data, base, what, budget) {
|
|
400
567
|
const out = [];
|
|
401
568
|
let head = base;
|
|
402
|
-
for (
|
|
569
|
+
for (let i = 0; i < count; i++) {
|
|
570
|
+
const type = typeAt(i);
|
|
403
571
|
const label = `${what}[${i}]`;
|
|
404
572
|
if (type.dynamic) {
|
|
405
|
-
const offset = Number(
|
|
573
|
+
const offset = Number(uintFromWord(wordAt(data, head, label, budget)));
|
|
406
574
|
// An offset is relative to the start of the enclosing tuple. Treating
|
|
407
575
|
// it as absolute reads the wrong bytes and usually still "works".
|
|
408
|
-
out.push(decodeValue(type, data, base + offset, label));
|
|
576
|
+
out.push(decodeValue(type, data, base + offset, label, budget));
|
|
409
577
|
head += WORD;
|
|
410
578
|
}
|
|
411
579
|
else {
|
|
412
|
-
out.push(decodeValue(type, data, head, label));
|
|
580
|
+
out.push(decodeValue(type, data, head, label, budget));
|
|
413
581
|
head += staticSize(type);
|
|
414
582
|
}
|
|
415
583
|
}
|
|
416
584
|
return out;
|
|
417
585
|
}
|
|
586
|
+
export function decodeTuple(types, data, base = 0, what = "value") {
|
|
587
|
+
return decodeSequence(types.length, (i) => types[i], data, base, what, budgetFor(data, types));
|
|
588
|
+
}
|
|
418
589
|
/** How many bytes a static type occupies in a head. */
|
|
419
590
|
function staticSize(type) {
|
|
420
591
|
if (type.base === "tuple")
|
package/dist/lib/address.d.ts
CHANGED
|
@@ -11,6 +11,24 @@ export type AddressResult = {
|
|
|
11
11
|
/** Apply the EIP-55 case checksum to a lowercase address. */
|
|
12
12
|
export declare function toChecksumAddress(lowercase: string): string;
|
|
13
13
|
export declare function validateAddress(raw: string): AddressResult;
|
|
14
|
+
/**
|
|
15
|
+
* An address a coder may put into calldata or a digest: 0x and 40 hex
|
|
16
|
+
* characters, whose EIP-55 checksum holds when it carries one. Returns the
|
|
17
|
+
* checksummed form; throws the reason otherwise.
|
|
18
|
+
*
|
|
19
|
+
* Every path that writes an address goes through this, not only
|
|
20
|
+
* `AddressCheck`: the encoder is the last point where a re-typed character
|
|
21
|
+
* can still be caught, and ethers and viem refuse the same input.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkedAddress(raw: unknown, what: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Whether `text` is an optional `-`, digits, an optional `.` and digits: what
|
|
26
|
+
* `/^-?\d*\.?\d*$/.test(text)` answered, in one pass. That pattern gave
|
|
27
|
+
* every digit back to the second run in turn when the text did not end where
|
|
28
|
+
* it should, so a long run of digits before a stray character was quadratic
|
|
29
|
+
* in its length, and the amount is the model's.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isDecimalText(text: string): boolean;
|
|
14
32
|
/**
|
|
15
33
|
* Parse a decimal amount into base units, exactly.
|
|
16
34
|
*
|
package/dist/lib/address.js
CHANGED
|
@@ -31,9 +31,11 @@ export function validateAddress(raw) {
|
|
|
31
31
|
valid: false,
|
|
32
32
|
checksummed: "",
|
|
33
33
|
hadChecksum: false,
|
|
34
|
-
reason:
|
|
35
|
-
?
|
|
36
|
-
: "
|
|
34
|
+
reason: text.startsWith("0X")
|
|
35
|
+
? "an address starts with a lowercase 0x; this starts with 0X"
|
|
36
|
+
: text.startsWith("0x")
|
|
37
|
+
? `an address is 0x followed by 40 hex characters; this has ${body.length}`
|
|
38
|
+
: "an address starts with 0x",
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
const checksummed = toChecksumAddress(body);
|
|
@@ -59,6 +61,44 @@ export function validateAddress(raw) {
|
|
|
59
61
|
: "this address carries no EIP-55 checksum, so only its shape was checked; a typo in an all-lowercase address cannot be detected",
|
|
60
62
|
};
|
|
61
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* An address a coder may put into calldata or a digest: 0x and 40 hex
|
|
66
|
+
* characters, whose EIP-55 checksum holds when it carries one. Returns the
|
|
67
|
+
* checksummed form; throws the reason otherwise.
|
|
68
|
+
*
|
|
69
|
+
* Every path that writes an address goes through this, not only
|
|
70
|
+
* `AddressCheck`: the encoder is the last point where a re-typed character
|
|
71
|
+
* can still be caught, and ethers and viem refuse the same input.
|
|
72
|
+
*/
|
|
73
|
+
export function checkedAddress(raw, what) {
|
|
74
|
+
if (typeof raw !== "string")
|
|
75
|
+
throw new Error(`${what}: an address is a 0x hex string`);
|
|
76
|
+
const result = validateAddress(raw);
|
|
77
|
+
if (!result.valid)
|
|
78
|
+
throw new Error(`${what}: ${result.reason}`);
|
|
79
|
+
return result.checksummed;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether `text` is an optional `-`, digits, an optional `.` and digits: what
|
|
83
|
+
* `/^-?\d*\.?\d*$/.test(text)` answered, in one pass. That pattern gave
|
|
84
|
+
* every digit back to the second run in turn when the text did not end where
|
|
85
|
+
* it should, so a long run of digits before a stray character was quadratic
|
|
86
|
+
* in its length, and the amount is the model's.
|
|
87
|
+
*/
|
|
88
|
+
export function isDecimalText(text) {
|
|
89
|
+
const digit = (i) => {
|
|
90
|
+
const c = text.charCodeAt(i);
|
|
91
|
+
return c >= 0x30 && c <= 0x39;
|
|
92
|
+
};
|
|
93
|
+
let i = text.charCodeAt(0) === 0x2d ? 1 : 0;
|
|
94
|
+
while (i < text.length && digit(i))
|
|
95
|
+
i++;
|
|
96
|
+
if (text.charCodeAt(i) === 0x2e)
|
|
97
|
+
i++;
|
|
98
|
+
while (i < text.length && digit(i))
|
|
99
|
+
i++;
|
|
100
|
+
return i === text.length;
|
|
101
|
+
}
|
|
62
102
|
/**
|
|
63
103
|
* Parse a decimal amount into base units, exactly.
|
|
64
104
|
*
|
|
@@ -67,7 +107,7 @@ export function validateAddress(raw) {
|
|
|
67
107
|
*/
|
|
68
108
|
export function parseUnits(amount, decimals) {
|
|
69
109
|
const text = amount.trim();
|
|
70
|
-
if (
|
|
110
|
+
if (!isDecimalText(text) || text === "" || text === "." || text === "-") {
|
|
71
111
|
throw new Error(`"${amount}" is not a decimal amount`);
|
|
72
112
|
}
|
|
73
113
|
const negative = text.startsWith("-");
|
package/dist/lib/multicall.d.ts
CHANGED
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
* Every entry point here takes the address as an argument.
|
|
8
8
|
*/
|
|
9
9
|
export declare const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
10
|
+
/**
|
|
11
|
+
* An operator's own Multicall3 deployments, from a tool_config block:
|
|
12
|
+
* `{ "<chainId>": "0x…" }`, for a chain where the deterministic deploy is not
|
|
13
|
+
* at {@link MULTICALL3_ADDRESS}. Every value must be an address whose
|
|
14
|
+
* checksum holds — an aggregator answers every read in a batch, so a typo
|
|
15
|
+
* here is every answer wrong. Returns chain id → EIP-55 address; `undefined`
|
|
16
|
+
* is an empty map.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseMulticallMap(input: unknown, what: string): ReadonlyMap<string, string>;
|
|
10
19
|
/** The canonical signature, which is what the selector is hashed over. */
|
|
11
20
|
export declare const AGGREGATE3_SIGNATURE = "aggregate3((address,bool,bytes)[])";
|
|
12
21
|
/** `0x82ad56cb` — the four bytes a block explorer shows for a Multicall3 batch. */
|
package/dist/lib/multicall.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* or submits a transaction.
|
|
28
28
|
*/
|
|
29
29
|
import { decodeData, encodeCall } from "./abi";
|
|
30
|
-
import {
|
|
30
|
+
import { checkedAddress } from "./address";
|
|
31
31
|
/**
|
|
32
32
|
* Where Multicall3 is deployed on most chains, via a deterministic deploy.
|
|
33
33
|
*
|
|
@@ -37,6 +37,26 @@ import { validateAddress } from "./address";
|
|
|
37
37
|
* Every entry point here takes the address as an argument.
|
|
38
38
|
*/
|
|
39
39
|
export const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
40
|
+
/**
|
|
41
|
+
* An operator's own Multicall3 deployments, from a tool_config block:
|
|
42
|
+
* `{ "<chainId>": "0x…" }`, for a chain where the deterministic deploy is not
|
|
43
|
+
* at {@link MULTICALL3_ADDRESS}. Every value must be an address whose
|
|
44
|
+
* checksum holds — an aggregator answers every read in a batch, so a typo
|
|
45
|
+
* here is every answer wrong. Returns chain id → EIP-55 address; `undefined`
|
|
46
|
+
* is an empty map.
|
|
47
|
+
*/
|
|
48
|
+
export function parseMulticallMap(input, what) {
|
|
49
|
+
const out = new Map();
|
|
50
|
+
if (input === undefined)
|
|
51
|
+
return out;
|
|
52
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
53
|
+
throw new Error(`${what} must map a chain id to its Multicall3 address, for example { "324": "0xF9cda624FBC7e059355ce98a31693d299FACd963" }`);
|
|
54
|
+
}
|
|
55
|
+
for (const [chainId, address] of Object.entries(input)) {
|
|
56
|
+
out.set(chainId.trim(), checkedAddress(address, `${what}["${chainId}"]`));
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
40
60
|
/** The canonical signature, which is what the selector is hashed over. */
|
|
41
61
|
export const AGGREGATE3_SIGNATURE = "aggregate3((address,bool,bytes)[])";
|
|
42
62
|
/** `0x82ad56cb` — the four bytes a block explorer shows for a Multicall3 batch. */
|
|
@@ -63,12 +83,6 @@ export const PANIC_REASONS = Object.freeze({
|
|
|
63
83
|
"0x41": "too much memory was allocated, or an array was created too large",
|
|
64
84
|
"0x51": "a zero-initialised variable of internal function type was called",
|
|
65
85
|
});
|
|
66
|
-
function checkedAddress(raw, what) {
|
|
67
|
-
const result = validateAddress(raw);
|
|
68
|
-
if (!result.valid)
|
|
69
|
-
throw new Error(`${what}: ${result.reason}`);
|
|
70
|
-
return result.checksummed;
|
|
71
|
-
}
|
|
72
86
|
function checkedBytes(raw, what) {
|
|
73
87
|
const text = raw.trim();
|
|
74
88
|
// An odd number of hex digits is a truncated paste, and the ABI coder would
|
package/dist/lib/typed.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type TypedTypes = Readonly<Record<string, ReadonlyArray<TypedField>>>;
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function encodeType(primary: string, types: TypedTypes): string;
|
|
12
12
|
export declare function typeHash(primary: string, types: TypedTypes): Uint8Array;
|
|
13
|
-
export declare function hashStruct(primary: string, data:
|
|
13
|
+
export declare function hashStruct(primary: string, data: unknown, types: TypedTypes): Uint8Array;
|
|
14
14
|
export type Domain = {
|
|
15
15
|
readonly name?: string;
|
|
16
16
|
readonly version?: string;
|
package/dist/lib/typed.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* was actually shown, compared against the one the dapp asked for.
|
|
12
12
|
*/
|
|
13
13
|
import { keccak256, toHex } from "@crewhaus/tool-encode";
|
|
14
|
-
import { encodeTuple, parseType } from "./abi";
|
|
14
|
+
import { MAX_TYPE_DEPTH, bytesArg, encodeTuple, parseType, } from "./abi";
|
|
15
15
|
const encoder = new TextEncoder();
|
|
16
16
|
const hash = (bytes) => keccak256(bytes);
|
|
17
17
|
const concat = (parts) => {
|
|
@@ -23,21 +23,62 @@ const concat = (parts) => {
|
|
|
23
23
|
}
|
|
24
24
|
return out;
|
|
25
25
|
};
|
|
26
|
+
const OPEN_BRACKET = 0x5b;
|
|
27
|
+
const CLOSE_BRACKET = 0x5d;
|
|
28
|
+
const isDigit = (code) => code >= 0x30 && code <= 0x39;
|
|
29
|
+
/**
|
|
30
|
+
* The outermost array suffix of `type` — `Foo[2][]` is `Foo[2]` with a
|
|
31
|
+
* dynamic suffix — or `undefined` when it has none. Read from the end, one
|
|
32
|
+
* character at a time: the regex this replaces backtracked in the square of
|
|
33
|
+
* the type's length, and a type string is the caller's.
|
|
34
|
+
*/
|
|
35
|
+
function outerArraySuffix(type) {
|
|
36
|
+
if (type.charCodeAt(type.length - 1) !== CLOSE_BRACKET)
|
|
37
|
+
return undefined;
|
|
38
|
+
let i = type.length - 2;
|
|
39
|
+
while (i >= 0 && isDigit(type.charCodeAt(i)))
|
|
40
|
+
i--;
|
|
41
|
+
if (i < 0 || type.charCodeAt(i) !== OPEN_BRACKET)
|
|
42
|
+
return undefined;
|
|
43
|
+
return { inner: type.slice(0, i), length: type.slice(i + 1, -1) };
|
|
44
|
+
}
|
|
45
|
+
/** `type` without its array suffixes, and how many it had, in one pass. */
|
|
46
|
+
function withoutArraySuffixes(type) {
|
|
47
|
+
let base = type;
|
|
48
|
+
let depth = 0;
|
|
49
|
+
for (let suffix = outerArraySuffix(base); suffix !== undefined;) {
|
|
50
|
+
base = suffix.inner;
|
|
51
|
+
depth++;
|
|
52
|
+
suffix = outerArraySuffix(base);
|
|
53
|
+
}
|
|
54
|
+
return { base, depth };
|
|
55
|
+
}
|
|
26
56
|
/** The struct types `primary` refers to, transitively. */
|
|
27
57
|
function referencedTypes(primary, types, seen = new Set()) {
|
|
28
58
|
if (seen.has(primary))
|
|
29
59
|
return seen;
|
|
30
|
-
const fields = types
|
|
60
|
+
const fields = structFields(types, primary);
|
|
31
61
|
if (!fields)
|
|
32
62
|
return seen;
|
|
33
63
|
seen.add(primary);
|
|
34
64
|
for (const field of fields) {
|
|
35
|
-
const base = field.type
|
|
36
|
-
if (types
|
|
65
|
+
const { base } = withoutArraySuffixes(field.type);
|
|
66
|
+
if (structFields(types, base))
|
|
37
67
|
referencedTypes(base, types, seen);
|
|
38
68
|
}
|
|
39
69
|
return seen;
|
|
40
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The fields of the struct `name`, when `types` DEFINES it — an own property.
|
|
73
|
+
*
|
|
74
|
+
* A plain lookup walks the prototype chain, so a type named `constructor` or
|
|
75
|
+
* `toString` "exists" in every types object: a field of that type was then
|
|
76
|
+
* hashed as a struct whose field list is a function, instead of being refused
|
|
77
|
+
* as a type nobody defined.
|
|
78
|
+
*/
|
|
79
|
+
function structFields(types, name) {
|
|
80
|
+
return Object.hasOwn(types, name) ? types[name] : undefined;
|
|
81
|
+
}
|
|
41
82
|
/**
|
|
42
83
|
* `encodeType`: the primary struct first, then every referenced struct in
|
|
43
84
|
* alphabetical order. The order is part of the standard — a different order
|
|
@@ -47,7 +88,7 @@ export function encodeType(primary, types) {
|
|
|
47
88
|
const referenced = [...referencedTypes(primary, types)].filter((t) => t !== primary).sort();
|
|
48
89
|
return [primary, ...referenced]
|
|
49
90
|
.map((name) => {
|
|
50
|
-
const fields = types
|
|
91
|
+
const fields = structFields(types, name);
|
|
51
92
|
if (!fields)
|
|
52
93
|
throw new Error(`the type "${name}" is referenced but not defined`);
|
|
53
94
|
return `${name}(${fields.map((f) => `${f.type} ${f.name}`).join(",")})`;
|
|
@@ -57,56 +98,119 @@ export function encodeType(primary, types) {
|
|
|
57
98
|
export function typeHash(primary, types) {
|
|
58
99
|
return hash(encoder.encode(encodeType(primary, types)));
|
|
59
100
|
}
|
|
101
|
+
function cachedTypeHash(ctx, primary) {
|
|
102
|
+
let known = ctx.typeHashes.get(primary);
|
|
103
|
+
if (known === undefined) {
|
|
104
|
+
known = typeHash(primary, ctx.types);
|
|
105
|
+
ctx.typeHashes.set(primary, known);
|
|
106
|
+
}
|
|
107
|
+
return known;
|
|
108
|
+
}
|
|
109
|
+
/** A caller's text in a refusal, cut to a length a message can carry. */
|
|
110
|
+
function clip(text) {
|
|
111
|
+
return text.length <= 80 ? text : `${text.slice(0, 80)}… (${text.length} characters)`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Every struct `primary` reaches, checked before anything is hashed: each
|
|
115
|
+
* field's type must be a struct `types` defines (with array suffixes, at
|
|
116
|
+
* most as many as an ABI type may nest) or an ABI type the coder parses.
|
|
117
|
+
* Checked on use alone, a struct referenced but never instantiated (in an
|
|
118
|
+
* empty array) went into the encoded type unexamined, and the digest came
|
|
119
|
+
* back "ok" over a type string no wallet would accept.
|
|
120
|
+
*/
|
|
121
|
+
function checkTypes(primary, types) {
|
|
122
|
+
if (!structFields(types, primary))
|
|
123
|
+
throw new Error(`the type "${clip(primary)}" is not defined`);
|
|
124
|
+
for (const name of referencedTypes(primary, types)) {
|
|
125
|
+
for (const field of structFields(types, name) ?? []) {
|
|
126
|
+
const what = `${clip(name)}.${clip(field.name)}`;
|
|
127
|
+
const { base, depth } = withoutArraySuffixes(field.type);
|
|
128
|
+
if (structFields(types, base)) {
|
|
129
|
+
if (depth > MAX_TYPE_DEPTH) {
|
|
130
|
+
throw new Error(`${what}: "${clip(field.type)}" nests arrays more than ${MAX_TYPE_DEPTH} levels deep`);
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
parseType(field.type);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
throw new Error(`${what}: "${clip(field.type)}" is neither a struct defined in types nor an ABI type (${err.message})`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
60
143
|
/** One field, as the 32 bytes it contributes to a struct's encoding. */
|
|
61
|
-
function encodeField(type, value,
|
|
62
|
-
const
|
|
63
|
-
|
|
144
|
+
function encodeField(ctx, type, value, what) {
|
|
145
|
+
const types = ctx.types;
|
|
146
|
+
const array = outerArraySuffix(type);
|
|
147
|
+
if (array) {
|
|
64
148
|
if (!Array.isArray(value))
|
|
65
149
|
throw new Error(`${what}: expected an array`);
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
if (expected !== undefined &&
|
|
69
|
-
expected !== "" &&
|
|
70
|
-
value.length !== Number.parseInt(expected, 10)) {
|
|
150
|
+
const expected = array.length;
|
|
151
|
+
if (expected !== "" && value.length !== Number.parseInt(expected, 10)) {
|
|
71
152
|
throw new Error(`${what}: expected ${expected} items, got ${value.length}`);
|
|
72
153
|
}
|
|
73
154
|
// An array contributes the hash of its members' encodings.
|
|
74
|
-
return hash(concat(value.map((item, i) => encodeField(inner, item,
|
|
155
|
+
return hash(concat(value.map((item, i) => encodeField(ctx, array.inner, item, `${what}[${i}]`))));
|
|
75
156
|
}
|
|
76
|
-
if (types
|
|
77
|
-
return
|
|
78
|
-
if (type === "string")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
throw new Error(`${what}: bytes must be hex`);
|
|
84
|
-
const bytes = new Uint8Array(text.length / 2);
|
|
85
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
86
|
-
bytes[i] = Number.parseInt(text.slice(i * 2, i * 2 + 2), 16);
|
|
157
|
+
if (structFields(types, type))
|
|
158
|
+
return structHash(ctx, type, value);
|
|
159
|
+
if (type === "string") {
|
|
160
|
+
// A number or a boolean reads as the text it prints as. An object does
|
|
161
|
+
// not: String({}) is "[object Object]", a value nobody was shown.
|
|
162
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
163
|
+
throw new Error(`${what}: a string field takes text, not ${describeValue(value)}`);
|
|
87
164
|
}
|
|
88
|
-
return hash(
|
|
165
|
+
return hash(encoder.encode(String(value)));
|
|
89
166
|
}
|
|
167
|
+
// The coder's own strict decoder: an odd digit count is refused rather
|
|
168
|
+
// than its last nibble dropped, which made 0xabc hash as 0xab.
|
|
169
|
+
if (type === "bytes")
|
|
170
|
+
return hash(bytesArg(value, what));
|
|
90
171
|
// Everything else must be an atomic ABI type. When it is not, the likely
|
|
91
172
|
// mistake is a struct named in a field and left out of `types`, so say
|
|
92
173
|
// both possibilities rather than only "not an ABI type".
|
|
174
|
+
let parsed;
|
|
93
175
|
try {
|
|
94
|
-
|
|
176
|
+
parsed = parseType(type);
|
|
95
177
|
}
|
|
96
178
|
catch (err) {
|
|
97
179
|
throw new Error(`${what}: "${type}" is neither a struct defined in types nor an ABI type (${err.message})`);
|
|
98
180
|
}
|
|
181
|
+
// Outside the try: a value the type refuses — an address whose checksum
|
|
182
|
+
// fails — is the value's problem, not an unknown type's.
|
|
183
|
+
return encodeTuple([parsed], [value], what);
|
|
184
|
+
}
|
|
185
|
+
/** What a value is, for a refusal: "an array", "null", "a number". */
|
|
186
|
+
function describeValue(value) {
|
|
187
|
+
if (value === null || value === undefined)
|
|
188
|
+
return String(value);
|
|
189
|
+
if (Array.isArray(value))
|
|
190
|
+
return "an array";
|
|
191
|
+
return typeof value === "object" ? "an object" : `a ${typeof value}`;
|
|
99
192
|
}
|
|
100
193
|
export function hashStruct(primary, data, types) {
|
|
101
|
-
|
|
194
|
+
return structHash({ types, typeHashes: new Map() }, primary, data);
|
|
195
|
+
}
|
|
196
|
+
function structHash(ctx, primary, data) {
|
|
197
|
+
const fields = structFields(ctx.types, primary);
|
|
102
198
|
if (!fields)
|
|
103
199
|
throw new Error(`the type "${primary}" is not defined`);
|
|
104
|
-
|
|
200
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
201
|
+
throw new Error(`"${primary}" expects an object for its fields, got ${describeValue(data)}`);
|
|
202
|
+
}
|
|
203
|
+
const record = data;
|
|
204
|
+
const parts = [cachedTypeHash(ctx, primary)];
|
|
105
205
|
for (const field of fields) {
|
|
106
|
-
|
|
206
|
+
// An OWN field. `in` also sees what every object inherits, so a message
|
|
207
|
+
// missing "toString" was hashed over the source text of
|
|
208
|
+
// Object.prototype.toString — a digest of a value nobody was shown —
|
|
209
|
+
// instead of being refused like any other missing field.
|
|
210
|
+
if (!Object.hasOwn(record, field.name)) {
|
|
107
211
|
throw new Error(`"${primary}" requires the field "${field.name}", which the message does not have`);
|
|
108
212
|
}
|
|
109
|
-
parts.push(encodeField(field.type,
|
|
213
|
+
parts.push(encodeField(ctx, field.type, record[field.name], `${primary}.${field.name}`));
|
|
110
214
|
}
|
|
111
215
|
return hash(concat(parts));
|
|
112
216
|
}
|
|
@@ -137,13 +241,15 @@ export function typedDataDigest(domain, types, primaryType, message) {
|
|
|
137
241
|
// defined by the standard and must not take part in encodeType.
|
|
138
242
|
const withoutDomain = Object.fromEntries(Object.entries(types).filter(([name]) => name !== "EIP712Domain"));
|
|
139
243
|
const separator = domainSeparator(domain);
|
|
140
|
-
|
|
141
|
-
const
|
|
244
|
+
checkTypes(primaryType, withoutDomain);
|
|
245
|
+
const ctx = { types: withoutDomain, typeHashes: new Map() };
|
|
246
|
+
const messageHash = structHash(ctx, primaryType, message);
|
|
247
|
+
const digest = hash(concat([new Uint8Array([0x19, 0x01]), separator, messageHash]));
|
|
142
248
|
return {
|
|
143
249
|
digest: `0x${toHex(digest)}`,
|
|
144
250
|
domainSeparator: `0x${toHex(separator)}`,
|
|
145
|
-
messageHash: `0x${toHex(
|
|
146
|
-
typeHash: `0x${toHex(
|
|
251
|
+
messageHash: `0x${toHex(messageHash)}`,
|
|
252
|
+
typeHash: `0x${toHex(cachedTypeHash(ctx, primaryType))}`,
|
|
147
253
|
encodedType: encodeType(primaryType, withoutDomain),
|
|
148
254
|
primaryType,
|
|
149
255
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/tool-onchain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic onchain tools: ABI encoding and decoding, EIP-55 address checksums, EIP-712 and EIP-191 hashing, unit conversion and fixed-point DeFi maths",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,17 +8,18 @@
|
|
|
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/errors": "0.
|
|
19
|
-
"@crewhaus/tool-builder": "0.
|
|
20
|
-
"@crewhaus/tool-catalog": "0.
|
|
21
|
-
"@crewhaus/tool-encode": "0.
|
|
19
|
+
"@crewhaus/errors": "0.8.1",
|
|
20
|
+
"@crewhaus/tool-builder": "0.8.1",
|
|
21
|
+
"@crewhaus/tool-catalog": "0.8.1",
|
|
22
|
+
"@crewhaus/tool-encode": "0.8.1",
|
|
22
23
|
"zod": "^3.23.8"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
@@ -42,5 +43,8 @@
|
|
|
42
43
|
"publishConfig": {
|
|
43
44
|
"access": "public"
|
|
44
45
|
},
|
|
45
|
-
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
46
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"],
|
|
47
|
+
"engines": {
|
|
48
|
+
"bun": ">=1.3.14"
|
|
49
|
+
}
|
|
46
50
|
}
|