@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,303 @@
1
+ import { createPublicClient, http, type Hex } from "viem";
2
+ import { clearingAbi } from "./clearing-abi.js";
3
+ import {
4
+ CLEARING_REFERENCE,
5
+ clearingUnits,
6
+ decodeSend,
7
+ encodeSend,
8
+ invoiceMessage,
9
+ outgoingRoot,
10
+ verifyReceipt,
11
+ type ClearingInvoice,
12
+ type ClearingReceipt,
13
+ } from "./clearing-codec.js";
14
+
15
+ export interface ClearingSigner {
16
+ publicKey: Hex;
17
+ sign(namespace: string, message: Hex): Hex | Promise<Hex>;
18
+ }
19
+ export interface ClearingEntry {
20
+ recipient: Hex;
21
+ cumulative: string;
22
+ count: string;
23
+ }
24
+ export interface ClearingPayment {
25
+ invoice: ClearingInvoice;
26
+ body: Hex;
27
+ payerSignature: Hex;
28
+ invoiceSignature: Hex;
29
+ entries: ClearingEntry[];
30
+ }
31
+ export interface ClearingPaid {
32
+ invoice: ClearingInvoice;
33
+ receipt: ClearingReceipt;
34
+ invoiceSignature: Hex;
35
+ entries: ClearingEntry[];
36
+ previous?: { receipt: ClearingReceipt; entries: ClearingEntry[] };
37
+ }
38
+ export interface ClearingClientState {
39
+ epoch?: string;
40
+ entries: ClearingEntry[];
41
+ sequence: string;
42
+ pending?: ClearingPayment | undefined;
43
+ payments: Record<string, ClearingPaid>;
44
+ }
45
+ /** save must complete an atomic durability barrier; one process owns each payer. */
46
+ export interface ClearingClientStore {
47
+ load(): Promise<ClearingClientState | null>;
48
+ save(state: ClearingClientState): Promise<void>;
49
+ }
50
+ export interface ClearingClientConfig {
51
+ serviceUrl: string;
52
+ rpcUrl: string;
53
+ network: `eip155:${number}`;
54
+ settlement: Hex;
55
+ asset: Hex;
56
+ signer: ClearingSigner;
57
+ store: ClearingClientStore;
58
+ maxAmount: bigint;
59
+ fetch?: typeof globalThis.fetch;
60
+ }
61
+
62
+ class PaymentRejected extends Error {}
63
+
64
+ export function createClearingClient(config: ClearingClientConfig) {
65
+ const fetcher = config.fetch ?? globalThis.fetch;
66
+ const chain = createPublicClient({ transport: http(config.rpcUrl) });
67
+ let tail: Promise<unknown> = Promise.resolve();
68
+ let broken = false;
69
+ async function save(state: ClearingClientState) {
70
+ try {
71
+ await config.store.save(state);
72
+ } catch (error) {
73
+ broken = true;
74
+ throw error;
75
+ }
76
+ }
77
+ async function request<T>(path: string, body?: unknown): Promise<T> {
78
+ const response = await fetcher(new URL(path, config.serviceUrl), {
79
+ method: body ? "POST" : "GET",
80
+ ...(body
81
+ ? {
82
+ headers: { "content-type": "application/json" },
83
+ body: JSON.stringify(body),
84
+ }
85
+ : {}),
86
+ signal: AbortSignal.timeout(30_000),
87
+ });
88
+ if (!response.ok) {
89
+ const result = (await response.json().catch(() => null)) as {
90
+ rejected?: boolean;
91
+ } | null;
92
+ if (result?.rejected === true)
93
+ throw new PaymentRejected(
94
+ "clearing payment rejected before acknowledgment",
95
+ );
96
+ throw new Error(`clearing service returned ${response.status}`);
97
+ }
98
+ return (await response.json()) as T;
99
+ }
100
+ /** Why this wallet does not pay `invoice`, or undefined when it does. */
101
+ function refusal(invoice: ClearingInvoice) {
102
+ if (
103
+ invoice.network !== config.network ||
104
+ invoice.asset.toLowerCase() !== config.asset.toLowerCase() ||
105
+ invoice.settlement.toLowerCase() !== config.settlement.toLowerCase()
106
+ )
107
+ return "clearing invoice deployment mismatch";
108
+ const amount = BigInt(invoice.amount);
109
+ if (amount <= 0n || amount > config.maxAmount)
110
+ return "clearing payment exceeds spending limit";
111
+ }
112
+ async function pay(invoice: ClearingInvoice) {
113
+ if (broken)
114
+ throw new Error("clearing wallet persistence failed; reopen the wallet");
115
+ const refused = refusal(invoice);
116
+ if (refused) throw new Error(refused);
117
+ const amount = BigInt(invoice.amount);
118
+ let state = (await config.store.load()) ?? {
119
+ entries: [],
120
+ sequence: "0",
121
+ payments: {},
122
+ };
123
+ const paid = state.payments[invoice.id];
124
+ if (paid) {
125
+ if (
126
+ invoiceMessage(paid.invoice, paid.receipt.body) !==
127
+ invoiceMessage(invoice, paid.receipt.body)
128
+ )
129
+ throw new Error("invoice id reused with different terms");
130
+ return paid;
131
+ }
132
+ if (
133
+ state.pending &&
134
+ invoiceMessage(state.pending.invoice, state.pending.body) !==
135
+ invoiceMessage(invoice, state.pending.body)
136
+ )
137
+ throw new Error(
138
+ "an earlier clearing payment has an unknown outcome; retry that invoice first",
139
+ );
140
+ if (!state.pending) {
141
+ const context = await request<{
142
+ reference: string;
143
+ network: string;
144
+ settlement: Hex;
145
+ asset: Hex;
146
+ operator: Hex;
147
+ epoch: string;
148
+ anchor: Hex;
149
+ }>("/context");
150
+ if (
151
+ context.reference !== CLEARING_REFERENCE ||
152
+ context.network !== config.network ||
153
+ context.settlement.toLowerCase() !== config.settlement.toLowerCase() ||
154
+ context.asset.toLowerCase() !== config.asset.toLowerCase()
155
+ )
156
+ throw new Error("unsupported clearing service deployment");
157
+ const epoch = BigInt(context.epoch);
158
+ const [registered, onchain, fault, chainId, admitted, block, unit] =
159
+ await Promise.all([
160
+ chain.readContract({
161
+ address: config.settlement,
162
+ abi: clearingAbi,
163
+ functionName: "registered",
164
+ }),
165
+ chain.readContract({
166
+ address: config.settlement,
167
+ abi: clearingAbi,
168
+ functionName: "contextOf",
169
+ args: [epoch],
170
+ }),
171
+ chain.readContract({
172
+ address: config.settlement,
173
+ abi: clearingAbi,
174
+ functionName: "fault",
175
+ }),
176
+ chain.getChainId(),
177
+ chain.readContract({
178
+ address: config.settlement,
179
+ abi: clearingAbi,
180
+ functionName: "admittedCount",
181
+ }),
182
+ chain.getBlock(),
183
+ chain.readContract({
184
+ address: config.settlement,
185
+ abi: clearingAbi,
186
+ functionName: "unit",
187
+ }),
188
+ ]);
189
+ if (
190
+ !registered ||
191
+ fault ||
192
+ admitted !== epoch ||
193
+ block.timestamp >= onchain.admissionDeadline ||
194
+ `eip155:${chainId}` !== config.network ||
195
+ onchain.anchor !== context.anchor ||
196
+ onchain.operatorKey !== context.operator
197
+ )
198
+ throw new Error("clearing epoch is not registered");
199
+ if (state.epoch !== context.epoch)
200
+ state = { ...state, epoch: context.epoch, sequence: "0", entries: [] };
201
+ const entries = state.entries.map((e) => ({ ...e }));
202
+ let entry = entries.find(
203
+ (e) => e.recipient.toLowerCase() === invoice.recipient.toLowerCase(),
204
+ );
205
+ if (!entry) {
206
+ entry = { recipient: invoice.recipient, cumulative: "0", count: "0" };
207
+ entries.push(entry);
208
+ }
209
+ // The invoice is in token base units; the vector counts clearing units.
210
+ entry.cumulative = (
211
+ BigInt(entry.cumulative) + clearingUnits(amount, unit)
212
+ ).toString();
213
+ entry.count = (BigInt(entry.count) + 1n).toString();
214
+ entries.sort((a, b) =>
215
+ a.recipient.toLowerCase().localeCompare(b.recipient.toLowerCase()),
216
+ );
217
+ const vector = entries.map((e) => ({
218
+ ...e,
219
+ cumulative: BigInt(e.cumulative),
220
+ count: BigInt(e.count),
221
+ }));
222
+ const body = encodeSend({
223
+ anchor: context.anchor,
224
+ epoch,
225
+ payer: config.signer.publicKey,
226
+ sequence: BigInt(state.sequence),
227
+ debit: vector.reduce((sum, e) => sum + e.cumulative, 0n),
228
+ vector: outgoingRoot(vector),
229
+ });
230
+ state = {
231
+ ...state,
232
+ pending: {
233
+ invoice,
234
+ body,
235
+ entries,
236
+ payerSignature: await config.signer.sign(
237
+ "_COMMONWARE_CLEARING_VECTOR_SEND",
238
+ body,
239
+ ),
240
+ invoiceSignature: await config.signer.sign(
241
+ "_METAL_CLEARING_INVOICE",
242
+ invoiceMessage(invoice, body),
243
+ ),
244
+ },
245
+ };
246
+ await save(state);
247
+ }
248
+ const pending = state.pending!;
249
+ let result: ClearingPaid;
250
+ try {
251
+ result = await request<ClearingPaid>("/payments", pending);
252
+ } catch (error) {
253
+ if (error instanceof PaymentRejected)
254
+ await save({ ...state, pending: undefined });
255
+ throw error;
256
+ }
257
+ const operator = await chain.readContract({
258
+ address: config.settlement,
259
+ abi: clearingAbi,
260
+ functionName: "operatorKey",
261
+ });
262
+ if (
263
+ result.receipt.body !== pending.body ||
264
+ result.receipt.payerSignature !== pending.payerSignature ||
265
+ invoiceMessage(result.invoice, pending.body) !==
266
+ invoiceMessage(invoice, pending.body) ||
267
+ !verifyReceipt(result.receipt, operator)
268
+ )
269
+ throw new Error(
270
+ "invalid clearing acknowledgment; retain pending payment",
271
+ );
272
+ const settled = {
273
+ invoice,
274
+ receipt: result.receipt,
275
+ invoiceSignature: pending.invoiceSignature,
276
+ entries: pending.entries,
277
+ ...(result.previous ? { previous: result.previous } : {}),
278
+ };
279
+ await save({
280
+ ...state,
281
+ entries: pending.entries,
282
+ sequence: (decodeSend(pending.body).sequence + 1n).toString(),
283
+ pending: undefined,
284
+ payments: { ...state.payments, [invoice.id]: settled },
285
+ });
286
+ return settled;
287
+ }
288
+ return {
289
+ account: config.signer.publicKey,
290
+ async pendingInvoice() {
291
+ return (await config.store.load())?.pending?.invoice ?? null;
292
+ },
293
+ payable(invoice: ClearingInvoice) {
294
+ return refusal(invoice) === undefined;
295
+ },
296
+ pay(invoice: ClearingInvoice) {
297
+ const result = tail.then(() => pay(invoice));
298
+ tail = result.catch(() => {});
299
+ return result;
300
+ },
301
+ request,
302
+ };
303
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readFileSync } from "node:fs";
3
+ import { clearingUnits, decodeSend, outgoingRoot, verifyReceipt } from "./clearing-codec.js";
4
+
5
+ const fixture = JSON.parse(
6
+ readFileSync(new URL("./clearing-fixture.json", import.meta.url), "utf8"),
7
+ );
8
+
9
+ describe("native Bajillion wire parity", () => {
10
+ it("verifies a receipt produced by the pinned Rust implementation", () => {
11
+ expect(verifyReceipt(fixture.receipt, fixture.operator)).toBe(true);
12
+ const body = decodeSend(fixture.receipt.body);
13
+ expect(body.debit).toBe(600n);
14
+ expect(
15
+ outgoingRoot([
16
+ {
17
+ recipient: fixture.receipt.entry.recipient,
18
+ cumulative: 600n,
19
+ count: 1n,
20
+ },
21
+ ]),
22
+ ).toBe(body.vector);
23
+ });
24
+
25
+ it("rejects tampered bodies and operator keys", () => {
26
+ expect(
27
+ verifyReceipt(
28
+ { ...fixture.receipt, body: fixture.receipt.body.slice(0, -2) + "00" },
29
+ fixture.operator,
30
+ ),
31
+ ).toBe(false);
32
+ expect(verifyReceipt(fixture.receipt, fixture.receipt.payer)).toBe(false);
33
+ });
34
+
35
+ it("rejects noncanonical vectors and out-of-range amounts", () => {
36
+ const entry = {
37
+ recipient: fixture.receipt.entry.recipient,
38
+ cumulative: 600n,
39
+ count: 1n,
40
+ };
41
+ expect(() => outgoingRoot([entry, entry])).toThrow();
42
+ expect(() => outgoingRoot([{ ...entry, cumulative: 1n << 64n }])).toThrow();
43
+ expect(() => outgoingRoot([{ ...entry, cumulative: 0n }])).toThrow();
44
+ expect(() =>
45
+ decodeSend((fixture.receipt.body + "00") as `0x${string}`),
46
+ ).toThrow();
47
+ });
48
+ });
49
+
50
+ describe("clearing units", () => {
51
+ it("counts an amount in a deployment's units and refuses a fraction of one", () => {
52
+ expect(clearingUnits(5n * 10n ** 12n, 10n ** 12n)).toBe(5n);
53
+ expect(clearingUnits(7n, 1n)).toBe(7n);
54
+ expect(() => clearingUnits(10n ** 12n + 1n, 10n ** 12n)).toThrow(/whole number of clearing units/);
55
+ expect(() => clearingUnits(5n, 0n)).toThrow();
56
+ });
57
+ });
@@ -0,0 +1,251 @@
1
+ import { p256 } from "@noble/curves/p256";
2
+ import {
3
+ concatHex,
4
+ hexToBytes,
5
+ sha256,
6
+ stringToHex,
7
+ toHex,
8
+ type Hex,
9
+ } from "viem";
10
+
11
+ export const CLEARING_REFERENCE = "3134e902ccf619987e2731a0c1e63114bcc0b060";
12
+ export const CLEARING_SCHEME = "bajillion";
13
+ const U64_MAX = (1n << 64n) - 1n;
14
+
15
+ export interface OutgoingEntry {
16
+ recipient: Hex;
17
+ cumulative: bigint;
18
+ count: bigint;
19
+ }
20
+ export interface ClearingSend {
21
+ anchor: Hex;
22
+ epoch: bigint;
23
+ payer: Hex;
24
+ sequence: bigint;
25
+ debit: bigint;
26
+ vector: Hex;
27
+ }
28
+ export interface ClearingReceipt {
29
+ body: Hex;
30
+ payerSignature: Hex;
31
+ operatorSignature: Hex;
32
+ }
33
+
34
+ /** The clearing units `amount` token base units make, where `unit` is the
35
+ * deployment's `Clearing.unit()`: Clearing counts whole units, so a fraction
36
+ * of one is refused rather than rounded. */
37
+ export function clearingUnits(amount: bigint, unit: bigint): bigint {
38
+ if (unit <= 0n || amount % unit !== 0n)
39
+ throw new Error(
40
+ `${amount} base units is not a whole number of clearing units of ${unit}`,
41
+ );
42
+ return amount / unit;
43
+ }
44
+ export function clearingUint64(value: bigint): Hex {
45
+ if (value < 0n || value > U64_MAX)
46
+ throw new Error("clearing amount exceeds uint64");
47
+ return toHex(value, { size: 8 });
48
+ }
49
+ function fixed(value: Hex, length: number): Hex {
50
+ if (!new RegExp(`^0x[0-9a-fA-F]{${length * 2}}$`).test(value))
51
+ throw new Error("invalid clearing wire length");
52
+ return value.toLowerCase() as Hex;
53
+ }
54
+ export function clearingAccount(value: Hex): Hex {
55
+ fixed(value, 33);
56
+ const point = p256.ProjectivePoint.fromHex(hexToBytes(value));
57
+ point.assertValidity();
58
+ // The canonical compressed encoding of the parsed point, not the wire
59
+ // string: what reaches a receipt URL or a signature check is what the
60
+ // curve produced, so no byte of the input passes through untouched.
61
+ return toHex(point.toRawBytes(true));
62
+ }
63
+ function hash(...values: Hex[]): Hex {
64
+ return sha256(concatHex(values));
65
+ }
66
+ const domain = (name: string) => stringToHex(`_COMMONWARE_CLEARING_${name}`);
67
+
68
+ /** The exact typed BMT root used by commonware-clearing at CLEARING_REFERENCE. */
69
+ export function outgoingRoot(entries: readonly OutgoingEntry[]): Hex {
70
+ if (entries.length > 1 << 24) throw new Error("too many clearing entries");
71
+ const length = toHex(entries.length, { size: 4 });
72
+ let previous = "";
73
+ let total = 0n;
74
+ let count = 0n;
75
+ let level = entries.map((entry, position) => {
76
+ const recipient = clearingAccount(entry.recipient);
77
+ if (
78
+ recipient <= previous ||
79
+ entry.count <= 0n ||
80
+ entry.cumulative < entry.count
81
+ )
82
+ throw new Error("noncanonical clearing vector");
83
+ previous = recipient;
84
+ total += entry.cumulative;
85
+ count += entry.count;
86
+ clearingUint64(total);
87
+ clearingUint64(count);
88
+ const encoded = concatHex([
89
+ recipient,
90
+ clearingUint64(entry.cumulative),
91
+ clearingUint64(entry.count),
92
+ ]);
93
+ const index = toHex(position, { size: 4 });
94
+ const leaf = hash(
95
+ domain("OUT_ENTRY_LEAF"),
96
+ length,
97
+ index,
98
+ clearingUint64(49n),
99
+ encoded,
100
+ );
101
+ return hash(index, leaf);
102
+ });
103
+ if (level.length === 0) level = [hash("0x")];
104
+ while (level.length > 1) {
105
+ const next: Hex[] = [];
106
+ for (let i = 0; i < level.length; i += 2)
107
+ next.push(hash(level[i]!, level[i + 1] ?? level[i]!));
108
+ level = next;
109
+ }
110
+ return hash(domain("OUT_ENTRY_ROOT"), length, hash(length, level[0]!));
111
+ }
112
+
113
+ export function encodeSend(body: ClearingSend): Hex {
114
+ return concatHex([
115
+ fixed(body.anchor, 32),
116
+ clearingUint64(body.epoch),
117
+ clearingAccount(body.payer),
118
+ clearingUint64(body.sequence),
119
+ clearingUint64(body.debit),
120
+ fixed(body.vector, 32),
121
+ ]);
122
+ }
123
+ export function decodeSend(encoded: Hex): ClearingSend {
124
+ fixed(encoded, 121);
125
+ const part = (start: number, length: number) =>
126
+ `0x${encoded.slice(2 + start * 2, 2 + (start + length) * 2)}` as Hex;
127
+ return {
128
+ anchor: part(0, 32),
129
+ epoch: BigInt(part(32, 8)),
130
+ payer: clearingAccount(part(40, 33)),
131
+ sequence: BigInt(part(73, 8)),
132
+ debit: BigInt(part(81, 8)),
133
+ vector: part(89, 32),
134
+ };
135
+ }
136
+ export function clearingSignatureDigest(namespace: string, message: Hex): Hex {
137
+ const bytes = stringToHex(namespace);
138
+ const length = (bytes.length - 2) / 2;
139
+ if (length >= 128) throw new Error("unsupported signature namespace length");
140
+ return hash(toHex(length, { size: 1 }), bytes, message);
141
+ }
142
+ export function verifyClearingSignature(
143
+ key: Hex,
144
+ namespace: string,
145
+ message: Hex,
146
+ signature: Hex,
147
+ ): boolean {
148
+ try {
149
+ clearingAccount(key);
150
+ fixed(signature, 64);
151
+ return p256.verify(
152
+ hexToBytes(signature),
153
+ hexToBytes(clearingSignatureDigest(namespace, message)),
154
+ hexToBytes(key),
155
+ { lowS: true, prehash: false },
156
+ );
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+ export function verifyReceipt(
162
+ receipt: ClearingReceipt,
163
+ operator: Hex,
164
+ ): boolean {
165
+ try {
166
+ const body = decodeSend(receipt.body);
167
+ return (
168
+ verifyClearingSignature(
169
+ body.payer,
170
+ "_COMMONWARE_CLEARING_VECTOR_SEND",
171
+ receipt.body,
172
+ receipt.payerSignature,
173
+ ) &&
174
+ verifyClearingSignature(
175
+ operator,
176
+ "_COMMONWARE_CLEARING_VECTOR_ACK",
177
+ receipt.body,
178
+ receipt.operatorSignature,
179
+ )
180
+ );
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+
186
+ export interface ClearingInvoice {
187
+ id: string;
188
+ network: string;
189
+ settlement: Hex;
190
+ asset: Hex;
191
+ recipient: Hex;
192
+ amount: string;
193
+ }
194
+
195
+ /** The HTTP invoice is separately signed; it does not alter Bajillion's wire format. */
196
+ export function invoiceMessage(invoice: ClearingInvoice, body: Hex): Hex {
197
+ if (!/^[a-zA-Z0-9:_./-]{1,256}$/.test(invoice.id))
198
+ throw new Error("invalid clearing invoice id");
199
+ if (!/^eip155:[1-9][0-9]*$/.test(invoice.network))
200
+ throw new Error("invalid clearing network");
201
+ return stringToHex(
202
+ JSON.stringify([
203
+ invoice.id,
204
+ invoice.network,
205
+ fixed(invoice.settlement, 20),
206
+ fixed(invoice.asset, 20),
207
+ clearingAccount(invoice.recipient),
208
+ // Invoices count ERC-20 base units; only clearing balances use uint64.
209
+ toHex(BigInt(invoice.amount), { size: 32 }),
210
+ sha256(body),
211
+ ]),
212
+ );
213
+ }
214
+
215
+ export function createClearingSigner(privateKey: Hex) {
216
+ const key = hexToBytes(fixed(privateKey, 32));
217
+ const publicKey = toHex(p256.getPublicKey(key, true));
218
+ return {
219
+ publicKey,
220
+ sign(namespace: string, message: Hex): Hex {
221
+ return toHex(
222
+ p256
223
+ .sign(hexToBytes(clearingSignatureDigest(namespace, message)), key, {
224
+ lowS: true,
225
+ prehash: false,
226
+ })
227
+ .toCompactRawBytes(),
228
+ );
229
+ },
230
+ };
231
+ }
232
+
233
+ export function decodeWithdrawal(encoded: Hex) {
234
+ const raw = hexToBytes(encoded);
235
+ if ((raw.length !== 199 && raw.length !== 191) || raw[97] !== 20)
236
+ throw new Error("invalid native withdrawal");
237
+ const part = (offset: number, length: number) =>
238
+ toHex(raw.slice(offset, offset + length));
239
+ const close = raw[118] === 2;
240
+ if ((!close && raw[118] !== 1) || raw.length !== (close ? 191 : 199))
241
+ throw new Error("invalid withdrawal action");
242
+ return {
243
+ account: clearingAccount(part(0, 33)),
244
+ deployment: part(33, 32),
245
+ state: part(65, 32),
246
+ destination: part(98, 20),
247
+ amount: close ? 0n : BigInt(part(119, 8)),
248
+ deadline: BigInt(part(close ? 119 : 127, 8)),
249
+ signature: part(close ? 127 : 135, 64),
250
+ };
251
+ }