@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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -2
  3. package/dist/accounts.d.ts +23 -0
  4. package/dist/accounts.js +148 -0
  5. package/dist/cards/accounts.d.ts +156 -0
  6. package/dist/cards/accounts.js +346 -0
  7. package/dist/cards/api.d.ts +379 -0
  8. package/dist/cards/api.js +241 -0
  9. package/dist/cards/commitment.d.ts +86 -0
  10. package/dist/cards/commitment.js +288 -0
  11. package/dist/cards/constants.d.ts +160 -0
  12. package/dist/cards/constants.js +163 -0
  13. package/dist/cards/draft.d.ts +57 -0
  14. package/dist/cards/draft.js +131 -0
  15. package/dist/cards/evidence.d.ts +69 -0
  16. package/dist/cards/evidence.js +41 -0
  17. package/dist/cards/hash.d.ts +9 -0
  18. package/dist/cards/hash.js +47 -0
  19. package/dist/cards/index.d.ts +14 -0
  20. package/dist/cards/index.js +14 -0
  21. package/dist/cards/instructions.d.ts +302 -0
  22. package/dist/cards/instructions.js +474 -0
  23. package/dist/cards/layout.d.ts +33 -0
  24. package/dist/cards/layout.js +137 -0
  25. package/dist/cards/math.d.ts +67 -0
  26. package/dist/cards/math.js +138 -0
  27. package/dist/cards/merchants.d.ts +23 -0
  28. package/dist/cards/merchants.js +38 -0
  29. package/dist/cards/pda.d.ts +42 -0
  30. package/dist/cards/pda.js +93 -0
  31. package/dist/cards/private-repayment.d.ts +198 -0
  32. package/dist/cards/private-repayment.js +485 -0
  33. package/dist/cards/redact.d.ts +18 -0
  34. package/dist/cards/redact.js +103 -0
  35. package/dist/cards/tee.d.ts +253 -0
  36. package/dist/cards/tee.js +605 -0
  37. package/dist/cli.d.ts +31 -0
  38. package/dist/cli.js +351 -0
  39. package/dist/client.d.ts +80 -0
  40. package/dist/client.js +493 -0
  41. package/dist/constants.d.ts +44 -0
  42. package/dist/constants.js +43 -0
  43. package/dist/crossmint-adapt.d.ts +43 -0
  44. package/dist/crossmint-adapt.js +68 -0
  45. package/dist/crossmint-order.d.ts +220 -0
  46. package/dist/crossmint-order.js +638 -0
  47. package/dist/delivery.d.ts +66 -0
  48. package/dist/delivery.js +232 -0
  49. package/dist/encoding.d.ts +51 -0
  50. package/dist/encoding.js +128 -0
  51. package/dist/index.d.ts +27 -0
  52. package/dist/index.js +26 -0
  53. package/dist/known-assets.d.ts +30 -0
  54. package/dist/known-assets.js +49 -0
  55. package/dist/mandate-request.d.ts +123 -0
  56. package/dist/mandate-request.js +401 -0
  57. package/dist/mandate.d.ts +44 -0
  58. package/dist/mandate.js +157 -0
  59. package/dist/ops-snapshot.d.ts +157 -0
  60. package/dist/ops-snapshot.js +356 -0
  61. package/dist/payment-request.d.ts +37 -0
  62. package/dist/payment-request.js +218 -0
  63. package/dist/payment.d.ts +40 -0
  64. package/dist/payment.js +214 -0
  65. package/dist/pda.d.ts +13 -0
  66. package/dist/pda.js +36 -0
  67. package/dist/receipt-export.d.ts +55 -0
  68. package/dist/receipt-export.js +149 -0
  69. package/dist/receipt.d.ts +108 -0
  70. package/dist/receipt.js +213 -0
  71. package/dist/solana.d.ts +5 -0
  72. package/dist/solana.js +20 -0
  73. package/dist/token-capabilities.d.ts +9 -0
  74. package/dist/token-capabilities.js +139 -0
  75. package/dist/token.d.ts +18 -0
  76. package/dist/token.js +55 -0
  77. package/dist/transaction-reader.d.ts +10 -0
  78. package/dist/transaction-reader.js +18 -0
  79. package/dist/transaction-v1.d.ts +18 -0
  80. package/dist/transaction-v1.js +73 -0
  81. package/dist/types.d.ts +267 -0
  82. package/dist/types.js +1 -0
  83. package/dist/x402-adapt.d.ts +20 -0
  84. package/dist/x402-adapt.js +62 -0
  85. package/dist/x402-challenge.d.ts +116 -0
  86. package/dist/x402-challenge.js +385 -0
  87. package/dist/x402.d.ts +18 -0
  88. package/dist/x402.js +35 -0
  89. package/package.json +54 -4
