@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/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
- /** One ERC-7730 field for a leaf of a function's parameters. */
95
- function fieldOf(node, path, label, ctx, where) {
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
- /** Every leaf of a viewed parameter tree, depth first in ABI order: its ERC-7730 path and its label. */
131
- function leaves(node, keys, out) {
132
- if (node.candid === "record") for (const [k, child] of entriesOf(node.fields)) leaves(child, [...keys, k], out);
133
- else if (node.candid === "vec") leaves(node.element, [...keys, "[]"], out);
134
- else out.push({ node, path: keys.join("."), label: labelOf(node) ?? [...keys].reverse().find((k) => k !== "[]") });
135
- return out;
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
- /** The template as an ERC-7730 interpolatedIntent: `{(0).to}` reads `{to}`, `[[` and `]]` are plain brackets. */
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 ctx.problem(where, e.message);
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 ctx.problem(where, "an optional [ ] segment has no ERC-7730 form");
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 ctx.problem(where, `{${p.path}} reads every element of an array; an ERC-7730 interpolation shows one value`);
169
- if (rp.node.candid === "record" || rp.node.candid === "vec") return ctx.problem(where, `{${p.path}} is a whole ${rp.node.abi}; an ERC-7730 interpolation shows one formatted field`);
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
- * The ERC-7730 file of an EVM contract's resolved form, and its ERC-8176 `descriptorHash`.
185
- *
186
- * @param {object} resolved an EVM contract's resolved form (`chain: "evm"`)
187
- * @param {object} [options]
188
- * @param {{ chainId: number, address: string }[]} [options.deployments] default: the deployments every profile lists
189
- * @param {string} [options.owner] ERC-7730 `metadata.owner`
190
- * @param {string} [options.contractName] `metadata.contractName`
191
- * @param {{ url: string, deploymentDate?: string }} [options.info] `metadata.info`
192
- * @param {{ name: string }} [options.token] the contract is the token its literal amounts are in: `metadata.token`
193
- * @returns {{ descriptor: object, descriptorHash: string }}
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 intent = method.tags?.intent?.args;
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 descriptor = { $schema: ERC7730_SCHEMA_URL, context: { contract: { deployments } } };
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;