@metalntwx/metal-pay 0.2.0 → 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 (70) hide show
  1. package/README.md +72 -33
  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 +7 -13
  15. package/dist/client.js +16 -18
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.js +4 -1
  18. package/dist/mandates/binding.d.ts +4 -27
  19. package/dist/mandates/binding.js +5 -9
  20. package/dist/mandates/checkout.js +5 -5
  21. package/dist/mandates/client.js +4 -3
  22. package/dist/mandates/l3a.d.ts +2 -2
  23. package/dist/mandates/l3a.js +5 -4
  24. package/dist/mandates/seller.d.ts +1 -1
  25. package/dist/mandates/seller.js +1 -1
  26. package/dist/mandates/verify.d.ts +4 -6
  27. package/dist/mandates/verify.js +42 -66
  28. package/dist/mandates/vi/credential.js +4 -4
  29. package/dist/node.d.ts +11 -0
  30. package/dist/node.js +97 -0
  31. package/package.json +18 -11
  32. package/src/clearing-abi.ts +2361 -0
  33. package/src/clearing-client.ts +303 -0
  34. package/src/clearing-codec.test.ts +57 -0
  35. package/src/clearing-codec.ts +251 -0
  36. package/src/clearing-custody.test.ts +534 -0
  37. package/src/clearing-custody.ts +665 -0
  38. package/src/clearing-fixture.json +37 -0
  39. package/src/clearing-server.test.ts +388 -0
  40. package/src/clearing-server.ts +123 -0
  41. package/src/clearing-x402.test.ts +181 -0
  42. package/src/clearing-x402.ts +279 -0
  43. package/src/client.test.ts +325 -0
  44. package/src/client.ts +156 -0
  45. package/src/index.ts +33 -0
  46. package/src/mandates/account.ts +91 -0
  47. package/src/mandates/binding.ts +280 -0
  48. package/src/mandates/chain.ts +68 -0
  49. package/src/mandates/checkout.ts +368 -0
  50. package/src/mandates/client.ts +159 -0
  51. package/src/mandates/extension.ts +48 -0
  52. package/src/mandates/index.ts +121 -0
  53. package/src/mandates/ingest.ts +42 -0
  54. package/src/mandates/jcs.ts +16 -0
  55. package/src/mandates/l3a.ts +216 -0
  56. package/src/mandates/seller.ts +91 -0
  57. package/src/mandates/verify.ts +532 -0
  58. package/src/mandates/vi/credential.ts +477 -0
  59. package/src/mandates/vi/issue.ts +118 -0
  60. package/src/mandates/vi/sd-jwt.ts +189 -0
  61. package/src/networks.test.ts +16 -0
  62. package/src/networks.ts +152 -0
  63. package/src/node.test.ts +24 -0
  64. package/src/node.ts +96 -0
  65. package/src/server.test.ts +223 -0
  66. package/src/server.ts +77 -0
  67. package/dist/clearing.d.ts +0 -204
  68. package/dist/clearing.js +0 -151
  69. package/dist/file-storage.d.ts +0 -39
  70. package/dist/file-storage.js +0 -195
