@bujius/checkout-sdk 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,891 @@
1
+ // src/errors.ts
2
+ var ErrorCode = {
3
+ ORDER_NOT_FOUND: "ORDER_NOT_FOUND",
4
+ ORDER_EXPIRED: "ORDER_EXPIRED",
5
+ ORDER_PAID: "ORDER_PAID",
6
+ ORDER_HELD: "ORDER_HELD",
7
+ INVALID_ACCOUNT: "INVALID_ACCOUNT",
8
+ INVALID_AMOUNT: "INVALID_AMOUNT",
9
+ CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
10
+ RPC_UNAVAILABLE: "RPC_UNAVAILABLE"
11
+ };
12
+ var BujiusCheckoutError = class extends Error {
13
+ code;
14
+ constructor(code, options) {
15
+ super(code, options);
16
+ this.name = "BujiusCheckoutError";
17
+ this.code = code;
18
+ }
19
+ };
20
+ function isBujiusCheckoutError(value) {
21
+ return value instanceof BujiusCheckoutError;
22
+ }
23
+
24
+ // src/program/pda.ts
25
+ import { PublicKey } from "@solana/web3.js";
26
+
27
+ // src/program/idl.json
28
+ var idl_default = {
29
+ address: "LNv2EvYZFN1fBJeQPDGffRxVYPMCGpa99yCcWDSDdjN",
30
+ metadata: {
31
+ name: "bujius_checkout",
32
+ version: "0.1.0",
33
+ spec: "0.1.0",
34
+ description: "Created with Anchor"
35
+ },
36
+ instructions: [
37
+ {
38
+ name: "create_split",
39
+ discriminator: [
40
+ 196,
41
+ 195,
42
+ 250,
43
+ 188,
44
+ 17,
45
+ 255,
46
+ 10,
47
+ 151
48
+ ],
49
+ accounts: [
50
+ {
51
+ name: "authority",
52
+ writable: true,
53
+ signer: true
54
+ },
55
+ {
56
+ name: "config",
57
+ writable: true,
58
+ pda: {
59
+ seeds: [
60
+ {
61
+ kind: "const",
62
+ value: [
63
+ 115,
64
+ 112,
65
+ 108,
66
+ 105,
67
+ 116
68
+ ]
69
+ },
70
+ {
71
+ kind: "account",
72
+ path: "authority"
73
+ },
74
+ {
75
+ kind: "arg",
76
+ path: "id"
77
+ }
78
+ ]
79
+ }
80
+ },
81
+ {
82
+ name: "mint",
83
+ docs: [
84
+ "Classic SPL Token mint only (Token-2022 is out of scope)."
85
+ ]
86
+ },
87
+ {
88
+ name: "system_program",
89
+ address: "11111111111111111111111111111111"
90
+ }
91
+ ],
92
+ args: [
93
+ {
94
+ name: "id",
95
+ type: "u16"
96
+ },
97
+ {
98
+ name: "recipients",
99
+ type: {
100
+ vec: {
101
+ defined: {
102
+ name: "Recipient"
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ]
108
+ },
109
+ {
110
+ name: "pay",
111
+ discriminator: [
112
+ 119,
113
+ 18,
114
+ 216,
115
+ 65,
116
+ 192,
117
+ 117,
118
+ 122,
119
+ 220
120
+ ],
121
+ accounts: [
122
+ {
123
+ name: "payer",
124
+ signer: true
125
+ },
126
+ {
127
+ name: "payer_token",
128
+ writable: true
129
+ },
130
+ {
131
+ name: "config"
132
+ },
133
+ {
134
+ name: "mint"
135
+ },
136
+ {
137
+ name: "merchant_token",
138
+ writable: true
139
+ },
140
+ {
141
+ name: "reference"
142
+ },
143
+ {
144
+ name: "token_program",
145
+ address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
146
+ }
147
+ ],
148
+ args: [
149
+ {
150
+ name: "amount",
151
+ type: "u64"
152
+ }
153
+ ]
154
+ }
155
+ ],
156
+ accounts: [
157
+ {
158
+ name: "SplitConfig",
159
+ discriminator: [
160
+ 49,
161
+ 201,
162
+ 50,
163
+ 228,
164
+ 22,
165
+ 142,
166
+ 12,
167
+ 222
168
+ ]
169
+ }
170
+ ],
171
+ events: [
172
+ {
173
+ name: "SplitCreated",
174
+ discriminator: [
175
+ 87,
176
+ 61,
177
+ 160,
178
+ 84,
179
+ 73,
180
+ 204,
181
+ 93,
182
+ 140
183
+ ]
184
+ },
185
+ {
186
+ name: "SplitPaid",
187
+ discriminator: [
188
+ 223,
189
+ 17,
190
+ 5,
191
+ 250,
192
+ 228,
193
+ 37,
194
+ 72,
195
+ 80
196
+ ]
197
+ }
198
+ ],
199
+ errors: [
200
+ {
201
+ code: 6e3,
202
+ name: "InvalidRecipientCount",
203
+ msg: "A split config needs between 1 and 4 recipients"
204
+ },
205
+ {
206
+ code: 6001,
207
+ name: "ZeroBps",
208
+ msg: "Every recipient needs at least 1 bps"
209
+ },
210
+ {
211
+ code: 6002,
212
+ name: "SplitOverflow",
213
+ msg: "The recipients' bps add up to more than 10 000"
214
+ },
215
+ {
216
+ code: 6003,
217
+ name: "DuplicateRecipient",
218
+ msg: "Recipient wallets must be distinct and different from the merchant"
219
+ },
220
+ {
221
+ code: 6004,
222
+ name: "ZeroAmount",
223
+ msg: "The amount must be greater than zero"
224
+ },
225
+ {
226
+ code: 6005,
227
+ name: "MintMismatch",
228
+ msg: "The mint does not match the split config's mint"
229
+ },
230
+ {
231
+ code: 6006,
232
+ name: "BadTokenAccount",
233
+ msg: "A token account is not the expected associated token account"
234
+ },
235
+ {
236
+ code: 6007,
237
+ name: "RecipientCountMismatch",
238
+ msg: "The number of remaining accounts does not match the recipients"
239
+ },
240
+ {
241
+ code: 6008,
242
+ name: "MathOverflow",
243
+ msg: "Arithmetic overflow"
244
+ }
245
+ ],
246
+ types: [
247
+ {
248
+ name: "Recipient",
249
+ type: {
250
+ kind: "struct",
251
+ fields: [
252
+ {
253
+ name: "wallet",
254
+ type: "pubkey"
255
+ },
256
+ {
257
+ name: "bps",
258
+ type: "u16"
259
+ }
260
+ ]
261
+ }
262
+ },
263
+ {
264
+ name: "SplitConfig",
265
+ docs: [
266
+ "One per merchant per `id`; immutable after creation."
267
+ ],
268
+ type: {
269
+ kind: "struct",
270
+ fields: [
271
+ {
272
+ name: "authority",
273
+ type: "pubkey"
274
+ },
275
+ {
276
+ name: "id",
277
+ type: "u16"
278
+ },
279
+ {
280
+ name: "mint",
281
+ type: "pubkey"
282
+ },
283
+ {
284
+ name: "recipients",
285
+ type: {
286
+ vec: {
287
+ defined: {
288
+ name: "Recipient"
289
+ }
290
+ }
291
+ }
292
+ },
293
+ {
294
+ name: "bump",
295
+ type: "u8"
296
+ }
297
+ ]
298
+ }
299
+ },
300
+ {
301
+ name: "SplitCreated",
302
+ type: {
303
+ kind: "struct",
304
+ fields: [
305
+ {
306
+ name: "config",
307
+ type: "pubkey"
308
+ },
309
+ {
310
+ name: "authority",
311
+ type: "pubkey"
312
+ },
313
+ {
314
+ name: "mint",
315
+ type: "pubkey"
316
+ },
317
+ {
318
+ name: "recipients",
319
+ type: {
320
+ vec: {
321
+ defined: {
322
+ name: "Recipient"
323
+ }
324
+ }
325
+ }
326
+ }
327
+ ]
328
+ }
329
+ },
330
+ {
331
+ name: "SplitPaid",
332
+ type: {
333
+ kind: "struct",
334
+ fields: [
335
+ {
336
+ name: "config",
337
+ type: "pubkey"
338
+ },
339
+ {
340
+ name: "payer",
341
+ type: "pubkey"
342
+ },
343
+ {
344
+ name: "mint",
345
+ type: "pubkey"
346
+ },
347
+ {
348
+ name: "reference",
349
+ type: "pubkey"
350
+ },
351
+ {
352
+ name: "amount",
353
+ type: "u64"
354
+ },
355
+ {
356
+ name: "merchant_amount",
357
+ type: "u64"
358
+ },
359
+ {
360
+ name: "shares",
361
+ type: {
362
+ vec: "u64"
363
+ }
364
+ },
365
+ {
366
+ name: "ts",
367
+ type: "i64"
368
+ }
369
+ ]
370
+ }
371
+ }
372
+ ]
373
+ };
374
+
375
+ // src/program/pda.ts
376
+ var PROGRAM_ID = new PublicKey(idl_default.address);
377
+ function findSplitConfig(authority, id) {
378
+ const idBytes = new Uint8Array(2);
379
+ new DataView(idBytes.buffer).setUint16(0, id, true);
380
+ return PublicKey.findProgramAddressSync(
381
+ [new TextEncoder().encode("split"), authority.toBytes(), idBytes],
382
+ PROGRAM_ID
383
+ )[0];
384
+ }
385
+
386
+ // src/program/config.ts
387
+ import { BorshAccountsCoder } from "@coral-xyz/anchor";
388
+ import "@solana/web3.js";
389
+ var coder = new BorshAccountsCoder(idl_default);
390
+ async function fetchSplitConfig(connection, address) {
391
+ let info;
392
+ try {
393
+ info = await connection.getAccountInfo(address, "confirmed");
394
+ } catch {
395
+ throw new BujiusCheckoutError(ErrorCode.RPC_UNAVAILABLE);
396
+ }
397
+ if (!info || !info.owner.equals(PROGRAM_ID)) {
398
+ throw new BujiusCheckoutError(ErrorCode.CONFIG_NOT_FOUND);
399
+ }
400
+ const decoded = coder.decode("SplitConfig", info.data);
401
+ return {
402
+ address,
403
+ authority: decoded.authority,
404
+ id: decoded.id,
405
+ mint: decoded.mint,
406
+ recipients: decoded.recipients.map((r) => ({
407
+ wallet: r.wallet,
408
+ bps: r.bps
409
+ }))
410
+ };
411
+ }
412
+
413
+ // src/program/create-split.ts
414
+ import { BorshInstructionCoder } from "@coral-xyz/anchor";
415
+ import {
416
+ createAssociatedTokenAccountIdempotentInstruction,
417
+ getAssociatedTokenAddressSync
418
+ } from "@solana/spl-token";
419
+ import {
420
+ SystemProgram,
421
+ Transaction,
422
+ TransactionInstruction
423
+ } from "@solana/web3.js";
424
+ var coder2 = new BorshInstructionCoder(idl_default);
425
+ async function buildCreateSplit(connection, authority, id, mint, recipients) {
426
+ const tx = new Transaction();
427
+ for (const owner of [authority, ...recipients.map((r) => r.wallet)]) {
428
+ tx.add(
429
+ createAssociatedTokenAccountIdempotentInstruction(
430
+ authority,
431
+ getAssociatedTokenAddressSync(mint, owner),
432
+ owner,
433
+ mint
434
+ )
435
+ );
436
+ }
437
+ tx.add(
438
+ new TransactionInstruction({
439
+ programId: PROGRAM_ID,
440
+ keys: [
441
+ { pubkey: authority, isSigner: true, isWritable: true },
442
+ { pubkey: findSplitConfig(authority, id), isSigner: false, isWritable: true },
443
+ { pubkey: mint, isSigner: false, isWritable: false },
444
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
445
+ ],
446
+ data: coder2.encode("create_split", { id, recipients })
447
+ })
448
+ );
449
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("confirmed");
450
+ tx.feePayer = authority;
451
+ tx.recentBlockhash = blockhash;
452
+ tx.lastValidBlockHeight = lastValidBlockHeight;
453
+ return tx;
454
+ }
455
+
456
+ // src/core/amounts.ts
457
+ function toBaseUnits(decimal, decimals) {
458
+ if (typeof decimal !== "string" || decimal.length === 0) {
459
+ throw new BujiusCheckoutError(ErrorCode.INVALID_AMOUNT);
460
+ }
461
+ if (!Number.isInteger(decimals) || decimals < 0) {
462
+ throw new BujiusCheckoutError(ErrorCode.INVALID_AMOUNT);
463
+ }
464
+ if (!/^\d+(\.\d+)?$/.test(decimal)) {
465
+ throw new BujiusCheckoutError(ErrorCode.INVALID_AMOUNT);
466
+ }
467
+ const [wholePart, fractionPart = ""] = decimal.split(".");
468
+ if (fractionPart.length > decimals) {
469
+ throw new BujiusCheckoutError(ErrorCode.INVALID_AMOUNT);
470
+ }
471
+ return BigInt(`${wholePart}${fractionPart.padEnd(decimals, "0")}`);
472
+ }
473
+ function fromBaseUnits(amount, decimals) {
474
+ if (typeof amount !== "bigint" || amount < 0n) {
475
+ throw new BujiusCheckoutError(ErrorCode.INVALID_AMOUNT);
476
+ }
477
+ if (!Number.isInteger(decimals) || decimals < 0) {
478
+ throw new BujiusCheckoutError(ErrorCode.INVALID_AMOUNT);
479
+ }
480
+ const raw = amount.toString();
481
+ if (decimals === 0) {
482
+ return raw;
483
+ }
484
+ const padded = raw.padStart(decimals + 1, "0");
485
+ const whole = padded.slice(0, -decimals);
486
+ const fraction = padded.slice(-decimals);
487
+ const trimmedFraction = fraction.replace(/0+$/, "");
488
+ return trimmedFraction.length === 0 ? whole : `${whole}.${trimmedFraction}`;
489
+ }
490
+
491
+ // src/core/reference.ts
492
+ import { Keypair } from "@solana/web3.js";
493
+ function newReference() {
494
+ return Keypair.generate().publicKey;
495
+ }
496
+
497
+ // src/core/url.ts
498
+ import { encodeURL } from "@solana/pay";
499
+ function paymentUrl(handlerHttpsUrl) {
500
+ return encodeURL({ link: new URL(handlerHttpsUrl) }).toString();
501
+ }
502
+
503
+ // src/core/split-math.ts
504
+ function computeSplit(amount, bps) {
505
+ if (amount < 0n) {
506
+ throw new BujiusCheckoutError(ErrorCode.INVALID_AMOUNT);
507
+ }
508
+ const validBps = bps.every((b) => Number.isInteger(b) && b >= 0);
509
+ if (!validBps || bps.reduce((total, b) => total + b, 0) > 1e4) {
510
+ throw new BujiusCheckoutError(ErrorCode.CONFIG_NOT_FOUND);
511
+ }
512
+ const shares = bps.map((b) => amount * BigInt(b) / 10000n);
513
+ let sum = 0n;
514
+ for (const share of shares) {
515
+ sum += share;
516
+ }
517
+ return { shares, merchant: amount - sum };
518
+ }
519
+
520
+ // src/strategies/transfers.ts
521
+ import {
522
+ createAssociatedTokenAccountIdempotentInstruction as createAssociatedTokenAccountIdempotentInstruction3,
523
+ createTransferCheckedInstruction,
524
+ getAssociatedTokenAddressSync as getAssociatedTokenAddressSync3,
525
+ unpackMint
526
+ } from "@solana/spl-token";
527
+ import { PublicKey as PublicKey3, Transaction as Transaction3 } from "@solana/web3.js";
528
+
529
+ // src/strategies/program.ts
530
+ import anchor, { BorshInstructionCoder as BorshInstructionCoder2 } from "@coral-xyz/anchor";
531
+ import {
532
+ createAssociatedTokenAccountIdempotentInstruction as createAssociatedTokenAccountIdempotentInstruction2,
533
+ getAssociatedTokenAddressSync as getAssociatedTokenAddressSync2,
534
+ TOKEN_PROGRAM_ID
535
+ } from "@solana/spl-token";
536
+ import {
537
+ Transaction as Transaction2,
538
+ TransactionInstruction as TransactionInstruction2
539
+ } from "@solana/web3.js";
540
+ var { BN } = anchor;
541
+ var coder3 = new BorshInstructionCoder2(idl_default);
542
+ async function buildProgramPayment(connection, config, payer, amount, reference) {
543
+ const { shares, merchant } = computeSplit(
544
+ amount,
545
+ config.recipients.map((r) => r.bps)
546
+ );
547
+ const ataOf = (wallet) => getAssociatedTokenAddressSync2(config.mint, wallet, true);
548
+ const merchantToken = ataOf(config.authority);
549
+ const recipientAtas = config.recipients.map((r) => ataOf(r.wallet));
550
+ const source = ataOf(payer);
551
+ const funded = [
552
+ ...config.recipients.map((r, i) => ({ wallet: r.wallet, ata: recipientAtas[i], amount: shares[i] })).filter((leg) => leg.amount > 0n),
553
+ ...merchant > 0n ? [{ wallet: config.authority, ata: merchantToken, amount: merchant }] : []
554
+ ];
555
+ let infos;
556
+ try {
557
+ infos = await connection.getMultipleAccountsInfo(
558
+ [...funded.map((leg) => leg.ata), source],
559
+ "confirmed"
560
+ );
561
+ } catch {
562
+ throw new BujiusCheckoutError(ErrorCode.RPC_UNAVAILABLE);
563
+ }
564
+ if (!infos[funded.length]) {
565
+ throw new BujiusCheckoutError(ErrorCode.INVALID_ACCOUNT);
566
+ }
567
+ const tx = new Transaction2();
568
+ for (let i = 0; i < funded.length; i++) {
569
+ if (infos[i] == null) {
570
+ const leg = funded[i];
571
+ tx.add(
572
+ createAssociatedTokenAccountIdempotentInstruction2(payer, leg.ata, leg.wallet, config.mint)
573
+ );
574
+ }
575
+ }
576
+ tx.add(
577
+ new TransactionInstruction2({
578
+ programId: PROGRAM_ID,
579
+ keys: [
580
+ { pubkey: payer, isSigner: true, isWritable: false },
581
+ { pubkey: source, isSigner: false, isWritable: true },
582
+ { pubkey: config.address, isSigner: false, isWritable: false },
583
+ { pubkey: config.mint, isSigner: false, isWritable: false },
584
+ { pubkey: merchantToken, isSigner: false, isWritable: true },
585
+ { pubkey: reference, isSigner: false, isWritable: false },
586
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
587
+ // remaining accounts, in config.recipients order
588
+ ...recipientAtas.map((pubkey) => ({ pubkey, isSigner: false, isWritable: true }))
589
+ ],
590
+ data: coder3.encode("pay", { amount: new BN(amount.toString()) })
591
+ })
592
+ );
593
+ let blockhash;
594
+ let lastValidBlockHeight;
595
+ try {
596
+ ({ blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("confirmed"));
597
+ } catch {
598
+ throw new BujiusCheckoutError(ErrorCode.RPC_UNAVAILABLE);
599
+ }
600
+ tx.feePayer = payer;
601
+ tx.recentBlockhash = blockhash;
602
+ tx.lastValidBlockHeight = lastValidBlockHeight;
603
+ return tx;
604
+ }
605
+
606
+ // src/strategies/transfers.ts
607
+ var U64_MAX = 18446744073709551615n;
608
+ async function buildSplitPayment(connection, config, payer, amount, reference, strategy) {
609
+ if (amount <= 0n || amount > U64_MAX) {
610
+ throw new BujiusCheckoutError(ErrorCode.INVALID_AMOUNT);
611
+ }
612
+ if (!PublicKey3.isOnCurve(payer) || payer.equals(config.authority) || config.recipients.some((r) => r.wallet.equals(payer))) {
613
+ throw new BujiusCheckoutError(ErrorCode.INVALID_ACCOUNT);
614
+ }
615
+ if (strategy === "program") {
616
+ return buildProgramPayment(connection, config, payer, amount, reference);
617
+ }
618
+ if (strategy !== "transfers") {
619
+ throw new Error(`unknown payment strategy: ${String(strategy)}`);
620
+ }
621
+ return buildTransfersPayment(connection, config, payer, amount, reference);
622
+ }
623
+ async function buildTransfersPayment(connection, config, payer, amount, reference) {
624
+ const { shares, merchant } = computeSplit(
625
+ amount,
626
+ config.recipients.map((r) => r.bps)
627
+ );
628
+ const legs = [];
629
+ for (let i = 0; i < config.recipients.length; i++) {
630
+ const share = shares[i];
631
+ if (share > 0n) {
632
+ legs.push({ wallet: config.recipients[i].wallet, amount: share });
633
+ }
634
+ }
635
+ if (merchant > 0n) {
636
+ legs.push({ wallet: config.authority, amount: merchant });
637
+ }
638
+ const ataOf = (wallet) => getAssociatedTokenAddressSync3(config.mint, wallet, true);
639
+ const destAtas = legs.map((leg) => ataOf(leg.wallet));
640
+ const source = ataOf(payer);
641
+ let infos;
642
+ try {
643
+ infos = await connection.getMultipleAccountsInfo(
644
+ [...destAtas, config.mint, source],
645
+ "confirmed"
646
+ );
647
+ } catch {
648
+ throw new BujiusCheckoutError(ErrorCode.RPC_UNAVAILABLE);
649
+ }
650
+ const mintInfo = infos[destAtas.length];
651
+ const sourceInfo = infos[destAtas.length + 1];
652
+ if (!mintInfo || !sourceInfo) {
653
+ throw new BujiusCheckoutError(ErrorCode.INVALID_ACCOUNT);
654
+ }
655
+ let decimals;
656
+ try {
657
+ decimals = unpackMint(config.mint, mintInfo).decimals;
658
+ } catch (cause) {
659
+ throw new BujiusCheckoutError(ErrorCode.INVALID_ACCOUNT, { cause });
660
+ }
661
+ const tx = new Transaction3();
662
+ for (let i = 0; i < destAtas.length; i++) {
663
+ if (infos[i] == null) {
664
+ const leg = legs[i];
665
+ const ata = destAtas[i];
666
+ tx.add(
667
+ createAssociatedTokenAccountIdempotentInstruction3(
668
+ payer,
669
+ ata,
670
+ leg.wallet,
671
+ config.mint
672
+ )
673
+ );
674
+ }
675
+ }
676
+ let firstTransfer = true;
677
+ for (let i = 0; i < legs.length; i++) {
678
+ const leg = legs[i];
679
+ const destination = destAtas[i];
680
+ const ix = createTransferCheckedInstruction(
681
+ source,
682
+ config.mint,
683
+ destination,
684
+ payer,
685
+ leg.amount,
686
+ decimals
687
+ );
688
+ if (firstTransfer) {
689
+ ix.keys.push({ pubkey: reference, isSigner: false, isWritable: false });
690
+ firstTransfer = false;
691
+ }
692
+ tx.add(ix);
693
+ }
694
+ let blockhash;
695
+ let lastValidBlockHeight;
696
+ try {
697
+ ({ blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("confirmed"));
698
+ } catch {
699
+ throw new BujiusCheckoutError(ErrorCode.RPC_UNAVAILABLE);
700
+ }
701
+ tx.feePayer = payer;
702
+ tx.recentBlockhash = blockhash;
703
+ tx.lastValidBlockHeight = lastValidBlockHeight;
704
+ return tx;
705
+ }
706
+
707
+ // src/verify/find-payment.ts
708
+ async function findPayment(connection, reference) {
709
+ let signatures;
710
+ try {
711
+ signatures = await connection.getSignaturesForAddress(reference, void 0, "confirmed");
712
+ } catch {
713
+ throw new BujiusCheckoutError(ErrorCode.RPC_UNAVAILABLE);
714
+ }
715
+ const successful = signatures.filter((s) => s.err === null);
716
+ return successful.length > 0 ? successful[successful.length - 1].signature : null;
717
+ }
718
+
719
+ // src/verify/verify-payment.ts
720
+ function totals(balances, mint) {
721
+ const byOwner = /* @__PURE__ */ new Map();
722
+ for (const b of balances ?? []) {
723
+ if (b.mint !== mint || !b.owner) continue;
724
+ byOwner.set(b.owner, (byOwner.get(b.owner) ?? 0n) + BigInt(b.uiTokenAmount.amount));
725
+ }
726
+ return byOwner;
727
+ }
728
+ async function verifySplitPayment(connection, signature, config, amount, reference, expiresAt) {
729
+ const expected = computeSplit(
730
+ amount,
731
+ config.recipients.map((r) => r.bps)
732
+ );
733
+ const wallets = [...config.recipients.map((r) => r.wallet), config.authority];
734
+ const expectedAmounts = [...expected.shares, expected.merchant];
735
+ const invalid = (reason, legs2 = []) => ({
736
+ status: "invalid",
737
+ reason,
738
+ legs: legs2
739
+ });
740
+ let tx;
741
+ try {
742
+ tx = await connection.getParsedTransaction(signature, {
743
+ commitment: "confirmed",
744
+ maxSupportedTransactionVersion: 0
745
+ });
746
+ } catch {
747
+ throw new BujiusCheckoutError(ErrorCode.RPC_UNAVAILABLE);
748
+ }
749
+ if (!tx || !tx.meta) return invalid("transaction not found");
750
+ if (tx.meta.err !== null) return invalid("transaction failed");
751
+ const referenceKey = reference.toBase58();
752
+ if (!tx.transaction.message.accountKeys.some((k) => String(k.pubkey) === referenceKey)) {
753
+ return invalid("reference not in the transaction");
754
+ }
755
+ const mint = config.mint.toBase58();
756
+ const pre = totals(tx.meta.preTokenBalances, mint);
757
+ const post = totals(tx.meta.postTokenBalances, mint);
758
+ const legs = wallets.map((wallet) => {
759
+ const key = wallet.toBase58();
760
+ return { wallet, amount: (post.get(key) ?? 0n) - (pre.get(key) ?? 0n) };
761
+ });
762
+ for (let i = 0; i < legs.length; i++) {
763
+ if (legs[i].amount !== expectedAmounts[i]) {
764
+ return invalid(
765
+ `${legs[i].wallet.toBase58()} received ${legs[i].amount}, expected ${expectedAmounts[i]}`,
766
+ legs
767
+ );
768
+ }
769
+ }
770
+ const late = expiresAt !== void 0 && tx.blockTime != null && tx.blockTime * 1e3 > expiresAt.getTime();
771
+ return { status: late ? "paid_late" : "paid", legs };
772
+ }
773
+
774
+ // src/server/handler.ts
775
+ import { PublicKey as PublicKey4 } from "@solana/web3.js";
776
+ var CORS_HEADERS = {
777
+ "Access-Control-Allow-Origin": "*",
778
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
779
+ "Access-Control-Allow-Headers": "Content-Type"
780
+ };
781
+ var ERROR_RESPONSES = {
782
+ [ErrorCode.INVALID_ACCOUNT]: { status: 400, sentence: "We could not read your account. Try again." },
783
+ [ErrorCode.ORDER_NOT_FOUND]: { status: 404, sentence: "This order does not exist." },
784
+ [ErrorCode.ORDER_HELD]: { status: 409, sentence: "Someone else is already paying this order." },
785
+ [ErrorCode.ORDER_PAID]: { status: 409, sentence: "This order is already paid." },
786
+ [ErrorCode.ORDER_EXPIRED]: {
787
+ status: 410,
788
+ sentence: "This order expired. Go back to the store and start again."
789
+ },
790
+ [ErrorCode.RPC_UNAVAILABLE]: { status: 503, sentence: "The network is busy. Try again in a moment." }
791
+ };
792
+ var UNPAYABLE = { status: 500, sentence: "This order cannot be paid. Go back to the store." };
793
+ function respond(status, body) {
794
+ const headers = new Headers(CORS_HEADERS);
795
+ if (body === void 0) {
796
+ return new Response(null, { status, headers });
797
+ }
798
+ headers.set("Content-Type", "application/json");
799
+ return new Response(JSON.stringify(body), { status, headers });
800
+ }
801
+ function describeFailure(error) {
802
+ if (isBujiusCheckoutError(error)) {
803
+ return error.code;
804
+ }
805
+ if (error instanceof Error) {
806
+ return `${error.name}: ${error.message.replace(/[a-z][a-z0-9+.-]*:\/\/\S+/gi, "<url>")}`;
807
+ }
808
+ return typeof error;
809
+ }
810
+ function respondToError(error) {
811
+ const known = isBujiusCheckoutError(error) ? ERROR_RESPONSES[error.code] : void 0;
812
+ const { status, sentence } = known ?? UNPAYABLE;
813
+ if (status >= 500) {
814
+ console.error(`Transaction request failed: ${describeFailure(error)}`);
815
+ }
816
+ return respond(status, { error: sentence });
817
+ }
818
+ async function readAccount(request) {
819
+ let body;
820
+ try {
821
+ body = await request.json();
822
+ } catch (cause) {
823
+ throw new BujiusCheckoutError(ErrorCode.INVALID_ACCOUNT, { cause });
824
+ }
825
+ const account = body?.account;
826
+ if (typeof account !== "string") {
827
+ throw new BujiusCheckoutError(ErrorCode.INVALID_ACCOUNT);
828
+ }
829
+ try {
830
+ return new PublicKey4(account);
831
+ } catch (cause) {
832
+ throw new BujiusCheckoutError(ErrorCode.INVALID_ACCOUNT, { cause });
833
+ }
834
+ }
835
+ function createTransactionRequestHandler(options) {
836
+ const { connection, label, iconUrl, strategy, resolve } = options;
837
+ return async (request) => {
838
+ if (request.method === "OPTIONS") {
839
+ return respond(204);
840
+ }
841
+ if (request.method === "GET") {
842
+ return respond(200, { label, icon: iconUrl });
843
+ }
844
+ if (request.method !== "POST") {
845
+ return respond(405, { error: "This request is not supported." });
846
+ }
847
+ try {
848
+ const account = await readAccount(request);
849
+ const key = new URL(request.url).pathname.split("/").filter(Boolean).at(-1) ?? "";
850
+ const resolved = await resolve(key, account);
851
+ if ("error" in resolved) {
852
+ throw new BujiusCheckoutError(resolved.error);
853
+ }
854
+ const { config, amount, reference, message } = resolved;
855
+ const transaction = await buildSplitPayment(
856
+ connection,
857
+ config,
858
+ account,
859
+ amount,
860
+ reference,
861
+ strategy
862
+ );
863
+ const serialized = transaction.serialize({
864
+ requireAllSignatures: false,
865
+ verifySignatures: false
866
+ });
867
+ return respond(200, { transaction: serialized.toString("base64"), message });
868
+ } catch (error) {
869
+ return respondToError(error);
870
+ }
871
+ };
872
+ }
873
+ export {
874
+ BujiusCheckoutError,
875
+ ErrorCode,
876
+ PROGRAM_ID,
877
+ buildCreateSplit,
878
+ buildSplitPayment,
879
+ computeSplit,
880
+ createTransactionRequestHandler,
881
+ fetchSplitConfig,
882
+ findPayment,
883
+ findSplitConfig,
884
+ fromBaseUnits,
885
+ isBujiusCheckoutError,
886
+ newReference,
887
+ paymentUrl,
888
+ toBaseUnits,
889
+ verifySplitPayment
890
+ };
891
+ //# sourceMappingURL=index.js.map