@meantjs/profile 0.1.0 → 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/CHANGELOG.md +23 -0
- package/README.md +31 -2
- package/data/packs/erc20.profile.json +221 -0
- package/data/profile-v0.schema.json +158 -6
- package/data/profile-v0.validate.mjs +4 -1
- package/package.json +8 -1
- package/src/abi-iface.mjs +112 -0
- package/src/abi-pack.mjs +114 -0
- package/src/abi.mjs +672 -0
- package/src/analyze.mjs +74 -28
- package/src/browser.d.mts +8 -4
- package/src/browser.mjs +2 -0
- package/src/checks.mjs +83 -10
- package/src/cli.mjs +12 -6
- package/src/evm.d.mts +86 -0
- package/src/evm.mjs +28 -0
- package/src/keccak.mjs +108 -0
- package/src/resolve.mjs +58 -5
- package/src/resolved.mjs +7 -3
- package/src/validate.mjs +20 -4
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
|
+
}
|