@meantjs/profile 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/LICENSE +202 -0
- package/README.md +277 -2
- package/bin/candid-profile.mjs +6 -0
- package/data/packs/erc20.profile.json +221 -0
- package/data/packs/icrc1.profile.json +275 -0
- package/data/packs/icrc2.profile.json +515 -0
- package/data/profile-v0.schema.json +1118 -0
- package/data/profile-v0.validate.mjs +12 -0
- package/package.json +77 -4
- package/src/abi-iface.mjs +112 -0
- package/src/abi-pack.mjs +114 -0
- package/src/abi.mjs +672 -0
- package/src/address.d.mts +30 -0
- package/src/address.mjs +62 -0
- package/src/analyze.mjs +344 -0
- package/src/browser.d.mts +212 -0
- package/src/browser.mjs +52 -0
- package/src/checks.mjs +154 -0
- package/src/cli.mjs +120 -0
- package/src/compile.mjs +304 -0
- package/src/doc-annotations.mjs +394 -0
- package/src/evm.d.mts +86 -0
- package/src/evm.mjs +28 -0
- package/src/idl.mjs +10 -0
- package/src/iface.mjs +123 -0
- package/src/index.d.mts +70 -0
- package/src/index.mjs +15 -0
- package/src/keccak.mjs +108 -0
- package/src/pack-match.mjs +19 -0
- package/src/packs.mjs +25 -0
- package/src/resolve.mjs +167 -0
- package/src/resolved.mjs +64 -0
- package/src/schema-core.mjs +45 -0
- package/src/schema.mjs +23 -0
- package/src/served.mjs +59 -0
- package/src/sha256.mjs +59 -0
- package/src/subtype.mjs +271 -0
- package/src/tags.mjs +90 -0
- package/src/util.mjs +16 -0
- package/src/validate.mjs +65 -0
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
*/
|
|
13
|
+
import { Iface } from "./iface.mjs";
|
|
14
|
+
import { idlHash } from "./idl.mjs";
|
|
15
|
+
import { AbiError, abiInterfaceId, componentKey, parseAbi, readAddress } from "./abi.mjs";
|
|
16
|
+
|
|
17
|
+
export class AbiIface extends Iface {
|
|
18
|
+
/** @param functions from parseAbi */
|
|
19
|
+
constructor(envelope, functions, label) {
|
|
20
|
+
super(envelope, label);
|
|
21
|
+
this.chain = "evm";
|
|
22
|
+
this.functions = new Map(functions.map((f) => [f.name, f]));
|
|
23
|
+
}
|
|
24
|
+
isBlob(ref) {
|
|
25
|
+
const p = this.prim(ref);
|
|
26
|
+
return p === "bytes" || /^bytes\d+$/.test(p ?? "");
|
|
27
|
+
}
|
|
28
|
+
isAccount() { return false; }
|
|
29
|
+
/** The canonical ABI type of a position: `uint256`, `address[]`, `(address,uint256)`. */
|
|
30
|
+
describeType(ref) {
|
|
31
|
+
const n = this.types[ref];
|
|
32
|
+
if (n.kind === "primitive") return n.primitive;
|
|
33
|
+
if (n.kind === "vec") return `${this.describeType(n.inner)}[${n.length ?? ""}]`;
|
|
34
|
+
return `(${n.fields.map((f) => this.describeType(f.type)).join(",")})`;
|
|
35
|
+
}
|
|
36
|
+
/** The `abi` of a resolved-form node: the primitive's type, `tuple` or `array`. */
|
|
37
|
+
abiType(ref) {
|
|
38
|
+
const n = this.types[ref];
|
|
39
|
+
return n.kind === "primitive" ? n.primitive : n.kind === "record" ? "tuple" : "array";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param entries an ABI: human-readable fragments or JSON ABI entries (parseAbi)
|
|
45
|
+
* @param label how diagnostics name it (`binds.abi`, `abi`)
|
|
46
|
+
* @returns {{ iface: AbiIface } | { failure: { at: string, message: string }[] }}
|
|
47
|
+
*/
|
|
48
|
+
export function abiInterface(entries, label = "abi", ifaceLabel = "contract") {
|
|
49
|
+
const { functions, errors } = parseAbi(entries, label);
|
|
50
|
+
if (errors.length) return { failure: errors };
|
|
51
|
+
if (functions.length === 0) return { failure: [{ at: label, message: "the ABI has no functions" }] };
|
|
52
|
+
const types = [];
|
|
53
|
+
const names = [];
|
|
54
|
+
const failure = [];
|
|
55
|
+
const add = (node) => types.push(node) - 1;
|
|
56
|
+
const build = (t, where) => {
|
|
57
|
+
if (t.kind === "tuple") {
|
|
58
|
+
const ref = add(null);
|
|
59
|
+
const ids = new Map();
|
|
60
|
+
const fields = t.components.map((c, i) => {
|
|
61
|
+
const id = c.name === null ? i : idlHash(c.name);
|
|
62
|
+
const key = componentKey(c, i);
|
|
63
|
+
if (ids.has(id)) failure.push({ at: where, message: `components ${ids.get(id)} and ${key} have the same field id ${id}; rename one` });
|
|
64
|
+
ids.set(id, key);
|
|
65
|
+
names.push([ref, id, key]);
|
|
66
|
+
return { id, type: build(c.type, `${where}.${key}`) };
|
|
67
|
+
});
|
|
68
|
+
types[ref] = { kind: "record", fields };
|
|
69
|
+
return ref;
|
|
70
|
+
}
|
|
71
|
+
if (t.kind === "array") {
|
|
72
|
+
const ref = add(null);
|
|
73
|
+
types[ref] = { kind: "vec", inner: build(t.element, `${where}[]`), length: t.length };
|
|
74
|
+
return ref;
|
|
75
|
+
}
|
|
76
|
+
return add({ kind: "primitive", primitive: t.kind === "uint" || t.kind === "int" ? `${t.kind}${t.bits}` : t.kind === "fixedbytes" ? `bytes${t.size}` : t.kind });
|
|
77
|
+
};
|
|
78
|
+
const methods = functions.map((f) => {
|
|
79
|
+
const args = f.inputs.length ? [build({ kind: "tuple", components: f.inputs }, `${f.name}(0)`)] : [];
|
|
80
|
+
const results = f.outputs.map((o, i) => build(o.type, `${f.name}->(${i})`));
|
|
81
|
+
const mode = f.stateMutability === "view" || f.stateMutability === "pure" ? "query" : "update";
|
|
82
|
+
return { name: f.name, function: add({ kind: "func", mode, args, results }) };
|
|
83
|
+
});
|
|
84
|
+
if (failure.length) return { failure };
|
|
85
|
+
const service = add({ kind: "service", methods });
|
|
86
|
+
const envelope = {
|
|
87
|
+
contract: { types, declarations: [], actor: { kind: "service", service }, identities: { interface: abiInterfaceId(functions) } },
|
|
88
|
+
extensions: { "org.candid-core.field-names/v1": names },
|
|
89
|
+
};
|
|
90
|
+
return { iface: new AbiIface(envelope, functions, ifaceLabel) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `binds.deployments` of a contract profile: an address in mixed case must match its EIP-55 checksum, and a deployment
|
|
95
|
+
* is listed once. `diag(severity, code, at, message)`.
|
|
96
|
+
*/
|
|
97
|
+
export function checkDeployments(doc, diag) {
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
(doc.binds.deployments ?? []).forEach((d, i) => {
|
|
100
|
+
const at = `binds.deployments[${i}]`;
|
|
101
|
+
let address;
|
|
102
|
+
try {
|
|
103
|
+
address = readAddress(d.address);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
if (!(e instanceof AbiError)) throw e;
|
|
106
|
+
return diag("error", "E_ADDRESS_CHECKSUM", `${at}.address`, e.message);
|
|
107
|
+
}
|
|
108
|
+
const key = `${d.chain_id}:${address}`;
|
|
109
|
+
if (seen.has(key)) diag("warning", "W_DUPLICATE", at, `chain ${d.chain_id} ${address} is listed twice`);
|
|
110
|
+
seen.add(key);
|
|
111
|
+
});
|
|
112
|
+
}
|
package/src/abi-pack.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
import { parseAddress, parseRelPath } from "./address.mjs";
|
|
11
|
+
import { componentKey, parseAbi, typeString } from "./abi.mjs";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param iface an AbiIface
|
|
15
|
+
* @param pack a schema-valid pack document with `binds.abi`
|
|
16
|
+
* @returns {{ matched: boolean, reasons: { at: string, message: string }[], relaxed: [], functions: object[] } | { error: { at: string, message: string }[] }}
|
|
17
|
+
*/
|
|
18
|
+
export function matchAbiPack(iface, pack) {
|
|
19
|
+
const { functions, errors } = parseAbi(pack.binds.abi, "binds.abi");
|
|
20
|
+
if (errors.length) return { error: errors };
|
|
21
|
+
const reasons = [];
|
|
22
|
+
const kind = (f) => (f.stateMutability === "view" || f.stateMutability === "pure" ? "a query (view or pure)" : f.stateMutability === "payable" ? "payable" : "not payable");
|
|
23
|
+
const outputs = (f) => `(${f.outputs.map((o) => typeString(o.type)).join(",")})`;
|
|
24
|
+
for (const f of functions) {
|
|
25
|
+
const g = iface.functions.get(f.name);
|
|
26
|
+
if (!g || g.selector !== f.selector) reasons.push({ at: f.name, message: `no function ${f.typeSignature} (selector ${f.selector})` });
|
|
27
|
+
else if (outputs(g) !== outputs(f)) reasons.push({ at: f.name, message: `returns ${outputs(g)}, the pack's ${outputs(f)}` });
|
|
28
|
+
else if (kind(g) !== kind(f)) reasons.push({ at: f.name, message: `is ${kind(g)}, the pack's ${kind(f)}` });
|
|
29
|
+
}
|
|
30
|
+
return { matched: reasons.length === 0, reasons, relaxed: [], functions };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A path through the pack's type, spelled with the contract's names; null when the pack's own type lacks it. */
|
|
34
|
+
function mapSegs(packT, ownT, segs) {
|
|
35
|
+
let p = packT;
|
|
36
|
+
let c = ownT;
|
|
37
|
+
let out = "";
|
|
38
|
+
for (const s of segs) {
|
|
39
|
+
if (s.kind === "elem") {
|
|
40
|
+
if (p.kind !== "array") return null;
|
|
41
|
+
out += "[]";
|
|
42
|
+
p = p.element;
|
|
43
|
+
c = c.element;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (p.kind !== "tuple") return null;
|
|
47
|
+
const i = p.components.findIndex((comp, j) => componentKey(comp, j) === s.label);
|
|
48
|
+
if (i < 0) return null;
|
|
49
|
+
out += `.${componentKey(c.components[i], i)}`;
|
|
50
|
+
p = p.components[i].type;
|
|
51
|
+
c = c.components[i].type;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const tupleOf = (params) => ({ kind: "tuple", components: params });
|
|
57
|
+
|
|
58
|
+
/** A template with each placeholder rewritten by `map` (null keeps it); escapes and literal text are kept as written. */
|
|
59
|
+
function mapTemplate(template, map) {
|
|
60
|
+
let out = "";
|
|
61
|
+
for (let i = 0; i < template.length; i++) {
|
|
62
|
+
const c = template[i];
|
|
63
|
+
if ((c === "{" || c === "}" || c === "[" || c === "]") && template[i + 1] === c) {
|
|
64
|
+
out += c + c;
|
|
65
|
+
i++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (c === "{") {
|
|
69
|
+
const end = template.indexOf("}", i);
|
|
70
|
+
if (end < 0) return template; // does not parse: left for E_TEMPLATE
|
|
71
|
+
const path = template.slice(i + 1, end);
|
|
72
|
+
out += `{${map(path) ?? path}}`;
|
|
73
|
+
i = end;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
out += c;
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The pack with its addresses and templates in the contract's names. `functions` are the pack's (matchAbiPack).
|
|
83
|
+
* A document whose names already agree comes back equal to the original.
|
|
84
|
+
*/
|
|
85
|
+
export function translatePack(pack, functions, iface) {
|
|
86
|
+
const mine = new Map(functions.map((f) => [f.name, f]));
|
|
87
|
+
const pair = (name) => (mine.has(name) && iface.functions.has(name) ? [mine.get(name), iface.functions.get(name)] : null);
|
|
88
|
+
const at = (text) => {
|
|
89
|
+
const addr = parseAddress(text);
|
|
90
|
+
const fs = addr && addr.kind !== "type" ? pair(addr.method) : null;
|
|
91
|
+
if (!fs || addr.kind === "method") return text;
|
|
92
|
+
const [p, c] = fs;
|
|
93
|
+
if (addr.kind === "arg") {
|
|
94
|
+
const mapped = addr.index === 0 && p.inputs.length ? mapSegs(tupleOf(p.inputs), tupleOf(c.inputs), addr.segs) : null;
|
|
95
|
+
return mapped === null ? text : `${addr.method}(0)${mapped}`;
|
|
96
|
+
}
|
|
97
|
+
const out = p.outputs[addr.index];
|
|
98
|
+
const mapped = out ? mapSegs(out.type, c.outputs[addr.index].type, addr.segs) : null;
|
|
99
|
+
return mapped === null ? text : `${addr.method}->(${addr.index})${mapped}`;
|
|
100
|
+
};
|
|
101
|
+
const placeholder = (method) => (path) => {
|
|
102
|
+
const fs = pair(method);
|
|
103
|
+
const rel = parseRelPath(path);
|
|
104
|
+
if (!fs || !rel || (rel.index ?? 0) !== 0 || !fs[0].inputs.length) return null;
|
|
105
|
+
const mapped = mapSegs(tupleOf(fs[0].inputs), tupleOf(fs[1].inputs), rel.segs);
|
|
106
|
+
return mapped === null ? null : `${rel.index === null ? "" : "(0)"}${rel.index === null ? mapped.slice(1) : mapped}`;
|
|
107
|
+
};
|
|
108
|
+
const annotations = pack.annotations.map((a) => {
|
|
109
|
+
const method = parseAddress(a.at)?.method;
|
|
110
|
+
const args = a.tag === "intent" && typeof a.args?.template === "string" && method ? { ...a.args, template: mapTemplate(a.args.template, placeholder(method)) } : a.args;
|
|
111
|
+
return { ...a, at: at(a.at), args };
|
|
112
|
+
});
|
|
113
|
+
return { ...pack, annotations };
|
|
114
|
+
}
|