@metalntwx/metal-pay 0.1.1 → 0.3.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.
Files changed (72) hide show
  1. package/README.md +82 -39
  2. package/dist/clearing-abi.d.ts +1836 -0
  3. package/dist/clearing-abi.js +2361 -0
  4. package/dist/clearing-client.d.ts +49 -159
  5. package/dist/clearing-client.js +191 -564
  6. package/dist/clearing-codec.d.ts +57 -0
  7. package/dist/clearing-codec.js +173 -0
  8. package/dist/clearing-custody.d.ts +191 -0
  9. package/dist/clearing-custody.js +490 -0
  10. package/dist/clearing-server.d.ts +30 -49
  11. package/dist/clearing-server.js +87 -67
  12. package/dist/clearing-x402.d.ts +29 -0
  13. package/dist/clearing-x402.js +203 -0
  14. package/dist/client.d.ts +13 -13
  15. package/dist/client.js +45 -18
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.js +4 -1
  18. package/dist/mandates/account.d.ts +5 -21
  19. package/dist/mandates/account.js +28 -10
  20. package/dist/mandates/binding.d.ts +4 -27
  21. package/dist/mandates/binding.js +5 -9
  22. package/dist/mandates/checkout.js +5 -5
  23. package/dist/mandates/client.js +10 -6
  24. package/dist/mandates/l3a.d.ts +2 -2
  25. package/dist/mandates/l3a.js +5 -4
  26. package/dist/mandates/seller.d.ts +1 -1
  27. package/dist/mandates/seller.js +1 -1
  28. package/dist/mandates/verify.d.ts +4 -6
  29. package/dist/mandates/verify.js +42 -66
  30. package/dist/mandates/vi/credential.js +4 -4
  31. package/dist/node.d.ts +11 -0
  32. package/dist/node.js +97 -0
  33. package/package.json +19 -12
  34. package/src/clearing-abi.ts +2361 -0
  35. package/src/clearing-client.ts +303 -0
  36. package/src/clearing-codec.test.ts +57 -0
  37. package/src/clearing-codec.ts +251 -0
  38. package/src/clearing-custody.test.ts +534 -0
  39. package/src/clearing-custody.ts +665 -0
  40. package/src/clearing-fixture.json +37 -0
  41. package/src/clearing-server.test.ts +388 -0
  42. package/src/clearing-server.ts +123 -0
  43. package/src/clearing-x402.test.ts +181 -0
  44. package/src/clearing-x402.ts +279 -0
  45. package/src/client.test.ts +325 -0
  46. package/src/client.ts +156 -0
  47. package/src/index.ts +33 -0
  48. package/src/mandates/account.ts +91 -0
  49. package/src/mandates/binding.ts +280 -0
  50. package/src/mandates/chain.ts +68 -0
  51. package/src/mandates/checkout.ts +368 -0
  52. package/src/mandates/client.ts +159 -0
  53. package/src/mandates/extension.ts +48 -0
  54. package/src/mandates/index.ts +121 -0
  55. package/src/mandates/ingest.ts +42 -0
  56. package/src/mandates/jcs.ts +16 -0
  57. package/src/mandates/l3a.ts +216 -0
  58. package/src/mandates/seller.ts +91 -0
  59. package/src/mandates/verify.ts +532 -0
  60. package/src/mandates/vi/credential.ts +477 -0
  61. package/src/mandates/vi/issue.ts +118 -0
  62. package/src/mandates/vi/sd-jwt.ts +189 -0
  63. package/src/networks.test.ts +16 -0
  64. package/src/networks.ts +152 -0
  65. package/src/node.test.ts +24 -0
  66. package/src/node.ts +96 -0
  67. package/src/server.test.ts +223 -0
  68. package/src/server.ts +77 -0
  69. package/dist/clearing.d.ts +0 -204
  70. package/dist/clearing.js +0 -151
  71. package/dist/file-storage.d.ts +0 -39
  72. package/dist/file-storage.js +0 -195
@@ -1,81 +1,101 @@
1
+ import * as x402Server from "@x402/core/server";
2
+ import { formatUnits, isAddress, sha256, stringToHex } from "viem";
3
+ import { CLEARING_SCHEME, clearingAccount } from "./clearing-codec.js";
4
+ import { CLEARING_MAX_AMOUNT } from "./clearing-custody.js";
1
5
  import { USDZ } from "./networks.js";
2
6
  import { dollarBaseUnits } from "./server.js";
