@meantjs/consent 0.2.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 +32 -0
- package/README.md +64 -8
- package/package.json +7 -3
- package/src/consent.mjs +12 -4
- package/src/display.mjs +51 -7
- package/src/erc7730.d.mts +15 -4
- package/src/erc7730.mjs +161 -46
- package/src/errors.mjs +2 -0
- package/src/evm.d.mts +64 -1
- package/src/evm.mjs +244 -7
- package/src/grant.mjs +140 -0
- package/src/index.d.mts +29 -0
- package/src/sha256.mjs +60 -0
- package/src/terms.d.mts +103 -0
- package/src/terms.mjs +500 -0
package/src/erc7730.mjs
CHANGED
|
@@ -19,6 +19,17 @@
|
|
|
19
19
|
// The descriptor binds to deployments (ERC-7730 `context.contract.deployments`): the profile's, or the caller's when
|
|
20
20
|
// every profile that lists deployments lists them too. `effect`, `idempotency`, `outcome` and the other agent tags
|
|
21
21
|
// have no ERC-7730 form and are left out: the file carries what a call says, not what it does.
|
|
22
|
+
//
|
|
23
|
+
// EIP-712 messages (spec section 15.9) are a file of their own (`options.context: "eip712"`): ERC-7730 binds a file to
|
|
24
|
+
// a contract or to messages, not both. Its formats are keyed by each message's `encode_type`, whose Keccak-256 must be
|
|
25
|
+
// the message's `type_hash`; `context.eip712` carries the deployments and the domain name and version the profiles
|
|
26
|
+
// give. An amount whose token a field names is `tokenAmount` with that field's path, inside a field group of the array
|
|
27
|
+
// the two share, as ERC-7730's PermitBatch example writes it.
|
|
28
|
+
//
|
|
29
|
+
// Some words a profile adds have no ERC-7730 form but change no value: a timestamp's `threshold`, `message` and
|
|
30
|
+
// `labels` (written as a plain date), and an intent template ERC-7730 cannot interpolate (an array, a whole tuple, a
|
|
31
|
+
// [ ] segment; `interpolatedIntent` is optional and a wallet falls back to `intent`). They are left out, and each is
|
|
32
|
+
// returned as a note, instead of refusing the file.
|
|
22
33
|
|
|
23
34
|
import { AbiError, jcs, keccak256Hex, parseSignature, readAddress, typedDataDigest } from "@meantjs/profile/evm";
|
|
24
35
|
import { CODES, fail } from "./errors.mjs";
|
|
@@ -35,7 +46,7 @@ export const EAS_ADDRESS = "0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587";
|
|
|
35
46
|
|
|
36
47
|
const ZERO32 = `0x${"0".repeat(64)}`;
|
|
37
48
|
const ZERO_ADDRESS = `0x${"0".repeat(40)}`;
|
|
38
|
-
const OPTIONS = new Set(["deployments", "owner", "contractName", "info", "token"]);
|
|
49
|
+
const OPTIONS = new Set(["context", "deployments", "owner", "contractName", "info", "token"]);
|
|
39
50
|
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
40
51
|
const nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
|
|
41
52
|
|
|
@@ -91,10 +102,32 @@ function erc20Metadata(resolved) {
|
|
|
91
102
|
return isObject(d) && isObject(s) && d.mode === "query" && s.mode === "query" && d.signature === "decimals()" && s.signature === "symbol()" && /^uint\d+$/.test(d.results?.[0]?.abi ?? "") && s.results?.[0]?.abi === "string" && d.results.length === 1 && s.results.length === 1;
|
|
92
103
|
}
|
|
93
104
|
|
|
94
|
-
/**
|
|
95
|
-
function
|
|
105
|
+
/** The keys of a placeholder or token path through a viewed tree (`["details", "[]", "token"]`), or null. */
|
|
106
|
+
function keysOf(method, relpath) {
|
|
107
|
+
const rp = resolvePath(method, relpath);
|
|
108
|
+
return rp.error ? null : rp.steps.map((st) => (st.kind === "elem" ? "[]" : st.key));
|
|
109
|
+
}
|
|
110
|
+
const startsWith = (keys, prefix) => prefix.length <= keys.length && prefix.every((k, i) => keys[i] === k);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* One ERC-7730 field for a leaf of a function's parameters or a message's members. `at` is where the leaf is:
|
|
114
|
+
* `{ method, abs, base }`, its keys from the root and the keys of the field group it is in (its path is relative to it).
|
|
115
|
+
*/
|
|
116
|
+
function fieldOf(node, path, label, ctx, where, at) {
|
|
96
117
|
const field = { path, label };
|
|
97
118
|
const amount = tagArgs(node, "amount");
|
|
119
|
+
if (amount?.token !== undefined) {
|
|
120
|
+
// In the token a field of the same value names (spec section 15.9): its path, relative to the group.
|
|
121
|
+
const keys = keysOf(at.method, String(amount.token));
|
|
122
|
+
if (keys === null) return ctx.problem(where, `amount.token ${JSON.stringify(amount.token)} does not resolve`);
|
|
123
|
+
if (!startsWith(keys, at.base)) return ctx.problem(where, "the amount is in an array group and its token is outside it; this export writes no path that leaves a group");
|
|
124
|
+
const params = { tokenPath: keys.slice(at.base.length).join(".") };
|
|
125
|
+
if (amount.threshold !== undefined) {
|
|
126
|
+
params.threshold = `0x${BigInt(amount.threshold).toString(16)}`;
|
|
127
|
+
params.message = amount.message ?? "Unlimited";
|
|
128
|
+
}
|
|
129
|
+
return { ...field, format: "tokenAmount", params };
|
|
130
|
+
}
|
|
98
131
|
if (amount !== undefined) {
|
|
99
132
|
const dynamic = amount.decimals?.from === "query:decimals" && amount.symbol?.from === "query:symbol";
|
|
100
133
|
const literal = Number.isInteger(amount.decimals) && typeof amount.symbol === "string";
|
|
@@ -120,6 +153,8 @@ function fieldOf(node, path, label, ctx, where) {
|
|
|
120
153
|
if (time !== undefined) {
|
|
121
154
|
const kind = tagArgs(node, "timestamp") !== undefined ? "timestamp" : "duration";
|
|
122
155
|
if (time.unit !== "s") return ctx.problem(where, `${kind} in ${time.unit}: ERC-7730 ${kind === "timestamp" ? "dates" : "durations"} are in seconds`);
|
|
156
|
+
const words = ["threshold", "labels"].filter((k) => time[k] !== undefined);
|
|
157
|
+
if (kind === "timestamp" && words.length) ctx.note(where, `the timestamp's ${words.join(" and ")} ha${words.length > 1 ? "ve" : "s"} no ERC-7730 date form and ${words.length > 1 ? "are" : "is"} left out; the file shows the date`);
|
|
123
158
|
return kind === "timestamp" ? { ...field, format: "date", params: { encoding: "timestamp" } } : { ...field, format: "duration" };
|
|
124
159
|
}
|
|
125
160
|
const bytes = tagArgs(node, "bytes");
|
|
@@ -127,12 +162,30 @@ function fieldOf(node, path, label, ctx, where) {
|
|
|
127
162
|
return { ...field, format: node.abi === "address" ? "addressName" : "raw" };
|
|
128
163
|
}
|
|
129
164
|
|
|
130
|
-
/**
|
|
131
|
-
function
|
|
132
|
-
if (node.candid === "record")
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
165
|
+
/** Whether a subtree holds an amount whose token path starts with `prefix` (it is in the same array element). */
|
|
166
|
+
function tokenInside(node, method, prefix) {
|
|
167
|
+
if (node.candid === "record") return entriesOf(node.fields).some(([, child]) => tokenInside(child, method, prefix));
|
|
168
|
+
if (node.candid === "vec") return tokenInside(node.element, method, prefix);
|
|
169
|
+
const token = tagArgs(node, "amount")?.token;
|
|
170
|
+
const keys = token === undefined ? null : keysOf(method, String(token));
|
|
171
|
+
return keys !== null && startsWith(keys, prefix);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The ERC-7730 fields of a viewed parameter tree, depth first in ABI order. An array whose elements hold an amount
|
|
176
|
+
* whose token is in the same element is a field group (`{ path: "details.[]", fields }`), its paths relative to the
|
|
177
|
+
* element, as ERC-7730's PermitBatch example writes it; any other array is flattened (`legs.[].to`).
|
|
178
|
+
*/
|
|
179
|
+
function fieldsOf(node, abs, rel, ctx, method, name) {
|
|
180
|
+
if (node.candid === "record") return entriesOf(node.fields).flatMap(([k, child]) => fieldsOf(child, [...abs, k], [...rel, k], ctx, method, name));
|
|
181
|
+
if (node.candid === "vec") {
|
|
182
|
+
const element = [...abs, "[]"];
|
|
183
|
+
if (tokenInside(node.element, method, element)) return [{ path: [...rel, "[]"].join("."), fields: fieldsOf(node.element, element, [], ctx, method, name) }];
|
|
184
|
+
return fieldsOf(node.element, element, [...rel, "[]"], ctx, method, name);
|
|
185
|
+
}
|
|
186
|
+
const label = labelOf(node) ?? [...abs].reverse().find((k) => k !== "[]");
|
|
187
|
+
const where = `${name}(0).${abs.join(".").replaceAll(".[]", "[]")}`;
|
|
188
|
+
return [fieldOf(node, rel.join("."), label, ctx, where, { method, abs, base: abs.slice(0, abs.length - rel.length) })];
|
|
136
189
|
}
|
|
137
190
|
|
|
138
191
|
/** A parameter or tuple component without a name: an ERC-7730 key names every one, and paths need the names. */
|
|
@@ -150,23 +203,28 @@ function unnamed(params, at = "") {
|
|
|
150
203
|
return null;
|
|
151
204
|
}
|
|
152
205
|
|
|
153
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* The template as an ERC-7730 interpolatedIntent: `{(0).to}` reads `{to}`, `[[` and `]]` are plain brackets. A
|
|
208
|
+
* template ERC-7730 cannot interpolate gets none, and a note: `interpolatedIntent` is optional, and `intent` and the
|
|
209
|
+
* fields still say everything.
|
|
210
|
+
*/
|
|
154
211
|
function interpolated(template, method, ctx, where) {
|
|
212
|
+
const leftOut = (why) => ctx.note(where, `${why}; interpolatedIntent is left out`);
|
|
155
213
|
let parts;
|
|
156
214
|
try {
|
|
157
215
|
parts = parseTemplate(template);
|
|
158
216
|
} catch (e) {
|
|
159
|
-
return
|
|
217
|
+
return leftOut(e.message);
|
|
160
218
|
}
|
|
161
219
|
let out = "";
|
|
162
220
|
for (const p of parts) {
|
|
163
221
|
if (p.t === "lit") out += p.text.replaceAll("{", "{{").replaceAll("}", "}}");
|
|
164
|
-
else if (p.t === "opt") return
|
|
222
|
+
else if (p.t === "opt") return leftOut("an optional [ ] segment has no ERC-7730 form");
|
|
165
223
|
else {
|
|
166
224
|
const rp = resolvePath(method, p.path);
|
|
167
225
|
if (rp.error) return ctx.problem(where, `{${p.path}}: ${rp.error}`);
|
|
168
|
-
if (rp.steps.some((s) => s.kind === "elem")) return
|
|
169
|
-
if (rp.node.candid === "record" || rp.node.candid === "vec") return
|
|
226
|
+
if (rp.steps.some((s) => s.kind === "elem")) return leftOut(`{${p.path}} reads every element of an array; an ERC-7730 interpolation shows one value`);
|
|
227
|
+
if (rp.node.candid === "record" || rp.node.candid === "vec") return leftOut(`{${p.path}} is a whole ${rp.node.abi}; an ERC-7730 interpolation shows one formatted field`);
|
|
170
228
|
out += `{${rp.steps.map((s) => s.key).join(".")}}`;
|
|
171
229
|
}
|
|
172
230
|
}
|
|
@@ -180,29 +238,20 @@ function checkText(v, where) {
|
|
|
180
238
|
else if (isObject(v)) for (const [k, x] of Object.entries(v)) checkText(k, where), checkText(x, `${where}.${k}`);
|
|
181
239
|
}
|
|
182
240
|
|
|
183
|
-
/**
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
*/
|
|
195
|
-
export function erc7730Descriptor(resolved, options = {}) {
|
|
196
|
-
if (!isObject(resolved) || resolved.format !== "candid-profile-resolved" || !isObject(resolved.methods) || resolved.chain !== "evm") methodOf(resolved, "", "evm"); // throws the right error
|
|
197
|
-
if (!isObject(options)) fail(CODES.REQUEST, "options must be an object");
|
|
198
|
-
for (const k of Object.keys(options)) if (!OPTIONS.has(k)) fail(CODES.REQUEST, `unknown option ${JSON.stringify(k)}; expected one of ${[...OPTIONS].join(", ")}`);
|
|
199
|
-
if (options.token !== undefined && !(isObject(options.token) && nonEmpty(options.token.name) && Object.keys(options.token).length === 1)) fail(CODES.REQUEST, "options.token must be { name } with a non-empty string");
|
|
200
|
-
|
|
201
|
-
const problems = [];
|
|
202
|
-
const ctx = { erc20: erc20Metadata(resolved), tokenName: options.token?.name, token: null, dynamic: false, problem: (at, message) => void problems.push(`${at}: ${message}`) };
|
|
203
|
-
const deployments = deploymentsOf(resolved, options.deployments);
|
|
204
|
-
const metadata = metadataOf(options);
|
|
241
|
+
/** One ERC-7730 format entry: the intent's title (else `describe.label`, else `fallback`), its template, the fields. */
|
|
242
|
+
function entryOf(name, method, viewed, fallback, ctx) {
|
|
243
|
+
const intent = method.tags?.intent?.args;
|
|
244
|
+
const entry = { intent: nonEmpty(intent?.title) ? intent.title : (labelOf(method) ?? fallback) };
|
|
245
|
+
if (typeof intent?.template === "string") {
|
|
246
|
+
const text = interpolated(intent.template, viewed, ctx, `${name}: intent template`);
|
|
247
|
+
if (text !== undefined) entry.interpolatedIntent = text;
|
|
248
|
+
}
|
|
249
|
+
const fields = viewed.args.length ? fieldsOf(viewed.args[0], [], [], ctx, viewed, name) : [];
|
|
250
|
+
return { entry, fields };
|
|
251
|
+
}
|
|
205
252
|
|
|
253
|
+
/** The formats of every function a transaction calls, keyed by its named signature. */
|
|
254
|
+
function functionFormats(resolved, ctx) {
|
|
206
255
|
const formats = {};
|
|
207
256
|
for (const [name, m] of entriesOf(resolved.methods)) {
|
|
208
257
|
if (!isObject(m) || m.mode !== "update") continue;
|
|
@@ -221,26 +270,92 @@ export function erc7730Descriptor(resolved, options = {}) {
|
|
|
221
270
|
continue;
|
|
222
271
|
}
|
|
223
272
|
const viewed = { ...method, args: method.args.map((n, i) => abiView(n, `${name}(${i})`)) };
|
|
224
|
-
const
|
|
225
|
-
const entry = { intent: nonEmpty(intent?.title) ? intent.title : (labelOf(method) ?? `Call ${name}`) };
|
|
226
|
-
if (typeof intent?.template === "string") {
|
|
227
|
-
const text = interpolated(intent.template, viewed, ctx, `${name}: intent template`);
|
|
228
|
-
if (text !== undefined) entry.interpolatedIntent = text;
|
|
229
|
-
}
|
|
230
|
-
const fields = viewed.args.length ? leaves(viewed.args[0], [], []).map((l) => fieldOf(l.node, l.path, l.label, ctx, `${name}(0).${l.path.replaceAll(".[]", "[]")}`)) : [];
|
|
273
|
+
const { entry, fields } = entryOf(name, method, viewed, `Call ${name}`, ctx);
|
|
231
274
|
if (method.state_mutability === "payable") fields.push({ path: "@.value", label: "Value sent", format: "amount" });
|
|
232
275
|
if (fields.length) entry.fields = fields;
|
|
233
276
|
formats[method.signature] = entry;
|
|
234
277
|
}
|
|
278
|
+
if (Object.keys(formats).length === 0) ctx.problem("functions", "the contract has no function a transaction calls (every function is view or pure)");
|
|
279
|
+
return formats;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The formats of every EIP-712 message (spec section 15.9), keyed by its encode_type, which must hash to its type_hash. */
|
|
283
|
+
function messageFormats(resolved, ctx) {
|
|
284
|
+
const formats = {};
|
|
285
|
+
for (const [name, m] of entriesOf(resolved.messages)) {
|
|
286
|
+
if (!isObject(m) || typeof m.encode_type !== "string" || !m.encode_type.startsWith(`${name}(`) || `0x${keccak256Hex(m.encode_type)}` !== m.type_hash) {
|
|
287
|
+
fail(CODES.RESOLVED_FORM, `messages.${name}: its encode_type does not hash to its type_hash, or names another type`);
|
|
288
|
+
}
|
|
289
|
+
if (!Array.isArray(m.args) || m.args.length !== 1) fail(CODES.RESOLVED_FORM, `messages.${name} has no args array of one struct`);
|
|
290
|
+
const viewed = { ...m, args: m.args.map((n, i) => abiView(n, `${name}(${i})`)) };
|
|
291
|
+
const { entry, fields } = entryOf(name, m, viewed, `Sign ${name}`, ctx);
|
|
292
|
+
if (fields.length) entry.fields = fields;
|
|
293
|
+
formats[m.encode_type] = entry;
|
|
294
|
+
}
|
|
295
|
+
if (Object.keys(formats).length === 0) ctx.problem("messages", "the resolved form describes no EIP-712 message");
|
|
296
|
+
return formats;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** ERC-7730 `context.eip712.domain`: the name and version every profile that gives one gives. */
|
|
300
|
+
function domainOf(resolved, ctx) {
|
|
301
|
+
const domain = {};
|
|
302
|
+
for (const p of Array.isArray(resolved.profiles) ? resolved.profiles : []) {
|
|
303
|
+
if (!isObject(p?.eip712_domain)) continue;
|
|
304
|
+
for (const [k, v] of Object.entries(p.eip712_domain)) {
|
|
305
|
+
if ((k !== "name" && k !== "version") || typeof v !== "string") fail(CODES.RESOLVED_FORM, `profile ${p.name}: eip712_domain has ${JSON.stringify(k)}; only a name and a version are bound by a profile`);
|
|
306
|
+
if (Object.hasOwn(domain, k) && domain[k] !== v) ctx.problem("domain", `profiles give the domain two ${k}s, ${JSON.stringify(domain[k])} and ${JSON.stringify(v)}`);
|
|
307
|
+
domain[k] = v;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return domain;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The ERC-7730 file of an EVM contract's resolved form, and its ERC-8176 `descriptorHash`.
|
|
315
|
+
*
|
|
316
|
+
* @param {object} resolved an EVM contract's resolved form (`chain: "evm"`)
|
|
317
|
+
* @param {object} [options]
|
|
318
|
+
* @param {"contract" | "eip712"} [options.context] the functions a transaction calls (default), or the EIP-712
|
|
319
|
+
* messages (spec section 15.9): ERC-7730 binds one file to one of the two
|
|
320
|
+
* @param {{ chainId: number, address: string }[]} [options.deployments] default: the deployments every profile lists
|
|
321
|
+
* @param {string} [options.owner] ERC-7730 `metadata.owner`
|
|
322
|
+
* @param {string} [options.contractName] `metadata.contractName`
|
|
323
|
+
* @param {{ url: string, deploymentDate?: string }} [options.info] `metadata.info`
|
|
324
|
+
* @param {{ name: string }} [options.token] the contract is the token its literal amounts are in: `metadata.token`
|
|
325
|
+
* @returns {{ descriptor: object, descriptorHash: string, notes: string[] }} `notes`: what was left out because a
|
|
326
|
+
* file cannot say it, though the file still shows every value (a timestamp's words, an interpolatedIntent)
|
|
327
|
+
*/
|
|
328
|
+
export function erc7730Descriptor(resolved, options = {}) {
|
|
329
|
+
if (!isObject(resolved) || resolved.format !== "candid-profile-resolved" || !isObject(resolved.methods) || resolved.chain !== "evm") methodOf(resolved, "", "evm"); // throws the right error
|
|
330
|
+
if (!isObject(options)) fail(CODES.REQUEST, "options must be an object");
|
|
331
|
+
for (const k of Object.keys(options)) if (!OPTIONS.has(k)) fail(CODES.REQUEST, `unknown option ${JSON.stringify(k)}; expected one of ${[...OPTIONS].join(", ")}`);
|
|
332
|
+
if (options.token !== undefined && !(isObject(options.token) && nonEmpty(options.token.name) && Object.keys(options.token).length === 1)) fail(CODES.REQUEST, "options.token must be { name } with a non-empty string");
|
|
333
|
+
const context = options.context ?? "contract";
|
|
334
|
+
if (context !== "contract" && context !== "eip712") fail(CODES.REQUEST, `options.context must be "contract" or "eip712", got ${JSON.stringify(options.context)}`);
|
|
335
|
+
|
|
336
|
+
const problems = [];
|
|
337
|
+
const notes = [];
|
|
338
|
+
const ctx = {
|
|
339
|
+
erc20: erc20Metadata(resolved),
|
|
340
|
+
tokenName: options.token?.name,
|
|
341
|
+
token: null,
|
|
342
|
+
dynamic: false,
|
|
343
|
+
problem: (at, message) => void problems.push(`${at}: ${message}`),
|
|
344
|
+
note: (at, message) => void notes.push(`${at}: ${message}`),
|
|
345
|
+
};
|
|
346
|
+
const deployments = deploymentsOf(resolved, options.deployments);
|
|
347
|
+
const metadata = metadataOf(options);
|
|
348
|
+
const formats = context === "eip712" ? messageFormats(resolved, ctx) : functionFormats(resolved, ctx);
|
|
349
|
+
const domain = context === "eip712" ? domainOf(resolved, ctx) : {};
|
|
235
350
|
if (ctx.dynamic && ctx.token) problems.push("amounts: some read decimals() and symbol(), others are literal; an ERC-7730 wallet reads one token for @.to");
|
|
236
351
|
if (problems.length) fail(CODES.EXPORT, `no ERC-7730 file: ${problems.join("; ")}`);
|
|
237
|
-
if (Object.keys(formats).length === 0) fail(CODES.EXPORT, "no ERC-7730 file: the contract has no function a transaction calls (every function is view or pure)");
|
|
238
352
|
|
|
239
353
|
if (ctx.token) metadata.token = { name: ctx.tokenName, ticker: ctx.token.ticker, decimals: ctx.token.decimals };
|
|
240
|
-
const
|
|
354
|
+
const bound = context === "eip712" ? { eip712: { ...(Object.keys(domain).length ? { domain } : {}), deployments } } : { contract: { deployments } };
|
|
355
|
+
const descriptor = { $schema: ERC7730_SCHEMA_URL, context: bound };
|
|
241
356
|
if (Object.keys(metadata).length) descriptor.metadata = metadata;
|
|
242
357
|
descriptor.display = { formats };
|
|
243
|
-
return { descriptor, descriptorHash: descriptorHash(descriptor) };
|
|
358
|
+
return { descriptor, descriptorHash: descriptorHash(descriptor), notes };
|
|
244
359
|
}
|
|
245
360
|
|
|
246
361
|
/** ERC-8176 `descriptorHash`: Keccak-256 of the RFC 8785 (JCS) form of a descriptor without `includes`. */
|
package/src/errors.mjs
CHANGED
|
@@ -22,6 +22,8 @@ export const CODES = Object.freeze({
|
|
|
22
22
|
LANGUAGE: "E_LANGUAGE", // only with languagePolicy "error": the requested language is not supported
|
|
23
23
|
REQUEST: "E_REQUEST", // the ICRC-21 request itself is malformed
|
|
24
24
|
DEPLOYMENT: "E_DEPLOYMENT", // an EVM call's target is not a deployment its profile lists (spec section 15)
|
|
25
|
+
DOMAIN: "E_DOMAIN", // an EIP-712 request's domain is for another chain, contract or name than its profile's (spec section 15.9)
|
|
26
|
+
TYPE_HASH: "E_TYPE_HASH", // no message of the resolved form has the request's EIP-712 type hash (spec section 15.9)
|
|
25
27
|
// this generator cannot describe the call (ICRC-21 ConsentMessageUnavailable)
|
|
26
28
|
UNSUPPORTED_TYPE: "E_UNSUPPORTED_TYPE", // a position the resolved form cannot type (recursive cut, func, service)
|
|
27
29
|
// the profile or the caller's context is wrong (ICRC-21 GenericError)
|
package/src/evm.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Types for @meantjs/consent/evm: the consent text of an EVM call (spec section 15, D-054). Hand-written; the source is
|
|
2
2
|
// src/evm.mjs. scripts/types.test.mjs keeps the declared names equal to the runtime exports.
|
|
3
3
|
import type { AbiLimits, AbiValue } from "@meantjs/profile/evm";
|
|
4
|
-
import type { ConsentDiagnostic, Model, Provenance, ResolvedForm } from "./index.mjs";
|
|
4
|
+
import type { ConsentDiagnostic, Grant, Model, Provenance, ResolvedForm } from "./index.mjs";
|
|
5
5
|
|
|
6
6
|
/** An EVM contract's resolved form: `chain: "evm"`, and each method with its selector and named signature. */
|
|
7
7
|
export type EvmResolvedForm = ResolvedForm & { chain: "evm" };
|
|
@@ -23,6 +23,11 @@ export interface EvmConsentInput {
|
|
|
23
23
|
chainId?: number | bigint | string;
|
|
24
24
|
/** Resolved dynamic refs, keyed by `from`: `{ "query:decimals": 6, "query:symbol": "USDC" }`. */
|
|
25
25
|
refs?: Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* For an amount whose token a field names (spec section 15.9): each such token's `decimals()` and `symbol()`, keyed by
|
|
28
|
+
* its address. An amount in a token not listed is E_REF_UNRESOLVED; it is shown beside its token's address.
|
|
29
|
+
*/
|
|
30
|
+
tokens?: Record<string, { decimals: number | bigint | string; symbol?: string }>;
|
|
26
31
|
/** Default "generic" (Markdown). */
|
|
27
32
|
form?: "generic" | "fields";
|
|
28
33
|
/** Generic form only: show timestamps in this zone (default UTC). */
|
|
@@ -45,6 +50,64 @@ export function evmConsent(input: EvmConsentInput): {
|
|
|
45
50
|
/** The contract, in its EIP-55 form. */
|
|
46
51
|
to: string;
|
|
47
52
|
chainId: string | null;
|
|
53
|
+
/** The decoded arguments by parameter name, as `decodeCalldata` gives them. */
|
|
54
|
+
values: Record<string, AbiValue>;
|
|
55
|
+
/** What the call grants (its token is the contract called, unless the amount's tag names a field), or null; a W_GRANT diagnostic says why one cannot be read. */
|
|
56
|
+
grant: Grant | null;
|
|
57
|
+
diagnostics: ConsentDiagnostic[];
|
|
58
|
+
provenance: Provenance;
|
|
59
|
+
model: Model;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** An EIP-712 signing request, as `eth_signTypedData_v4` takes it. `types.EIP712Domain` may be left out. */
|
|
63
|
+
export interface TypedData {
|
|
64
|
+
types: Record<string, { name: string; type: string }[]>;
|
|
65
|
+
primaryType: string;
|
|
66
|
+
domain: { name?: string; version?: string; chainId?: number | bigint | string; verifyingContract?: string; salt?: string };
|
|
67
|
+
message: Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
export interface Eip712ConsentInput {
|
|
70
|
+
/** An EVM contract's resolved form with `messages` (spec section 15.9). */
|
|
71
|
+
resolved: ResolvedForm;
|
|
72
|
+
typedData: TypedData;
|
|
73
|
+
/** The wallet's active chain: a domain for another chain is refused (E_DOMAIN). */
|
|
74
|
+
chainId: number | bigint | string;
|
|
75
|
+
/** Resolved dynamic refs; `query:` refs are calls to the verifying contract. */
|
|
76
|
+
refs?: Record<string, unknown>;
|
|
77
|
+
/** For an amount whose token a member names: each token's `decimals()` and `symbol()`, keyed by its address. */
|
|
78
|
+
tokens?: Record<string, { decimals: number | bigint | string; symbol?: string }>;
|
|
79
|
+
/** Default "generic" (Markdown). */
|
|
80
|
+
form?: "generic" | "fields";
|
|
81
|
+
/** Generic form only: show timestamps in this zone (default UTC). */
|
|
82
|
+
utcOffsetMinutes?: number | null;
|
|
83
|
+
/** Append the message's values (spec section 11 item 1). */
|
|
84
|
+
rawArguments?: boolean;
|
|
85
|
+
limits?: { maxBytes?: number; maxElements?: number };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The consent text of an EIP-712 signing request (spec section 15.9): the message a profile describes, matched by its
|
|
89
|
+
* type hash, in words, ending with the verifying contract, the digest and what a signature is. Refused before anything
|
|
90
|
+
* is shown: a domain for another chain, without a chainId or verifyingContract, with a salt, or whose name or version
|
|
91
|
+
* is not its profiles' (E_DOMAIN); a contract its profiles do not list (E_DEPLOYMENT); no message with the request's
|
|
92
|
+
* type hash (E_TYPE_HASH); values that are not exactly the members, canonically written (E_DECODE); an amount in a
|
|
93
|
+
* token not in `tokens` (E_REF_UNRESOLVED).
|
|
94
|
+
*/
|
|
95
|
+
export function eip712Consent(input: Eip712ConsentInput): {
|
|
96
|
+
form: "generic" | "fields";
|
|
97
|
+
/** Markdown for "generic"; the intent line and `[label, text]` rows for "fields". */
|
|
98
|
+
message: string | { intent: string; fields: [string, string][] };
|
|
99
|
+
primaryType: string;
|
|
100
|
+
/** `0x` and 64 hex digits. */
|
|
101
|
+
typeHash: string;
|
|
102
|
+
/** The EIP-712 digest the signature signs: `0x` and 64 hex digits. */
|
|
103
|
+
digest: string;
|
|
104
|
+
/** The verifying contract, in its EIP-55 form. */
|
|
105
|
+
verifyingContract: string;
|
|
106
|
+
chainId: string;
|
|
107
|
+
/** The message's values by member name, as read from the typed data: integers as decimal strings, addresses in EIP-55 form. */
|
|
108
|
+
values: Record<string, AbiValue>;
|
|
109
|
+
/** What the signature grants (its token is the verifying contract, unless the amount's tag names a member), or null; a W_GRANT diagnostic says why one cannot be read. */
|
|
110
|
+
grant: Grant | null;
|
|
48
111
|
diagnostics: ConsentDiagnostic[];
|
|
49
112
|
provenance: Provenance;
|
|
50
113
|
model: Model;
|