@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 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 = parseType(arrayMatch[1]);
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(parseType);
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
- function hexToBytes(raw, what) {
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
- const bytes = hexToBytes(String(value), what);
268
- if (bytes.length !== 20)
269
- throw new Error(`${what}: an address is 20 bytes, got ${bytes.length}`);
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 = hexToBytes(String(value), what);
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
- function wordAt(data, offset, what) {
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
- function bigIntFromWord(word, signed, bits) {
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
- if (!signed)
357
- return value;
358
- const limit = 1n << BigInt(bits);
359
- const half = limit >> 1n;
360
- const truncated = value & (limit - 1n);
361
- return truncated >= half ? truncated - limit : truncated;
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(bigIntFromWord(wordAt(data, offset, what), false, 256));
368
- if (length * WORD > data.length) {
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 decodeTuple(new Array(type.arrayLength).fill(child), data, offset, what);
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(bigIntFromWord(wordAt(data, offset, what), false, 256));
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
- const word = wordAt(data, offset, what);
387
- if (type.base === "address")
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
- return `0x${toHex(word.subarray(0, type.bits / 8))}`;
397
- return bigIntFromWord(word, type.base === "int", type.bits).toString();
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
- export function decodeTuple(types, data, base = 0, what = "value") {
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 (const [i, type] of types.entries()) {
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(bigIntFromWord(wordAt(data, head, label), false, 256));
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")
@@ -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
  *
@@ -31,9 +31,11 @@ export function validateAddress(raw) {
31
31
  valid: false,
32
32
  checksummed: "",
33
33
  hadChecksum: false,
34
- reason: /^0x/i.test(text)
35
- ? `an address is 0x followed by 40 hex characters; this has ${body.length}`
36
- : "an address starts with 0x",
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 (!/^-?\d*\.?\d*$/.test(text) || text === "" || text === "." || text === "-") {
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("-");
@@ -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. */
@@ -27,7 +27,7 @@
27
27
  * or submits a transaction.
28
28
  */
29
29
  import { decodeData, encodeCall } from "./abi";
30
- import { validateAddress } from "./address";
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
@@ -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: Record<string, unknown>, types: TypedTypes): Uint8Array;
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[primary];
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.replace(/(\[\d*\])+$/, "");
36
- if (types[base])
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[name];
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, types, what) {
62
- const arrayMatch = /^(.*)\[(\d*)\]$/.exec(type);
63
- if (arrayMatch) {
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 inner = arrayMatch[1];
67
- const expected = arrayMatch[2];
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, types, `${what}[${i}]`))));
155
+ return hash(concat(value.map((item, i) => encodeField(ctx, array.inner, item, `${what}[${i}]`))));
75
156
  }
76
- if (types[type])
77
- return hashStruct(type, value, types);
78
- if (type === "string")
79
- return hash(encoder.encode(String(value)));
80
- if (type === "bytes") {
81
- const text = String(value).replace(/^0x/i, "");
82
- if (!/^[0-9a-fA-F]*$/.test(text))
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(bytes);
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
- return encodeTuple([parseType(type)], [value], what);
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
- const fields = types[primary];
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
- const parts = [typeHash(primary, types)];
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
- if (!(field.name in data)) {
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, data[field.name], types, `${primary}.${field.name}`));
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
- const structHash = hashStruct(primaryType, message, withoutDomain);
141
- const digest = hash(concat([new Uint8Array([0x19, 0x01]), separator, structHash]));
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(structHash)}`,
146
- typeHash: `0x${toHex(typeHash(primaryType, withoutDomain))}`,
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.7.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.7.0",
19
- "@crewhaus/tool-builder": "0.7.0",
20
- "@crewhaus/tool-catalog": "0.7.0",
21
- "@crewhaus/tool-encode": "0.7.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
  }