package/dist/client.js ADDED
@@ -0,0 +1,493 @@
1
+ import { Connection, } from "@solana/web3.js";
2
+ import { DEFAULT_PROGRAM_ID, DEVNET_RPC_URL, RECEIPT_ACCOUNT_LENGTHS, SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, } from "./constants.js";
3
+ import { decodeMandate, decodeProtocolConfig, decodeSupportedAsset } from "./accounts.js";
4
+ import { address, publicKey, tokenProgramFromAddress, } from "./encoding.js";
5
+ import { buildApproveDelegateInstruction, buildCreateMandateTransaction, buildInitializeConfigInstruction, buildPauseMandateInstruction, buildRevokeDelegateInstruction, buildRevokeMandateInstruction, buildRegisterAssetInstruction, buildSetAssetStatusInstruction, buildUpdateMandateInstruction, } from "./mandate.js";
6
+ import { DUPLICATE_INVOICE_MESSAGE, DuplicateInvoiceError, buildExecutePaymentInstruction, isDuplicateInvoiceError, isReceiptAlreadyInUse, preflightPayment, preparePayment as preparePaymentRequest, preparedPaymentTransaction, } from "./payment.js";
7
+ import { deriveAssetAddress, deriveConfigAddress, deriveReceiptAddress } from "./pda.js";
8
+ import { buildCreateAssociatedTokenAccountInstruction, deriveAssociatedTokenAddress, paymentPreflightContextFromTokenAccount, readTokenAccountFields, } from "./token.js";
9
+ import { amountDisplayFromMint, readCurrentMandateFields, readPaymentReceiptAccount, readPublicSettledReceipt, readVerifiedMintDecimals, } from "./receipt.js";
10
+ import { inspectTokenCapabilities } from "./token-capabilities.js";
11
+ export class ChainPayClient {
12
+ connection;
13
+ programId;
14
+ commitment;
15
+ constructor(options = {}) {
16
+ if (options instanceof Connection) {
17
+ this.connection = options;
18
+ this.programId = DEFAULT_PROGRAM_ID;
19
+ this.commitment = "confirmed";
20
+ return;
21
+ }
22
+ this.connection = new Connection(options.rpcUrl ?? DEVNET_RPC_URL, options.commitment ?? "confirmed");
23
+ this.programId = address(options.programId ?? DEFAULT_PROGRAM_ID);
24
+ this.commitment = options.commitment ?? "confirmed";
25
+ }
26
+ async getCurrentSlot() {
27
+ return BigInt(await this.connection.getSlot(this.commitment));
28
+ }
29
+ async getConfig() {
30
+ const account = await this.getProgramAccount(deriveConfigAddress(this.programId));
31
+ return account ? decodeProtocolConfig(account.data, account.address) : null;
32
+ }
33
+ async getMandateCreationMetadata(mandateAddress) {
34
+ try {
35
+ let before;
36
+ let oldest;
37
+ // Signatures are returned newest first. Walk pages so updates and
38
+ // payments do not make the creation time look newer than it is.
39
+ const historyCommitment = this.commitment === "finalized" ? "finalized" : "confirmed";
40
+ for (let page = 0; page < 100; page += 1) {
41
+ const options = { limit: 100 };
42
+ if (before)
43
+ options.before = before;
44
+ const signatures = await this.connection.getSignaturesForAddress(publicKey(mandateAddress), options, historyCommitment);
45
+ if (signatures.length === 0)
46
+ break;
47
+ oldest = signatures[signatures.length - 1];
48
+ if (signatures.length < 100)
49
+ break;
50
+ before = oldest.signature;
51
+ }
52
+ if (!oldest)
53
+ return {};
54
+ return {
55
+ ...(oldest.blockTime === null ? {} : { createdAt: oldest.blockTime }),
56
+ createdAtSlot: BigInt(oldest.slot),
57
+ };
58
+ }
59
+ catch {
60
+ // History is display metadata only. A temporary history RPC failure
61
+ // must not hide a valid mandate or block payment preparation.
62
+ return {};
63
+ }
64
+ }
65
+ async getMandate(mandateAddress) {
66
+ const currentSlot = await this.getCurrentSlot();
67
+ const account = await this.getProgramAccount(mandateAddress);
68
+ if (!account)
69
+ return null;
70
+ const decoded = decodeMandate(account.data, account.address, currentSlot);
71
+ const source = await this.connection.getAccountInfo(publicKey(decoded.sourceTokenAccount), this.commitment);
72
+ const tokenProgram = source ? tokenProgramFromAddress(source.owner.toBase58()) : undefined;
73
+ const creation = await this.getMandateCreationMetadata(decoded.address);
74
+ return { ...decoded, tokenProgram, ...creation };
75
+ }
76
+ async getMandatesByOwner(owner) {
77
+ const currentSlot = await this.getCurrentSlot();
78
+ const accounts = await this.connection.getProgramAccounts(publicKey(this.programId), {
79
+ commitment: this.commitment,
80
+ filters: [
81
+ { dataSize: 235 },
82
+ { memcmp: { offset: 8, bytes: owner } },
83
+ ],
84
+ });
85
+ const sourceAccounts = new Map();
86
+ return Promise.all(accounts.map(async (account) => {
87
+ const address = account.pubkey.toBase58();
88
+ const decoded = decodeMandate(new Uint8Array(account.account.data), address, currentSlot);
89
+ let sourceRequest = sourceAccounts.get(decoded.sourceTokenAccount);
90
+ if (!sourceRequest) {
91
+ sourceRequest = this.connection.getAccountInfo(publicKey(decoded.sourceTokenAccount), this.commitment);
92
+ sourceAccounts.set(decoded.sourceTokenAccount, sourceRequest);
93
+ }
94
+ const source = await sourceRequest;
95
+ const tokenProgram = source ? tokenProgramFromAddress(source.owner.toBase58()) : undefined;
96
+ const creation = await this.getMandateCreationMetadata(decoded.address);
97
+ return { ...decoded, tokenProgram, ...creation };
98
+ }));
99
+ }
100
+ async getSupportedAsset(mint) {
101
+ const assetAddress = deriveAssetAddress(mint, this.programId);
102
+ const account = await this.getProgramAccount(assetAddress);
103
+ return account ? decodeSupportedAsset(account.data, account.address) : null;
104
+ }
105
+ async getSupportedAssets() {
106
+ const accounts = await this.connection.getProgramAccounts(publicKey(this.programId), {
107
+ commitment: this.commitment,
108
+ filters: [{ dataSize: 106 }],
109
+ });
110
+ return accounts
111
+ .map((account) => decodeSupportedAsset(new Uint8Array(account.account.data), account.pubkey.toBase58()))
112
+ .sort((left, right) => left.mint.localeCompare(right.mint));
113
+ }
114
+ /**
115
+ * Inspect the canonical token account for one enabled registry asset and,
116
+ * when absent, return an unsigned creation transaction for wallet review.
117
+ */
118
+ async prepareAssociatedTokenAccount(input) {
119
+ const mint = address(input.mint);
120
+ const asset = await this.getSupportedAsset(mint);
121
+ if (!asset || !asset.enabled) {
122
+ throw new Error(`Asset is not enabled in the ChainPay registry: ${mint}`);
123
+ }
124
+ return this.prepareAssociatedTokenAccountForAsset(asset, input.owner, input.payer);
125
+ }
126
+ /** Inspect every enabled registry asset. Disabled assets are intentionally excluded. */
127
+ async prepareRegisteredAssetTokenAccounts(owner, payer) {
128
+ const assets = (await this.getSupportedAssets()).filter((asset) => asset.enabled);
129
+ return Promise.all(assets.map((asset) => (this.prepareAssociatedTokenAccountForAsset(asset, owner, payer))));
130
+ }
131
+ async prepareAssociatedTokenAccountForAsset(asset, ownerInput, payerInput) {
132
+ const owner = address(ownerInput);
133
+ const payer = address(payerInput ?? owner);
134
+ const mint = address(asset.mint);
135
+ const tokenProgram = tokenProgramFromAddress(asset.tokenProgram);
136
+ if (!tokenProgram) {
137
+ throw new Error(`Registry asset uses an unsupported token program: ${asset.tokenProgram}`);
138
+ }
139
+ const mintAccount = await this.connection.getAccountInfo(publicKey(mint), this.commitment);
140
+ if (!mintAccount)
141
+ throw new Error(`Mint account not found: ${mint}`);
142
+ const mintProgram = tokenProgramFromAddress(mintAccount.owner.toBase58());
143
+ if (!mintProgram) {
144
+ throw new Error(`Mint is not owned by a supported token program: ${mintAccount.owner.toBase58()}`);
145
+ }
146
+ const verifiedMint = readVerifiedMintDecimals({
147
+ owner: mintAccount.owner.toBase58(),
148
+ data: new Uint8Array(mintAccount.data),
149
+ });
150
+ if (!verifiedMint.ok)
151
+ throw new Error(`Mint account data is invalid: ${mint}`);
152
+ if (mintProgram !== tokenProgram) {
153
+ throw new Error(`Registry token program does not match mint owner for ${mint}`);
154
+ }
155
+ const associatedTokenAccount = deriveAssociatedTokenAddress(owner, mint, tokenProgram);
156
+ const existing = await this.connection.getAccountInfo(publicKey(associatedTokenAccount), this.commitment);
157
+ const base = {
158
+ asset,
159
+ address: associatedTokenAccount,
160
+ owner,
161
+ mint,
162
+ tokenProgram,
163
+ };
164
+ if (existing) {
165
+ if (existing.owner.toBase58() !== asset.tokenProgram) {
166
+ throw new Error(`Canonical token account is owned by an unexpected program: ${associatedTokenAccount}`);
167
+ }
168
+ const fields = readTokenAccountFields(new Uint8Array(existing.data));
169
+ if (!fields || fields.owner !== owner || fields.mint !== mint) {
170
+ throw new Error(`Canonical token account data is invalid: ${associatedTokenAccount}`);
171
+ }
172
+ return { ...base, status: "ready" };
173
+ }
174
+ return {
175
+ ...base,
176
+ status: "missing",
177
+ transaction: {
178
+ instructions: [buildCreateAssociatedTokenAccountInstruction({
179
+ payer,
180
+ owner,
181
+ mint,
182
+ tokenProgram,
183
+ })],
184
+ requiredSigners: [payer],
185
+ feePayer: payer,
186
+ },
187
+ };
188
+ }
189
+ async getPayment(lookup) {
190
+ const receiptAddress = typeof lookup === "string"
191
+ ? address(lookup)
192
+ : deriveReceiptAddress(lookup.mandate, lookup.invoiceHash, this.programId);
193
+ const account = await this.getProgramAccount(receiptAddress);
194
+ if (!account)
195
+ return null;
196
+ const result = readPaymentReceiptAccount({ address: account.address, owner: this.programId, data: account.data }, { programId: this.programId, requireSettled: false });
197
+ if (!result.valid)
198
+ throw new Error(result.reason);
199
+ return result.receipt;
200
+ }
201
+ /**
202
+ * Current mandate account fields only. Does not page creation history or
203
+ * load source token metadata. Public verify uses this for optional
204
+ * enrichment; failure must not invalidate a settled receipt.
205
+ */
206
+ async getCurrentMandateFields(mandateAddress) {
207
+ const currentSlot = await this.getCurrentSlot();
208
+ try {
209
+ const account = await this.getProgramAccount(mandateAddress);
210
+ return readCurrentMandateFields(account ? { address: account.address, owner: this.programId, data: account.data } : null, { programId: this.programId, currentSlot, expectedAddress: mandateAddress });
211
+ }
212
+ catch (error) {
213
+ return {
214
+ status: "unavailable",
215
+ reason: error instanceof Error ? error.message : String(error),
216
+ };
217
+ }
218
+ }
219
+ async getVerifiedSettledPayment(receiptAddress) {
220
+ const normalized = address(receiptAddress);
221
+ const info = await this.connection.getAccountInfo(publicKey(normalized), this.commitment);
222
+ if (!info) {
223
+ return { valid: false, code: "not_found", reason: "Receipt account not found" };
224
+ }
225
+ return readPublicSettledReceipt({ address: normalized, owner: info.owner.toBase58(), data: new Uint8Array(info.data) }, { programId: this.programId });
226
+ }
227
+ async readPublicReceipt(receiptAddress) {
228
+ const receipt = await this.getVerifiedSettledPayment(receiptAddress);
229
+ if (!receipt.valid) {
230
+ return { receipt, amount: null, currentMandate: { status: "absent" } };
231
+ }
232
+ let mintAccount = null;
233
+ try {
234
+ const mintInfo = await this.connection.getAccountInfo(publicKey(receipt.receipt.mint), this.commitment);
235
+ if (mintInfo) {
236
+ mintAccount = { owner: mintInfo.owner.toBase58(), data: new Uint8Array(mintInfo.data) };
237
+ }
238
+ }
239
+ catch {
240
+ mintAccount = null;
241
+ }
242
+ let currentMandate = { status: "absent" };
243
+ try {
244
+ currentMandate = await this.getCurrentMandateFields(receipt.receipt.mandate);
245
+ }
246
+ catch (error) {
247
+ currentMandate = {
248
+ status: "unavailable",
249
+ reason: error instanceof Error ? error.message : String(error),
250
+ };
251
+ }
252
+ return {
253
+ receipt,
254
+ amount: amountDisplayFromMint(receipt.receipt.amount, mintAccount),
255
+ currentMandate,
256
+ };
257
+ }
258
+ async getPaymentsByMandate(mandateAddress) {
259
+ const mandate = address(mandateAddress);
260
+ // RPC filters match one exact size, and original and snapshot receipts
261
+ // coexist after the program upgrade. Ask for each size; a query that
262
+ // asked for only one would silently drop the other half of the history.
263
+ const pages = await Promise.all(RECEIPT_ACCOUNT_LENGTHS.map((dataSize) => (this.connection.getProgramAccounts(publicKey(this.programId), {
264
+ commitment: this.commitment,
265
+ filters: [
266
+ { dataSize },
267
+ { memcmp: { offset: 8, bytes: mandate } },
268
+ ],
269
+ }))));
270
+ const seen = new Set();
271
+ const accounts = pages.flat().filter((account) => {
272
+ const key = account.pubkey.toBase58();
273
+ if (seen.has(key))
274
+ return false;
275
+ seen.add(key);
276
+ return true;
277
+ });
278
+ const historyCommitment = this.commitment === "finalized" ? "finalized" : "confirmed";
279
+ const receipts = await Promise.all(accounts.map(async (account) => {
280
+ let transactionSignature;
281
+ try {
282
+ const history = await this.connection.getSignaturesForAddress(account.pubkey, { limit: 1 }, historyCommitment);
283
+ transactionSignature = history[0]?.signature;
284
+ }
285
+ catch {
286
+ // The receipt itself remains verifiable if transaction history is
287
+ // temporarily unavailable. The dashboard can retry enrichment.
288
+ }
289
+ const result = readPaymentReceiptAccount({
290
+ address: account.pubkey.toBase58(),
291
+ owner: account.account.owner.toBase58(),
292
+ data: new Uint8Array(account.account.data),
293
+ }, { programId: this.programId, requireSettled: false, transactionSignature });
294
+ // The getProgramAccounts filter is dataSize plus a memcmp on the mandate;
295
+ // it does not check the receipt discriminator. Any program-owned account of
296
+ // a receipt size matching those bytes lands here, and a partially initialized or
297
+ // future account type would otherwise reject the whole Promise.all and
298
+ // empty an owner's entire history. Skip the row, keep the rest.
299
+ return result.valid ? result.receipt : null;
300
+ }));
301
+ return receipts.filter((receipt) => receipt !== null).sort((left, right) => (left.executedAtSlot === right.executedAtSlot
302
+ ? right.address.localeCompare(left.address)
303
+ : left.executedAtSlot > right.executedAtSlot ? -1 : 1));
304
+ }
305
+ async getTokenProgram(accountAddress) {
306
+ const account = await this.connection.getAccountInfo(publicKey(accountAddress), this.commitment);
307
+ if (!account)
308
+ throw new Error(`Token account not found: ${accountAddress}`);
309
+ const tokenProgram = tokenProgramFromAddress(account.owner.toBase58());
310
+ if (!tokenProgram)
311
+ throw new Error(`Unsupported token program: ${account.owner.toBase58()}`);
312
+ return tokenProgram;
313
+ }
314
+ async getMintDecimals(mint) {
315
+ const account = await this.connection.getAccountInfo(publicKey(mint), this.commitment);
316
+ if (!account)
317
+ throw new Error(`Mint account not found: ${mint}`);
318
+ const decimals = readVerifiedMintDecimals({
319
+ owner: account.owner.toBase58(),
320
+ data: new Uint8Array(account.data),
321
+ });
322
+ if (!decimals.ok && decimals.reason === "unsupported_owner") {
323
+ throw new Error(`Mint is not owned by a supported token program: ${account.owner.toBase58()}`);
324
+ }
325
+ if (!decimals.ok)
326
+ throw new Error("Mint account data is truncated");
327
+ return decimals.decimals;
328
+ }
329
+ async buildCreateMandate(input, owner) {
330
+ const mintProgram = await this.getTokenProgram(input.allowedMint);
331
+ if (mintProgram !== input.tokenProgram) {
332
+ throw new Error(`Mint token program is ${mintProgram}, but mandate requested ${input.tokenProgram}`);
333
+ }
334
+ const sourceProgram = await this.getTokenProgram(input.sourceTokenAccount);
335
+ if (sourceProgram !== input.tokenProgram) {
336
+ throw new Error(`Source token account uses ${sourceProgram}, but mandate requested ${input.tokenProgram}`);
337
+ }
338
+ const decimals = await this.getMintDecimals(input.allowedMint);
339
+ return buildCreateMandateTransaction(input, owner, this.programId, decimals);
340
+ }
341
+ buildInitializeConfig(supportedMints, authority) {
342
+ return {
343
+ instructions: [buildInitializeConfigInstruction(supportedMints, authority, this.programId)],
344
+ requiredSigners: [authority],
345
+ feePayer: authority,
346
+ };
347
+ }
348
+ buildUpdateMandate(input, owner, mandateAddress) {
349
+ return {
350
+ instructions: [buildUpdateMandateInstruction(input, owner, this.programId, mandateAddress)],
351
+ requiredSigners: [owner],
352
+ feePayer: owner,
353
+ };
354
+ }
355
+ buildRegisterAsset(mint, tokenProgram, authority) {
356
+ return {
357
+ instructions: [buildRegisterAssetInstruction({ mint, tokenProgram }, authority, this.programId)],
358
+ requiredSigners: [authority],
359
+ feePayer: authority,
360
+ };
361
+ }
362
+ buildSetAssetStatus(mint, authority, enabled) {
363
+ return {
364
+ instructions: [buildSetAssetStatusInstruction(mint, authority, enabled, this.programId)],
365
+ requiredSigners: [authority],
366
+ feePayer: authority,
367
+ };
368
+ }
369
+ buildPauseMandate(owner, mandateAddress) {
370
+ return {
371
+ instructions: [buildPauseMandateInstruction(owner, this.programId, mandateAddress)],
372
+ requiredSigners: [owner],
373
+ feePayer: owner,
374
+ };
375
+ }
376
+ buildRevokeMandate(owner, mandateAddress) {
377
+ return {
378
+ instructions: [buildRevokeMandateInstruction(owner, this.programId, mandateAddress)],
379
+ requiredSigners: [owner],
380
+ feePayer: owner,
381
+ };
382
+ }
383
+ buildRevokeDelegate(sourceTokenAccount, owner, tokenProgram) {
384
+ return {
385
+ instructions: [buildRevokeDelegateInstruction(sourceTokenAccount, owner, tokenProgram)],
386
+ requiredSigners: [owner],
387
+ feePayer: owner,
388
+ };
389
+ }
390
+ buildApproveDelegate(input) {
391
+ return {
392
+ instructions: [buildApproveDelegateInstruction(input)],
393
+ requiredSigners: [input.owner],
394
+ feePayer: input.owner,
395
+ };
396
+ }
397
+ async preparePayment(input, agent) {
398
+ const mandate = await this.getMandate(input.mandate);
399
+ if (!mandate)
400
+ throw new Error(`Mandate not found: ${input.mandate}`);
401
+ const tokenProgram = input.tokenProgram ?? mandate.tokenProgram ?? await this.getTokenProgram(mandate.sourceTokenAccount);
402
+ const untrustedRemainingAccounts = input.remainingAccounts;
403
+ if (untrustedRemainingAccounts?.length) {
404
+ throw new Error("Caller-supplied remainingAccounts are not accepted; extension accounts must be resolved from verified on-chain state");
405
+ }
406
+ const request = preparePaymentRequest({ ...input, tokenProgram });
407
+ const receiptAddress = deriveReceiptAddress(mandate.address, request.invoiceHash, this.programId);
408
+ // The receipt PDA is derived from mandate and invoice hash, so an existing
409
+ // account means this invoice is already paid. Stop before building anything.
410
+ const existingReceipt = await this.getPayment(receiptAddress);
411
+ if (existingReceipt !== null)
412
+ throw new DuplicateInvoiceError(receiptAddress);
413
+ const asset = await this.getSupportedAsset(input.mint);
414
+ const expectedProgram = tokenProgram === "token-2022" ? TOKEN_2022_PROGRAM_ID : SPL_TOKEN_PROGRAM_ID;
415
+ if (!asset || !asset.enabled) {
416
+ throw new Error("Payment mint is not enabled in the ChainPay SupportedAsset registry");
417
+ }
418
+ if (asset.mint !== input.mint || asset.tokenProgram !== expectedProgram) {
419
+ throw new Error("SupportedAsset mint or token program does not match the payment request");
420
+ }
421
+ const capabilityProfile = await inspectTokenCapabilities(this.connection, {
422
+ mint: input.mint,
423
+ sourceTokenAccount: mandate.sourceTokenAccount,
424
+ recipientTokenAccount: input.recipient,
425
+ tokenProgram,
426
+ commitment: this.commitment,
427
+ });
428
+ if (!capabilityProfile.compatible) {
429
+ throw new Error(`Token capability check failed: ${capabilityProfile.blockers.join("; ")}`);
430
+ }
431
+ const currentSlot = await this.getCurrentSlot();
432
+ const executionAgent = agent ?? mandate.approvedAgent;
433
+ const sourceAccountInfo = await this.connection.getAccountInfo(publicKey(mandate.sourceTokenAccount), this.commitment);
434
+ const sourceContext = sourceAccountInfo
435
+ ? paymentPreflightContextFromTokenAccount(new Uint8Array(sourceAccountInfo.data))
436
+ : undefined;
437
+ const preflight = preflightPayment(request, mandate, currentSlot, executionAgent, false, sourceContext ?? undefined);
438
+ const instruction = buildExecutePaymentInstruction(request, executionAgent, mandate, this.programId);
439
+ return {
440
+ request,
441
+ mandate,
442
+ receiptAddress,
443
+ instruction,
444
+ transaction: preparedPaymentTransaction(instruction, executionAgent),
445
+ preflight,
446
+ capabilityProfile,
447
+ };
448
+ }
449
+ async executePayment(prepared, adapter) {
450
+ try {
451
+ const submission = await adapter.submit(prepared.transaction);
452
+ let status = submission.status ?? "submitted";
453
+ let slot = submission.slot;
454
+ if (adapter.confirm) {
455
+ const confirmation = await adapter.confirm(submission.signature);
456
+ status = "confirmed";
457
+ slot = confirmation.slot ?? slot;
458
+ }
459
+ return {
460
+ status,
461
+ receiptAddress: prepared.receiptAddress,
462
+ signature: submission.signature,
463
+ slot,
464
+ };
465
+ }
466
+ catch (error) {
467
+ const message = error instanceof Error ? error.message : String(error);
468
+ if (isDuplicateInvoiceError(error) || isReceiptAlreadyInUse(message, prepared.receiptAddress)) {
469
+ return {
470
+ status: "failed",
471
+ receiptAddress: prepared.receiptAddress,
472
+ code: "DuplicateInvoice",
473
+ error: DUPLICATE_INVOICE_MESSAGE,
474
+ };
475
+ }
476
+ return {
477
+ status: "failed",
478
+ receiptAddress: prepared.receiptAddress,
479
+ error: message,
480
+ };
481
+ }
482
+ }
483
+ async getProgramAccount(accountAddress) {
484
+ const normalized = address(accountAddress);
485
+ const info = await this.connection.getAccountInfo(publicKey(normalized), this.commitment);
486
+ if (!info)
487
+ return null;
488
+ if (info.owner.toBase58() !== this.programId) {
489
+ throw new Error(`Account ${normalized} is not owned by ChainPay program`);
490
+ }
491
+ return { address: normalized, data: new Uint8Array(info.data) };
492
+ }
493
+ }
@@ -0,0 +1,44 @@
1
+ import type { Address } from "./types.js";
2
+ export declare const DEFAULT_PROGRAM_ID: Address;
3
+ export declare const DEVNET_RPC_URL = "https://api.devnet.solana.com";
4
+ export declare const SYSTEM_PROGRAM_ID: Address;
5
+ export declare const SPL_TOKEN_PROGRAM_ID: Address;
6
+ export declare const TOKEN_2022_PROGRAM_ID: Address;
7
+ export declare const ASSOCIATED_TOKEN_PROGRAM_ID: Address;
8
+ /** SPL Memo v2. Accepts a memo with no signer accounts. */
9
+ export declare const MEMO_PROGRAM_ID: Address;
10
+ export declare const DISCRIMINATORS: {
11
+ readonly initializeConfig: Uint8Array<ArrayBuffer>;
12
+ readonly createMandate: Uint8Array<ArrayBuffer>;
13
+ readonly executePayment: Uint8Array<ArrayBuffer>;
14
+ readonly pauseMandate: Uint8Array<ArrayBuffer>;
15
+ readonly revokeMandate: Uint8Array<ArrayBuffer>;
16
+ readonly updateMandate: Uint8Array<ArrayBuffer>;
17
+ readonly registerAsset: Uint8Array<ArrayBuffer>;
18
+ readonly setAssetStatus: Uint8Array<ArrayBuffer>;
19
+ readonly approveChecked: Uint8Array<ArrayBuffer>;
20
+ readonly revokeDelegate: Uint8Array<ArrayBuffer>;
21
+ };
22
+ export declare const ACCOUNT_DISCRIMINATORS: {
23
+ readonly protocolConfig: Uint8Array<ArrayBuffer>;
24
+ readonly supportedAsset: Uint8Array<ArrayBuffer>;
25
+ readonly paymentMandate: Uint8Array<ArrayBuffer>;
26
+ readonly paymentReceipt: Uint8Array<ArrayBuffer>;
27
+ };
28
+ export declare const MANDATE_SEED = "mandate";
29
+ export declare const MANDATE_NONCE_PREFIX: Uint8Array<ArrayBuffer>;
30
+ export declare const CONFIG_SEED = "config";
31
+ export declare const RECEIPT_SEED = "receipt";
32
+ export declare const ASSET_SEED = "asset";
33
+ export declare const RECEIPT_STATUS_SETTLED = 1;
34
+ /** Original PaymentReceipt account size. Still the minimum any reader accepts. */
35
+ export declare const RECEIPT_ACCOUNT_LENGTH = 282;
36
+ /**
37
+ * PaymentReceipt with the policy snapshot appended after `bump`. Offsets of
38
+ * every original field are unchanged, so a v2 receipt is a v1 receipt plus a
39
+ * tail. Both sizes exist on chain at once after the program upgrade.
40
+ */
41
+ export declare const RECEIPT_ACCOUNT_LENGTH_V2 = 371;
42
+ /** Every receipt account size a discovery query must ask for. */
43
+ export declare const RECEIPT_ACCOUNT_LENGTHS: readonly [282, 371];
44
+ export declare const MANDATE_ACCOUNT_LENGTH = 235;
@@ -0,0 +1,43 @@
1
+ export const DEFAULT_PROGRAM_ID = "3H9TV1EPR2BAQgVmcMqpufiZKPXbAMnjHp13LA9Lndv4";
2
+ export const DEVNET_RPC_URL = "https://api.devnet.solana.com";
3
+ export const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
4
+ export const SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
5
+ export const TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
6
+ export const ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
7
+ /** SPL Memo v2. Accepts a memo with no signer accounts. */
8
+ export const MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
9
+ export const DISCRIMINATORS = {
10
+ initializeConfig: Uint8Array.from([208, 127, 21, 1, 194, 190, 196, 70]),
11
+ createMandate: Uint8Array.from([230, 170, 158, 68, 33, 169, 16, 158]),
12
+ executePayment: Uint8Array.from([86, 4, 7, 7, 120, 139, 232, 139]),
13
+ pauseMandate: Uint8Array.from([192, 108, 97, 124, 56, 229, 236, 3]),
14
+ revokeMandate: Uint8Array.from([252, 97, 140, 119, 67, 43, 177, 108]),
15
+ updateMandate: Uint8Array.from([69, 131, 248, 29, 105, 50, 139, 30]),
16
+ registerAsset: Uint8Array.from([21, 80, 155, 149, 117, 207, 235, 16]),
17
+ setAssetStatus: Uint8Array.from([58, 54, 181, 102, 68, 238, 240, 245]),
18
+ approveChecked: Uint8Array.from([13]),
19
+ revokeDelegate: Uint8Array.from([5]),
20
+ };
21
+ export const ACCOUNT_DISCRIMINATORS = {
22
+ protocolConfig: Uint8Array.from([207, 91, 250, 28, 152, 179, 215, 209]),
23
+ supportedAsset: Uint8Array.from([129, 27, 96, 192, 89, 180, 227, 200]),
24
+ paymentMandate: Uint8Array.from([139, 106, 43, 122, 82, 211, 96, 162]),
25
+ paymentReceipt: Uint8Array.from([168, 198, 209, 4, 60, 235, 126, 109]),
26
+ };
27
+ export const MANDATE_SEED = "mandate";
28
+ export const MANDATE_NONCE_PREFIX = Uint8Array.from([67, 80, 78, 79, 78, 67, 69, 33]);
29
+ export const CONFIG_SEED = "config";
30
+ export const RECEIPT_SEED = "receipt";
31
+ export const ASSET_SEED = "asset";
32
+ export const RECEIPT_STATUS_SETTLED = 1;
33
+ /** Original PaymentReceipt account size. Still the minimum any reader accepts. */
34
+ export const RECEIPT_ACCOUNT_LENGTH = 282;
35
+ /**
36
+ * PaymentReceipt with the policy snapshot appended after `bump`. Offsets of
37
+ * every original field are unchanged, so a v2 receipt is a v1 receipt plus a
38
+ * tail. Both sizes exist on chain at once after the program upgrade.
39
+ */
40
+ export const RECEIPT_ACCOUNT_LENGTH_V2 = 371;
41
+ /** Every receipt account size a discovery query must ask for. */
42
+ export const RECEIPT_ACCOUNT_LENGTHS = [RECEIPT_ACCOUNT_LENGTH, RECEIPT_ACCOUNT_LENGTH_V2];
43
+ export const MANDATE_ACCOUNT_LENGTH = 235;
@@ -0,0 +1,43 @@
1
+ import type { PreparePaymentInput } from "./payment.js";
2
+ import type { Address, ChainPayInstruction, PreparedTransaction, TokenProgram } from "./types.js";
3
+ import { CROSSMINT_CONNECTOR, type CrossmintPaymentTerms } from "./crossmint-order.js";
4
+ export type CrossmintPrepareFields = {
5
+ connector: typeof CROSSMINT_CONNECTOR;
6
+ connectorLabel: string;
7
+ orderId: string;
8
+ mint: Address;
9
+ recipient: Address;
10
+ amount: string;
11
+ tokenProgram: TokenProgram;
12
+ invoiceHash: string;
13
+ paymentId: string;
14
+ signatureReference: string;
15
+ };
16
+ /** Terms plus the references the receipt PDA is derived from. */
17
+ export declare function crossmintTermsToPrepareFields(terms: CrossmintPaymentTerms): Promise<CrossmintPrepareFields>;
18
+ export declare function crossmintFieldsToPreparePaymentInput(mandate: Address, fields: CrossmintPrepareFields): PreparePaymentInput;
19
+ /** One call from a raw Crossmint order response to a ChainPay payment input. */
20
+ export declare function crossmintOrderToPreparePaymentInput(mandate: Address, order: unknown, expected?: {
21
+ mint?: Address;
22
+ tokenProgram?: TokenProgram;
23
+ }): Promise<{
24
+ terms: CrossmintPaymentTerms;
25
+ fields: CrossmintPrepareFields;
26
+ input: PreparePaymentInput;
27
+ }>;
28
+ /**
29
+ * Crossmint's order memo as a top-level SPL Memo instruction with no accounts.
30
+ *
31
+ * Crossmint's own preparation has the payer sign the memo. In ChainPay the
32
+ * approved agent signs and pays fees, never the owner, so the memo names no
33
+ * signer; the memo program accepts an unsigned memo. The bytes are Crossmint's
34
+ * memo exactly, because Crossmint matches a payment to its order by this text.
35
+ */
36
+ export declare function crossmintMemoInstruction(memo: string): ChainPayInstruction;
37
+ /**
38
+ * The transaction a Crossmint checkout signs: the unchanged mandate payment,
39
+ * then Crossmint's memo. Nothing else is added and nothing is dropped; a
40
+ * prepared payment that is not exactly one `execute_payment` is refused.
41
+ * The receipt PDA and invoice hash are untouched: they come from the order id.
42
+ */
43
+ export declare function crossmintPaymentTransaction(prepared: PreparedTransaction, terms: Pick<CrossmintPaymentTerms, "memo">): PreparedTransaction;