@meantjs/profile 0.3.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 +9 -0
- 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 +17 -2
- package/data/profile-v0.schema.json +53 -0
- package/data/profile-v0.validate.mjs +3 -3
- package/package.json +1 -1
- package/src/analyze.mjs +28 -4
- package/src/checks.mjs +50 -1
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/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,
|
|
13
13
|
} from "./checks.mjs";
|
|
14
14
|
|
|
15
15
|
/** Spec section 8, highest precedence first. */
|
|
@@ -147,6 +147,18 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
147
147
|
}
|
|
148
148
|
};
|
|
149
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
|
+
|
|
150
162
|
const sets = new Map(); // names set -> { at, method, fields: { name, value, show?, kind? } } (spec 5.3, D-045)
|
|
151
163
|
const named = []; // named positions, checked once every set is known
|
|
152
164
|
for (const e of entries) {
|
|
@@ -167,8 +179,10 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
167
179
|
const why = caseTargetError(iface, addr, res, a.tag);
|
|
168
180
|
if (why) { diag("error", "E_TAG_TARGET", at, why); continue; }
|
|
169
181
|
const m = iface.methods.get(addr.method);
|
|
170
|
-
if (a.tag === "effect")
|
|
171
|
-
|
|
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);
|
|
172
186
|
caseEntries.push({ e, diag });
|
|
173
187
|
continue;
|
|
174
188
|
}
|
|
@@ -176,6 +190,11 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
176
190
|
const m = res.method;
|
|
177
191
|
const methodName = addr.method;
|
|
178
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
|
+
}
|
|
179
198
|
if (a.tag === "idempotency") {
|
|
180
199
|
if (m.mode !== "update") diag("error", "E_IDEMPOTENCY_MODE", at, "idempotency applies to update methods");
|
|
181
200
|
const r = iface.resolveRel(methodName, "args", a.args.stamp);
|
|
@@ -289,9 +308,14 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
289
308
|
}
|
|
290
309
|
}
|
|
291
310
|
|
|
292
|
-
// 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.
|
|
293
313
|
if (dropped.size) {
|
|
294
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
|
+
}
|
|
295
319
|
for (const e of entries) {
|
|
296
320
|
if (!e.item.apply || e.ann.tag !== "idempotency" || dropped.has(e)) continue;
|
|
297
321
|
const r = iface.resolveRel(e.addr.method, "args", e.ann.args.stamp);
|
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", "E_TOKEN_PATH"]);
|
|
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);
|
|
@@ -57,6 +57,55 @@ export function tokenPathError(iface, addr, token) {
|
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
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
|
+
|
|
60
109
|
export function checkDynamic(iface, value, want, diag, at) {
|
|
61
110
|
if (!value || typeof value !== "object") return;
|
|
62
111
|
const from = value.from;
|