@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,474 @@
1
+ import { instruction, meta } from "../encoding.js";
2
+ import { DEFAULT_PROGRAM_ID, SPL_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID } from "../constants.js";
3
+ import { deriveAssetAddress, deriveConfigAddress, deriveReceiptAddress } from "../pda.js";
4
+ import { CARD_POLICY_DISCRIMINATORS, DELEGATION_PROGRAM_ID, EPHEMERAL_VAULT_ID, MAGIC_CONTEXT_ID, MAGIC_PROGRAM_ID, MAX_BUDGET_CENTS, MAX_FEE_BPS, MAX_MCCS, MAX_MERCHANTS, MIN_PERIOD_SECONDS, PERMISSION_PROGRAM_ID, TEE_VALIDATOR_ALLOWLIST, resolveCardPolicyProgramId, } from "./constants.js";
5
+ import { currencyBytes } from "./accounts.js";
6
+ import { BorshReader, BorshWriter } from "./layout.js";
7
+ import { deriveAuthGuardAddress, deriveCardAccounts, deriveCheckoutIntentAddress, deriveDelegationBufferAddress, deriveDelegationMetadataAddress, deriveDelegationRecordAddress, derivePermissionAddress, deriveRepayAgentAddress, deriveReservationAddress, } from "./pda.js";
8
+ /**
9
+ * Instruction data follows contracts.md §1.3 (Anchor discriminator + Borsh
10
+ * args in the listed order). Account lists, discriminators and arg layouts
11
+ * are checked against the deployed program's IDL
12
+ * (programs/card_policy/idl/card_policy.json, Devnet
13
+ * H3aetJdQXG8EeJSCHZrpQa8iKHBw8e1p9fSPjTUsB93n) by test/cards-idl.test.mjs,
14
+ * including the `#[delegate]` macro accounts of `delegate_card`.
15
+ */
16
+ export const CARD_POLICY_ACCOUNT_ORDER_PROVISIONAL = false;
17
+ const empty = { encode() { }, decode: () => ({}) };
18
+ function writePolicy(w, p) {
19
+ if (p.merchantIdHashes.length > MAX_MERCHANTS)
20
+ throw new Error(`At most ${MAX_MERCHANTS} merchants`);
21
+ if (p.mccs.length > MAX_MCCS)
22
+ throw new Error(`At most ${MAX_MCCS} MCCs`);
23
+ w.u64(p.budgetCents, "budgetCents")
24
+ .u64(p.maxPurchaseCents, "maxPurchaseCents")
25
+ .u16(p.maxPurchasesPerPeriod, "maxPurchasesPerPeriod")
26
+ .u32(p.periodSeconds, "periodSeconds")
27
+ .fixed(currencyBytes(p.currency), 3, "currency")
28
+ .u32(p.merchantIdHashes.length);
29
+ p.merchantIdHashes.forEach((hash, i) => w.fixed(hash, 32, `merchantIdHashes[${i}]`));
30
+ w.u32(p.mccs.length);
31
+ p.mccs.forEach((mcc, i) => w.u16(mcc, `mccs[${i}]`));
32
+ w.i64(p.expiresAt, "expiresAt").bool(p.recurringAllowed).u16(p.feeBps, "feeBps").pubkey(p.authorizer);
33
+ }
34
+ function readVecLength(r, max, name) {
35
+ const length = r.u32(`${name}.length`);
36
+ if (length > max)
37
+ throw new Error(`${name} exceeds ${max} entries`);
38
+ return length;
39
+ }
40
+ function readPolicy(r) {
41
+ const budgetCents = r.u64("budgetCents");
42
+ const maxPurchaseCents = r.u64("maxPurchaseCents");
43
+ const maxPurchasesPerPeriod = r.u16("maxPurchasesPerPeriod");
44
+ const periodSeconds = r.u32("periodSeconds");
45
+ const currency = String.fromCharCode(...r.fixed(3, "currency"));
46
+ const merchantIdHashes = Array.from({ length: readVecLength(r, MAX_MERCHANTS, "merchantIdHashes") }, (_, i) => r.fixed(32, `merchantIdHashes[${i}]`));
47
+ const mccs = Array.from({ length: readVecLength(r, MAX_MCCS, "mccs") }, (_, i) => r.u16(`mccs[${i}]`));
48
+ return {
49
+ budgetCents,
50
+ maxPurchaseCents,
51
+ maxPurchasesPerPeriod,
52
+ periodSeconds,
53
+ currency,
54
+ merchantIdHashes,
55
+ mccs,
56
+ expiresAt: r.i64("expiresAt"),
57
+ recurringAllowed: r.bool("recurringAllowed"),
58
+ feeBps: r.u16("feeBps"),
59
+ authorizer: r.pubkey("authorizer"),
60
+ };
61
+ }
62
+ const CODECS = {
63
+ initCard: {
64
+ encode: (w, a) => { w.fixed(a.cardId, 32, "cardId").u8(a.issuer, "issuer").fixed(a.issuerCardRefHash, 32, "issuerCardRefHash").u64(a.prefundLamports, "prefundLamports"); },
65
+ decode: (r) => ({ cardId: r.fixed(32, "cardId"), issuer: r.u8("issuer"), issuerCardRefHash: r.fixed(32, "issuerCardRefHash"), prefundLamports: r.u64("prefundLamports") }),
66
+ },
67
+ delegateCard: { encode: (w, a) => { w.pubkey(a.validator); }, decode: (r) => ({ validator: r.pubkey("validator") }) },
68
+ initPermission: { encode: (w, a) => { w.pubkey(a.authorizer); }, decode: (r) => ({ authorizer: r.pubkey("authorizer") }) },
69
+ updatePermission: {
70
+ encode: (w, a) => { w.u8(a.op.kind === "add_reader" ? 0 : a.op.kind === "remove_reader" ? 1 : 255).pubkey(a.op.pubkey); },
71
+ decode: (r) => {
72
+ const tag = r.u8("op");
73
+ if (tag > 1)
74
+ throw new Error(`Unknown permission op ${tag}`);
75
+ return { op: { kind: tag === 0 ? "add_reader" : "remove_reader", pubkey: r.pubkey("op.pubkey") } };
76
+ },
77
+ },
78
+ syncPermission: empty,
79
+ setPolicy: { encode: (w, a) => writePolicy(w, a.policy), decode: (r) => ({ policy: readPolicy(r) }) },
80
+ openCheckoutIntent: {
81
+ encode: (w, a) => { w.fixed(a.intentId, 16, "intentId").pubkey(a.agent).fixed(a.merchantIdHash, 32, "merchantIdHash").u16(a.mcc, "mcc").u64(a.maxAmountCents, "maxAmountCents").fixed(currencyBytes(a.currency), 3, "currency").i64(a.expiresAt, "expiresAt"); },
82
+ decode: (r) => ({ intentId: r.fixed(16, "intentId"), agent: r.pubkey("agent"), merchantIdHash: r.fixed(32, "merchantIdHash"), mcc: r.u16("mcc"), maxAmountCents: r.u64("maxAmountCents"), currency: String.fromCharCode(...r.fixed(3, "currency")), expiresAt: r.i64("expiresAt") }),
83
+ },
84
+ cancelCheckoutIntent: empty,
85
+ closeCheckoutIntent: empty,
86
+ authorize: {
87
+ encode: (w, a) => { w.fixed(a.authIdHash, 32, "authIdHash").fixed(a.intentId, 16, "intentId").u64(a.amountCents, "amountCents").fixed(currencyBytes(a.currency), 3, "currency").fixed(a.merchantIdHash, 32, "merchantIdHash").u16(a.mcc, "mcc").bool(a.merchantInitiated).bool(a.singleMessage); },
88
+ decode: (r) => ({ authIdHash: r.fixed(32, "authIdHash"), intentId: r.fixed(16, "intentId"), amountCents: r.u64("amountCents"), currency: String.fromCharCode(...r.fixed(3, "currency")), merchantIdHash: r.fixed(32, "merchantIdHash"), mcc: r.u16("mcc"), merchantInitiated: r.bool("merchantInitiated"), singleMessage: r.bool("singleMessage") }),
89
+ },
90
+ adjustReservation: { encode: (w, a) => { w.u64(a.newAmountCents, "newAmountCents"); }, decode: (r) => ({ newAmountCents: r.u64("newAmountCents") }) },
91
+ capture: {
92
+ encode: (w, a) => { w.u64(a.amountCents, "amountCents").fixed(a.captureIdHash, 32, "captureIdHash"); },
93
+ decode: (r) => ({ amountCents: r.u64("amountCents"), captureIdHash: r.fixed(32, "captureIdHash") }),
94
+ },
95
+ reverse: {
96
+ encode: (w, a) => { w.u64(a.amountCents, "amountCents").u8(a.reason, "reason").fixed(a.eventIdHash, 32, "eventIdHash"); },
97
+ decode: (r) => ({ amountCents: r.u64("amountCents"), reason: r.u8("reason"), eventIdHash: r.fixed(32, "eventIdHash") }),
98
+ },
99
+ refund: {
100
+ encode: (w, a) => { w.u64(a.amountCents, "amountCents").fixed(a.eventIdHash, 32, "eventIdHash"); },
101
+ decode: (r) => ({ amountCents: r.u64("amountCents"), eventIdHash: r.fixed(32, "eventIdHash") }),
102
+ },
103
+ recordDispute: {
104
+ encode: (w, a) => { w.u8(a.state, "state").fixed(a.eventIdHash, 32, "eventIdHash"); },
105
+ decode: (r) => ({ state: r.u8("state"), eventIdHash: r.fixed(32, "eventIdHash") }),
106
+ },
107
+ recordException: {
108
+ encode: (w, a) => { w.u8(a.kind, "kind").u64(a.amountCents, "amountCents").fixed(a.eventIdHash, 32, "eventIdHash"); },
109
+ decode: (r) => ({ kind: r.u8("kind"), amountCents: r.u64("amountCents"), eventIdHash: r.fixed(32, "eventIdHash") }),
110
+ },
111
+ resolveException: {
112
+ encode: (w, a) => { w.fixed(a.eventIdHash, 32, "eventIdHash").u8(a.resolution, "resolution"); },
113
+ decode: (r) => ({ eventIdHash: r.fixed(32, "eventIdHash"), resolution: r.u8("resolution") }),
114
+ },
115
+ rollPeriod: empty,
116
+ freeze: { encode: (w, a) => { w.u8(a.reason, "reason"); }, decode: (r) => ({ reason: r.u8("reason") }) },
117
+ unfreeze: empty,
118
+ recoveryFreeze: { encode: (w, a) => { w.u8(a.reason, "reason"); }, decode: (r) => ({ reason: r.u8("reason") }) },
119
+ restore: {
120
+ encode: (w, a) => {
121
+ const s = a.restore;
122
+ writePolicy(w, s.policy);
123
+ w.u32(s.periodIndex, "periodIndex").u64(s.capturedCents, "capturedCents").u64(s.reservedCents, "reservedCents").u64(s.refundedCents, "refundedCents")
124
+ .u16(s.purchasesCount, "purchasesCount").u64(s.exceptionCents, "exceptionCents").u64(s.statementOutstandingCents, "statementOutstandingCents")
125
+ .fixed(s.ledgerHead, 32, "ledgerHead").u64(s.ledgerSeq, "ledgerSeq").fixed(s.reconDigest, 32, "reconDigest");
126
+ },
127
+ decode: (r) => ({
128
+ restore: {
129
+ policy: readPolicy(r),
130
+ periodIndex: r.u32("periodIndex"),
131
+ capturedCents: r.u64("capturedCents"),
132
+ reservedCents: r.u64("reservedCents"),
133
+ refundedCents: r.u64("refundedCents"),
134
+ purchasesCount: r.u16("purchasesCount"),
135
+ exceptionCents: r.u64("exceptionCents"),
136
+ statementOutstandingCents: r.u64("statementOutstandingCents"),
137
+ ledgerHead: r.fixed(32, "ledgerHead"),
138
+ ledgerSeq: r.u64("ledgerSeq"),
139
+ reconDigest: r.fixed(32, "reconDigest"),
140
+ },
141
+ }),
142
+ },
143
+ confirmReconciled: { encode: (w, a) => { w.fixed(a.reconDigest, 32, "reconDigest"); }, decode: (r) => ({ reconDigest: r.fixed(32, "reconDigest") }) },
144
+ checkpoint: {
145
+ encode: (w, a) => { w.fixed(a.masterSalt, 32, "masterSalt").u64(a.seq, "seq"); },
146
+ decode: (r) => ({ masterSalt: r.fixed(32, "masterSalt"), seq: r.u64("seq") }),
147
+ },
148
+ writeCommitment: {
149
+ encode: (w, a) => { w.fixed(a.root, 32, "root").u64(a.seq, "seq").u32(a.policyVersion, "policyVersion").u32(a.periodIndex, "periodIndex"); },
150
+ decode: (r) => ({ root: r.fixed(32, "root"), seq: r.u64("seq"), policyVersion: r.u32("policyVersion"), periodIndex: r.u32("periodIndex") }),
151
+ },
152
+ wipeCard: empty,
153
+ closeCard: empty,
154
+ closeReservation: empty,
155
+ recordRepayment: {
156
+ encode: (w, a) => { w.fixed(a.statementDigest, 32, "statementDigest").u64(a.amountCents, "amountCents"); },
157
+ decode: (r) => ({ statementDigest: r.fixed(32, "statementDigest"), amountCents: r.u64("amountCents") }),
158
+ },
159
+ repayStatement: {
160
+ encode: (w, a) => { w.fixed(a.statementDigest, 32, "statementDigest").u64(a.amount, "amount"); },
161
+ decode: (r) => ({ statementDigest: r.fixed(32, "statementDigest"), amount: r.u64("amount") }),
162
+ },
163
+ recordPrivateRepayment: {
164
+ encode: (w, a) => { w.fixed(a.statementDigest, 32, "statementDigest").u64(a.amountCents, "amountCents"); },
165
+ decode: (r) => ({ statementDigest: r.fixed(32, "statementDigest"), amountCents: r.u64("amountCents") }),
166
+ },
167
+ };
168
+ export function encodeCardInstructionData(name, args) {
169
+ const w = new BorshWriter().bytes(CARD_POLICY_DISCRIMINATORS[name]);
170
+ CODECS[name].encode(w, args);
171
+ return w.toBytes();
172
+ }
173
+ export function decodeCardInstructionData(data) {
174
+ if (data.length < 8)
175
+ throw new Error("Instruction data is truncated");
176
+ for (const name of Object.keys(CARD_POLICY_DISCRIMINATORS)) {
177
+ const disc = CARD_POLICY_DISCRIMINATORS[name];
178
+ if (disc.every((byte, i) => data[i] === byte)) {
179
+ const r = new BorshReader(data, 8);
180
+ const args = CODECS[name].decode(r);
181
+ if (r.remaining() !== 0)
182
+ throw new Error(`${name} has ${r.remaining()} unexpected trailing bytes`);
183
+ return { name, args };
184
+ }
185
+ }
186
+ throw new Error("Unknown card_policy instruction discriminator");
187
+ }
188
+ /**
189
+ * Client-side mirror of the `set_policy` validation (contracts.md §1.3 #6).
190
+ * The program is the authority; this only stops an owner from signing a
191
+ * transaction that is certain to fail. Returns plain-language problems.
192
+ */
193
+ export function policyArgsProblems(p) {
194
+ const problems = [];
195
+ if (p.maxPurchaseCents <= 0n)
196
+ problems.push("Max purchase must be more than $0.");
197
+ if (p.maxPurchaseCents > p.budgetCents)
198
+ problems.push("Max purchase can't be more than the budget.");
199
+ if (p.budgetCents > MAX_BUDGET_CENTS)
200
+ problems.push("Budget can't be more than $10,000 in the sandbox.");
201
+ if (p.periodSeconds < MIN_PERIOD_SECONDS)
202
+ problems.push("A period must be at least one day.");
203
+ if (p.currency !== "USD")
204
+ problems.push("Cards only spend USD.");
205
+ if (!Number.isInteger(p.feeBps) || p.feeBps < 0 || p.feeBps > MAX_FEE_BPS)
206
+ problems.push("Fee can't be more than 10%.");
207
+ if (p.merchantIdHashes.length === 0 && p.mccs.length === 0)
208
+ problems.push("Pick at least one shop or merchant category.");
209
+ if (p.merchantIdHashes.length > MAX_MERCHANTS)
210
+ problems.push(`At most ${MAX_MERCHANTS} shops.`);
211
+ if (p.mccs.length > MAX_MCCS)
212
+ problems.push(`At most ${MAX_MCCS} merchant categories.`);
213
+ const hashes = new Set(p.merchantIdHashes.map((hash) => Array.from(hash).join(",")));
214
+ if (hashes.size !== p.merchantIdHashes.length)
215
+ problems.push("A shop is listed twice.");
216
+ if (new Set(p.mccs).size !== p.mccs.length)
217
+ problems.push("A merchant category is listed twice.");
218
+ return problems;
219
+ }
220
+ function build(name, snake, programId, keys, args) {
221
+ return instruction(snake, resolveCardPolicyProgramId(programId), keys, encodeCardInstructionData(name, args));
222
+ }
223
+ /** OwnerCardEvent: owner (signer), policy (mut), period (mut). */
224
+ function ownerEventKeys(owner, a) {
225
+ return [meta(owner, false, true), meta(a.policy, true), meta(a.period, true)];
226
+ }
227
+ // ------------------------------------------------ owner-signed (browser) builders
228
+ export function buildInitCardInstruction(input, programId) {
229
+ const a = deriveCardAccounts(input.owner, input.cardId, programId);
230
+ return build("initCard", "init_card", programId, [
231
+ meta(input.owner, true, true),
232
+ meta(a.binding, true),
233
+ meta(a.policy, true),
234
+ meta(a.period, true),
235
+ meta(a.commitment, true),
236
+ meta(SYSTEM_PROGRAM_ID),
237
+ ], { cardId: input.cardId, issuer: input.issuer, issuerCardRefHash: input.issuerCardRefHash, prefundLamports: input.prefundLamports });
238
+ }
239
+ export function buildDelegateCardInstruction(input, programId) {
240
+ if (!TEE_VALIDATOR_ALLOWLIST.includes(input.validator))
241
+ throw new Error("Validator is not an allowed TEE validator");
242
+ const id = resolveCardPolicyProgramId(programId);
243
+ const a = deriveCardAccounts(input.owner, input.cardId, id);
244
+ const delegated = (account) => [
245
+ meta(deriveDelegationBufferAddress(account, id), true),
246
+ meta(deriveDelegationRecordAddress(account), true),
247
+ meta(deriveDelegationMetadataAddress(account), true),
248
+ meta(account, true),
249
+ ];
250
+ return build("delegateCard", "delegate_card", id, [
251
+ meta(input.owner, true, true),
252
+ meta(a.binding),
253
+ ...delegated(a.policy),
254
+ ...delegated(a.period),
255
+ meta(id),
256
+ meta(DELEGATION_PROGRAM_ID),
257
+ meta(SYSTEM_PROGRAM_ID),
258
+ ], { validator: input.validator });
259
+ }
260
+ function permissionKeys(owner, cardId, programId) {
261
+ const a = deriveCardAccounts(owner, cardId, programId);
262
+ return [
263
+ meta(owner, false, true),
264
+ meta(a.policy, true),
265
+ meta(a.period, true),
266
+ meta(a.policyPermission, true),
267
+ meta(a.periodPermission, true),
268
+ meta(EPHEMERAL_VAULT_ID, true),
269
+ meta(MAGIC_PROGRAM_ID),
270
+ meta(PERMISSION_PROGRAM_ID),
271
+ ];
272
+ }
273
+ /** PER. Owner creates the private permissions for policy and period. */
274
+ export function buildInitPermissionInstruction(input, programId) {
275
+ return build("initPermission", "init_permission", programId, permissionKeys(input.owner, input.cardId, programId), { authorizer: input.authorizer });
276
+ }
277
+ /** `[ephemeral account, its permission]` pairs, as `wipe_card` and `update_permission` take them in remaining_accounts. */
278
+ function ephemeralPairs(accounts) {
279
+ return (accounts ?? []).flatMap((account) => [meta(account, true), meta(derivePermissionAddress(account), true)]);
280
+ }
281
+ /**
282
+ * PER. Owner adds or removes a read-only finance reader. There is no "make public" op.
283
+ * `ephemeralAccounts` (open Reservations/CheckoutIntents) are re-synced in the same
284
+ * transaction, so removing a reader also revokes their view of open holds at once.
285
+ */
286
+ export function buildUpdatePermissionInstruction(input, programId) {
287
+ if (input.op.kind !== "add_reader" && input.op.kind !== "remove_reader")
288
+ throw new Error("Only add_reader and remove_reader are allowed");
289
+ return build("updatePermission", "update_permission", programId, [...permissionKeys(input.owner, input.cardId, programId), ...ephemeralPairs(input.ephemeralAccounts)], { op: input.op });
290
+ }
291
+ /**
292
+ * PER. The owner signs this over their own TEE connection. Rejects a policy the program would reject.
293
+ *
294
+ * Once the card's policy is set, changing the credit terms (the authorizer, the
295
+ * fee, a larger budget or a shorter period) also needs ChainPay's current
296
+ * authorizer as `coSigner`: it rides as an extra signer account and the
297
+ * program refuses the change without it. The authorizer is never the owner.
298
+ */
299
+ export function buildSetPolicyInstruction(input, programId) {
300
+ const problems = policyArgsProblems(input.policy);
301
+ if (input.policy.authorizer === input.owner)
302
+ problems.push("The authorizer can't be the owner.");
303
+ if (input.coSigner !== undefined && input.coSigner === input.owner)
304
+ problems.push("The co-signer must be ChainPay's authorizer, not the owner.");
305
+ if (problems.length)
306
+ throw new Error(problems.join(" "));
307
+ // set_policy shares the CardPermissions account struct; the co-signer is the first remaining account.
308
+ const keys = permissionKeys(input.owner, input.cardId, programId);
309
+ if (input.coSigner !== undefined)
310
+ keys.push(meta(input.coSigner, false, true));
311
+ return build("setPolicy", "set_policy", programId, keys, { policy: input.policy });
312
+ }
313
+ /** PER. Owner freeze (reason 1). The authorizer variant lives in Axum. */
314
+ export function buildFreezeInstruction(input, programId) {
315
+ const a = deriveCardAccounts(input.owner, input.cardId, programId);
316
+ return build("freeze", "freeze", programId, [meta(input.signer ?? input.owner, false, true), meta(a.policy, true), meta(a.period)], { reason: input.reason ?? 1 });
317
+ }
318
+ /** PER. Owner only. Never built or sent by an agent tool. */
319
+ export function buildUnfreezeInstruction(input, programId) {
320
+ const a = deriveCardAccounts(input.owner, input.cardId, programId);
321
+ return build("unfreeze", "unfreeze", programId, ownerEventKeys(input.owner, a), {});
322
+ }
323
+ export function buildResolveExceptionInstruction(input, programId) {
324
+ const a = deriveCardAccounts(input.owner, input.cardId, programId);
325
+ return build("resolveException", "resolve_exception", programId, ownerEventKeys(input.owner, a), { eventIdHash: input.eventIdHash, resolution: input.resolution });
326
+ }
327
+ export function buildCancelCheckoutIntentInstruction(input, programId) {
328
+ const a = deriveCardAccounts(input.owner, input.cardId, programId);
329
+ return build("cancelCheckoutIntent", "cancel_checkout_intent", programId, [
330
+ meta(input.signer ?? input.owner, false, true),
331
+ meta(a.policy),
332
+ meta(deriveCheckoutIntentAddress(a.policy, input.intentId, programId), true),
333
+ ], {});
334
+ }
335
+ /**
336
+ * PER. Co-signed: owner and the card's member authorizer must both sign
337
+ * (program 1A review), so the owner alone can't rewrite counters or debt.
338
+ * Axum signs as authorizer first; the owner checks and adds a signature.
339
+ */
340
+ export function buildRestoreInstruction(input, programId) {
341
+ if (input.authorizer === input.owner)
342
+ throw new Error("The authorizer can't be the owner");
343
+ const a = deriveCardAccounts(input.owner, input.cardId, programId);
344
+ return build("restore", "restore", programId, [
345
+ meta(input.owner, false, true),
346
+ meta(input.authorizer, false, true),
347
+ meta(a.policy, true),
348
+ meta(a.period, true),
349
+ ], { restore: input.restore });
350
+ }
351
+ /**
352
+ * PER. Owner or authorizer closes a consumed, cancelled or expired intent and
353
+ * its permission, returning the rent to the card prefund.
354
+ */
355
+ export function buildCloseCheckoutIntentInstruction(input, programId) {
356
+ const a = deriveCardAccounts(input.owner, input.cardId, programId);
357
+ const intent = deriveCheckoutIntentAddress(a.policy, input.intentId, programId);
358
+ return build("closeCheckoutIntent", "close_checkout_intent", programId, [
359
+ meta(input.signer ?? input.owner, false, true),
360
+ meta(a.policy, true),
361
+ meta(intent, true),
362
+ meta(derivePermissionAddress(intent), true),
363
+ meta(EPHEMERAL_VAULT_ID, true),
364
+ meta(MAGIC_PROGRAM_ID),
365
+ meta(PERMISSION_PROGRAM_ID),
366
+ ], {});
367
+ }
368
+ /**
369
+ * Base layer, owner-signed. card_policy's repay agent PDA `["repay_agent", binding]`
370
+ * signs ChainPay `execute_payment` by CPI, so ChainPay enforces the mandate and creates
371
+ * the receipt `["receipt", mandate, statementDigest]`. The owner pays the receipt rent.
372
+ * Returns the receipt address the Axum relay verifies and `record_repayment` re-checks on PER.
373
+ */
374
+ export function buildRepayStatementInstruction(input, programId) {
375
+ if (input.amount <= 0n)
376
+ throw new Error("Repayment amount must be positive");
377
+ const id = resolveCardPolicyProgramId(programId);
378
+ const chainpay = input.chainpayProgramId ?? DEFAULT_PROGRAM_ID;
379
+ const a = deriveCardAccounts(input.owner, input.cardId, id);
380
+ const repayAgent = deriveRepayAgentAddress(a.binding, id);
381
+ const receipt = deriveReceiptAddress(input.mandate, input.statementDigest, chainpay);
382
+ const ix = build("repayStatement", "repay_statement", id, [
383
+ meta(input.owner, true, true),
384
+ meta(a.binding),
385
+ meta(repayAgent, true),
386
+ meta(deriveConfigAddress(chainpay)),
387
+ meta(deriveAssetAddress(input.mint, chainpay)),
388
+ meta(input.mandate, true),
389
+ meta(receipt, true),
390
+ meta(input.mint),
391
+ meta(input.sourceTokenAccount, true),
392
+ meta(input.recipientTokenAccount, true),
393
+ meta(input.tokenProgram ?? SPL_TOKEN_PROGRAM_ID),
394
+ meta(SYSTEM_PROGRAM_ID),
395
+ meta(chainpay),
396
+ ], { statementDigest: input.statementDigest, amount: input.amount });
397
+ return { instruction: ix, receipt, repayAgent };
398
+ }
399
+ export function buildConfirmReconciledInstruction(input, programId) {
400
+ const a = deriveCardAccounts(input.owner, input.cardId, programId);
401
+ return build("confirmReconciled", "confirm_reconciled", programId, ownerEventKeys(input.owner, a), { reconDigest: input.reconDigest });
402
+ }
403
+ /**
404
+ * PER. Owner or authorizer closes a final Reservation (fully captured, reversed
405
+ * or expired; nothing held; no open dispute) and its permission. The rent goes
406
+ * back to the card prefund and the auth id moves into the card's AuthGuard
407
+ * ring, so a replayed issuer authorization still gets `DuplicateAuthorization`.
408
+ * Axum normally does this; the owner can too.
409
+ */
410
+ export function buildCloseReservationInstruction(input, programId) {
411
+ const a = deriveCardAccounts(input.owner, input.cardId, programId);
412
+ const reservation = deriveReservationAddress(a.policy, input.authIdHash, programId);
413
+ const guard = deriveAuthGuardAddress(a.policy, programId);
414
+ return build("closeReservation", "close_reservation", programId, [
415
+ meta(input.signer ?? input.owner, false, true),
416
+ meta(a.policy, true),
417
+ meta(a.period),
418
+ meta(reservation, true),
419
+ meta(derivePermissionAddress(reservation), true),
420
+ meta(guard, true),
421
+ meta(derivePermissionAddress(guard), true),
422
+ meta(EPHEMERAL_VAULT_ID, true),
423
+ meta(MAGIC_PROGRAM_ID),
424
+ meta(PERMISSION_PROGRAM_ID),
425
+ ], {});
426
+ }
427
+ /**
428
+ * `ephemeralAccounts` lists every live Reservation and CheckoutIntent of the
429
+ * card, plus its AuthGuard once a reservation was ever closed
430
+ * (`deriveAuthGuardAddress`); each goes in remaining_accounts as
431
+ * [account, its permission] so the program can close it. Omitting one leaves
432
+ * private data behind (`EphemeralAccountsOpen`).
433
+ */
434
+ export function buildWipeCardInstruction(input, programId) {
435
+ const a = deriveCardAccounts(input.owner, input.cardId, programId);
436
+ const remaining = ephemeralPairs(input.ephemeralAccounts);
437
+ return build("wipeCard", "wipe_card", programId, [
438
+ meta(input.owner, true, true),
439
+ meta(a.policy, true),
440
+ meta(a.period, true),
441
+ meta(a.policyPermission, true),
442
+ meta(a.periodPermission, true),
443
+ meta(EPHEMERAL_VAULT_ID, true),
444
+ meta(MAGIC_CONTEXT_ID, true),
445
+ meta(MAGIC_PROGRAM_ID),
446
+ meta(PERMISSION_PROGRAM_ID),
447
+ ...remaining,
448
+ ], {});
449
+ }
450
+ export function buildCloseCardInstruction(input, programId) {
451
+ const a = deriveCardAccounts(input.owner, input.cardId, programId);
452
+ return build("closeCard", "close_card", programId, [
453
+ meta(input.owner, true, true),
454
+ meta(a.binding, true),
455
+ meta(a.policy, true),
456
+ meta(a.period, true),
457
+ ], {});
458
+ }
459
+ /**
460
+ * Base layer `#[action]`. Account order is fixed by contracts.md §1.3 #24:
461
+ * commitment, binding, source_program, escrow_auth (= policy PDA), escrow (signer).
462
+ * Only the delegation program can satisfy the escrow signer; exposed for tests and card-sim.
463
+ */
464
+ export function buildWriteCommitmentInstruction(input, programId) {
465
+ const id = resolveCardPolicyProgramId(programId);
466
+ const a = deriveCardAccounts(input.owner, input.cardId, id);
467
+ return build("writeCommitment", "write_commitment", id, [
468
+ meta(a.commitment, true),
469
+ meta(a.binding),
470
+ meta(id),
471
+ meta(a.policy),
472
+ meta(a.escrow, false, true),
473
+ ], { root: input.root, seq: input.seq, policyVersion: input.policyVersion, periodIndex: input.periodIndex });
474
+ }
@@ -0,0 +1,33 @@
1
+ import type { Address } from "../types.js";
2
+ /** Little-endian Borsh writer for the fixed card_policy layouts. */
3
+ export declare class BorshWriter {
4
+ private readonly parts;
5
+ u8(value: number, name?: string): this;
6
+ u16(value: number, name?: string): this;
7
+ u32(value: number, name?: string): this;
8
+ u64(value: bigint, name?: string): this;
9
+ i64(value: bigint, name?: string): this;
10
+ bool(value: boolean): this;
11
+ fixed(value: Uint8Array, length: number, name: string): this;
12
+ pubkey(value: Address, _name?: string): this;
13
+ bytes(value: Uint8Array): this;
14
+ toBytes(): Uint8Array;
15
+ }
16
+ /** Little-endian Borsh reader. Every read is bounds-checked. */
17
+ export declare class BorshReader {
18
+ private readonly data;
19
+ offset: number;
20
+ private readonly view;
21
+ constructor(data: Uint8Array, offset?: number);
22
+ private need;
23
+ u8(name?: string): number;
24
+ u16(name?: string): number;
25
+ u32(name?: string): number;
26
+ u64(name?: string): bigint;
27
+ i64(name?: string): bigint;
28
+ bool(name?: string): boolean;
29
+ fixed(length: number, name: string): Uint8Array;
30
+ pubkey(name?: string): Address;
31
+ remaining(): number;
32
+ }
33
+ export declare const ZERO_ADDRESS: Address;
@@ -0,0 +1,137 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ import { publicKey } from "../encoding.js";
3
+ const U64_MAX = 18446744073709551615n;
4
+ const I64_MIN = -9223372036854775808n;
5
+ const I64_MAX = 9223372036854775807n;
6
+ /** Little-endian Borsh writer for the fixed card_policy layouts. */
7
+ export class BorshWriter {
8
+ parts = [];
9
+ u8(value, name = "u8") {
10
+ if (!Number.isInteger(value) || value < 0 || value > 0xff)
11
+ throw new Error(`${name} must fit in u8`);
12
+ this.parts.push(Uint8Array.of(value));
13
+ return this;
14
+ }
15
+ u16(value, name = "u16") {
16
+ if (!Number.isInteger(value) || value < 0 || value > 0xffff)
17
+ throw new Error(`${name} must fit in u16`);
18
+ const bytes = new Uint8Array(2);
19
+ new DataView(bytes.buffer).setUint16(0, value, true);
20
+ this.parts.push(bytes);
21
+ return this;
22
+ }
23
+ u32(value, name = "u32") {
24
+ if (!Number.isInteger(value) || value < 0 || value > 0xffff_ffff)
25
+ throw new Error(`${name} must fit in u32`);
26
+ const bytes = new Uint8Array(4);
27
+ new DataView(bytes.buffer).setUint32(0, value, true);
28
+ this.parts.push(bytes);
29
+ return this;
30
+ }
31
+ u64(value, name = "u64") {
32
+ if (typeof value !== "bigint" || value < 0n || value > U64_MAX)
33
+ throw new Error(`${name} must fit in u64`);
34
+ const bytes = new Uint8Array(8);
35
+ new DataView(bytes.buffer).setBigUint64(0, value, true);
36
+ this.parts.push(bytes);
37
+ return this;
38
+ }
39
+ i64(value, name = "i64") {
40
+ if (typeof value !== "bigint" || value < I64_MIN || value > I64_MAX)
41
+ throw new Error(`${name} must fit in i64`);
42
+ const bytes = new Uint8Array(8);
43
+ new DataView(bytes.buffer).setBigInt64(0, value, true);
44
+ this.parts.push(bytes);
45
+ return this;
46
+ }
47
+ bool(value) {
48
+ this.parts.push(Uint8Array.of(value ? 1 : 0));
49
+ return this;
50
+ }
51
+ fixed(value, length, name) {
52
+ if (!(value instanceof Uint8Array) || value.length !== length)
53
+ throw new Error(`${name} must be exactly ${length} bytes`);
54
+ this.parts.push(new Uint8Array(value));
55
+ return this;
56
+ }
57
+ pubkey(value, _name = "pubkey") {
58
+ this.parts.push(publicKey(value).toBytes());
59
+ return this;
60
+ }
61
+ bytes(value) {
62
+ this.parts.push(new Uint8Array(value));
63
+ return this;
64
+ }
65
+ toBytes() {
66
+ const total = this.parts.reduce((sum, part) => sum + part.length, 0);
67
+ const out = new Uint8Array(total);
68
+ let offset = 0;
69
+ for (const part of this.parts) {
70
+ out.set(part, offset);
71
+ offset += part.length;
72
+ }
73
+ return out;
74
+ }
75
+ }
76
+ /** Little-endian Borsh reader. Every read is bounds-checked. */
77
+ export class BorshReader {
78
+ data;
79
+ offset;
80
+ view;
81
+ constructor(data, offset = 0) {
82
+ this.data = data;
83
+ this.offset = offset;
84
+ this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
85
+ }
86
+ need(length, name) {
87
+ if (this.offset + length > this.data.length)
88
+ throw new Error(`Data ended while reading ${name}`);
89
+ }
90
+ u8(name = "u8") {
91
+ this.need(1, name);
92
+ return this.data[this.offset++];
93
+ }
94
+ u16(name = "u16") {
95
+ this.need(2, name);
96
+ const value = this.view.getUint16(this.offset, true);
97
+ this.offset += 2;
98
+ return value;
99
+ }
100
+ u32(name = "u32") {
101
+ this.need(4, name);
102
+ const value = this.view.getUint32(this.offset, true);
103
+ this.offset += 4;
104
+ return value;
105
+ }
106
+ u64(name = "u64") {
107
+ this.need(8, name);
108
+ const value = this.view.getBigUint64(this.offset, true);
109
+ this.offset += 8;
110
+ return value;
111
+ }
112
+ i64(name = "i64") {
113
+ this.need(8, name);
114
+ const value = this.view.getBigInt64(this.offset, true);
115
+ this.offset += 8;
116
+ return value;
117
+ }
118
+ bool(name = "bool") {
119
+ const value = this.u8(name);
120
+ if (value > 1)
121
+ throw new Error(`${name} is not a valid bool`);
122
+ return value === 1;
123
+ }
124
+ fixed(length, name) {
125
+ this.need(length, name);
126
+ const value = new Uint8Array(this.data.slice(this.offset, this.offset + length));
127
+ this.offset += length;
128
+ return value;
129
+ }
130
+ pubkey(name = "pubkey") {
131
+ return new PublicKey(this.fixed(32, name)).toBase58();
132
+ }
133
+ remaining() {
134
+ return this.data.length - this.offset;
135
+ }
136
+ }
137
+ export const ZERO_ADDRESS = PublicKey.default.toBase58();