3
- /** The clearing receipt in a settle response, or null when it has none. */
4
- export function clearingReceiptOf(response) {
5
- const receipt = response.extra?.["receipt"];
6
- return receipt &&
7
- typeof receipt.receiptDigest === "string" &&
8
- typeof receipt.receiptSignature === "string" &&
9
- typeof receipt.receipt === "object" &&
10
- receipt.receipt !== null
11
- ? receipt
12
- : null;
13
- }
14
- /** Metal's clearing lane on the seller's side. Register it next to the
15
- * standard scheme, and list both in a route's `accepts`:
16
- *
17
- * server
18
- * .register(network, new ExactEvmScheme().registerMoneyParser(metalUsd(network)))
19
- * .register(network, new ClearingDebitServerScheme());
20
- * accepts: [
21
- * { scheme: "exact", network, payTo, price: "$0.01" },
22
- * { scheme: "clearing-debit", network, payTo, price: "$0.001" },
23
- * ]
7
+ /**
8
+ * The `bajillion` scheme for an `x402ResourceServer`, registered beside
9
+ * `exact` on `network`. A route's `payTo` is the seller's clearing key,
10
+ * `signer.publicKey` of a wallet from `openClearingWallet`, and its `price`
11
+ * is dollars in `token`. The offer names the Clearing deployment the
12
+ * facilitator lists for the network in `/supported`, so a seller takes only
13
+ * payments its facilitator settles.
24
14
  *
25
- * A payer pays by signing a running total; the facilitator has Metal's
26
- * clearing operator admit it, and the seller is credited in clearing, from
27
- * which it withdraws whenever it likes. The Clearing contract comes from the
28
- * facilitator's list of what it supports.
29
- *
30
- * `onReceipt` is handed the operator's signed receipt for every payment
31
- * settled, and again with any later answer for the same payment; store it
32
- * by its `receiptDigest`. */
33
- export class ClearingDebitServerScheme {
15
+ * server.register(network, new ClearingServerScheme(network));
16
+ */
17
+ export class ClearingServerScheme {
18
+ network;
34
19
  token;
35
- scheme = "clearing-debit";
36
- schemeHooks;
37
- constructor(token = USDZ, options = {}) {
20
+ scheme = CLEARING_SCHEME;
21
+ // The money moved when the clearing service issued the receipt, so the
22
+ // facilitator records it before the handler runs: a receipt presented
23
+ // again, or twice at once, is served at most once.
24
+ defaultAssetTransferMethod = "default";
25
+ paymentFlows = {
26
+ default: { supported: ["upfront"], default: "upfront" },
27
+ };
28
+ constructor(network, token = USDZ) {
29
+ this.network = network;
38
30
  this.token = token;
39
- const onReceipt = options.onReceipt;
40
- this.schemeHooks = onReceipt
41
- ? {
42
- onAfterSettle: async ({ result }) => {
43
- const receipt = clearingReceiptOf(result);
44
- if (receipt)
45
- await onReceipt(receipt);
46
- },
47
- }
48
- : {};
31
+ if (!/^eip155:\d+$/.test(network))
32
+ throw new Error(`not a network: ${network}`);
33
+ // A server without payment flows ignores `paymentFlows` and settles after the handler.
34
+ if (!("resolvePaymentFlow" in x402Server))
35
+ throw new Error("ClearingServerScheme needs @x402/core 2.28 or later, which settles a clearing payment before the handler runs");
49
36
  }
50
37
  async parsePrice(price) {
51
- if (typeof price === "object")
52
- return price;
53
- return {
54
- amount: dollarBaseUnits(price, this.token),
55
- asset: this.token.address,
56
- };
38
+ if (typeof price === "object" && price !== null && "amount" in price) {
39
+ let amount;
40
+ try {
41
+ amount = BigInt(price.amount);
42
+ }
43
+ catch {
44
+ throw new Error(`not a price: ${price.amount}`);
45
+ }
46
+ if (amount <= 0n)
47
+ throw new Error(`not a price: ${price.amount}`);
48
+ return { amount: this.payable(amount).toString(), asset: price.asset ?? this.token.address, extra: price.extra ?? {} };
49
+ }
50
+ return { amount: this.payable(BigInt(dollarBaseUnits(price, this.token))).toString(), asset: this.token.address };
51
+ }
52
+ /** Clearing counts whole clearing units, at most 2^64 - 1 of them in all.
53
+ * The unit is the one `Clearing.unit()` derives from the token's decimals
54
+ * at deploy: a millionth of the token, or its base unit at six decimals or
55
+ * fewer. */
56
+ payable(amount) {
57
+ const decimals = this.token.decimals;
58
+ const unit = decimals > 6 ? 10n ** BigInt(decimals - 6) : 1n;
59
+ if (amount % unit !== 0n)
60
+ throw new Error(`a clearing price is a whole number of ${formatUnits(unit, decimals)} ${this.token.symbol}`);
61
+ if (amount / unit > CLEARING_MAX_AMOUNT)
62
+ throw new Error(`a clearing price is at most ${formatUnits(CLEARING_MAX_AMOUNT * unit, decimals)} ${this.token.symbol}`);
63
+ return amount;
57
64
  }
58
65
  getAssetDecimals() {
59
66
  return this.token.decimals;
60
67
  }
61
68
  async enhancePaymentRequirements(requirements, supportedKind) {
62
- const contract = supportedKind.extra?.["contract"];
63
- if (typeof contract !== "string")
64
- throw new Error(`the facilitator offers no Clearing contract on ${requirements.network}`);
65
- return {
66
- ...requirements,
67
- extra: {
68
- ...requirements.extra,
69
- // The EIP-712 domain the payer signs its Debit under.
70
- name: "MetalClearing",
71
- version: "1",
72
- clearing: {
73
- contract,
74
- // Binds a Debit to this seller. The same in the 402 and when the
75
- // paid request is matched against it, so it must not vary.
76
- ref: `0x${"0".repeat(24)}${requirements.payTo.slice(2).toLowerCase()}`,
77
- },
78
- },
79
- };
69
+ if (requirements.network !== this.network)
70
+ throw new Error(`this clearing scheme is on ${this.network}; the route offers it on ${requirements.network}`);
71
+ try {
72
+ clearingAccount(requirements.payTo);
73
+ }
74
+ catch {
75
+ throw new Error(`a clearing offer's payTo is the seller's clearing key (signer.publicKey of a wallet from openClearingWallet); got ${requirements.payTo}`);
76
+ }
77
+ if (requirements.asset.toLowerCase() !== this.token.address.toLowerCase())
78
+ throw new Error(`this clearing scheme prices in ${this.token.address}; the route offers ${requirements.asset}`);
79
+ const prefix = requirements.extra?.invoicePrefix;
80
+ if (prefix !== undefined && (typeof prefix !== "string" || !/^0x[0-9a-f]{64}$/.test(prefix)))
81
+ throw new Error(`a route's invoicePrefix is 32 bytes of lowercase hex; got ${String(prefix)}`);
82
+ const { settlement, asset, serviceUrl } = supportedKind.extra ?? {};
83
+ if (typeof settlement !== "string" ||
84
+ !isAddress(settlement) ||
85
+ typeof asset !== "string" ||
86
+ asset.toLowerCase() !== this.token.address.toLowerCase() ||
87
+ typeof serviceUrl !== "string" ||
88
+ !URL.canParse(serviceUrl))
89
+ throw new Error(`the facilitator settles no clearing deployment in ${this.token.symbol} on ${this.network}`);
90
+ return { ...requirements, extra: { ...requirements.extra, settlement, serviceUrl } };
91
+ }
92
+ /** Binds this network's offers' invoices to the resource asked for, at
93
+ * the challenge and again when a payment for it is verified. A route that
94
+ * sets its own `invoicePrefix` keeps it. */
95
+ async enrichPaymentRequiredResponse(ctx) {
96
+ const invoicePrefix = sha256(stringToHex(ctx.resourceInfo.url));
97
+ return ctx.requirements.map((offer) => offer.scheme === CLEARING_SCHEME && offer.network === this.network && offer.extra?.invoicePrefix === undefined
98
+ ? { ...offer, extra: { ...offer.extra, invoicePrefix } }
99
+ : offer);
80
100
  }
81
101
  }
@@ -0,0 +1,29 @@
1
+ import type { PaymentRequirements, PaymentPayload } from "@x402/core/types";
2
+ import type { x402Client } from "@x402/core/client";
3
+ import { type Hex } from "viem";
4
+ import { type ClearingInvoice } from "./clearing-codec.js";
5
+ import type { ClearingPaid, createClearingClient } from "./clearing-client.js";
6
+ export interface ClearingSeller {
7
+ network: `eip155:${number}`;
8
+ asset: Hex;
9
+ settlement: Hex;
10
+ recipient: Hex;
11
+ serviceUrl: string;
12
+ }
13
+ /** A resource-bound invoice namespace; the payer appends a unique payment id. */
14
+ export declare function clearingRequirements(seller: ClearingSeller, amount: bigint, resource: string): PaymentRequirements;
15
+ export declare function clearingInvoice(requirements: PaymentRequirements, id: string): ClearingInvoice;
16
+ export declare function clearingPaymentOf(payload: Pick<PaymentPayload, "payload">): ClearingPaid | null;
17
+ export declare function clearingPaymentMismatch(paid: ClearingPaid, requirements: PaymentRequirements): string | null;
18
+ export declare function clearingPaymentId(paid: ClearingPaid): string;
19
+ /** Both the RPC and service URL must come from the seller's trusted configuration. */
20
+ export declare function verifyClearingPayment(paid: ClearingPaid, requirements: PaymentRequirements, config: {
21
+ rpcUrl: string;
22
+ serviceUrl: string;
23
+ settlement: Hex;
24
+ }): Promise<{
25
+ payer: `0x${string}`;
26
+ id: string;
27
+ status: "preconfirmed";
28
+ }>;
29
+ export declare function registerClearingClient(client: x402Client, network: `eip155:${number}`, payer: Pick<ReturnType<typeof createClearingClient>, "pay" | "payable" | "pendingInvoice">): x402Client;
@@ -0,0 +1,203 @@
1
+ import { createPublicClient, http, sha256, stringToHex } from "viem";
2
+ import { clearingAbi } from "./clearing-abi.js";
3
+ import { CLEARING_SCHEME, clearingUnits, decodeSend, invoiceMessage, verifyClearingSignature, verifyReceipt, outgoingRoot, } from "./clearing-codec.js";
4
+ /** A resource-bound invoice namespace; the payer appends a unique payment id. */
5
+ export function clearingRequirements(seller, amount, resource) {
6
+ if (amount <= 0n)
7
+ throw new Error("invalid clearing price");
8
+ return {
9
+ scheme: CLEARING_SCHEME,
10
+ network: seller.network,
11
+ asset: seller.asset,
12
+ amount: amount.toString(),
13
+ payTo: seller.recipient,
14
+ maxTimeoutSeconds: 60,
15
+ extra: {
16
+ settlement: seller.settlement,
17
+ serviceUrl: seller.serviceUrl,
18
+ invoicePrefix: sha256(stringToHex(resource)),
19
+ },
20
+ };
21
+ }
22
+ export function clearingInvoice(requirements, id) {
23
+ const extra = requirements.extra;
24
+ if (requirements.scheme !== CLEARING_SCHEME ||
25
+ !/^0x[0-9a-fA-F]{66}$/.test(requirements.payTo) ||
26
+ typeof extra?.settlement !== "string" ||
27
+ !/^0x[0-9a-fA-F]{40}$/.test(extra.settlement) ||
28
+ typeof extra.invoicePrefix !== "string" ||
29
+ !/^0x[0-9a-f]{64}$/.test(extra.invoicePrefix) ||
30
+ !id.startsWith(extra.invoicePrefix + ":"))
31
+ throw new Error("invalid clearing requirement");
32
+ return {
33
+ id,
34
+ network: requirements.network,
35
+ asset: requirements.asset,
36
+ settlement: extra.settlement,
37
+ recipient: requirements.payTo,
38
+ amount: requirements.amount,
39
+ };
40
+ }
41
+ export function clearingPaymentOf(payload) {
42
+ try {
43
+ const paid = payload.payload;
44
+ const body = decodeSend(paid.receipt.body);
45
+ invoiceMessage(paid.invoice, paid.receipt.body);
46
+ const entries = paid.entries.map((e) => ({
47
+ ...e,
48
+ cumulative: BigInt(e.cumulative),
49
+ count: BigInt(e.count),
50
+ }));
51
+ if (entries.length > 1024 ||
52
+ outgoingRoot(entries) !== body.vector ||
53
+ entries.reduce((n, e) => n + e.cumulative, 0n) !== body.debit ||
54
+ !verifyClearingSignature(body.payer, "_METAL_CLEARING_INVOICE", invoiceMessage(paid.invoice, paid.receipt.body), paid.invoiceSignature))
55
+ return null;
56
+ return paid;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ export function clearingPaymentMismatch(paid, requirements) {
63
+ try {
64
+ return invoiceMessage(clearingInvoice(requirements, paid.invoice.id), paid.receipt.body) === invoiceMessage(paid.invoice, paid.receipt.body)
65
+ ? null
66
+ : "invoice";
67
+ }
68
+ catch {
69
+ return "invoice";
70
+ }
71
+ }
72
+ export function clearingPaymentId(paid) {
73
+ return `${paid.invoice.network}:bajillion:${paid.invoice.settlement.toLowerCase()}:${sha256(paid.receipt.body)}`;
74
+ }
75
+ /** Both the RPC and service URL must come from the seller's trusted configuration. */
76
+ export async function verifyClearingPayment(paid, requirements, config) {
77
+ if (clearingPaymentMismatch(paid, requirements) ||
78
+ paid.invoice.settlement.toLowerCase() !== config.settlement.toLowerCase())
79
+ throw new Error("clearing invoice mismatch");
80
+ if (!clearingPaymentOf({ payload: paid }))
81
+ throw new Error("invalid clearing payment");
82
+ const send = decodeSend(paid.receipt.body);
83
+ const client = createPublicClient({ transport: http(config.rpcUrl) });
84
+ const [context, fault, asset, chainId, unit] = await Promise.all([
85
+ client.readContract({
86
+ address: config.settlement,
87
+ abi: clearingAbi,
88
+ functionName: "contextOf",
89
+ args: [send.epoch],
90
+ }),
91
+ client.readContract({
92
+ address: config.settlement,
93
+ abi: clearingAbi,
94
+ functionName: "fault",
95
+ }),
96
+ client.readContract({
97
+ address: config.settlement,
98
+ abi: clearingAbi,
99
+ functionName: "token",
100
+ }),
101
+ client.getChainId(),
102
+ client.readContract({
103
+ address: config.settlement,
104
+ abi: clearingAbi,
105
+ functionName: "unit",
106
+ }),
107
+ ]);
108
+ if (fault ||
109
+ `eip155:${chainId}` !== paid.invoice.network ||
110
+ asset.toLowerCase() !== paid.invoice.asset.toLowerCase() ||
111
+ context.anchor !== send.anchor ||
112
+ !verifyReceipt(paid.receipt, context.operatorKey))
113
+ throw new Error("invalid clearing receipt");
114
+ const previous = paid.previous;
115
+ let before = [];
116
+ if (send.sequence === 0n) {
117
+ if (previous)
118
+ throw new Error("unexpected predecessor receipt");
119
+ }
120
+ else {
121
+ if (!previous || !verifyReceipt(previous.receipt, context.operatorKey))
122
+ throw new Error("missing predecessor receipt");
123
+ const prior = decodeSend(previous.receipt.body);
124
+ before = previous.entries;
125
+ if (prior.epoch !== send.epoch ||
126
+ prior.anchor !== send.anchor ||
127
+ prior.payer !== send.payer ||
128
+ prior.sequence + 1n !== send.sequence ||
129
+ outgoingRoot(before.map((e) => ({
130
+ ...e,
131
+ cumulative: BigInt(e.cumulative),
132
+ count: BigInt(e.count),
133
+ }))) !== prior.vector)
134
+ throw new Error("invalid predecessor vector");
135
+ }
136
+ const expected = new Map(before.map((e) => [e.recipient.toLowerCase(), { ...e }]));
137
+ const key = paid.invoice.recipient.toLowerCase();
138
+ const old = expected.get(key);
139
+ // The invoice is in token base units; the vector counts clearing units.
140
+ expected.set(key, {
141
+ recipient: paid.invoice.recipient,
142
+ cumulative: ((old ? BigInt(old.cumulative) : 0n) +
143
+ clearingUnits(BigInt(paid.invoice.amount), unit)).toString(),
144
+ count: ((old ? BigInt(old.count) : 0n) + 1n).toString(),
145
+ });
146
+ if (expected.size !== paid.entries.length ||
147
+ paid.entries.some((e) => {
148
+ const wanted = expected.get(e.recipient.toLowerCase());
149
+ return (!wanted ||
150
+ wanted.count !== e.count ||
151
+ wanted.cumulative !== e.cumulative);
152
+ }))
153
+ throw new Error("invoice is not the incremental payment");
154
+ // The operator's durable invoice journal attests the incremental charge,
155
+ // which cannot be inferred from a cumulative vector alone.
156
+ const response = await fetch(new URL(`/receipt/${send.payer}/${encodeURIComponent(paid.invoice.id)}?epoch=${send.epoch}`, config.serviceUrl), { signal: AbortSignal.timeout(10_000) });
157
+ if (!response.ok)
158
+ throw new Error("clearing receipt unavailable");
159
+ const recorded = (await response.json());
160
+ if (!recorded ||
161
+ recorded.receipt.body !== paid.receipt.body ||
162
+ recorded.receipt.operatorSignature !== paid.receipt.operatorSignature ||
163
+ invoiceMessage(recorded.invoice, recorded.receipt.body) !==
164
+ invoiceMessage(paid.invoice, paid.receipt.body))
165
+ throw new Error("clearing invoice not retained");
166
+ return {
167
+ payer: send.payer,
168
+ id: clearingPaymentId(paid),
169
+ status: "preconfirmed",
170
+ };
171
+ }
172
+ export function registerClearingClient(client, network, payer) {
173
+ client.register(network, {
174
+ scheme: CLEARING_SCHEME,
175
+ async createPaymentPayload(x402Version, requirements) {
176
+ const pending = await payer.pendingInvoice();
177
+ const suffix = globalThis.crypto.randomUUID();
178
+ const invoice = clearingInvoice(requirements, pending?.id ?? `${requirements.extra?.invoicePrefix}:${suffix}`);
179
+ const paid = await payer.pay(invoice);
180
+ return {
181
+ x402Version,
182
+ payload: paid,
183
+ };
184
+ },
185
+ });
186
+ // The stock client keeps only `authorization` offers when any remain after
187
+ // its policies, and a clearing offer is `upfront`: so a payer set up for
188
+ // clearing keeps the clearing offers its wallet can pay, when there are any.
189
+ client.registerPolicy((_, offers) => {
190
+ const clearing = offers.filter((offer) => {
191
+ if (offer.scheme !== CLEARING_SCHEME || offer.network !== network)
192
+ return false;
193
+ try {
194
+ return payer.payable(clearingInvoice(offer, `${offer.extra?.invoicePrefix}:`));
195
+ }
196
+ catch {
197
+ return false;
198
+ }
199
+ });
200
+ return clearing.length > 0 ? clearing : offers;
201
+ });
202
+ return client;
203
+ }
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { x402Client } from "@x402/core/client";
2
2
  import { wrapFetchWithPayment } from "@x402/fetch";
3
3
  import type { ClientEvmSigner } from "@x402/evm";
4
- import type { ClearingDebitScheme } from "./clearing-client.js";
4
+ import type { Network } from "@x402/core/types";
5
5
  import { type MetalNetwork, type MetalNetworkName } from "./networks.js";
6
6
  /**
7
7
  * What signs a payment: a viem local account (`privateKeyToAccount`) or
@@ -17,8 +17,8 @@ export interface RegisterMetalNetworksConfig {
17
17
  * Register Metal's devnet and testnet with an `@x402/core` client for the
18
18
  * `exact` scheme. The client signs an `exact` offer on `eip155:1337` or
19
19
  * `eip155:63825` only when it is in `*USDZ` under USDZ's EIP-712 domain, and
20
- * refuses any other before signing. Offers in other schemes, such as
21
- * clearing, are left to the schemes registered for them.
20
+ * refuses any other before signing. Offers in other schemes are left to
21
+ * the schemes explicitly registered for them.
22
22
  */
23
23
  export declare function registerMetalNetworks(client: x402Client, { signer, networks }: RegisterMetalNetworksConfig): x402Client;
24
24
  /** The most a payment client pays for one offer unless told otherwise: 1 `*USDZ`, in base units. */
@@ -34,18 +34,18 @@ export interface MetalPaymentClientConfig {
34
34
  * by default. An offer above it is refused before anything is signed.
35
35
  */
36
36
  maxAmount?: bigint;
37
- /**
38
- * Metal clearing, for sellers that offer it: the client pays by clearing
39
- * and, while custody is short, by `exact` instead.
40
- */
41
- clearing?: ClearingDebitScheme;
42
37
  }
43
38
  /**
44
- * `@x402/core` from 2.23 pays only assets in its own table unless its spend
45
- * controls list others. This lists `asset` on `network`, capped at
46
- * `maxAmountPerPayment` base units when one is given.
39
+ * The stock client pays only assets in its own table unless its spend
40
+ * controls list others. This lists `asset` on `network`.
41
+ */
42
+ export declare function allowAsset(client: x402Client, network: Network, asset: string): void;
43
+ /**
44
+ * `fetch` that follows the redirects of a request carrying a payment only
45
+ * within the origin it was made for, and answers a redirect to any other
46
+ * origin as it is, so no other origin receives the payment.
47
47
  */
48
- export declare function allowAsset(client: x402Client, network: string, asset: string, maxAmountPerPayment?: bigint): void;
48
+ export declare function keepPaymentOnOrigin(fetch: typeof globalThis.fetch): typeof globalThis.fetch;
49
49
  export interface MetalPaymentClient {
50
50
  /** `fetch` that answers a 402 by paying and retrying, once. */
51
51
  fetchWithPayment: ReturnType<typeof wrapFetchWithPayment>;
@@ -53,4 +53,4 @@ export interface MetalPaymentClient {
53
53
  network: MetalNetwork;
54
54
  }
55
55
  /** One call to pay: `const { fetchWithPayment } = createMetalPaymentClient({ account, network: "devnet" })`. */
56
- export declare function createMetalPaymentClient({ account, network, fetch, maxAmount, clearing, }: MetalPaymentClientConfig): MetalPaymentClient;
56
+ export declare function createMetalPaymentClient({ account, network, fetch, maxAmount, }: MetalPaymentClientConfig): MetalPaymentClient;
package/dist/client.js CHANGED
@@ -6,8 +6,8 @@ import { METAL_NETWORKS, USDZ, formatUsdz, usdzBaseUnits, resolveNetwork, } from
6
6
  * Register Metal's devnet and testnet with an `@x402/core` client for the
7
7
  * `exact` scheme. The client signs an `exact` offer on `eip155:1337` or
8
8
  * `eip155:63825` only when it is in `*USDZ` under USDZ's EIP-712 domain, and
9
- * refuses any other before signing. Offers in other schemes, such as
10
- * clearing, are left to the schemes registered for them.
9
+ * refuses any other before signing. Offers in other schemes are left to
10
+ * the schemes explicitly registered for them.
11
11
  */
12
12
  export function registerMetalNetworks(client, { signer, networks }) {
13
13
  const scheme = new ExactEvmScheme(signer);
@@ -19,7 +19,10 @@ export function registerMetalNetworks(client, { signer, networks }) {
19
19
  if (scheme !== "exact" || !registered.includes(network))
20
20
  return;
21
21
  if (asset.toLowerCase() !== USDZ.address.toLowerCase()) {
22
- return { abort: true, reason: `offer is in ${asset}, not ${USDZ.symbol} (${USDZ.address})` };
22
+ return {
23
+ abort: true,
24
+ reason: `offer is in ${asset}, not ${USDZ.symbol} (${USDZ.address})`,
25
+ };
23
26
  }
24
27
  if (extra?.name !== USDZ.name || extra?.version !== USDZ.version) {
25
28
  return {
@@ -33,27 +36,51 @@ export function registerMetalNetworks(client, { signer, networks }) {
33
36
  /** The most a payment client pays for one offer unless told otherwise: 1 `*USDZ`, in base units. */
34
37
  export const DEFAULT_MAX_PAYMENT = BigInt(usdzBaseUnits("1"));
35
38
  /**
36
- * `@x402/core` from 2.23 pays only assets in its own table unless its spend
37
- * controls list others. This lists `asset` on `network`, capped at
38
- * `maxAmountPerPayment` base units when one is given.
39
+ * The stock client pays only assets in its own table unless its spend
40
+ * controls list others. This lists `asset` on `network`.
39
41
  */
40
- export function allowAsset(client, network, asset, maxAmountPerPayment) {
41
- const controlled = client;
42
- controlled.setSpendControls?.({
43
- allowedAssets: [{ network, asset, ...(maxAmountPerPayment !== undefined && { maxAmountPerPayment: maxAmountPerPayment.toString() }) }],
44
- });
42
+ export function allowAsset(client, network, asset) {
43
+ client.setSpendControls({ allowedAssets: [{ network, asset }] });
44
+ }
45
+ const REDIRECTS = new Set([301, 302, 303, 307, 308]);
46
+ /**
47
+ * `fetch` that follows the redirects of a request carrying a payment only
48
+ * within the origin it was made for, and answers a redirect to any other
49
+ * origin as it is, so no other origin receives the payment.
50
+ */
51
+ export function keepPaymentOnOrigin(fetch) {
52
+ return async (input, init) => {
53
+ const request = new Request(input, init);
54
+ if (!request.headers.has("PAYMENT-SIGNATURE") && !request.headers.has("X-PAYMENT"))
55
+ return fetch(request);
56
+ const body = request.body === null ? undefined : await request.arrayBuffer();
57
+ let url = new URL(request.url);
58
+ let method = request.method;
59
+ for (let hops = 0;; hops++) {
60
+ const sendsBody = method !== "GET" && method !== "HEAD";
61
+ const response = await fetch(new Request(url, { method, headers: request.headers, ...(sendsBody && body !== undefined && { body }), signal: request.signal, redirect: "manual" }));
62
+ const location = response.headers.get("location");
63
+ if (!REDIRECTS.has(response.status) || location === null || hops === 20)
64
+ return response;
65
+ const next = new URL(location, url);
66
+ if (next.origin !== url.origin)
67
+ return response;
68
+ if (response.status === 303 || (response.status <= 302 && method === "POST"))
69
+ method = "GET";
70
+ url = next;
71
+ }
72
+ };
45
73
  }
46
74
  /** One call to pay: `const { fetchWithPayment } = createMetalPaymentClient({ account, network: "devnet" })`. */
47
- export function createMetalPaymentClient({ account, network, fetch = globalThis.fetch, maxAmount = DEFAULT_MAX_PAYMENT, clearing, }) {
75
+ export function createMetalPaymentClient({ account, network, fetch = globalThis.fetch, maxAmount = DEFAULT_MAX_PAYMENT, }) {
48
76
  const resolved = resolveNetwork(network);
49
- // The clearing scheme's selector chooses between its offer and the standard one.
50
- const client = registerMetalNetworks(new x402Client(clearing?.prefer), {
77
+ const client = registerMetalNetworks(new x402Client(), {
51
78
  signer: account,
52
79
  networks: [resolved],
53
80
  });
54
- if (clearing)
55
- client.register(resolved.network, clearing);
56
- allowAsset(client, resolved.network, USDZ.address, maxAmount);
81
+ // The hooks are the guard: the client's own spend controls would refuse
82
+ // first, naming nothing of the offer, so they stand aside on this client.
83
+ client.setSpendControls({ allowedAssets: true });
57
84
  // @x402/fetch signs whatever amount an offer asks; this hook is the payer's spending guard.
58
85
  client.onBeforePaymentCreation(async ({ selectedRequirements: { amount } }) => {
59
86
  if (BigInt(amount) <= maxAmount)
@@ -64,7 +91,7 @@ export function createMetalPaymentClient({ account, network, fetch = globalThis.
64
91
  };
65
92
  });
66
93
  return {
67
- fetchWithPayment: wrapFetchWithPayment(clearing ? clearing.wrapFetch(fetch) : fetch, client),
94
+ fetchWithPayment: wrapFetchWithPayment(keepPaymentOnOrigin(fetch), client),
68
95
  client,
69
96
  network: resolved,
70
97
  };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export { METAL_NETWORKS, USDZ, formatUsdz, metalNetwork, usdzBaseUnits, accountFactoryFor, rpcUrlFor, type DollarToken, type MetalNetwork, type MetalNetworkName, } from "./networks.js";
2
2
  export { DEFAULT_MAX_PAYMENT, createMetalPaymentClient, registerMetalNetworks, type MetalPaymentClient, type MetalPaymentClientConfig, type MetalPaymentSigner, type RegisterMetalNetworksConfig, } from "./client.js";
3
3
  export { metalUsd, registerMetalScheme, sellerToken, type RegisterMetalSchemeConfig, } from "./server.js";
4
- export * from "./clearing.js";
4
+ export * from "./clearing-codec.js";
5
+ export { clearingAbi } from "./clearing-abi.js";
5
6
  export * from "./clearing-client.js";
7
+ export * from "./clearing-x402.js";
6
8
  export * from "./clearing-server.js";
9
+ export * from "./clearing-custody.js";
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  export { METAL_NETWORKS, USDZ, formatUsdz, metalNetwork, usdzBaseUnits, accountFactoryFor, rpcUrlFor, } from "./networks.js";
2
2
  export { DEFAULT_MAX_PAYMENT, createMetalPaymentClient, registerMetalNetworks, } from "./client.js";
3
3
  export { metalUsd, registerMetalScheme, sellerToken, } from "./server.js";
4
- export * from "./clearing.js";
4
+ export * from "./clearing-codec.js";
5
+ export { clearingAbi } from "./clearing-abi.js";
5
6
  export * from "./clearing-client.js";
7
+ export * from "./clearing-x402.js";
6
8
  export * from "./clearing-server.js";
9
+ export * from "./clearing-custody.js";
@@ -57,23 +57,6 @@ export declare const metalAccountAbi: readonly [{
57
57
  readonly outputs: readonly [{
58
58
  readonly type: "uint256";
59
59
  }];
60
- }, {
61
- readonly name: "executeDigest";
62
- readonly type: "function";
63
- readonly stateMutability: "view";
64
- readonly inputs: readonly [{
65
- readonly type: "bytes32";
66
- readonly name: "mode";
67
- }, {
68
- readonly type: "bytes";
69
- readonly name: "executionData";
70
- }, {
71
- readonly type: "uint256";
72
- readonly name: "nonce";
73
- }];
74
- readonly outputs: readonly [{
75
- readonly type: "bytes32";
76
- }];
77
60
  }, {
78
61
  readonly name: "executeSigned";
79
62
  readonly type: "function";
@@ -100,7 +83,8 @@ export declare function metalAccountCode(implementation: Address): Hex;
100
83
  export declare function assertMetalAccount(client: PublicClient, account: Address, factory: Address, blockNumber?: bigint): Promise<void>;
101
84
  /** The gas an executeSigned adds to the call it carries, by the quorum signature's signers. */
102
85
  export declare function quorumGas(signature: Hex): bigint;
103
- /** One self-call approved by the account's existing ERC-7913 quorum. The signing
104
- * callback may collect mixed keys, weights and passkeys; it returns the normal
105
- * abi.encode(bytes[] signers, bytes[] signatures) account signature. */
106
- export declare function authorizeMandateCall(client: PublicClient, factory: Address, call: MandateCall, signQuorum: (digest: Hex) => Promise<Hex>): Promise<MandateCall>;
86
+ /** Self-calls approved by the account's existing ERC-7913 quorum, in one batch
87
+ * that runs whole or not at all. The signing callback may collect mixed keys,
88
+ * weights and passkeys; it returns the normal abi.encode(bytes[] signers,
89
+ * bytes[] signatures) account signature. */
90
+ export declare function authorizeMandateCall(client: PublicClient, factory: Address, call: MandateCall | readonly MandateCall[], signQuorum: (digest: Hex) => Promise<Hex>): Promise<MandateCall>;