@meantjs/profile 0.3.0 → 0.5.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 +29 -0
- package/README.md +8 -4
- package/data/packs/dai-permit.profile.json +5 -1
- package/data/packs/erc20.profile.json +10 -2
- package/data/packs/erc2612.profile.json +10 -2
- package/data/packs/icrc1.profile.json +5 -1
- package/data/packs/icrc2.profile.json +8 -1
- package/data/packs/permit2.profile.json +26 -3
- package/data/packs/usdc-base-sepolia.profile.json +301 -0
- package/data/packs/usdc.profile.json +309 -0
- package/data/packs/x402-exact-permit2.profile.json +187 -0
- package/data/profile-v0.schema.json +88 -2
- package/data/profile-v0.validate.mjs +3 -3
- package/package.json +1 -1
- package/src/analyze.mjs +82 -16
- package/src/browser.mjs +2 -1
- package/src/checks.mjs +72 -1
- package/src/resolve.mjs +3 -1
- package/src/served.mjs +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meantjs/profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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/analyze.mjs
CHANGED
|
@@ -9,7 +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,
|
|
12
|
+
tokenPathError, grantError, pinError,
|
|
13
13
|
} from "./checks.mjs";
|
|
14
14
|
|
|
15
15
|
/** Spec section 8, highest precedence first. */
|
|
@@ -29,7 +29,9 @@ function buildLayers(entries, keep = () => true) {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* @param iface the interface every address resolves against
|
|
32
|
-
* @param items [{ doc, pid, rebinding, apply }]; `apply` marks a pack that matched a service and is applied to it
|
|
32
|
+
* @param items [{ doc, pid, rebinding, bound, apply }]; `apply` marks a pack that matched a service and is applied to it;
|
|
33
|
+
* `bound` is { at, id, given }, what a profile binds to against what was given, when it is not `binds.interface_id`
|
|
34
|
+
* against the interface's id (a contract profile binds by its own ABI, spec section 15.3)
|
|
33
35
|
* @param opts.attribute add `profile` to each diagnostic (multi-profile output)
|
|
34
36
|
* @returns { diagnostics, layered, ordered, stale } `ordered` is items by precedence; `stale` the stale items
|
|
35
37
|
*/
|
|
@@ -44,11 +46,24 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
44
46
|
item.diags.push(d);
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
|
-
const
|
|
49
|
+
const boundOf = (item) => item.bound ?? { at: "binds.interface_id", id: item.doc.binds.interface_id, given: iface.interfaceId };
|
|
50
|
+
|
|
51
|
+
// A pin (`fixed`, spec 15.4, D-058) holds the value the profile's words were written for. A profile with a pin that
|
|
52
|
+
// does not hold keeps only its effects, without grants, and its pins that hold: never its words without the pin. A
|
|
53
|
+
// pin's problem is always an error, never a pack annotation skipped.
|
|
54
|
+
const unpinned = new Set();
|
|
55
|
+
const pinFailed = (item, code, at, message) => {
|
|
56
|
+
unpinned.add(item);
|
|
57
|
+
item.diag("error", code, at, `${message}; this profile keeps only its effects (without grants) and its pins that hold, since its words were written for the pinned value (spec section 15.4)`);
|
|
58
|
+
};
|
|
59
|
+
const pins = new Map(); // position key -> { value, item, at }: every profile pins a position to one value (spec 15.4)
|
|
60
|
+
const badPins = new Set(); // pin entries that do not hold, left out of the resolved form
|
|
48
61
|
|
|
49
62
|
for (const item of ordered) {
|
|
50
63
|
if (item.rebinding) {
|
|
51
|
-
|
|
64
|
+
const { at, id, given } = boundOf(item);
|
|
65
|
+
const what = at === "binds.abi" ? "ABI" : "interface";
|
|
66
|
+
item.diag("warning", "W_REBOUND", at, `profile is bound to ${id} but the given ${what} is ${given}; checking whether every address still resolves`);
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
69
|
|
|
@@ -64,16 +79,28 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
64
79
|
let a = original;
|
|
65
80
|
const at = a.at;
|
|
66
81
|
const addr = parseAddress(at);
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
const pinned = a.tag === "fixed";
|
|
83
|
+
const fail = pinned ? (code, message) => pinFailed(item, code, at, message) : (code, message) => diag("error", code, at, message);
|
|
84
|
+
if (!addr) { fail("E_ADDR_SYNTAX", "address does not parse"); continue; }
|
|
85
|
+
if (doc.kind === "pack" && addr.kind === "type") { fail("E_PACK_TYPE_REF", "packs must use method addresses (declaration names are not standardized)"); continue; }
|
|
69
86
|
const res = iface.resolve(addr);
|
|
70
87
|
if (res.error) {
|
|
71
88
|
// A pack applied to a service that is a narrower subtype (fewer result cases, fewer optional fields) cannot
|
|
72
89
|
// address what is not there. That is expected, not an error in the pack.
|
|
73
|
-
if (item.apply) diag("info", "I_PACK_ANNOTATION_SKIPPED", at, `${a.tag}: ${res.error}`);
|
|
74
|
-
else
|
|
90
|
+
if (item.apply && !pinned) diag("info", "I_PACK_ANNOTATION_SKIPPED", at, `${a.tag}: ${res.error}`);
|
|
91
|
+
else fail(res.code ?? "E_ADDR_UNRESOLVED", res.error);
|
|
75
92
|
continue;
|
|
76
93
|
}
|
|
94
|
+
if (pinned) {
|
|
95
|
+
const prev = pins.get(res.key);
|
|
96
|
+
const value = String(a.args.value).toLowerCase();
|
|
97
|
+
if (prev && prev.value !== value) {
|
|
98
|
+
const where = prev.item === item ? `"${prev.at}"` : `profile "${prev.item.doc.name}" "${prev.at}"`;
|
|
99
|
+
pinFailed(item, "E_CONFLICT", at, `fixed pins this position to ${a.args.value}, where ${where} pins it to another value`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!prev) pins.set(res.key, { value, item, at });
|
|
103
|
+
}
|
|
77
104
|
if (addr.kind === "type") {
|
|
78
105
|
const others = (iface.aliases.get(iface.decls.get(addr.decl)) ?? []).filter((n) => n !== addr.decl);
|
|
79
106
|
if (others.length) diag("warning", "W_STRUCTURAL_ALIAS", at, `declaration "${addr.decl}" is structurally identical to ${others.map((n) => `"${n}"`).join(", ")}; the tag reaches every one of them`);
|
|
@@ -147,6 +174,18 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
147
174
|
}
|
|
148
175
|
};
|
|
149
176
|
|
|
177
|
+
/**
|
|
178
|
+
* A grant that does not hold (spec 5.3, D-056). A pack applied to a service loses only its grant, never the effect:
|
|
179
|
+
* dropping the effect would leave a transfer or approve with the default effect, which needs no confirmation.
|
|
180
|
+
*/
|
|
181
|
+
const grants = []; // effects whose grant holds, checked again once dropped tags are known
|
|
182
|
+
const badGrant = (e, diag, why) => {
|
|
183
|
+
if (!e.item.apply) return diag("error", "E_GRANT", e.ann.at, why);
|
|
184
|
+
const { grant, ...args } = e.entry.args;
|
|
185
|
+
e.entry.args = args;
|
|
186
|
+
e.item.diag("info", "I_PACK_ANNOTATION_SKIPPED", e.ann.at, `effect grant dropped, the effect kept: E_GRANT: ${why}`);
|
|
187
|
+
};
|
|
188
|
+
|
|
150
189
|
const sets = new Map(); // names set -> { at, method, fields: { name, value, show?, kind? } } (spec 5.3, D-045)
|
|
151
190
|
const named = []; // named positions, checked once every set is known
|
|
152
191
|
for (const e of entries) {
|
|
@@ -167,8 +206,10 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
167
206
|
const why = caseTargetError(iface, addr, res, a.tag);
|
|
168
207
|
if (why) { diag("error", "E_TAG_TARGET", at, why); continue; }
|
|
169
208
|
const m = iface.methods.get(addr.method);
|
|
170
|
-
if (a.tag === "effect")
|
|
171
|
-
|
|
209
|
+
if (a.tag === "effect") {
|
|
210
|
+
checkEffectMode(m, a.args, diag, at);
|
|
211
|
+
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)");
|
|
212
|
+
} else checkTemplate(item, addr.method, a.args.template, diag, at, addr);
|
|
172
213
|
caseEntries.push({ e, diag });
|
|
173
214
|
continue;
|
|
174
215
|
}
|
|
@@ -176,6 +217,11 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
176
217
|
const m = res.method;
|
|
177
218
|
const methodName = addr.method;
|
|
178
219
|
if (a.tag === "effect") checkEffectMode(m, a.args, diag, at);
|
|
220
|
+
if (a.tag === "effect" && a.args.grant) {
|
|
221
|
+
const why = grantError(iface, tags, methodName, a.args);
|
|
222
|
+
if (why) badGrant(e, diag, why);
|
|
223
|
+
else grants.push({ e, diag });
|
|
224
|
+
}
|
|
179
225
|
if (a.tag === "idempotency") {
|
|
180
226
|
if (m.mode !== "update") diag("error", "E_IDEMPOTENCY_MODE", at, "idempotency applies to update methods");
|
|
181
227
|
const r = iface.resolveRel(methodName, "args", a.args.stamp);
|
|
@@ -233,6 +279,14 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
233
279
|
}
|
|
234
280
|
continue;
|
|
235
281
|
}
|
|
282
|
+
if (a.tag === "fixed") {
|
|
283
|
+
const why = pinError(iface, addr, res, a.args.value);
|
|
284
|
+
if (why) {
|
|
285
|
+
badPins.add(e);
|
|
286
|
+
pinFailed(item, why.code, at, why.message);
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
236
290
|
if (res.method) {
|
|
237
291
|
if (a.tag !== "describe") diag("error", "E_TAG_TARGET", at, `${a.tag} is a value tag; address a value, not a method`);
|
|
238
292
|
continue;
|
|
@@ -289,9 +343,14 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
289
343
|
}
|
|
290
344
|
}
|
|
291
345
|
|
|
292
|
-
// Dropping a value tag can leave an idempotency stamp without its timestamp tag: drop that idempotency too.
|
|
346
|
+
// Dropping a value tag can leave an idempotency stamp without its timestamp tag: drop that idempotency too. It can
|
|
347
|
+
// also leave a grant without the amount, timestamp or account tag it needs.
|
|
293
348
|
if (dropped.size) {
|
|
294
349
|
const without = buildLayers(entries, (e) => !dropped.has(e));
|
|
350
|
+
for (const { e, diag } of grants) {
|
|
351
|
+
const why = !dropped.has(e) && grantError(iface, without, e.addr.method, e.entry.args);
|
|
352
|
+
if (why) badGrant(e, diag, `${why} (a tag it needs was dropped in this service)`);
|
|
353
|
+
}
|
|
295
354
|
for (const e of entries) {
|
|
296
355
|
if (!e.item.apply || e.ann.tag !== "idempotency" || dropped.has(e)) continue;
|
|
297
356
|
const r = iface.resolveRel(e.addr.method, "args", e.ann.args.stamp);
|
|
@@ -302,12 +361,19 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
302
361
|
}
|
|
303
362
|
}
|
|
304
363
|
|
|
305
|
-
// Binding (spec
|
|
364
|
+
// Binding (spec sections 7 and 15.3): a canister or contract profile whose interface changed is stale when anything
|
|
365
|
+
// no longer holds.
|
|
306
366
|
const stale = new Set(ordered.filter((item) => item.rebinding && item.diags.some((d) => STALE_CODES.has(d.code))));
|
|
307
367
|
// A stale profile must not supply value-conversion tags, and a dropped pack annotation is not applied: keep both
|
|
308
|
-
// out of the merged result.
|
|
309
|
-
|
|
310
|
-
const
|
|
368
|
+
// out of the merged result. A profile with a pin that does not hold supplies its effects, without grants, and its
|
|
369
|
+
// pins that hold (spec 15.4, D-058).
|
|
370
|
+
for (const e of entries) {
|
|
371
|
+
if (!unpinned.has(e.item) || e.ann.tag !== "effect" || e.entry.args.grant === undefined) continue;
|
|
372
|
+
const { grant, ...args } = e.entry.args;
|
|
373
|
+
e.entry.args = args;
|
|
374
|
+
}
|
|
375
|
+
const keep = (e) => !dropped.has(e) && !badPins.has(e) && !(stale.has(e.item) && VALUE_CONVERSION_TAGS.has(e.ann.tag)) && !(unpinned.has(e.item) && e.ann.tag !== "effect" && e.ann.tag !== "fixed");
|
|
376
|
+
const final = stale.size || dropped.size || unpinned.size ? buildLayers(entries, keep) : tags;
|
|
311
377
|
|
|
312
378
|
// Overrides: the same tag at the same position in a lower layer (W_OVERRIDDEN, spec section 8).
|
|
313
379
|
if (final.layers.length > 1) {
|
|
@@ -348,7 +414,7 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
348
414
|
}
|
|
349
415
|
|
|
350
416
|
for (const item of stale) {
|
|
351
|
-
item.diag("error", "E_STALE",
|
|
417
|
+
item.diag("error", "E_STALE", boundOf(item).at, "the interface changed and some annotations no longer hold; value-conversion tags must not be used (the resolved form omits this profile's value-conversion tags)");
|
|
352
418
|
}
|
|
353
419
|
return { diagnostics, layered: final, ordered, stale };
|
|
354
420
|
}
|
package/src/browser.mjs
CHANGED
|
@@ -35,7 +35,8 @@ export const PROFILE_SCHEMA = schema;
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
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
|
|
38
|
+
* `loadPacks` returns. The standard contract profiles there (Permit2, DAI's permit, USDC, x402's Permit2 payment) are
|
|
39
|
+
* under `./packs/*` too.
|
|
39
40
|
*/
|
|
40
41
|
export const STANDARD_PACKS = Object.freeze([
|
|
41
42
|
{ file: "erc20.profile.json", doc: erc20 },
|
package/src/checks.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-annotation semantic checks (spec sections 3 to 6). Moved verbatim from scripts/validate.mjs.
|
|
3
3
|
*/
|
|
4
|
+
import { AbiError, readAddress } from "./abi.mjs";
|
|
4
5
|
import { parseRelPath } from "./address.mjs";
|
|
5
6
|
|
|
6
7
|
/** EVM integer types (spec section 15): uint8 to uint256 and int8 to int256. int8 to int64 are also Candid's, with the same bounds. */
|
|
@@ -18,7 +19,25 @@ export const CASE_TAGS = new Set(["intent", "effect"]);
|
|
|
18
19
|
export const CONFIRM_KINDS = new Set(["transfer", "approve", "admin", "destroy"]);
|
|
19
20
|
/** Tags that convert a value between its wire form and a human form; a stale profile must not supply them (spec section 7). */
|
|
20
21
|
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", "E_TOKEN_PATH"]);
|
|
22
|
+
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"]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Why a pin (`fixed`, spec 15.4, D-058) does not hold, as `{ code, message }`, or null. A pin sits on an `address` in the
|
|
26
|
+
* arguments of an EVM function or message, where a consumer reads it from what is sent or signed before any text, and
|
|
27
|
+
* its value is an address (§15.2: in mixed case, with its EIP-55 checksum).
|
|
28
|
+
*/
|
|
29
|
+
export function pinError(iface, addr, res, value) {
|
|
30
|
+
if (res.method || addr.kind !== "arg") return { code: "E_TAG_TARGET", message: "fixed pins a value in the arguments of a function or a message, where a consumer reads it before it shows text" };
|
|
31
|
+
const wrong = checkApplicability(iface, "fixed", res.ref);
|
|
32
|
+
if (wrong) return { code: "E_TAG_NOT_APPLICABLE", message: wrong };
|
|
33
|
+
try {
|
|
34
|
+
readAddress(String(value));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
if (!(e instanceof AbiError)) throw e;
|
|
37
|
+
return { code: "E_ADDRESS_CHECKSUM", message: `fixed: ${e.message}` };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
22
41
|
|
|
23
42
|
export function checkApplicability(iface, tag, ref) {
|
|
24
43
|
const { ref: r } = iface.unwrapOpt(ref);
|
|
@@ -33,6 +52,9 @@ export function checkApplicability(iface, tag, ref) {
|
|
|
33
52
|
case "bytes": return iface.isBlob(r) ? null : `${tag} needs ${iface.chain === "evm" ? "bytes or bytesN" : "blob"}, found ${iface.describeType(ref)}`;
|
|
34
53
|
case "text": return TEXT.has(p) ? null : `text needs ${iface.chain === "evm" ? "string" : "text"}, found ${iface.describeType(ref)}`;
|
|
35
54
|
case "range": return NUMERIC.has(p) || TEXT.has(p) || iface.node(r).kind === "vec" ? null : `range needs numeric, text or vec, found ${iface.describeType(ref)}`;
|
|
55
|
+
case "fixed":
|
|
56
|
+
if (iface.chain !== "evm") return "fixed pins an EVM address; a Candid position cannot carry it yet (spec section 15.4)";
|
|
57
|
+
return p === "address" ? null : `fixed needs an address, found ${iface.describeType(ref)}`;
|
|
36
58
|
default: return null;
|
|
37
59
|
}
|
|
38
60
|
}
|
|
@@ -57,6 +79,55 @@ export function tokenPathError(iface, addr, token) {
|
|
|
57
79
|
return null;
|
|
58
80
|
}
|
|
59
81
|
|
|
82
|
+
/**
|
|
83
|
+
* An effect's grant (spec 5.3, D-056): who may take the tokens, how many and until when, as positions in the arguments
|
|
84
|
+
* of the method the effect is on. `tags` is the merged view: the amount, timestamp and account tags a grant needs may
|
|
85
|
+
* come from another profile. The reason it is wrong, or null.
|
|
86
|
+
*/
|
|
87
|
+
export function grantError(iface, tags, methodName, { kind, grant }) {
|
|
88
|
+
if (kind !== "transfer" && kind !== "approve") return `a grant goes with effect kind transfer or approve, not ${kind}`;
|
|
89
|
+
if ((grant.spender === undefined) === (grant.to === undefined)) return "a grant names exactly one of spender and to";
|
|
90
|
+
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";
|
|
91
|
+
const at = (name, rel, mayBeAbsent) => {
|
|
92
|
+
const p = parseRelPath(rel);
|
|
93
|
+
const r = iface.resolveRel(methodName, "args", rel);
|
|
94
|
+
const what = `grant ${name} ${JSON.stringify(rel)}`;
|
|
95
|
+
if (!p || r.error) return { error: `${what}: ${r.error ?? "not a relative path"}` };
|
|
96
|
+
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` };
|
|
97
|
+
// The value must be in every call: no case of a variant on the way, and no opt unless it may be left out.
|
|
98
|
+
let cur = iface.methods.get(methodName).args[p.index ?? 0];
|
|
99
|
+
let optional = false;
|
|
100
|
+
for (const s of p.segs) {
|
|
101
|
+
const u = iface.unwrapOpt(cur);
|
|
102
|
+
optional ||= u.optional;
|
|
103
|
+
const n = iface.node(u.ref);
|
|
104
|
+
if (n.kind === "variant") return { error: `${what} passes through a case of a variant, so a call need not hold it` };
|
|
105
|
+
cur = n.fields.find((f) => f.id === s.id).type;
|
|
106
|
+
}
|
|
107
|
+
if ((optional || iface.unwrapOpt(cur).optional) && !mayBeAbsent) return { error: `${what} is optional, but a grant needs its ${name} in every call` };
|
|
108
|
+
return { r, t: tags.at(r, iface) ?? {} };
|
|
109
|
+
};
|
|
110
|
+
const who = grant.spender !== undefined ? "spender" : "to";
|
|
111
|
+
const party = at(who, grant[who], false);
|
|
112
|
+
if (party.error) return party.error;
|
|
113
|
+
if (iface.prim(party.r.ref) !== "address" && !party.t.account) {
|
|
114
|
+
return `grant ${who} ${JSON.stringify(grant[who])} is ${iface.describeType(party.r.ref)}; it must be an address or carry an account tag`;
|
|
115
|
+
}
|
|
116
|
+
const amount = at("amount", grant.amount, false);
|
|
117
|
+
if (amount.error) return amount.error;
|
|
118
|
+
const p = iface.prim(amount.r.ref);
|
|
119
|
+
if (p !== "bool") {
|
|
120
|
+
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`;
|
|
121
|
+
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`;
|
|
122
|
+
}
|
|
123
|
+
for (const rel of grant.ends ?? []) {
|
|
124
|
+
const end = at("ends", rel, true);
|
|
125
|
+
if (end.error) return end.error;
|
|
126
|
+
if (!end.t.timestamp) return `grant ends ${JSON.stringify(rel)} has no timestamp tag`;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
60
131
|
export function checkDynamic(iface, value, want, diag, at) {
|
|
61
132
|
if (!value || typeof value !== "object") return;
|
|
62
133
|
const from = value.from;
|
package/src/resolve.mjs
CHANGED
|
@@ -141,7 +141,9 @@ async function resolveWith(iface, opts, diagnostics, statuses, rebuild = null) {
|
|
|
141
141
|
continue;
|
|
142
142
|
}
|
|
143
143
|
checkDeployments(doc, note);
|
|
144
|
-
|
|
144
|
+
// It binds by the identity of its own ABI (spec section 15.1), compared with the given ABI's functions only.
|
|
145
|
+
const bound = doc.binds.abi === undefined ? null : { at: "binds.abi", id: abiInterfaceId(own.functions), given: iface.abiId };
|
|
146
|
+
items.push({ doc, pid, rebinding: bound !== null && bound.id !== bound.given, bound, apply: false, status, eip712 });
|
|
145
147
|
} else if (doc.kind === "pack") {
|
|
146
148
|
const m = await matchPack(iface, doc);
|
|
147
149
|
if (m.error) {
|
package/src/served.mjs
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* A profile served by its canister (spec section 8.1, D-048): the public custom section `icp:public candid:profile`,
|
|
3
3
|
* read by a consumer through a certified `read_state` at `/canister/<id>/metadata/candid:profile`.
|
|
4
4
|
*
|
|
5
|
-
* This module reads the section's bytes; it does not fetch them, so the library keeps no agent dependency (the
|
|
6
|
-
*
|
|
7
|
-
* JSON of at most 1 MiB, the document is a `canister` profile of origin `author`,
|
|
8
|
-
* given, is the canister that served it. Everything else (the schema, the binding to
|
|
9
|
-
* serves as `candid:service`, every tag) is `resolveService`'s, as for any profile.
|
|
5
|
+
* This module reads the section's bytes; it does not fetch them, so the library keeps no agent dependency (the caller
|
|
6
|
+
* does the network part, for example with `@icp-sdk/core`'s `readState`). What it checks is what only a served profile
|
|
7
|
+
* can get wrong: the bytes are UTF-8 JSON of at most 1 MiB, the document is a `canister` profile of origin `author`,
|
|
8
|
+
* and its `binds.canister_id`, when given, is the canister that served it. Everything else (the schema, the binding to
|
|
9
|
+
* the interface the same canister serves as `candid:service`, every tag) is `resolveService`'s, as for any profile.
|
|
10
10
|
*
|
|
11
11
|
* New diagnostic code (spec section 13): E_SERVED.
|
|
12
12
|
*/
|
|
@@ -23,7 +23,7 @@ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
|
23
23
|
/**
|
|
24
24
|
* @param bytes the content of the canister's `candid:profile` metadata (a Uint8Array)
|
|
25
25
|
* @param opts.canisterId the textual id of the canister that served it; null only before the module is installed
|
|
26
|
-
* anywhere (
|
|
26
|
+
* anywhere (a section added to a built module), which leaves `binds.canister_id` unchecked
|
|
27
27
|
* @param opts.label how diagnostics name the input (default "candid:profile")
|
|
28
28
|
* @returns { ok, doc, diagnostics } `doc` is the parsed profile, or null when it cannot be used (E_SERVED)
|
|
29
29
|
*/
|