@meantjs/profile 0.0.0-stage → 0.2.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/src/abi.mjs ADDED
@@ -0,0 +1,672 @@
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
+ /** The identity of an EVM interface: SHA-256 over the JCS array of its canonical fragments, sorted (spec section 15). */
324
+ export function abiInterfaceId(functions) {
325
+ return `candid-profile:evm-abi:v0:sha256:${sha256Hex(jcs(functions.map((f) => f.canonical).sort()))}`;
326
+ }
327
+
328
+ /** Read one function from its named signature (`transfer(address to,uint256 value)`), as a resolved form carries it. */
329
+ export function parseSignature(text, where = "signature") {
330
+ const r = new Reader(`function ${text}`, where);
331
+ r.word();
332
+ const name = r.word();
333
+ if (name === null) fail("E_ABI", `${where}: no function name in ${JSON.stringify(text)}`);
334
+ const inputs = r.params();
335
+ if (!r.end()) fail("E_ABI", `${where}: unexpected text after the parameters of ${JSON.stringify(text)}`);
336
+ return finish({ name, inputs, outputs: [], stateMutability: "nonpayable" }, where);
337
+ }
338
+
339
+ // ---------------------------------------------------------------------------------------------- EIP-55 addresses
340
+
341
+ const HEX40 = /^0x[0-9a-fA-F]{40}$/;
342
+
343
+ /** `0x` and 40 hex digits in the EIP-55 mixed-case checksum form. */
344
+ export function toChecksumAddress(address) {
345
+ if (typeof address !== "string" || !HEX40.test(address)) fail("E_ADDRESS", `${JSON.stringify(address)} is not an address (0x and 40 hex digits)`);
346
+ const lower = address.slice(2).toLowerCase();
347
+ const hash = keccak256Hex(lower);
348
+ let out = "0x";
349
+ for (let i = 0; i < 40; i++) out += parseInt(hash[i], 16) >= 8 ? lower[i].toUpperCase() : lower[i];
350
+ return out;
351
+ }
352
+
353
+ /**
354
+ * 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
355
+ * case carries none (EIP-55) and is accepted (O3). Returns the checksummed form.
356
+ */
357
+ export function readAddress(text) {
358
+ const checksummed = toChecksumAddress(text);
359
+ const digits = text.slice(2);
360
+ const mixed = digits !== digits.toLowerCase() && digits !== digits.toUpperCase();
361
+ if (mixed && text !== checksummed) fail("E_ADDRESS_CHECKSUM", `${text} is in mixed case but its EIP-55 checksum does not match (expected ${checksummed})`);
362
+ return checksummed;
363
+ }
364
+
365
+ // ---------------------------------------------------------------------------------------------- codec
366
+
367
+ const WORD = 32;
368
+ const isDynamic = (t) =>
369
+ 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)));
370
+ const headSize = (t) => {
371
+ if (isDynamic(t)) return WORD;
372
+ if (t.kind === "array") return t.length * headSize(t.element);
373
+ if (t.kind === "tuple") return t.components.reduce((n, c) => n + headSize(c.type), 0);
374
+ return WORD;
375
+ };
376
+
377
+ function word(v) {
378
+ const out = new Uint8Array(WORD);
379
+ let x = BigInt.asUintN(256, v);
380
+ for (let i = WORD - 1; i >= 0 && x > 0n; i--) {
381
+ out[i] = Number(x & 0xffn);
382
+ x >>= 8n;
383
+ }
384
+ return out;
385
+ }
386
+ const concat = (parts) => {
387
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
388
+ let at = 0;
389
+ for (const p of parts) {
390
+ out.set(p, at);
391
+ at += p.length;
392
+ }
393
+ return out;
394
+ };
395
+ const padded = (bytes) => {
396
+ const out = new Uint8Array(Math.ceil(bytes.length / WORD) * WORD);
397
+ out.set(bytes);
398
+ return out;
399
+ };
400
+ const hexOf = (bytes) => [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
401
+ const bytesFrom = (v, where) => {
402
+ if (v instanceof Uint8Array) return v;
403
+ if (typeof v === "string" && /^0x(?:[0-9a-fA-F]{2})*$/.test(v)) return Uint8Array.from(v.slice(2).match(/../g) ?? [], (h) => parseInt(h, 16));
404
+ fail("E_ABI_ENCODE", `${where}: bytes are a Uint8Array or 0x hex text`);
405
+ };
406
+
407
+ function intValue(t, v, where) {
408
+ let x;
409
+ if (typeof v === "bigint") x = v;
410
+ else if (typeof v === "string" && /^-?(?:0|[1-9]\d*)$/.test(v) && v !== "-0") x = BigInt(v);
411
+ else fail("E_ABI_ENCODE", `${where}: ${typeString(t)} needs a decimal string or a bigint, got ${typeof v === "number" ? "a number" : JSON.stringify(v)}`);
412
+ const [min, max] = t.kind === "uint" ? [0n, 2n ** BigInt(t.bits) - 1n] : [-(2n ** BigInt(t.bits - 1)), 2n ** BigInt(t.bits - 1) - 1n];
413
+ if (x < min || x > max) fail("E_ABI_ENCODE", `${where}: ${x} is outside ${typeString(t)}`);
414
+ return x;
415
+ }
416
+
417
+ function encodeValue(t, v, where) {
418
+ switch (t.kind) {
419
+ case "uint":
420
+ case "int":
421
+ return word(intValue(t, v, where));
422
+ case "bool":
423
+ if (typeof v !== "boolean") fail("E_ABI_ENCODE", `${where}: bool needs true or false`);
424
+ return word(v ? 1n : 0n);
425
+ case "address": {
426
+ let a;
427
+ try {
428
+ a = readAddress(v);
429
+ } catch (e) {
430
+ if (e instanceof AbiError) fail("E_ABI_ENCODE", `${where}: ${e.message}`);
431
+ throw e;
432
+ }
433
+ return word(BigInt(a));
434
+ }
435
+ case "fixedbytes": {
436
+ const b = bytesFrom(v, where);
437
+ if (b.length !== t.size) fail("E_ABI_ENCODE", `${where}: bytes${t.size} needs ${t.size} bytes, got ${b.length}`);
438
+ return padded(b);
439
+ }
440
+ case "bytes":
441
+ case "string": {
442
+ 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);
443
+ return concat([word(BigInt(b.length)), padded(b)]);
444
+ }
445
+ case "array": {
446
+ if (!Array.isArray(v)) fail("E_ABI_ENCODE", `${where}: ${typeString(t)} needs an array`);
447
+ if (t.length !== null && v.length !== t.length) fail("E_ABI_ENCODE", `${where}: ${typeString(t)} needs ${t.length} elements, got ${v.length}`);
448
+ const body = encodeTuple(v.map(() => t.element), v, (i) => `${where}[${i}]`);
449
+ return t.length === null ? concat([word(BigInt(v.length)), body]) : body;
450
+ }
451
+ case "tuple": {
452
+ if (v === null || typeof v !== "object" || Array.isArray(v)) fail("E_ABI_ENCODE", `${where}: a tuple needs an object keyed by component`);
453
+ const keys = t.components.map(componentKey);
454
+ for (const k of Object.keys(v)) if (!keys.includes(k)) fail("E_ABI_ENCODE", `${where}: no component ${k}`);
455
+ return encodeTuple(
456
+ t.components.map((c) => c.type),
457
+ keys.map((k) => (Object.hasOwn(v, k) ? v[k] : fail("E_ABI_ENCODE", `${where}: missing component ${k}`))),
458
+ (i) => `${where}.${keys[i]}`,
459
+ );
460
+ }
461
+ }
462
+ fail("E_ABI_ENCODE", `${where}: unknown type`);
463
+ }
464
+
465
+ function encodeTuple(types, values, at) {
466
+ const heads = [];
467
+ const tails = [];
468
+ let offset = types.reduce((n, t) => n + headSize(t), 0);
469
+ types.forEach((t, i) => {
470
+ const enc = encodeValue(t, values[i], at(i));
471
+ if (isDynamic(t)) {
472
+ heads.push(word(BigInt(offset)));
473
+ tails.push(enc);
474
+ offset += enc.length;
475
+ } else heads.push(enc);
476
+ });
477
+ return concat([...heads, ...tails]);
478
+ }
479
+
480
+ /** ABI-encode values of a parameter list (`[{ name, type }]`), keyed like a tuple. */
481
+ export function encodeParams(params, values) {
482
+ return encodeValue({ kind: "tuple", components: params }, values, "arguments");
483
+ }
484
+
485
+ /** Calldata: the selector, then the encoded parameters. */
486
+ export function encodeCall(f, values) {
487
+ const sel = Uint8Array.from(f.selector.slice(2).match(/../g), (h) => parseInt(h, 16));
488
+ return concat([sel, f.inputs.length ? encodeParams(f.inputs, values) : new Uint8Array(0)]);
489
+ }
490
+
491
+ export const DEFAULT_ABI_LIMITS = Object.freeze({ maxBytes: 128 * 1024, maxDepth: 32, maxElements: 4096 });
492
+
493
+ class Decoder {
494
+ constructor(data, limits) {
495
+ this.data = data;
496
+ this.limits = limits;
497
+ this.elements = 0;
498
+ }
499
+ word(at, where) {
500
+ if (at + WORD > this.data.length) fail("E_ABI_DECODE", `${where}: the data ends before this value`);
501
+ let x = 0n;
502
+ for (let i = 0; i < WORD; i++) x = (x << 8n) | BigInt(this.data[at + i]);
503
+ return x;
504
+ }
505
+ size(at, where, what) {
506
+ const x = this.word(at, where);
507
+ if (x > BigInt(this.data.length)) fail("E_ABI_DECODE", `${where}: ${what} ${x} is beyond the end of the data`);
508
+ return Number(x);
509
+ }
510
+ tuple(types, base, depth, at) {
511
+ if (depth > this.limits.maxDepth) fail("E_ABI_DECODE", `${at(0)}: nested deeper than ${this.limits.maxDepth}`);
512
+ const out = [];
513
+ let pos = base;
514
+ types.forEach((t, i) => {
515
+ if (isDynamic(t)) {
516
+ out.push(this.value(t, base + this.size(pos, at(i), "offset"), depth + 1, at(i)));
517
+ pos += WORD;
518
+ } else {
519
+ out.push(this.value(t, pos, depth + 1, at(i)));
520
+ pos += headSize(t);
521
+ }
522
+ });
523
+ return out;
524
+ }
525
+ value(t, at, depth, where) {
526
+ switch (t.kind) {
527
+ case "uint":
528
+ case "int": {
529
+ let x = this.word(at, where);
530
+ if (t.kind === "int") x = BigInt.asIntN(256, x);
531
+ return x.toString(); // a value outside the type is caught when re-encoded
532
+ }
533
+ case "bool":
534
+ return this.word(at, where) !== 0n;
535
+ case "address":
536
+ return toChecksumAddress(`0x${this.word(at, where).toString(16).padStart(64, "0").slice(-40)}`);
537
+ case "fixedbytes":
538
+ this.word(at, where);
539
+ return this.data.slice(at, at + t.size);
540
+ case "bytes":
541
+ case "string": {
542
+ const n = this.size(at, where, "length");
543
+ if (n > this.limits.maxBytes) fail("E_ABI_DECODE", `${where}: ${n} bytes is over the limit of ${this.limits.maxBytes}`);
544
+ if (at + WORD + n > this.data.length) fail("E_ABI_DECODE", `${where}: the data ends inside this value`);
545
+ const b = this.data.slice(at + WORD, at + WORD + n);
546
+ if (t.kind === "bytes") return b;
547
+ try {
548
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(b);
549
+ } catch {
550
+ fail("E_ABI_DECODE", `${where}: the string is not UTF-8`);
551
+ }
552
+ break;
553
+ }
554
+ case "array": {
555
+ let n = t.length;
556
+ let base = at;
557
+ if (n === null) {
558
+ n = this.size(at, where, "length");
559
+ base = at + WORD;
560
+ }
561
+ this.elements += n;
562
+ if (this.elements > this.limits.maxElements) fail("E_ABI_DECODE", `${where}: more than ${this.limits.maxElements} array elements in the call`);
563
+ return this.tuple(Array(n).fill(t.element), base, depth, (i) => `${where}[${i}]`);
564
+ }
565
+ case "tuple": {
566
+ const keys = t.components.map(componentKey);
567
+ const vals = this.tuple(t.components.map((c) => c.type), at, depth, (i) => `${where}.${keys[i]}`);
568
+ return Object.fromEntries(keys.map((k, i) => [k, vals[i]]));
569
+ }
570
+ }
571
+ fail("E_ABI_DECODE", `${where}: unknown type`);
572
+ }
573
+ }
574
+
575
+ /**
576
+ * Decode ABI-encoded parameters strictly: the bytes must be exactly what `encodeParams` writes for the values read,
577
+ * so dirty padding, a value outside its type, an offset that is not the canonical one and trailing bytes are all
578
+ * refused (E_ABI_DECODE).
579
+ *
580
+ * @param {{ name: string | null, type: object }[]} params
581
+ * @param {Uint8Array} data
582
+ * @param {{ maxBytes?: number, maxDepth?: number, maxElements?: number }} [limits]
583
+ */
584
+ export function decodeParams(params, data, limits = {}) {
585
+ const lim = { ...DEFAULT_ABI_LIMITS, ...limits };
586
+ if (!(data instanceof Uint8Array)) fail("E_ABI_DECODE", "the data must be a Uint8Array");
587
+ if (data.length > lim.maxBytes) fail("E_ABI_DECODE", `${data.length} bytes is over the limit of ${lim.maxBytes}`);
588
+ const t = { kind: "tuple", components: params };
589
+ const values = new Decoder(data, lim).value(t, 0, 0, "arguments");
590
+ let again;
591
+ try {
592
+ again = encodeValue(t, values, "arguments");
593
+ } catch (e) {
594
+ if (e instanceof AbiError) fail("E_ABI_DECODE", e.message.replace(/^arguments/, "arguments"));
595
+ throw e;
596
+ }
597
+ if (again.length !== data.length || again.some((b, i) => b !== data[i])) {
598
+ fail("E_ABI_DECODE", "the data is not the canonical ABI encoding of its values (dirty padding, a non-canonical offset or trailing bytes)");
599
+ }
600
+ return values;
601
+ }
602
+
603
+ // ---------------------------------------------------------------------------------------------- EIP-712
604
+
605
+ const EIP712_DOMAIN = [
606
+ ["name", "string"],
607
+ ["version", "string"],
608
+ ["chainId", "uint256"],
609
+ ["verifyingContract", "address"],
610
+ ["salt", "bytes32"],
611
+ ];
612
+
613
+ function typeEncoding(types, primary) {
614
+ const deps = new Set();
615
+ const visit = (name) => {
616
+ if (deps.has(name) || !Object.hasOwn(types, name)) return;
617
+ deps.add(name);
618
+ for (const f of types[name]) visit(f.type.replace(/(\[\d*\])+$/, ""));
619
+ };
620
+ visit(primary);
621
+ deps.delete(primary);
622
+ return [primary, ...[...deps].sort()].map((n) => `${n}(${types[n].map((f) => `${f.type} ${f.name}`).join(",")})`).join("");
623
+ }
624
+
625
+ function encodeField(types, type, value, where) {
626
+ const arr = /^(.*)\[(\d*)\]$/.exec(type);
627
+ if (arr) {
628
+ if (!Array.isArray(value)) fail("E_ABI_ENCODE", `${where}: ${type} needs an array`);
629
+ if (arr[2] !== "" && value.length !== Number(arr[2])) fail("E_ABI_ENCODE", `${where}: ${type} needs ${arr[2]} elements`);
630
+ return keccak256(concat(value.map((v, i) => encodeField(types, arr[1], v, `${where}[${i}]`))));
631
+ }
632
+ if (Object.hasOwn(types, type)) return hashStruct(types, type, value, where);
633
+ if (type === "string") {
634
+ if (typeof value !== "string" || !value.isWellFormed()) fail("E_ABI_ENCODE", `${where}: string needs well-formed text`);
635
+ return keccak256(new TextEncoder().encode(value));
636
+ }
637
+ if (type === "bytes") return keccak256(bytesFrom(value, where));
638
+ const t = elementary(type, where);
639
+ if (t.kind === "uint" || t.kind === "int") {
640
+ const v = typeof value === "number" && Number.isSafeInteger(value) ? BigInt(value) : value; // EIP-712 JSON allows small numbers
641
+ return encodeValue(t, v, where);
642
+ }
643
+ return encodeValue(t, value, where);
644
+ }
645
+
646
+ function hashStruct(types, name, value, where) {
647
+ if (value === null || typeof value !== "object") fail("E_ABI_ENCODE", `${where}: ${name} needs an object`);
648
+ const fields = types[name];
649
+ for (const k of Object.keys(value)) if (!fields.some((f) => f.name === k)) fail("E_ABI_ENCODE", `${where}: ${name} has no field ${k}`);
650
+ const parts = [keccak256(typeEncoding(types, name))];
651
+ for (const f of fields) {
652
+ if (!Object.hasOwn(value, f.name)) fail("E_ABI_ENCODE", `${where}: missing ${name}.${f.name}`);
653
+ parts.push(encodeField(types, f.type, value[f.name], `${where}.${f.name}`));
654
+ }
655
+ return keccak256(concat(parts));
656
+ }
657
+
658
+ /**
659
+ * The EIP-712 digest of typed data (`keccak256(0x1901 ‖ domainSeparator ‖ hashStruct(message))`): what a signer signs.
660
+ * `types` need not list EIP712Domain; it is derived from the domain's fields when absent.
661
+ *
662
+ * @param {{ domain: object, types: Record<string, { name: string, type: string }[]>, primaryType: string, message: object }} typed
663
+ * @returns {string} `0x` and 64 hex digits
664
+ */
665
+ export function typedDataDigest({ domain, types, primaryType, message }) {
666
+ const all = { ...types };
667
+ if (!Object.hasOwn(all, "EIP712Domain")) all.EIP712Domain = EIP712_DOMAIN.filter(([k]) => Object.hasOwn(domain, k)).map(([name, type]) => ({ name, type }));
668
+ if (!Object.hasOwn(all, primaryType)) fail("E_ABI_ENCODE", `no type ${primaryType}`);
669
+ const separator = hashStruct(all, "EIP712Domain", domain, "domain");
670
+ const struct = hashStruct(all, primaryType, message, "message");
671
+ return `0x${hexOf(keccak256(concat([Uint8Array.of(0x19, 0x01), separator, struct])))}`;
672
+ }