@metalntwx/metal-pay 0.3.0 → 0.3.2

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.
@@ -3,12 +3,12 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
3
3
  import { Hono } from "hono";
4
4
  import { paymentMiddleware } from "@x402/hono";
5
5
  import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
6
- import { decodePaymentRequiredHeader, decodePaymentResponseHeader, encodePaymentSignatureHeader } from "@x402/core/http";
6
+ import { decodePaymentRequiredHeader, decodePaymentResponseHeader, decodePaymentSignatureHeader, encodePaymentSignatureHeader } from "@x402/core/http";
7
7
  import type { PaymentPayload, PaymentRequirements } from "@x402/core/types";
8
8
  import { sha256, stringToHex } from "viem";
9
9
  import { privateKeyToAccount } from "viem/accounts";
10
10
  import { createClearingSigner, encodeSend, invoiceMessage, outgoingRoot, type ClearingInvoice } from "./clearing-codec.js";
11
- import type { ClearingPaid } from "./clearing-client.js";
11
+ import { ClearingRefusal, type ClearingPaid } from "./clearing-client.js";
12
12
  import { ClearingServerScheme } from "./clearing-server.js";
13
13
  import { clearingPaymentMismatch, clearingPaymentOf, registerClearingClient } from "./clearing-x402.js";
14
14
  import { createMetalPaymentClient } from "./client.js";
