@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.
- package/README.md +72 -33
- package/dist/clearing-abi.d.ts +1836 -0
- package/dist/clearing-abi.js +2361 -0
- package/dist/clearing-client.d.ts +49 -159
- package/dist/clearing-client.js +191 -564
- package/dist/clearing-codec.d.ts +57 -0
- package/dist/clearing-codec.js +173 -0
- package/dist/clearing-custody.d.ts +191 -0
- package/dist/clearing-custody.js +490 -0
- package/dist/clearing-server.d.ts +30 -49
- package/dist/clearing-server.js +87 -67
- package/dist/clearing-x402.d.ts +29 -0
- package/dist/clearing-x402.js +203 -0
- package/dist/client.d.ts +7 -13
- package/dist/client.js +16 -18
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/mandates/binding.d.ts +4 -27
- package/dist/mandates/binding.js +5 -9
- package/dist/mandates/checkout.js +5 -5
- package/dist/mandates/client.js +4 -3
- package/dist/mandates/l3a.d.ts +2 -2
- package/dist/mandates/l3a.js +5 -4
- package/dist/mandates/seller.d.ts +1 -1
- package/dist/mandates/seller.js +1 -1
- package/dist/mandates/verify.d.ts +4 -6
- package/dist/mandates/verify.js +42 -66
- package/dist/mandates/vi/credential.js +4 -4
- package/dist/node.d.ts +11 -0
- package/dist/node.js +97 -0
- package/package.json +18 -11
- package/src/clearing-abi.ts +2361 -0
- package/src/clearing-client.ts +303 -0
- package/src/clearing-codec.test.ts +57 -0
- package/src/clearing-codec.ts +251 -0
- package/src/clearing-custody.test.ts +534 -0
- package/src/clearing-custody.ts +665 -0
- package/src/clearing-fixture.json +37 -0
- package/src/clearing-server.test.ts +388 -0
- package/src/clearing-server.ts +123 -0
- package/src/clearing-x402.test.ts +181 -0
- package/src/clearing-x402.ts +279 -0
- package/src/client.test.ts +325 -0
- package/src/client.ts +156 -0
- package/src/index.ts +33 -0
- package/src/mandates/account.ts +91 -0
- package/src/mandates/binding.ts +280 -0
- package/src/mandates/chain.ts +68 -0
- package/src/mandates/checkout.ts +368 -0
- package/src/mandates/client.ts +159 -0
- package/src/mandates/extension.ts +48 -0
- package/src/mandates/index.ts +121 -0
- package/src/mandates/ingest.ts +42 -0
- package/src/mandates/jcs.ts +16 -0
- package/src/mandates/l3a.ts +216 -0
- package/src/mandates/seller.ts +91 -0
- package/src/mandates/verify.ts +532 -0
- package/src/mandates/vi/credential.ts +477 -0
- package/src/mandates/vi/issue.ts +118 -0
- package/src/mandates/vi/sd-jwt.ts +189 -0
- package/src/networks.test.ts +16 -0
- package/src/networks.ts +152 -0
- package/src/node.test.ts +24 -0
- package/src/node.ts +96 -0
- package/src/server.test.ts +223 -0
- package/src/server.ts +77 -0
- package/dist/clearing.d.ts +0 -204
- package/dist/clearing.js +0 -151
- package/dist/file-storage.d.ts +0 -39
- 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
|
+
}
|
package/dist/clearing.d.ts
DELETED
|
@@ -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
|
-
}
|
package/dist/file-storage.d.ts
DELETED
|
@@ -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
|
-
}
|