@meantjs/profile 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +39 -2
- package/data/packs/dai-permit.profile.json +113 -0
- package/data/packs/erc20.profile.json +221 -0
- package/data/packs/erc2612.profile.json +196 -0
- package/data/packs/permit2.profile.json +436 -0
- package/data/profile-v0.schema.json +302 -8
- package/data/profile-v0.validate.mjs +13 -1
- package/package.json +8 -1
- package/src/abi-iface.mjs +154 -0
- package/src/abi-pack.mjs +117 -0
- package/src/abi.mjs +779 -0
- package/src/analyze.mjs +87 -31
- package/src/browser.d.mts +8 -4
- package/src/browser.mjs +8 -1
- package/src/checks.mjs +103 -10
- package/src/cli.mjs +12 -6
- package/src/evm.d.mts +101 -0
- package/src/evm.mjs +32 -0
- package/src/keccak.mjs +108 -0
- package/src/resolve.mjs +96 -5
- package/src/resolved.mjs +20 -4
- package/src/validate.mjs +24 -4
package/src/keccak.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Keccak-256 in plain JavaScript, synchronous, as Ethereum uses it: the original Keccak padding (0x01), not the NIST
|
|
2
|
+
// SHA3-256 one (0x06). For function selectors, EIP-55 checksums, ERC-8176 descriptor hashes and EIP-712 digests
|
|
3
|
+
// (spec section 15, D-054). test/evm.test.mjs checks it against the known hashes of the empty string and of the
|
|
4
|
+
// ERC-20 selectors, and against the ERC-8176 schema UID.
|
|
5
|
+
//
|
|
6
|
+
// The 1600-bit state is 25 lanes of 64 bits, each kept as two 32-bit halves: s[2i] the low half, s[2i + 1] the high.
|
|
7
|
+
|
|
8
|
+
const RC = new Uint32Array([
|
|
9
|
+
// round constants, [low, high] per round
|
|
10
|
+
0x00000001, 0x00000000, 0x00008082, 0x00000000, 0x0000808a, 0x80000000, 0x80008000, 0x80000000,
|
|
11
|
+
0x0000808b, 0x00000000, 0x80000001, 0x00000000, 0x80008081, 0x80000000, 0x00008009, 0x80000000,
|
|
12
|
+
0x0000008a, 0x00000000, 0x00000088, 0x00000000, 0x80008009, 0x00000000, 0x8000000a, 0x00000000,
|
|
13
|
+
0x8000808b, 0x00000000, 0x0000008b, 0x80000000, 0x00008089, 0x80000000, 0x00008003, 0x80000000,
|
|
14
|
+
0x00008002, 0x80000000, 0x00000080, 0x80000000, 0x0000800a, 0x00000000, 0x8000000a, 0x80000000,
|
|
15
|
+
0x80008081, 0x80000000, 0x00008080, 0x80000000, 0x80000001, 0x00000000, 0x80008008, 0x80000000,
|
|
16
|
+
]);
|
|
17
|
+
// rotation offsets of the rho step, by lane index x + 5y
|
|
18
|
+
const ROT = [0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14];
|
|
19
|
+
// the lane the pi step moves lane x + 5y to: (y, 2x + 3y)
|
|
20
|
+
const PI = ROT.map((_, i) => {
|
|
21
|
+
const x = i % 5;
|
|
22
|
+
const y = (i - x) / 5;
|
|
23
|
+
return y + 5 * ((2 * x + 3 * y) % 5);
|
|
24
|
+
});
|
|
25
|
+
const RATE = 136; // bytes: 1600 - 2 * 256 bits
|
|
26
|
+
|
|
27
|
+
function permute(s) {
|
|
28
|
+
const c = new Uint32Array(10);
|
|
29
|
+
const b = new Uint32Array(50);
|
|
30
|
+
for (let round = 0; round < 24; round++) {
|
|
31
|
+
// theta
|
|
32
|
+
for (let x = 0; x < 5; x++) {
|
|
33
|
+
c[2 * x] = s[2 * x] ^ s[2 * x + 10] ^ s[2 * x + 20] ^ s[2 * x + 30] ^ s[2 * x + 40];
|
|
34
|
+
c[2 * x + 1] = s[2 * x + 1] ^ s[2 * x + 11] ^ s[2 * x + 21] ^ s[2 * x + 31] ^ s[2 * x + 41];
|
|
35
|
+
}
|
|
36
|
+
for (let x = 0; x < 5; x++) {
|
|
37
|
+
const next = (x + 1) % 5;
|
|
38
|
+
const prev = (x + 4) % 5;
|
|
39
|
+
const lo = c[2 * prev] ^ ((c[2 * next] << 1) | (c[2 * next + 1] >>> 31));
|
|
40
|
+
const hi = c[2 * prev + 1] ^ ((c[2 * next + 1] << 1) | (c[2 * next] >>> 31));
|
|
41
|
+
for (let y = 0; y < 25; y += 5) {
|
|
42
|
+
s[2 * (x + y)] ^= lo;
|
|
43
|
+
s[2 * (x + y) + 1] ^= hi;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// rho and pi
|
|
47
|
+
for (let i = 0; i < 25; i++) {
|
|
48
|
+
const lo = s[2 * i];
|
|
49
|
+
const hi = s[2 * i + 1];
|
|
50
|
+
const n = ROT[i];
|
|
51
|
+
const j = PI[i];
|
|
52
|
+
if (n === 0) {
|
|
53
|
+
b[2 * j] = lo;
|
|
54
|
+
b[2 * j + 1] = hi;
|
|
55
|
+
} else if (n < 32) {
|
|
56
|
+
b[2 * j] = (lo << n) | (hi >>> (32 - n));
|
|
57
|
+
b[2 * j + 1] = (hi << n) | (lo >>> (32 - n));
|
|
58
|
+
} else if (n === 32) {
|
|
59
|
+
b[2 * j] = hi;
|
|
60
|
+
b[2 * j + 1] = lo;
|
|
61
|
+
} else {
|
|
62
|
+
const m = n - 32;
|
|
63
|
+
b[2 * j] = (hi << m) | (lo >>> (32 - m));
|
|
64
|
+
b[2 * j + 1] = (lo << m) | (hi >>> (32 - m));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// chi
|
|
68
|
+
for (let y = 0; y < 25; y += 5) {
|
|
69
|
+
for (let x = 0; x < 5; x++) {
|
|
70
|
+
const i = x + y;
|
|
71
|
+
const i1 = ((x + 1) % 5) + y;
|
|
72
|
+
const i2 = ((x + 2) % 5) + y;
|
|
73
|
+
s[2 * i] = b[2 * i] ^ (~b[2 * i1] & b[2 * i2]);
|
|
74
|
+
s[2 * i + 1] = b[2 * i + 1] ^ (~b[2 * i1 + 1] & b[2 * i2 + 1]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// iota
|
|
78
|
+
s[0] ^= RC[2 * round];
|
|
79
|
+
s[1] ^= RC[2 * round + 1];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Keccak-256 of a byte array (or a string, as its UTF-8 bytes), as 32 bytes. */
|
|
84
|
+
export function keccak256(input) {
|
|
85
|
+
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
86
|
+
const s = new Uint32Array(50);
|
|
87
|
+
const blocks = Math.floor(bytes.length / RATE) + 1;
|
|
88
|
+
const padded = new Uint8Array(blocks * RATE);
|
|
89
|
+
padded.set(bytes);
|
|
90
|
+
padded[bytes.length] ^= 0x01;
|
|
91
|
+
padded[padded.length - 1] ^= 0x80;
|
|
92
|
+
for (let off = 0; off < padded.length; off += RATE) {
|
|
93
|
+
for (let k = 0; k < RATE; k += 4) {
|
|
94
|
+
s[k >> 2] ^= padded[off + k] | (padded[off + k + 1] << 8) | (padded[off + k + 2] << 16) | (padded[off + k + 3] << 24);
|
|
95
|
+
}
|
|
96
|
+
permute(s);
|
|
97
|
+
}
|
|
98
|
+
const out = new Uint8Array(32);
|
|
99
|
+
for (let k = 0; k < 32; k++) out[k] = (s[k >> 2] >>> (8 * (k & 3))) & 0xff;
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Keccak-256 as 64 lowercase hex digits, without `0x`. */
|
|
104
|
+
export function keccak256Hex(input) {
|
|
105
|
+
let hex = "";
|
|
106
|
+
for (const b of keccak256(input)) hex += b.toString(16).padStart(2, "0");
|
|
107
|
+
return hex;
|
|
108
|
+
}
|
package/src/resolve.mjs
CHANGED
|
@@ -15,6 +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, eip712Of } from "./abi-iface.mjs";
|
|
19
|
+
import { matchAbiPack, translatePack } from "./abi-pack.mjs";
|
|
20
|
+
import { abiInterfaceId, parseAbi } from "./abi.mjs";
|
|
21
|
+
import { jcs } from "./util.mjs";
|
|
18
22
|
import { resolvedForm } from "./resolved.mjs";
|
|
19
23
|
import { schemaDiagnostics } from "./schema-core.mjs";
|
|
20
24
|
|
|
@@ -25,8 +29,9 @@ const describeReasons = (reasons) => {
|
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* @param opts.did Candid source of the service, compiled here; or
|
|
28
|
-
* @param opts.envelope the service's candid-core ContractEnvelope, already compiled
|
|
29
|
-
* @param opts.
|
|
32
|
+
* @param opts.envelope the service's candid-core ContractEnvelope, already compiled; or
|
|
33
|
+
* @param opts.abi an EVM contract's ABI: fragments or JSON ABI entries (spec section 15; exactly one of the three)
|
|
34
|
+
* @param opts.didLabel how E_CANDID (or E_ENVELOPE, E_ABI) names the input
|
|
30
35
|
* @param opts.profiles [{ doc, label? }] explicit profiles, applied or rejected
|
|
31
36
|
* @param opts.packs [{ doc, label? }] candidate packs, applied only if they match
|
|
32
37
|
* @param opts.validateSchema from createSchemaValidator()
|
|
@@ -37,7 +42,8 @@ const describeReasons = (reasons) => {
|
|
|
37
42
|
*/
|
|
38
43
|
export async function resolveService(opts) {
|
|
39
44
|
const fromEnvelope = opts.envelope !== undefined;
|
|
40
|
-
if (
|
|
45
|
+
if ([opts.did, opts.envelope, opts.abi].filter((x) => x !== undefined).length !== 1) throw new TypeError("resolveService: give exactly one of `did`, `envelope` and `abi`");
|
|
46
|
+
if (opts.abi !== undefined) return resolveCompiled(opts, false);
|
|
41
47
|
if (!fromEnvelope) return resolveCompiled(opts, false);
|
|
42
48
|
// interfaceFromEnvelope checks the graph before anything walks it; anything it still misses in someone else's
|
|
43
49
|
// document is reported against the envelope rather than thrown.
|
|
@@ -52,13 +58,32 @@ export async function resolveService(opts) {
|
|
|
52
58
|
async function resolveCompiled(opts, fromEnvelope) {
|
|
53
59
|
const diagnostics = [];
|
|
54
60
|
const statuses = [];
|
|
61
|
+
if (opts.abi !== undefined) {
|
|
62
|
+
const label = opts.didLabel ?? "abi";
|
|
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);
|
|
67
|
+
if (compiled.failure) {
|
|
68
|
+
for (const d of compiled.failure) diagnostics.push({ severity: "error", code: "E_ABI", at: d.at, message: d.message });
|
|
69
|
+
return { ok: false, resolved: null, diagnostics, profiles: statuses };
|
|
70
|
+
}
|
|
71
|
+
return resolveWith(compiled.iface, opts, diagnostics, statuses, build);
|
|
72
|
+
}
|
|
55
73
|
const compiled = fromEnvelope ? interfaceFromEnvelope(opts.envelope, "canister") : await compileInterface(opts.did, "canister");
|
|
56
74
|
if (compiled.failure) {
|
|
57
75
|
const [code, at] = fromEnvelope ? ["E_ENVELOPE", opts.didLabel ?? "envelope"] : ["E_CANDID", opts.didLabel ?? "service.did"];
|
|
58
76
|
for (const d of compiled.failure) diagnostics.push({ severity: "error", code, at, message: `${d.code}: ${d.message}` });
|
|
59
77
|
return { ok: false, resolved: null, diagnostics, profiles: statuses };
|
|
60
78
|
}
|
|
61
|
-
|
|
79
|
+
return resolveWith(compiled.iface, opts, diagnostics, statuses);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A profile or pack for an EVM contract (spec section 15) rather than a Candid service. */
|
|
83
|
+
const isEvmDoc = (doc) => doc.kind === "contract" || (doc.kind === "pack" && doc.binds.abi !== undefined);
|
|
84
|
+
|
|
85
|
+
async function resolveWith(iface, opts, diagnostics, statuses, rebuild = null) {
|
|
86
|
+
const evm = iface.chain === "evm";
|
|
62
87
|
|
|
63
88
|
const candidates = [
|
|
64
89
|
...(opts.profiles ?? []).map((p, i) => ({ ...p, explicit: true, label: p.label ?? `profile #${i + 1}` })),
|
|
@@ -78,7 +103,46 @@ async function resolveCompiled(opts, fromEnvelope) {
|
|
|
78
103
|
const pid = profileId(doc);
|
|
79
104
|
if (taken.has(pid)) continue; // the same document given twice (for example explicitly and through the pack search)
|
|
80
105
|
const status = { name: doc.name, origin: doc.origin, kind: doc.kind, profile_id: pid, status: "applied", explicit: c.explicit };
|
|
81
|
-
if (doc
|
|
106
|
+
if (isEvmDoc(doc) !== evm) {
|
|
107
|
+
if (!c.explicit) continue; // a pack for the other kind of interface can never apply
|
|
108
|
+
const what = doc.kind === "pack" ? "this pack" : `a ${doc.kind} profile`;
|
|
109
|
+
note("error", "E_CHAIN", "kind", `${what} describes ${isEvmDoc(doc) ? "an EVM contract" : "a Candid service"}, but the interface is ${evm ? "an EVM contract's ABI" : "a Candid service"}`);
|
|
110
|
+
statuses.push({ ...status, status: "invalid" });
|
|
111
|
+
continue;
|
|
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
|
+
}
|
|
119
|
+
if (evm && doc.kind === "pack") {
|
|
120
|
+
const m = matchAbiPack(iface, doc);
|
|
121
|
+
if (m.error) {
|
|
122
|
+
for (const d of m.error) note("error", "E_ABI", d.at, d.message);
|
|
123
|
+
statuses.push({ ...status, status: "invalid" });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!m.matched) {
|
|
127
|
+
const why = describeReasons(m.reasons);
|
|
128
|
+
if (c.explicit) note("error", "E_PACK_NOT_MATCHED", doc.name, `the contract lacks functions of the pack's binds.abi: ${why}`);
|
|
129
|
+
else note("info", "I_PACK_NOT_MATCHED", doc.name, `pack does not apply: ${why}`);
|
|
130
|
+
statuses.push({ ...status, status: "not_matched" });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
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)");
|
|
134
|
+
items.push({ doc: translatePack(doc, m.functions, iface), pid, rebinding: false, apply: true, status, eip712 });
|
|
135
|
+
} else if (evm) {
|
|
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");
|
|
138
|
+
if (own.errors.length) {
|
|
139
|
+
for (const d of own.errors) note("error", "E_ABI", d.at, d.message);
|
|
140
|
+
statuses.push({ ...status, status: "invalid" });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
checkDeployments(doc, note);
|
|
144
|
+
items.push({ doc, pid, rebinding: doc.binds.abi !== undefined && abiInterfaceId(own.functions) !== iface.abiId, apply: false, status, eip712 });
|
|
145
|
+
} else if (doc.kind === "pack") {
|
|
82
146
|
const m = await matchPack(iface, doc);
|
|
83
147
|
if (m.error) {
|
|
84
148
|
for (const d of m.error) note("error", "E_CANDID", "binds.requires", `${d.code}: ${d.message}`);
|
|
@@ -101,6 +165,33 @@ async function resolveCompiled(opts, fromEnvelope) {
|
|
|
101
165
|
taken.add(pid);
|
|
102
166
|
}
|
|
103
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
|
+
|
|
104
195
|
const analysis = analyzeProfiles(iface, items, { attribute: true });
|
|
105
196
|
diagnostics.push(...analysis.diagnostics);
|
|
106
197
|
for (const item of analysis.ordered) {
|
package/src/resolved.mjs
CHANGED
|
@@ -10,7 +10,8 @@ export function buildTree(iface, methodName, side, tags) {
|
|
|
10
10
|
for (const pos of positions(iface, methodName, side)) {
|
|
11
11
|
const { ref: r, optional } = iface.unwrapOpt(pos.ref);
|
|
12
12
|
const n = iface.node(r);
|
|
13
|
-
const node = { candid: n.kind === "primitive" ? n.primitive : iface.isBlob(r) ? "blob" : n.kind };
|
|
13
|
+
const node = iface.chain === "evm" ? { abi: iface.abiType(r) } : { candid: n.kind === "primitive" ? n.primitive : iface.isBlob(r) ? "blob" : n.kind };
|
|
14
|
+
if (n.kind === "vec" && typeof n.length === "number") node.length = n.length; // an EVM fixed-size array, T[k]
|
|
14
15
|
if (optional) node.optional = true;
|
|
15
16
|
const t = tags.at(pos, iface);
|
|
16
17
|
if (t) node.tags = t;
|
|
@@ -40,21 +41,36 @@ export function buildTree(iface, methodName, side, tags) {
|
|
|
40
41
|
*/
|
|
41
42
|
export function resolvedForm(iface, profiles, tags, diagnostics) {
|
|
42
43
|
const methods = {};
|
|
44
|
+
const messages = {};
|
|
43
45
|
for (const [name, m] of [...iface.methods.entries()].sort(([a], [b]) => (a < b ? -1 : 1))) {
|
|
44
46
|
const mt = { ...tags.methodTags(name) };
|
|
45
47
|
if (!mt.effect) {
|
|
46
|
-
|
|
48
|
+
// A signed message authorizes what someone else submits later (spec section 15.9): as conservative as an update.
|
|
49
|
+
mt.effect = m.mode === "update" || m.mode === "oneway" || m.mode === "message"
|
|
47
50
|
? { args: { kind: "write", irreversible: true }, origin: "default" }
|
|
48
51
|
: { args: { kind: "read", irreversible: false }, origin: "default" };
|
|
49
52
|
}
|
|
50
|
-
|
|
53
|
+
if (m.mode === "message") {
|
|
54
|
+
const { encodeType, typeHash } = iface.messages.get(name); // an EIP-712 message: what a consumer matches a request by
|
|
55
|
+
messages[name] = { encode_type: encodeType, type_hash: typeHash, tags: mt, args: buildTree(iface, name, "args", tags) };
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const f = iface.functions?.get(name); // an EVM function: what a consumer needs to match and decode calldata
|
|
59
|
+
const abi = f ? { selector: f.selector, signature: f.signature, state_mutability: f.stateMutability } : {};
|
|
60
|
+
methods[name] = { mode: m.mode, ...abi, tags: mt, args: buildTree(iface, name, "args", tags), results: buildTree(iface, name, "results", tags) };
|
|
51
61
|
}
|
|
62
|
+
const bound = (doc) => ({
|
|
63
|
+
...(doc.binds?.deployments ? { deployments: doc.binds.deployments } : {}),
|
|
64
|
+
...(doc.binds?.eip712?.domain ? { eip712_domain: doc.binds.eip712.domain } : {}),
|
|
65
|
+
});
|
|
52
66
|
return {
|
|
53
67
|
format: "candid-profile-resolved",
|
|
54
68
|
format_version: 0,
|
|
69
|
+
...(iface.chain === "evm" ? { chain: "evm" } : {}),
|
|
55
70
|
interface_id: iface.contract.identities.interface ?? null,
|
|
56
|
-
profiles: profiles.map(({ doc, pid }) => ({ name: doc.name, origin: doc.origin, profile_id: pid })),
|
|
71
|
+
profiles: profiles.map(({ doc, pid }) => ({ name: doc.name, origin: doc.origin, profile_id: pid, ...bound(doc) })),
|
|
57
72
|
methods,
|
|
73
|
+
...(Object.keys(messages).length ? { messages } : {}),
|
|
58
74
|
diagnostics,
|
|
59
75
|
};
|
|
60
76
|
}
|
package/src/validate.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { profileId } from "./util.mjs";
|
|
6
6
|
import { compileInterface } from "./compile.mjs";
|
|
7
|
+
import { abiInterface, checkDeployments, eip712Of } from "./abi-iface.mjs";
|
|
7
8
|
import { analyzeProfiles } from "./analyze.mjs";
|
|
8
9
|
import { resolvedForm } from "./resolved.mjs";
|
|
9
10
|
import { schemaDiagnostics } from "./schema-core.mjs";
|
|
@@ -26,6 +27,21 @@ export async function validateProfile(doc, opts) {
|
|
|
26
27
|
}
|
|
27
28
|
const pid = profileId(doc);
|
|
28
29
|
|
|
30
|
+
// An EVM contract profile or pack carries its own ABI and EIP-712 types (spec sections 15 and 15.9): it is
|
|
31
|
+
// checked against them.
|
|
32
|
+
if (doc.kind === "contract" || (doc.kind === "pack" && doc.binds.abi !== undefined)) {
|
|
33
|
+
const eip712 = eip712Of(doc);
|
|
34
|
+
for (const d of eip712.errors) diag("error", "E_EIP712", d.at, d.message);
|
|
35
|
+
if (eip712.errors.length) return { ok: false, profile_id: pid, diagnostics };
|
|
36
|
+
const compiled = abiInterface(doc.binds.abi ?? [], "binds.abi", "contract", doc.binds.eip712 ? { ...eip712, label: "binds.eip712.types" } : null);
|
|
37
|
+
if (compiled.failure) {
|
|
38
|
+
for (const d of compiled.failure) diag("error", d.at.startsWith("binds.eip712") ? "E_EIP712" : "E_ABI", d.at, d.message);
|
|
39
|
+
return { ok: false, profile_id: pid, diagnostics };
|
|
40
|
+
}
|
|
41
|
+
if (doc.kind === "contract") checkDeployments(doc, diag);
|
|
42
|
+
return finish(doc, pid, compiled.iface, false, diagnostics, opts);
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
let didSource;
|
|
30
46
|
if (doc.kind === "pack") didSource = doc.binds.requires;
|
|
31
47
|
else if (opts.did !== undefined && opts.did !== null) didSource = opts.did;
|
|
@@ -40,10 +56,14 @@ export async function validateProfile(doc, opts) {
|
|
|
40
56
|
}
|
|
41
57
|
const { iface } = compiled;
|
|
42
58
|
const rebinding = doc.kind === "canister" && doc.binds.interface_id !== iface.interfaceId;
|
|
43
|
-
|
|
59
|
+
return finish(doc, pid, iface, rebinding, diagnostics, opts);
|
|
60
|
+
}
|
|
44
61
|
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
62
|
+
function finish(doc, pid, iface, rebinding, earlier, opts) {
|
|
63
|
+
const analysis = analyzeProfiles(iface, [{ doc, pid, rebinding, apply: false }], { attribute: false });
|
|
64
|
+
const diagnostics = [...earlier, ...analysis.diagnostics];
|
|
65
|
+
const ok = !diagnostics.some((d) => d.severity === "error");
|
|
66
|
+
const result = { ok, profile_id: pid, interface_id: iface.interfaceId, methods: iface.methods.size, annotations: doc.annotations.length, diagnostics };
|
|
67
|
+
if (opts.resolved) result.resolved = resolvedForm(iface, [{ doc, pid }], analysis.layered, diagnostics);
|
|
48
68
|
return result;
|
|
49
69
|
}
|