@chainpayhq/sdk 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +42 -2
- package/dist/accounts.d.ts +23 -0
- package/dist/accounts.js +148 -0
- package/dist/cards/accounts.d.ts +156 -0
- package/dist/cards/accounts.js +346 -0
- package/dist/cards/api.d.ts +379 -0
- package/dist/cards/api.js +241 -0
- package/dist/cards/commitment.d.ts +86 -0
- package/dist/cards/commitment.js +288 -0
- package/dist/cards/constants.d.ts +160 -0
- package/dist/cards/constants.js +163 -0
- package/dist/cards/draft.d.ts +57 -0
- package/dist/cards/draft.js +131 -0
- package/dist/cards/evidence.d.ts +69 -0
- package/dist/cards/evidence.js +41 -0
- package/dist/cards/hash.d.ts +9 -0
- package/dist/cards/hash.js +47 -0
- package/dist/cards/index.d.ts +14 -0
- package/dist/cards/index.js +14 -0
- package/dist/cards/instructions.d.ts +302 -0
- package/dist/cards/instructions.js +474 -0
- package/dist/cards/layout.d.ts +33 -0
- package/dist/cards/layout.js +137 -0
- package/dist/cards/math.d.ts +67 -0
- package/dist/cards/math.js +138 -0
- package/dist/cards/merchants.d.ts +23 -0
- package/dist/cards/merchants.js +38 -0
- package/dist/cards/pda.d.ts +42 -0
- package/dist/cards/pda.js +93 -0
- package/dist/cards/private-repayment.d.ts +198 -0
- package/dist/cards/private-repayment.js +485 -0
- package/dist/cards/redact.d.ts +18 -0
- package/dist/cards/redact.js +103 -0
- package/dist/cards/tee.d.ts +253 -0
- package/dist/cards/tee.js +605 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.js +351 -0
- package/dist/client.d.ts +80 -0
- package/dist/client.js +493 -0
- package/dist/constants.d.ts +44 -0
- package/dist/constants.js +43 -0
- package/dist/crossmint-adapt.d.ts +43 -0
- package/dist/crossmint-adapt.js +68 -0
- package/dist/crossmint-order.d.ts +220 -0
- package/dist/crossmint-order.js +638 -0
- package/dist/delivery.d.ts +66 -0
- package/dist/delivery.js +232 -0
- package/dist/encoding.d.ts +51 -0
- package/dist/encoding.js +128 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +26 -0
- package/dist/known-assets.d.ts +30 -0
- package/dist/known-assets.js +49 -0
- package/dist/mandate-request.d.ts +123 -0
- package/dist/mandate-request.js +401 -0
- package/dist/mandate.d.ts +44 -0
- package/dist/mandate.js +157 -0
- package/dist/ops-snapshot.d.ts +157 -0
- package/dist/ops-snapshot.js +356 -0
- package/dist/payment-request.d.ts +37 -0
- package/dist/payment-request.js +218 -0
- package/dist/payment.d.ts +40 -0
- package/dist/payment.js +214 -0
- package/dist/pda.d.ts +13 -0
- package/dist/pda.js +36 -0
- package/dist/receipt-export.d.ts +55 -0
- package/dist/receipt-export.js +149 -0
- package/dist/receipt.d.ts +108 -0
- package/dist/receipt.js +213 -0
- package/dist/solana.d.ts +5 -0
- package/dist/solana.js +20 -0
- package/dist/token-capabilities.d.ts +9 -0
- package/dist/token-capabilities.js +139 -0
- package/dist/token.d.ts +18 -0
- package/dist/token.js +55 -0
- package/dist/transaction-reader.d.ts +10 -0
- package/dist/transaction-reader.js +18 -0
- package/dist/transaction-v1.d.ts +18 -0
- package/dist/transaction-v1.js +73 -0
- package/dist/types.d.ts +267 -0
- package/dist/types.js +1 -0
- package/dist/x402-adapt.d.ts +20 -0
- package/dist/x402-adapt.js +62 -0
- package/dist/x402-challenge.d.ts +116 -0
- package/dist/x402-challenge.js +385 -0
- package/dist/x402.d.ts +18 -0
- package/dist/x402.js +35 -0
- package/package.json +54 -4
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { PaymentReceipt, PaymentRequestPayload, PaymentRequestVerification, SignedPaymentRequest } from "./types.js";
|
|
2
|
+
/** Bounds for the optional "what was bought" fields. The relay enforces the same. */
|
|
3
|
+
export declare const MAX_PAYMENT_REQUEST_DESCRIPTION_LENGTH = 280;
|
|
4
|
+
export declare const MAX_PAYMENT_REQUEST_LINE_ITEMS = 20;
|
|
5
|
+
export declare const MAX_PAYMENT_REQUEST_LINE_ITEM_LABEL_LENGTH = 120;
|
|
6
|
+
/**
|
|
7
|
+
* Check the optional description and line items. Returns a reason when they
|
|
8
|
+
* are out of bounds, otherwise undefined.
|
|
9
|
+
*/
|
|
10
|
+
export declare function paymentRequestPurposeError(payload: PaymentRequestPayload): string | undefined;
|
|
11
|
+
export declare function canonicalPaymentRequest(payload: PaymentRequestPayload): string;
|
|
12
|
+
export declare function verifyPaymentRequest(request: SignedPaymentRequest, currentSlot?: bigint): Promise<PaymentRequestVerification>;
|
|
13
|
+
export declare function paymentRequestTokenProgramAddress(request: PaymentRequestPayload): string;
|
|
14
|
+
export type ReceiptPurchaseMismatch = "amount" | "mint" | "recipient";
|
|
15
|
+
export type ReceiptPurchaseVerification = {
|
|
16
|
+
/** Merchant signature is valid and the request hashes to the receipt's invoice hash. */
|
|
17
|
+
valid: boolean;
|
|
18
|
+
/** `valid`, and the receipt paid the amount, mint, and recipient the request named. */
|
|
19
|
+
matched: boolean;
|
|
20
|
+
signatureValid: boolean;
|
|
21
|
+
hashMatches: boolean;
|
|
22
|
+
/** Fields the receipt paid differently from the request. Empty when matched. */
|
|
23
|
+
mismatches: ReceiptPurchaseMismatch[];
|
|
24
|
+
payload: PaymentRequestPayload;
|
|
25
|
+
invoiceHash: Uint8Array;
|
|
26
|
+
reason?: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Check that a merchant-signed request is the one a settled receipt paid:
|
|
30
|
+
* the signature verifies, and sha256 of its canonical form equals the
|
|
31
|
+
* receipt's invoice hash. Request expiry is not enforced, because a request
|
|
32
|
+
* that has since expired is still the request that was paid.
|
|
33
|
+
*
|
|
34
|
+
* The program binds the invoice hash, not the request's amount, mint, or
|
|
35
|
+
* recipient, so those are compared too and reported as mismatches.
|
|
36
|
+
*/
|
|
37
|
+
export declare function verifyReceiptPurchase(receipt: Pick<PaymentReceipt, "invoiceHash" | "amount" | "mint" | "recipientTokenAccount">, signedRequest: SignedPaymentRequest): Promise<ReceiptPurchaseVerification>;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { address, tokenProgramAddress } from "./encoding.js";
|
|
3
|
+
const MAX_U64 = 18446744073709551615n;
|
|
4
|
+
/** Bounds for the optional "what was bought" fields. The relay enforces the same. */
|
|
5
|
+
export const MAX_PAYMENT_REQUEST_DESCRIPTION_LENGTH = 280;
|
|
6
|
+
export const MAX_PAYMENT_REQUEST_LINE_ITEMS = 20;
|
|
7
|
+
export const MAX_PAYMENT_REQUEST_LINE_ITEM_LABEL_LENGTH = 120;
|
|
8
|
+
const MAX_QUANTITY_LENGTH = 32;
|
|
9
|
+
function orderedLineItem(item) {
|
|
10
|
+
// Canonical order is label, amount, quantity; absent fields are omitted.
|
|
11
|
+
if (!item || typeof item !== "object")
|
|
12
|
+
return item;
|
|
13
|
+
return {
|
|
14
|
+
label: item.label,
|
|
15
|
+
...(item.amount === undefined ? {} : { amount: item.amount }),
|
|
16
|
+
...(item.quantity === undefined ? {} : { quantity: item.quantity }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function orderedPayload(payload) {
|
|
20
|
+
return {
|
|
21
|
+
version: 1,
|
|
22
|
+
cluster: payload.cluster,
|
|
23
|
+
merchant: address(payload.merchant),
|
|
24
|
+
invoice: payload.invoice,
|
|
25
|
+
mint: address(payload.mint),
|
|
26
|
+
tokenProgram: payload.tokenProgram,
|
|
27
|
+
recipient: address(payload.recipient),
|
|
28
|
+
amount: payload.amount,
|
|
29
|
+
decimals: payload.decimals,
|
|
30
|
+
nonce: payload.nonce,
|
|
31
|
+
...(payload.expiresAtSlot === undefined ? {} : { expiresAtSlot: payload.expiresAtSlot }),
|
|
32
|
+
...(payload.resource === undefined ? {} : { resource: payload.resource }),
|
|
33
|
+
// Included only when present, after resource, so a request without them
|
|
34
|
+
// keeps the exact canonical bytes and invoice hash it always had.
|
|
35
|
+
...(payload.description === undefined ? {} : { description: payload.description }),
|
|
36
|
+
...(payload.lineItems === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { lineItems: Array.isArray(payload.lineItems) ? payload.lineItems.map(orderedLineItem) : payload.lineItems }),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Length in Unicode code points, the unit the relay counts in. */
|
|
42
|
+
function codePoints(value) {
|
|
43
|
+
return Array.from(value).length;
|
|
44
|
+
}
|
|
45
|
+
const CONTROL_CHARACTER = /[\u0000-\u001f\u007f-\u009f]/;
|
|
46
|
+
/**
|
|
47
|
+
* Check the optional description and line items. Returns a reason when they
|
|
48
|
+
* are out of bounds, otherwise undefined.
|
|
49
|
+
*/
|
|
50
|
+
export function paymentRequestPurposeError(payload) {
|
|
51
|
+
if (payload.description !== undefined) {
|
|
52
|
+
const description = payload.description;
|
|
53
|
+
if (typeof description !== "string"
|
|
54
|
+
|| !description.trim()
|
|
55
|
+
|| codePoints(description) > MAX_PAYMENT_REQUEST_DESCRIPTION_LENGTH) {
|
|
56
|
+
return `Description must be 1 to ${MAX_PAYMENT_REQUEST_DESCRIPTION_LENGTH} characters`;
|
|
57
|
+
}
|
|
58
|
+
if (CONTROL_CHARACTER.test(description))
|
|
59
|
+
return "Description must be a single line of text";
|
|
60
|
+
}
|
|
61
|
+
if (payload.lineItems !== undefined) {
|
|
62
|
+
const items = payload.lineItems;
|
|
63
|
+
if (!Array.isArray(items) || items.length === 0 || items.length > MAX_PAYMENT_REQUEST_LINE_ITEMS) {
|
|
64
|
+
return `Line items must contain 1 to ${MAX_PAYMENT_REQUEST_LINE_ITEMS} items`;
|
|
65
|
+
}
|
|
66
|
+
for (const item of items) {
|
|
67
|
+
if (!item || typeof item !== "object")
|
|
68
|
+
return "Each line item must be an object";
|
|
69
|
+
if (typeof item.label !== "string"
|
|
70
|
+
|| !item.label.trim()
|
|
71
|
+
|| codePoints(item.label) > MAX_PAYMENT_REQUEST_LINE_ITEM_LABEL_LENGTH
|
|
72
|
+
|| CONTROL_CHARACTER.test(item.label)) {
|
|
73
|
+
return `Each line item label must be one line of 1 to ${MAX_PAYMENT_REQUEST_LINE_ITEM_LABEL_LENGTH} characters`;
|
|
74
|
+
}
|
|
75
|
+
if (item.amount !== undefined) {
|
|
76
|
+
if (typeof item.amount !== "string" || !/^\d+$/.test(item.amount) || BigInt(item.amount) > MAX_U64) {
|
|
77
|
+
return "Line item amount must be an unsigned integer string in base units";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (item.quantity !== undefined) {
|
|
81
|
+
if (typeof item.quantity !== "string"
|
|
82
|
+
|| item.quantity.length > MAX_QUANTITY_LENGTH
|
|
83
|
+
|| !/^\d+(\.\d+)?$/.test(item.quantity)) {
|
|
84
|
+
return "Line item quantity must be an unsigned decimal string";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
export function canonicalPaymentRequest(payload) {
|
|
92
|
+
return JSON.stringify(orderedPayload(payload));
|
|
93
|
+
}
|
|
94
|
+
function base64Bytes(value) {
|
|
95
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
96
|
+
const binary = atob(normalized);
|
|
97
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
98
|
+
}
|
|
99
|
+
async function sha256(value) {
|
|
100
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", value.slice().buffer);
|
|
101
|
+
return new Uint8Array(digest);
|
|
102
|
+
}
|
|
103
|
+
function validationError(payload, reason) {
|
|
104
|
+
return { valid: false, payload, invoiceHash: new Uint8Array(32), reason };
|
|
105
|
+
}
|
|
106
|
+
export async function verifyPaymentRequest(request, currentSlot) {
|
|
107
|
+
if (request.payload.version !== 1)
|
|
108
|
+
return validationError(request.payload, "Unsupported payment request version");
|
|
109
|
+
const payload = orderedPayload(request.payload);
|
|
110
|
+
try {
|
|
111
|
+
if (payload.version !== 1)
|
|
112
|
+
return validationError(payload, "Unsupported payment request version");
|
|
113
|
+
if (payload.cluster !== "devnet" && payload.cluster !== "mainnet-beta") {
|
|
114
|
+
return validationError(payload, "Unsupported Solana cluster");
|
|
115
|
+
}
|
|
116
|
+
if (!payload.invoice.trim() || !payload.nonce.trim()) {
|
|
117
|
+
return validationError(payload, "Payment request invoice and nonce are required");
|
|
118
|
+
}
|
|
119
|
+
if (!/^\d+$/.test(payload.amount))
|
|
120
|
+
return validationError(payload, "Amount must be an unsigned integer string");
|
|
121
|
+
const amount = BigInt(payload.amount);
|
|
122
|
+
if (amount <= 0n || amount > MAX_U64)
|
|
123
|
+
return validationError(payload, "Amount must fit in u64 and be positive");
|
|
124
|
+
if (!Number.isInteger(payload.decimals) || payload.decimals < 0 || payload.decimals > 255) {
|
|
125
|
+
return validationError(payload, "Decimals must be between 0 and 255");
|
|
126
|
+
}
|
|
127
|
+
if (payload.tokenProgram !== "spl-token" && payload.tokenProgram !== "token-2022") {
|
|
128
|
+
return validationError(payload, "Unsupported token program");
|
|
129
|
+
}
|
|
130
|
+
address(payload.merchant);
|
|
131
|
+
address(payload.mint);
|
|
132
|
+
address(payload.recipient);
|
|
133
|
+
const purposeError = paymentRequestPurposeError(payload);
|
|
134
|
+
if (purposeError)
|
|
135
|
+
return validationError(payload, purposeError);
|
|
136
|
+
if (payload.expiresAtSlot !== undefined) {
|
|
137
|
+
if (!/^\d+$/.test(payload.expiresAtSlot))
|
|
138
|
+
return validationError(payload, "Expiry slot must be an unsigned integer");
|
|
139
|
+
if (currentSlot !== undefined && BigInt(payload.expiresAtSlot) <= currentSlot) {
|
|
140
|
+
return validationError(payload, "Payment request has expired");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const signature = base64Bytes(request.signature);
|
|
144
|
+
if (signature.length !== 64)
|
|
145
|
+
return validationError(payload, "Ed25519 signature must be 64 bytes");
|
|
146
|
+
const merchant = new PublicKey(payload.merchant).toBytes();
|
|
147
|
+
const key = await globalThis.crypto.subtle.importKey("raw", merchant.slice().buffer, { name: "Ed25519" }, false, ["verify"]);
|
|
148
|
+
const message = new TextEncoder().encode(canonicalPaymentRequest(payload));
|
|
149
|
+
const validSignature = await globalThis.crypto.subtle.verify("Ed25519", key, signature.slice().buffer, message.slice().buffer);
|
|
150
|
+
if (!validSignature)
|
|
151
|
+
return validationError(payload, "Payment request signature is invalid");
|
|
152
|
+
const invoiceHash = await sha256(message);
|
|
153
|
+
return { valid: true, payload, invoiceHash };
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
return validationError(payload, error instanceof Error ? error.message : String(error));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export function paymentRequestTokenProgramAddress(request) {
|
|
160
|
+
return tokenProgramAddress(request.tokenProgram);
|
|
161
|
+
}
|
|
162
|
+
function bytesEqual(left, right) {
|
|
163
|
+
return left.length === right.length && left.every((byte, index) => byte === right[index]);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Check that a merchant-signed request is the one a settled receipt paid:
|
|
167
|
+
* the signature verifies, and sha256 of its canonical form equals the
|
|
168
|
+
* receipt's invoice hash. Request expiry is not enforced, because a request
|
|
169
|
+
* that has since expired is still the request that was paid.
|
|
170
|
+
*
|
|
171
|
+
* The program binds the invoice hash, not the request's amount, mint, or
|
|
172
|
+
* recipient, so those are compared too and reported as mismatches.
|
|
173
|
+
*/
|
|
174
|
+
export async function verifyReceiptPurchase(receipt, signedRequest) {
|
|
175
|
+
const verification = await verifyPaymentRequest(signedRequest);
|
|
176
|
+
const base = {
|
|
177
|
+
payload: verification.payload,
|
|
178
|
+
invoiceHash: verification.invoiceHash,
|
|
179
|
+
mismatches: [],
|
|
180
|
+
};
|
|
181
|
+
if (!verification.valid) {
|
|
182
|
+
return {
|
|
183
|
+
...base,
|
|
184
|
+
valid: false,
|
|
185
|
+
matched: false,
|
|
186
|
+
signatureValid: false,
|
|
187
|
+
hashMatches: false,
|
|
188
|
+
reason: verification.reason,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (!bytesEqual(verification.invoiceHash, receipt.invoiceHash)) {
|
|
192
|
+
return {
|
|
193
|
+
...base,
|
|
194
|
+
valid: false,
|
|
195
|
+
matched: false,
|
|
196
|
+
signatureValid: true,
|
|
197
|
+
hashMatches: false,
|
|
198
|
+
reason: "This request is not the one this receipt paid: its hash differs from the receipt's invoice hash",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const payload = verification.payload;
|
|
202
|
+
const mismatches = [];
|
|
203
|
+
if (BigInt(payload.amount) !== receipt.amount)
|
|
204
|
+
mismatches.push("amount");
|
|
205
|
+
if (payload.mint !== receipt.mint)
|
|
206
|
+
mismatches.push("mint");
|
|
207
|
+
if (payload.recipient !== receipt.recipientTokenAccount)
|
|
208
|
+
mismatches.push("recipient");
|
|
209
|
+
return {
|
|
210
|
+
...base,
|
|
211
|
+
valid: true,
|
|
212
|
+
matched: mismatches.length === 0,
|
|
213
|
+
signatureValid: true,
|
|
214
|
+
hashMatches: true,
|
|
215
|
+
mismatches,
|
|
216
|
+
...(mismatches.length ? { reason: `The receipt paid a different ${mismatches.join(", ")} than the request named` } : {}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Address, BatchPreflightEntry, ChainPayInstruction, Mandate, PaymentBatchPreflight, PaymentPreflight, PaymentPreflightContext, PaymentRequest, PreparedTransaction, TokenProgram } from "./types.js";
|
|
2
|
+
export declare const DUPLICATE_INVOICE_MESSAGE = "This invoice was already paid. Nothing new was submitted.";
|
|
3
|
+
/**
|
|
4
|
+
* The receipt for this mandate and invoice hash already exists, so the invoice
|
|
5
|
+
* was paid before. Raised before anything is built, signed, or sent. The
|
|
6
|
+
* program would refuse the payment anyway; this says why in plain words.
|
|
7
|
+
*/
|
|
8
|
+
export declare class DuplicateInvoiceError extends Error {
|
|
9
|
+
readonly code: "DuplicateInvoice";
|
|
10
|
+
readonly receiptAddress: Address;
|
|
11
|
+
constructor(receiptAddress: Address);
|
|
12
|
+
}
|
|
13
|
+
export declare function isDuplicateInvoiceError(error: unknown): error is DuplicateInvoiceError;
|
|
14
|
+
/**
|
|
15
|
+
* True when a simulation or send error says the receipt account already
|
|
16
|
+
* exists ("Allocate: account ... already in use"). Matches only when the
|
|
17
|
+
* message names this payment's receipt, so an unrelated account collision is
|
|
18
|
+
* still reported as itself.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isReceiptAlreadyInUse(message: string, receiptAddress: Address): boolean;
|
|
21
|
+
export type PreparePaymentInput = {
|
|
22
|
+
mandate: Address;
|
|
23
|
+
invoiceHash: Uint8Array;
|
|
24
|
+
paymentId: Uint8Array;
|
|
25
|
+
signatureReference: Uint8Array;
|
|
26
|
+
mint: Address;
|
|
27
|
+
recipient: Address;
|
|
28
|
+
amount: bigint;
|
|
29
|
+
tokenProgram?: TokenProgram;
|
|
30
|
+
};
|
|
31
|
+
export declare function preparePayment(input: PreparePaymentInput): PaymentRequest;
|
|
32
|
+
export declare function buildExecutePaymentInstruction(request: PaymentRequest, agent: Address, mandate: Mandate, programId?: Address): ChainPayInstruction;
|
|
33
|
+
export declare function preflightPayment(request: PaymentRequest, mandate: Mandate, currentSlot: bigint, agent?: Address, receiptAlreadyExists?: boolean, sourceContext?: PaymentPreflightContext): PaymentPreflight;
|
|
34
|
+
/**
|
|
35
|
+
* Run per-payment preflight plus cumulative batch checks grouped by mandate.
|
|
36
|
+
* Dashboard batch import uses the same cumulative rules when source context
|
|
37
|
+
* is supplied for each row.
|
|
38
|
+
*/
|
|
39
|
+
export declare function preflightPaymentBatch(entries: BatchPreflightEntry[], currentSlot: bigint): PaymentBatchPreflight;
|
|
40
|
+
export declare function preparedPaymentTransaction(instructionData: ChainPayInstruction, agent: Address): PreparedTransaction;
|
package/dist/payment.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { DEFAULT_PROGRAM_ID, SYSTEM_PROGRAM_ID } from "./constants.js";
|
|
2
|
+
import { address, bytes32, encodePayment, instruction, meta, publicKey, tokenProgramAddress, } from "./encoding.js";
|
|
3
|
+
import { deriveAssetAddress, deriveConfigAddress, deriveReceiptAddress } from "./pda.js";
|
|
4
|
+
export const DUPLICATE_INVOICE_MESSAGE = "This invoice was already paid. Nothing new was submitted.";
|
|
5
|
+
/**
|
|
6
|
+
* The receipt for this mandate and invoice hash already exists, so the invoice
|
|
7
|
+
* was paid before. Raised before anything is built, signed, or sent. The
|
|
8
|
+
* program would refuse the payment anyway; this says why in plain words.
|
|
9
|
+
*/
|
|
10
|
+
export class DuplicateInvoiceError extends Error {
|
|
11
|
+
code = "DuplicateInvoice";
|
|
12
|
+
receiptAddress;
|
|
13
|
+
constructor(receiptAddress) {
|
|
14
|
+
super(DUPLICATE_INVOICE_MESSAGE);
|
|
15
|
+
this.name = "DuplicateInvoiceError";
|
|
16
|
+
this.receiptAddress = receiptAddress;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function isDuplicateInvoiceError(error) {
|
|
20
|
+
return error instanceof DuplicateInvoiceError
|
|
21
|
+
|| (typeof error === "object" && error !== null && error.code === "DuplicateInvoice");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* True when a simulation or send error says the receipt account already
|
|
25
|
+
* exists ("Allocate: account ... already in use"). Matches only when the
|
|
26
|
+
* message names this payment's receipt, so an unrelated account collision is
|
|
27
|
+
* still reported as itself.
|
|
28
|
+
*/
|
|
29
|
+
export function isReceiptAlreadyInUse(message, receiptAddress) {
|
|
30
|
+
return message.includes("already in use") && message.includes(receiptAddress);
|
|
31
|
+
}
|
|
32
|
+
export function preparePayment(input) {
|
|
33
|
+
publicKey(input.mandate);
|
|
34
|
+
publicKey(input.mint);
|
|
35
|
+
publicKey(input.recipient);
|
|
36
|
+
const untrustedRemainingAccounts = input.remainingAccounts;
|
|
37
|
+
if (untrustedRemainingAccounts?.length) {
|
|
38
|
+
throw new Error("Caller-supplied remainingAccounts are not accepted; ChainPay must resolve extension accounts from verified on-chain state");
|
|
39
|
+
}
|
|
40
|
+
bytes32(input.invoiceHash, "invoiceHash");
|
|
41
|
+
bytes32(input.paymentId, "paymentId");
|
|
42
|
+
bytes32(input.signatureReference, "signatureReference");
|
|
43
|
+
if (input.invoiceHash.length !== 32) {
|
|
44
|
+
throw new Error("invoiceHash must be 32 bytes");
|
|
45
|
+
}
|
|
46
|
+
if (input.paymentId.length !== 32) {
|
|
47
|
+
throw new Error("paymentId must be 32 bytes");
|
|
48
|
+
}
|
|
49
|
+
if (input.signatureReference.length !== 32) {
|
|
50
|
+
throw new Error("signatureReference must be 32 bytes");
|
|
51
|
+
}
|
|
52
|
+
if (input.amount <= 0n) {
|
|
53
|
+
throw new Error("amount must be positive");
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
...input,
|
|
57
|
+
invoiceHash: new Uint8Array(input.invoiceHash),
|
|
58
|
+
paymentId: new Uint8Array(input.paymentId),
|
|
59
|
+
signatureReference: new Uint8Array(input.signatureReference),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function buildExecutePaymentInstruction(request, agent, mandate, programId = DEFAULT_PROGRAM_ID) {
|
|
63
|
+
publicKey(agent);
|
|
64
|
+
if (address(request.mandate) !== mandate.address) {
|
|
65
|
+
throw new Error("Payment request mandate does not match the loaded mandate");
|
|
66
|
+
}
|
|
67
|
+
const tokenProgram = request.tokenProgram ?? mandate.tokenProgram;
|
|
68
|
+
if (!tokenProgram) {
|
|
69
|
+
throw new Error("Token program is required to build an execute_payment instruction");
|
|
70
|
+
}
|
|
71
|
+
return instruction("execute_payment", programId, [
|
|
72
|
+
meta(deriveConfigAddress(programId)),
|
|
73
|
+
meta(deriveAssetAddress(mandate.allowedMint, programId)),
|
|
74
|
+
meta(mandate.address, true),
|
|
75
|
+
meta(deriveReceiptAddress(mandate.address, request.invoiceHash, programId), true),
|
|
76
|
+
meta(agent, true, true),
|
|
77
|
+
meta(request.mint),
|
|
78
|
+
meta(mandate.sourceTokenAccount, true),
|
|
79
|
+
meta(request.recipient, true),
|
|
80
|
+
meta(tokenProgramAddress(tokenProgram)),
|
|
81
|
+
meta(SYSTEM_PROGRAM_ID),
|
|
82
|
+
], encodePayment(request));
|
|
83
|
+
}
|
|
84
|
+
function check(name, ok, message) {
|
|
85
|
+
return { name, ok, message };
|
|
86
|
+
}
|
|
87
|
+
function sourceAccountChecks(request, mandate, context) {
|
|
88
|
+
return [
|
|
89
|
+
check("source_owner", address(context.sourceOwner) === mandate.owner, address(context.sourceOwner) === mandate.owner
|
|
90
|
+
? "Source token account owner matches the mandate owner"
|
|
91
|
+
: "Source token account owner does not match the mandate owner"),
|
|
92
|
+
check("source_balance", request.amount <= context.sourceBalance, request.amount <= context.sourceBalance
|
|
93
|
+
? "Source token account balance covers this payment"
|
|
94
|
+
: "Source token account balance is insufficient for this payment"),
|
|
95
|
+
check("delegate_identity", context.delegate !== null && address(context.delegate) === mandate.address, context.delegate !== null && address(context.delegate) === mandate.address
|
|
96
|
+
? "Source token account delegates spending authority to this mandate"
|
|
97
|
+
: "Source token account is not delegated to this mandate"),
|
|
98
|
+
check("delegated_amount", context.delegatedAmount > 0n && request.amount <= context.delegatedAmount, context.delegatedAmount > 0n && request.amount <= context.delegatedAmount
|
|
99
|
+
? "Remaining delegated allowance covers this payment"
|
|
100
|
+
: context.delegatedAmount <= 0n
|
|
101
|
+
? "Source token account has no remaining delegated allowance"
|
|
102
|
+
: "Payment exceeds the remaining delegated allowance"),
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
export function preflightPayment(request, mandate, currentSlot, agent, receiptAlreadyExists = false, sourceContext) {
|
|
106
|
+
const checks = [
|
|
107
|
+
check("mandate_status", mandate.status === "active", mandate.status === "active"
|
|
108
|
+
? "Mandate is active"
|
|
109
|
+
: `Mandate is ${mandate.status}`),
|
|
110
|
+
check("approved_agent", agent === undefined || address(agent) === mandate.approvedAgent, agent === undefined || address(agent) === mandate.approvedAgent
|
|
111
|
+
? "Payment agent matches the mandate"
|
|
112
|
+
: "Payment agent does not match the mandate"),
|
|
113
|
+
check("mint", address(request.mint) === mandate.allowedMint, address(request.mint) === mandate.allowedMint
|
|
114
|
+
? "Payment mint matches the mandate"
|
|
115
|
+
: "Payment mint does not match the mandate"),
|
|
116
|
+
check("recipient", address(request.recipient) !== SYSTEM_PROGRAM_ID, address(request.recipient) !== SYSTEM_PROGRAM_ID
|
|
117
|
+
? "Payment recipient is specified for this request"
|
|
118
|
+
: "Payment recipient must be specified"),
|
|
119
|
+
check("amount_positive", request.amount > 0n, request.amount > 0n ? "Payment amount is positive" : "Payment amount must be positive"),
|
|
120
|
+
check("per_payment_limit", request.amount <= mandate.maxPerPayment, request.amount <= mandate.maxPerPayment
|
|
121
|
+
? "Payment is within the per-payment limit"
|
|
122
|
+
: "Payment exceeds the per-payment limit"),
|
|
123
|
+
check("total_limit", request.amount >= 0n && mandate.amountSpent + request.amount <= mandate.totalLimit, request.amount >= 0n && mandate.amountSpent + request.amount <= mandate.totalLimit
|
|
124
|
+
? "Payment is within the total spend limit"
|
|
125
|
+
: "Payment exceeds the total spend limit"),
|
|
126
|
+
check("payment_count_limit", mandate.maxPaymentCount === 0n || mandate.paymentCount + 1n <= mandate.maxPaymentCount, mandate.maxPaymentCount === 0n || mandate.paymentCount + 1n <= mandate.maxPaymentCount
|
|
127
|
+
? "Payment is within the payment-count limit"
|
|
128
|
+
: "Payment exceeds the payment-count limit"),
|
|
129
|
+
check("cooldown", mandate.lastPaymentSlot === 0n || currentSlot >= mandate.lastPaymentSlot + mandate.cooldownSlots, mandate.lastPaymentSlot === 0n || currentSlot >= mandate.lastPaymentSlot + mandate.cooldownSlots
|
|
130
|
+
? "Mandate cooldown has elapsed"
|
|
131
|
+
: "Mandate cooldown is still active"),
|
|
132
|
+
check("expiry", mandate.expiresAtSlot > currentSlot, mandate.expiresAtSlot > currentSlot ? "Mandate has not expired" : "Mandate has expired"),
|
|
133
|
+
check("invoice_hash", request.invoiceHash.some((byte) => byte !== 0), request.invoiceHash.some((byte) => byte !== 0)
|
|
134
|
+
? "Invoice hash is non-zero"
|
|
135
|
+
: "Invoice hash must not be all zeroes"),
|
|
136
|
+
check("payment_id", request.paymentId.some((byte) => byte !== 0), request.paymentId.some((byte) => byte !== 0)
|
|
137
|
+
? "Payment ID is non-zero"
|
|
138
|
+
: "Payment ID must not be all zeroes"),
|
|
139
|
+
check("signature_reference", request.signatureReference.some((byte) => byte !== 0), request.signatureReference.some((byte) => byte !== 0)
|
|
140
|
+
? "Signature reference is non-zero"
|
|
141
|
+
: "Signature reference must not be all zeroes"),
|
|
142
|
+
check("duplicate_invoice", !receiptAlreadyExists, receiptAlreadyExists
|
|
143
|
+
? "This invoice hash already has a receipt under the mandate"
|
|
144
|
+
: "No receipt exists for this invoice hash"),
|
|
145
|
+
check("token_program", request.tokenProgram === undefined ||
|
|
146
|
+
mandate.tokenProgram === undefined ||
|
|
147
|
+
request.tokenProgram === mandate.tokenProgram, request.tokenProgram === undefined ||
|
|
148
|
+
mandate.tokenProgram === undefined ||
|
|
149
|
+
request.tokenProgram === mandate.tokenProgram
|
|
150
|
+
? "Token program matches the loaded mandate context"
|
|
151
|
+
: "Token program does not match the source token account"),
|
|
152
|
+
...(sourceContext ? sourceAccountChecks(request, mandate, sourceContext) : []),
|
|
153
|
+
];
|
|
154
|
+
return { valid: checks.every((item) => item.ok), currentSlot, checks };
|
|
155
|
+
}
|
|
156
|
+
function batchChecksForMandateGroup(group, mandate) {
|
|
157
|
+
const totalAmount = group.reduce((total, entry) => total + entry.request.amount, 0n);
|
|
158
|
+
const checks = [
|
|
159
|
+
check("batch_total_limit", mandate.amountSpent + totalAmount <= mandate.totalLimit, mandate.amountSpent + totalAmount <= mandate.totalLimit
|
|
160
|
+
? "Batch total is within the mandate spending limit"
|
|
161
|
+
: "Together, these payments exceed the mandate total spending limit"),
|
|
162
|
+
check("batch_payment_count", mandate.maxPaymentCount === 0n || mandate.paymentCount + BigInt(group.length) <= mandate.maxPaymentCount, mandate.maxPaymentCount === 0n || mandate.paymentCount + BigInt(group.length) <= mandate.maxPaymentCount
|
|
163
|
+
? "Batch size is within the mandate payment-count limit"
|
|
164
|
+
: "Together, these payments exceed the mandate payment-count limit"),
|
|
165
|
+
check("batch_cooldown", group.length <= 1 || mandate.cooldownSlots === 0n, group.length <= 1 || mandate.cooldownSlots === 0n
|
|
166
|
+
? "Batch respects the mandate cooldown policy"
|
|
167
|
+
: "This mandate has a cooldown and can only settle once per atomic batch"),
|
|
168
|
+
];
|
|
169
|
+
const context = group.find((entry) => entry.sourceContext)?.sourceContext;
|
|
170
|
+
if (context) {
|
|
171
|
+
checks.push(check("batch_source_balance", totalAmount <= context.sourceBalance, totalAmount <= context.sourceBalance
|
|
172
|
+
? "Source token account balance covers the batch total"
|
|
173
|
+
: "Source token account balance is insufficient for the batch total"), check("batch_delegated_amount", context.delegatedAmount > 0n && totalAmount <= context.delegatedAmount, context.delegatedAmount > 0n && totalAmount <= context.delegatedAmount
|
|
174
|
+
? "Remaining delegated allowance covers the batch total"
|
|
175
|
+
: context.delegatedAmount <= 0n
|
|
176
|
+
? "Source token account has no remaining delegated allowance for this batch"
|
|
177
|
+
: "Batch total exceeds the remaining delegated allowance"));
|
|
178
|
+
}
|
|
179
|
+
return checks;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Run per-payment preflight plus cumulative batch checks grouped by mandate.
|
|
183
|
+
* Dashboard batch import uses the same cumulative rules when source context
|
|
184
|
+
* is supplied for each row.
|
|
185
|
+
*/
|
|
186
|
+
export function preflightPaymentBatch(entries, currentSlot) {
|
|
187
|
+
const preparedEntries = entries.map((entry) => ({
|
|
188
|
+
request: entry.request,
|
|
189
|
+
mandate: address(entry.mandate.address),
|
|
190
|
+
preflight: preflightPayment(entry.request, entry.mandate, currentSlot, entry.agent, entry.receiptAlreadyExists ?? false, entry.sourceContext),
|
|
191
|
+
}));
|
|
192
|
+
const groups = new Map();
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
const key = address(entry.mandate.address);
|
|
195
|
+
const group = groups.get(key) ?? [];
|
|
196
|
+
group.push(entry);
|
|
197
|
+
groups.set(key, group);
|
|
198
|
+
}
|
|
199
|
+
const batchChecks = [...groups.values()].flatMap((group) => batchChecksForMandateGroup(group, group[0].mandate));
|
|
200
|
+
const valid = preparedEntries.every((entry) => entry.preflight.valid) && batchChecks.every((item) => item.ok);
|
|
201
|
+
return {
|
|
202
|
+
valid,
|
|
203
|
+
currentSlot,
|
|
204
|
+
entries: preparedEntries,
|
|
205
|
+
batchChecks,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
export function preparedPaymentTransaction(instructionData, agent) {
|
|
209
|
+
return {
|
|
210
|
+
instructions: [instructionData],
|
|
211
|
+
requiredSigners: [agent],
|
|
212
|
+
feePayer: agent,
|
|
213
|
+
};
|
|
214
|
+
}
|
package/dist/pda.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Address } from "./types.js";
|
|
2
|
+
export declare function deriveConfigAddress(programId?: Address): Address;
|
|
3
|
+
export declare function deriveLegacyMandateAddress(owner: Address, programId?: Address): Address;
|
|
4
|
+
export declare function deriveMintMandateAddress(owner: Address, allowedMint: Address, programId?: Address): Address;
|
|
5
|
+
export declare function deriveVersionedMandateAddress(owner: Address, allowedMint: Address, mandateNonce: Address, programId?: Address): Address;
|
|
6
|
+
/**
|
|
7
|
+
* Derive a mandate address. Without a mint this returns the legacy
|
|
8
|
+
* owner-scoped address; with a mint it returns the production mint-scoped
|
|
9
|
+
* address used by new mandates.
|
|
10
|
+
*/
|
|
11
|
+
export declare function deriveMandateAddress(owner: Address, programId?: Address, allowedMint?: Address): Address;
|
|
12
|
+
export declare function deriveReceiptAddress(mandate: Address, invoiceHash: Uint8Array, programId?: Address): Address;
|
|
13
|
+
export declare function deriveAssetAddress(mint: Address, programId?: Address): Address;
|
package/dist/pda.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { CONFIG_SEED, ASSET_SEED, DEFAULT_PROGRAM_ID, MANDATE_SEED, RECEIPT_SEED, } from "./constants.js";
|
|
3
|
+
import { bytes32, publicKey } from "./encoding.js";
|
|
4
|
+
export function deriveConfigAddress(programId = DEFAULT_PROGRAM_ID) {
|
|
5
|
+
return PublicKey.findProgramAddressSync([Buffer.from(CONFIG_SEED)], publicKey(programId))[0].toBase58();
|
|
6
|
+
}
|
|
7
|
+
export function deriveLegacyMandateAddress(owner, programId = DEFAULT_PROGRAM_ID) {
|
|
8
|
+
return PublicKey.findProgramAddressSync([Buffer.from(MANDATE_SEED), publicKey(owner).toBytes()], publicKey(programId))[0].toBase58();
|
|
9
|
+
}
|
|
10
|
+
export function deriveMintMandateAddress(owner, allowedMint, programId = DEFAULT_PROGRAM_ID) {
|
|
11
|
+
return PublicKey.findProgramAddressSync([Buffer.from(MANDATE_SEED), publicKey(owner).toBytes(), publicKey(allowedMint).toBytes()], publicKey(programId))[0].toBase58();
|
|
12
|
+
}
|
|
13
|
+
export function deriveVersionedMandateAddress(owner, allowedMint, mandateNonce, programId = DEFAULT_PROGRAM_ID) {
|
|
14
|
+
return PublicKey.findProgramAddressSync([
|
|
15
|
+
Buffer.from(MANDATE_SEED),
|
|
16
|
+
publicKey(owner).toBytes(),
|
|
17
|
+
publicKey(allowedMint).toBytes(),
|
|
18
|
+
publicKey(mandateNonce).toBytes(),
|
|
19
|
+
], publicKey(programId))[0].toBase58();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Derive a mandate address. Without a mint this returns the legacy
|
|
23
|
+
* owner-scoped address; with a mint it returns the production mint-scoped
|
|
24
|
+
* address used by new mandates.
|
|
25
|
+
*/
|
|
26
|
+
export function deriveMandateAddress(owner, programId = DEFAULT_PROGRAM_ID, allowedMint) {
|
|
27
|
+
return allowedMint === undefined
|
|
28
|
+
? deriveLegacyMandateAddress(owner, programId)
|
|
29
|
+
: deriveMintMandateAddress(owner, allowedMint, programId);
|
|
30
|
+
}
|
|
31
|
+
export function deriveReceiptAddress(mandate, invoiceHash, programId = DEFAULT_PROGRAM_ID) {
|
|
32
|
+
return PublicKey.findProgramAddressSync([Buffer.from(RECEIPT_SEED), publicKey(mandate).toBytes(), bytes32(invoiceHash, "invoiceHash")], publicKey(programId))[0].toBase58();
|
|
33
|
+
}
|
|
34
|
+
export function deriveAssetAddress(mint, programId = DEFAULT_PROGRAM_ID) {
|
|
35
|
+
return PublicKey.findProgramAddressSync([Buffer.from(ASSET_SEED), publicKey(mint).toBytes()], publicKey(programId))[0].toBase58();
|
|
36
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { PaymentReceipt, PaymentRequestPayload, ReceiptPolicy } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The limits beside a receipt, preferring the snapshot the program wrote into
|
|
4
|
+
* the receipt itself. A relay observation is used only when there is none.
|
|
5
|
+
*/
|
|
6
|
+
export declare function receiptPolicy(receipt: Pick<PaymentReceipt, "policySnapshot">, relayObserved?: ReceiptPolicy | null): ReceiptPolicy;
|
|
7
|
+
/**
|
|
8
|
+
* Read the `policy` object the ChainPay relay returns from
|
|
9
|
+
* `GET /v1/receipts/{receipt}`. Only a well-formed relay observation is
|
|
10
|
+
* accepted; anything else is null, never a guessed limit. An "on-chain" value
|
|
11
|
+
* from the relay is ignored because the receipt account is read directly.
|
|
12
|
+
*/
|
|
13
|
+
export declare function relayObservedPolicy(value: unknown): ReceiptPolicy | null;
|
|
14
|
+
export declare function receiptExplorerUrl(receipt: Pick<PaymentReceipt, "address" | "transactionSignature">, cluster?: "devnet" | "mainnet-beta"): string;
|
|
15
|
+
export type ReceiptCsvRow = {
|
|
16
|
+
receipt: PaymentReceipt;
|
|
17
|
+
/** Verified mint decimals. Null keeps amounts in labeled base units. */
|
|
18
|
+
decimals: number | null;
|
|
19
|
+
symbol?: string;
|
|
20
|
+
/** Unix seconds from getBlockTime for the executed slot. Null when unknown. */
|
|
21
|
+
blockTime?: number | null;
|
|
22
|
+
/** Defaults to the receipt's own snapshot, else "not-recorded". */
|
|
23
|
+
policy?: ReceiptPolicy;
|
|
24
|
+
/** From the merchant-signed request, only after verifyReceiptPurchase. */
|
|
25
|
+
purpose?: Partial<Pick<PaymentRequestPayload, "invoice" | "description" | "lineItems">>;
|
|
26
|
+
verifyUrl?: string;
|
|
27
|
+
explorerUrl?: string;
|
|
28
|
+
/** From the mandate request the owner accepted, when one is linked. */
|
|
29
|
+
poNumber?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The Order match pill as the reader saw it ("Matched", "Payee differs",
|
|
32
|
+
* "No order", ...). Empty when it was not checked. A check, not a guarantee.
|
|
33
|
+
*/
|
|
34
|
+
orderMatch?: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Accounting tools read the first five columns; the rest let a reader trace
|
|
38
|
+
* every row back to Solana. New columns are appended so existing imports keep
|
|
39
|
+
* their column positions.
|
|
40
|
+
*/
|
|
41
|
+
export declare const RECEIPT_CSV_HEADERS: readonly ["Date", "Description", "Amount", "Payee", "Reference", "Token", "Agent", "Spending permission", "Per-payment limit", "Total limit", "Spent after", "Limits source", "Receipt", "Verify URL", "Explorer URL", "PO number", "Order match"];
|
|
42
|
+
/**
|
|
43
|
+
* One CSV cell. A value a spreadsheet would run as a formula (leading =, +,
|
|
44
|
+
* -, @, tab, or carriage return) is prefixed with an apostrophe so it stays
|
|
45
|
+
* text. Then RFC 4180 quoting: wrap in quotes when the value holds a comma,
|
|
46
|
+
* quote, or line break, doubling inner quotes.
|
|
47
|
+
*/
|
|
48
|
+
export declare function csvCell(value: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* One CSV of receipts with QuickBooks-friendly leading columns (Date,
|
|
51
|
+
* Description, Amount, Payee, Reference), then the ChainPay columns. Amounts
|
|
52
|
+
* are exact decimals from base units, never floating point. CRLF line
|
|
53
|
+
* endings, header row always present.
|
|
54
|
+
*/
|
|
55
|
+
export declare function receiptsToCsv(rows: readonly ReceiptCsvRow[]): string;
|