@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meantjs/profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": {
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"icrc-1",
|
|
20
20
|
"icrc-2",
|
|
21
21
|
"profile",
|
|
22
|
+
"evm",
|
|
23
|
+
"erc-20",
|
|
24
|
+
"erc-7730",
|
|
22
25
|
"resolver",
|
|
23
26
|
"json-schema"
|
|
24
27
|
],
|
|
@@ -39,6 +42,10 @@
|
|
|
39
42
|
"types": "./src/browser.d.mts",
|
|
40
43
|
"default": "./src/browser.mjs"
|
|
41
44
|
},
|
|
45
|
+
"./evm": {
|
|
46
|
+
"types": "./src/evm.d.mts",
|
|
47
|
+
"default": "./src/evm.mjs"
|
|
48
|
+
},
|
|
42
49
|
"./schema.json": "./data/profile-v0.schema.json",
|
|
43
50
|
"./packs/*": "./data/packs/*",
|
|
44
51
|
"./package.json": "./package.json"
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An EVM contract interface as the resolver sees one (spec section 15, D-054): its function fragments laid out as the
|
|
3
|
+
* graph a Candid interface compiles to, so that addresses, tags, merging and the resolved form work unchanged.
|
|
4
|
+
*
|
|
5
|
+
* a function a method; `view` and `pure` are queries, `nonpayable` and `payable` updates
|
|
6
|
+
* its parameters one record at argument index 0 (`transfer(0).to`); a function without parameters has no argument
|
|
7
|
+
* its outputs the result tuple (`balanceOf->(0)`)
|
|
8
|
+
* a tuple a record whose fields keep the ABI order; a named component's id is the Candid hash of its name,
|
|
9
|
+
* an unnamed one's is its position (`#1`)
|
|
10
|
+
* T[] and T[k] a vector (`[]`); T[k] keeps its length
|
|
11
|
+
* anything else a primitive named by its canonical ABI type (`uint256`, `address`, `bytes32`, `string`)
|
|
12
|
+
* an EIP-712 a method of mode "message" (spec section 15.9) whose one argument is the struct, a record of its
|
|
13
|
+
* message members in order (`PermitSingle(0).details.amount`); it has no results
|
|
14
|
+
*/
|
|
15
|
+
import { Iface } from "./iface.mjs";
|
|
16
|
+
import { idlHash } from "./idl.mjs";
|
|
17
|
+
import { AbiError, abiInterfaceId, componentKey, eip712AbiType, eip712TypeHash, encodeType, parseAbi, parseEip712Types, readAddress } from "./abi.mjs";
|
|
18
|
+
|
|
19
|
+
export class AbiIface extends Iface {
|
|
20
|
+
/**
|
|
21
|
+
* @param functions from parseAbi
|
|
22
|
+
* @param eip712 { types, messages } (spec section 15.9): the struct types, and which of them are messages
|
|
23
|
+
*/
|
|
24
|
+
constructor(envelope, functions, label, eip712 = { types: {}, messages: [] }) {
|
|
25
|
+
super(envelope, label);
|
|
26
|
+
this.chain = "evm";
|
|
27
|
+
this.functions = new Map(functions.map((f) => [f.name, f]));
|
|
28
|
+
/** The identity of the functions alone: what a contract profile's `binds.abi` binds to (spec section 15.1). */
|
|
29
|
+
this.abiId = abiInterfaceId(functions);
|
|
30
|
+
this.structs = eip712.types;
|
|
31
|
+
this.messages = new Map(eip712.messages.map((name) => [name, { encodeType: encodeType(eip712.types, name), typeHash: eip712TypeHash(eip712.types, name) }]));
|
|
32
|
+
}
|
|
33
|
+
resolve(addr) {
|
|
34
|
+
const r = super.resolve(addr);
|
|
35
|
+
if (r.error && addr.kind !== "type" && !this.methods.has(addr.method) && Object.hasOwn(this.structs, addr.method)) {
|
|
36
|
+
return { error: `${addr.method} is an EIP-712 type but not a message: tag its name with intent to make it one, or address it through a message that contains it` };
|
|
37
|
+
}
|
|
38
|
+
return r;
|
|
39
|
+
}
|
|
40
|
+
isBlob(ref) {
|
|
41
|
+
const p = this.prim(ref);
|
|
42
|
+
return p === "bytes" || /^bytes\d+$/.test(p ?? "");
|
|
43
|
+
}
|
|
44
|
+
isAccount() { return false; }
|
|
45
|
+
/** The canonical ABI type of a position: `uint256`, `address[]`, `(address,uint256)`. */
|
|
46
|
+
describeType(ref) {
|
|
47
|
+
const n = this.types[ref];
|
|
48
|
+
if (n.kind === "primitive") return n.primitive;
|
|
49
|
+
if (n.kind === "vec") return `${this.describeType(n.inner)}[${n.length ?? ""}]`;
|
|
50
|
+
return `(${n.fields.map((f) => this.describeType(f.type)).join(",")})`;
|
|
51
|
+
}
|
|
52
|
+
/** The `abi` of a resolved-form node: the primitive's type, `tuple` or `array`. */
|
|
53
|
+
abiType(ref) {
|
|
54
|
+
const n = this.types[ref];
|
|
55
|
+
return n.kind === "primitive" ? n.primitive : n.kind === "record" ? "tuple" : "array";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param entries an ABI: human-readable fragments or JSON ABI entries (parseAbi)
|
|
61
|
+
* @param label how diagnostics name it (`binds.abi`, `abi`)
|
|
62
|
+
* @param eip712 { types, messages, label } (spec section 15.9): struct types already read by parseEip712Types, the
|
|
63
|
+
* names of those that are messages, and how diagnostics name the types; null for none
|
|
64
|
+
* @param opts.allowEmpty an interface with no function and no message is not refused (resolveService adds the
|
|
65
|
+
* messages of its profiles later)
|
|
66
|
+
* @returns {{ iface: AbiIface } | { failure: { at: string, message: string }[] }}
|
|
67
|
+
*/
|
|
68
|
+
export function abiInterface(entries, label = "abi", ifaceLabel = "contract", eip712 = null, { allowEmpty = false } = {}) {
|
|
69
|
+
const { functions, errors } = parseAbi(entries, label);
|
|
70
|
+
if (errors.length) return { failure: errors };
|
|
71
|
+
const messages = eip712?.messages ?? [];
|
|
72
|
+
if (functions.length === 0 && messages.length === 0 && !allowEmpty) return { failure: [eip712 ? { at: eip712.label, message: "the ABI has no functions and no type is a message (tag a type's name with intent)" } : { at: label, message: "the ABI has no functions" }] };
|
|
73
|
+
const types = [];
|
|
74
|
+
const names = [];
|
|
75
|
+
const failure = [];
|
|
76
|
+
const add = (node) => types.push(node) - 1;
|
|
77
|
+
const build = (t, where) => {
|
|
78
|
+
if (t.kind === "tuple") {
|
|
79
|
+
const ref = add(null);
|
|
80
|
+
const ids = new Map();
|
|
81
|
+
const fields = t.components.map((c, i) => {
|
|
82
|
+
const id = c.name === null ? i : idlHash(c.name);
|
|
83
|
+
const key = componentKey(c, i);
|
|
84
|
+
if (ids.has(id)) failure.push({ at: where, message: `components ${ids.get(id)} and ${key} have the same field id ${id}; rename one` });
|
|
85
|
+
ids.set(id, key);
|
|
86
|
+
names.push([ref, id, key]);
|
|
87
|
+
return { id, type: build(c.type, `${where}.${key}`) };
|
|
88
|
+
});
|
|
89
|
+
types[ref] = { kind: "record", fields };
|
|
90
|
+
return ref;
|
|
91
|
+
}
|
|
92
|
+
if (t.kind === "array") {
|
|
93
|
+
const ref = add(null);
|
|
94
|
+
types[ref] = { kind: "vec", inner: build(t.element, `${where}[]`), length: t.length };
|
|
95
|
+
return ref;
|
|
96
|
+
}
|
|
97
|
+
return add({ kind: "primitive", primitive: t.kind === "uint" || t.kind === "int" ? `${t.kind}${t.bits}` : t.kind === "fixedbytes" ? `bytes${t.size}` : t.kind });
|
|
98
|
+
};
|
|
99
|
+
const methods = functions.map((f) => {
|
|
100
|
+
const args = f.inputs.length ? [build({ kind: "tuple", components: f.inputs }, `${f.name}(0)`)] : [];
|
|
101
|
+
const results = f.outputs.map((o, i) => build(o.type, `${f.name}->(${i})`));
|
|
102
|
+
const mode = f.stateMutability === "view" || f.stateMutability === "pure" ? "query" : "update";
|
|
103
|
+
return { name: f.name, function: add({ kind: "func", mode, args, results }) };
|
|
104
|
+
});
|
|
105
|
+
for (const name of messages) {
|
|
106
|
+
const at = `${eip712.label}.${name}`;
|
|
107
|
+
if (functions.some((f) => f.name === name)) {
|
|
108
|
+
failure.push({ at, message: `a function and a message are both named ${name}; an address names one of them` });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
methods.push({ name, function: add({ kind: "func", mode: "message", args: [build(eip712AbiType(eip712.types, name), `${name}(0)`)], results: [] }) });
|
|
112
|
+
}
|
|
113
|
+
if (failure.length) return { failure };
|
|
114
|
+
const service = add({ kind: "service", methods });
|
|
115
|
+
const structs = eip712?.types ?? {};
|
|
116
|
+
const envelope = {
|
|
117
|
+
contract: { types, declarations: [], actor: { kind: "service", service }, identities: { interface: abiInterfaceId(functions, structs) } },
|
|
118
|
+
extensions: { "org.candid-core.field-names/v1": names },
|
|
119
|
+
};
|
|
120
|
+
return { iface: new AbiIface(envelope, functions, ifaceLabel, { types: structs, messages }) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The messages a profile or pack declares (spec section 15.9): the types of `binds.eip712` whose name the same
|
|
125
|
+
* document tags with `intent`. Returns { types, messages, errors }.
|
|
126
|
+
*/
|
|
127
|
+
export function eip712Of(doc, label = "binds.eip712.types") {
|
|
128
|
+
const spec = doc.binds?.eip712;
|
|
129
|
+
if (spec === undefined) return { types: {}, messages: [], errors: [] };
|
|
130
|
+
const { types, errors } = parseEip712Types(spec.types, label);
|
|
131
|
+
const messages = [...new Set(doc.annotations.filter((a) => a.tag === "intent" && Object.hasOwn(types, a.at)).map((a) => a.at))].sort();
|
|
132
|
+
return { types, messages, errors };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* `binds.deployments` of a contract profile: an address in mixed case must match its EIP-55 checksum, and a deployment
|
|
137
|
+
* is listed once. `diag(severity, code, at, message)`.
|
|
138
|
+
*/
|
|
139
|
+
export function checkDeployments(doc, diag) {
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
(doc.binds.deployments ?? []).forEach((d, i) => {
|
|
142
|
+
const at = `binds.deployments[${i}]`;
|
|
143
|
+
let address;
|
|
144
|
+
try {
|
|
145
|
+
address = readAddress(d.address);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
if (!(e instanceof AbiError)) throw e;
|
|
148
|
+
return diag("error", "E_ADDRESS_CHECKSUM", `${at}.address`, e.message);
|
|
149
|
+
}
|
|
150
|
+
const key = `${d.chain_id}:${address}`;
|
|
151
|
+
if (seen.has(key)) diag("warning", "W_DUPLICATE", at, `chain ${d.chain_id} ${address} is listed twice`);
|
|
152
|
+
seen.add(key);
|
|
153
|
+
});
|
|
154
|
+
}
|
package/src/abi-pack.mjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVM packs (spec section 15, D-054). A pack with `binds.abi` applies to a contract that has every one of its
|
|
3
|
+
* functions: the same selector (so the same name and parameter types), the same output types, and the same kind of
|
|
4
|
+
* call (a query, or an update that is payable exactly when the pack's is). There is no subtyping.
|
|
5
|
+
*
|
|
6
|
+
* Parameter names are not part of a selector, and contracts name the parameters of one standard differently (EIP-20
|
|
7
|
+
* says `_to`, OpenZeppelin `to`, WETH `dst`). A pack's addresses and template placeholders use the pack's own names;
|
|
8
|
+
* when the pack applies, they are rewritten to the contract's names, component by component, by position. So is an
|
|
9
|
+
* amount's `token` path (spec section 15.9).
|
|
10
|
+
*/
|
|
11
|
+
import { parseAddress, parseRelPath } from "./address.mjs";
|
|
12
|
+
import { componentKey, parseAbi, typeString } from "./abi.mjs";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param iface an AbiIface
|
|
16
|
+
* @param pack a schema-valid pack document with `binds.abi`
|
|
17
|
+
* @returns {{ matched: boolean, reasons: { at: string, message: string }[], relaxed: [], functions: object[] } | { error: { at: string, message: string }[] }}
|
|
18
|
+
*/
|
|
19
|
+
export function matchAbiPack(iface, pack) {
|
|
20
|
+
const { functions, errors } = parseAbi(pack.binds.abi, "binds.abi");
|
|
21
|
+
if (errors.length) return { error: errors };
|
|
22
|
+
const reasons = [];
|
|
23
|
+
const kind = (f) => (f.stateMutability === "view" || f.stateMutability === "pure" ? "a query (view or pure)" : f.stateMutability === "payable" ? "payable" : "not payable");
|
|
24
|
+
const outputs = (f) => `(${f.outputs.map((o) => typeString(o.type)).join(",")})`;
|
|
25
|
+
for (const f of functions) {
|
|
26
|
+
const g = iface.functions.get(f.name);
|
|
27
|
+
if (!g || g.selector !== f.selector) reasons.push({ at: f.name, message: `no function ${f.typeSignature} (selector ${f.selector})` });
|
|
28
|
+
else if (outputs(g) !== outputs(f)) reasons.push({ at: f.name, message: `returns ${outputs(g)}, the pack's ${outputs(f)}` });
|
|
29
|
+
else if (kind(g) !== kind(f)) reasons.push({ at: f.name, message: `is ${kind(g)}, the pack's ${kind(f)}` });
|
|
30
|
+
}
|
|
31
|
+
return { matched: reasons.length === 0, reasons, relaxed: [], functions };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A path through the pack's type, spelled with the contract's names; null when the pack's own type lacks it. */
|
|
35
|
+
function mapSegs(packT, ownT, segs) {
|
|
36
|
+
let p = packT;
|
|
37
|
+
let c = ownT;
|
|
38
|
+
let out = "";
|
|
39
|
+
for (const s of segs) {
|
|
40
|
+
if (s.kind === "elem") {
|
|
41
|
+
if (p.kind !== "array") return null;
|
|
42
|
+
out += "[]";
|
|
43
|
+
p = p.element;
|
|
44
|
+
c = c.element;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (p.kind !== "tuple") return null;
|
|
48
|
+
const i = p.components.findIndex((comp, j) => componentKey(comp, j) === s.label);
|
|
49
|
+
if (i < 0) return null;
|
|
50
|
+
out += `.${componentKey(c.components[i], i)}`;
|
|
51
|
+
p = p.components[i].type;
|
|
52
|
+
c = c.components[i].type;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const tupleOf = (params) => ({ kind: "tuple", components: params });
|
|
58
|
+
|
|
59
|
+
/** A template with each placeholder rewritten by `map` (null keeps it); escapes and literal text are kept as written. */
|
|
60
|
+
function mapTemplate(template, map) {
|
|
61
|
+
let out = "";
|
|
62
|
+
for (let i = 0; i < template.length; i++) {
|
|
63
|
+
const c = template[i];
|
|
64
|
+
if ((c === "{" || c === "}" || c === "[" || c === "]") && template[i + 1] === c) {
|
|
65
|
+
out += c + c;
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === "{") {
|
|
70
|
+
const end = template.indexOf("}", i);
|
|
71
|
+
if (end < 0) return template; // does not parse: left for E_TEMPLATE
|
|
72
|
+
const path = template.slice(i + 1, end);
|
|
73
|
+
out += `{${map(path) ?? path}}`;
|
|
74
|
+
i = end;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
out += c;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The pack with its addresses and templates in the contract's names. `functions` are the pack's (matchAbiPack).
|
|
84
|
+
* A document whose names already agree comes back equal to the original.
|
|
85
|
+
*/
|
|
86
|
+
export function translatePack(pack, functions, iface) {
|
|
87
|
+
const mine = new Map(functions.map((f) => [f.name, f]));
|
|
88
|
+
const pair = (name) => (mine.has(name) && iface.functions.has(name) ? [mine.get(name), iface.functions.get(name)] : null);
|
|
89
|
+
const at = (text) => {
|
|
90
|
+
const addr = parseAddress(text);
|
|
91
|
+
const fs = addr && addr.kind !== "type" ? pair(addr.method) : null;
|
|
92
|
+
if (!fs || addr.kind === "method") return text;
|
|
93
|
+
const [p, c] = fs;
|
|
94
|
+
if (addr.kind === "arg") {
|
|
95
|
+
const mapped = addr.index === 0 && p.inputs.length ? mapSegs(tupleOf(p.inputs), tupleOf(c.inputs), addr.segs) : null;
|
|
96
|
+
return mapped === null ? text : `${addr.method}(0)${mapped}`;
|
|
97
|
+
}
|
|
98
|
+
const out = p.outputs[addr.index];
|
|
99
|
+
const mapped = out ? mapSegs(out.type, c.outputs[addr.index].type, addr.segs) : null;
|
|
100
|
+
return mapped === null ? text : `${addr.method}->(${addr.index})${mapped}`;
|
|
101
|
+
};
|
|
102
|
+
const placeholder = (method) => (path) => {
|
|
103
|
+
const fs = pair(method);
|
|
104
|
+
const rel = parseRelPath(path);
|
|
105
|
+
if (!fs || !rel || (rel.index ?? 0) !== 0 || !fs[0].inputs.length) return null;
|
|
106
|
+
const mapped = mapSegs(tupleOf(fs[0].inputs), tupleOf(fs[1].inputs), rel.segs);
|
|
107
|
+
return mapped === null ? null : `${rel.index === null ? "" : "(0)"}${rel.index === null ? mapped.slice(1) : mapped}`;
|
|
108
|
+
};
|
|
109
|
+
const annotations = pack.annotations.map((a) => {
|
|
110
|
+
const method = parseAddress(a.at)?.method;
|
|
111
|
+
let args = a.args;
|
|
112
|
+
if (a.tag === "intent" && typeof a.args?.template === "string" && method) args = { ...a.args, template: mapTemplate(a.args.template, placeholder(method)) };
|
|
113
|
+
if (a.tag === "amount" && typeof a.args?.token === "string" && method) args = { ...a.args, token: placeholder(method)(a.args.token) ?? a.args.token };
|
|
114
|
+
return { ...a, at: at(a.at), args };
|
|
115
|
+
});
|
|
116
|
+
return { ...pack, annotations };
|
|
117
|
+
}
|