@meantjs/profile 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +39 -2
- package/data/packs/dai-permit.profile.json +113 -0
- package/data/packs/erc20.profile.json +221 -0
- package/data/packs/erc2612.profile.json +196 -0
- package/data/packs/permit2.profile.json +436 -0
- package/data/profile-v0.schema.json +302 -8
- package/data/profile-v0.validate.mjs +13 -1
- package/package.json +8 -1
- package/src/abi-iface.mjs +154 -0
- package/src/abi-pack.mjs +117 -0
- package/src/abi.mjs +779 -0
- package/src/analyze.mjs +87 -31
- package/src/browser.d.mts +8 -4
- package/src/browser.mjs +8 -1
- package/src/checks.mjs +103 -10
- package/src/cli.mjs +12 -6
- package/src/evm.d.mts +101 -0
- package/src/evm.mjs +32 -0
- package/src/keccak.mjs +108 -0
- package/src/resolve.mjs +96 -5
- package/src/resolved.mjs +20 -4
- package/src/validate.mjs +24 -4
package/src/abi.mjs
ADDED
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
// EVM ABI function fragments (spec section 15, D-054): reading them, their canonical text, selectors, the identity
|
|
2
|
+
// of an interface, and a strict codec for calldata. Plain JavaScript with no dependency, so that it is part of the
|
|
3
|
+
// browser-safe `@meantjs/profile/evm` entry.
|
|
4
|
+
//
|
|
5
|
+
// A type is a tree:
|
|
6
|
+
// { kind: "uint" | "int", bits } bits 8 to 256 in steps of 8; `uint` and `int` alone are 256
|
|
7
|
+
// { kind: "address" } | { kind: "bool" } | { kind: "string" } | { kind: "bytes" }
|
|
8
|
+
// { kind: "fixedbytes", size } bytes1 to bytes32 (`byte` is bytes1)
|
|
9
|
+
// { kind: "array", element, length } length null for T[], else k >= 1 for T[k]
|
|
10
|
+
// { kind: "tuple", components: [{ name, type }] } name null when unnamed
|
|
11
|
+
// Refused: `function`, `fixed` and `ufixed` types, overloaded names (O1), names Candid Profile addresses cannot spell.
|
|
12
|
+
//
|
|
13
|
+
// Values, as the codec reads and writes them (the consent package's Candid values have the same shape rules):
|
|
14
|
+
// uintN, intN decimal string when decoded; a decimal string or a bigint when encoded (never a number, D-006)
|
|
15
|
+
// address EIP-55 text when decoded; any valid address text when encoded (mixed case must match its checksum)
|
|
16
|
+
// bool boolean
|
|
17
|
+
// bytes, bytesN Uint8Array
|
|
18
|
+
// string string (the bytes must be UTF-8)
|
|
19
|
+
// T[], T[k] array
|
|
20
|
+
// tuple object keyed by component name, `#i` for the unnamed component at position i
|
|
21
|
+
|
|
22
|
+
import { keccak256, keccak256Hex } from "./keccak.mjs";
|
|
23
|
+
import { sha256Hex } from "./sha256.mjs";
|
|
24
|
+
import { jcs } from "./util.mjs";
|
|
25
|
+
|
|
26
|
+
export class AbiError extends Error {
|
|
27
|
+
/** @param {string} code E_ABI (a fragment), E_ABI_ENCODE or E_ABI_DECODE (a value) */
|
|
28
|
+
constructor(code, message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "AbiError";
|
|
31
|
+
this.code = code;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const fail = (code, message) => {
|
|
35
|
+
throw new AbiError(code, message);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
39
|
+
const MUTABILITY = new Set(["pure", "view", "nonpayable", "payable"]);
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------------------------- types
|
|
42
|
+
|
|
43
|
+
/** The key of a tuple component: its name, else `#` and its position. */
|
|
44
|
+
export const componentKey = (c, i) => c.name ?? `#${i}`;
|
|
45
|
+
|
|
46
|
+
/** Canonical type text: `uint256`, `(address token,uint256 amount)[]`; with `names`, tuple components keep theirs. */
|
|
47
|
+
export function typeString(t, names = false) {
|
|
48
|
+
switch (t.kind) {
|
|
49
|
+
case "uint":
|
|
50
|
+
case "int":
|
|
51
|
+
return `${t.kind}${t.bits}`;
|
|
52
|
+
case "fixedbytes":
|
|
53
|
+
return `bytes${t.size}`;
|
|
54
|
+
case "array":
|
|
55
|
+
return `${typeString(t.element, names)}[${t.length ?? ""}]`;
|
|
56
|
+
case "tuple":
|
|
57
|
+
return `(${t.components.map((c) => param(c, names)).join(",")})`;
|
|
58
|
+
default:
|
|
59
|
+
return t.kind;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const param = (p, names) => (names && p.name !== null ? `${typeString(p.type, true)} ${p.name}` : typeString(p.type, names));
|
|
63
|
+
|
|
64
|
+
function elementary(name, where) {
|
|
65
|
+
if (name === "address" || name === "bool" || name === "string" || name === "bytes") return { kind: name };
|
|
66
|
+
if (name === "byte") return { kind: "fixedbytes", size: 1 };
|
|
67
|
+
let m = /^(u?int)(\d*)$/.exec(name);
|
|
68
|
+
if (m) {
|
|
69
|
+
const bits = m[2] === "" ? 256 : Number(m[2]);
|
|
70
|
+
if (!/^(?:|[1-9]\d*)$/.test(m[2]) || bits < 8 || bits > 256 || bits % 8 !== 0) fail("E_ABI", `${where}: ${name} is not an ABI integer type (8 to 256 bits in steps of 8)`);
|
|
71
|
+
return { kind: m[1], bits };
|
|
72
|
+
}
|
|
73
|
+
m = /^bytes([1-9]\d*)$/.exec(name);
|
|
74
|
+
if (m) {
|
|
75
|
+
const size = Number(m[1]);
|
|
76
|
+
if (size > 32) fail("E_ABI", `${where}: ${name} is not an ABI type (bytes1 to bytes32)`);
|
|
77
|
+
return { kind: "fixedbytes", size };
|
|
78
|
+
}
|
|
79
|
+
if (/^u?fixed/.test(name) || name === "function") fail("E_ABI", `${where}: the ${name} type is not supported (spec section 15)`);
|
|
80
|
+
fail("E_ABI", `${where}: unknown ABI type ${JSON.stringify(name)}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function withDims(base, dims, where) {
|
|
84
|
+
let t = base;
|
|
85
|
+
for (const d of dims) {
|
|
86
|
+
if (d !== "" && !/^[1-9]\d{0,5}$/.test(d)) fail("E_ABI", `${where}: array length [${d}] must be a positive integer`);
|
|
87
|
+
t = { kind: "array", element: t, length: d === "" ? null : Number(d) };
|
|
88
|
+
}
|
|
89
|
+
return t;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function checkComponents(components, where) {
|
|
93
|
+
if (components.length === 0) fail("E_ABI", `${where}: a tuple needs at least one component`);
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
components.forEach((c, i) => {
|
|
96
|
+
if (c.name === null) return;
|
|
97
|
+
if (!IDENT.test(c.name)) fail("E_ABI", `${where}: the name ${JSON.stringify(c.name)} is not an identifier (letters, digits and _)`);
|
|
98
|
+
if (seen.has(c.name)) fail("E_ABI", `${where}: two components are named ${JSON.stringify(c.name)}`);
|
|
99
|
+
if (/^#/.test(c.name) || components.some((d, j) => j !== i && d.name === null && `#${j}` === c.name)) fail("E_ABI", `${where}: ambiguous name ${c.name}`);
|
|
100
|
+
seen.add(c.name);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------------------------- human-readable fragments
|
|
105
|
+
|
|
106
|
+
class Reader {
|
|
107
|
+
constructor(text, where) {
|
|
108
|
+
this.s = text;
|
|
109
|
+
this.i = 0;
|
|
110
|
+
this.where = where;
|
|
111
|
+
}
|
|
112
|
+
ws() {
|
|
113
|
+
while (this.i < this.s.length && /\s/.test(this.s[this.i])) this.i++;
|
|
114
|
+
}
|
|
115
|
+
peek(ch) {
|
|
116
|
+
this.ws();
|
|
117
|
+
return this.s[this.i] === ch;
|
|
118
|
+
}
|
|
119
|
+
eat(ch) {
|
|
120
|
+
this.ws();
|
|
121
|
+
if (this.s[this.i] !== ch) fail("E_ABI", `${this.where}: expected "${ch}" at position ${this.i} of ${JSON.stringify(this.s)}`);
|
|
122
|
+
this.i++;
|
|
123
|
+
}
|
|
124
|
+
word() {
|
|
125
|
+
this.ws();
|
|
126
|
+
const m = /^[A-Za-z_$][A-Za-z0-9_$]*/.exec(this.s.slice(this.i));
|
|
127
|
+
if (!m) return null;
|
|
128
|
+
this.i += m[0].length;
|
|
129
|
+
return m[0];
|
|
130
|
+
}
|
|
131
|
+
peekWord() {
|
|
132
|
+
const at = this.i;
|
|
133
|
+
const w = this.word();
|
|
134
|
+
this.i = at;
|
|
135
|
+
return w;
|
|
136
|
+
}
|
|
137
|
+
end() {
|
|
138
|
+
this.ws();
|
|
139
|
+
return this.i >= this.s.length;
|
|
140
|
+
}
|
|
141
|
+
dims() {
|
|
142
|
+
const out = [];
|
|
143
|
+
for (;;) {
|
|
144
|
+
this.ws();
|
|
145
|
+
const m = /^\[(\d*)\]/.exec(this.s.slice(this.i));
|
|
146
|
+
if (!m) return out;
|
|
147
|
+
out.push(m[1]);
|
|
148
|
+
this.i += m[0].length;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** `( param, … )` */
|
|
152
|
+
params() {
|
|
153
|
+
this.eat("(");
|
|
154
|
+
const out = [];
|
|
155
|
+
if (this.peek(")")) {
|
|
156
|
+
this.i++;
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
for (;;) {
|
|
160
|
+
out.push(this.param());
|
|
161
|
+
if (this.peek(",")) {
|
|
162
|
+
this.i++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
this.eat(")");
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** `type [location] [name]`; a tuple type is `(…)` or `tuple(…)` */
|
|
170
|
+
param() {
|
|
171
|
+
let base;
|
|
172
|
+
if (this.peek("(")) base = { kind: "tuple", components: this.params() };
|
|
173
|
+
else {
|
|
174
|
+
const w = this.word();
|
|
175
|
+
if (w === null) fail("E_ABI", `${this.where}: expected a type at position ${this.i} of ${JSON.stringify(this.s)}`);
|
|
176
|
+
if (w === "tuple" && this.peek("(")) base = { kind: "tuple", components: this.params() };
|
|
177
|
+
else {
|
|
178
|
+
base = elementary(w, this.where);
|
|
179
|
+
if (w === "address" && this.peekWord() === "payable") this.word();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (base.kind === "tuple") checkComponents(base.components, this.where);
|
|
183
|
+
const type = withDims(base, this.dims(), this.where);
|
|
184
|
+
let name = this.peek(",") || this.peek(")") ? null : this.word();
|
|
185
|
+
if (name === "indexed") fail("E_ABI", `${this.where}: "indexed" belongs to events; only functions are read`);
|
|
186
|
+
if (name === "memory" || name === "calldata" || name === "storage") name = this.peek(",") || this.peek(")") ? null : this.word();
|
|
187
|
+
return { name, type };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function fromText(text, where) {
|
|
192
|
+
const r = new Reader(text, where);
|
|
193
|
+
const kw = r.word();
|
|
194
|
+
if (kw !== "function") {
|
|
195
|
+
if (["event", "error", "constructor", "fallback", "receive", "struct"].includes(kw)) return null; // not a function: not read (spec section 15)
|
|
196
|
+
fail("E_ABI", `${where}: a fragment starts with "function", found ${JSON.stringify(text)}`);
|
|
197
|
+
}
|
|
198
|
+
const name = r.word();
|
|
199
|
+
if (name === null) fail("E_ABI", `${where}: the function has no name`);
|
|
200
|
+
const inputs = r.params();
|
|
201
|
+
let stateMutability = "nonpayable";
|
|
202
|
+
let outputs = [];
|
|
203
|
+
let seen = false;
|
|
204
|
+
while (!r.end()) {
|
|
205
|
+
const w = r.word();
|
|
206
|
+
if (w === "external" || w === "public") continue;
|
|
207
|
+
if (w === "returns") {
|
|
208
|
+
outputs = r.params();
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const m = w === "constant" ? "view" : w;
|
|
212
|
+
if (!MUTABILITY.has(m)) fail("E_ABI", `${where}: unexpected ${JSON.stringify(w ?? r.s.slice(r.i))} in ${JSON.stringify(text)}`);
|
|
213
|
+
if (seen) fail("E_ABI", `${where}: two state mutabilities in ${JSON.stringify(text)}`);
|
|
214
|
+
seen = true;
|
|
215
|
+
stateMutability = m;
|
|
216
|
+
}
|
|
217
|
+
return { name, inputs, outputs, stateMutability };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------------------------- JSON ABI entries
|
|
221
|
+
|
|
222
|
+
function jsonParam(p, where) {
|
|
223
|
+
if (p === null || typeof p !== "object" || typeof p.type !== "string") fail("E_ABI", `${where}: a parameter needs a "type" string`);
|
|
224
|
+
const m = /^([a-z0-9]+)((?:\[\d*\])*)$/.exec(p.type);
|
|
225
|
+
if (!m) fail("E_ABI", `${where}: unknown ABI type ${JSON.stringify(p.type)}`);
|
|
226
|
+
let base;
|
|
227
|
+
if (m[1] === "tuple") {
|
|
228
|
+
if (!Array.isArray(p.components)) fail("E_ABI", `${where}: a tuple parameter needs "components"`);
|
|
229
|
+
base = { kind: "tuple", components: p.components.map((c, i) => jsonParam(c, `${where}.components[${i}]`)) };
|
|
230
|
+
checkComponents(base.components, where);
|
|
231
|
+
} else base = elementary(m[1], where);
|
|
232
|
+
const dims = [...m[2].matchAll(/\[(\d*)\]/g)].map((d) => d[1]);
|
|
233
|
+
const name = typeof p.name === "string" && p.name !== "" ? p.name : null;
|
|
234
|
+
return { name, type: withDims(base, dims, where) };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function fromJson(e, where) {
|
|
238
|
+
const kind = e.type ?? "function";
|
|
239
|
+
if (kind !== "function") return null; // events, errors, the constructor, fallback and receive are not read
|
|
240
|
+
if (typeof e.name !== "string" || e.name === "") fail("E_ABI", `${where}: the function has no name`);
|
|
241
|
+
let stateMutability = e.stateMutability;
|
|
242
|
+
if (stateMutability === undefined) stateMutability = e.constant === true ? "view" : e.payable === true ? "payable" : "nonpayable";
|
|
243
|
+
if (!MUTABILITY.has(stateMutability)) fail("E_ABI", `${where}: unknown stateMutability ${JSON.stringify(stateMutability)}`);
|
|
244
|
+
const list = (xs, side) => {
|
|
245
|
+
if (xs === undefined) return [];
|
|
246
|
+
if (!Array.isArray(xs)) fail("E_ABI", `${where}: "${side}" must be an array`);
|
|
247
|
+
return xs.map((p, i) => jsonParam(p, `${where}.${side}[${i}]`));
|
|
248
|
+
};
|
|
249
|
+
return { name: e.name, inputs: list(e.inputs, "inputs"), outputs: list(e.outputs, "outputs"), stateMutability };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---------------------------------------------------------------------------------------------- functions
|
|
253
|
+
|
|
254
|
+
/** `transfer(address,uint256)`, the text a selector hashes. */
|
|
255
|
+
export const typeSignature = (f) => `${f.name}(${f.inputs.map((p) => typeString(p.type)).join(",")})`;
|
|
256
|
+
/** `transfer(address to,uint256 value)`, as ERC-7730 keys a function. */
|
|
257
|
+
export const namedSignature = (f) => `${f.name}(${f.inputs.map((p) => param(p, true)).join(",")})`;
|
|
258
|
+
/** `function decimals() view returns (uint8)`: names kept, `nonpayable` left out, one form per function. */
|
|
259
|
+
export function canonicalFragment(f) {
|
|
260
|
+
const mut = f.stateMutability === "nonpayable" ? "" : ` ${f.stateMutability}`;
|
|
261
|
+
const out = f.outputs.length ? ` returns (${f.outputs.map((p) => param(p, true)).join(",")})` : "";
|
|
262
|
+
return `function ${namedSignature(f)}${mut}${out}`;
|
|
263
|
+
}
|
|
264
|
+
/** The 4-byte selector as `0x` and 8 hex digits. */
|
|
265
|
+
export const selectorOf = (f) => `0x${keccak256Hex(typeSignature(f)).slice(0, 8)}`;
|
|
266
|
+
|
|
267
|
+
function finish(f, where) {
|
|
268
|
+
if (!IDENT.test(f.name)) fail("E_ABI", `${where}: the function name ${JSON.stringify(f.name)} is not an identifier (letters, digits and _)`);
|
|
269
|
+
checkParams(f.inputs, `${where} (inputs)`);
|
|
270
|
+
checkParams(f.outputs, `${where} (outputs)`);
|
|
271
|
+
return { ...f, signature: namedSignature(f), typeSignature: typeSignature(f), selector: selectorOf(f), canonical: canonicalFragment(f) };
|
|
272
|
+
}
|
|
273
|
+
function checkParams(params, where) {
|
|
274
|
+
if (params.length) checkComponents(params, where);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Read an ABI: an array whose entries are human-readable fragments (`function transfer(address to, uint256 value)
|
|
279
|
+
* returns (bool)`) or JSON ABI entries. Only functions are read; events, errors, the constructor, fallback and receive
|
|
280
|
+
* are skipped. Every problem is reported, none thrown.
|
|
281
|
+
*
|
|
282
|
+
* @param {unknown} entries
|
|
283
|
+
* @param {string} [label] how the diagnostics name the array (`binds.abi`)
|
|
284
|
+
* @returns {{ functions: object[], errors: { at: string, message: string }[] }}
|
|
285
|
+
*/
|
|
286
|
+
export function parseAbi(entries, label = "abi") {
|
|
287
|
+
const errors = [];
|
|
288
|
+
const functions = [];
|
|
289
|
+
if (!Array.isArray(entries)) return { functions, errors: [{ at: label, message: "an ABI is an array of fragments or JSON ABI entries" }] };
|
|
290
|
+
entries.forEach((e, i) => {
|
|
291
|
+
const where = `${label}[${i}]`;
|
|
292
|
+
try {
|
|
293
|
+
const f = typeof e === "string" ? fromText(e, where) : e !== null && typeof e === "object" && !Array.isArray(e) ? fromJson(e, where) : fail("E_ABI", `${where}: neither a fragment nor a JSON ABI entry`);
|
|
294
|
+
if (f !== null) functions.push({ ...finish(f, where), at: where });
|
|
295
|
+
} catch (err) {
|
|
296
|
+
if (!(err instanceof AbiError)) throw err;
|
|
297
|
+
errors.push({ at: where, message: err.message.startsWith(`${where}: `) ? err.message.slice(where.length + 2) : err.message });
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
const byName = new Map();
|
|
301
|
+
const bySelector = new Map();
|
|
302
|
+
const kept = [];
|
|
303
|
+
for (const f of functions) {
|
|
304
|
+
const same = byName.get(f.name);
|
|
305
|
+
if (same && same.canonical === f.canonical) continue; // the same function twice
|
|
306
|
+
if (same) {
|
|
307
|
+
const why = same.typeSignature === f.typeSignature ? `${f.typeSignature} is declared twice, with different names or results` : `${f.name} is overloaded (${same.typeSignature} and ${f.typeSignature}); overloaded functions are not supported yet (spec section 15, O1)`;
|
|
308
|
+
errors.push({ at: f.at, message: why });
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const clash = bySelector.get(f.selector);
|
|
312
|
+
if (clash) {
|
|
313
|
+
errors.push({ at: f.at, message: `${f.typeSignature} and ${clash.typeSignature} have the same selector ${f.selector}` });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
byName.set(f.name, f);
|
|
317
|
+
bySelector.set(f.selector, f);
|
|
318
|
+
kept.push(f);
|
|
319
|
+
}
|
|
320
|
+
return { functions: kept.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)), errors };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The identity of an EVM interface: SHA-256 over the JCS array of its canonical fragments and, when it has EIP-712
|
|
325
|
+
* types (spec section 15.9), one `struct Name(type member,…)` line per type, sorted (spec section 15).
|
|
326
|
+
*/
|
|
327
|
+
export function abiInterfaceId(functions, types = {}) {
|
|
328
|
+
const structs = Object.keys(types).map((n) => `struct ${structText(types, n)}`);
|
|
329
|
+
return `candid-profile:evm-abi:v0:sha256:${sha256Hex(jcs([...functions.map((f) => f.canonical), ...structs].sort()))}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Read one function from its named signature (`transfer(address to,uint256 value)`), as a resolved form carries it. */
|
|
333
|
+
export function parseSignature(text, where = "signature") {
|
|
334
|
+
const r = new Reader(`function ${text}`, where);
|
|
335
|
+
r.word();
|
|
336
|
+
const name = r.word();
|
|
337
|
+
if (name === null) fail("E_ABI", `${where}: no function name in ${JSON.stringify(text)}`);
|
|
338
|
+
const inputs = r.params();
|
|
339
|
+
if (!r.end()) fail("E_ABI", `${where}: unexpected text after the parameters of ${JSON.stringify(text)}`);
|
|
340
|
+
return finish({ name, inputs, outputs: [], stateMutability: "nonpayable" }, where);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---------------------------------------------------------------------------------------------- EIP-55 addresses
|
|
344
|
+
|
|
345
|
+
const HEX40 = /^0x[0-9a-fA-F]{40}$/;
|
|
346
|
+
|
|
347
|
+
/** `0x` and 40 hex digits in the EIP-55 mixed-case checksum form. */
|
|
348
|
+
export function toChecksumAddress(address) {
|
|
349
|
+
if (typeof address !== "string" || !HEX40.test(address)) fail("E_ADDRESS", `${JSON.stringify(address)} is not an address (0x and 40 hex digits)`);
|
|
350
|
+
const lower = address.slice(2).toLowerCase();
|
|
351
|
+
const hash = keccak256Hex(lower);
|
|
352
|
+
let out = "0x";
|
|
353
|
+
for (let i = 0; i < 40; i++) out += parseInt(hash[i], 16) >= 8 ? lower[i].toUpperCase() : lower[i];
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Read an address: `0x` and 40 hex digits. Text in mixed case is an EIP-55 checksum and must match it; text all in one
|
|
359
|
+
* case carries none (EIP-55) and is accepted (O3). Returns the checksummed form.
|
|
360
|
+
*/
|
|
361
|
+
export function readAddress(text) {
|
|
362
|
+
const checksummed = toChecksumAddress(text);
|
|
363
|
+
const digits = text.slice(2);
|
|
364
|
+
const mixed = digits !== digits.toLowerCase() && digits !== digits.toUpperCase();
|
|
365
|
+
if (mixed && text !== checksummed) fail("E_ADDRESS_CHECKSUM", `${text} is in mixed case but its EIP-55 checksum does not match (expected ${checksummed})`);
|
|
366
|
+
return checksummed;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------------------------- codec
|
|
370
|
+
|
|
371
|
+
const WORD = 32;
|
|
372
|
+
const isDynamic = (t) =>
|
|
373
|
+
t.kind === "bytes" || t.kind === "string" || (t.kind === "array" && (t.length === null || isDynamic(t.element))) || (t.kind === "tuple" && t.components.some((c) => isDynamic(c.type)));
|
|
374
|
+
const headSize = (t) => {
|
|
375
|
+
if (isDynamic(t)) return WORD;
|
|
376
|
+
if (t.kind === "array") return t.length * headSize(t.element);
|
|
377
|
+
if (t.kind === "tuple") return t.components.reduce((n, c) => n + headSize(c.type), 0);
|
|
378
|
+
return WORD;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
function word(v) {
|
|
382
|
+
const out = new Uint8Array(WORD);
|
|
383
|
+
let x = BigInt.asUintN(256, v);
|
|
384
|
+
for (let i = WORD - 1; i >= 0 && x > 0n; i--) {
|
|
385
|
+
out[i] = Number(x & 0xffn);
|
|
386
|
+
x >>= 8n;
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
const concat = (parts) => {
|
|
391
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
392
|
+
let at = 0;
|
|
393
|
+
for (const p of parts) {
|
|
394
|
+
out.set(p, at);
|
|
395
|
+
at += p.length;
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
};
|
|
399
|
+
const padded = (bytes) => {
|
|
400
|
+
const out = new Uint8Array(Math.ceil(bytes.length / WORD) * WORD);
|
|
401
|
+
out.set(bytes);
|
|
402
|
+
return out;
|
|
403
|
+
};
|
|
404
|
+
const hexOf = (bytes) => [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
405
|
+
const bytesFrom = (v, where) => {
|
|
406
|
+
if (v instanceof Uint8Array) return v;
|
|
407
|
+
if (typeof v === "string" && /^0x(?:[0-9a-fA-F]{2})*$/.test(v)) return Uint8Array.from(v.slice(2).match(/../g) ?? [], (h) => parseInt(h, 16));
|
|
408
|
+
fail("E_ABI_ENCODE", `${where}: bytes are a Uint8Array or 0x hex text`);
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
function intValue(t, v, where) {
|
|
412
|
+
let x;
|
|
413
|
+
if (typeof v === "bigint") x = v;
|
|
414
|
+
else if (typeof v === "string" && /^-?(?:0|[1-9]\d*)$/.test(v) && v !== "-0") x = BigInt(v);
|
|
415
|
+
else fail("E_ABI_ENCODE", `${where}: ${typeString(t)} needs a decimal string or a bigint, got ${typeof v === "number" ? "a number" : JSON.stringify(v)}`);
|
|
416
|
+
const [min, max] = t.kind === "uint" ? [0n, 2n ** BigInt(t.bits) - 1n] : [-(2n ** BigInt(t.bits - 1)), 2n ** BigInt(t.bits - 1) - 1n];
|
|
417
|
+
if (x < min || x > max) fail("E_ABI_ENCODE", `${where}: ${x} is outside ${typeString(t)}`);
|
|
418
|
+
return x;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function encodeValue(t, v, where) {
|
|
422
|
+
switch (t.kind) {
|
|
423
|
+
case "uint":
|
|
424
|
+
case "int":
|
|
425
|
+
return word(intValue(t, v, where));
|
|
426
|
+
case "bool":
|
|
427
|
+
if (typeof v !== "boolean") fail("E_ABI_ENCODE", `${where}: bool needs true or false`);
|
|
428
|
+
return word(v ? 1n : 0n);
|
|
429
|
+
case "address": {
|
|
430
|
+
let a;
|
|
431
|
+
try {
|
|
432
|
+
a = readAddress(v);
|
|
433
|
+
} catch (e) {
|
|
434
|
+
if (e instanceof AbiError) fail("E_ABI_ENCODE", `${where}: ${e.message}`);
|
|
435
|
+
throw e;
|
|
436
|
+
}
|
|
437
|
+
return word(BigInt(a));
|
|
438
|
+
}
|
|
439
|
+
case "fixedbytes": {
|
|
440
|
+
const b = bytesFrom(v, where);
|
|
441
|
+
if (b.length !== t.size) fail("E_ABI_ENCODE", `${where}: bytes${t.size} needs ${t.size} bytes, got ${b.length}`);
|
|
442
|
+
return padded(b);
|
|
443
|
+
}
|
|
444
|
+
case "bytes":
|
|
445
|
+
case "string": {
|
|
446
|
+
const b = t.kind === "string" ? (typeof v === "string" && v.isWellFormed() ? new TextEncoder().encode(v) : fail("E_ABI_ENCODE", `${where}: string needs well-formed text`)) : bytesFrom(v, where);
|
|
447
|
+
return concat([word(BigInt(b.length)), padded(b)]);
|
|
448
|
+
}
|
|
449
|
+
case "array": {
|
|
450
|
+
if (!Array.isArray(v)) fail("E_ABI_ENCODE", `${where}: ${typeString(t)} needs an array`);
|
|
451
|
+
if (t.length !== null && v.length !== t.length) fail("E_ABI_ENCODE", `${where}: ${typeString(t)} needs ${t.length} elements, got ${v.length}`);
|
|
452
|
+
const body = encodeTuple(v.map(() => t.element), v, (i) => `${where}[${i}]`);
|
|
453
|
+
return t.length === null ? concat([word(BigInt(v.length)), body]) : body;
|
|
454
|
+
}
|
|
455
|
+
case "tuple": {
|
|
456
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) fail("E_ABI_ENCODE", `${where}: a tuple needs an object keyed by component`);
|
|
457
|
+
const keys = t.components.map(componentKey);
|
|
458
|
+
for (const k of Object.keys(v)) if (!keys.includes(k)) fail("E_ABI_ENCODE", `${where}: no component ${k}`);
|
|
459
|
+
return encodeTuple(
|
|
460
|
+
t.components.map((c) => c.type),
|
|
461
|
+
keys.map((k) => (Object.hasOwn(v, k) ? v[k] : fail("E_ABI_ENCODE", `${where}: missing component ${k}`))),
|
|
462
|
+
(i) => `${where}.${keys[i]}`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
fail("E_ABI_ENCODE", `${where}: unknown type`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function encodeTuple(types, values, at) {
|
|
470
|
+
const heads = [];
|
|
471
|
+
const tails = [];
|
|
472
|
+
let offset = types.reduce((n, t) => n + headSize(t), 0);
|
|
473
|
+
types.forEach((t, i) => {
|
|
474
|
+
const enc = encodeValue(t, values[i], at(i));
|
|
475
|
+
if (isDynamic(t)) {
|
|
476
|
+
heads.push(word(BigInt(offset)));
|
|
477
|
+
tails.push(enc);
|
|
478
|
+
offset += enc.length;
|
|
479
|
+
} else heads.push(enc);
|
|
480
|
+
});
|
|
481
|
+
return concat([...heads, ...tails]);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** ABI-encode values of a parameter list (`[{ name, type }]`), keyed like a tuple. */
|
|
485
|
+
export function encodeParams(params, values) {
|
|
486
|
+
return encodeValue({ kind: "tuple", components: params }, values, "arguments");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Calldata: the selector, then the encoded parameters. */
|
|
490
|
+
export function encodeCall(f, values) {
|
|
491
|
+
const sel = Uint8Array.from(f.selector.slice(2).match(/../g), (h) => parseInt(h, 16));
|
|
492
|
+
return concat([sel, f.inputs.length ? encodeParams(f.inputs, values) : new Uint8Array(0)]);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export const DEFAULT_ABI_LIMITS = Object.freeze({ maxBytes: 128 * 1024, maxDepth: 32, maxElements: 4096 });
|
|
496
|
+
|
|
497
|
+
class Decoder {
|
|
498
|
+
constructor(data, limits) {
|
|
499
|
+
this.data = data;
|
|
500
|
+
this.limits = limits;
|
|
501
|
+
this.elements = 0;
|
|
502
|
+
}
|
|
503
|
+
word(at, where) {
|
|
504
|
+
if (at + WORD > this.data.length) fail("E_ABI_DECODE", `${where}: the data ends before this value`);
|
|
505
|
+
let x = 0n;
|
|
506
|
+
for (let i = 0; i < WORD; i++) x = (x << 8n) | BigInt(this.data[at + i]);
|
|
507
|
+
return x;
|
|
508
|
+
}
|
|
509
|
+
size(at, where, what) {
|
|
510
|
+
const x = this.word(at, where);
|
|
511
|
+
if (x > BigInt(this.data.length)) fail("E_ABI_DECODE", `${where}: ${what} ${x} is beyond the end of the data`);
|
|
512
|
+
return Number(x);
|
|
513
|
+
}
|
|
514
|
+
tuple(types, base, depth, at) {
|
|
515
|
+
if (depth > this.limits.maxDepth) fail("E_ABI_DECODE", `${at(0)}: nested deeper than ${this.limits.maxDepth}`);
|
|
516
|
+
const out = [];
|
|
517
|
+
let pos = base;
|
|
518
|
+
types.forEach((t, i) => {
|
|
519
|
+
if (isDynamic(t)) {
|
|
520
|
+
out.push(this.value(t, base + this.size(pos, at(i), "offset"), depth + 1, at(i)));
|
|
521
|
+
pos += WORD;
|
|
522
|
+
} else {
|
|
523
|
+
out.push(this.value(t, pos, depth + 1, at(i)));
|
|
524
|
+
pos += headSize(t);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
return out;
|
|
528
|
+
}
|
|
529
|
+
value(t, at, depth, where) {
|
|
530
|
+
switch (t.kind) {
|
|
531
|
+
case "uint":
|
|
532
|
+
case "int": {
|
|
533
|
+
let x = this.word(at, where);
|
|
534
|
+
if (t.kind === "int") x = BigInt.asIntN(256, x);
|
|
535
|
+
return x.toString(); // a value outside the type is caught when re-encoded
|
|
536
|
+
}
|
|
537
|
+
case "bool":
|
|
538
|
+
return this.word(at, where) !== 0n;
|
|
539
|
+
case "address":
|
|
540
|
+
return toChecksumAddress(`0x${this.word(at, where).toString(16).padStart(64, "0").slice(-40)}`);
|
|
541
|
+
case "fixedbytes":
|
|
542
|
+
this.word(at, where);
|
|
543
|
+
return this.data.slice(at, at + t.size);
|
|
544
|
+
case "bytes":
|
|
545
|
+
case "string": {
|
|
546
|
+
const n = this.size(at, where, "length");
|
|
547
|
+
if (n > this.limits.maxBytes) fail("E_ABI_DECODE", `${where}: ${n} bytes is over the limit of ${this.limits.maxBytes}`);
|
|
548
|
+
if (at + WORD + n > this.data.length) fail("E_ABI_DECODE", `${where}: the data ends inside this value`);
|
|
549
|
+
const b = this.data.slice(at + WORD, at + WORD + n);
|
|
550
|
+
if (t.kind === "bytes") return b;
|
|
551
|
+
try {
|
|
552
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(b);
|
|
553
|
+
} catch {
|
|
554
|
+
fail("E_ABI_DECODE", `${where}: the string is not UTF-8`);
|
|
555
|
+
}
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
case "array": {
|
|
559
|
+
let n = t.length;
|
|
560
|
+
let base = at;
|
|
561
|
+
if (n === null) {
|
|
562
|
+
n = this.size(at, where, "length");
|
|
563
|
+
base = at + WORD;
|
|
564
|
+
}
|
|
565
|
+
this.elements += n;
|
|
566
|
+
if (this.elements > this.limits.maxElements) fail("E_ABI_DECODE", `${where}: more than ${this.limits.maxElements} array elements in the call`);
|
|
567
|
+
return this.tuple(Array(n).fill(t.element), base, depth, (i) => `${where}[${i}]`);
|
|
568
|
+
}
|
|
569
|
+
case "tuple": {
|
|
570
|
+
const keys = t.components.map(componentKey);
|
|
571
|
+
const vals = this.tuple(t.components.map((c) => c.type), at, depth, (i) => `${where}.${keys[i]}`);
|
|
572
|
+
return Object.fromEntries(keys.map((k, i) => [k, vals[i]]));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
fail("E_ABI_DECODE", `${where}: unknown type`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Decode ABI-encoded parameters strictly: the bytes must be exactly what `encodeParams` writes for the values read,
|
|
581
|
+
* so dirty padding, a value outside its type, an offset that is not the canonical one and trailing bytes are all
|
|
582
|
+
* refused (E_ABI_DECODE).
|
|
583
|
+
*
|
|
584
|
+
* @param {{ name: string | null, type: object }[]} params
|
|
585
|
+
* @param {Uint8Array} data
|
|
586
|
+
* @param {{ maxBytes?: number, maxDepth?: number, maxElements?: number }} [limits]
|
|
587
|
+
*/
|
|
588
|
+
export function decodeParams(params, data, limits = {}) {
|
|
589
|
+
const lim = { ...DEFAULT_ABI_LIMITS, ...limits };
|
|
590
|
+
if (!(data instanceof Uint8Array)) fail("E_ABI_DECODE", "the data must be a Uint8Array");
|
|
591
|
+
if (data.length > lim.maxBytes) fail("E_ABI_DECODE", `${data.length} bytes is over the limit of ${lim.maxBytes}`);
|
|
592
|
+
const t = { kind: "tuple", components: params };
|
|
593
|
+
const values = new Decoder(data, lim).value(t, 0, 0, "arguments");
|
|
594
|
+
let again;
|
|
595
|
+
try {
|
|
596
|
+
again = encodeValue(t, values, "arguments");
|
|
597
|
+
} catch (e) {
|
|
598
|
+
if (e instanceof AbiError) fail("E_ABI_DECODE", e.message.replace(/^arguments/, "arguments"));
|
|
599
|
+
throw e;
|
|
600
|
+
}
|
|
601
|
+
if (again.length !== data.length || again.some((b, i) => b !== data[i])) {
|
|
602
|
+
fail("E_ABI_DECODE", "the data is not the canonical ABI encoding of its values (dirty padding, a non-canonical offset or trailing bytes)");
|
|
603
|
+
}
|
|
604
|
+
return values;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ---------------------------------------------------------------------------------------------- EIP-712
|
|
608
|
+
|
|
609
|
+
const EIP712_DOMAIN = [
|
|
610
|
+
["name", "string"],
|
|
611
|
+
["version", "string"],
|
|
612
|
+
["chainId", "uint256"],
|
|
613
|
+
["verifyingContract", "address"],
|
|
614
|
+
["salt", "bytes32"],
|
|
615
|
+
];
|
|
616
|
+
|
|
617
|
+
const structText = (types, name) => `${name}(${types[name].map((f) => `${f.type} ${f.name}`).join(",")})`;
|
|
618
|
+
|
|
619
|
+
function typeEncoding(types, primary) {
|
|
620
|
+
const deps = new Set();
|
|
621
|
+
const visit = (name) => {
|
|
622
|
+
if (deps.has(name) || !Object.hasOwn(types, name)) return;
|
|
623
|
+
deps.add(name);
|
|
624
|
+
for (const f of types[name]) visit(f.type.replace(/(\[\d*\])+$/, ""));
|
|
625
|
+
};
|
|
626
|
+
visit(primary);
|
|
627
|
+
deps.delete(primary);
|
|
628
|
+
return [primary, ...[...deps].sort()].map((n) => structText(types, n)).join("");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* EIP-712 `encodeType` of a struct type: the type with its members, then every struct type it refers to, sorted by
|
|
633
|
+
* name (`Mail(Person from,Person to,string contents)Person(string name,address wallet)`).
|
|
634
|
+
*
|
|
635
|
+
* @param {Record<string, { name: string, type: string }[]>} types
|
|
636
|
+
* @param {string} primary
|
|
637
|
+
*/
|
|
638
|
+
export function encodeType(types, primary) {
|
|
639
|
+
if (!Object.hasOwn(types, primary)) fail("E_EIP712", `no type ${primary}`);
|
|
640
|
+
return typeEncoding(types, primary);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** EIP-712 `typeHash`: the Keccak-256 of `encodeType`, as `0x` and 64 hex digits. */
|
|
644
|
+
export const eip712TypeHash = (types, primary) => `0x${keccak256Hex(encodeType(types, primary))}`;
|
|
645
|
+
|
|
646
|
+
const EIP712_MEMBER = /^([A-Za-z_][A-Za-z0-9_]*)((?:\[\d*\])*)$/;
|
|
647
|
+
/** Names a struct may not take: they read as ABI types, or are the domain's own type. */
|
|
648
|
+
const RESERVED_STRUCT = /^(?:u?int\d*|u?fixed.*|address|bool|string|bytes\d*|byte|function|tuple|EIP712Domain)$/;
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Read the EIP-712 types of a profile (`binds.eip712.types`, spec section 15.9): struct names that are identifiers and
|
|
652
|
+
* not ABI type names, members with unique names, member types that are canonical ABI types (`uint256`, never `uint`:
|
|
653
|
+
* the type hash hashes the text as written) or struct types of the same map, with array suffixes, and no struct that
|
|
654
|
+
* contains itself. Every problem is reported, none thrown.
|
|
655
|
+
*
|
|
656
|
+
* @param {unknown} types
|
|
657
|
+
* @param {string} [label] how diagnostics name the map (`binds.eip712.types`)
|
|
658
|
+
* @returns {{ types: Record<string, { name: string, type: string }[]>, errors: { at: string, message: string }[] }}
|
|
659
|
+
*/
|
|
660
|
+
export function parseEip712Types(types, label = "types") {
|
|
661
|
+
const errors = [];
|
|
662
|
+
const err = (at, message) => void errors.push({ at, message });
|
|
663
|
+
if (types === null || typeof types !== "object" || Array.isArray(types)) return { types: {}, errors: [{ at: label, message: "EIP-712 types are an object of struct name to members" }] };
|
|
664
|
+
const out = {};
|
|
665
|
+
for (const [name, members] of Object.entries(types)) {
|
|
666
|
+
const at = `${label}.${name}`;
|
|
667
|
+
if (!IDENT.test(name)) { err(at, `the struct name ${JSON.stringify(name)} is not an identifier (letters, digits and _)`); continue; }
|
|
668
|
+
if (RESERVED_STRUCT.test(name)) { err(at, `a struct cannot be named ${name}: the name reads as an ABI type or the domain's type`); continue; }
|
|
669
|
+
if (!Array.isArray(members) || members.length === 0) { err(at, "a struct has at least one member, each { name, type }"); continue; }
|
|
670
|
+
const seen = new Set();
|
|
671
|
+
let ok = true;
|
|
672
|
+
members.forEach((m, i) => {
|
|
673
|
+
const where = `${at}[${i}]`;
|
|
674
|
+
if (m === null || typeof m !== "object" || typeof m.name !== "string" || typeof m.type !== "string") { ok = false; return err(where, "a member is { name, type } with two strings"); }
|
|
675
|
+
if (!IDENT.test(m.name)) { ok = false; return err(where, `the member name ${JSON.stringify(m.name)} is not an identifier`); }
|
|
676
|
+
if (seen.has(m.name)) { ok = false; return err(where, `two members are named ${m.name}`); }
|
|
677
|
+
seen.add(m.name);
|
|
678
|
+
const t = EIP712_MEMBER.exec(m.type);
|
|
679
|
+
if (!t) { ok = false; return err(where, `${JSON.stringify(m.type)} is not a type`); }
|
|
680
|
+
for (const d of t[2].matchAll(/\[(\d*)\]/g)) if (d[1] !== "" && !/^[1-9]\d{0,5}$/.test(d[1])) { ok = false; return err(where, `array length [${d[1]}] must be a positive integer`); }
|
|
681
|
+
if (Object.hasOwn(types, t[1])) return;
|
|
682
|
+
try {
|
|
683
|
+
const e = elementary(t[1], where);
|
|
684
|
+
if (typeString(e) !== t[1]) { ok = false; err(where, `write ${typeString(e)}, not ${t[1]}: the type hash hashes the type as written`); }
|
|
685
|
+
} catch (e) {
|
|
686
|
+
if (!(e instanceof AbiError)) throw e;
|
|
687
|
+
ok = false;
|
|
688
|
+
err(where, `${t[1]} is neither an ABI type nor a struct of these types`);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
if (ok) out[name] = members.map((m) => ({ name: m.name, type: m.type }));
|
|
692
|
+
}
|
|
693
|
+
// A struct that contains itself, directly or not, has no finite value to sign.
|
|
694
|
+
const state = new Map(); // name -> 1 on the walk, 2 done
|
|
695
|
+
const cyclic = new Set();
|
|
696
|
+
const visit = (name, trail) => {
|
|
697
|
+
if (state.get(name) === 2 || !Object.hasOwn(out, name)) return;
|
|
698
|
+
if (state.get(name) === 1) {
|
|
699
|
+
const loop = trail.slice(trail.indexOf(name));
|
|
700
|
+
for (const n of loop) cyclic.add(n);
|
|
701
|
+
err(`${label}.${name}`, `${[...loop, name].join(" -> ")}: a struct cannot contain itself`);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
state.set(name, 1);
|
|
705
|
+
for (const m of out[name]) visit(m.type.replace(/(\[\d*\])+$/, ""), [...trail, name]);
|
|
706
|
+
state.set(name, 2);
|
|
707
|
+
};
|
|
708
|
+
for (const name of Object.keys(out).sort()) visit(name, []);
|
|
709
|
+
for (const n of cyclic) delete out[n];
|
|
710
|
+
// A struct that refers to one refused above cannot be encoded either.
|
|
711
|
+
for (let dropped = true; dropped; ) {
|
|
712
|
+
dropped = false;
|
|
713
|
+
for (const name of Object.keys(out).sort()) {
|
|
714
|
+
const bad = out[name].map((m) => m.type.replace(/(\[\d*\])+$/, "")).find((t) => Object.hasOwn(types, t) && !Object.hasOwn(out, t));
|
|
715
|
+
if (bad === undefined) continue;
|
|
716
|
+
err(`${label}.${name}`, `refers to ${bad}, which is refused above`);
|
|
717
|
+
delete out[name];
|
|
718
|
+
dropped = true;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return { types: out, errors };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** The ABI type tree of an EIP-712 member type: a struct is a tuple of its members, in order. */
|
|
725
|
+
export function eip712AbiType(types, text) {
|
|
726
|
+
const m = EIP712_MEMBER.exec(text);
|
|
727
|
+
if (!m) fail("E_EIP712", `${JSON.stringify(text)} is not a type`);
|
|
728
|
+
const base = Object.hasOwn(types, m[1]) ? { kind: "tuple", components: types[m[1]].map((f) => ({ name: f.name, type: eip712AbiType(types, f.type) })) } : elementary(m[1], text);
|
|
729
|
+
return withDims(base, [...m[2].matchAll(/\[(\d*)\]/g)].map((d) => d[1]), text);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function encodeField(types, type, value, where) {
|
|
733
|
+
const arr = /^(.*)\[(\d*)\]$/.exec(type);
|
|
734
|
+
if (arr) {
|
|
735
|
+
if (!Array.isArray(value)) fail("E_ABI_ENCODE", `${where}: ${type} needs an array`);
|
|
736
|
+
if (arr[2] !== "" && value.length !== Number(arr[2])) fail("E_ABI_ENCODE", `${where}: ${type} needs ${arr[2]} elements`);
|
|
737
|
+
return keccak256(concat(value.map((v, i) => encodeField(types, arr[1], v, `${where}[${i}]`))));
|
|
738
|
+
}
|
|
739
|
+
if (Object.hasOwn(types, type)) return hashStruct(types, type, value, where);
|
|
740
|
+
if (type === "string") {
|
|
741
|
+
if (typeof value !== "string" || !value.isWellFormed()) fail("E_ABI_ENCODE", `${where}: string needs well-formed text`);
|
|
742
|
+
return keccak256(new TextEncoder().encode(value));
|
|
743
|
+
}
|
|
744
|
+
if (type === "bytes") return keccak256(bytesFrom(value, where));
|
|
745
|
+
const t = elementary(type, where);
|
|
746
|
+
if (t.kind === "uint" || t.kind === "int") {
|
|
747
|
+
const v = typeof value === "number" && Number.isSafeInteger(value) ? BigInt(value) : value; // EIP-712 JSON allows small numbers
|
|
748
|
+
return encodeValue(t, v, where);
|
|
749
|
+
}
|
|
750
|
+
return encodeValue(t, value, where);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function hashStruct(types, name, value, where) {
|
|
754
|
+
if (value === null || typeof value !== "object") fail("E_ABI_ENCODE", `${where}: ${name} needs an object`);
|
|
755
|
+
const fields = types[name];
|
|
756
|
+
for (const k of Object.keys(value)) if (!fields.some((f) => f.name === k)) fail("E_ABI_ENCODE", `${where}: ${name} has no field ${k}`);
|
|
757
|
+
const parts = [keccak256(typeEncoding(types, name))];
|
|
758
|
+
for (const f of fields) {
|
|
759
|
+
if (!Object.hasOwn(value, f.name)) fail("E_ABI_ENCODE", `${where}: missing ${name}.${f.name}`);
|
|
760
|
+
parts.push(encodeField(types, f.type, value[f.name], `${where}.${f.name}`));
|
|
761
|
+
}
|
|
762
|
+
return keccak256(concat(parts));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* The EIP-712 digest of typed data (`keccak256(0x1901 ‖ domainSeparator ‖ hashStruct(message))`): what a signer signs.
|
|
767
|
+
* `types` need not list EIP712Domain; it is derived from the domain's fields when absent.
|
|
768
|
+
*
|
|
769
|
+
* @param {{ domain: object, types: Record<string, { name: string, type: string }[]>, primaryType: string, message: object }} typed
|
|
770
|
+
* @returns {string} `0x` and 64 hex digits
|
|
771
|
+
*/
|
|
772
|
+
export function typedDataDigest({ domain, types, primaryType, message }) {
|
|
773
|
+
const all = { ...types };
|
|
774
|
+
if (!Object.hasOwn(all, "EIP712Domain")) all.EIP712Domain = EIP712_DOMAIN.filter(([k]) => Object.hasOwn(domain, k)).map(([name, type]) => ({ name, type }));
|
|
775
|
+
if (!Object.hasOwn(all, primaryType)) fail("E_ABI_ENCODE", `no type ${primaryType}`);
|
|
776
|
+
const separator = hashStruct(all, "EIP712Domain", domain, "domain");
|
|
777
|
+
const struct = hashStruct(all, primaryType, message, "message");
|
|
778
|
+
return `0x${hexOf(keccak256(concat([Uint8Array.of(0x19, 0x01), separator, struct])))}`;
|
|
779
|
+
}
|