@@ -82,8 +82,8 @@ const fakePayer = {
82
82
  async pendingInvoice() {
83
83
  return null;
84
84
  },
85
- payable() {
86
- return true;
85
+ unpayable() {
86
+ return undefined;
87
87
  },
88
88
  async pay(invoice: ClearingInvoice): Promise<ClearingPaid> {
89
89
  const entries = [{ recipient: invoice.recipient, cumulative: invoice.amount, count: "1" }];
@@ -260,16 +260,78 @@ describe("ClearingServerScheme under the stock middleware", () => {
260
260
  expect(served).toBe(1);
261
261
  });
262
262
 
263
- it("pays with clearing when offered clearing and exact, in either order, and with exact when it cannot pay the clearing offer", async () => {
263
+ it("pays with clearing when offered clearing and exact, in either order", async () => {
264
264
  const accepts = await challenge("http://seller.local/chunk");
265
- const chosen = async (payer: typeof fakePayer, offers: PaymentRequirements[]) => {
265
+ const chosen = async (offers: PaymentRequirements[]) => {
266
266
  const { client } = createMetalPaymentClient({ account: privateKeyToAccount(`0x${"45".repeat(32)}`), network: "devnet" });
267
- registerClearingClient(client, NETWORK, payer);
267
+ registerClearingClient(client, NETWORK, fakePayer);
268
268
  return (await client.createPaymentPayload({ x402Version: 2, resource: { url: "http://seller.local/chunk" }, accepts: offers })).accepted.scheme;
269
269
  };
270
- expect(await chosen(fakePayer, accepts)).toBe("bajillion");
271
- expect(await chosen(fakePayer, [...accepts].reverse())).toBe("bajillion");
272
- expect(await chosen({ ...fakePayer, payable: () => false }, accepts)).toBe("exact");
270
+ expect(await chosen(accepts)).toBe("bajillion");
271
+ expect(await chosen([...accepts].reverse())).toBe("bajillion");
272
+ });
273
+
274
+ /** The Metal client over the seller, and the schemes of the payments it sent. */
275
+ const paying = (options: { maxAmount?: bigint } = {}) => {
276
+ const sent: string[] = [];
277
+ const { client, fetchWithPayment } = createMetalPaymentClient({
278
+ account: privateKeyToAccount(`0x${"45".repeat(32)}`),
279
+ network: "devnet",
280
+ ...options,
281
+ fetch: async (input, init) => {
282
+ const request = new Request(input as RequestInfo, init);
283
+ const header = request.headers.get("payment-signature");
284
+ if (header) sent.push(decodePaymentSignatureHeader(header).accepted.scheme);
285
+ return app.request(request);
286
+ },
287
+ });
288
+ return { client, fetchWithPayment, sent };
289
+ };
290
+ const unpayable = { ...fakePayer, unpayable: () => ({ reason: "offer is for deployment 0x1, not this wallet's 0x2", aboveMaxAmount: false }) };
291
+
292
+ it("pays a request it cannot pay by clearing by exact only with exactFallback: unset, the request fails and sends no payment", async () => {
293
+ const accepts = await challenge("http://seller.local/chunk");
294
+ const refused = paying();
295
+ registerClearingClient(refused.client, NETWORK, unpayable);
296
+ await expect(refused.fetchWithPayment("http://seller.local/chunk")).rejects.toThrow(
297
+ /pays none of the clearing offers here \(offer is for deployment 0x1, not this wallet's 0x2\), and paying by exact is off; nothing was paid: pass exactFallback: true/,
298
+ );
299
+ await expect(
300
+ refused.client.createPaymentPayload({ x402Version: 2, resource: { url: "http://seller.local/chunk" }, accepts: [accepts[1]!] }),
301
+ ).rejects.toThrow(/offers no clearing payment on eip155:1337, and paying by exact is off/);
302
+ expect(refused.sent).toEqual([]);
303
+ const fallback = paying();
304
+ registerClearingClient(fallback.client, NETWORK, unpayable, { exactFallback: true });
305
+ await fallback.fetchWithPayment("http://seller.local/chunk");
306
+ expect(fallback.sent).toEqual(["exact"]);
307
+ });
308
+
309
+ it("caps exact payments, not clearing ones, by the payment client's maxAmount", async () => {
310
+ // k12-visual's outer limit of 0, set to block the exact fallback.
311
+ const clearing = paying({ maxAmount: 0n });
312
+ registerClearingClient(clearing.client, NETWORK, fakePayer);
313
+ expect((await clearing.fetchWithPayment("http://seller.local/chunk")).status).toBe(200);
314
+ expect(clearing.sent).toEqual(["bajillion"]);
315
+ const exact = paying({ maxAmount: 0n });
316
+ registerClearingClient(exact.client, NETWORK, unpayable, { exactFallback: true });
317
+ await expect(exact.fetchWithPayment("http://seller.local/chunk")).rejects.toThrow(/offer of 0.01 \*USDZ is above the 0 \*USDZ limit per payment/);
318
+ expect(exact.sent).toEqual([]);
319
+ });
320
+
321
+ it("fails a request whose clearing payment is refused with the refusal as its error's cause, sending no payment", async () => {
322
+ const refusal = new ClearingRefusal("between_epochs", { action: "retry_after_ms", ms: 1000 }, "clearing is between epochs");
323
+ const refused = paying();
324
+ registerClearingClient(refused.client, NETWORK, {
325
+ ...fakePayer,
326
+ async pay() {
327
+ throw refusal;
328
+ },
329
+ });
330
+ const error = await refused.fetchWithPayment("http://seller.local/chunk").catch((e: unknown) => e);
331
+ expect(error).toBeInstanceOf(Error);
332
+ expect((error as Error).message).toBe("Failed to create payment payload: clearing is between epochs");
333
+ expect((error as Error).cause).toBe(refusal);
334
+ expect(refused.sent).toEqual([]);
273
335
  });
274
336
  });
275
337
 
@@ -150,7 +150,7 @@ describe("native x402 invoice binding", () => {
150
150
  async pendingInvoice() {
151
151
  return pending;
152
152
  },
153
- payable: () => true,
153
+ unpayable: () => undefined,
154
154
  async pay(next: ClearingInvoice) {
155
155
  ids.push(next.id);
156
156
  if (!pending) {
@@ -1,6 +1,6 @@
1
1
  import type { PaymentRequirements, PaymentPayload } from "@x402/core/types";
2
2
  import type { x402Client } from "@x402/core/client";
3
- import { createPublicClient, http, sha256, stringToHex, type Hex } from "viem";
3
+ import { createPublicClient, sha256, stringToHex, type Hex } from "viem";
4
4
  import { clearingAbi } from "./clearing-abi.js";
5
5
  import {
6
6
  CLEARING_SCHEME,
@@ -13,6 +13,7 @@ import {
13
13
  type ClearingInvoice,
14
14
  } from "./clearing-codec.js";
15
15
  import type { ClearingPaid, createClearingClient } from "./clearing-client.js";
16
+ import { rpcTransport } from "./networks.js";
16
17
 
17
18
  export interface ClearingSeller {
18
19
  network: `eip155:${number}`;
@@ -129,7 +130,7 @@ export async function verifyClearingPayment(
129
130
  )
130
131
  throw new Error("invalid clearing payment");
131
132
  const send = decodeSend(paid.receipt.body);
132
- const client = createPublicClient({ transport: http(config.rpcUrl) });
133
+ const client = createPublicClient({ transport: rpcTransport(config.rpcUrl) });
133
134
  const [context, fault, asset, chainId, unit] = await Promise.all([
134
135
  client.readContract({
135
136
  address: config.settlement,
@@ -237,24 +238,43 @@ export async function verifyClearingPayment(
237
238
  status: "preconfirmed" as const,
238
239
  };
239
240
  }
241
+ /** Pay by clearing through `client` on `network`. A request with an offer on
242
+ * `network` but none the wallet can pay fails, unless `exactFallback` is
243
+ * true: then it is paid by another offer of the request on `network`,
244
+ * `exact` from its EVM account, and fails when it has none. A request with
245
+ * no offer on `network` is left to the
246
+ * client's other registrations. A payment the clearing service does not take
247
+ * throws a `ClearingRefusal`. */
240
248
  export function registerClearingClient(
241
249
  client: x402Client,
242
250
  network: `eip155:${number}`,
243
251
  payer: Pick<
244
252
  ReturnType<typeof createClearingClient>,
245
- "pay" | "payable" | "pendingInvoice"
253
+ "pay" | "unpayable" | "pendingInvoice"
246
254
  >,
255
+ { exactFallback = false }: { exactFallback?: boolean } = {},
247
256
  ) {
257
+ // The URL each offer was asked for, kept with a payment whose outcome is
258
+ // unknown so a refusal names the request to send again.
259
+ const urls = new WeakMap<PaymentRequirements, string>();
248
260
  client.register(network, {
249
261
  scheme: CLEARING_SCHEME,
262
+ schemeHooks: {
263
+ async onBeforePaymentCreation({ paymentRequired, selectedRequirements }) {
264
+ urls.set(selectedRequirements, paymentRequired.resource.url);
265
+ },
266
+ },
250
267
  async createPaymentPayload(x402Version, requirements) {
251
268
  const pending = await payer.pendingInvoice();
252
- const suffix = globalThis.crypto.randomUUID();
269
+ // A payment held for another URL is the wallet's to refuse, naming it.
270
+ const prefix = `${requirements.extra?.invoicePrefix}:`;
253
271
  const invoice = clearingInvoice(
254
272
  requirements,
255
- pending?.id ?? `${requirements.extra?.invoicePrefix}:${suffix}`,
273
+ pending?.id.startsWith(prefix)
274
+ ? pending.id
275
+ : prefix + globalThis.crypto.randomUUID(),
256
276
  );
257
- const paid = await payer.pay(invoice);
277
+ const paid = await payer.pay(invoice, urls.get(requirements));
258
278
  return {
259
279
  x402Version,
260
280
  payload: paid as unknown as Record<string, unknown>,
@@ -265,15 +285,44 @@ export function registerClearingClient(
265
285
  // its policies, and a clearing offer is `upfront`: so a payer set up for
266
286
  // clearing keeps the clearing offers its wallet can pay, when there are any.
267
287
  client.registerPolicy((_, offers) => {
268
- const clearing = offers.filter((offer) => {
269
- if (offer.scheme !== CLEARING_SCHEME || offer.network !== network) return false;
288
+ if (!offers.some((offer) => offer.network === network)) return offers;
289
+ const offered = offers.filter((offer) => offer.scheme === CLEARING_SCHEME && offer.network === network);
290
+ // Why the wallet does not pay `offer`, or undefined when it does.
291
+ const unpaid = (offer: PaymentRequirements) => {
292
+ if (!/^[0-9]+$/.test(offer.amount) || BigInt(offer.amount) === 0n)
293
+ return { reason: `offer is priced at "${offer.amount}", not a positive whole number of base units`, aboveMaxAmount: false };
294
+ let invoice: ClearingInvoice;
270
295
  try {
271
- return payer.payable(clearingInvoice(offer, `${offer.extra?.invoicePrefix}:`));
296
+ invoice = clearingInvoice(offer, `${offer.extra?.invoicePrefix}:`);
272
297
  } catch {
273
- return false;
298
+ return { reason: "offer is malformed (invalid clearing requirement)", aboveMaxAmount: false };
274
299
  }
275
- });
276
- return clearing.length > 0 ? clearing : offers;
300
+ return payer.unpayable(invoice);
301
+ };
302
+ const reasons = offered.map(unpaid);
303
+ const clearing = offered.filter((_, i) => reasons[i] === undefined);
304
+ if (clearing.length > 0) return clearing;
305
+ if (exactFallback) {
306
+ // The offers this wallet just refused stay out: the stock selector can
307
+ // choose them again, since a clearing offer with no extra.paymentFlow
308
+ // is otherwise indistinguishable from one it can pay. The fallback is
309
+ // for this request's offers on `network`; with none left, it fails.
310
+ if (offers.some((offer) => offer.network === network && !offered.includes(offer)))
311
+ return offers.filter((offer) => !offered.includes(offer));
312
+ }
313
+ const why =
314
+ offered.length > 0
315
+ ? `this clearing wallet pays none of the clearing offers here (${[...new Set(reasons.map((r) => r?.reason))].join("; ")})`
316
+ : `this request offers no clearing payment on ${network}`;
317
+ // An offer above the wallet's maxAmount is for its own deployment: the
318
+ // limit or the seller is what to change, not the scheme.
319
+ throw new Error(
320
+ reasons.some((r) => r?.aboveMaxAmount)
321
+ ? `${why}; nothing was paid: raise the wallet's maxAmount or pay another seller`
322
+ : exactFallback
323
+ ? `${why}, and this request offers no other way to pay it; nothing was paid`
324
+ : `${why}, and paying by exact is off; nothing was paid: pass exactFallback: true to registerClearingClient to pay such a request by exact`,
325
+ );
277
326
  });
278
327
  return client;
279
328
  }
@@ -318,6 +318,14 @@ describe("usdz amounts", () => {
318
318
  expect(formatUsdz("2000000000000000000")).toBe("2");
319
319
  });
320
320
 
321
+ it("formats a negative amount, as clearingSpendable can read, with one leading sign", () => {
322
+ expect(formatUsdz(-100000000000000000n)).toBe("-0.1");
323
+ expect(formatUsdz(-1500000000000000000n)).toBe("-1.5");
324
+ expect(formatUsdz(-1n)).toBe("-0.000000000000000001");
325
+ expect(formatUsdz("-2000000000000000000")).toBe("-2");
326
+ expect(formatUsdz(0n)).toBe("0");
327
+ });
328
+
321
329
  it("rejects malformed and over-precise amounts", () => {
322
330
  expect(() => usdzBaseUnits("1e3")).toThrow();
323
331
  expect(() => usdzBaseUnits("0." + "1".repeat(19))).toThrow(/decimals/);
package/src/client.ts CHANGED
@@ -1,8 +1,10 @@
1
- import { x402Client } from "@x402/core/client";
1
+ import { x402Client, x402HTTPClient } from "@x402/core/client";
2
2
  import { wrapFetchWithPayment } from "@x402/fetch";
3
3
  import type { ClientEvmSigner } from "@x402/evm";
4
4
  import type { Network } from "@x402/core/types";
5
5
  import { ExactEvmScheme } from "@x402/evm/exact/client";
6
+ import { ClearingRefusal } from "./clearing-client.js";
7
+ import { CLEARING_SCHEME } from "./clearing-codec.js";
6
8
  import {
7
9
  METAL_NETWORKS,
8
10
  USDZ,
@@ -76,7 +78,8 @@ export interface MetalPaymentClientConfig {
76
78
  fetch?: typeof globalThis.fetch;
77
79
  /**
78
80
  * The most one payment may cost, in `*USDZ` base units; `DEFAULT_MAX_PAYMENT`
79
- * by default. An offer above it is refused before anything is signed.
81
+ * by default. An offer above it is refused before anything is signed. A
82
+ * clearing payment is capped by its wallet's own `maxAmount` instead.
80
83
  */
81
84
  maxAmount?: bigint;
82
85
  }
@@ -117,7 +120,11 @@ export function keepPaymentOnOrigin(fetch: typeof globalThis.fetch): typeof glob
117
120
  }
118
121
 
119
122
  export interface MetalPaymentClient {
120
- /** `fetch` that answers a 402 by paying and retrying, once. */
123
+ /**
124
+ * `fetch` that answers a 402 by paying and retrying, once. When the
125
+ * clearing service does not take a payment, the error's `cause` is the
126
+ * `ClearingRefusal`.
127
+ */
121
128
  fetchWithPayment: ReturnType<typeof wrapFetchWithPayment>;
122
129
  client: x402Client;
123
130
  network: MetalNetwork;
@@ -138,18 +145,50 @@ export function createMetalPaymentClient({
138
145
  // The hooks are the guard: the client's own spend controls would refuse
139
146
  // first, naming nothing of the offer, so they stand aside on this client.
140
147
  client.setSpendControls({ allowedAssets: true });
141
- // @x402/fetch signs whatever amount an offer asks; this hook is the payer's spending guard.
148
+ // @x402/fetch signs whatever amount an offer asks; this hook is the payer's
149
+ // spending guard. A clearing wallet guards its own payments.
142
150
  client.onBeforePaymentCreation(
143
- async ({ selectedRequirements: { amount } }) => {
144
- if (BigInt(amount) <= maxAmount) return;
151
+ async ({ selectedRequirements: { scheme, amount } }) => {
152
+ if (scheme === CLEARING_SCHEME || BigInt(amount) <= maxAmount) return;
145
153
  return {
146
154
  abort: true,
147
155
  reason: `offer of ${formatUsdz(amount)} ${USDZ.symbol} is above the ${formatUsdz(maxAmount)} ${USDZ.symbol} limit per payment`,
148
156
  };
149
157
  },
150
158
  );
159
+ const paid = keepPaymentOnOrigin(fetch);
151
160
  return {
152
- fetchWithPayment: wrapFetchWithPayment(keepPaymentOnOrigin(fetch), client),
161
+ // @x402/fetch rethrows a failure to pay as a new Error with the message
162
+ // alone; each request keeps its clearing refusal to give as the cause.
163
+ fetchWithPayment: async (input, init) => {
164
+ let refused: ClearingRefusal | undefined;
165
+ // A clearing payment records the URL this request went to, as the
166
+ // fetch resolved it, for a refusal to name: behind a proxy, the
167
+ // seller's own name for the resource can differ from it. The seller
168
+ // receives its own name back.
169
+ let url: string | undefined;
170
+ const sending: typeof globalThis.fetch = (request, options) => {
171
+ url ??= request instanceof Request ? request.url : String(request);
172
+ return paid(request, options);
173
+ };
174
+ const paying = new x402HTTPClient(client);
175
+ paying.createPaymentPayload = (paymentRequired) =>
176
+ client
177
+ .createPaymentPayload({ ...paymentRequired, resource: { ...paymentRequired.resource, url: url ?? paymentRequired.resource.url } })
178
+ .then(
179
+ (payload) => ({ ...payload, resource: paymentRequired.resource }),
180
+ (error: unknown) => {
181
+ if (error instanceof ClearingRefusal) refused = error;
182
+ throw error;
183
+ },
184
+ );
185
+ try {
186
+ return await wrapFetchWithPayment(sending, paying)(input, init);
187
+ } catch (error) {
188
+ if (!refused) throw error;
189
+ throw new Error(`Failed to create payment payload: ${refused.message}`, { cause: refused });
190
+ }
191
+ },
153
192
  client,
154
193
  network: resolved,
155
194
  };
@@ -8,7 +8,7 @@ export const metalAccountFactoryAbi = parseAbi([
8
8
  ]);
9
9
  export const metalAccountAbi = parseAbi([
10
10
  "function nonces(address owner, uint192 key) view returns (uint256)",
11
- "function executeSigned(bytes32 mode, bytes executionData, uint256 nonce, bytes signature)",
11
+ "function executeSigned(bytes32 mode, bytes executionData, uint256 nonce, uint64 validUntil, bytes signature)",
12
12
  ]);
13
13
  export const ACCOUNT_BATCH_MODE = "0x0100000000000000000000000000000000000000000000000000000000000000";
14
14
 
@@ -59,10 +59,12 @@ export function quorumGas(signature: Hex): bigint {
59
59
  }
60
60
 
61
61
  /** Self-calls approved by the account's existing ERC-7913 quorum, in one batch
62
- * that runs whole or not at all. The signing callback may collect mixed keys,
63
- * weights and passkeys; it returns the normal abi.encode(bytes[] signers,
64
- * bytes[] signatures) account signature. */
65
- export async function authorizeMandateCall(client: PublicClient, factory: Address, call: MandateCall | readonly MandateCall[], signQuorum: (digest: Hex) => Promise<Hex>): Promise<MandateCall> {
62
+ * that runs whole or not at all. The account refuses the signed batch once the
63
+ * chain's block timestamp passes `validUntil` (unix seconds): pick a deadline
64
+ * just long enough to collect the signatures and submit. The signing callback
65
+ * may collect mixed keys, weights and passkeys; it returns the normal
66
+ * abi.encode(bytes[] signers, bytes[] signatures) account signature. */
67
+ export async function authorizeMandateCall(client: PublicClient, factory: Address, call: MandateCall | readonly MandateCall[], validUntil: bigint, signQuorum: (digest: Hex) => Promise<Hex>): Promise<MandateCall> {
66
68
  const chainId = client.chain?.id;
67
69
  if (chainId === undefined) throw new Error("the RPC client names no chain");
68
70
  const calls = Array.isArray(call) ? (call as readonly MandateCall[]) : [call as MandateCall];
@@ -71,9 +73,11 @@ export async function authorizeMandateCall(client: PublicClient, factory: Addres
71
73
  const account = first.to;
72
74
  if (calls.some((c) => !isAddressEqual(c.to, account))) throw new Error("every call in one batch goes to the same account");
73
75
  await assertMetalAccount(client, account, factory);
74
- // A fresh 192-bit nonce lane per call: two administration calls in flight,
75
- // or one beside the owner's own operations on lane 0, never claim one nonce.
76
- const lane = hexToBigInt(bytesToHex(crypto.getRandomValues(new Uint8Array(24))));
76
+ // A fresh nonce lane per call: two administration calls in flight, or one
77
+ // beside the owner's own operations on lane 0, never claim one nonce. A
78
+ // quorum's lane keeps the key's top bit clear; set, it is a lane for
79
+ // signatures below the threshold, which the account refuses for the quorum.
80
+ const lane = hexToBigInt(bytesToHex(crypto.getRandomValues(new Uint8Array(24)))) >> 1n;
77
81
  const sequence = await client.readContract({ address: account, abi: metalAccountAbi, functionName: "nonces", args: [account, lane] });
78
82
  const nonce = (lane << 64n) | sequence;
79
83
  const executionData = encodeAbiParameters(parseAbiParameters("(address target, uint256 value, bytes callData)[]"), [calls.map((c) => ({ target: account, value: 0n, callData: c.data }))]);
@@ -81,11 +85,11 @@ export async function authorizeMandateCall(client: PublicClient, factory: Addres
81
85
  // it could have the quorum sign a batch of its own choosing.
82
86
  const digest = hashTypedData({
83
87
  domain: { name: "MetalAccount", version: "1", chainId, verifyingContract: account },
84
- types: { Execute: [{ name: "mode", type: "bytes32" }, { name: "executionData", type: "bytes" }, { name: "nonce", type: "uint256" }] },
88
+ types: { Execute: [{ name: "mode", type: "bytes32" }, { name: "executionData", type: "bytes" }, { name: "nonce", type: "uint256" }, { name: "validUntil", type: "uint64" }] },
85
89
  primaryType: "Execute",
86
- message: { mode: ACCOUNT_BATCH_MODE, executionData, nonce },
90
+ message: { mode: ACCOUNT_BATCH_MODE, executionData, nonce, validUntil },
87
91
  });
88
92
  const signature = await signQuorum(digest);
89
93
  const gas = calls.reduce((sum, c) => sum + c.gas, 0n) + quorumGas(signature);
90
- return { ...first, gas, data: encodeFunctionData({ abi: metalAccountAbi, functionName: "executeSigned", args: [ACCOUNT_BATCH_MODE, executionData, nonce, signature] }) };
94
+ return { ...first, gas, data: encodeFunctionData({ abi: metalAccountAbi, functionName: "executeSigned", args: [ACCOUNT_BATCH_MODE, executionData, nonce, validUntil, signature] }) };
91
95
  }
@@ -3,8 +3,8 @@
3
3
  // offer for this very offer, under the agent key the payer's account binds.
4
4
 
5
5
  import type { x402ResourceServer } from "@x402/core/server";
6
- import { type Address, type Hex, type PublicClient, createPublicClient, getAddress, http, isAddress, isHex, isAddressEqual } from "viem";
7
- import { accountFactoryFor, metalNetwork } from "../networks.js";
6
+ import { type Address, type Hex, type PublicClient, createPublicClient, getAddress, isAddress, isHex, isAddressEqual } from "viem";
7
+ import { accountFactoryFor, metalNetwork, rpcTransport } from "../networks.js";
8
8
  import { assertMetalAccount } from "./account.js";
9
9
  import { referencePrefix } from "./binding.js";
10
10
  import { decodeMandateBlob } from "./l3a.js";
@@ -32,7 +32,7 @@ export type RequireSignedOfferConfig = { accountFactory?: Address } & ({ publicC
32
32
  * route that does not ask is left to the facilitator.
33
33
  */
34
34
  export function requireSignedOffer(server: x402ResourceServer, config: RequireSignedOfferConfig): x402ResourceServer {
35
- const publicClient = "publicClient" in config ? config.publicClient : createPublicClient({ transport: http(config.rpcUrl) });
35
+ const publicClient = "publicClient" in config ? config.publicClient : createPublicClient({ transport: rpcTransport(config.rpcUrl) });
36
36
  const refuse = (reason: string) => ({ abort: true as const, reason });
37
37
  // x402 treats a hook that throws as one that passed, so a check that cannot
38
38
  // finish refuses the payment instead. The refusal reaches the payer, so it