@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/analyze.mjs
CHANGED
|
@@ -7,8 +7,9 @@ import { parseAddress } from "./address.mjs";
|
|
|
7
7
|
import { jcs } from "./util.mjs";
|
|
8
8
|
import { TagIndex, LayeredTags, positions } from "./tags.mjs";
|
|
9
9
|
import {
|
|
10
|
-
NUMERIC, METHOD_TAGS, STALE_CODES, VALUE_CONVERSION_TAGS,
|
|
11
|
-
checkApplicability, checkDynamic, parseTemplate, outcomeCoverage,
|
|
10
|
+
NUMERIC, METHOD_TAGS, CASE_TAGS, CONFIRM_KINDS, STALE_CODES, VALUE_CONVERSION_TAGS, EVM_LATER_TAGS,
|
|
11
|
+
checkApplicability, checkDynamic, parseTemplate, outcomeCoverage, caseTargetError, otherCase, overlap, defaultEffect,
|
|
12
|
+
tokenPathError,
|
|
12
13
|
} from "./checks.mjs";
|
|
13
14
|
|
|
14
15
|
/** Spec section 8, highest precedence first. */
|
|
@@ -107,6 +108,45 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
107
108
|
// A pack applied to a service is allowed to differ from its fragment in the ways Candid subtyping allows, so an
|
|
108
109
|
// annotation that no longer holds on this service is dropped with an info, not reported as an error in the pack.
|
|
109
110
|
const dropped = new Set();
|
|
111
|
+
const caseEntries = []; // case intents and effects, checked against each other and their method after the loop
|
|
112
|
+
|
|
113
|
+
/** An effect against the method's mode (spec 5.3); for a case effect, the mode of the method it sits in. */
|
|
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
|
+
}
|
|
120
|
+
if (m.mode !== "update" && m.mode !== "oneway" && kind !== "read") diag("error", "E_EFFECT_MODE", at, `${m.mode} method cannot have effect kind "${kind}"`);
|
|
121
|
+
// A read-only update (D-038): a read sent as an update so that its answer goes through consensus.
|
|
122
|
+
if (replicated === true) {
|
|
123
|
+
if (m.mode !== "update") diag("error", "E_EFFECT_MODE", at, `replicated: true applies to update methods, not to a ${m.mode} method`);
|
|
124
|
+
if (kind !== "read" || irreversible !== false) diag("error", "E_EFFECT_MODE", at, `replicated: true needs kind "read" and irreversible: false, found kind "${kind}" and irreversible: ${irreversible}`);
|
|
125
|
+
}
|
|
126
|
+
const replicatedRead = replicated === true && m.mode === "update" && kind === "read" && irreversible === false;
|
|
127
|
+
if ((m.mode === "update" || m.mode === "oneway") && kind === "read" && !replicatedRead) {
|
|
128
|
+
diag("warning", "W_EFFECT_MODE", at, `${m.mode} method declared as read${m.mode === "update" && replicated !== true ? "; a read sent as an update says irreversible: false, replicated: true" : ""}`);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** An intent template's placeholders (spec section 6); `caseAddr` is the case a case intent sits on. */
|
|
133
|
+
const checkTemplate = (item, methodName, template, diag, at, caseAddr) => {
|
|
134
|
+
const t = parseTemplate(template);
|
|
135
|
+
if (t.error) return diag("error", "E_TEMPLATE", at, t.error);
|
|
136
|
+
if (iface.chain === "evm" && t.segments) return diag("error", "E_TEMPLATE", at, "an EVM contract's template has no [ ] segments: an ABI value is never absent (spec section 15)");
|
|
137
|
+
for (const p of t.placeholders) {
|
|
138
|
+
const r = iface.resolveRel(methodName, "args", p.path);
|
|
139
|
+
if (r.error && item.apply && p.inOptional) { // absent in this service: the optional segment renders empty (spec section 6)
|
|
140
|
+
item.diag("info", "I_PACK_ANNOTATION_SKIPPED", at, `intent: {${p.path}} is absent in this service (${r.error}); its optional segment renders empty`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (r.error) { diag("error", "E_TEMPLATE_PATH", at, `{${p.path}}: ${r.error}`); continue; }
|
|
144
|
+
const other = caseAddr && otherCase(iface, caseAddr, p.path);
|
|
145
|
+
if (other) { diag("error", "E_TEMPLATE_PATH", at, `{${p.path}}: ${other}`); continue; }
|
|
146
|
+
if (!p.inOptional && iface.node(r.ref).kind === "opt") diag("warning", "W_TEMPLATE_OPTIONAL", at, `{${p.path}} is optional but sits outside a [ ] segment`);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
110
150
|
const sets = new Map(); // names set -> { at, method, fields: { name, value, show?, kind? } } (spec 5.3, D-045)
|
|
111
151
|
const named = []; // named positions, checked once every set is known
|
|
112
152
|
for (const e of entries) {
|
|
@@ -118,22 +158,24 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
118
158
|
item.diag("info", "I_PACK_ANNOTATION_SKIPPED", at, `${a.tag} dropped: ${code}: ${message}`);
|
|
119
159
|
};
|
|
120
160
|
const at = a.at;
|
|
161
|
+
if (iface.chain === "evm" && EVM_LATER_TAGS.has(a.tag)) {
|
|
162
|
+
diag("error", "E_TAG_TARGET", at, `${a.tag} is not defined for EVM contracts yet: it needs receipts, nonces or reads that a later phase adds (spec section 15)`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
// intent and effect on a case of a variant in the arguments (spec 5.2, D-053)
|
|
166
|
+
if (CASE_TAGS.has(a.tag) && !res.method) {
|
|
167
|
+
const why = caseTargetError(iface, addr, res, a.tag);
|
|
168
|
+
if (why) { diag("error", "E_TAG_TARGET", at, why); continue; }
|
|
169
|
+
const m = iface.methods.get(addr.method);
|
|
170
|
+
if (a.tag === "effect") checkEffectMode(m, a.args, diag, at);
|
|
171
|
+
else checkTemplate(item, addr.method, a.args.template, diag, at, addr);
|
|
172
|
+
caseEntries.push({ e, diag });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
121
175
|
if (METHOD_TAGS.has(a.tag)) {
|
|
122
176
|
const m = res.method;
|
|
123
177
|
const methodName = addr.method;
|
|
124
|
-
if (a.tag === "effect")
|
|
125
|
-
const { kind, irreversible, replicated } = a.args;
|
|
126
|
-
if (m.mode !== "update" && m.mode !== "oneway" && kind !== "read") diag("error", "E_EFFECT_MODE", at, `${m.mode} method cannot have effect kind "${kind}"`);
|
|
127
|
-
// A read-only update (D-038): a read sent as an update so that its answer goes through consensus.
|
|
128
|
-
if (replicated === true) {
|
|
129
|
-
if (m.mode !== "update") diag("error", "E_EFFECT_MODE", at, `replicated: true applies to update methods, not to a ${m.mode} method`);
|
|
130
|
-
if (kind !== "read" || irreversible !== false) diag("error", "E_EFFECT_MODE", at, `replicated: true needs kind "read" and irreversible: false, found kind "${kind}" and irreversible: ${irreversible}`);
|
|
131
|
-
}
|
|
132
|
-
const replicatedRead = replicated === true && m.mode === "update" && kind === "read" && irreversible === false;
|
|
133
|
-
if ((m.mode === "update" || m.mode === "oneway") && kind === "read" && !replicatedRead) {
|
|
134
|
-
diag("warning", "W_EFFECT_MODE", at, `${m.mode} method declared as read${m.mode === "update" && replicated !== true ? "; a read sent as an update says irreversible: false, replicated: true" : ""}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
178
|
+
if (a.tag === "effect") checkEffectMode(m, a.args, diag, at);
|
|
137
179
|
if (a.tag === "idempotency") {
|
|
138
180
|
if (m.mode !== "update") diag("error", "E_IDEMPOTENCY_MODE", at, "idempotency applies to update methods");
|
|
139
181
|
const r = iface.resolveRel(methodName, "args", a.args.stamp);
|
|
@@ -153,19 +195,7 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
153
195
|
outcomeCoverage(iface, methodName, classes, diag, at, Boolean(a.args.default));
|
|
154
196
|
if (doc.kind === "pack" && !a.args.default) diag("warning", "W_PACK_NO_DEFAULT", at, "pack outcome without default: result cases a matching canister adds (spec §7) would be unclassified");
|
|
155
197
|
}
|
|
156
|
-
if (a.tag === "intent")
|
|
157
|
-
const t = parseTemplate(a.args.template);
|
|
158
|
-
if (t.error) diag("error", "E_TEMPLATE", at, t.error);
|
|
159
|
-
else for (const p of t.placeholders) {
|
|
160
|
-
const r = iface.resolveRel(methodName, "args", p.path);
|
|
161
|
-
if (r.error && item.apply && p.inOptional) { // absent in this service: the optional segment renders empty (spec section 6)
|
|
162
|
-
item.diag("info", "I_PACK_ANNOTATION_SKIPPED", at, `intent: {${p.path}} is absent in this service (${r.error}); its optional segment renders empty`);
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (r.error) { diag("error", "E_TEMPLATE_PATH", at, `{${p.path}}: ${r.error}`); continue; }
|
|
166
|
-
if (!p.inOptional && iface.node(r.ref).kind === "opt") diag("warning", "W_TEMPLATE_OPTIONAL", at, `{${p.path}} is optional but sits outside a [ ] segment`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
198
|
+
if (a.tag === "intent") checkTemplate(item, methodName, a.args.template, diag, at);
|
|
169
199
|
if (a.tag === "names") {
|
|
170
200
|
const effect = tags.methodTags(methodName).effect?.args;
|
|
171
201
|
const replicatedRead = m.mode === "update" && effect?.kind === "read" && effect.irreversible === false && effect.replicated === true;
|
|
@@ -210,7 +240,10 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
210
240
|
const err = checkApplicability(iface, a.tag, res.ref);
|
|
211
241
|
if (err) diag("error", "E_TAG_NOT_APPLICABLE", at, err);
|
|
212
242
|
if (a.tag === "named") named.push({ a, res, diag, at });
|
|
213
|
-
if (a.tag === "amount") {
|
|
243
|
+
if (a.tag === "amount" && a.args.token !== undefined) {
|
|
244
|
+
const why = tokenPathError(iface, addr, a.args.token);
|
|
245
|
+
if (why) diag("error", "E_TOKEN_PATH", at, why);
|
|
246
|
+
} else if (a.tag === "amount") {
|
|
214
247
|
checkDynamic(iface, a.args.decimals, "numeric", diag, at);
|
|
215
248
|
checkDynamic(iface, a.args.symbol, "text", diag, at);
|
|
216
249
|
}
|
|
@@ -234,6 +267,28 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
234
267
|
}
|
|
235
268
|
}
|
|
236
269
|
|
|
270
|
+
// Tagged cases of one method must not overlap, and a case effect needs a method effect at least as strong
|
|
271
|
+
// (spec 5.2 and 5.3, D-053).
|
|
272
|
+
for (let j = 0; j < caseEntries.length; j++) {
|
|
273
|
+
const { e, diag } = caseEntries[j];
|
|
274
|
+
const { addr, ann: a } = e;
|
|
275
|
+
for (const { e: prev } of caseEntries.slice(0, j)) {
|
|
276
|
+
if (prev.ann.tag !== a.tag || prev.addr.method !== addr.method || prev.res.key === e.res.key) continue;
|
|
277
|
+
const why = overlap(iface, prev.addr, addr);
|
|
278
|
+
if (why) diag("error", "E_CASE_OVERLAP", a.at, `${a.tag} on "${prev.ann.at}" and on "${a.at}": one call can take both cases (${why})`);
|
|
279
|
+
}
|
|
280
|
+
if (a.tag !== "effect") continue;
|
|
281
|
+
const m = iface.methods.get(addr.method);
|
|
282
|
+
const declared = tags.methodTags(addr.method).effect?.args;
|
|
283
|
+
const method = declared ?? defaultEffect(m.mode);
|
|
284
|
+
const whose = `the method's effect is ${method.kind}${method.irreversible ? ", irreversible" : ""}${declared ? "" : " (the default)"}`;
|
|
285
|
+
if (CONFIRM_KINDS.has(a.args.kind) && !CONFIRM_KINDS.has(method.kind)) {
|
|
286
|
+
diag("error", "E_CASE_EFFECT", a.at, `case effect ${a.args.kind} needs a person's confirmation, but ${whose}; declare the method's effect as transfer, approve, admin or destroy, so that a consumer that reads only method tags confirms it too`);
|
|
287
|
+
} else if (a.args.irreversible === true && method.irreversible !== true) {
|
|
288
|
+
diag("error", "E_CASE_EFFECT", a.at, `case effect is irreversible, but ${whose}; a method with an irreversible case is irreversible`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
237
292
|
// Dropping a value tag can leave an idempotency stamp without its timestamp tag: drop that idempotency too.
|
|
238
293
|
if (dropped.size) {
|
|
239
294
|
const without = buildLayers(entries, (e) => !dropped.has(e));
|
|
@@ -280,10 +335,11 @@ export function analyzeProfiles(iface, items, { attribute = true } = {}) {
|
|
|
280
335
|
}
|
|
281
336
|
}
|
|
282
337
|
|
|
283
|
-
// Coverage hint: numeric positions in update-
|
|
338
|
+
// Coverage hint: numeric positions in the arguments of update methods and EIP-712 messages with no meaning attached
|
|
339
|
+
// (merged view).
|
|
284
340
|
const MEANING = ["amount", "timestamp", "duration", "describe", "range"];
|
|
285
341
|
for (const [name, m] of iface.methods) {
|
|
286
|
-
if (m.mode !== "update") continue;
|
|
342
|
+
if (m.mode !== "update" && m.mode !== "message") continue;
|
|
287
343
|
for (const pos of positions(iface, name, "args")) {
|
|
288
344
|
const p = iface.prim(iface.unwrapOpt(pos.ref).ref);
|
|
289
345
|
const t = final.at(pos, iface);
|
package/src/browser.d.mts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Types for @meantjs/profile/browser. Hand-written; the source is plain ES modules (src/*.mjs). The internal
|
|
2
2
|
// graph classes (Iface, TagIndex, LayeredTags, SubtypeChecker) are declared opaquely.
|
|
3
3
|
|
|
4
|
-
/** A Candid Profile document (docs/profile-v0.md): a canister profile or
|
|
4
|
+
/** A Candid Profile document (docs/profile-v0.md): a canister profile, a pack, or an EVM contract profile (section 15). */
|
|
5
5
|
export interface ProfileDocument {
|
|
6
6
|
format: "candid-profile";
|
|
7
7
|
format_version: 0;
|
|
8
|
-
kind: "canister" | "pack";
|
|
8
|
+
kind: "canister" | "pack" | "contract";
|
|
9
9
|
name: string;
|
|
10
10
|
origin: "author" | "third_party" | "pack" | "inferred";
|
|
11
11
|
annotations: { at: string; tag: string; args?: Record<string, unknown> }[];
|
|
@@ -17,6 +17,8 @@ export interface ResolvedForm {
|
|
|
17
17
|
format: "candid-profile-resolved";
|
|
18
18
|
format_version: 0;
|
|
19
19
|
interface_id: string;
|
|
20
|
+
/** "evm" for an EVM contract's form (section 15.5); absent for a Candid service's. */
|
|
21
|
+
chain?: "evm";
|
|
20
22
|
profiles: unknown[];
|
|
21
23
|
methods: Record<string, unknown>;
|
|
22
24
|
diagnostics: Diagnostic[];
|
|
@@ -54,14 +56,16 @@ export interface SchemaValidator {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface ResolveOptions {
|
|
57
|
-
/** Candid source of the service (exactly one of `did` and `
|
|
59
|
+
/** Candid source of the service (exactly one of `did`, `envelope` and `abi`). */
|
|
58
60
|
did?: string;
|
|
59
61
|
/** The service's candid-core ContractEnvelope. */
|
|
60
62
|
envelope?: unknown;
|
|
63
|
+
/** An EVM contract's ABI: human-readable fragments or JSON ABI entries (section 15). */
|
|
64
|
+
abi?: readonly (string | Record<string, unknown>)[];
|
|
61
65
|
didLabel?: string;
|
|
62
66
|
/** Applied as given; a pack here must match. */
|
|
63
67
|
profiles?: { doc: unknown; label?: string }[];
|
|
64
|
-
/** Tried by Candid subtyping; applied only if they match. */
|
|
68
|
+
/** Tried by Candid subtyping (an EVM pack by selector); applied only if they match. */
|
|
65
69
|
packs?: { doc: unknown; label?: string }[];
|
|
66
70
|
validateSchema: SchemaValidator;
|
|
67
71
|
}
|
package/src/browser.mjs
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
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
|
+
import erc20 from "../data/packs/erc20.profile.json" with { type: "json" };
|
|
14
|
+
import erc2612 from "../data/packs/erc2612.profile.json" with { type: "json" };
|
|
13
15
|
import { compileSchema } from "./schema-core.mjs";
|
|
14
16
|
|
|
15
17
|
export { init as initCompiler } from "@candid-core/cli";
|
|
@@ -31,8 +33,13 @@ export { readServedProfile, SERVED_SECTION, SERVED_METADATA, SERVED_MAX_BYTES }
|
|
|
31
33
|
/** The profile schema (a copy of spec/profile-v0.schema.json). */
|
|
32
34
|
export const PROFILE_SCHEMA = schema;
|
|
33
35
|
|
|
34
|
-
/**
|
|
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
|
+
*/
|
|
35
40
|
export const STANDARD_PACKS = Object.freeze([
|
|
41
|
+
{ file: "erc20.profile.json", doc: erc20 },
|
|
42
|
+
{ file: "erc2612.profile.json", doc: erc2612 },
|
|
36
43
|
{ file: "icrc1.profile.json", doc: icrc1 },
|
|
37
44
|
{ file: "icrc2.profile.json", doc: icrc2 },
|
|
38
45
|
]);
|
package/src/checks.mjs
CHANGED
|
@@ -1,29 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-annotation semantic checks (spec sections 3 to 6). Moved verbatim from scripts/validate.mjs.
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { parseRelPath } from "./address.mjs";
|
|
5
|
+
|
|
6
|
+
/** EVM integer types (spec section 15): uint8 to uint256 and int8 to int256. int8 to int64 are also Candid's, with the same bounds. */
|
|
7
|
+
const ABI_BITS = Array.from({ length: 32 }, (_, i) => 8 * (i + 1));
|
|
8
|
+
export const NUMERIC = new Set(["nat", "int", "nat8", "nat16", "nat32", "nat64", "int8", "int16", "int32", "int64", ...ABI_BITS.flatMap((b) => [`uint${b}`, `int${b}`])]);
|
|
9
|
+
export const TIMEISH = new Set(["nat", "int", "nat32", "nat64", "int64", ...ABI_BITS.filter((b) => b >= 32).map((b) => `uint${b}`), ...ABI_BITS.filter((b) => b >= 64).map((b) => `int${b}`)]);
|
|
10
|
+
/** Text: Candid's `text`, the ABI's `string`. */
|
|
11
|
+
export const TEXT = new Set(["text", "string"]);
|
|
12
|
+
/** Tags an EVM contract profile cannot use yet: they need receipts, nonces or reads that phase 2 adds (spec section 15). */
|
|
13
|
+
export const EVM_LATER_TAGS = new Set(["idempotency", "outcome", "list", "names", "named"]);
|
|
6
14
|
export const METHOD_TAGS = new Set(["intent", "effect", "idempotency", "outcome", "list", "names"]);
|
|
15
|
+
/** Method tags that may also sit on a case of a variant in the arguments (spec 5.2, D-053). */
|
|
16
|
+
export const CASE_TAGS = new Set(["intent", "effect"]);
|
|
17
|
+
/** Effect kinds that need a person's confirmation (spec 5.3). */
|
|
18
|
+
export const CONFIRM_KINDS = new Set(["transfer", "approve", "admin", "destroy"]);
|
|
7
19
|
/** Tags that convert a value between its wire form and a human form; a stale profile must not supply them (spec section 7). */
|
|
8
20
|
export const VALUE_CONVERSION_TAGS = new Set(["amount", "timestamp", "duration", "account", "subaccount", "bytes", "named"]);
|
|
9
|
-
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"]);
|
|
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"]);
|
|
10
22
|
|
|
11
23
|
export function checkApplicability(iface, tag, ref) {
|
|
12
24
|
const { ref: r } = iface.unwrapOpt(ref);
|
|
13
25
|
const p = iface.prim(r);
|
|
26
|
+
if (iface.chain === "evm" && (tag === "account" || tag === "subaccount")) return `${tag} is an ICRC type; an EVM address needs no tag (it is shown with its EIP-55 checksum, spec section 15)`;
|
|
14
27
|
switch (tag) {
|
|
15
28
|
case "amount": return NUMERIC.has(p) ? null : `amount needs a numeric type, found ${iface.describeType(ref)}`;
|
|
16
29
|
case "timestamp":
|
|
17
|
-
case "duration": return TIMEISH.has(p) ? null : `${tag} needs nat/int/nat32/nat64/int64, found ${iface.describeType(ref)}`;
|
|
30
|
+
case "duration": return TIMEISH.has(p) ? null : `${tag} needs ${iface.chain === "evm" ? "uint32 to uint256 or int64 to int256" : "nat/int/nat32/nat64/int64"}, found ${iface.describeType(ref)}`;
|
|
18
31
|
case "account": return iface.isAccount(r) ? null : `account needs record { owner : principal; subaccount : opt blob }, found ${iface.describeType(ref)}`;
|
|
19
32
|
case "subaccount":
|
|
20
|
-
case "bytes": return iface.isBlob(r) ? null : `${tag} needs blob, found ${iface.describeType(ref)}`;
|
|
21
|
-
case "text": return p
|
|
22
|
-
case "range": return NUMERIC.has(p) || p
|
|
33
|
+
case "bytes": return iface.isBlob(r) ? null : `${tag} needs ${iface.chain === "evm" ? "bytes or bytesN" : "blob"}, found ${iface.describeType(ref)}`;
|
|
34
|
+
case "text": return TEXT.has(p) ? null : `text needs ${iface.chain === "evm" ? "string" : "text"}, found ${iface.describeType(ref)}`;
|
|
35
|
+
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)}`;
|
|
23
36
|
default: return null;
|
|
24
37
|
}
|
|
25
38
|
}
|
|
26
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
|
+
|
|
27
60
|
export function checkDynamic(iface, value, want, diag, at) {
|
|
28
61
|
if (!value || typeof value !== "object") return;
|
|
29
62
|
const from = value.from;
|
|
@@ -39,17 +72,18 @@ export function checkDynamic(iface, value, want, diag, at) {
|
|
|
39
72
|
else {
|
|
40
73
|
const p = iface.prim(iface.unwrapOpt(m.results[0]).ref);
|
|
41
74
|
if (want === "numeric" && !NUMERIC.has(p)) diag("error", "E_REF_TYPE", at, `"${from}" returns ${p}, expected a number`);
|
|
42
|
-
if (want === "text" && p
|
|
75
|
+
if (want === "text" && !TEXT.has(p)) diag("error", "E_REF_TYPE", at, `"${from}" returns ${p}, expected text`);
|
|
43
76
|
}
|
|
44
77
|
}
|
|
45
78
|
|
|
46
79
|
export function parseTemplate(t) {
|
|
47
80
|
const placeholders = [];
|
|
48
81
|
let depth = 0;
|
|
82
|
+
let segments = 0;
|
|
49
83
|
for (let i = 0; i < t.length; i++) {
|
|
50
84
|
const c = t[i];
|
|
51
85
|
if ((c === "{" || c === "}" || c === "[" || c === "]") && t[i + 1] === c) { i++; continue; } // escaped
|
|
52
|
-
if (c === "[") { if (depth) return { error: "nested [ ] segments are not allowed in v0" }; depth = 1; }
|
|
86
|
+
if (c === "[") { if (depth) return { error: "nested [ ] segments are not allowed in v0" }; depth = 1; segments++; }
|
|
53
87
|
else if (c === "]") { if (!depth) return { error: "unbalanced ]" }; depth = 0; }
|
|
54
88
|
else if (c === "{") {
|
|
55
89
|
const end = t.indexOf("}", i);
|
|
@@ -59,7 +93,7 @@ export function parseTemplate(t) {
|
|
|
59
93
|
} else if (c === "}") return { error: "unbalanced }" };
|
|
60
94
|
}
|
|
61
95
|
if (depth) return { error: "unclosed [" };
|
|
62
|
-
return { placeholders };
|
|
96
|
+
return { placeholders, segments };
|
|
63
97
|
}
|
|
64
98
|
|
|
65
99
|
export function outcomeCoverage(iface, methodName, classes, diag, at, hasDefault) {
|
|
@@ -79,3 +113,62 @@ export function outcomeCoverage(iface, methodName, classes, diag, at, hasDefault
|
|
|
79
113
|
};
|
|
80
114
|
visit(m.results[0], "");
|
|
81
115
|
}
|
|
116
|
+
|
|
117
|
+
/** The effect a method has without an effect tag (spec 5.3): queries read, updates write irreversibly. */
|
|
118
|
+
export function defaultEffect(mode) {
|
|
119
|
+
return mode === "query" || mode === "composite_query" ? { kind: "read", irreversible: false } : { kind: "write", irreversible: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Why an intent or effect at a value address is not on a case of an argument (spec 5.2, D-053), or null. */
|
|
123
|
+
export function caseTargetError(iface, addr, res, tag) {
|
|
124
|
+
if (addr.kind === "type") return `${tag} sits on a method or on a case of a variant in its arguments, never at a type: address (it can reach several methods)`;
|
|
125
|
+
if (addr.kind === "result") return `${tag} sits on a method or on a case of a variant in its arguments, not in its results`;
|
|
126
|
+
if (addr.segs.some((s) => s.kind === "elem")) return `${tag} on a case cannot step into a vector element ([]): one call holds many elements`;
|
|
127
|
+
if (res.parent?.kind !== "variant") return `${tag} sits on a method or on a case of a variant in its arguments; this address ends on ${res.parent ? "a record field" : "a whole argument"}`;
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Walk two segment lists of one method argument side by side. Returns where they part: { kind } of the container at
|
|
133
|
+
* the first differing step, with the two labels, or null when one is a prefix of the other.
|
|
134
|
+
*/
|
|
135
|
+
function part(iface, start, a, b) {
|
|
136
|
+
let cur = start;
|
|
137
|
+
for (let k = 0; k < Math.min(a.length, b.length); k++) {
|
|
138
|
+
cur = iface.unwrapOpt(cur).ref;
|
|
139
|
+
const n = iface.node(cur);
|
|
140
|
+
const x = a[k];
|
|
141
|
+
const y = b[k];
|
|
142
|
+
if (x.kind === "elem" || y.kind === "elem") {
|
|
143
|
+
if (x.kind === y.kind) { cur = n.inner; continue; }
|
|
144
|
+
return { kind: n.kind };
|
|
145
|
+
}
|
|
146
|
+
if (x.id !== y.id) return { kind: n.kind, a: x.label, b: y.label };
|
|
147
|
+
cur = n.fields.find((f) => f.id === x.id).type;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A case template's placeholder that selects another case of a variant the case address passes through, so it can
|
|
154
|
+
* never be present (spec section 6, D-053): the reason, or null. Both paths resolve; `rel` is a method-tuple relpath.
|
|
155
|
+
*/
|
|
156
|
+
export function otherCase(iface, caseAddr, rel) {
|
|
157
|
+
const p = parseRelPath(rel);
|
|
158
|
+
const index = p.index ?? 0;
|
|
159
|
+
if (index !== caseAddr.index) return null;
|
|
160
|
+
const m = iface.methods.get(caseAddr.method);
|
|
161
|
+
const at = part(iface, m.args[index], caseAddr.segs, p.segs);
|
|
162
|
+
return at?.kind === "variant" ? `selects case "${at.b}" of a variant where this intent's case takes "${at.a}", so it is never present` : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Whether one call can take two tagged cases of one method (spec 5.2, D-053): the reason, or null when they exclude
|
|
167
|
+
* each other (they part at a variant) or nest.
|
|
168
|
+
*/
|
|
169
|
+
export function overlap(iface, a, b) {
|
|
170
|
+
if (a.index !== b.index) return "they sit in different arguments";
|
|
171
|
+
const m = iface.methods.get(a.method);
|
|
172
|
+
const at = part(iface, m.args[a.index], a.segs, b.segs);
|
|
173
|
+
return at && at.kind !== "variant" ? "their paths part at a record field" : null;
|
|
174
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* candid-profile resolve (<service.did> | --envelope <file>) [--profile p.json ...] [--packs] [--packs-dir dir]
|
|
2
|
+
* candid-profile resolve (<service.did> | --envelope <file> | --abi <file>) [--profile p.json ...] [--packs] [--packs-dir dir]
|
|
3
3
|
*
|
|
4
4
|
* Prints the resolved form (docs/profile-v0.md section 10) on stdout and a summary on stderr.
|
|
5
5
|
* Exit code: 0 when no diagnostic is an error, 1 when one is (the resolved form is still printed when the service
|
|
@@ -13,9 +13,12 @@ import { resolveService } from "./resolve.mjs";
|
|
|
13
13
|
|
|
14
14
|
export const USAGE = `usage: candid-profile resolve <service.did> [options] > resolved.json
|
|
15
15
|
candid-profile resolve --envelope <file> [options] > resolved.json
|
|
16
|
+
candid-profile resolve --abi <file> [options] > resolved.json
|
|
16
17
|
|
|
17
18
|
--envelope <file> the service's candid-core ContractEnvelope (as \`candid-core-cli gen\` writes it), instead of
|
|
18
19
|
compiling a .did here
|
|
20
|
+
--abi <file> an EVM contract's ABI, a JSON array of human-readable fragments or JSON ABI entries
|
|
21
|
+
(experimental, spec section 15)
|
|
19
22
|
--profile <file> a profile to apply (repeatable). A pack given this way must match the service.
|
|
20
23
|
--packs try every pack in the packs directory; apply those that match
|
|
21
24
|
--packs-dir <dir> packs directory (default: the standard packs this package ships)
|
|
@@ -27,7 +30,7 @@ export const USAGE = `usage: candid-profile resolve <service.did> [options] > re
|
|
|
27
30
|
class UsageError extends Error {}
|
|
28
31
|
|
|
29
32
|
function parseArgs(argv) {
|
|
30
|
-
const opts = { did: null, envelope: null, profiles: [], packs: false, packsDir: DEFAULT_PACKS_DIR, verbose: false, quiet: false, help: false };
|
|
33
|
+
const opts = { did: null, envelope: null, abi: null, profiles: [], packs: false, packsDir: DEFAULT_PACKS_DIR, verbose: false, quiet: false, help: false };
|
|
31
34
|
const args = [...argv];
|
|
32
35
|
const value = (name, inline) => {
|
|
33
36
|
if (inline !== undefined) return inline;
|
|
@@ -40,6 +43,7 @@ function parseArgs(argv) {
|
|
|
40
43
|
const [flag, inline] = a.startsWith("--") && a.includes("=") ? [a.slice(0, a.indexOf("=")), a.slice(a.indexOf("=") + 1)] : [a, undefined];
|
|
41
44
|
if (flag === "--profile") opts.profiles.push(value(flag, inline));
|
|
42
45
|
else if (flag === "--envelope") opts.envelope = value(flag, inline);
|
|
46
|
+
else if (flag === "--abi") opts.abi = value(flag, inline);
|
|
43
47
|
else if (flag === "--packs") opts.packs = true;
|
|
44
48
|
else if (flag === "--packs-dir") opts.packsDir = value(flag, inline);
|
|
45
49
|
else if (flag === "--verbose") opts.verbose = true;
|
|
@@ -73,10 +77,12 @@ export async function main(argv, io) {
|
|
|
73
77
|
if (command !== "resolve") throw new UsageError(`unknown command ${command}`);
|
|
74
78
|
const opts = parseArgs(rest);
|
|
75
79
|
if (opts.help) { err(USAGE.trimEnd()); return 0; }
|
|
76
|
-
|
|
77
|
-
if (
|
|
80
|
+
const given = [opts.did, opts.envelope, opts.abi].filter((x) => x !== null).length;
|
|
81
|
+
if (given > 1) throw new UsageError("give only one of <service.did>, --envelope and --abi");
|
|
82
|
+
if (given === 0) throw new UsageError("missing <service.did> (or --envelope <file>, or --abi <file>)");
|
|
78
83
|
const input = {};
|
|
79
84
|
if (opts.envelope) input.envelope = await readJsonFile(opts.envelope, "envelope");
|
|
85
|
+
else if (opts.abi) input.abi = await readJsonFile(opts.abi, "ABI");
|
|
80
86
|
else {
|
|
81
87
|
try { input.did = await readFile(opts.did, "utf8"); } catch (e) { throw new UsageError(`cannot read ${opts.did}: ${e.message}`); }
|
|
82
88
|
}
|
|
@@ -92,7 +98,7 @@ export async function main(argv, io) {
|
|
|
92
98
|
for (const p of found) packs.push({ doc: p.doc, label: path.relative(process.cwd(), p.file) });
|
|
93
99
|
}
|
|
94
100
|
|
|
95
|
-
const r = await resolveService({ ...input, didLabel: opts.did ?? opts.envelope, validateSchema, profiles, packs });
|
|
101
|
+
const r = await resolveService({ ...input, didLabel: opts.did ?? opts.envelope ?? opts.abi, validateSchema, profiles, packs });
|
|
96
102
|
if (r.resolved) io.stdout.write(`${JSON.stringify(r.resolved, null, 2)}\n`);
|
|
97
103
|
if (!opts.quiet) summary(r, opts, err);
|
|
98
104
|
return r.ok ? 0 : 1;
|
|
@@ -104,7 +110,7 @@ export async function main(argv, io) {
|
|
|
104
110
|
|
|
105
111
|
function summary(r, opts, err) {
|
|
106
112
|
const count = (s) => r.diagnostics.filter((d) => d.severity === s).length;
|
|
107
|
-
if (r.resolved) err(`candid-profile: ${opts.did ?? opts.envelope} ${r.resolved.interface_id} [${Object.keys(r.resolved.methods).length} methods]`);
|
|
113
|
+
if (r.resolved) err(`candid-profile: ${opts.did ?? opts.envelope ?? opts.abi} ${r.resolved.interface_id} [${Object.keys(r.resolved.methods).length} methods]`);
|
|
108
114
|
for (const p of r.profiles) err(` ${p.status.padEnd(11)} ${String(p.name).padEnd(16)} ${String(p.origin ?? "?").padEnd(12)} ${p.profile_id ?? ""}`);
|
|
109
115
|
err(` ${count("error")} error, ${count("warning")} warning, ${count("info")} info`);
|
|
110
116
|
for (const d of r.diagnostics) {
|
package/src/evm.d.mts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Types for @meantjs/profile/evm: the EVM helpers of spec section 15 (experimental), with no dependency, for a
|
|
2
|
+
// browser bundle. Hand-written; the source is src/evm.mjs, src/abi.mjs and src/keccak.mjs.
|
|
3
|
+
// test/types.test.mjs keeps the declared names equal to the runtime exports.
|
|
4
|
+
|
|
5
|
+
/** An ABI type: `kind` is uint, int, address, bool, string, bytes, fixedbytes, array or tuple. */
|
|
6
|
+
export type AbiType =
|
|
7
|
+
| { kind: "uint" | "int"; bits: number }
|
|
8
|
+
| { kind: "address" | "bool" | "string" | "bytes" }
|
|
9
|
+
| { kind: "fixedbytes"; size: number }
|
|
10
|
+
| { kind: "array"; element: AbiType; length: number | null }
|
|
11
|
+
| { kind: "tuple"; components: AbiParam[] };
|
|
12
|
+
/** A parameter, output or tuple component; `name` is null when it has none. */
|
|
13
|
+
export interface AbiParam {
|
|
14
|
+
name: string | null;
|
|
15
|
+
type: AbiType;
|
|
16
|
+
}
|
|
17
|
+
/** A function read from an ABI. */
|
|
18
|
+
export interface AbiFunction {
|
|
19
|
+
name: string;
|
|
20
|
+
inputs: AbiParam[];
|
|
21
|
+
outputs: AbiParam[];
|
|
22
|
+
stateMutability: "pure" | "view" | "nonpayable" | "payable";
|
|
23
|
+
/** `transfer(address to,uint256 value)`: the ERC-7730 key, and a resolved form's `signature`. */
|
|
24
|
+
signature: string;
|
|
25
|
+
/** `transfer(address,uint256)`: what the selector hashes. */
|
|
26
|
+
typeSignature: string;
|
|
27
|
+
/** `0xa9059cbb` */
|
|
28
|
+
selector: string;
|
|
29
|
+
/** `function transfer(address to,uint256 value) returns (bool)` */
|
|
30
|
+
canonical: string;
|
|
31
|
+
/** Where it was read (`abi[3]`); parseSignature leaves it out. */
|
|
32
|
+
at?: string;
|
|
33
|
+
}
|
|
34
|
+
/** A decoded or encodable value: decimal strings (or bigints) for integers, EIP-55 text for addresses, Uint8Array for bytes. */
|
|
35
|
+
export type AbiValue = string | bigint | boolean | Uint8Array | AbiValue[] | { [key: string]: AbiValue };
|
|
36
|
+
export interface AbiLimits {
|
|
37
|
+
maxBytes?: number;
|
|
38
|
+
maxDepth?: number;
|
|
39
|
+
maxElements?: number;
|
|
40
|
+
}
|
|
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
|
+
export declare class AbiError extends Error {
|
|
43
|
+
constructor(code: string, message: string);
|
|
44
|
+
name: "AbiError";
|
|
45
|
+
code: string;
|
|
46
|
+
}
|
|
47
|
+
export interface TypedData {
|
|
48
|
+
domain: Record<string, unknown>;
|
|
49
|
+
types: Record<string, { name: string; type: string }[]>;
|
|
50
|
+
primaryType: string;
|
|
51
|
+
message: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Keccak-256 (Ethereum's, not NIST SHA3-256) of bytes or of a string's UTF-8 bytes. */
|
|
55
|
+
export function keccak256(input: Uint8Array | string): Uint8Array;
|
|
56
|
+
/** Keccak-256 as 64 lowercase hex digits, without `0x`. */
|
|
57
|
+
export function keccak256Hex(input: Uint8Array | string): string;
|
|
58
|
+
/** RFC 8785 canonical JSON. */
|
|
59
|
+
export function jcs(value: unknown): string;
|
|
60
|
+
export const DEFAULT_ABI_LIMITS: Readonly<Required<AbiLimits>>;
|
|
61
|
+
/** Read an ABI (human-readable fragments or JSON ABI entries); only functions are read. Problems are listed, not thrown. */
|
|
62
|
+
export function parseAbi(entries: unknown, label?: string): { functions: AbiFunction[]; errors: { at: string; message: string }[] };
|
|
63
|
+
/** One function from its named signature, as a resolved form carries it. Throws AbiError. */
|
|
64
|
+
export function parseSignature(signature: string, where?: string): AbiFunction;
|
|
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;
|
|
70
|
+
export function canonicalFragment(f: Pick<AbiFunction, "name" | "inputs" | "outputs" | "stateMutability">): string;
|
|
71
|
+
export function namedSignature(f: Pick<AbiFunction, "name" | "inputs">): string;
|
|
72
|
+
export function typeSignature(f: Pick<AbiFunction, "name" | "inputs">): string;
|
|
73
|
+
export function selectorOf(f: Pick<AbiFunction, "name" | "inputs">): string;
|
|
74
|
+
/** Canonical type text; with `names`, tuple components keep their names. */
|
|
75
|
+
export function typeString(type: AbiType, names?: boolean): string;
|
|
76
|
+
/** A tuple component's key in values and resolved forms: its name, else `#` and its position. */
|
|
77
|
+
export function componentKey(param: AbiParam, index: number): string;
|
|
78
|
+
/** ABI-encode a parameter list's values, keyed like a tuple. Throws AbiError (E_ABI_ENCODE). */
|
|
79
|
+
export function encodeParams(params: readonly AbiParam[], values: Record<string, AbiValue>): Uint8Array;
|
|
80
|
+
/** The selector followed by the encoded parameters. */
|
|
81
|
+
export function encodeCall(f: AbiFunction, values: Record<string, AbiValue>): Uint8Array;
|
|
82
|
+
/** Decode strictly: the bytes must be the canonical encoding of the values read. Throws AbiError (E_ABI_DECODE). */
|
|
83
|
+
export function decodeParams(params: readonly AbiParam[], data: Uint8Array, limits?: AbiLimits): Record<string, AbiValue>;
|
|
84
|
+
/** The EIP-55 checksummed form of `0x` and 40 hex digits. */
|
|
85
|
+
export function toChecksumAddress(address: string): string;
|
|
86
|
+
/** Read an address: mixed case must match its EIP-55 checksum. Returns the checksummed form. */
|
|
87
|
+
export function readAddress(text: string): string;
|
|
88
|
+
/** The EIP-712 digest a signer signs, as `0x` and 64 hex digits. */
|
|
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
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@meantjs/profile/evm`: the EVM helpers of spec section 15 (D-054), browser-safe and without dependencies.
|
|
3
|
+
* Keccak-256, ABI fragments (reading, canonical text, selectors, the interface identity), a strict ABI codec, EIP-55
|
|
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.
|
|
6
|
+
*
|
|
7
|
+
* test/evm.test.mjs checks the vectors; test/address-entry.test.mjs keeps this entry free of node:* imports.
|
|
8
|
+
*/
|
|
9
|
+
export { keccak256, keccak256Hex } from "./keccak.mjs";
|
|
10
|
+
export {
|
|
11
|
+
AbiError,
|
|
12
|
+
DEFAULT_ABI_LIMITS,
|
|
13
|
+
abiInterfaceId,
|
|
14
|
+
canonicalFragment,
|
|
15
|
+
componentKey,
|
|
16
|
+
decodeParams,
|
|
17
|
+
eip712TypeHash,
|
|
18
|
+
encodeCall,
|
|
19
|
+
encodeParams,
|
|
20
|
+
encodeType,
|
|
21
|
+
namedSignature,
|
|
22
|
+
parseAbi,
|
|
23
|
+
parseEip712Types,
|
|
24
|
+
parseSignature,
|
|
25
|
+
readAddress,
|
|
26
|
+
selectorOf,
|
|
27
|
+
toChecksumAddress,
|
|
28
|
+
typeSignature,
|
|
29
|
+
typeString,
|
|
30
|
+
typedDataDigest,
|
|
31
|
+
} from "./abi.mjs";
|
|
32
|
+
export { jcs } from "./util.mjs";
|