@tollxyz/guard 0.3.0 → 0.4.1

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/dist/store.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Where the guard keeps its spend counters and the x402 payment ids it has paid. The default MemorySpendStore lives in
3
+ * this process: it resets on restart and is not shared between processes. Pass your own SpendStore (Redis, a database)
4
+ * for counters that survive restarts or span several workers.
5
+ */
6
+ /** the start of the UTC day holding `t` (ms) */
7
+ export const dayStart = (t) => t - (((t % 86_400_000) + 86_400_000) % 86_400_000);
8
+ /** an in-process SpendStore: resets on restart, not shared between processes */
9
+ export class MemorySpendStore {
10
+ buckets = new Map();
11
+ where = new Map();
12
+ paid = new Map();
13
+ now;
14
+ seq = 0;
15
+ constructor(o = {}) {
16
+ this.now = o.now ?? Date.now;
17
+ }
18
+ prune(windowStart) {
19
+ // windows older than the previous day are dropped
20
+ for (const b of this.buckets.keys()) {
21
+ const ws = Number(b.slice(b.lastIndexOf("@") + 1));
22
+ if (ws < windowStart - 86_400_000)
23
+ this.buckets.delete(b);
24
+ }
25
+ }
26
+ async reserve(k, amt, limit, windowStart) {
27
+ // no await before the check and the write: atomic within this process
28
+ const bk = `${k}@${windowStart}`;
29
+ let b = this.buckets.get(bk);
30
+ if (!b) {
31
+ this.prune(windowStart);
32
+ b = { committed: 0n, pending: new Map() };
33
+ this.buckets.set(bk, b);
34
+ }
35
+ let used = b.committed;
36
+ for (const v of b.pending.values())
37
+ used += v;
38
+ if (amt < 0n || used + amt > limit)
39
+ return { ok: false, id: "", used };
40
+ const id = `r${++this.seq}`;
41
+ b.pending.set(id, amt);
42
+ this.where.set(id, bk);
43
+ return { ok: true, id, used: used + amt };
44
+ }
45
+ async commit(id) {
46
+ const bk = this.where.get(id);
47
+ const b = bk ? this.buckets.get(bk) : undefined;
48
+ this.where.delete(id);
49
+ const v = b?.pending.get(id);
50
+ if (!b || v === undefined)
51
+ return;
52
+ b.pending.delete(id);
53
+ b.committed += v;
54
+ }
55
+ async release(id) {
56
+ const bk = this.where.get(id);
57
+ this.where.delete(id);
58
+ if (bk)
59
+ this.buckets.get(bk)?.pending.delete(id);
60
+ }
61
+ async paymentSeen(id) {
62
+ const until = this.paid.get(id);
63
+ if (until === undefined)
64
+ return false;
65
+ if (until <= this.now()) {
66
+ this.paid.delete(id);
67
+ return false;
68
+ }
69
+ return true;
70
+ }
71
+ async markPaid(id, ttlS) {
72
+ const t = this.now();
73
+ for (const [k, until] of this.paid)
74
+ if (until <= t)
75
+ this.paid.delete(k);
76
+ this.paid.set(id, t + ttlS * 1000);
77
+ }
78
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 0.4.1: Toll's single-use ALLOW ticket. Ask for one with the `ticket` option; Toll signs a compact EIP-712 verdict
3
+ * bound to one exact call, one account, an optional consumer contract and a 60 second window. A contract built on
4
+ * toll-allow (https://github.com/t0ll-xyz/toll-allow) accepts the call only with a valid, unused ticket.
5
+ * A BLOCK is never signed. No ticket without `from`, or for a transaction with an EIP-7702 authorization list.
6
+ *
7
+ * appendTicketSuffix MUST match toll-allow's TollAllow.encodeSuffix / splitSuffix byte for byte:
8
+ * body ‖ abi.encode((uint8,uint8,address,address,bytes32,uint64,uint64,bytes32,bytes32), bytes) ‖ uint32 len ‖ 0x900bc3e1
9
+ * where len is the byte length of the abi.encode part and 0x900bc3e1 = bytes4(keccak256("TollAllow.v1")).
10
+ */
11
+ import type { Hex } from "./check.js";
12
+ /** what to ask Toll for: `true` = scope "call", any consumer */
13
+ export interface TicketAsk {
14
+ /** the contract that will check the ticket (its `consumer` field); unset = any contract */
15
+ consumer?: string;
16
+ /** "call" (default): bound to (target, value, keccak256(data)); "v4swap": bound to the swap a HookrRouter call makes */
17
+ scope?: "call" | "v4swap";
18
+ }
19
+ /** the ticket as Toll returns it (x-toll.ticket.ticket); uint64 times are unix seconds */
20
+ export interface AllowTicket {
21
+ scope: number;
22
+ verdict: number;
23
+ account: string;
24
+ consumer: string;
25
+ subject: string;
26
+ validAfter: number | bigint | string;
27
+ validUntil: number | bigint | string;
28
+ nonce: string;
29
+ reportHash: string;
30
+ }
31
+ /** x-toll.ticket: a signed ticket, or why none was issued */
32
+ export type TicketResult = {
33
+ ticket: AllowTicket;
34
+ signature: Hex;
35
+ signer: string;
36
+ } | {
37
+ why: string;
38
+ };
39
+ /** bytes4(keccak256("TollAllow.v1")) */
40
+ export declare const TICKET_SUFFIX_MAGIC = "0x900bc3e1";
41
+ /** Toll's production ALLOW signer on Robinhood Chain (4663): pin it, or a TollSignerSet you control that includes it */
42
+ export declare const TOLL_ALLOW_SIGNER = "0x4A2C93066d94F1E69f3EAd5438c4efDdf80a46ba";
43
+ export declare const ALLOW_DOCS = "https://t0ll.xyz/agents/allow";
44
+ /** the ask as the route reads it (`ticket: {consumer?, scope?}`), or null for none */
45
+ export declare function ticketAskOf(t: boolean | TicketAsk | undefined | null): TicketAsk | null;
46
+ /** the ticket in an answer, when the answer carries one (signed or with its why) */
47
+ export declare function ticketOfAnswer(a: {
48
+ "x-toll"?: Record<string, unknown>;
49
+ } | null | undefined): TicketResult | undefined;
50
+ /** abi.encode(Ticket, bytes) as hex without 0x: the static tuple inline (9 words), then the offset and the bytes */
51
+ export declare function encodeTicketAndSig(t: AllowTicket, signature: string): string;
52
+ /**
53
+ * The calldata a ticket-checking router reads: `data ‖ abi.encode(ticket, signature) ‖ uint32 len ‖ 0x900bc3e1`.
54
+ * The ticket's subject is over `data` (the body) only. Identical to TollAllow.encodeSuffix in toll-allow.
55
+ */
56
+ export declare function appendTicketSuffix(data: Hex, ticket: AllowTicket, signature: Hex): Hex;
package/dist/ticket.js ADDED
@@ -0,0 +1,90 @@
1
+ /** bytes4(keccak256("TollAllow.v1")) */
2
+ export const TICKET_SUFFIX_MAGIC = "0x900bc3e1";
3
+ /** Toll's production ALLOW signer on Robinhood Chain (4663): pin it, or a TollSignerSet you control that includes it */
4
+ export const TOLL_ALLOW_SIGNER = "0x4A2C93066d94F1E69f3EAd5438c4efDdf80a46ba";
5
+ export const ALLOW_DOCS = "https://t0ll.xyz/agents/allow";
6
+ /** the ask as the route reads it (`ticket: {consumer?, scope?}`), or null for none */
7
+ export function ticketAskOf(t) {
8
+ if (t === undefined || t === null || t === false)
9
+ return null;
10
+ if (t === true)
11
+ return {};
12
+ const o = {};
13
+ if (t.consumer !== undefined)
14
+ o.consumer = t.consumer;
15
+ if (t.scope !== undefined)
16
+ o.scope = t.scope;
17
+ return o;
18
+ }
19
+ /** the ticket in an answer, when the answer carries one (signed or with its why) */
20
+ export function ticketOfAnswer(a) {
21
+ const t = a?.["x-toll"]?.ticket;
22
+ if (!t || typeof t !== "object")
23
+ return undefined;
24
+ if (typeof t.why === "string")
25
+ return { why: t.why };
26
+ if (t.ticket && typeof t.ticket === "object" && typeof t.signature === "string" && typeof t.signer === "string") {
27
+ return { ticket: t.ticket, signature: t.signature, signer: t.signer };
28
+ }
29
+ return undefined;
30
+ }
31
+ /* ------------------------------------------------------------------ ABI encoding (no dependencies) */
32
+ const hexBody = (h, what) => {
33
+ if (typeof h !== "string" || !/^0x([0-9a-fA-F]{2})*$/.test(h))
34
+ throw new TypeError(`${what} must be 0x hex with whole bytes`);
35
+ return h.slice(2).toLowerCase();
36
+ };
37
+ const word = (n, bits, what) => {
38
+ if (n < 0n || n >= 1n << BigInt(bits))
39
+ throw new RangeError(`${what} does not fit in uint${bits}`);
40
+ return n.toString(16).padStart(64, "0");
41
+ };
42
+ const uint = (v, bits, what) => {
43
+ let n;
44
+ try {
45
+ n = BigInt(v);
46
+ }
47
+ catch {
48
+ throw new TypeError(`${what} must be a whole number`);
49
+ }
50
+ return word(n, bits, what);
51
+ };
52
+ const addr = (a, what) => {
53
+ if (typeof a !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(a))
54
+ throw new TypeError(`${what} must be an address`);
55
+ return a.slice(2).toLowerCase().padStart(64, "0");
56
+ };
57
+ const b32 = (h, what) => {
58
+ if (typeof h !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(h))
59
+ throw new TypeError(`${what} must be 32 bytes`);
60
+ return h.slice(2).toLowerCase();
61
+ };
62
+ /** abi.encode(Ticket, bytes) as hex without 0x: the static tuple inline (9 words), then the offset and the bytes */
63
+ export function encodeTicketAndSig(t, signature) {
64
+ const sig = hexBody(signature, "signature");
65
+ const head = uint(t.scope, 8, "ticket.scope") +
66
+ uint(t.verdict, 8, "ticket.verdict") +
67
+ addr(t.account, "ticket.account") +
68
+ addr(t.consumer, "ticket.consumer") +
69
+ b32(t.subject, "ticket.subject") +
70
+ uint(t.validAfter, 64, "ticket.validAfter") +
71
+ uint(t.validUntil, 64, "ticket.validUntil") +
72
+ b32(t.nonce, "ticket.nonce") +
73
+ b32(t.reportHash, "ticket.reportHash");
74
+ const offset = word(10n * 32n, 256, "offset"); // 9 tuple words + this offset word
75
+ const len = word(BigInt(sig.length / 2), 256, "signature length");
76
+ const padded = sig + "0".repeat((64 - (sig.length % 64)) % 64);
77
+ return head + offset + len + padded;
78
+ }
79
+ /**
80
+ * The calldata a ticket-checking router reads: `data ‖ abi.encode(ticket, signature) ‖ uint32 len ‖ 0x900bc3e1`.
81
+ * The ticket's subject is over `data` (the body) only. Identical to TollAllow.encodeSuffix in toll-allow.
82
+ */
83
+ export function appendTicketSuffix(data, ticket, signature) {
84
+ const body = hexBody(data, "data");
85
+ const enc = encodeTicketAndSig(ticket, signature);
86
+ const n = enc.length / 2;
87
+ if (n >= 2 ** 32)
88
+ throw new RangeError("ticket encoding too long");
89
+ return `0x${body}${enc}${n.toString(16).padStart(8, "0")}${TICKET_SUFFIX_MAGIC.slice(2)}`;
90
+ }
package/dist/wallets.d.ts CHANGED
@@ -28,7 +28,7 @@ export declare function guardViem<T extends object>(client: T, opts?: ViemGuardO
28
28
  * `calls` are checked one by one as transactions from the smart account's address; a ready `callData` is sent as a
29
29
  * user operation with the account's EntryPoint. An EIP-7702 `authorization` goes to onAuthorization.
30
30
  */
31
- export declare function guardSmartAccount<T extends object>(client: T, opts?: ViemGuardOptions): T;
31
+ export declare function guardSmartAccount<T extends object>(client: T, sopts?: ViemGuardOptions): T;
32
32
  /** the EIP-712 primary type: the one type no other type refers to (as ethers' TypedDataEncoder.getPrimaryType) */
33
33
  export declare function primaryTypeOf(types: Record<string, Array<{
34
34
  name: string;
package/dist/wallets.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * The wrapper is a Proxy: every other method and field of the client passes through unchanged.
4
4
  */
5
5
  import { chainIdOf, isAddress } from "./check.js";
6
- import { enforce, enforceCalls, gateAuthorization } from "./policy.js";
6
+ import { commitSpend, enforce, enforceCalls, gateAuthorization, releaseSpend } from "./policy.js";
7
7
  /** wraps an object's methods; everything else passes through, bound to the original */
8
8
  function proxy(target, wrapped) {
9
9
  return new Proxy(target, {
@@ -15,6 +15,36 @@ function proxy(target, wrapped) {
15
15
  },
16
16
  });
17
17
  }
18
+ /**
19
+ * One wrapped call: the check, then the send. With `spend` set, the day-cap reservations the check made are committed
20
+ * after the send and released when the check or the send throws. `confirm` is a preview's confirmId (tollConfirm).
21
+ */
22
+ async function guarded(o, confirm, check, send) {
23
+ if (!o.spend && confirm === undefined) {
24
+ await check(o);
25
+ return send();
26
+ }
27
+ const col = [];
28
+ const io = { ...o, _collect: col, ...(confirm !== undefined ? { _confirm: confirm } : {}) };
29
+ let out;
30
+ try {
31
+ await check(io);
32
+ out = await send();
33
+ }
34
+ catch (e) {
35
+ await releaseSpend(col, o);
36
+ throw e;
37
+ }
38
+ await commitSpend(col, o);
39
+ return out;
40
+ }
41
+ /** a preview's confirmId on the request (tollConfirm), and the request without it (what the wallet gets) */
42
+ function takeConfirm(args) {
43
+ if (!args || typeof args !== "object" || !Object.prototype.hasOwnProperty.call(args, "tollConfirm"))
44
+ return [undefined, args];
45
+ const { tollConfirm, ...rest } = args;
46
+ return [typeof tollConfirm === "string" && tollConfirm ? tollConfirm : undefined, rest];
47
+ }
18
48
  /** a transaction that carries an EIP-7702 authorizationList (any entry at all) */
19
49
  const hasAuthorizations = (tx) => {
20
50
  const l = tx?.authorizationList;
@@ -114,61 +144,57 @@ export function guardViem(client, opts = {}) {
114
144
  for (const m of ["sendTransaction", "signTransaction"]) {
115
145
  if (typeof c[m] !== "function")
116
146
  continue;
117
- wrapped[m] = async (args, ...rest) => {
147
+ wrapped[m] = async (raw, ...rest) => {
148
+ const [confirm, args] = takeConfirm(raw);
118
149
  // a deployment (no `to`) goes to the unsupported policy
119
- await gateTx(m, () => txInput(args, args?.data), hasAuthorizations(args), opts);
120
- return c[m](args, ...rest);
150
+ return guarded(opts, confirm, (o) => gateTx(m, () => txInput(args, args?.data), hasAuthorizations(args), o), () => c[m](args, ...rest));
121
151
  };
122
152
  }
123
153
  if (typeof c.writeContract === "function") {
124
- wrapped.writeContract = async (args, ...rest) => {
125
- await gateTx("writeContract", async () => {
154
+ wrapped.writeContract = async (raw, ...rest) => {
155
+ const [confirm, args] = takeConfirm(raw);
156
+ const build = async () => {
126
157
  const enc = opts.encodeFunctionData ?? (await loadViemEncode());
127
158
  let data = String(enc({ abi: args.abi, functionName: args.functionName, args: args.args }));
128
159
  if (typeof args.dataSuffix === "string")
129
160
  data += args.dataSuffix.replace(/^0x/, "");
130
161
  return txInput({ ...args, to: args.address }, data);
131
- }, hasAuthorizations(args), opts);
132
- return c.writeContract(args, ...rest);
162
+ };
163
+ return guarded(opts, confirm, (o) => gateTx("writeContract", build, hasAuthorizations(args), o), () => c.writeContract(args, ...rest));
133
164
  };
134
165
  }
135
166
  if (typeof c.signTypedData === "function") {
136
167
  wrapped.signTypedData = async (args, ...rest) => {
137
168
  const td = { domain: args?.domain ?? {}, types: args?.types ?? {}, primaryType: args?.primaryType, message: args?.message ?? {} };
138
- await enforce("typedData", () => ({ typedData: td, ...origin }), opts);
139
- return c.signTypedData(args, ...rest);
169
+ // the signer (0.3.1): Toll reads what the signature lets someone take from it (a Permit2 message names no owner)
170
+ const from = fromOf(args);
171
+ return guarded(opts, undefined, (o) => enforce("typedData", () => ({ typedData: td, ...(from ? { from } : {}), ...origin }), o), () => c.signTypedData(args, ...rest));
140
172
  };
141
173
  }
142
174
  if (typeof c.signMessage === "function") {
143
- wrapped.signMessage = async (args, ...rest) => {
144
- await enforce("message", () => {
145
- const m = args?.message;
146
- const raw = m && typeof m === "object" ? m.raw : undefined;
147
- const data = typeof m === "string" ? utf8Hex(m) : typeof raw === "string" ? raw : raw instanceof Uint8Array ? bytesHex(raw) : null;
148
- if (data === null)
149
- throw new Error("signMessage: message must be a string or {raw}");
150
- const from = fromOf(args);
151
- return { message: { kind: "personal_sign", data, ...(from ? { from } : {}) }, ...origin };
152
- }, opts);
153
- return c.signMessage(args, ...rest);
154
- };
175
+ wrapped.signMessage = async (args, ...rest) => guarded(opts, undefined, (o) => enforce("message", () => {
176
+ const m = args?.message;
177
+ const raw = m && typeof m === "object" ? m.raw : undefined;
178
+ const data = typeof m === "string" ? utf8Hex(m) : typeof raw === "string" ? raw : raw instanceof Uint8Array ? bytesHex(raw) : null;
179
+ if (data === null)
180
+ throw new Error("signMessage: message must be a string or {raw}");
181
+ const from = fromOf(args);
182
+ return { message: { kind: "personal_sign", data, ...(from ? { from } : {}) }, ...origin };
183
+ }, o), () => c.signMessage(args, ...rest));
155
184
  }
156
185
  if (typeof c.sendCalls === "function") {
157
186
  wrapped.sendCalls = async (args, ...rest) => {
158
- await enforceCalls(() => callsOf(args?.calls, chainOf(args), fromOf(args), opts), opts);
159
- return c.sendCalls(args, ...rest);
187
+ return guarded(opts, undefined, (o) => enforceCalls(() => callsOf(args?.calls, chainOf(args), fromOf(args), opts), o), () => c.sendCalls(args, ...rest));
160
188
  };
161
189
  }
162
190
  if (typeof c.signAuthorization === "function") {
163
191
  wrapped.signAuthorization = async (args, ...rest) => {
164
- await gateAuthorization("transaction", "signAuthorization", () => authInput(args, c.chain?.id, fromOf(args)), opts);
165
- return c.signAuthorization(args, ...rest);
192
+ return guarded(opts, undefined, (o) => gateAuthorization("transaction", "signAuthorization", () => authInput(args, c.chain?.id, fromOf(args)), o), () => c.signAuthorization(args, ...rest));
166
193
  };
167
194
  }
168
195
  if (typeof c.request === "function") {
169
196
  wrapped.request = async (args, ...rest) => {
170
- await checkRequest(args, c, fromOf({}), opts);
171
- return c.request(args, ...rest);
197
+ return guarded(opts, undefined, (o) => checkRequest(args, c, fromOf({}), { ...opts, ...o }), () => c.request(args, ...rest));
172
198
  };
173
199
  }
174
200
  return proxy(client, wrapped);
@@ -232,9 +258,9 @@ const USEROP_FIELDS = [
232
258
  * `calls` are checked one by one as transactions from the smart account's address; a ready `callData` is sent as a
233
259
  * user operation with the account's EntryPoint. An EIP-7702 `authorization` goes to onAuthorization.
234
260
  */
235
- export function guardSmartAccount(client, opts = {}) {
261
+ export function guardSmartAccount(client, sopts = {}) {
236
262
  const c = client;
237
- const check = async (where, args) => {
263
+ const check = async (where, args, opts) => {
238
264
  const acct = args?.account ?? c.account;
239
265
  const sender = addrOf(acct);
240
266
  const chainId = args?.chain?.id ?? c.chain?.id;
@@ -262,10 +288,7 @@ export function guardSmartAccount(client, opts = {}) {
262
288
  for (const m of ["sendUserOperation", "prepareUserOperation"]) {
263
289
  if (typeof c[m] !== "function")
264
290
  continue;
265
- wrapped[m] = async (args, ...rest) => {
266
- await check(m, args);
267
- return c[m](args, ...rest);
268
- };
291
+ wrapped[m] = async (args, ...rest) => guarded(sopts, undefined, (o) => check(m, args, { ...sopts, ...o }), () => c[m](args, ...rest));
269
292
  }
270
293
  return proxy(client, wrapped);
271
294
  }
@@ -326,38 +349,38 @@ export function guardEthers(signer, opts = {}) {
326
349
  for (const m of ["sendTransaction", "signTransaction"]) {
327
350
  if (typeof s[m] !== "function")
328
351
  continue;
329
- wrapped[m] = async (tx, ...rest) => {
330
- await gateTx(m, () => ethersTx(s, tx), hasAuthorizations(tx), opts);
331
- return s[m](tx, ...rest);
352
+ wrapped[m] = async (raw, ...rest) => {
353
+ const [confirm, tx] = takeConfirm(raw);
354
+ return guarded(opts, confirm, (o) => gateTx(m, () => ethersTx(s, tx), hasAuthorizations(tx), o), () => s[m](tx, ...rest));
332
355
  };
333
356
  }
334
357
  if (typeof s.signTypedData === "function") {
335
358
  wrapped.signTypedData = async (domain, types, value, ...rest) => {
336
- await enforce("typedData", () => ({ typedData: { domain: domain ?? {}, types: types ?? {}, primaryType: primaryTypeOf(types ?? {}), message: value ?? {} }, ...origin }), opts);
337
- return s.signTypedData(domain, types, value, ...rest);
359
+ return guarded(opts, undefined, (o) => enforce("typedData", async () => {
360
+ // the signer (0.3.1): Toll reads what the signature lets someone take from it (a Permit2 message names no owner)
361
+ const from = typeof s.getAddress === "function" ? await s.getAddress() : undefined;
362
+ return { typedData: { domain: domain ?? {}, types: types ?? {}, primaryType: primaryTypeOf(types ?? {}), message: value ?? {} }, ...(isAddress(from) ? { from } : {}), ...origin };
363
+ }, o), () => s.signTypedData(domain, types, value, ...rest));
338
364
  };
339
365
  }
340
366
  if (typeof s.signMessage === "function") {
341
- wrapped.signMessage = async (message, ...rest) => {
342
- await enforce("message", async () => {
343
- // ethers signs a string as its UTF-8 bytes, even one that looks like hex
344
- const data = typeof message === "string" ? utf8Hex(message) : message instanceof Uint8Array ? bytesHex(message) : null;
345
- if (data === null)
346
- throw new Error("signMessage: message must be a string or bytes");
347
- const from = typeof s.getAddress === "function" ? await s.getAddress() : undefined;
348
- return { message: { kind: "personal_sign", data, ...(isAddress(from) ? { from } : {}) }, ...origin };
349
- }, opts);
350
- return s.signMessage(message, ...rest);
351
- };
367
+ wrapped.signMessage = async (message, ...rest) => guarded(opts, undefined, (o) => enforce("message", async () => {
368
+ // ethers signs a string as its UTF-8 bytes, even one that looks like hex
369
+ const data = typeof message === "string" ? utf8Hex(message) : message instanceof Uint8Array ? bytesHex(message) : null;
370
+ if (data === null)
371
+ throw new Error("signMessage: message must be a string or bytes");
372
+ const from = typeof s.getAddress === "function" ? await s.getAddress() : undefined;
373
+ return { message: { kind: "personal_sign", data, ...(isAddress(from) ? { from } : {}) }, ...origin };
374
+ }, o), () => s.signMessage(message, ...rest));
352
375
  }
353
376
  if (typeof s.authorize === "function") {
354
377
  wrapped.authorize = async (auth, ...rest) => {
355
- await gateAuthorization("transaction", "authorize", async () => {
378
+ const build = async () => {
356
379
  const chainId = chainOut(await ethersChainId(s));
357
380
  const from = typeof s.getAddress === "function" ? await s.getAddress() : undefined;
358
381
  return authInput(auth ?? {}, chainId, isAddress(from) ? from : undefined);
359
- }, opts);
360
- return s.authorize(auth, ...rest);
382
+ };
383
+ return guarded(opts, undefined, (o) => gateAuthorization("transaction", "authorize", build, o), () => s.authorize(auth, ...rest));
361
384
  };
362
385
  }
363
386
  return proxy(signer, wrapped);
package/dist/x402.d.ts CHANGED
@@ -14,9 +14,21 @@ export declare function readPaymentForms(res: Response): Promise<{
14
14
  export declare function readPaymentRequired(res: Response): Promise<PR | null>;
15
15
  /** true when the header and the body would pay differently (a payee, amount, asset or scheme in one and not the other) */
16
16
  export declare function paymentFormsDiffer(header: PR, body: PR): boolean;
17
+ /**
18
+ * The x402 payment-identifier extension's id in a signed payment header (base64 JSON PaymentPayload):
19
+ * extensions["payment-identifier"].info.id (or .id); null when there is none.
20
+ */
21
+ export declare function paymentIdOf(header: string | null): string | null;
17
22
  export interface X402GuardOptions extends GuardOptions {
18
23
  /** called with each 402's report (also passed to onReport) */
19
24
  on402?: (report: GuardReport, url: string) => void;
25
+ /**
26
+ * With `spend`: how long a checked 402's cap reservation waits for its paid retry before it is given back
27
+ * (default 120000 ms). The paid retry commits it on a non-402 answer and releases it on a 402 or an error.
28
+ */
29
+ holdMs?: number;
30
+ /** With `spend`: how long a paid payment-identifier is remembered (default 86400 s) */
31
+ paymentIdTtlS?: number;
20
32
  }
21
33
  /**
22
34
  * Wraps a fetch so every 402 is checked by Toll before an x402 client pays it. A 402 to a request that already