@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/src/resolve.mjs CHANGED
@@ -15,6 +15,9 @@ 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";
19
+ import { matchAbiPack, translatePack } from "./abi-pack.mjs";
20
+ import { abiInterfaceId, parseAbi } from "./abi.mjs";
18
21
  import { resolvedForm } from "./resolved.mjs";
19
22
  import { schemaDiagnostics } from "./schema-core.mjs";
20
23
 
@@ -25,8 +28,9 @@ const describeReasons = (reasons) => {
25
28
 
26
29
  /**
27
30
  * @param opts.did Candid source of the service, compiled here; or
28
- * @param opts.envelope the service's candid-core ContractEnvelope, already compiled (exactly one of the two)
29
- * @param opts.didLabel how E_CANDID (or E_ENVELOPE) names the input
31
+ * @param opts.envelope the service's candid-core ContractEnvelope, already compiled; or
32
+ * @param opts.abi an EVM contract's ABI: fragments or JSON ABI entries (spec section 15; exactly one of the three)
33
+ * @param opts.didLabel how E_CANDID (or E_ENVELOPE, E_ABI) names the input
30
34
  * @param opts.profiles [{ doc, label? }] explicit profiles, applied or rejected
31
35
  * @param opts.packs [{ doc, label? }] candidate packs, applied only if they match
32
36
  * @param opts.validateSchema from createSchemaValidator()
@@ -37,7 +41,8 @@ const describeReasons = (reasons) => {
37
41
  */
38
42
  export async function resolveService(opts) {
39
43
  const fromEnvelope = opts.envelope !== undefined;
40
- if (fromEnvelope === (opts.did !== undefined)) throw new TypeError("resolveService: give exactly one of `did` and `envelope`");
44
+ 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`");
45
+ if (opts.abi !== undefined) return resolveCompiled(opts, false);
41
46
  if (!fromEnvelope) return resolveCompiled(opts, false);
42
47
  // interfaceFromEnvelope checks the graph before anything walks it; anything it still misses in someone else's
43
48
  // document is reported against the envelope rather than thrown.
@@ -52,13 +57,29 @@ export async function resolveService(opts) {
52
57
  async function resolveCompiled(opts, fromEnvelope) {
53
58
  const diagnostics = [];
54
59
  const statuses = [];
60
+ if (opts.abi !== undefined) {
61
+ const label = opts.didLabel ?? "abi";
62
+ const compiled = abiInterface(opts.abi, label);
63
+ if (compiled.failure) {
64
+ for (const d of compiled.failure) diagnostics.push({ severity: "error", code: "E_ABI", at: d.at, message: d.message });
65
+ return { ok: false, resolved: null, diagnostics, profiles: statuses };
66
+ }
67
+ return resolveWith(compiled.iface, opts, diagnostics, statuses);
68
+ }
55
69
  const compiled = fromEnvelope ? interfaceFromEnvelope(opts.envelope, "canister") : await compileInterface(opts.did, "canister");
56
70
  if (compiled.failure) {
57
71
  const [code, at] = fromEnvelope ? ["E_ENVELOPE", opts.didLabel ?? "envelope"] : ["E_CANDID", opts.didLabel ?? "service.did"];
58
72
  for (const d of compiled.failure) diagnostics.push({ severity: "error", code, at, message: `${d.code}: ${d.message}` });
59
73
  return { ok: false, resolved: null, diagnostics, profiles: statuses };
60
74
  }
61
- const { iface } = compiled;
75
+ return resolveWith(compiled.iface, opts, diagnostics, statuses);
76
+ }
77
+
78
+ /** A profile or pack for an EVM contract (spec section 15) rather than a Candid service. */
79
+ const isEvmDoc = (doc) => doc.kind === "contract" || (doc.kind === "pack" && doc.binds.abi !== undefined);
80
+
81
+ async function resolveWith(iface, opts, diagnostics, statuses) {
82
+ const evm = iface.chain === "evm";
62
83
 
63
84
  const candidates = [
64
85
  ...(opts.profiles ?? []).map((p, i) => ({ ...p, explicit: true, label: p.label ?? `profile #${i + 1}` })),
@@ -78,7 +99,39 @@ async function resolveCompiled(opts, fromEnvelope) {
78
99
  const pid = profileId(doc);
79
100
  if (taken.has(pid)) continue; // the same document given twice (for example explicitly and through the pack search)
80
101
  const status = { name: doc.name, origin: doc.origin, kind: doc.kind, profile_id: pid, status: "applied", explicit: c.explicit };
81
- if (doc.kind === "pack") {
102
+ if (isEvmDoc(doc) !== evm) {
103
+ if (!c.explicit) continue; // a pack for the other kind of interface can never apply
104
+ const what = doc.kind === "pack" ? "this pack" : `a ${doc.kind} profile`;
105
+ 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"}`);
106
+ statuses.push({ ...status, status: "invalid" });
107
+ continue;
108
+ }
109
+ if (evm && doc.kind === "pack") {
110
+ const m = matchAbiPack(iface, doc);
111
+ if (m.error) {
112
+ for (const d of m.error) note("error", "E_ABI", d.at, d.message);
113
+ statuses.push({ ...status, status: "invalid" });
114
+ continue;
115
+ }
116
+ if (!m.matched) {
117
+ const why = describeReasons(m.reasons);
118
+ if (c.explicit) note("error", "E_PACK_NOT_MATCHED", doc.name, `the contract lacks functions of the pack's binds.abi: ${why}`);
119
+ else note("info", "I_PACK_NOT_MATCHED", doc.name, `pack does not apply: ${why}`);
120
+ statuses.push({ ...status, status: "not_matched" });
121
+ continue;
122
+ }
123
+ 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 });
125
+ } else if (evm) {
126
+ const own = parseAbi(doc.binds.abi, "binds.abi");
127
+ if (own.errors.length) {
128
+ for (const d of own.errors) note("error", "E_ABI", d.at, d.message);
129
+ statuses.push({ ...status, status: "invalid" });
130
+ continue;
131
+ }
132
+ checkDeployments(doc, note);
133
+ items.push({ doc, pid, rebinding: abiInterfaceId(own.functions) !== iface.interfaceId, apply: false, status });
134
+ } else if (doc.kind === "pack") {
82
135
  const m = await matchPack(iface, doc);
83
136
  if (m.error) {
84
137
  for (const d of m.error) note("error", "E_CANDID", "binds.requires", `${d.code}: ${d.message}`);
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;
@@ -47,13 +48,16 @@ export function resolvedForm(iface, profiles, tags, diagnostics) {
47
48
  ? { args: { kind: "write", irreversible: true }, origin: "default" }
48
49
  : { args: { kind: "read", irreversible: false }, origin: "default" };
49
50
  }
50
- methods[name] = { mode: m.mode, tags: mt, args: buildTree(iface, name, "args", tags), results: buildTree(iface, name, "results", tags) };
51
+ const f = iface.functions?.get(name); // an EVM function: what a consumer needs to match and decode calldata
52
+ const abi = f ? { selector: f.selector, signature: f.signature, state_mutability: f.stateMutability } : {};
53
+ methods[name] = { mode: m.mode, ...abi, tags: mt, args: buildTree(iface, name, "args", tags), results: buildTree(iface, name, "results", tags) };
51
54
  }
52
55
  return {
53
56
  format: "candid-profile-resolved",
54
57
  format_version: 0,
58
+ ...(iface.chain === "evm" ? { chain: "evm" } : {}),
55
59
  interface_id: iface.contract.identities.interface ?? null,
56
- profiles: profiles.map(({ doc, pid }) => ({ name: doc.name, origin: doc.origin, profile_id: pid })),
60
+ profiles: profiles.map(({ doc, pid }) => ({ name: doc.name, origin: doc.origin, profile_id: pid, ...(doc.binds?.deployments ? { deployments: doc.binds.deployments } : {}) })),
57
61
  methods,
58
62
  diagnostics,
59
63
  };
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 } 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,17 @@ 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 (spec section 15): it is checked against that.
31
+ if (doc.kind === "contract" || (doc.kind === "pack" && doc.binds.abi !== undefined)) {
32
+ const compiled = abiInterface(doc.binds.abi, "binds.abi");
33
+ if (compiled.failure) {
34
+ for (const d of compiled.failure) diag("error", "E_ABI", d.at, d.message);
35
+ return { ok: false, profile_id: pid, diagnostics };
36
+ }
37
+ if (doc.kind === "contract") checkDeployments(doc, diag);
38
+ return finish(doc, pid, compiled.iface, false, diagnostics, opts);
39
+ }
40
+
29
41
  let didSource;
30
42
  if (doc.kind === "pack") didSource = doc.binds.requires;
31
43
  else if (opts.did !== undefined && opts.did !== null) didSource = opts.did;
@@ -40,10 +52,14 @@ export async function validateProfile(doc, opts) {
40
52
  }
41
53
  const { iface } = compiled;
42
54
  const rebinding = doc.kind === "canister" && doc.binds.interface_id !== iface.interfaceId;
43
- const analysis = analyzeProfiles(iface, [{ doc, pid, rebinding, apply: false }], { attribute: false });
55
+ return finish(doc, pid, iface, rebinding, diagnostics, opts);
56
+ }
44
57
 
45
- const ok = !analysis.diagnostics.some((d) => d.severity === "error");
46
- const result = { ok, profile_id: pid, interface_id: iface.interfaceId, methods: iface.methods.size, annotations: doc.annotations.length, diagnostics: analysis.diagnostics };
47
- if (opts.resolved) result.resolved = resolvedForm(iface, [{ doc, pid }], analysis.layered, analysis.diagnostics);
58
+ function finish(doc, pid, iface, rebinding, earlier, opts) {
59
+ const analysis = analyzeProfiles(iface, [{ doc, pid, rebinding, apply: false }], { attribute: false });
60
+ const diagnostics = [...earlier, ...analysis.diagnostics];
61
+ const ok = !diagnostics.some((d) => d.severity === "error");
62
+ const result = { ok, profile_id: pid, interface_id: iface.interfaceId, methods: iface.methods.size, annotations: doc.annotations.length, diagnostics };
63
+ if (opts.resolved) result.resolved = resolvedForm(iface, [{ doc, pid }], analysis.layered, diagnostics);
48
64
  return result;
49
65
  }