@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
@@ -0,0 +1,149 @@
1
+ import { bytesToHex, formatExactTokenAmount } from "./receipt.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 function receiptPolicy(receipt, relayObserved) {
7
+ const snapshot = receipt.policySnapshot;
8
+ if (snapshot) {
9
+ const { version: _version, ...limits } = snapshot;
10
+ return { source: "on-chain", limits };
11
+ }
12
+ if (relayObserved?.source === "relay-observed")
13
+ return relayObserved;
14
+ return { source: "not-recorded" };
15
+ }
16
+ function u64Field(value) {
17
+ if (typeof value !== "string" || !/^\d+$/.test(value))
18
+ return null;
19
+ const parsed = BigInt(value);
20
+ return parsed <= 18446744073709551615n ? parsed : null;
21
+ }
22
+ /**
23
+ * Read the `policy` object the ChainPay relay returns from
24
+ * `GET /v1/receipts/{receipt}`. Only a well-formed relay observation is
25
+ * accepted; anything else is null, never a guessed limit. An "on-chain" value
26
+ * from the relay is ignored because the receipt account is read directly.
27
+ */
28
+ export function relayObservedPolicy(value) {
29
+ if (!value || typeof value !== "object")
30
+ return null;
31
+ const policy = value;
32
+ if (policy.source !== "relay-observed")
33
+ return null;
34
+ const fields = {
35
+ maxPerPayment: u64Field(policy.max_per_payment),
36
+ totalLimit: u64Field(policy.total_limit),
37
+ amountSpentAfter: u64Field(policy.amount_spent_after),
38
+ paymentCountAfter: u64Field(policy.payment_count_after),
39
+ maxPaymentCount: u64Field(policy.max_payment_count),
40
+ expiresAtSlot: u64Field(policy.expires_at_slot),
41
+ cooldownSlots: u64Field(policy.cooldown_slots),
42
+ };
43
+ const observedAtSlot = u64Field(policy.observed_at_slot);
44
+ if (observedAtSlot === null || Object.values(fields).some((field) => field === null))
45
+ return null;
46
+ return {
47
+ source: "relay-observed",
48
+ limits: fields,
49
+ observedAtSlot,
50
+ includesLaterPayments: policy.includes_later_payments === true,
51
+ };
52
+ }
53
+ export function receiptExplorerUrl(receipt, cluster = "devnet") {
54
+ const suffix = cluster === "devnet" ? "?cluster=devnet" : "";
55
+ return receipt.transactionSignature
56
+ ? `https://explorer.solana.com/tx/${receipt.transactionSignature}${suffix}`
57
+ : `https://explorer.solana.com/address/${receipt.address}${suffix}`;
58
+ }
59
+ /**
60
+ * Accounting tools read the first five columns; the rest let a reader trace
61
+ * every row back to Solana. New columns are appended so existing imports keep
62
+ * their column positions.
63
+ */
64
+ export const RECEIPT_CSV_HEADERS = [
65
+ "Date",
66
+ "Description",
67
+ "Amount",
68
+ "Payee",
69
+ "Reference",
70
+ "Token",
71
+ "Agent",
72
+ "Spending permission",
73
+ "Per-payment limit",
74
+ "Total limit",
75
+ "Spent after",
76
+ "Limits source",
77
+ "Receipt",
78
+ "Verify URL",
79
+ "Explorer URL",
80
+ "PO number",
81
+ "Order match",
82
+ ];
83
+ /** Exact decimal, trailing zeros trimmed. Unknown decimals stay labeled base units. */
84
+ function exactAmount(value, decimals) {
85
+ const exact = formatExactTokenAmount(value, decimals);
86
+ if (exact.displayKind === "base-units")
87
+ return `${exact.baseUnits} base units`;
88
+ return exact.display.includes(".") ? exact.display.replace(/0+$/, "").replace(/\.$/, "") : exact.display;
89
+ }
90
+ function isoDate(blockTime) {
91
+ if (blockTime === null || blockTime === undefined || !Number.isFinite(blockTime))
92
+ return "";
93
+ return new Date(blockTime * 1000).toISOString().slice(0, 10);
94
+ }
95
+ /**
96
+ * One CSV cell. A value a spreadsheet would run as a formula (leading =, +,
97
+ * -, @, tab, or carriage return) is prefixed with an apostrophe so it stays
98
+ * text. Then RFC 4180 quoting: wrap in quotes when the value holds a comma,
99
+ * quote, or line break, doubling inner quotes.
100
+ */
101
+ export function csvCell(value) {
102
+ const safe = /^[=+\-@\t\r]/.test(value) ? `'${value}` : value;
103
+ return /[",\r\n]/.test(safe) ? `"${safe.replace(/"/g, '""')}"` : safe;
104
+ }
105
+ function description(purpose) {
106
+ if (purpose?.description)
107
+ return purpose.description;
108
+ if (purpose?.lineItems?.length)
109
+ return purpose.lineItems.map((item) => item.label).join("; ");
110
+ return "";
111
+ }
112
+ function rowCells(row) {
113
+ const { receipt, decimals } = row;
114
+ const policy = row.policy ?? receiptPolicy(receipt);
115
+ const limits = policy.source === "not-recorded" ? null : policy.limits;
116
+ // A relay read taken after a later payment no longer says what was spent
117
+ // after this one. Leave the cell empty rather than show a larger number.
118
+ const spentAfterKnown = limits && !(policy.source === "relay-observed" && policy.includesLaterPayments);
119
+ const payee = receipt.recipientTokenAccount;
120
+ return [
121
+ isoDate(row.blockTime),
122
+ description(row.purpose),
123
+ exactAmount(receipt.amount, decimals),
124
+ payee,
125
+ row.purpose?.invoice ?? bytesToHex(receipt.invoiceHash),
126
+ row.symbol ?? receipt.mint,
127
+ receipt.agent,
128
+ receipt.mandate,
129
+ limits ? exactAmount(limits.maxPerPayment, decimals) : "",
130
+ limits ? exactAmount(limits.totalLimit, decimals) : "",
131
+ spentAfterKnown ? exactAmount(limits.amountSpentAfter, decimals) : "",
132
+ policy.source,
133
+ receipt.address,
134
+ row.verifyUrl ?? "",
135
+ row.explorerUrl ?? receiptExplorerUrl(receipt),
136
+ row.poNumber ?? "",
137
+ row.orderMatch ?? "",
138
+ ];
139
+ }
140
+ /**
141
+ * One CSV of receipts with QuickBooks-friendly leading columns (Date,
142
+ * Description, Amount, Payee, Reference), then the ChainPay columns. Amounts
143
+ * are exact decimals from base units, never floating point. CRLF line
144
+ * endings, header row always present.
145
+ */
146
+ export function receiptsToCsv(rows) {
147
+ const lines = [RECEIPT_CSV_HEADERS.map(csvCell), ...rows.map((row) => rowCells(row).map(csvCell))];
148
+ return lines.map((cells) => cells.join(",")).join("\r\n") + "\r\n";
149
+ }
@@ -0,0 +1,108 @@
1
+ import type { Address, Mandate, PaymentReceipt, PaymentStatus } from "./types.js";
2
+ import { decodePaymentReceipt } from "./accounts.js";
3
+ export type ReceiptAccountView = {
4
+ address: Address;
5
+ owner: Address;
6
+ data: Uint8Array | Buffer;
7
+ };
8
+ export type ReceiptValidationCode = "wrong_owner" | "wrong_discriminator" | "truncated" | "pda_mismatch" | "unsettled" | "not_found";
9
+ export type ReceiptReadFailure = {
10
+ valid: false;
11
+ code: ReceiptValidationCode;
12
+ reason: string;
13
+ };
14
+ export type ReceiptReadSuccess = {
15
+ valid: true;
16
+ receipt: PaymentReceipt;
17
+ programId: Address;
18
+ derivedAddress: Address;
19
+ settled: boolean;
20
+ };
21
+ export type ReceiptReadResult = ReceiptReadSuccess | ReceiptReadFailure;
22
+ export type ReadReceiptOptions = {
23
+ programId?: Address;
24
+ requireSettled?: boolean;
25
+ transactionSignature?: string;
26
+ };
27
+ export type TokenAmountDisplay = {
28
+ /** Exact unsigned integer string of base units. Never a JS Number. */
29
+ baseUnits: string;
30
+ decimals: number | null;
31
+ /**
32
+ * Decimal-inserted UI amount when a supported mint owner and decimals
33
+ * are known. Otherwise the exact base-unit string. Never rounded.
34
+ */
35
+ display: string;
36
+ displayKind: "ui-amount" | "base-units";
37
+ };
38
+ export type MintDecimalsRead = {
39
+ ok: true;
40
+ decimals: number;
41
+ } | {
42
+ ok: false;
43
+ reason: "unsupported_owner" | "truncated" | "not_a_mint" | "missing";
44
+ };
45
+ export type CurrentMandateRead = {
46
+ status: "present";
47
+ mandate: Mandate;
48
+ } | {
49
+ status: "absent";
50
+ } | {
51
+ status: "unavailable";
52
+ reason: string;
53
+ };
54
+ export type PublicReceiptProof = {
55
+ receipt: ReceiptReadResult;
56
+ amount: TokenAmountDisplay | null;
57
+ currentMandate: CurrentMandateRead;
58
+ };
59
+ export declare function createReceiptReference(address: Address, mandate: Address, invoiceHash: Uint8Array, status: PaymentStatus): Pick<PaymentReceipt, "address" | "mandate" | "invoiceHash" | "status">;
60
+ export declare function receiptAddress(mandate: Address, invoiceHash: Uint8Array, programId?: Address): Address;
61
+ export { decodePaymentReceipt };
62
+ export declare function bytesToHex(value: Uint8Array): string;
63
+ export declare function hexToBytes(value: string, name: string): Uint8Array;
64
+ export declare function readVerifiedMintDecimals(account: {
65
+ owner: Address;
66
+ data: Uint8Array | Buffer;
67
+ } | null | undefined): MintDecimalsRead;
68
+ /**
69
+ * Format a u64 token amount without Number conversion or rounding.
70
+ * Unknown decimals stay exact base units so a public card never invents
71
+ * a UI amount.
72
+ */
73
+ export declare function formatExactTokenAmount(amount: bigint, decimals?: number | null): TokenAmountDisplay;
74
+ export declare function amountDisplayFromMint(amount: bigint, mintAccount?: {
75
+ owner: Address;
76
+ data: Uint8Array | Buffer;
77
+ } | null): TokenAmountDisplay;
78
+ /**
79
+ * Optional current-mandate enrichment. Independent of creation history
80
+ * and source token metadata. Missing or undecodable accounts never
81
+ * become fabricated limits.
82
+ */
83
+ export declare function readCurrentMandateFields(account: ReceiptAccountView | null | undefined, options?: {
84
+ programId?: Address;
85
+ currentSlot?: bigint;
86
+ expectedAddress?: Address;
87
+ }): CurrentMandateRead;
88
+ /**
89
+ * Validating receipt reader for public `/verify` and shared ledger cards.
90
+ * Confirms program owner, discriminator, length, PDA seeds
91
+ * `["receipt", mandate, invoice_hash]`, and optional settled status.
92
+ * `signatureReference` is never treated as seller delivery.
93
+ */
94
+ export declare function readPaymentReceiptAccount(account: ReceiptAccountView, options?: ReadReceiptOptions): ReceiptReadResult;
95
+ /** Public verify path: same checks plus settled status. */
96
+ export declare function readPublicSettledReceipt(account: ReceiptAccountView, options?: Omit<ReadReceiptOptions, "requireSettled">): ReceiptReadResult;
97
+ export declare function requirePaymentReceiptAccount(account: ReceiptAccountView, options?: ReadReceiptOptions): PaymentReceipt;
98
+ export declare function assemblePublicReceiptProof(input: {
99
+ receiptAccount: ReceiptAccountView;
100
+ programId?: Address;
101
+ mintAccount?: {
102
+ owner: Address;
103
+ data: Uint8Array | Buffer;
104
+ } | null;
105
+ mandateAccount?: ReceiptAccountView | null;
106
+ currentSlot?: bigint;
107
+ transactionSignature?: string;
108
+ }): PublicReceiptProof;
@@ -0,0 +1,213 @@
1
+ import { decodeCurrentMandateFields, decodePaymentReceipt } from "./accounts.js";
2
+ import { ACCOUNT_DISCRIMINATORS, DEFAULT_PROGRAM_ID, RECEIPT_ACCOUNT_LENGTH, RECEIPT_STATUS_SETTLED, SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, } from "./constants.js";
3
+ import { address } from "./encoding.js";
4
+ import { deriveReceiptAddress } from "./pda.js";
5
+ const MAX_U64 = 18446744073709551615n;
6
+ const ACCOUNT_DISCRIMINATOR_LENGTH = 8;
7
+ function accountBytes(data) {
8
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
9
+ }
10
+ function discriminatorsEqual(data, expected) {
11
+ for (let index = 0; index < expected.length; index += 1) {
12
+ if (data[index] !== expected[index])
13
+ return false;
14
+ }
15
+ return true;
16
+ }
17
+ function failure(code, reason) {
18
+ return { valid: false, code, reason };
19
+ }
20
+ export function createReceiptReference(address, mandate, invoiceHash, status) {
21
+ return { address, mandate, invoiceHash, status };
22
+ }
23
+ export function receiptAddress(mandate, invoiceHash, programId = DEFAULT_PROGRAM_ID) {
24
+ return deriveReceiptAddress(mandate, invoiceHash, programId);
25
+ }
26
+ export { decodePaymentReceipt };
27
+ export function bytesToHex(value) {
28
+ return Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("");
29
+ }
30
+ export function hexToBytes(value, name) {
31
+ const normalized = value.startsWith("0x") ? value.slice(2) : value;
32
+ if (!/^[0-9a-fA-F]+$/.test(normalized) || normalized.length !== 64) {
33
+ throw new Error(`${name} must be exactly 32 bytes encoded as hexadecimal`);
34
+ }
35
+ const bytes = new Uint8Array(32);
36
+ for (let index = 0; index < 32; index += 1) {
37
+ bytes[index] = Number.parseInt(normalized.slice(index * 2, index * 2 + 2), 16);
38
+ }
39
+ return bytes;
40
+ }
41
+ /**
42
+ * Read mint decimals only after confirming the account owner is a supported
43
+ * token program. Callers must not inspect the decimals byte first.
44
+ */
45
+ /** SPL Mint account length. Token-2022 mints pad to 165 and then tag themselves. */
46
+ const MINT_LEN = 82;
47
+ /** Offset of the Token-2022 `account_type` discriminator. */
48
+ const ACCOUNT_TYPE_OFFSET = 165;
49
+ /** `account_type` value meaning Mint. A token account is 2. */
50
+ const ACCOUNT_TYPE_MINT = 1;
51
+ export function readVerifiedMintDecimals(account) {
52
+ if (!account)
53
+ return { ok: false, reason: "missing" };
54
+ const owner = address(account.owner);
55
+ if (owner !== SPL_TOKEN_PROGRAM_ID && owner !== TOKEN_2022_PROGRAM_ID) {
56
+ return { ok: false, reason: "unsupported_owner" };
57
+ }
58
+ const data = accountBytes(account.data);
59
+ if (data.length <= 44)
60
+ return { ok: false, reason: "truncated" };
61
+ // `data[44]` is the decimals byte of an SPL Mint, but a 165-byte token account
62
+ // is owned by the same programs and would put part of its `owner` pubkey at
63
+ // that offset, yielding an arbitrary 0-255 as decimals and a confidently wrong
64
+ // amount. Accept only a base mint (exactly 82 bytes) or a Token-2022 mint,
65
+ // which pads to 165 and tags `account_type = 1` at offset 165.
66
+ const isBaseMint = data.length === MINT_LEN;
67
+ const isExtendedMint = data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_MINT;
68
+ if (!isBaseMint && !isExtendedMint)
69
+ return { ok: false, reason: "not_a_mint" };
70
+ return { ok: true, decimals: data[44] };
71
+ }
72
+ /**
73
+ * Format a u64 token amount without Number conversion or rounding.
74
+ * Unknown decimals stay exact base units so a public card never invents
75
+ * a UI amount.
76
+ */
77
+ export function formatExactTokenAmount(amount, decimals) {
78
+ if (amount < 0n || amount > MAX_U64) {
79
+ throw new Error("amount must fit in an unsigned 64-bit integer");
80
+ }
81
+ const baseUnits = amount.toString();
82
+ if (decimals === null
83
+ || decimals === undefined
84
+ || !Number.isInteger(decimals)
85
+ || decimals < 0
86
+ || decimals > 255) {
87
+ return { baseUnits, decimals: null, display: baseUnits, displayKind: "base-units" };
88
+ }
89
+ if (decimals === 0) {
90
+ return { baseUnits, decimals, display: baseUnits, displayKind: "ui-amount" };
91
+ }
92
+ const padded = baseUnits.padStart(decimals + 1, "0");
93
+ const whole = padded.slice(0, padded.length - decimals);
94
+ const fraction = padded.slice(padded.length - decimals);
95
+ return { baseUnits, decimals, display: `${whole}.${fraction}`, displayKind: "ui-amount" };
96
+ }
97
+ export function amountDisplayFromMint(amount, mintAccount) {
98
+ const mint = readVerifiedMintDecimals(mintAccount);
99
+ return formatExactTokenAmount(amount, mint.ok ? mint.decimals : null);
100
+ }
101
+ /**
102
+ * Optional current-mandate enrichment. Independent of creation history
103
+ * and source token metadata. Missing or undecodable accounts never
104
+ * become fabricated limits.
105
+ */
106
+ export function readCurrentMandateFields(account, options = {}) {
107
+ if (!account)
108
+ return { status: "absent" };
109
+ const programId = address(options.programId ?? DEFAULT_PROGRAM_ID);
110
+ try {
111
+ if (address(account.owner) !== programId) {
112
+ return { status: "unavailable", reason: "Mandate account is not owned by the ChainPay program" };
113
+ }
114
+ if (options.expectedAddress !== undefined && address(account.address) !== address(options.expectedAddress)) {
115
+ return { status: "unavailable", reason: "Mandate account address does not match the receipt mandate" };
116
+ }
117
+ return {
118
+ status: "present",
119
+ mandate: decodeCurrentMandateFields(account.data, account.address, options.currentSlot),
120
+ };
121
+ }
122
+ catch (error) {
123
+ return {
124
+ status: "unavailable",
125
+ reason: error instanceof Error ? error.message : String(error),
126
+ };
127
+ }
128
+ }
129
+ /**
130
+ * Validating receipt reader for public `/verify` and shared ledger cards.
131
+ * Confirms program owner, discriminator, length, PDA seeds
132
+ * `["receipt", mandate, invoice_hash]`, and optional settled status.
133
+ * `signatureReference` is never treated as seller delivery.
134
+ */
135
+ export function readPaymentReceiptAccount(account, options = {}) {
136
+ const programId = address(options.programId ?? DEFAULT_PROGRAM_ID);
137
+ let requestedAddress;
138
+ try {
139
+ requestedAddress = address(account.address);
140
+ }
141
+ catch {
142
+ return failure("not_found", "Receipt address is not a valid Solana public key");
143
+ }
144
+ try {
145
+ if (address(account.owner) !== programId) {
146
+ return failure("wrong_owner", "Receipt account is not owned by the ChainPay program");
147
+ }
148
+ }
149
+ catch {
150
+ return failure("wrong_owner", "Receipt account is not owned by the ChainPay program");
151
+ }
152
+ const data = accountBytes(account.data);
153
+ if (data.length < ACCOUNT_DISCRIMINATOR_LENGTH) {
154
+ return failure("truncated", "PaymentReceipt account data is truncated");
155
+ }
156
+ if (!discriminatorsEqual(data, ACCOUNT_DISCRIMINATORS.paymentReceipt)) {
157
+ return failure("wrong_discriminator", "Invalid PaymentReceipt account discriminator");
158
+ }
159
+ if (data.length < RECEIPT_ACCOUNT_LENGTH) {
160
+ return failure("truncated", "PaymentReceipt account data is truncated");
161
+ }
162
+ const receipt = decodePaymentReceipt(data, requestedAddress, options.transactionSignature);
163
+ const derivedAddress = deriveReceiptAddress(receipt.mandate, receipt.invoiceHash, programId);
164
+ if (derivedAddress !== requestedAddress) {
165
+ return failure("pda_mismatch", "Receipt address does not match PDA seeds [\"receipt\", mandate, invoice_hash]");
166
+ }
167
+ const settled = receipt.onChainStatus === RECEIPT_STATUS_SETTLED;
168
+ if ((options.requireSettled ?? false) && !settled) {
169
+ return failure("unsettled", "Receipt is not settled");
170
+ }
171
+ return {
172
+ valid: true,
173
+ receipt,
174
+ programId,
175
+ derivedAddress,
176
+ settled,
177
+ };
178
+ }
179
+ /** Public verify path: same checks plus settled status. */
180
+ export function readPublicSettledReceipt(account, options = {}) {
181
+ return readPaymentReceiptAccount(account, { ...options, requireSettled: true });
182
+ }
183
+ export function requirePaymentReceiptAccount(account, options = {}) {
184
+ const result = readPaymentReceiptAccount(account, options);
185
+ if (!result.valid)
186
+ throw new Error(result.reason);
187
+ return result.receipt;
188
+ }
189
+ export function assemblePublicReceiptProof(input) {
190
+ const receipt = readPublicSettledReceipt(input.receiptAccount, {
191
+ programId: input.programId,
192
+ transactionSignature: input.transactionSignature,
193
+ });
194
+ if (!receipt.valid) {
195
+ return {
196
+ receipt,
197
+ amount: null,
198
+ currentMandate: readCurrentMandateFields(input.mandateAccount, {
199
+ programId: input.programId,
200
+ currentSlot: input.currentSlot,
201
+ }),
202
+ };
203
+ }
204
+ return {
205
+ receipt,
206
+ amount: amountDisplayFromMint(receipt.receipt.amount, input.mintAccount),
207
+ currentMandate: readCurrentMandateFields(input.mandateAccount, {
208
+ programId: input.programId,
209
+ currentSlot: input.currentSlot,
210
+ expectedAddress: receipt.receipt.mandate,
211
+ }),
212
+ };
213
+ }
@@ -0,0 +1,5 @@
1
+ import { Connection, Transaction, TransactionInstruction } from "@solana/web3.js";
2
+ import type { ChainPayInstruction, PreparedTransaction } from "./types.js";
3
+ export declare function toWeb3Instruction(instruction: ChainPayInstruction): TransactionInstruction;
4
+ export declare function toWeb3Transaction(prepared: PreparedTransaction, recentBlockhash: string): Transaction;
5
+ export { Connection };
package/dist/solana.js ADDED
@@ -0,0 +1,20 @@
1
+ import { Connection, Transaction, TransactionInstruction, } from "@solana/web3.js";
2
+ import { publicKey } from "./encoding.js";
3
+ export function toWeb3Instruction(instruction) {
4
+ return new TransactionInstruction({
5
+ programId: publicKey(instruction.programId),
6
+ keys: instruction.keys.map((key) => ({
7
+ pubkey: publicKey(key.address),
8
+ isSigner: key.isSigner,
9
+ isWritable: key.isWritable,
10
+ })),
11
+ data: Buffer.from(instruction.data),
12
+ });
13
+ }
14
+ export function toWeb3Transaction(prepared, recentBlockhash) {
15
+ const feePayer = prepared.feePayer ?? prepared.requiredSigners[0];
16
+ if (!feePayer)
17
+ throw new Error("A prepared transaction needs a fee payer");
18
+ return new Transaction({ feePayer: publicKey(feePayer), recentBlockhash }).add(...prepared.instructions.map(toWeb3Instruction));
19
+ }
20
+ export { Connection };
@@ -0,0 +1,9 @@
1
+ import { type Commitment, type Connection } from "@solana/web3.js";
2
+ import type { Address, TokenCapabilityProfile, TokenProgram } from "./types.js";
3
+ export declare function inspectTokenCapabilities(connection: Connection, input: {
4
+ mint: Address;
5
+ sourceTokenAccount: Address;
6
+ recipientTokenAccount: Address;
7
+ tokenProgram: TokenProgram;
8
+ commitment?: Commitment;
9
+ }): Promise<TokenCapabilityProfile>;
@@ -0,0 +1,139 @@
1
+ import { AccountState, ExtensionType, TOKEN_2022_PROGRAM_ID as SPL_TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID as SPL_TOKEN_PROGRAM_ID, getAccount, getCpiGuard, getDefaultAccountState, getEpochFee, getExtensionTypes, getMemoTransfer, getMint, getPausableConfig, getTransferFeeConfig, getTransferHook, } from "@solana/spl-token";
2
+ import { PublicKey } from "@solana/web3.js";
3
+ import { publicKey } from "./encoding.js";
4
+ const CONFIDENTIAL_TRANSFER_FEE_CONFIG = 16;
5
+ const CONFIDENTIAL_TRANSFER_FEE_AMOUNT = 17;
6
+ const KNOWN_MINT_EXTENSIONS = new Set([
7
+ ExtensionType.TransferFeeConfig,
8
+ ExtensionType.MintCloseAuthority,
9
+ ExtensionType.ConfidentialTransferMint,
10
+ ExtensionType.DefaultAccountState,
11
+ ExtensionType.NonTransferable,
12
+ ExtensionType.InterestBearingConfig,
13
+ ExtensionType.PermanentDelegate,
14
+ ExtensionType.TransferHook,
15
+ CONFIDENTIAL_TRANSFER_FEE_CONFIG,
16
+ ExtensionType.MetadataPointer,
17
+ ExtensionType.TokenMetadata,
18
+ ExtensionType.GroupPointer,
19
+ ExtensionType.TokenGroup,
20
+ ExtensionType.GroupMemberPointer,
21
+ ExtensionType.TokenGroupMember,
22
+ ExtensionType.ScaledUiAmountConfig,
23
+ ExtensionType.PausableConfig,
24
+ ExtensionType.PermissionedBurn,
25
+ ]);
26
+ const KNOWN_ACCOUNT_EXTENSIONS = new Set([
27
+ ExtensionType.TransferFeeAmount,
28
+ ExtensionType.ConfidentialTransferAccount,
29
+ ExtensionType.ImmutableOwner,
30
+ ExtensionType.MemoTransfer,
31
+ ExtensionType.CpiGuard,
32
+ ExtensionType.NonTransferableAccount,
33
+ ExtensionType.TransferHookAccount,
34
+ CONFIDENTIAL_TRANSFER_FEE_AMOUNT,
35
+ ExtensionType.PausableAccount,
36
+ ]);
37
+ function extensionName(extension) {
38
+ if (extension === CONFIDENTIAL_TRANSFER_FEE_CONFIG)
39
+ return "ConfidentialTransferFeeConfig";
40
+ if (extension === CONFIDENTIAL_TRANSFER_FEE_AMOUNT)
41
+ return "ConfidentialTransferFeeAmount";
42
+ return ExtensionType[extension] ?? `UnknownExtension(${extension})`;
43
+ }
44
+ function unexpectedExtensions(extensions, known, scope) {
45
+ return extensions
46
+ .filter((extension) => !known.has(extension))
47
+ .map((extension) => `${scope} has unsupported ${extensionName(extension)}`);
48
+ }
49
+ export async function inspectTokenCapabilities(connection, input) {
50
+ const programId = input.tokenProgram === "token-2022"
51
+ ? SPL_TOKEN_2022_PROGRAM_ID
52
+ : SPL_TOKEN_PROGRAM_ID;
53
+ const mintAddress = publicKey(input.mint);
54
+ const commitment = input.commitment ?? "confirmed";
55
+ const [mint, source, recipient] = await Promise.all([
56
+ getMint(connection, mintAddress, commitment, programId),
57
+ getAccount(connection, publicKey(input.sourceTokenAccount), commitment, programId),
58
+ getAccount(connection, publicKey(input.recipientTokenAccount), commitment, programId),
59
+ ]);
60
+ const blockers = [];
61
+ const warnings = [];
62
+ if (!mint.isInitialized)
63
+ blockers.push("mint is not initialized");
64
+ if (!source.isInitialized)
65
+ blockers.push("source token account is not initialized");
66
+ if (!recipient.isInitialized)
67
+ blockers.push("recipient token account is not initialized");
68
+ if (!source.mint.equals(mintAddress))
69
+ blockers.push("source token account mint does not match payment mint");
70
+ if (!recipient.mint.equals(mintAddress))
71
+ blockers.push("recipient token account mint does not match payment mint");
72
+ if (source.isFrozen)
73
+ blockers.push("source token account is frozen");
74
+ if (recipient.isFrozen)
75
+ blockers.push("recipient token account is frozen");
76
+ const mintExtensions = input.tokenProgram === "token-2022" ? getExtensionTypes(mint.tlvData) : [];
77
+ const sourceExtensions = input.tokenProgram === "token-2022" ? getExtensionTypes(source.tlvData) : [];
78
+ const recipientExtensions = input.tokenProgram === "token-2022" ? getExtensionTypes(recipient.tlvData) : [];
79
+ let transferFee;
80
+ let transferHookProgram;
81
+ if (input.tokenProgram === "token-2022") {
82
+ blockers.push(...unexpectedExtensions(mintExtensions, KNOWN_MINT_EXTENSIONS, "mint"));
83
+ blockers.push(...unexpectedExtensions(sourceExtensions, KNOWN_ACCOUNT_EXTENSIONS, "source account"));
84
+ blockers.push(...unexpectedExtensions(recipientExtensions, KNOWN_ACCOUNT_EXTENSIONS, "recipient account"));
85
+ if (mintExtensions.includes(ExtensionType.NonTransferable)) {
86
+ blockers.push("mint is non-transferable");
87
+ }
88
+ if (sourceExtensions.includes(ExtensionType.NonTransferableAccount) || recipientExtensions.includes(ExtensionType.NonTransferableAccount)) {
89
+ blockers.push("payment uses a non-transferable token account");
90
+ }
91
+ const feeConfig = getTransferFeeConfig(mint);
92
+ if (feeConfig) {
93
+ const epoch = BigInt((await connection.getEpochInfo(commitment)).epoch);
94
+ const fee = getEpochFee(feeConfig, epoch);
95
+ transferFee = {
96
+ basisPoints: fee.transferFeeBasisPoints,
97
+ maximumFee: fee.maximumFee,
98
+ };
99
+ if (fee.transferFeeBasisPoints !== 0 || fee.maximumFee !== 0n) {
100
+ blockers.push("non-zero transfer fees require gross/net/withheld-fee receipt support");
101
+ }
102
+ }
103
+ const hook = getTransferHook(mint);
104
+ if (hook && !hook.programId.equals(PublicKey.default)) {
105
+ transferHookProgram = hook.programId.toBase58();
106
+ blockers.push("active transfer hook requires verified on-chain extra-account resolution and a Devnet-tested adapter");
107
+ }
108
+ const memo = getMemoTransfer(recipient);
109
+ if (memo?.requireIncomingTransferMemos) {
110
+ blockers.push("recipient requires an incoming memo, but the memo-aware settlement adapter is not enabled");
111
+ }
112
+ const cpiGuard = getCpiGuard(source);
113
+ if (cpiGuard?.lockCpi) {
114
+ blockers.push("source account CPI guard blocks the current Anchor delegate transfer path");
115
+ }
116
+ const pausable = getPausableConfig(mint);
117
+ if (pausable?.paused)
118
+ blockers.push("mint transfers are paused");
119
+ const defaultState = getDefaultAccountState(mint);
120
+ if (defaultState?.state === AccountState.Frozen) {
121
+ warnings.push("mint defaults new token accounts to frozen; current source and recipient were checked individually");
122
+ }
123
+ if (mintExtensions.includes(ExtensionType.ConfidentialTransferMint)) {
124
+ warnings.push("confidential extension is present; ChainPay uses only the transparent transfer_checked balance path");
125
+ }
126
+ }
127
+ return {
128
+ mint: mint.address.toBase58(),
129
+ tokenProgram: input.tokenProgram,
130
+ compatible: blockers.length === 0,
131
+ mintExtensions: mintExtensions.map(extensionName),
132
+ sourceAccountExtensions: sourceExtensions.map(extensionName),
133
+ recipientAccountExtensions: recipientExtensions.map(extensionName),
134
+ blockers,
135
+ warnings,
136
+ ...(transferFee ? { transferFee } : {}),
137
+ ...(transferHookProgram ? { transferHookProgram } : {}),
138
+ };
139
+ }
@@ -0,0 +1,18 @@
1
+ import type { Address, ChainPayInstruction, PaymentPreflightContext, TokenProgram } from "./types.js";
2
+ export declare function deriveAssociatedTokenAddress(owner: Address, mint: Address, tokenProgram: TokenProgram): Address;
3
+ export declare function buildCreateAssociatedTokenAccountInstruction(input: {
4
+ payer: Address;
5
+ owner: Address;
6
+ mint: Address;
7
+ tokenProgram: TokenProgram;
8
+ }): ChainPayInstruction;
9
+ export declare function associatedTokenProgramAddress(): Address;
10
+ export type TokenAccountFields = {
11
+ mint: Address;
12
+ owner: Address;
13
+ balance: bigint;
14
+ delegate: Address | null;
15
+ delegatedAmount: bigint;
16
+ };
17
+ export declare function readTokenAccountFields(data: Uint8Array | Buffer): TokenAccountFields | null;
18
+ export declare function paymentPreflightContextFromTokenAccount(data: Uint8Array | Buffer): PaymentPreflightContext | null;