@@ -0,0 +1,223 @@
1
+ import { createServer, type Server } from "node:http";
2
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
3
+ import { Hono } from "hono";
4
+ import { paymentMiddleware } from "@x402/hono";
5
+ import { ExactEvmScheme } from "@x402/evm/exact/server";
6
+ import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
7
+ import {
8
+ decodePaymentRequiredHeader,
9
+ decodePaymentResponseHeader,
10
+ encodePaymentSignatureHeader,
11
+ } from "@x402/core/http";
12
+ import type { PaymentPayload, PaymentRequirements } from "@x402/core/types";
13
+ import { USDZ } from "./networks.js";
14
+ import { metalUsd, registerMetalScheme } from "./server.js";
15
+
16
+ const PAY_TO = "0x000000000000000000000000000000000000b22b";
17
+ const TX = `0x${"ab".repeat(32)}`;
18
+
19
+ const verified: PaymentPayload[] = [];
20
+ const facilitator = createServer((req, res) => {
21
+ const chunks: Buffer[] = [];
22
+ req.on("data", (c: Buffer) => chunks.push(c));
23
+ req.on("end", () => {
24
+ const body = (chunks.length ? JSON.parse(Buffer.concat(chunks).toString()) : {}) as {
25
+ paymentPayload?: PaymentPayload & { payload: { authorization?: { from?: string } } };
26
+ };
27
+ const payer = body.paymentPayload?.payload.authorization?.from;
28
+ const reply = (status: number, json: unknown) => {
29
+ res.writeHead(status, { "content-type": "application/json" });
30
+ res.end(JSON.stringify(json));
31
+ };
32
+ if (req.url === "/supported") {
33
+ return reply(200, {
34
+ kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:1337" }],
35
+ extensions: [],
36
+ signers: { "eip155:*": ["0x00000000000000000000000000000000000000fa"] },
37
+ });
38
+ }
39
+ if (req.url === "/verify") {
40
+ if (body.paymentPayload) verified.push(body.paymentPayload);
41
+ if (payer?.endsWith("dead")) return reply(200, { isValid: false, invalidReason: "insufficient_funds", payer });
42
+ return reply(200, { isValid: true, payer });
43
+ }
44
+ if (req.url === "/settle") {
45
+ if (payer?.endsWith("f00d")) {
46
+ return reply(200, {
47
+ success: false,
48
+ errorReason: "transaction_failed",
49
+ errorMessage: "transferWithAuthorization reverted",
50
+ transaction: TX,
51
+ network: "eip155:1337",
52
+ payer,
53
+ });
54
+ }
55
+ return reply(200, { success: true, transaction: TX, network: "eip155:1337", payer });
56
+ }
57
+ reply(404, {});
58
+ });
59
+ });
60
+
61
+ let app: Hono;
62
+
63
+ beforeAll(async () => {
64
+ const facilitatorUrl = await listen(facilitator);
65
+ const server = registerMetalScheme(
66
+ new x402ResourceServer(new HTTPFacilitatorClient({ url: facilitatorUrl })),
67
+ { networks: ["devnet"] },
68
+ );
69
+ app = new Hono();
70
+ app.use(
71
+ paymentMiddleware(
72
+ {
73
+ "GET /quote": {
74
+ accepts: { scheme: "exact", network: "eip155:1337", payTo: PAY_TO, price: "0.01", maxTimeoutSeconds: 60 },
75
+ description: "a quote",
76
+ },
77
+ },
78
+ server,
79
+ ),
80
+ );
81
+ app.get("/quote", (c) => c.json({ quote: "hono" }));
82
+ app.get("/free", (c) => c.json({ free: true }));
83
+ });
84
+
85
+ afterAll(() => new Promise<void>((resolve) => facilitator.close(() => resolve())));
86
+
87
+ const fetchQuote = async (headers?: Record<string, string>) => app.request("http://seller.local/quote", { headers });
88
+
89
+ function paymentFor(requirements: PaymentRequirements, from: string): PaymentPayload {
90
+ return {
91
+ x402Version: 2,
92
+ accepted: requirements,
93
+ payload: {
94
+ signature: `0x${"11".repeat(65)}`,
95
+ authorization: {
96
+ from,
97
+ to: requirements.payTo,
98
+ value: requirements.amount,
99
+ validAfter: "0",
100
+ validBefore: "9999999999",
101
+ nonce: `0x${"22".repeat(32)}`,
102
+ },
103
+ },
104
+ };
105
+ }
106
+
107
+ async function challenge(): Promise<PaymentRequirements> {
108
+ const unpaid = await fetchQuote();
109
+ expect(unpaid.status).toBe(402);
110
+ const required = decodePaymentRequiredHeader(unpaid.headers.get("payment-required") as string);
111
+ expect(required.accepts).toHaveLength(1);
112
+ expect(required.accepts[0]).toMatchObject({
113
+ scheme: "exact",
114
+ network: "eip155:1337",
115
+ asset: USDZ.address,
116
+ amount: "10000000000000000",
117
+ payTo: PAY_TO,
118
+ maxTimeoutSeconds: 60,
119
+ extra: { name: USDZ.name, version: USDZ.version, primaryType: "TransferWithAuthorization" },
120
+ });
121
+ return required.accepts[0] as PaymentRequirements;
122
+ }
123
+
124
+ describe("registerMetalScheme under @x402/hono", () => {
125
+ it("leaves free routes alone", async () => {
126
+ const res = await app.request("http://seller.local/free");
127
+ expect(res.status).toBe(200);
128
+ expect(await res.json()).toEqual({ free: true });
129
+ });
130
+
131
+ it("challenges in *USDZ, then serves and settles a verified payment", async () => {
132
+ const offer = await challenge();
133
+ const paid = await fetchQuote({
134
+ "payment-signature": encodePaymentSignatureHeader(
135
+ paymentFor(offer, "0x000000000000000000000000000000000000beef"),
136
+ ),
137
+ });
138
+ expect(paid.status).toBe(200);
139
+ expect(await paid.json()).toEqual({ quote: "hono" });
140
+ const settlement = decodePaymentResponseHeader(paid.headers.get("payment-response") as string);
141
+ expect(settlement).toMatchObject({ success: true, transaction: TX, network: "eip155:1337" });
142
+ expect(verified.at(-1)?.payload).toMatchObject({
143
+ authorization: { from: "0x000000000000000000000000000000000000beef", to: PAY_TO },
144
+ });
145
+ });
146
+
147
+ it("returns the facilitator's reason as a 402", async () => {
148
+ const offer = await challenge();
149
+ const refused = await fetchQuote({
150
+ "payment-signature": encodePaymentSignatureHeader(paymentFor(offer, "0x000000000000000000000000000000000000dead")),
151
+ });
152
+ expect(refused.status).toBe(402);
153
+ expect(decodePaymentRequiredHeader(refused.headers.get("payment-required") as string).error).toBe(
154
+ "insufficient_funds",
155
+ );
156
+ });
157
+
158
+ it("returns a settle refusal as a 402 whose payment-response header carries the reason", async () => {
159
+ const offer = await challenge();
160
+ const refused = await fetchQuote({
161
+ "payment-signature": encodePaymentSignatureHeader(paymentFor(offer, "0x000000000000000000000000000000000000f00d")),
162
+ });
163
+ expect(refused.status).toBe(402);
164
+ expect(refused.headers.get("payment-required")).toBeNull();
165
+ expect(await refused.json()).toEqual({});
166
+ expect(decodePaymentResponseHeader(refused.headers.get("payment-response") as string)).toMatchObject({
167
+ success: false,
168
+ errorReason: "transaction_failed",
169
+ errorMessage: "transferWithAuthorization reverted",
170
+ transaction: TX,
171
+ network: "eip155:1337",
172
+ });
173
+ });
174
+
175
+ it("refuses an unknown network name", () => {
176
+ expect(() => registerMetalScheme(new x402ResourceServer(), { networks: ["mainnet" as never] })).toThrow(
177
+ /unknown Metal network/,
178
+ );
179
+ });
180
+ });
181
+
182
+ describe("metalUsd on the stock exact scheme", () => {
183
+ const scheme = new ExactEvmScheme().registerMoneyParser(metalUsd("eip155:1337"));
184
+
185
+ it("prices dollars on its network in *USDZ under USDZ's EIP-712 domain, as a transfer authorization", async () => {
186
+ expect(await scheme.parsePrice("$0.001", "eip155:1337")).toEqual({
187
+ amount: (10n ** 15n).toString(),
188
+ asset: USDZ.address,
189
+ extra: { name: USDZ.name, version: USDZ.version, primaryType: "TransferWithAuthorization" },
190
+ });
191
+ expect(await scheme.parsePrice(1.5, "eip155:1337")).toMatchObject({ amount: (15n * 10n ** 17n).toString() });
192
+ expect(await scheme.parsePrice(1e-7, "eip155:1337")).toMatchObject({ amount: "100000000000" });
193
+ });
194
+
195
+ it("leaves every other network to the scheme's defaults", async () => {
196
+ expect(await scheme.parsePrice("$0.01", "eip155:8453")).toMatchObject({
197
+ amount: "10000",
198
+ asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
199
+ });
200
+ });
201
+
202
+ it("prices in another dollar token with its own decimals", async () => {
203
+ const usdc = { ...USDZ, address: "0x2222222222222222222222222222222222222222", symbol: "USDC", decimals: 6 } as const;
204
+ const six = new ExactEvmScheme().registerMoneyParser(metalUsd("eip155:1337", usdc));
205
+ expect(await six.parsePrice("$0.01", "eip155:1337")).toMatchObject({ amount: "10000", asset: usdc.address });
206
+ });
207
+
208
+ it("refuses a price the token cannot carry exactly, and a price of zero", async () => {
209
+ await expect(scheme.parsePrice(1e-19, "eip155:1337")).rejects.toThrow(/decimals/);
210
+ await expect(scheme.parsePrice(0, "eip155:1337")).rejects.toThrow("not a price: 0");
211
+ });
212
+ });
213
+
214
+ function listen(server: Server): Promise<string> {
215
+ return new Promise((resolve, reject) => {
216
+ server.once("error", reject);
217
+ server.listen(0, "127.0.0.1", () => {
218
+ const address = server.address();
219
+ if (!address || typeof address === "string") return reject(new Error("no port"));
220
+ resolve(`http://127.0.0.1:${address.port}`);
221
+ });
222
+ });
223
+ }
package/src/server.ts ADDED
@@ -0,0 +1,77 @@
1
+ import type { x402ResourceServer } from "@x402/core/server";
2
+ import type { AssetAmount, MoneyParser, Network } from "@x402/core/types";
3
+ import { numberToDecimalString } from "@x402/core/utils";
4
+ import { ExactEvmScheme } from "@x402/evm/exact/server";
5
+ import {
6
+ METAL_NETWORKS,
7
+ USDZ,
8
+ baseUnits,
9
+ resolveNetwork,
10
+ type DollarToken,
11
+ type MetalNetwork,
12
+ type MetalNetworkName,
13
+ } from "./networks.js";
14
+
15
+ /**
16
+ * A dollar price (`"$0.01"`, `"0.01"`, `0.01`) in base units of `token`.
17
+ * Refuses a price the token cannot carry exactly, and one of zero.
18
+ */
19
+ export function dollarBaseUnits(price: string | number, token: DollarToken): string {
20
+ const units = baseUnits(typeof price === "number" ? numberToDecimalString(price) : price, token);
21
+ if (BigInt(units) === 0n) throw new Error(`not a price: ${String(price)}`);
22
+ return units;
23
+ }
24
+
25
+ /**
26
+ * A money parser that prices dollar amounts on `network` in `token`, and
27
+ * leaves every other network to the scheme's own defaults. The stock seller
28
+ * scheme looks dollar prices up in a table of networks it ships, and Metal is
29
+ * not in it; this is the hook the scheme provides for that:
30
+ *
31
+ * new ExactEvmScheme().registerMoneyParser(metalUsd("eip155:<chain id>"))
32
+ */
33
+ export function metalUsd(network: Network, token: DollarToken = USDZ): MoneyParser {
34
+ return async (amount, on): Promise<AssetAmount | null> =>
35
+ on === network
36
+ ? {
37
+ amount: dollarBaseUnits(amount, token),
38
+ asset: token.address,
39
+ // `primaryType` names the ERC-3009 message the payer signs, so a
40
+ // client need not probe the token for which one it supports.
41
+ extra: { name: token.name, version: token.version, primaryType: "TransferWithAuthorization" },
42
+ }
43
+ : null;
44
+ }
45
+
46
+ /**
47
+ * The `createAuthHeaders` option of the stock `HTTPFacilitatorClient`, for
48
+ * the token Metal's facilitator issued to this seller.
49
+ */
50
+ export function sellerToken(token: string) {
51
+ return async () => {
52
+ const headers = { Authorization: `Bearer ${token}` };
53
+ return { verify: headers, settle: headers };
54
+ };
55
+ }
56
+
57
+ export interface RegisterMetalSchemeConfig {
58
+ /** Which Metal networks to price offers on; both by default. */
59
+ networks?: readonly (MetalNetworkName | MetalNetwork)[];
60
+ }
61
+
62
+ /**
63
+ * Register the stock `exact` scheme on Metal's devnet and testnet (or the
64
+ * named ones) with a resource server, pricing dollar amounts in `*USDZ`, so
65
+ * the official middleware issues Metal offers and settles through the
66
+ * server's facilitator client.
67
+ */
68
+ export function registerMetalScheme(
69
+ server: x402ResourceServer,
70
+ { networks }: RegisterMetalSchemeConfig = {},
71
+ ): x402ResourceServer {
72
+ for (const entry of networks ?? Object.values(METAL_NETWORKS)) {
73
+ const { network } = resolveNetwork(entry);
74
+ server.register(network, new ExactEvmScheme().registerMoneyParser(metalUsd(network)));
75
+ }
76
+ return server;
77
+ }
@@ -1,204 +0,0 @@
1
- import { type Address, type Hex, type TypedDataDomain } from "viem";
2
- /** A Debit as it travels in a payment: uint values as decimal strings. */
3
- export interface DebitWire {
4
- payer: Address;
5
- recipient: Address;
6
- epoch: string;
7
- index: string;
8
- cumulativeDebit: string;
9
- ref: Hex;
10
- }
11
- /** The x402 scheme name of Metal's clearing lane. */
12
- export declare const CLEARING_DEBIT = "clearing-debit";
13
- export declare const CLEARING_DOMAIN_NAME = "MetalClearing";
14
- export declare const DEBIT_TYPES: {
15
- readonly Debit: readonly [{
16
- readonly name: "payer";
17
- readonly type: "address";
18
- }, {
19
- readonly name: "recipient";
20
- readonly type: "address";
21
- }, {
22
- readonly name: "epoch";
23
- readonly type: "uint64";
24
- }, {
25
- readonly name: "index";
26
- readonly type: "uint64";
27
- }, {
28
- readonly name: "cumulativeDebit";
29
- readonly type: "uint256";
30
- }, {
31
- readonly name: "ref";
32
- readonly type: "bytes32";
33
- }];
34
- };
35
- export declare const RECEIPT_TYPES: {
36
- readonly Receipt: readonly [{
37
- readonly name: "recipient";
38
- readonly type: "address";
39
- }, {
40
- readonly name: "epoch";
41
- readonly type: "uint64";
42
- }, {
43
- readonly name: "shard";
44
- readonly type: "uint64";
45
- }, {
46
- readonly name: "index";
47
- readonly type: "uint64";
48
- }, {
49
- readonly name: "cumulativeCredit";
50
- readonly type: "uint256";
51
- }, {
52
- readonly name: "prevReceiptDigest";
53
- readonly type: "bytes32";
54
- }, {
55
- readonly name: "debitDigest";
56
- readonly type: "bytes32";
57
- }];
58
- };
59
- /** The payer's cumulative outgoing authorization for one epoch. */
60
- export interface Debit {
61
- payer: Address;
62
- recipient: Address;
63
- epoch: bigint;
64
- index: bigint;
65
- cumulativeDebit: bigint;
66
- ref: Hex;
67
- }
68
- /** The operator's acknowledgment crediting a recipient's receive shard. */
69
- export interface Receipt {
70
- recipient: Address;
71
- epoch: bigint;
72
- shard: bigint;
73
- index: bigint;
74
- cumulativeCredit: bigint;
75
- prevReceiptDigest: Hex;
76
- debitDigest: Hex;
77
- }
78
- export declare function clearingDomain(chainId: number, contract: Address): TypedDataDomain;
79
- export declare function debitFromWire(wire: DebitWire): Debit;
80
- export declare function debitTypedData(domain: TypedDataDomain, debit: Debit): {
81
- readonly domain: TypedDataDomain;
82
- readonly types: {
83
- readonly Debit: readonly [{
84
- readonly name: "payer";
85
- readonly type: "address";
86
- }, {
87
- readonly name: "recipient";
88
- readonly type: "address";
89
- }, {
90
- readonly name: "epoch";
91
- readonly type: "uint64";
92
- }, {
93
- readonly name: "index";
94
- readonly type: "uint64";
95
- }, {
96
- readonly name: "cumulativeDebit";
97
- readonly type: "uint256";
98
- }, {
99
- readonly name: "ref";
100
- readonly type: "bytes32";
101
- }];
102
- };
103
- readonly primaryType: "Debit";
104
- readonly message: Debit;
105
- };
106
- export declare function receiptTypedData(domain: TypedDataDomain, receipt: Receipt): {
107
- readonly domain: TypedDataDomain;
108
- readonly types: {
109
- readonly Receipt: readonly [{
110
- readonly name: "recipient";
111
- readonly type: "address";
112
- }, {
113
- readonly name: "epoch";
114
- readonly type: "uint64";
115
- }, {
116
- readonly name: "shard";
117
- readonly type: "uint64";
118
- }, {
119
- readonly name: "index";
120
- readonly type: "uint64";
121
- }, {
122
- readonly name: "cumulativeCredit";
123
- readonly type: "uint256";
124
- }, {
125
- readonly name: "prevReceiptDigest";
126
- readonly type: "bytes32";
127
- }, {
128
- readonly name: "debitDigest";
129
- readonly type: "bytes32";
130
- }];
131
- };
132
- readonly primaryType: "Receipt";
133
- readonly message: Receipt;
134
- };
135
- export declare function debitDigest(domain: TypedDataDomain, debit: Debit): Hex;
136
- export declare function receiptDigest(domain: TypedDataDomain, receipt: Receipt): Hex;
137
- /** An account's request to take funds out of clearing custody. Only the
138
- * account can sign it, and it names where the funds go. */
139
- export declare const EXIT_TYPES: {
140
- readonly Exit: readonly [{
141
- readonly name: "account";
142
- readonly type: "address";
143
- }, {
144
- readonly name: "destination";
145
- readonly type: "address";
146
- }, {
147
- readonly name: "amount";
148
- readonly type: "uint256";
149
- }, {
150
- readonly name: "deadline";
151
- readonly type: "uint64";
152
- }];
153
- };
154
- export interface Exit {
155
- account: Address;
156
- destination: Address;
157
- amount: bigint;
158
- deadline: bigint;
159
- }
160
- export declare function exitTypedData(domain: TypedDataDomain, exit: Exit): {
161
- readonly domain: TypedDataDomain;
162
- readonly types: {
163
- readonly Exit: readonly [{
164
- readonly name: "account";
165
- readonly type: "address";
166
- }, {
167
- readonly name: "destination";
168
- readonly type: "address";
169
- }, {
170
- readonly name: "amount";
171
- readonly type: "uint256";
172
- }, {
173
- readonly name: "deadline";
174
- readonly type: "uint64";
175
- }];
176
- };
177
- readonly primaryType: "Exit";
178
- readonly message: Exit;
179
- };
180
- export declare function exitDigest(domain: TypedDataDomain, exit: Exit): Hex;
181
- export declare function recoverExitSigner(domain: TypedDataDomain, exit: Exit, signature: Hex): Promise<Address | null>;
182
- /** An account asking the facilitator about itself: where its next Debit
183
- * starts, or what it can withdraw. Signed under the clearing domain so that
184
- * only the account can read its own position; the deadline keeps a request
185
- * someone else has seen from being used for long. Never sent on chain. */
186
- export declare const ACCOUNT_REQUEST_TYPES: {
187
- readonly AccountRequest: readonly [{
188
- readonly name: "account";
189
- readonly type: "address";
190
- }, {
191
- readonly name: "deadline";
192
- readonly type: "uint64";
193
- }];
194
- };
195
- export declare function recoverAccountRequestSigner(domain: TypedDataDomain, request: {
196
- account: Address;
197
- deadline: bigint;
198
- }, signature: Hex): Promise<Address | null>;
199
- export declare function recoverDebitSigner(domain: TypedDataDomain, debit: Debit, signature: Hex): Promise<Address | null>;
200
- /** Receive shards spread a hot recipient over parallel credit lanes. The
201
- * shard is a pure function of `ref`, so a replayed debit lands on the same
202
- * one. Same formula as the café's. */
203
- export declare const REVENUE_SHARDS = 8n;
204
- export declare function shardFor(ref: Hex): bigint;
package/dist/clearing.js DELETED
@@ -1,151 +0,0 @@
1
- // Metal clearing's signed messages: the EIP-712 types and digests that a
2
- // payer, a seller, the facilitator and the clearing operator must agree on
3
- // field for field. They mirror contracts/src/clearing/ClearingTypes.sol and
4
- // the operator's local API (apps/clearing-operator).
5
- //
6
- // - A Debit is a payer's signed running total, naming who it pays.
7
- // - A Receipt is the operator's signature crediting a recipient's shard.
8
- // - An Exit is an account's signed withdrawal, naming where funds go.
9
- // - An AccountRequest is an account asking the facilitator about itself.
10
- //
11
- // All are signed under the Clearing contract's domain ("MetalClearing", "1",
12
- // chainId, contract). Pure: no I/O.
13
- import { hashTypedData, recoverTypedDataAddress, } from "viem";
14
- /** The x402 scheme name of Metal's clearing lane. */
15
- export const CLEARING_DEBIT = "clearing-debit";
16
- export const CLEARING_DOMAIN_NAME = "MetalClearing";
17
- export const DEBIT_TYPES = {
18
- Debit: [
19
- { name: "payer", type: "address" },
20
- { name: "recipient", type: "address" },
21
- { name: "epoch", type: "uint64" },
22
- { name: "index", type: "uint64" },
23
- { name: "cumulativeDebit", type: "uint256" },
24
- { name: "ref", type: "bytes32" },
25
- ],
26
- };
27
- export const RECEIPT_TYPES = {
28
- Receipt: [
29
- { name: "recipient", type: "address" },
30
- { name: "epoch", type: "uint64" },
31
- { name: "shard", type: "uint64" },
32
- { name: "index", type: "uint64" },
33
- { name: "cumulativeCredit", type: "uint256" },
34
- { name: "prevReceiptDigest", type: "bytes32" },
35
- { name: "debitDigest", type: "bytes32" },
36
- ],
37
- };
38
- export function clearingDomain(chainId, contract) {
39
- return {
40
- name: CLEARING_DOMAIN_NAME,
41
- version: "1",
42
- chainId,
43
- verifyingContract: contract,
44
- };
45
- }
46
- export function debitFromWire(wire) {
47
- return {
48
- payer: wire.payer,
49
- recipient: wire.recipient,
50
- epoch: BigInt(wire.epoch),
51
- index: BigInt(wire.index),
52
- cumulativeDebit: BigInt(wire.cumulativeDebit),
53
- ref: wire.ref,
54
- };
55
- }
56
- export function debitTypedData(domain, debit) {
57
- return {
58
- domain,
59
- types: DEBIT_TYPES,
60
- primaryType: "Debit",
61
- message: debit,
62
- };
63
- }
64
- export function receiptTypedData(domain, receipt) {
65
- return {
66
- domain,
67
- types: RECEIPT_TYPES,
68
- primaryType: "Receipt",
69
- message: receipt,
70
- };
71
- }
72
- export function debitDigest(domain, debit) {
73
- return hashTypedData(debitTypedData(domain, debit));
74
- }
75
- export function receiptDigest(domain, receipt) {
76
- return hashTypedData(receiptTypedData(domain, receipt));
77
- }
78
- /** An account's request to take funds out of clearing custody. Only the
79
- * account can sign it, and it names where the funds go. */
80
- export const EXIT_TYPES = {
81
- Exit: [
82
- { name: "account", type: "address" },
83
- { name: "destination", type: "address" },
84
- { name: "amount", type: "uint256" },
85
- { name: "deadline", type: "uint64" },
86
- ],
87
- };
88
- export function exitTypedData(domain, exit) {
89
- return {
90
- domain,
91
- types: EXIT_TYPES,
92
- primaryType: "Exit",
93
- message: exit,
94
- };
95
- }
96
- export function exitDigest(domain, exit) {
97
- return hashTypedData(exitTypedData(domain, exit));
98
- }
99
- export async function recoverExitSigner(domain, exit, signature) {
100
- try {
101
- return await recoverTypedDataAddress({
102
- ...exitTypedData(domain, exit),
103
- signature,
104
- });
105
- }
106
- catch {
107
- return null;
108
- }
109
- }
110
- /** An account asking the facilitator about itself: where its next Debit
111
- * starts, or what it can withdraw. Signed under the clearing domain so that
112
- * only the account can read its own position; the deadline keeps a request
113
- * someone else has seen from being used for long. Never sent on chain. */
114
- export const ACCOUNT_REQUEST_TYPES = {
115
- AccountRequest: [
116
- { name: "account", type: "address" },
117
- { name: "deadline", type: "uint64" },
118
- ],
119
- };
120
- export async function recoverAccountRequestSigner(domain, request, signature) {
121
- try {
122
- return await recoverTypedDataAddress({
123
- domain,
124
- types: ACCOUNT_REQUEST_TYPES,
125
- primaryType: "AccountRequest",
126
- message: request,
127
- signature,
128
- });
129
- }
130
- catch {
131
- return null;
132
- }
133
- }
134
- export async function recoverDebitSigner(domain, debit, signature) {
135
- try {
136
- return await recoverTypedDataAddress({
137
- ...debitTypedData(domain, debit),
138
- signature,
139
- });
140
- }
141
- catch {
142
- return null;
143
- }
144
- }
145
- /** Receive shards spread a hot recipient over parallel credit lanes. The
146
- * shard is a pure function of `ref`, so a replayed debit lands on the same
147
- * one. Same formula as the café's. */
148
- export const REVENUE_SHARDS = 8n;
149
- export function shardFor(ref) {
150
- return BigInt(`0x${ref.slice(-2)}`) % REVENUE_SHARDS;
151
- }
@@ -1,39 +0,0 @@
1
- import { type ClearingPositionStorage, type StoredClearingPosition } from "./clearing-client.js";
2
- export interface FileClearingPositionStorageOptions {
3
- /** How long a writer waits for the lock before giving up. Default 5s. */
4
- lockWaitMs?: number;
5
- /** A lock this old was left by a writer that died or hung. Default 30s;
6
- * a write holds the lock for milliseconds. */
7
- lockStaleMs?: number;
8
- }
9
- export declare class FileClearingPositionStorage implements ClearingPositionStorage {
10
- private readonly path;
11
- private readonly lockPath;
12
- private readonly takeoverPath;
13
- private readonly lockWaitMs;
14
- private readonly lockStaleMs;
15
- constructor(path: string, options?: FileClearingPositionStorageOptions);
16
- private read;
17
- /** Hold the lock file while `job` runs. The whole file is read, changed
18
- * and rewritten, so two writers (two instances, or two processes) must
19
- * not overlap or one would drop the other's record.
20
- *
21
- * The lock is a file created exclusively, holding a token unique to its
22
- * holder. A lock left by a writer that died is taken over, and that is
23
- * the delicate part: two waiters must not both conclude it is abandoned
24
- * and both remove it, or the second would delete the first one's fresh
25
- * lock. So removal itself is guarded (see `takeOver`), and a writer whose
26
- * lock was taken over finds out before it writes (`stillHeld`). */
27
- private locked;
28
- private inspect;
29
- /** Remove `path` only while it still holds `token`. */
30
- private removeIfHolds;
31
- /** Remove an abandoned lock, at most one waiter at a time. The takeover
32
- * file is itself created exclusively, so only one waiter is ever inside;
33
- * it removes the lock only if it is still the abandoned one it saw. A
34
- * waiter that arrives later finds either no lock or a fresh one from a
35
- * new holder, and leaves it alone. */
36
- private takeOver;
37
- get(key: string): Promise<StoredClearingPosition | undefined>;
38
- set(key: string, position: StoredClearingPosition): Promise<void>;
39
- }