@meantjs/profile 0.2.0 → 0.4.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 +24 -0
- package/README.md +12 -4
- package/data/packs/dai-permit.profile.json +117 -0
- package/data/packs/erc20.profile.json +10 -2
- package/data/packs/erc2612.profile.json +204 -0
- package/data/packs/icrc1.profile.json +5 -1
- package/data/packs/icrc2.profile.json +8 -1
- package/data/packs/permit2.profile.json +451 -0
- package/data/profile-v0.schema.json +204 -9
- package/data/profile-v0.validate.mjs +11 -2
- package/package.json +1 -1
- package/src/abi-iface.mjs +49 -7
- package/src/abi-pack.mjs +5 -2
- package/src/abi.mjs +111 -4
- package/src/analyze.mjs +40 -6
- package/src/browser.mjs +6 -1
- package/src/checks.mjs +70 -1
- package/src/evm.d.mts +18 -3
- package/src/evm.mjs +5 -1
- package/src/resolve.mjs +45 -7
- package/src/resolved.mjs +14 -2
- package/src/validate.mjs +8 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meantjs/profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Candid Profile resolver: validates profiles, matches packs by Candid subtyping, merges layers (spec section 8) and emits the resolved form (spec section 10).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
package/src/abi-iface.mjs
CHANGED
|
@@ -9,17 +9,33 @@
|
|
|
9
9
|
* an unnamed one's is its position (`#1`)
|
|
10
10
|
* T[] and T[k] a vector (`[]`); T[k] keeps its length
|
|
11
11
|
* anything else a primitive named by its canonical ABI type (`uint256`, `address`, `bytes32`, `string`)
|
|
12
|
+
* an EIP-712 a method of mode "message" (spec section 15.9) whose one argument is the struct, a record of its
|
|
13
|
+
* message members in order (`PermitSingle(0).details.amount`); it has no results
|
|
12
14
|
*/
|
|
13
15
|
import { Iface } from "./iface.mjs";
|
|
14
16
|
import { idlHash } from "./idl.mjs";
|
|
15
|
-
import { AbiError, abiInterfaceId, componentKey, parseAbi, readAddress } from "./abi.mjs";
|
|
17
|
+
import { AbiError, abiInterfaceId, componentKey, eip712AbiType, eip712TypeHash, encodeType, parseAbi, parseEip712Types, readAddress } from "./abi.mjs";
|
|
16
18
|
|
|
17
19
|
export class AbiIface extends Iface {
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
+
/**
|
|
21
|
+
* @param functions from parseAbi
|
|
22
|
+
* @param eip712 { types, messages } (spec section 15.9): the struct types, and which of them are messages
|
|
23
|
+
*/
|
|
24
|
+
constructor(envelope, functions, label, eip712 = { types: {}, messages: [] }) {
|
|
20
25
|
super(envelope, label);
|
|
21
26
|
this.chain = "evm";
|
|
22
27
|
this.functions = new Map(functions.map((f) => [f.name, f]));
|
|
28
|
+
/** The identity of the functions alone: what a contract profile's `binds.abi` binds to (spec section 15.1). */
|
|
29
|
+
this.abiId = abiInterfaceId(functions);
|
|
30
|
+
this.structs = eip712.types;
|
|
31
|
+
this.messages = new Map(eip712.messages.map((name) => [name, { encodeType: encodeType(eip712.types, name), typeHash: eip712TypeHash(eip712.types, name) }]));
|
|
32
|
+
}
|
|
33
|
+
resolve(addr) {
|
|
34
|
+
const r = super.resolve(addr);
|
|
35
|
+
if (r.error && addr.kind !== "type" && !this.methods.has(addr.method) && Object.hasOwn(this.structs, addr.method)) {
|
|
36
|
+
return { error: `${addr.method} is an EIP-712 type but not a message: tag its name with intent to make it one, or address it through a message that contains it` };
|
|
37
|
+
}
|
|
38
|
+
return r;
|
|
23
39
|
}
|
|
24
40
|
isBlob(ref) {
|
|
25
41
|
const p = this.prim(ref);
|
|
@@ -43,12 +59,17 @@ export class AbiIface extends Iface {
|
|
|
43
59
|
/**
|
|
44
60
|
* @param entries an ABI: human-readable fragments or JSON ABI entries (parseAbi)
|
|
45
61
|
* @param label how diagnostics name it (`binds.abi`, `abi`)
|
|
62
|
+
* @param eip712 { types, messages, label } (spec section 15.9): struct types already read by parseEip712Types, the
|
|
63
|
+
* names of those that are messages, and how diagnostics name the types; null for none
|
|
64
|
+
* @param opts.allowEmpty an interface with no function and no message is not refused (resolveService adds the
|
|
65
|
+
* messages of its profiles later)
|
|
46
66
|
* @returns {{ iface: AbiIface } | { failure: { at: string, message: string }[] }}
|
|
47
67
|
*/
|
|
48
|
-
export function abiInterface(entries, label = "abi", ifaceLabel = "contract") {
|
|
68
|
+
export function abiInterface(entries, label = "abi", ifaceLabel = "contract", eip712 = null, { allowEmpty = false } = {}) {
|
|
49
69
|
const { functions, errors } = parseAbi(entries, label);
|
|
50
70
|
if (errors.length) return { failure: errors };
|
|
51
|
-
|
|
71
|
+
const messages = eip712?.messages ?? [];
|
|
72
|
+
if (functions.length === 0 && messages.length === 0 && !allowEmpty) return { failure: [eip712 ? { at: eip712.label, message: "the ABI has no functions and no type is a message (tag a type's name with intent)" } : { at: label, message: "the ABI has no functions" }] };
|
|
52
73
|
const types = [];
|
|
53
74
|
const names = [];
|
|
54
75
|
const failure = [];
|
|
@@ -81,13 +102,34 @@ export function abiInterface(entries, label = "abi", ifaceLabel = "contract") {
|
|
|
81
102
|
const mode = f.stateMutability === "view" || f.stateMutability === "pure" ? "query" : "update";
|
|
82
103
|
return { name: f.name, function: add({ kind: "func", mode, args, results }) };
|
|
83
104
|
});
|
|
105
|
+
for (const name of messages) {
|
|
106
|
+
const at = `${eip712.label}.${name}`;
|
|
107
|
+
if (functions.some((f) => f.name === name)) {
|
|
108
|
+
failure.push({ at, message: `a function and a message are both named ${name}; an address names one of them` });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
methods.push({ name, function: add({ kind: "func", mode: "message", args: [build(eip712AbiType(eip712.types, name), `${name}(0)`)], results: [] }) });
|
|
112
|
+
}
|
|
84
113
|
if (failure.length) return { failure };
|
|
85
114
|
const service = add({ kind: "service", methods });
|
|
115
|
+
const structs = eip712?.types ?? {};
|
|
86
116
|
const envelope = {
|
|
87
|
-
contract: { types, declarations: [], actor: { kind: "service", service }, identities: { interface: abiInterfaceId(functions) } },
|
|
117
|
+
contract: { types, declarations: [], actor: { kind: "service", service }, identities: { interface: abiInterfaceId(functions, structs) } },
|
|
88
118
|
extensions: { "org.candid-core.field-names/v1": names },
|
|
89
119
|
};
|
|
90
|
-
return { iface: new AbiIface(envelope, functions, ifaceLabel) };
|
|
120
|
+
return { iface: new AbiIface(envelope, functions, ifaceLabel, { types: structs, messages }) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The messages a profile or pack declares (spec section 15.9): the types of `binds.eip712` whose name the same
|
|
125
|
+
* document tags with `intent`. Returns { types, messages, errors }.
|
|
126
|
+
*/
|
|
127
|
+
export function eip712Of(doc, label = "binds.eip712.types") {
|
|
128
|
+
const spec = doc.binds?.eip712;
|
|
129
|
+
if (spec === undefined) return { types: {}, messages: [], errors: [] };
|
|
130
|
+
const { types, errors } = parseEip712Types(spec.types, label);
|
|
131
|
+
const messages = [...new Set(doc.annotations.filter((a) => a.tag === "intent" && Object.hasOwn(types, a.at)).map((a) => a.at))].sort();
|
|
132
|
+
return { types, messages, errors };
|
|
91
133
|
}
|
|
92
134
|
|
|
93
135
|
/**
|
package/src/abi-pack.mjs
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Parameter names are not part of a selector, and contracts name the parameters of one standard differently (EIP-20
|
|
7
7
|
* says `_to`, OpenZeppelin `to`, WETH `dst`). A pack's addresses and template placeholders use the pack's own names;
|
|
8
|
-
* when the pack applies, they are rewritten to the contract's names, component by component, by position.
|
|
8
|
+
* when the pack applies, they are rewritten to the contract's names, component by component, by position. So is an
|
|
9
|
+
* amount's `token` path (spec section 15.9).
|
|
9
10
|
*/
|
|
10
11
|
import { parseAddress, parseRelPath } from "./address.mjs";
|
|
11
12
|
import { componentKey, parseAbi, typeString } from "./abi.mjs";
|
|
@@ -107,7 +108,9 @@ export function translatePack(pack, functions, iface) {
|
|
|
107
108
|
};
|
|
108
109
|
const annotations = pack.annotations.map((a) => {
|
|
109
110
|
const method = parseAddress(a.at)?.method;
|
|
110
|
-
|
|
111
|
+
let args = a.args;
|
|
112
|
+
if (a.tag === "intent" && typeof a.args?.template === "string" && method) args = { ...a.args, template: mapTemplate(a.args.template, placeholder(method)) };
|
|
113
|
+
if (a.tag === "amount" && typeof a.args?.token === "string" && method) args = { ...a.args, token: placeholder(method)(a.args.token) ?? a.args.token };
|
|
111
114
|
return { ...a, at: at(a.at), args };
|
|
112
115
|
});
|
|
113
116
|
return { ...pack, annotations };
|
package/src/abi.mjs
CHANGED
|
@@ -320,9 +320,13 @@ export function parseAbi(entries, label = "abi") {
|
|
|
320
320
|
return { functions: kept.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)), errors };
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
/**
|
|
324
|
-
|
|
325
|
-
|
|
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()))}`;
|
|
326
330
|
}
|
|
327
331
|
|
|
328
332
|
/** Read one function from its named signature (`transfer(address to,uint256 value)`), as a resolved form carries it. */
|
|
@@ -610,6 +614,8 @@ const EIP712_DOMAIN = [
|
|
|
610
614
|
["salt", "bytes32"],
|
|
611
615
|
];
|
|
612
616
|
|
|
617
|
+
const structText = (types, name) => `${name}(${types[name].map((f) => `${f.type} ${f.name}`).join(",")})`;
|
|
618
|
+
|
|
613
619
|
function typeEncoding(types, primary) {
|
|
614
620
|
const deps = new Set();
|
|
615
621
|
const visit = (name) => {
|
|
@@ -619,7 +625,108 @@ function typeEncoding(types, primary) {
|
|
|
619
625
|
};
|
|
620
626
|
visit(primary);
|
|
621
627
|
deps.delete(primary);
|
|
622
|
-
return [primary, ...[...deps].sort()].map((n) =>
|
|
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);
|
|
623
730
|
}
|
|
624
731
|
|
|
625
732
|
function encodeField(types, type, value, where) {
|
package/src/analyze.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { TagIndex, LayeredTags, positions } from "./tags.mjs";
|
|
|
9
9
|
import {
|
|
10
10
|
NUMERIC, METHOD_TAGS, CASE_TAGS, CONFIRM_KINDS, STALE_CODES, VALUE_CONVERSION_TAGS, EVM_LATER_TAGS,
|
|
11
11
|
checkApplicability, checkDynamic, parseTemplate, outcomeCoverage, caseTargetError, otherCase, overlap, defaultEffect,
|
|
12
|
+
tokenPathError, grantError,
|
|
12
13
|
} from "./checks.mjs";
|
|
13
14
|
|
|
14
15
|
/** Spec section 8, highest precedence first. */
|
|
@@ -111,6 +112,11 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
111
112
|
|
|
112
113
|
/** An effect against the method's mode (spec 5.3); for a case effect, the mode of the method it sits in. */
|
|
113
114
|
const checkEffectMode = (m, { kind, irreversible, replicated }, diag, at) => {
|
|
115
|
+
// A signed EIP-712 message (spec section 15.9) runs nothing when it is signed: any kind describes what it allows.
|
|
116
|
+
if (m.mode === "message") {
|
|
117
|
+
if (replicated === true) diag("error", "E_EFFECT_MODE", at, "replicated: true applies to update methods, not to a message");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
114
120
|
if (m.mode !== "update" && m.mode !== "oneway" && kind !== "read") diag("error", "E_EFFECT_MODE", at, `${m.mode} method cannot have effect kind "${kind}"`);
|
|
115
121
|
// A read-only update (D-038): a read sent as an update so that its answer goes through consensus.
|
|
116
122
|
if (replicated === true) {
|
|
@@ -141,6 +147,18 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
141
147
|
}
|
|
142
148
|
};
|
|
143
149
|
|
|
150
|
+
/**
|
|
151
|
+
* A grant that does not hold (spec 5.3, D-056). A pack applied to a service loses only its grant, never the effect:
|
|
152
|
+
* dropping the effect would leave a transfer or approve with the default effect, which needs no confirmation.
|
|
153
|
+
*/
|
|
154
|
+
const grants = []; // effects whose grant holds, checked again once dropped tags are known
|
|
155
|
+
const badGrant = (e, diag, why) => {
|
|
156
|
+
if (!e.item.apply) return diag("error", "E_GRANT", e.ann.at, why);
|
|
157
|
+
const { grant, ...args } = e.entry.args;
|
|
158
|
+
e.entry.args = args;
|
|
159
|
+
e.item.diag("info", "I_PACK_ANNOTATION_SKIPPED", e.ann.at, `effect grant dropped, the effect kept: E_GRANT: ${why}`);
|
|
160
|
+
};
|
|
161
|
+
|
|
144
162
|
const sets = new Map(); // names set -> { at, method, fields: { name, value, show?, kind? } } (spec 5.3, D-045)
|
|
145
163
|
const named = []; // named positions, checked once every set is known
|
|
146
164
|
for (const e of entries) {
|
|
@@ -161,8 +179,10 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
161
179
|
const why = caseTargetError(iface, addr, res, a.tag);
|
|
162
180
|
if (why) { diag("error", "E_TAG_TARGET", at, why); continue; }
|
|
163
181
|
const m = iface.methods.get(addr.method);
|
|
164
|
-
if (a.tag === "effect")
|
|
165
|
-
|
|
182
|
+
if (a.tag === "effect") {
|
|
183
|
+
checkEffectMode(m, a.args, diag, at);
|
|
184
|
+
if (a.args.grant) badGrant(e, diag, "a grant sits on the effect of a method, not of a case, in this version (D-056)");
|
|
185
|
+
} else checkTemplate(item, addr.method, a.args.template, diag, at, addr);
|
|
166
186
|
caseEntries.push({ e, diag });
|
|
167
187
|
continue;
|
|
168
188
|
}
|
|
@@ -170,6 +190,11 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
170
190
|
const m = res.method;
|
|
171
191
|
const methodName = addr.method;
|
|
172
192
|
if (a.tag === "effect") checkEffectMode(m, a.args, diag, at);
|
|
193
|
+
if (a.tag === "effect" && a.args.grant) {
|
|
194
|
+
const why = grantError(iface, tags, methodName, a.args);
|
|
195
|
+
if (why) badGrant(e, diag, why);
|
|
196
|
+
else grants.push({ e, diag });
|
|
197
|
+
}
|
|
173
198
|
if (a.tag === "idempotency") {
|
|
174
199
|
if (m.mode !== "update") diag("error", "E_IDEMPOTENCY_MODE", at, "idempotency applies to update methods");
|
|
175
200
|
const r = iface.resolveRel(methodName, "args", a.args.stamp);
|
|
@@ -234,7 +259,10 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
234
259
|
const err = checkApplicability(iface, a.tag, res.ref);
|
|
235
260
|
if (err) diag("error", "E_TAG_NOT_APPLICABLE", at, err);
|
|
236
261
|
if (a.tag === "named") named.push({ a, res, diag, at });
|
|
237
|
-
if (a.tag === "amount") {
|
|
262
|
+
if (a.tag === "amount" && a.args.token !== undefined) {
|
|
263
|
+
const why = tokenPathError(iface, addr, a.args.token);
|
|
264
|
+
if (why) diag("error", "E_TOKEN_PATH", at, why);
|
|
265
|
+
} else if (a.tag === "amount") {
|
|
238
266
|
checkDynamic(iface, a.args.decimals, "numeric", diag, at);
|
|
239
267
|
checkDynamic(iface, a.args.symbol, "text", diag, at);
|
|
240
268
|
}
|
|
@@ -280,9 +308,14 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
280
308
|
}
|
|
281
309
|
}
|
|
282
310
|
|
|
283
|
-
// Dropping a value tag can leave an idempotency stamp without its timestamp tag: drop that idempotency too.
|
|
311
|
+
// Dropping a value tag can leave an idempotency stamp without its timestamp tag: drop that idempotency too. It can
|
|
312
|
+
// also leave a grant without the amount, timestamp or account tag it needs.
|
|
284
313
|
if (dropped.size) {
|
|
285
314
|
const without = buildLayers(entries, (e) => !dropped.has(e));
|
|
315
|
+
for (const { e, diag } of grants) {
|
|
316
|
+
const why = !dropped.has(e) && grantError(iface, without, e.addr.method, e.entry.args);
|
|
317
|
+
if (why) badGrant(e, diag, `${why} (a tag it needs was dropped in this service)`);
|
|
318
|
+
}
|
|
286
319
|
for (const e of entries) {
|
|
287
320
|
if (!e.item.apply || e.ann.tag !== "idempotency" || dropped.has(e)) continue;
|
|
288
321
|
const r = iface.resolveRel(e.addr.method, "args", e.ann.args.stamp);
|
|
@@ -326,10 +359,11 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
326
359
|
}
|
|
327
360
|
}
|
|
328
361
|
|
|
329
|
-
// Coverage hint: numeric positions in update-
|
|
362
|
+
// Coverage hint: numeric positions in the arguments of update methods and EIP-712 messages with no meaning attached
|
|
363
|
+
// (merged view).
|
|
330
364
|
const MEANING = ["amount", "timestamp", "duration", "describe", "range"];
|
|
331
365
|
for (const [name, m] of iface.methods) {
|
|
332
|
-
if (m.mode !== "update") continue;
|
|
366
|
+
if (m.mode !== "update" && m.mode !== "message") continue;
|
|
333
367
|
for (const pos of positions(iface, name, "args")) {
|
|
334
368
|
const p = iface.prim(iface.unwrapOpt(pos.ref).ref);
|
|
335
369
|
const t = final.at(pos, iface);
|
package/src/browser.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import schema from "../data/profile-v0.schema.json" with { type: "json" };
|
|
|
11
11
|
import icrc1 from "../data/packs/icrc1.profile.json" with { type: "json" };
|
|
12
12
|
import icrc2 from "../data/packs/icrc2.profile.json" with { type: "json" };
|
|
13
13
|
import erc20 from "../data/packs/erc20.profile.json" with { type: "json" };
|
|
14
|
+
import erc2612 from "../data/packs/erc2612.profile.json" with { type: "json" };
|
|
14
15
|
import { compileSchema } from "./schema-core.mjs";
|
|
15
16
|
|
|
16
17
|
export { init as initCompiler } from "@candid-core/cli";
|
|
@@ -32,9 +33,13 @@ export { readServedProfile, SERVED_SECTION, SERVED_METADATA, SERVED_MAX_BYTES }
|
|
|
32
33
|
/** The profile schema (a copy of spec/profile-v0.schema.json). */
|
|
33
34
|
export const PROFILE_SCHEMA = schema;
|
|
34
35
|
|
|
35
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* The standard packs (copies of packs/*.profile.json whose kind is "pack"), sorted by file name, in the shape
|
|
38
|
+
* `loadPacks` returns. The standard contract profiles there (Permit2, DAI's permit) are under `./packs/*` too.
|
|
39
|
+
*/
|
|
36
40
|
export const STANDARD_PACKS = Object.freeze([
|
|
37
41
|
{ file: "erc20.profile.json", doc: erc20 },
|
|
42
|
+
{ file: "erc2612.profile.json", doc: erc2612 },
|
|
38
43
|
{ file: "icrc1.profile.json", doc: icrc1 },
|
|
39
44
|
{ file: "icrc2.profile.json", doc: icrc2 },
|
|
40
45
|
]);
|
package/src/checks.mjs
CHANGED
|
@@ -18,7 +18,7 @@ export const CASE_TAGS = new Set(["intent", "effect"]);
|
|
|
18
18
|
export const CONFIRM_KINDS = new Set(["transfer", "approve", "admin", "destroy"]);
|
|
19
19
|
/** Tags that convert a value between its wire form and a human form; a stale profile must not supply them (spec section 7). */
|
|
20
20
|
export const VALUE_CONVERSION_TAGS = new Set(["amount", "timestamp", "duration", "account", "subaccount", "bytes", "named"]);
|
|
21
|
-
export const STALE_CODES = new Set(["E_ADDR_UNRESOLVED", "E_TYPE_NODE", "E_TAG_NOT_APPLICABLE", "E_REF_UNRESOLVED", "E_REF_NOT_QUERY", "E_REF_SHAPE", "E_REF_TYPE", "E_STAMP_UNTAGGED", "E_OUTCOME_NOT_CASE", "E_TEMPLATE_PATH", "E_TAG_TARGET", "E_NAMES_MODE", "E_NAMES_SHAPE", "E_NAMES_FIELD", "E_NAMED_SET", "E_NAMED_TYPE", "E_CASE_OVERLAP", "E_CASE_EFFECT"]);
|
|
21
|
+
export const STALE_CODES = new Set(["E_ADDR_UNRESOLVED", "E_TYPE_NODE", "E_TAG_NOT_APPLICABLE", "E_REF_UNRESOLVED", "E_REF_NOT_QUERY", "E_REF_SHAPE", "E_REF_TYPE", "E_STAMP_UNTAGGED", "E_OUTCOME_NOT_CASE", "E_TEMPLATE_PATH", "E_TAG_TARGET", "E_NAMES_MODE", "E_NAMES_SHAPE", "E_NAMES_FIELD", "E_NAMED_SET", "E_NAMED_TYPE", "E_CASE_OVERLAP", "E_CASE_EFFECT", "E_TOKEN_PATH", "E_GRANT"]);
|
|
22
22
|
|
|
23
23
|
export function checkApplicability(iface, tag, ref) {
|
|
24
24
|
const { ref: r } = iface.unwrapOpt(ref);
|
|
@@ -37,6 +37,75 @@ export function checkApplicability(iface, tag, ref) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* An amount's `token` (spec section 15.9): a path from the arguments of the same function or message to the `address`
|
|
42
|
+
* of the token the amount is in, through the same array elements as the amount. The reason it is wrong, or null.
|
|
43
|
+
*/
|
|
44
|
+
export function tokenPathError(iface, addr, token) {
|
|
45
|
+
if (iface.chain !== "evm") return "token names the address of an EVM token; a Candid amount's ledger cannot be named by a field yet (OQ-6)";
|
|
46
|
+
if (addr.kind !== "arg") return "an amount with a token sits in the arguments of a function or message, where the token's address is";
|
|
47
|
+
const p = parseRelPath(token);
|
|
48
|
+
const r = iface.resolveRel(addr.method, "args", token);
|
|
49
|
+
if (!p || r.error) return `token ${JSON.stringify(token)}: ${r.error ?? "not a relative path"}`;
|
|
50
|
+
if ((p.index ?? 0) !== addr.index) return `token ${JSON.stringify(token)} is in another argument than the amount`;
|
|
51
|
+
if (iface.prim(r.ref) !== "address") return `token ${JSON.stringify(token)} is ${iface.describeType(r.ref)}, not an address`;
|
|
52
|
+
const last = p.segs.findLastIndex((s) => s.kind === "elem");
|
|
53
|
+
const same = (a, b) => a.kind === b.kind && (a.kind === "elem" || a.id === b.id);
|
|
54
|
+
if (last >= 0 && (addr.segs.length <= last || !p.segs.slice(0, last + 1).every((s, i) => same(s, addr.segs[i])))) {
|
|
55
|
+
return `token ${JSON.stringify(token)} must be in the same array element as the amount: its path up to its last [] must be the amount's`;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* An effect's grant (spec 5.3, D-056): who may take the tokens, how many and until when, as positions in the arguments
|
|
62
|
+
* of the method the effect is on. `tags` is the merged view: the amount, timestamp and account tags a grant needs may
|
|
63
|
+
* come from another profile. The reason it is wrong, or null.
|
|
64
|
+
*/
|
|
65
|
+
export function grantError(iface, tags, methodName, { kind, grant }) {
|
|
66
|
+
if (kind !== "transfer" && kind !== "approve") return `a grant goes with effect kind transfer or approve, not ${kind}`;
|
|
67
|
+
if ((grant.spender === undefined) === (grant.to === undefined)) return "a grant names exactly one of spender and to";
|
|
68
|
+
if (kind === "approve" && grant.to !== undefined) return "an approve's grant names a spender: an allowance is taken by its spender, not paid to it";
|
|
69
|
+
const at = (name, rel, mayBeAbsent) => {
|
|
70
|
+
const p = parseRelPath(rel);
|
|
71
|
+
const r = iface.resolveRel(methodName, "args", rel);
|
|
72
|
+
const what = `grant ${name} ${JSON.stringify(rel)}`;
|
|
73
|
+
if (!p || r.error) return { error: `${what}: ${r.error ?? "not a relative path"}` };
|
|
74
|
+
if (p.segs.some((s) => s.kind === "elem")) return { error: `${what} steps into []: a grant covers one token, so a request that grants several has none` };
|
|
75
|
+
// The value must be in every call: no case of a variant on the way, and no opt unless it may be left out.
|
|
76
|
+
let cur = iface.methods.get(methodName).args[p.index ?? 0];
|
|
77
|
+
let optional = false;
|
|
78
|
+
for (const s of p.segs) {
|
|
79
|
+
const u = iface.unwrapOpt(cur);
|
|
80
|
+
optional ||= u.optional;
|
|
81
|
+
const n = iface.node(u.ref);
|
|
82
|
+
if (n.kind === "variant") return { error: `${what} passes through a case of a variant, so a call need not hold it` };
|
|
83
|
+
cur = n.fields.find((f) => f.id === s.id).type;
|
|
84
|
+
}
|
|
85
|
+
if ((optional || iface.unwrapOpt(cur).optional) && !mayBeAbsent) return { error: `${what} is optional, but a grant needs its ${name} in every call` };
|
|
86
|
+
return { r, t: tags.at(r, iface) ?? {} };
|
|
87
|
+
};
|
|
88
|
+
const who = grant.spender !== undefined ? "spender" : "to";
|
|
89
|
+
const party = at(who, grant[who], false);
|
|
90
|
+
if (party.error) return party.error;
|
|
91
|
+
if (iface.prim(party.r.ref) !== "address" && !party.t.account) {
|
|
92
|
+
return `grant ${who} ${JSON.stringify(grant[who])} is ${iface.describeType(party.r.ref)}; it must be an address or carry an account tag`;
|
|
93
|
+
}
|
|
94
|
+
const amount = at("amount", grant.amount, false);
|
|
95
|
+
if (amount.error) return amount.error;
|
|
96
|
+
const p = iface.prim(amount.r.ref);
|
|
97
|
+
if (p !== "bool") {
|
|
98
|
+
if (!amount.t.amount) return `grant amount ${JSON.stringify(grant.amount)} is ${iface.describeType(amount.r.ref)} without an amount tag; it must carry one, or be a bool`;
|
|
99
|
+
if (!/^(nat|uint)/.test(p)) return `grant amount ${JSON.stringify(grant.amount)} is ${p}; an amount granted is never negative, so its type is unsigned`;
|
|
100
|
+
}
|
|
101
|
+
for (const rel of grant.ends ?? []) {
|
|
102
|
+
const end = at("ends", rel, true);
|
|
103
|
+
if (end.error) return end.error;
|
|
104
|
+
if (!end.t.timestamp) return `grant ends ${JSON.stringify(rel)} has no timestamp tag`;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
40
109
|
export function checkDynamic(iface, value, want, diag, at) {
|
|
41
110
|
if (!value || typeof value !== "object") return;
|
|
42
111
|
const from = value.from;
|
package/src/evm.d.mts
CHANGED
|
@@ -38,7 +38,7 @@ export interface AbiLimits {
|
|
|
38
38
|
maxDepth?: number;
|
|
39
39
|
maxElements?: number;
|
|
40
40
|
}
|
|
41
|
-
/** Every failure of the ABI helpers: E_ABI (a fragment), E_ABI_ENCODE, E_ABI_DECODE, E_ADDRESS or E_ADDRESS_CHECKSUM. */
|
|
41
|
+
/** Every failure of the ABI helpers: E_ABI (a fragment), E_ABI_ENCODE, E_ABI_DECODE, E_EIP712, E_ADDRESS or E_ADDRESS_CHECKSUM. */
|
|
42
42
|
export declare class AbiError extends Error {
|
|
43
43
|
constructor(code: string, message: string);
|
|
44
44
|
name: "AbiError";
|
|
@@ -62,8 +62,11 @@ export const DEFAULT_ABI_LIMITS: Readonly<Required<AbiLimits>>;
|
|
|
62
62
|
export function parseAbi(entries: unknown, label?: string): { functions: AbiFunction[]; errors: { at: string; message: string }[] };
|
|
63
63
|
/** One function from its named signature, as a resolved form carries it. Throws AbiError. */
|
|
64
64
|
export function parseSignature(signature: string, where?: string): AbiFunction;
|
|
65
|
-
/**
|
|
66
|
-
|
|
65
|
+
/**
|
|
66
|
+
* `candid-profile:evm-abi:v0:sha256:…`: SHA-256 over the JCS array of the canonical fragments and, when given EIP-712
|
|
67
|
+
* types, one `struct Name(type member,…)` line per type, sorted.
|
|
68
|
+
*/
|
|
69
|
+
export function abiInterfaceId(functions: readonly AbiFunction[], types?: Eip712Types): string;
|
|
67
70
|
export function canonicalFragment(f: Pick<AbiFunction, "name" | "inputs" | "outputs" | "stateMutability">): string;
|
|
68
71
|
export function namedSignature(f: Pick<AbiFunction, "name" | "inputs">): string;
|
|
69
72
|
export function typeSignature(f: Pick<AbiFunction, "name" | "inputs">): string;
|
|
@@ -84,3 +87,15 @@ export function toChecksumAddress(address: string): string;
|
|
|
84
87
|
export function readAddress(text: string): string;
|
|
85
88
|
/** The EIP-712 digest a signer signs, as `0x` and 64 hex digits. */
|
|
86
89
|
export function typedDataDigest(typed: TypedData): string;
|
|
90
|
+
/** EIP-712 struct types: struct name to its members, in order. */
|
|
91
|
+
export type Eip712Types = Record<string, { name: string; type: string }[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Read EIP-712 struct types (spec section 15.9): identifier names that are not ABI types, unique member names,
|
|
94
|
+
* canonical member types (`uint256`, never `uint`) or structs of the same map, and no struct that contains itself.
|
|
95
|
+
* Problems are listed, not thrown; `types` keeps the structs that read.
|
|
96
|
+
*/
|
|
97
|
+
export function parseEip712Types(types: unknown, label?: string): { types: Eip712Types; errors: { at: string; message: string }[] };
|
|
98
|
+
/** EIP-712 `encodeType`: the struct with its members, then the structs it refers to, sorted by name. Throws AbiError (E_EIP712). */
|
|
99
|
+
export function encodeType(types: Eip712Types, primary: string): string;
|
|
100
|
+
/** EIP-712 `typeHash`: the Keccak-256 of `encodeType`, as `0x` and 64 hex digits. */
|
|
101
|
+
export function eip712TypeHash(types: Eip712Types, primary: string): string;
|
package/src/evm.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@meantjs/profile/evm`: the EVM helpers of spec section 15 (D-054), browser-safe and without dependencies.
|
|
3
3
|
* Keccak-256, ABI fragments (reading, canonical text, selectors, the interface identity), a strict ABI codec, EIP-55
|
|
4
|
-
* addresses
|
|
4
|
+
* addresses, EIP-712 types (section 15.9: reading them, encodeType, type hashes) and digests. The consent package
|
|
5
|
+
* builds calldata display, typed-message display and the ERC-7730 export on them.
|
|
5
6
|
*
|
|
6
7
|
* test/evm.test.mjs checks the vectors; test/address-entry.test.mjs keeps this entry free of node:* imports.
|
|
7
8
|
*/
|
|
@@ -13,10 +14,13 @@ export {
|
|
|
13
14
|
canonicalFragment,
|
|
14
15
|
componentKey,
|
|
15
16
|
decodeParams,
|
|
17
|
+
eip712TypeHash,
|
|
16
18
|
encodeCall,
|
|
17
19
|
encodeParams,
|
|
20
|
+
encodeType,
|
|
18
21
|
namedSignature,
|
|
19
22
|
parseAbi,
|
|
23
|
+
parseEip712Types,
|
|
20
24
|
parseSignature,
|
|
21
25
|
readAddress,
|
|
22
26
|
selectorOf,
|
package/src/resolve.mjs
CHANGED
|
@@ -15,9 +15,10 @@ import { profileId } from "./util.mjs";
|
|
|
15
15
|
import { compileInterface, interfaceFromEnvelope } from "./compile.mjs";
|
|
16
16
|
import { analyzeProfiles } from "./analyze.mjs";
|
|
17
17
|
import { matchPack } from "./pack-match.mjs";
|
|
18
|
-
import { abiInterface, checkDeployments } from "./abi-iface.mjs";
|
|
18
|
+
import { abiInterface, checkDeployments, eip712Of } from "./abi-iface.mjs";
|
|
19
19
|
import { matchAbiPack, translatePack } from "./abi-pack.mjs";
|
|
20
20
|
import { abiInterfaceId, parseAbi } from "./abi.mjs";
|
|
21
|
+
import { jcs } from "./util.mjs";
|
|
21
22
|
import { resolvedForm } from "./resolved.mjs";
|
|
22
23
|
import { schemaDiagnostics } from "./schema-core.mjs";
|
|
23
24
|
|
|
@@ -59,12 +60,15 @@ async function resolveCompiled(opts, fromEnvelope) {
|
|
|
59
60
|
const statuses = [];
|
|
60
61
|
if (opts.abi !== undefined) {
|
|
61
62
|
const label = opts.didLabel ?? "abi";
|
|
62
|
-
|
|
63
|
+
// The messages of an EVM contract come from its profiles and packs (spec section 15.9), so the interface is built
|
|
64
|
+
// again once they are known; an ABI without functions is refused only if none brings a message.
|
|
65
|
+
const build = (eip712, allowEmpty = false) => abiInterface(opts.abi, label, "contract", eip712, { allowEmpty });
|
|
66
|
+
const compiled = build(null, true);
|
|
63
67
|
if (compiled.failure) {
|
|
64
68
|
for (const d of compiled.failure) diagnostics.push({ severity: "error", code: "E_ABI", at: d.at, message: d.message });
|
|
65
69
|
return { ok: false, resolved: null, diagnostics, profiles: statuses };
|
|
66
70
|
}
|
|
67
|
-
return resolveWith(compiled.iface, opts, diagnostics, statuses);
|
|
71
|
+
return resolveWith(compiled.iface, opts, diagnostics, statuses, build);
|
|
68
72
|
}
|
|
69
73
|
const compiled = fromEnvelope ? interfaceFromEnvelope(opts.envelope, "canister") : await compileInterface(opts.did, "canister");
|
|
70
74
|
if (compiled.failure) {
|
|
@@ -78,7 +82,7 @@ async function resolveCompiled(opts, fromEnvelope) {
|
|
|
78
82
|
/** A profile or pack for an EVM contract (spec section 15) rather than a Candid service. */
|
|
79
83
|
const isEvmDoc = (doc) => doc.kind === "contract" || (doc.kind === "pack" && doc.binds.abi !== undefined);
|
|
80
84
|
|
|
81
|
-
async function resolveWith(iface, opts, diagnostics, statuses) {
|
|
85
|
+
async function resolveWith(iface, opts, diagnostics, statuses, rebuild = null) {
|
|
82
86
|
const evm = iface.chain === "evm";
|
|
83
87
|
|
|
84
88
|
const candidates = [
|
|
@@ -106,6 +110,12 @@ async function resolveWith(iface, opts, diagnostics, statuses) {
|
|
|
106
110
|
statuses.push({ ...status, status: "invalid" });
|
|
107
111
|
continue;
|
|
108
112
|
}
|
|
113
|
+
const eip712 = evm ? eip712Of(doc) : null;
|
|
114
|
+
if (eip712?.errors.length) {
|
|
115
|
+
for (const d of eip712.errors) note("error", "E_EIP712", d.at, d.message);
|
|
116
|
+
statuses.push({ ...status, status: "invalid" });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
109
119
|
if (evm && doc.kind === "pack") {
|
|
110
120
|
const m = matchAbiPack(iface, doc);
|
|
111
121
|
if (m.error) {
|
|
@@ -121,16 +131,17 @@ async function resolveWith(iface, opts, diagnostics, statuses) {
|
|
|
121
131
|
continue;
|
|
122
132
|
}
|
|
123
133
|
note("info", "I_PACK_MATCHED", doc.name, "the contract has every function of the pack's binds.abi (same selectors, outputs and kind of call)");
|
|
124
|
-
items.push({ doc: translatePack(doc, m.functions, iface), pid, rebinding: false, apply: true, status });
|
|
134
|
+
items.push({ doc: translatePack(doc, m.functions, iface), pid, rebinding: false, apply: true, status, eip712 });
|
|
125
135
|
} else if (evm) {
|
|
126
|
-
|
|
136
|
+
// A contract profile binds the functions of its `binds.abi`; one that only describes messages binds none.
|
|
137
|
+
const own = parseAbi(doc.binds.abi ?? [], "binds.abi");
|
|
127
138
|
if (own.errors.length) {
|
|
128
139
|
for (const d of own.errors) note("error", "E_ABI", d.at, d.message);
|
|
129
140
|
statuses.push({ ...status, status: "invalid" });
|
|
130
141
|
continue;
|
|
131
142
|
}
|
|
132
143
|
checkDeployments(doc, note);
|
|
133
|
-
items.push({ doc, pid, rebinding: abiInterfaceId(own.functions) !== iface.
|
|
144
|
+
items.push({ doc, pid, rebinding: doc.binds.abi !== undefined && abiInterfaceId(own.functions) !== iface.abiId, apply: false, status, eip712 });
|
|
134
145
|
} else if (doc.kind === "pack") {
|
|
135
146
|
const m = await matchPack(iface, doc);
|
|
136
147
|
if (m.error) {
|
|
@@ -154,6 +165,33 @@ async function resolveWith(iface, opts, diagnostics, statuses) {
|
|
|
154
165
|
taken.add(pid);
|
|
155
166
|
}
|
|
156
167
|
|
|
168
|
+
if (evm) {
|
|
169
|
+
// The EIP-712 types of every profile and pack applied, merged: one name, one struct (spec section 15.9).
|
|
170
|
+
const types = {};
|
|
171
|
+
const owner = {};
|
|
172
|
+
const messages = new Set();
|
|
173
|
+
for (const item of [...items]) {
|
|
174
|
+
const clash = Object.entries(item.eip712.types).find(([n, members]) => Object.hasOwn(types, n) && jcs(types[n]) !== jcs(members));
|
|
175
|
+
if (clash) {
|
|
176
|
+
diagnostics.push({ severity: "error", code: "E_EIP712", at: `binds.eip712.types.${clash[0]}`, message: `the type ${clash[0]} differs from the one profile "${owner[clash[0]]}" declares; one name is one struct`, profile: item.doc.name });
|
|
177
|
+
statuses.push({ ...item.status, status: "invalid" });
|
|
178
|
+
items.splice(items.indexOf(item), 1);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
for (const [n, members] of Object.entries(item.eip712.types)) {
|
|
182
|
+
types[n] = members;
|
|
183
|
+
owner[n] ??= item.doc.name;
|
|
184
|
+
}
|
|
185
|
+
for (const n of item.eip712.messages) messages.add(n);
|
|
186
|
+
}
|
|
187
|
+
const built = Object.keys(types).length ? rebuild({ types, messages: [...messages].sort(), label: "binds.eip712.types" }) : iface.methods.size ? { iface } : rebuild(null);
|
|
188
|
+
if (built.failure) {
|
|
189
|
+
for (const d of built.failure) diagnostics.push({ severity: "error", code: d.at.startsWith("binds.eip712") ? "E_EIP712" : "E_ABI", at: d.at, message: d.message });
|
|
190
|
+
return { ok: false, resolved: null, diagnostics, profiles: [...items.map((i) => ({ ...i.status, status: "invalid" })), ...statuses] };
|
|
191
|
+
}
|
|
192
|
+
iface = built.iface;
|
|
193
|
+
}
|
|
194
|
+
|
|
157
195
|
const analysis = analyzeProfiles(iface, items, { attribute: true });
|
|
158
196
|
diagnostics.push(...analysis.diagnostics);
|
|
159
197
|
for (const item of analysis.ordered) {
|