@cloak.dev/sdk 0.1.4 → 0.1.6

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 CHANGED
@@ -24,10 +24,7 @@ import {
24
24
  // src/core/CloakSDK.ts
25
25
  import {
26
26
  Keypair,
27
- PublicKey as PublicKey4,
28
- Transaction,
29
- TransactionInstruction as TransactionInstruction2,
30
- ComputeBudgetProgram
27
+ PublicKey as PublicKey2
31
28
  } from "@solana/web3.js";
32
29
 
33
30
  // src/core/types.ts
@@ -85,79 +82,6 @@ function hexToBigint2(hex) {
85
82
  const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
86
83
  return BigInt("0x" + cleanHex);
87
84
  }
88
- async function computeCommitment2(amount, r, sk_spend) {
89
- const [sk0, sk1] = splitTo2Limbs(sk_spend);
90
- const [r0, r1] = splitTo2Limbs(r);
91
- const pk_spend = await poseidonHash([sk0, sk1]);
92
- return await poseidonHash([amount, r0, r1, pk_spend]);
93
- }
94
- async function generateCommitmentAsync(amountLamports, r, skSpend) {
95
- const amount = BigInt(amountLamports);
96
- const rValue = hexToBigint2(bytesToHex(r));
97
- const skValue = hexToBigint2(bytesToHex(skSpend));
98
- return await computeCommitment2(amount, rValue, skValue);
99
- }
100
- function generateCommitment(_amountLamports, _r, _skSpend) {
101
- throw new Error("generateCommitment is deprecated. Use generateCommitmentAsync instead.");
102
- }
103
- async function computeNullifier2(sk_spend, leafIndex) {
104
- const [sk0, sk1] = splitTo2Limbs(sk_spend);
105
- return await poseidonHash([sk0, sk1, leafIndex]);
106
- }
107
- async function computeNullifierAsync(skSpend, leafIndex) {
108
- const skValue = typeof skSpend === "string" ? hexToBigint2(skSpend) : hexToBigint2(bytesToHex(skSpend));
109
- return await computeNullifier2(skValue, BigInt(leafIndex));
110
- }
111
- function computeNullifierSync(_skSpend, _leafIndex) {
112
- throw new Error("computeNullifierSync is deprecated. Use computeNullifierAsync instead.");
113
- }
114
- async function computeOutputsHashAsync(outputs) {
115
- let hash = 0n;
116
- for (const output of outputs) {
117
- const [lo, hi] = pubkeyToLimbs(output.recipient);
118
- hash = await poseidonHash([hash, lo, hi, BigInt(output.amount)]);
119
- }
120
- return hash;
121
- }
122
- async function computeOutputsHash(outAddr, outAmount, outFlags) {
123
- let hash = 0n;
124
- for (let i = 0; i < 5; i++) {
125
- if (outFlags[i] === 1) {
126
- hash = await poseidonHash([hash, outAddr[i][0], outAddr[i][1], outAmount[i]]);
127
- }
128
- }
129
- return hash;
130
- }
131
- function computeOutputsHashSync(_outputs) {
132
- throw new Error("computeOutputsHashSync is deprecated. Use computeOutputsHashAsync instead.");
133
- }
134
- async function computeSwapOutputsHash(inputMintLimbs, outputMintLimbs, recipientAtaLimbs, minOutputAmount, publicAmount) {
135
- return await poseidonHash([
136
- inputMintLimbs[0],
137
- inputMintLimbs[1],
138
- outputMintLimbs[0],
139
- outputMintLimbs[1],
140
- recipientAtaLimbs[0],
141
- recipientAtaLimbs[1],
142
- minOutputAmount,
143
- publicAmount
144
- ]);
145
- }
146
- async function computeSwapOutputsHashAsync(inputMint, outputMint, recipientAta, minOutputAmount, amount) {
147
- const inputMintLimbs = pubkeyToLimbs(inputMint);
148
- const outputMintLimbs = pubkeyToLimbs(outputMint);
149
- const recipientAtaLimbs = pubkeyToLimbs(recipientAta);
150
- return await computeSwapOutputsHash(
151
- inputMintLimbs,
152
- outputMintLimbs,
153
- recipientAtaLimbs,
154
- BigInt(minOutputAmount),
155
- BigInt(amount)
156
- );
157
- }
158
- function computeSwapOutputsHashSync(_outputMint, _recipientAta, _minOutputAmount, _amount) {
159
- throw new Error("computeSwapOutputsHashSync is deprecated. Use computeSwapOutputsHashAsync instead.");
160
- }
161
85
  function bigintToBytes322(n) {
162
86
  const hex = n.toString(16).padStart(64, "0");
163
87
  const bytes = new Uint8Array(32);
@@ -430,223 +354,11 @@ function importKeys(exported) {
430
354
  return generateCloakKeys(masterSeed);
431
355
  }
432
356
 
433
- // src/utils/network.ts
434
- function detectNetworkFromRpcUrl(rpcUrl) {
435
- const url = rpcUrl || process.env.NEXT_PUBLIC_SOLANA_RPC_URL || "";
436
- const lowerUrl = url.toLowerCase();
437
- if (lowerUrl.includes("mainnet") || lowerUrl.includes("api.mainnet-beta") || lowerUrl.includes("mainnet-beta")) {
438
- return "mainnet";
439
- }
440
- if (lowerUrl.includes("testnet") || lowerUrl.includes("api.testnet")) {
441
- return "testnet";
442
- }
443
- if (lowerUrl.includes("devnet") || lowerUrl.includes("api.devnet")) {
444
- return "devnet";
445
- }
446
- if (lowerUrl.includes("localhost") || lowerUrl.includes("127.0.0.1") || lowerUrl.includes("local")) {
447
- return "localnet";
448
- }
449
- return "mainnet";
450
- }
451
- function getRpcUrlForNetwork(network) {
452
- switch (network) {
453
- case "mainnet":
454
- return "https://api.mainnet-beta.solana.com";
455
- case "devnet":
456
- return "https://api.devnet.solana.com";
457
- case "localnet":
458
- return "http://localhost:8899";
459
- default:
460
- return "https://api.mainnet-beta.solana.com";
461
- }
462
- }
463
- function isValidRpcUrl(url) {
464
- try {
465
- const parsed = new URL(url);
466
- return parsed.protocol === "http:" || parsed.protocol === "https:";
467
- } catch {
468
- return false;
469
- }
470
- }
471
- function getExplorerUrl(signature, network = "devnet") {
472
- const cluster = network === "mainnet" ? "" : `?cluster=${network}`;
473
- return `https://explorer.solana.com/tx/${signature}${cluster}`;
474
- }
475
- function getAddressExplorerUrl(address, network = "devnet") {
476
- const cluster = network === "mainnet" ? "" : `?cluster=${network}`;
477
- return `https://explorer.solana.com/address/${address}${cluster}`;
478
- }
479
-
480
- // src/utils/fees.ts
481
- var LAMPORTS_PER_SOL = 1e9;
482
- var FIXED_FEE_LAMPORTS = 5e6;
483
- var VARIABLE_FEE_NUMERATOR = 3;
484
- var VARIABLE_FEE_DENOMINATOR = 1e3;
485
- var VARIABLE_FEE_RATE = VARIABLE_FEE_NUMERATOR / VARIABLE_FEE_DENOMINATOR;
486
- var MIN_DEPOSIT_LAMPORTS = 1e7;
487
- function calculateFee(amountLamports) {
488
- const variableFee = Math.floor(amountLamports * VARIABLE_FEE_NUMERATOR / VARIABLE_FEE_DENOMINATOR);
489
- return FIXED_FEE_LAMPORTS + variableFee;
490
- }
491
- function calculateFeeBigint(amountLamports) {
492
- const fixed = BigInt(FIXED_FEE_LAMPORTS);
493
- const variable = amountLamports * BigInt(VARIABLE_FEE_NUMERATOR) / BigInt(VARIABLE_FEE_DENOMINATOR);
494
- return fixed + variable;
495
- }
496
- function isWithdrawAmountSufficient(amountLamports) {
497
- return amountLamports > calculateFeeBigint(amountLamports);
498
- }
499
- function getDistributableAmount(amountLamports) {
500
- return amountLamports - calculateFee(amountLamports);
501
- }
502
- function formatAmount(lamports, decimals = 9) {
503
- return (lamports / LAMPORTS_PER_SOL).toFixed(decimals);
504
- }
505
- function parseAmount(sol) {
506
- const num = parseFloat(sol);
507
- if (isNaN(num) || num < 0) {
508
- throw new Error(`Invalid SOL amount: ${sol}`);
509
- }
510
- return Math.floor(num * LAMPORTS_PER_SOL);
511
- }
512
- function validateOutputsSum(outputs, expectedTotal) {
513
- const sum = outputs.reduce((acc, out) => acc + out.amount, 0);
514
- return sum === expectedTotal;
515
- }
516
- function calculateRelayFee(amountLamports, feeBps) {
517
- if (feeBps < 0 || feeBps > 1e4) {
518
- throw new Error("Fee basis points must be between 0 and 10000");
519
- }
520
- return Math.floor(amountLamports * feeBps / 1e4);
521
- }
522
-
523
- // src/core/note-manager.ts
524
- async function generateNote(amountLamports, network) {
525
- const actualNetwork = network || detectNetworkFromRpcUrl();
526
- const skSpend = randomBytes(32);
527
- const rBytes = randomBytes(32);
528
- const commitmentBigint = await generateCommitmentAsync(amountLamports, rBytes, skSpend);
529
- const commitmentHex = commitmentBigint.toString(16).padStart(64, "0");
530
- const skSpendHex = bytesToHex(skSpend);
531
- const rHex = bytesToHex(rBytes);
532
- return {
533
- version: "1.0",
534
- amount: amountLamports,
535
- commitment: commitmentHex,
536
- sk_spend: skSpendHex,
537
- r: rHex,
538
- timestamp: Date.now(),
539
- network: actualNetwork
540
- };
541
- }
542
- async function generateNoteFromWallet(amountLamports, keys, network) {
543
- const actualNetwork = network || detectNetworkFromRpcUrl();
544
- const rBytes = randomBytes(32);
545
- const sk_spend = hexToBytes(keys.spend.sk_spend_hex);
546
- const commitmentBigint = await generateCommitmentAsync(amountLamports, rBytes, sk_spend);
547
- const commitmentHex = commitmentBigint.toString(16).padStart(64, "0");
548
- return {
549
- version: "2.0",
550
- amount: amountLamports,
551
- commitment: commitmentHex,
552
- sk_spend: keys.spend.sk_spend_hex,
553
- r: bytesToHex(rBytes),
554
- timestamp: Date.now(),
555
- network: actualNetwork
556
- };
557
- }
558
- function parseNote(jsonString) {
559
- const note = JSON.parse(jsonString);
560
- if (!note.version || !note.amount || !note.commitment || !note.sk_spend || !note.r) {
561
- throw new CloakError(
562
- "Invalid note format: missing required fields",
563
- "validation",
564
- false
565
- );
566
- }
567
- if (!/^[0-9a-f]{64}$/i.test(note.commitment)) {
568
- throw new CloakError("Invalid commitment format", "validation", false);
569
- }
570
- if (!/^[0-9a-f]{64}$/i.test(note.sk_spend)) {
571
- throw new CloakError("Invalid sk_spend format", "validation", false);
572
- }
573
- if (!/^[0-9a-f]{64}$/i.test(note.r)) {
574
- throw new CloakError("Invalid r format", "validation", false);
575
- }
576
- return note;
577
- }
578
- function exportNote(note, pretty = false) {
579
- return pretty ? JSON.stringify(note, null, 2) : JSON.stringify(note);
580
- }
581
- function isWithdrawable(note) {
582
- return !!(note.depositSignature && note.leafIndex !== void 0 && note.root);
583
- }
584
- function updateNoteWithDeposit(note, depositInfo) {
585
- const updated = {
586
- ...note,
587
- depositSignature: depositInfo.signature,
588
- depositSlot: depositInfo.slot,
589
- leafIndex: depositInfo.leafIndex,
590
- root: depositInfo.root
591
- };
592
- if (depositInfo.merkleProof) {
593
- updated.merkleProof = depositInfo.merkleProof;
594
- }
595
- return updated;
596
- }
597
- function findNoteByCommitment(notes, commitment) {
598
- return notes.find((n) => n.commitment === commitment);
599
- }
600
- function filterNotesByNetwork(notes, network) {
601
- return notes.filter((n) => n.network === network);
602
- }
603
- function filterWithdrawableNotes(notes) {
604
- return notes.filter(isWithdrawable);
605
- }
606
- function exportWalletKeys(keys) {
607
- return exportKeys(keys);
608
- }
609
- function importWalletKeys(keysJson) {
610
- return importKeys(keysJson);
611
- }
612
- function getPublicViewKey(keys) {
613
- return keys.view.pvk_hex;
614
- }
615
- function getViewKey(keys) {
616
- return keys.view;
617
- }
618
- var calculateFee2 = calculateFee;
619
- function getDistributableAmount2(amountLamports) {
620
- return amountLamports - calculateFee2(amountLamports);
621
- }
622
- function getRecipientAmount(amountLamports) {
623
- return getDistributableAmount2(amountLamports);
624
- }
625
-
626
357
  // src/core/storage.ts
627
358
  var MemoryStorageAdapter = class {
628
359
  constructor() {
629
- this.notes = /* @__PURE__ */ new Map();
630
360
  this.keys = null;
631
361
  }
632
- saveNote(note) {
633
- this.notes.set(note.commitment, note);
634
- }
635
- loadAllNotes() {
636
- return Array.from(this.notes.values());
637
- }
638
- updateNote(commitment, updates) {
639
- const existing = this.notes.get(commitment);
640
- if (existing) {
641
- this.notes.set(commitment, { ...existing, ...updates });
642
- }
643
- }
644
- deleteNote(commitment) {
645
- this.notes.delete(commitment);
646
- }
647
- clearAllNotes() {
648
- this.notes.clear();
649
- }
650
362
  saveKeys(keys) {
651
363
  this.keys = keys;
652
364
  }
@@ -657,10 +369,10 @@ var MemoryStorageAdapter = class {
657
369
  this.keys = null;
658
370
  }
659
371
  };
660
- var LocalStorageAdapter = class {
661
- constructor(notesKey = "cloak_notes", keysKey = "cloak_wallet_keys") {
662
- this.notesKey = notesKey;
372
+ var _LocalStorageAdapter = class _LocalStorageAdapter {
373
+ constructor(keysKey = "cloak_wallet_keys") {
663
374
  this.keysKey = keysKey;
375
+ _LocalStorageAdapter.runLegacyPurgeOnce();
664
376
  }
665
377
  getStorage() {
666
378
  if (typeof globalThis !== "undefined" && globalThis.localStorage) {
@@ -668,47 +380,6 @@ var LocalStorageAdapter = class {
668
380
  }
669
381
  return null;
670
382
  }
671
- saveNote(note) {
672
- const storage = this.getStorage();
673
- if (!storage) throw new Error("localStorage not available");
674
- const notes = this.loadAllNotes();
675
- notes.push(note);
676
- storage.setItem(this.notesKey, JSON.stringify(notes));
677
- }
678
- loadAllNotes() {
679
- const storage = this.getStorage();
680
- if (!storage) return [];
681
- const stored = storage.getItem(this.notesKey);
682
- if (!stored) return [];
683
- try {
684
- return JSON.parse(stored);
685
- } catch {
686
- return [];
687
- }
688
- }
689
- updateNote(commitment, updates) {
690
- const storage = this.getStorage();
691
- if (!storage) return;
692
- const notes = this.loadAllNotes();
693
- const index = notes.findIndex((n) => n.commitment === commitment);
694
- if (index !== -1) {
695
- notes[index] = { ...notes[index], ...updates };
696
- storage.setItem(this.notesKey, JSON.stringify(notes));
697
- }
698
- }
699
- deleteNote(commitment) {
700
- const storage = this.getStorage();
701
- if (!storage) return;
702
- const notes = this.loadAllNotes();
703
- const filtered = notes.filter((n) => n.commitment !== commitment);
704
- storage.setItem(this.notesKey, JSON.stringify(filtered));
705
- }
706
- clearAllNotes() {
707
- const storage = this.getStorage();
708
- if (storage) {
709
- storage.removeItem(this.notesKey);
710
- }
711
- }
712
383
  saveKeys(keys) {
713
384
  const storage = this.getStorage();
714
385
  if (!storage) throw new Error("localStorage not available");
@@ -731,126 +402,64 @@ var LocalStorageAdapter = class {
731
402
  storage.removeItem(this.keysKey);
732
403
  }
733
404
  }
734
- };
735
-
736
- // src/utils/validation.ts
737
- import { PublicKey } from "@solana/web3.js";
738
- function isValidSolanaAddress(address) {
739
- try {
740
- new PublicKey(address);
741
- return true;
742
- } catch {
743
- return false;
744
- }
745
- }
746
- function validateNote(note) {
747
- if (!note || typeof note !== "object") {
748
- throw new Error("Note must be an object");
749
- }
750
- const requiredFields = ["version", "amount", "commitment", "sk_spend", "r", "timestamp", "network"];
751
- for (const field of requiredFields) {
752
- if (!(field in note)) {
753
- throw new Error(`Missing required field: ${field}`);
754
- }
755
- }
756
- if (typeof note.version !== "string") {
757
- throw new Error("Version must be a string");
758
- }
759
- if (typeof note.amount !== "number" || note.amount <= 0) {
760
- throw new Error("Amount must be a positive number");
761
- }
762
- if (!isValidHex(note.commitment, 32)) {
763
- throw new Error("Invalid commitment format (expected 64 hex characters)");
764
- }
765
- if (!isValidHex(note.sk_spend, 32)) {
766
- throw new Error("Invalid sk_spend format (expected 64 hex characters)");
767
- }
768
- if (!isValidHex(note.r, 32)) {
769
- throw new Error("Invalid r format (expected 64 hex characters)");
770
- }
771
- if (typeof note.timestamp !== "number" || note.timestamp <= 0) {
772
- throw new Error("Timestamp must be a positive number");
773
- }
774
- if (!["localnet", "devnet", "testnet", "mainnet"].includes(note.network)) {
775
- throw new Error("Network must be localnet, devnet, testnet, or mainnet");
776
- }
777
- if (note.depositSignature !== void 0 && typeof note.depositSignature !== "string") {
778
- throw new Error("Deposit signature must be a string");
779
- }
780
- if (note.depositSlot !== void 0 && typeof note.depositSlot !== "number") {
781
- throw new Error("Deposit slot must be a number");
782
- }
783
- if (note.leafIndex !== void 0) {
784
- if (typeof note.leafIndex !== "number" || note.leafIndex < 0) {
785
- throw new Error("Leaf index must be a non-negative number");
786
- }
787
- }
788
- if (note.root !== void 0 && !isValidHex(note.root, 32)) {
789
- throw new Error("Invalid root format (expected 64 hex characters)");
790
- }
791
- if (note.merkleProof !== void 0) {
792
- if (!Array.isArray(note.merkleProof.pathElements)) {
793
- throw new Error("Merkle proof pathElements must be an array");
405
+ /**
406
+ * Runs the legacy-purge exactly once per browser origin. Invoked
407
+ * automatically by the constructor; exposed as a static for tests and
408
+ * for callers who want to force the check before the first adapter
409
+ * instantiation.
410
+ */
411
+ static runLegacyPurgeOnce() {
412
+ if (typeof globalThis === "undefined" || !globalThis.localStorage) {
413
+ return;
794
414
  }
795
- if (!Array.isArray(note.merkleProof.pathIndices)) {
796
- throw new Error("Merkle proof pathIndices must be an array");
415
+ const storage = globalThis.localStorage;
416
+ if (storage.getItem(_LocalStorageAdapter.LEGACY_PURGE_SENTINEL_KEY)) {
417
+ return;
797
418
  }
798
- if (note.merkleProof.pathElements.length !== note.merkleProof.pathIndices.length) {
799
- throw new Error("Merkle proof pathElements and pathIndices must have same length");
419
+ storage.removeItem(_LocalStorageAdapter.LEGACY_NOTES_KEY);
420
+ try {
421
+ storage.setItem(_LocalStorageAdapter.LEGACY_PURGE_SENTINEL_KEY, "1");
422
+ } catch {
800
423
  }
801
424
  }
802
- }
803
- function parseNote2(jsonString) {
804
- let parsed;
805
- try {
806
- parsed = JSON.parse(jsonString);
807
- } catch (error) {
808
- throw new Error("Invalid JSON format");
809
- }
810
- validateNote(parsed);
811
- return parsed;
812
- }
813
- function validateWithdrawableNote(note) {
814
- if (!note.depositSignature) {
815
- throw new Error("Note must be deposited before withdrawal (missing depositSignature)");
816
- }
817
- if (note.leafIndex === void 0) {
818
- throw new Error("Note must be deposited before withdrawal (missing leafIndex)");
819
- }
820
- if (!note.root) {
821
- throw new Error("Note must have historical root for withdrawal");
822
- }
823
- if (!note.merkleProof) {
824
- throw new Error("Note must have Merkle proof for withdrawal");
825
- }
826
- if (note.merkleProof.pathElements.length === 0) {
827
- throw new Error("Merkle proof is empty");
828
- }
829
- }
830
- function validateTransfers(recipients, totalAmount) {
831
- if (recipients.length === 0) {
832
- throw new Error("At least one recipient is required");
833
- }
834
- if (recipients.length > 5) {
835
- throw new Error("Maximum 5 recipients allowed");
836
- }
837
- for (let i = 0; i < recipients.length; i++) {
838
- const transfer2 = recipients[i];
839
- const isPublicKeyLike = transfer2.recipient && typeof transfer2.recipient.toBase58 === "function" && typeof transfer2.recipient.toBuffer === "function";
840
- if (!isPublicKeyLike) {
841
- throw new Error(`Recipient ${i} must be a PublicKey (got ${typeof transfer2.recipient})`);
842
- }
843
- if (typeof transfer2.amount !== "number" || transfer2.amount <= 0) {
844
- throw new Error(`Recipient ${i} amount must be a positive number`);
425
+ /**
426
+ * Manual cleanup helper for callers that stored notes under a non-default
427
+ * key. The auto-purge in the constructor only handles the canonical
428
+ * `cloak_notes` slot; pass the custom key here.
429
+ *
430
+ * Pre-0.1.6 versions stored the user's `CloakNote[]` (including
431
+ * `r` randomness and `sk_spend` secret-key hex) as JSON plaintext.
432
+ * Those notes are no longer usable (the OLD `withdraw_regular.circom`
433
+ * flow is incompatible with the deployed UTXO program) but the plaintext
434
+ * would otherwise linger indefinitely.
435
+ *
436
+ * Idempotent; safe to call when the key doesn't exist or when running
437
+ * outside a browser (no-op). Does NOT update the auto-purge sentinel.
438
+ *
439
+ * @param notesKey - localStorage key to remove (default `"cloak_notes"`,
440
+ * matching the pre-0.1.6 default).
441
+ */
442
+ static purgeLegacyNoteStorage(notesKey = _LocalStorageAdapter.LEGACY_NOTES_KEY) {
443
+ if (typeof globalThis === "undefined" || !globalThis.localStorage) {
444
+ return;
845
445
  }
446
+ globalThis.localStorage.removeItem(notesKey);
846
447
  }
847
- const sum = recipients.reduce((acc, t) => acc + t.amount, 0);
848
- if (sum !== totalAmount) {
849
- throw new Error(
850
- `Recipients sum (${sum}) does not match expected total (${totalAmount})`
851
- );
852
- }
853
- }
448
+ };
449
+ /**
450
+ * Sentinel key set after the auto-purge runs. Persists across browser
451
+ * sessions so the purge never re-runs on the same origin.
452
+ */
453
+ _LocalStorageAdapter.LEGACY_PURGE_SENTINEL_KEY = "cloak_purged_v0_1_5";
454
+ /**
455
+ * Default localStorage key the legacy `<= 0.1.5` SDK used for the
456
+ * plaintext `CloakNote[]` blob.
457
+ */
458
+ _LocalStorageAdapter.LEGACY_NOTES_KEY = "cloak_notes";
459
+ var LocalStorageAdapter = _LocalStorageAdapter;
460
+
461
+ // src/services/RelayService.ts
462
+ import { sha256 } from "@noble/hashes/sha2";
854
463
 
855
464
  // src/utils/errors.ts
856
465
  var RootNotFoundError = class extends Error {
@@ -877,6 +486,71 @@ function parseRelayErrorResponse(responseText) {
877
486
  return { isRootNotFound: false };
878
487
  }
879
488
  }
489
+ var UtxoAlreadySpentError = class extends Error {
490
+ constructor(message, spentUtxoCommitments = []) {
491
+ super(message);
492
+ this.spentUtxoCommitments = spentUtxoCommitments;
493
+ this.name = "UtxoAlreadySpentError";
494
+ }
495
+ };
496
+ var SanctionsQuoteError = class extends Error {
497
+ constructor(message, onChainCode, subKind) {
498
+ super(message);
499
+ this.onChainCode = onChainCode;
500
+ this.subKind = subKind;
501
+ this.name = "SanctionsQuoteError";
502
+ }
503
+ };
504
+ var RelayInternalError = class extends Error {
505
+ constructor(message, relayMessage, httpStatus, cachedTreeFromChain) {
506
+ super(message);
507
+ this.relayMessage = relayMessage;
508
+ this.httpStatus = httpStatus;
509
+ this.cachedTreeFromChain = cachedTreeFromChain;
510
+ this.name = "RelayInternalError";
511
+ }
512
+ };
513
+ function classifyRelayError(responseText, httpStatus) {
514
+ let inner = "";
515
+ try {
516
+ const parsed = JSON.parse(responseText);
517
+ inner = parsed?.message ?? "";
518
+ } catch {
519
+ inner = responseText;
520
+ }
521
+ const haystack = inner.toLowerCase();
522
+ if (haystack.includes("0x1020") || haystack.includes("doublespend")) {
523
+ return new UtxoAlreadySpentError(
524
+ "This shielded balance was already spent. The UTXO's nullifier is registered on-chain \u2014 typically means you spent it via another session or device. Run verifyUtxos() to reconcile local state."
525
+ );
526
+ }
527
+ if (haystack.includes("0x10b0") || haystack.includes("rangequoteexpired")) {
528
+ return new SanctionsQuoteError(
529
+ "Range sanctions quote expired before the on-chain program processed the transaction. The relay's quote-expiry window is too short relative to proof-gen + confirmation latency, or on-chain clock drifted. Retry the send; the relay will issue a fresh quote.",
530
+ 4272,
531
+ "expired"
532
+ );
533
+ }
534
+ if (haystack.includes("0x10b2") || haystack.includes("rangequotewalletmismatch")) {
535
+ return new SanctionsQuoteError(
536
+ "Range sanctions quote's wallet doesn't match the on-chain expected wallet. Indicates a relay config drift \u2014 the sender used for quote signing doesn't match the program's pool.range_signer / fee payer. Relay operators must fix; caller retry will not help.",
537
+ 4274,
538
+ "wallet_mismatch"
539
+ );
540
+ }
541
+ if (haystack.includes("0x10b3") || haystack.includes("rangequotemissinged25519")) {
542
+ return new SanctionsQuoteError(
543
+ "Transaction reached the on-chain program without the required Ed25519 sanctions quote instruction. This is a relay bug \u2014 the relay constructed the tx without attaching the signed quote. Caller retry will not help.",
544
+ 4275,
545
+ "missing_ix"
546
+ );
547
+ }
548
+ return new RelayInternalError(
549
+ `Relay returned ${httpStatus ?? "an error"}: ${inner || responseText}`,
550
+ inner || responseText,
551
+ httpStatus
552
+ );
553
+ }
880
554
  var ShieldPoolErrors = {
881
555
  // Root management errors
882
556
  4096: "Invalid Merkle root",
@@ -1681,7 +1355,6 @@ function formatErrorForLogging(error) {
1681
1355
  }
1682
1356
 
1683
1357
  // src/services/RelayService.ts
1684
- import { sha256 } from "@noble/hashes/sha2";
1685
1358
  var RelayService = class {
1686
1359
  /**
1687
1360
  * Create a new Relay Service client
@@ -2050,91 +1723,23 @@ var RelayService = class {
2050
1723
  }
2051
1724
  };
2052
1725
 
2053
- // src/solana/instructions.ts
2054
- import {
2055
- PublicKey as PublicKey2,
2056
- SystemProgram,
2057
- TransactionInstruction
2058
- } from "@solana/web3.js";
2059
- function createDepositInstruction(params) {
2060
- if (params.commitment.length !== 32) {
2061
- throw new Error(
2062
- `Invalid commitment length: ${params.commitment.length} (expected 32 bytes)`
2063
- );
2064
- }
2065
- if (params.amount <= 0) {
2066
- throw new Error("Amount must be positive");
2067
- }
2068
- const discriminant = new Uint8Array([1]);
2069
- const amountBytes = new Uint8Array(8);
2070
- new DataView(amountBytes.buffer).setBigUint64(
2071
- 0,
2072
- BigInt(params.amount),
2073
- true
2074
- // little-endian
2075
- );
2076
- const data = new Uint8Array(41);
2077
- data.set(discriminant, 0);
2078
- data.set(amountBytes, 1);
2079
- data.set(params.commitment, 9);
2080
- return new TransactionInstruction({
2081
- programId: params.programId,
2082
- keys: [
2083
- // Account 0: Payer (signer, writable) - pays for transaction
2084
- { pubkey: params.payer, isSigner: true, isWritable: true },
2085
- // Account 1: Pool (writable) - receives SOL
2086
- { pubkey: params.pool, isSigner: false, isWritable: true },
2087
- // Account 2: System Program (readonly) - for transfers
2088
- { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
2089
- // Account 3: Merkle Tree (writable) - stores on-chain Merkle tree
2090
- { pubkey: params.merkleTree, isSigner: false, isWritable: true }
2091
- ],
2092
- data: Buffer.from(data)
2093
- });
2094
- }
2095
- function validateDepositParams(params) {
2096
- if (!(params.programId instanceof PublicKey2)) {
2097
- throw new Error("programId must be a PublicKey");
2098
- }
2099
- if (!(params.payer instanceof PublicKey2)) {
2100
- throw new Error("payer must be a PublicKey");
2101
- }
2102
- if (!(params.pool instanceof PublicKey2)) {
2103
- throw new Error("pool must be a PublicKey");
2104
- }
2105
- if (!(params.merkleTree instanceof PublicKey2)) {
2106
- throw new Error("merkleTree must be a PublicKey");
2107
- }
2108
- if (typeof params.amount !== "number" || params.amount <= 0) {
2109
- throw new Error("amount must be a positive number");
2110
- }
2111
- if (!(params.commitment instanceof Uint8Array)) {
2112
- throw new Error("commitment must be a Uint8Array");
2113
- }
2114
- if (params.commitment.length !== 32) {
2115
- throw new Error(
2116
- `commitment must be 32 bytes (got ${params.commitment.length})`
2117
- );
2118
- }
2119
- }
2120
-
2121
1726
  // src/utils/pda.ts
2122
- import { PublicKey as PublicKey3 } from "@solana/web3.js";
1727
+ import { PublicKey } from "@solana/web3.js";
2123
1728
  function getShieldPoolPDAs(programId, mint = NATIVE_SOL_MINT) {
2124
1729
  const pid = programId || CLOAK_PROGRAM_ID;
2125
- const [pool] = PublicKey3.findProgramAddressSync(
1730
+ const [pool] = PublicKey.findProgramAddressSync(
2126
1731
  [Buffer.from("pool"), mint.toBuffer()],
2127
1732
  pid
2128
1733
  );
2129
- const [merkleTree] = PublicKey3.findProgramAddressSync(
1734
+ const [merkleTree] = PublicKey.findProgramAddressSync(
2130
1735
  [Buffer.from("merkle_tree"), mint.toBuffer()],
2131
1736
  pid
2132
1737
  );
2133
- const [treasury] = PublicKey3.findProgramAddressSync(
1738
+ const [treasury] = PublicKey.findProgramAddressSync(
2134
1739
  [Buffer.from("treasury"), mint.toBuffer()],
2135
1740
  pid
2136
1741
  );
2137
- const [vaultAuthority] = PublicKey3.findProgramAddressSync(
1742
+ const [vaultAuthority] = PublicKey.findProgramAddressSync(
2138
1743
  [Buffer.from("vault_authority"), mint.toBuffer()],
2139
1744
  pid
2140
1745
  );
@@ -2150,7 +1755,7 @@ function getNullifierPDA(poolPubkey, nullifier, programId) {
2150
1755
  if (nullifier.length !== 32) {
2151
1756
  throw new Error(`Nullifier must be 32 bytes, got ${nullifier.length}`);
2152
1757
  }
2153
- return PublicKey3.findProgramAddressSync(
1758
+ return PublicKey.findProgramAddressSync(
2154
1759
  [Buffer.from("nullifier"), poolPubkey.toBuffer(), Buffer.from(nullifier)],
2155
1760
  pid
2156
1761
  );
@@ -2160,256 +1765,12 @@ function getSwapStatePDA(poolPubkey, nullifier, programId) {
2160
1765
  if (nullifier.length !== 32) {
2161
1766
  throw new Error(`Nullifier must be 32 bytes, got ${nullifier.length}`);
2162
1767
  }
2163
- return PublicKey3.findProgramAddressSync(
1768
+ return PublicKey.findProgramAddressSync(
2164
1769
  [Buffer.from("swap_state"), poolPubkey.toBuffer(), Buffer.from(nullifier)],
2165
1770
  pid
2166
1771
  );
2167
1772
  }
2168
1773
 
2169
- // src/utils/proof-generation.ts
2170
- import * as snarkjs from "snarkjs";
2171
- var IS_REACT_NATIVE = typeof navigator !== "undefined" && navigator.product === "ReactNative";
2172
- var IS_BROWSER = IS_REACT_NATIVE || typeof window !== "undefined" || typeof globalThis !== "undefined" && typeof globalThis.document !== "undefined";
2173
- var DEFAULT_CIRCUITS_URL = "https://cloak-circuits.s3.us-east-1.amazonaws.com/circuits/0.1.0";
2174
- var SKIP_CIRCUIT_VERIFY_ENV = typeof process !== "undefined" && process.env && process.env.CLOAK_SKIP_CIRCUIT_INTEGRITY_CHECK === "1";
2175
- function isUrl(path) {
2176
- return path.startsWith("http://") || path.startsWith("https://");
2177
- }
2178
- function resolveCircuitsUrl(circuitsPath2) {
2179
- if (circuitsPath2.trim() !== "" && circuitsPath2.trim() !== DEFAULT_CIRCUITS_URL) {
2180
- return DEFAULT_CIRCUITS_URL;
2181
- }
2182
- return DEFAULT_CIRCUITS_URL;
2183
- }
2184
- async function fetchFileAsBuffer(url) {
2185
- const response = await fetch(url);
2186
- if (!response.ok) {
2187
- throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
2188
- }
2189
- const arrayBuffer = await response.arrayBuffer();
2190
- return new Uint8Array(arrayBuffer);
2191
- }
2192
- async function sha256Hex(data) {
2193
- if (IS_BROWSER) {
2194
- const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
2195
- const buffer = bytes.buffer.slice(
2196
- bytes.byteOffset,
2197
- bytes.byteOffset + bytes.byteLength
2198
- );
2199
- const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
2200
- return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
2201
- }
2202
- const nodeCrypto = await getNodeCrypto();
2203
- return nodeCrypto.createHash("sha256").update(data).digest("hex");
2204
- }
2205
- var _nodeCrypto = null;
2206
- function nodeRequire(moduleName) {
2207
- if (IS_BROWSER) {
2208
- throw new Error(`Node.js ${moduleName} module not available in browser/React Native`);
2209
- }
2210
- try {
2211
- const requireFunc = new Function("moduleName", "return require(moduleName)");
2212
- return requireFunc(moduleName);
2213
- } catch {
2214
- throw new Error(`Cannot load Node.js module ${moduleName} synchronously in ESM.`);
2215
- }
2216
- }
2217
- async function getNodeCrypto() {
2218
- if (_nodeCrypto) return _nodeCrypto;
2219
- _nodeCrypto = nodeRequire("crypto");
2220
- return _nodeCrypto;
2221
- }
2222
- async function generateWithdrawRegularProof(inputs, circuitsPath2) {
2223
- const circuitsUrl = resolveCircuitsUrl(circuitsPath2);
2224
- let wasmInput;
2225
- let zkeyInput;
2226
- if (IS_BROWSER) {
2227
- wasmInput = `${circuitsUrl}/withdraw_regular_js/withdraw_regular.wasm`;
2228
- zkeyInput = `${circuitsUrl}/withdraw_regular_final.zkey`;
2229
- } else {
2230
- const wasmUrl = `${circuitsUrl}/withdraw_regular_js/withdraw_regular.wasm`;
2231
- const zkeyUrl = `${circuitsUrl}/withdraw_regular_final.zkey`;
2232
- const [wasmData, zkeyData] = await Promise.all([
2233
- fetchFileAsBuffer(wasmUrl),
2234
- fetchFileAsBuffer(zkeyUrl)
2235
- ]);
2236
- wasmInput = wasmData;
2237
- zkeyInput = zkeyData;
2238
- }
2239
- const verification = await verifyCircuitIntegrity(circuitsUrl, "withdraw_regular");
2240
- if (!verification.valid) {
2241
- throw new Error(
2242
- `withdraw_regular circuit integrity verification failed: ${verification.error ?? "hash mismatch"}`
2243
- );
2244
- }
2245
- const circuitInputs = {
2246
- // Public signals
2247
- root: inputs.root.toString(),
2248
- nullifier: inputs.nullifier.toString(),
2249
- outputs_hash: inputs.outputs_hash.toString(),
2250
- public_amount: inputs.public_amount.toString(),
2251
- // Private common inputs
2252
- amount: inputs.amount.toString(),
2253
- leaf_index: inputs.leaf_index.toString(),
2254
- sk: inputs.sk.map((x) => x.toString()),
2255
- r: inputs.r.map((x) => x.toString()),
2256
- pathElements: inputs.pathElements.map((x) => x.toString()),
2257
- pathIndices: inputs.pathIndices,
2258
- // Outputs
2259
- num_outputs: inputs.num_outputs,
2260
- out_addr: inputs.out_addr.map((addr) => addr.map((x) => x.toString())),
2261
- out_amount: inputs.out_amount.map((x) => x.toString()),
2262
- out_flags: inputs.out_flags,
2263
- // Fee calculation
2264
- var_fee: inputs.var_fee.toString(),
2265
- rem: inputs.rem.toString()
2266
- };
2267
- const { proof, publicSignals } = await snarkjs.groth16.fullProve(circuitInputs, wasmInput, zkeyInput);
2268
- const proofBytes = proofToBytes(proof);
2269
- return {
2270
- proof,
2271
- publicSignals,
2272
- proofBytes,
2273
- publicInputsBytes: new Uint8Array(0)
2274
- // Not used, kept for compatibility
2275
- };
2276
- }
2277
- async function generateWithdrawSwapProof(inputs, circuitsPath2) {
2278
- const circuitsUrl = resolveCircuitsUrl(circuitsPath2);
2279
- let wasmInput;
2280
- let zkeyInput;
2281
- if (IS_BROWSER) {
2282
- wasmInput = `${circuitsUrl}/withdraw_swap_js/withdraw_swap.wasm`;
2283
- zkeyInput = `${circuitsUrl}/withdraw_swap_final.zkey`;
2284
- } else {
2285
- const wasmUrl = `${circuitsUrl}/withdraw_swap_js/withdraw_swap.wasm`;
2286
- const zkeyUrl = `${circuitsUrl}/withdraw_swap_final.zkey`;
2287
- const [wasmData, zkeyData] = await Promise.all([
2288
- fetchFileAsBuffer(wasmUrl),
2289
- fetchFileAsBuffer(zkeyUrl)
2290
- ]);
2291
- wasmInput = wasmData;
2292
- zkeyInput = zkeyData;
2293
- }
2294
- const verification = await verifyCircuitIntegrity(circuitsUrl, "withdraw_swap");
2295
- if (!verification.valid) {
2296
- throw new Error(
2297
- `withdraw_swap circuit integrity verification failed: ${verification.error ?? "hash mismatch"}`
2298
- );
2299
- }
2300
- const sk = splitTo2Limbs(inputs.sk_spend);
2301
- const r = splitTo2Limbs(inputs.r);
2302
- const circuitInputs = {
2303
- // Public signals (must be provided for witness generation)
2304
- root: inputs.root.toString(),
2305
- nullifier: inputs.nullifier.toString(),
2306
- outputs_hash: inputs.outputs_hash.toString(),
2307
- public_amount: inputs.public_amount.toString(),
2308
- // Private inputs
2309
- sk: sk.map((x) => x.toString()),
2310
- r: r.map((x) => x.toString()),
2311
- amount: inputs.amount.toString(),
2312
- leaf_index: inputs.leaf_index.toString(),
2313
- pathElements: inputs.path_elements.map((x) => x.toString()),
2314
- pathIndices: inputs.path_indices,
2315
- input_mint: inputs.input_mint.map((x) => x.toString()),
2316
- output_mint: inputs.output_mint.map((x) => x.toString()),
2317
- recipient_ata: inputs.recipient_ata.map((x) => x.toString()),
2318
- min_output_amount: inputs.min_output_amount.toString(),
2319
- // Note: fee is computed off-chain and validated on-chain:
2320
- // - fixed_fee is 5000000 (0.005 SOL)
2321
- // - var_fee and rem are computed from amount * 3 / 1000 (0.3%)
2322
- var_fee: inputs.var_fee.toString(),
2323
- rem: inputs.rem.toString()
2324
- };
2325
- const { proof, publicSignals } = await snarkjs.groth16.fullProve(
2326
- circuitInputs,
2327
- wasmInput,
2328
- zkeyInput
2329
- );
2330
- const proofBytes = proofToBytes(proof);
2331
- return {
2332
- proof,
2333
- publicSignals,
2334
- proofBytes,
2335
- publicInputsBytes: new Uint8Array(0)
2336
- // Not used, kept for compatibility
2337
- };
2338
- }
2339
- async function areCircuitsAvailable(circuitsPath2) {
2340
- if (!circuitsPath2 || circuitsPath2 === "") {
2341
- return false;
2342
- }
2343
- return isUrl(resolveCircuitsUrl(circuitsPath2));
2344
- }
2345
- async function getDefaultCircuitsPath() {
2346
- return DEFAULT_CIRCUITS_URL;
2347
- }
2348
- var EXPECTED_CIRCUIT_HASHES = {
2349
- withdraw_regular_wasm: "b80c364c926332111945ef437b07a71720fae774e08206cb11b6171f2a2420a3",
2350
- withdraw_regular_zkey: "6dbca2612f7cc257b897d93384ae9dd09c1617fd1668f8287efb1b124488d144",
2351
- withdraw_swap_wasm: "a296badc0047c211a4724c96a4e64a77cd75fbde7cfa4cf23af3640a723e5686",
2352
- withdraw_swap_zkey: "95251954feaac80396651d12a0df3288630fa75db9410aebecb8aaee17a4f6ea"
2353
- };
2354
- async function verifyCircuitIntegrity(circuitsPath2, circuit) {
2355
- const circuitsUrl = resolveCircuitsUrl(circuitsPath2);
2356
- if (SKIP_CIRCUIT_VERIFY_ENV) {
2357
- return {
2358
- valid: true,
2359
- circuit,
2360
- error: "Verification skipped (CLOAK_SKIP_CIRCUIT_INTEGRITY_CHECK=1)"
2361
- };
2362
- }
2363
- const expected = circuit === "withdraw_regular" ? {
2364
- wasm: EXPECTED_CIRCUIT_HASHES.withdraw_regular_wasm,
2365
- zkey: EXPECTED_CIRCUIT_HASHES.withdraw_regular_zkey
2366
- } : {
2367
- wasm: EXPECTED_CIRCUIT_HASHES.withdraw_swap_wasm,
2368
- zkey: EXPECTED_CIRCUIT_HASHES.withdraw_swap_zkey
2369
- };
2370
- const wasmPath = `${circuitsUrl}/${circuit}_js/${circuit}.wasm`;
2371
- const zkeyPath = `${circuitsUrl}/${circuit}_final.zkey`;
2372
- try {
2373
- const [wasmBytes, zkeyBytes] = await Promise.all([
2374
- fetchFileAsBuffer(wasmPath),
2375
- fetchFileAsBuffer(zkeyPath)
2376
- ]);
2377
- const computed = {
2378
- wasm: await sha256Hex(wasmBytes),
2379
- zkey: await sha256Hex(zkeyBytes)
2380
- };
2381
- if (computed.wasm === expected.wasm && computed.zkey === expected.zkey) {
2382
- return {
2383
- valid: true,
2384
- circuit,
2385
- computed,
2386
- expected
2387
- };
2388
- }
2389
- return {
2390
- valid: false,
2391
- circuit,
2392
- error: `Circuit artifact hash mismatch for ${circuit}`,
2393
- computed,
2394
- expected
2395
- };
2396
- } catch (error) {
2397
- return {
2398
- valid: false,
2399
- circuit,
2400
- error: `Circuit verification failed: ${error instanceof Error ? error.message : String(error)}`
2401
- };
2402
- }
2403
- }
2404
- async function verifyAllCircuits(circuitsPath2) {
2405
- const circuitsUrl = resolveCircuitsUrl(circuitsPath2);
2406
- const results = await Promise.all([
2407
- verifyCircuitIntegrity(circuitsUrl, "withdraw_regular"),
2408
- verifyCircuitIntegrity(circuitsUrl, "withdraw_swap")
2409
- ]);
2410
- return results;
2411
- }
2412
-
2413
1774
  // src/utils/logger.ts
2414
1775
  var globalDebugEnabled = false;
2415
1776
  function checkEnvDebug() {
@@ -2626,44 +1987,16 @@ async function computeProofInternal(leafIndex, _nextIndex, subtrees) {
2626
1987
  }
2627
1988
 
2628
1989
  // src/core/CloakSDK.ts
2629
- var COMPUTE_BUDGET_PROGRAM_ID = new PublicKey4("ComputeBudget111111111111111111111111111111");
2630
- function createSetLoadedAccountsDataSizeLimitInstruction(bytes) {
2631
- const data = Buffer.alloc(5);
2632
- data.writeUInt8(4, 0);
2633
- data.writeUInt32LE(bytes, 1);
2634
- return new TransactionInstruction2({
2635
- keys: [],
2636
- programId: COMPUTE_BUDGET_PROGRAM_ID,
2637
- data
2638
- });
2639
- }
2640
- var CLOAK_PROGRAM_ID = new PublicKey4("zh1eLd6rSphLejbFfJEneUwzHRfMKxgzrgkfwA6qRkW");
1990
+ var CLOAK_PROGRAM_ID = new PublicKey2("zh1eLd6rSphLejbFfJEneUwzHRfMKxgzrgkfwA6qRkW");
2641
1991
  var CLOAK_API_URL = "https://api.cloak.ag";
2642
1992
  var CloakSDK = class {
2643
- /**
2644
- * Create a new Cloak SDK client
2645
- *
2646
- * @param config - Client configuration
2647
- *
2648
- * @example Node.js mode (with keypair)
2649
- * ```typescript
2650
- * const sdk = new CloakSDK({
2651
- * keypairBytes: keypair.secretKey,
2652
- * network: "devnet"
2653
- * });
2654
- * ```
2655
- *
2656
- * @example Browser mode (with wallet adapter)
2657
- * ```typescript
2658
- * const sdk = new CloakSDK({
2659
- * wallet: walletAdapter,
2660
- * network: "devnet"
2661
- * });
2662
- * ```
2663
- */
2664
1993
  constructor(config) {
2665
1994
  if (!config.keypairBytes && !config.wallet) {
2666
- throw new Error("Must provide either keypairBytes (Node.js) or wallet (Browser)");
1995
+ throw new CloakError(
1996
+ "Must provide either keypairBytes (Node.js) or wallet (Browser)",
1997
+ "validation",
1998
+ false
1999
+ );
2667
2000
  }
2668
2001
  if (config.debug) {
2669
2002
  setDebugMode(true);
@@ -2683,13 +2016,9 @@ var CloakSDK = class {
2683
2016
  }
2684
2017
  }
2685
2018
  const programId = config.programId || CLOAK_PROGRAM_ID;
2686
- const { pool, merkleTree, treasury } = getShieldPoolPDAs(
2687
- programId,
2688
- NATIVE_SOL_MINT
2689
- );
2019
+ const { pool, merkleTree, treasury } = getShieldPoolPDAs(programId, NATIVE_SOL_MINT);
2690
2020
  this.config = {
2691
2021
  network: config.network || "mainnet",
2692
- keypairBytes: config.keypairBytes,
2693
2022
  cloakKeys: config.cloakKeys,
2694
2023
  programId,
2695
2024
  poolAddress: pool,
@@ -2698,9 +2027,7 @@ var CloakSDK = class {
2698
2027
  debug: config.debug
2699
2028
  };
2700
2029
  }
2701
- /**
2702
- * Get the public key for deposits (from keypair or wallet)
2703
- */
2030
+ /** Public key of the configured signer (keypair or wallet). */
2704
2031
  getPublicKey() {
2705
2032
  if (this.keypair) {
2706
2033
  return this.keypair.publicKey;
@@ -2708,950 +2035,44 @@ var CloakSDK = class {
2708
2035
  if (this.wallet?.publicKey) {
2709
2036
  return this.wallet.publicKey;
2710
2037
  }
2711
- throw new Error("No public key available - wallet not connected or keypair not provided");
2038
+ throw new CloakError(
2039
+ "No public key available - wallet not connected and no keypair provided",
2040
+ "wallet",
2041
+ false
2042
+ );
2712
2043
  }
2713
- /**
2714
- * Check if the SDK is using a wallet adapter
2715
- */
2044
+ /** True when the SDK was constructed with a wallet adapter (browser). */
2716
2045
  isWalletMode() {
2717
2046
  return !!this.wallet && !this.keypair;
2718
2047
  }
2719
2048
  /**
2720
- * Deposit SOL into the Cloak protocol
2721
- *
2722
- * Creates a new note (or uses a provided one), submits a deposit transaction,
2723
- * and registers with the indexer.
2724
- *
2725
- * @param connection - Solana connection
2726
- * @param payer - Payer wallet with sendTransaction method
2727
- * @param amountOrNote - Amount in lamports OR an existing note to deposit
2728
- * @param options - Optional configuration
2729
- * @returns Deposit result with note and transaction info
2730
- *
2731
- * @example
2732
- * ```typescript
2733
- * // Generate and deposit in one step
2734
- * const result = await client.deposit(
2735
- * connection,
2736
- * wallet,
2737
- * 1_000_000_000,
2738
- * {
2739
- * onProgress: (status) => console.log(status)
2740
- * }
2741
- * );
2742
- *
2743
- * // Or deposit a pre-generated note
2744
- * const note = client.generateNote(1_000_000_000);
2745
- * const result = await client.deposit(connection, wallet, note);
2746
- * ```
2747
- */
2748
- async deposit(connection, amountOrNote, options) {
2749
- try {
2750
- let note;
2751
- let isNewNote = false;
2752
- if (typeof amountOrNote === "number") {
2753
- options?.onProgress?.("generating_note", { message: "Generating note with secrets..." });
2754
- note = await generateNote(amountOrNote, this.config.network);
2755
- isNewNote = true;
2756
- } else {
2757
- note = amountOrNote;
2758
- if (note.depositSignature) {
2759
- throw new Error("Note has already been deposited");
2760
- }
2761
- }
2762
- if (isNewNote) {
2763
- await this.storage.saveNote(note);
2764
- if (options?.onNoteGenerated) {
2765
- options?.onProgress?.("awaiting_note_acknowledgment", {
2766
- message: "Waiting for note to be saved before proceeding with deposit..."
2767
- });
2768
- try {
2769
- await options.onNoteGenerated(note);
2770
- } catch (error) {
2771
- throw new Error(
2772
- `Failed to save note before deposit: ${error instanceof Error ? error.message : String(error)}. For your safety, the deposit has been aborted. Your funds are safe.`
2773
- );
2774
- }
2775
- }
2776
- options?.onProgress?.("note_saved", {
2777
- message: "Note saved. Proceeding with on-chain deposit..."
2778
- });
2779
- }
2780
- const payerPubkey = this.getPublicKey();
2781
- const balance = await connection.getBalance(payerPubkey);
2782
- const requiredAmount = note.amount + 5e3;
2783
- if (balance < requiredAmount) {
2784
- throw new Error(
2785
- `Insufficient balance. Required: ${requiredAmount} lamports (${note.amount} + fees), Available: ${balance} lamports`
2786
- );
2787
- }
2788
- const commitmentBytes = hexToBytes(note.commitment);
2789
- const programId = this.config.programId || CLOAK_PROGRAM_ID;
2790
- const depositIx = createDepositInstruction({
2791
- programId,
2792
- payer: payerPubkey,
2793
- pool: this.config.poolAddress,
2794
- merkleTree: this.config.merkleTreeAddress,
2795
- amount: note.amount,
2796
- commitment: commitmentBytes
2797
- });
2798
- const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
2799
- const priorityFee = options?.priorityFee ?? 1e4;
2800
- const cuPriceIx = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFee });
2801
- const dataSizeLimit = options?.loadedAccountsDataSizeLimit ?? 256 * 1024;
2802
- const dataSizeLimitIx = dataSizeLimit > 0 ? createSetLoadedAccountsDataSizeLimitInstruction(dataSizeLimit) : null;
2803
- let computeUnits;
2804
- if (options?.optimizeCU && this.keypair) {
2805
- options?.onProgress?.("simulating", { message: "Simulating transaction for optimal CU..." });
2806
- const simCuLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 2e5 });
2807
- const simTransaction = new Transaction({
2808
- feePayer: payerPubkey,
2809
- recentBlockhash: blockhash
2810
- }).add(simCuLimitIx).add(cuPriceIx);
2811
- if (dataSizeLimitIx) {
2812
- simTransaction.add(dataSizeLimitIx);
2813
- }
2814
- simTransaction.add(depositIx);
2815
- try {
2816
- const simulation = await connection.simulateTransaction(simTransaction, [this.keypair]);
2817
- if (simulation.value.err) {
2818
- console.warn("Simulation failed, using default CU:", simulation.value.err);
2819
- computeUnits = 4e4;
2820
- } else {
2821
- const simulatedCU = simulation.value.unitsConsumed ?? 3e4;
2822
- computeUnits = Math.ceil(simulatedCU * 1.2);
2823
- console.log(`CU optimization: ${simulatedCU} simulated \u2192 ${computeUnits} limit`);
2824
- }
2825
- } catch (simError) {
2826
- console.warn("Simulation RPC error, using default CU:", simError);
2827
- computeUnits = 4e4;
2828
- }
2829
- } else if (options?.optimizeCU && this.wallet) {
2830
- console.warn("CU optimization via simulation not available in wallet mode (would require double signing). Using default.");
2831
- computeUnits = options?.computeUnits ?? 4e4;
2832
- } else {
2833
- computeUnits = options?.computeUnits ?? 4e4;
2834
- }
2835
- const cuLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits });
2836
- const transaction = new Transaction({
2837
- feePayer: payerPubkey,
2838
- recentBlockhash: blockhash
2839
- }).add(cuLimitIx).add(cuPriceIx);
2840
- if (dataSizeLimitIx) {
2841
- transaction.add(dataSizeLimitIx);
2842
- }
2843
- transaction.add(depositIx);
2844
- if (!transaction.feePayer) {
2845
- throw new Error("Transaction feePayer is not set");
2846
- }
2847
- if (!transaction.recentBlockhash) {
2848
- throw new Error("Transaction recentBlockhash is not set");
2849
- }
2850
- let signature;
2851
- if (this.wallet) {
2852
- if (!this.wallet.publicKey) {
2853
- throw new Error("Wallet not connected - publicKey is null");
2854
- }
2855
- if (this.wallet.signTransaction) {
2856
- try {
2857
- const signedTransaction = await this.wallet.signTransaction(transaction);
2858
- const rawTransaction = signedTransaction.serialize();
2859
- signature = await connection.sendRawTransaction(rawTransaction, {
2860
- skipPreflight: options?.skipPreflight || false,
2861
- preflightCommitment: "confirmed",
2862
- maxRetries: 3
2863
- });
2864
- } catch (signError) {
2865
- if (this.wallet.sendTransaction) {
2866
- signature = await this.wallet.sendTransaction(transaction, connection, {
2867
- skipPreflight: options?.skipPreflight || false,
2868
- preflightCommitment: "confirmed",
2869
- maxRetries: 3
2870
- });
2871
- } else {
2872
- throw signError;
2873
- }
2874
- }
2875
- } else if (this.wallet.sendTransaction) {
2876
- signature = await this.wallet.sendTransaction(transaction, connection, {
2877
- skipPreflight: options?.skipPreflight || false,
2878
- preflightCommitment: "confirmed",
2879
- maxRetries: 3
2880
- });
2881
- } else {
2882
- throw new Error("Wallet adapter must provide either sendTransaction or signTransaction");
2883
- }
2884
- } else if (this.keypair) {
2885
- signature = await connection.sendTransaction(transaction, [this.keypair], {
2886
- skipPreflight: options?.skipPreflight || false,
2887
- preflightCommitment: "confirmed",
2888
- maxRetries: 3
2889
- });
2890
- } else {
2891
- throw new Error("No signing method available - provide keypair or wallet");
2892
- }
2893
- const confirmation = await connection.confirmTransaction({
2894
- signature,
2895
- blockhash,
2896
- lastValidBlockHeight
2897
- });
2898
- if (confirmation.value.err) {
2899
- throw new Error(
2900
- `Transaction failed: ${JSON.stringify(confirmation.value.err)}`
2901
- );
2902
- }
2903
- const txDetails = await connection.getTransaction(signature, {
2904
- commitment: "confirmed",
2905
- maxSupportedTransactionVersion: 0
2906
- });
2907
- const depositSlot = txDetails?.slot ?? 0;
2908
- let leafIndex = null;
2909
- let root = null;
2910
- try {
2911
- if (txDetails?.meta?.logMessages) {
2912
- for (const log of txDetails.meta.logMessages) {
2913
- if (log.includes("Program data:")) {
2914
- try {
2915
- const parts = log.split("Program data:");
2916
- if (parts.length > 1) {
2917
- const base64Data = parts[1].trim();
2918
- const eventData = Buffer.from(base64Data, "base64");
2919
- if (eventData.length >= 81 && eventData[0] === 1) {
2920
- const parsedLeafIndex = Number(eventData.readBigUInt64LE(1));
2921
- const loggedCommitment = eventData.slice(9, 41);
2922
- if (Buffer.compare(loggedCommitment, Buffer.from(commitmentBytes)) === 0) {
2923
- leafIndex = parsedLeafIndex;
2924
- const loggedRoot = eventData.slice(49, 81);
2925
- root = Buffer.from(loggedRoot).toString("hex");
2926
- break;
2927
- }
2928
- }
2929
- }
2930
- } catch (e) {
2931
- continue;
2932
- }
2933
- }
2934
- }
2935
- }
2936
- } catch (e) {
2937
- }
2938
- if (leafIndex === null) {
2939
- const programId2 = this.config.programId || CLOAK_PROGRAM_ID;
2940
- const mintForSOL = NATIVE_SOL_MINT;
2941
- const { merkleTree } = getShieldPoolPDAs(programId2, mintForSOL);
2942
- const merkleTreeAccount = await connection.getAccountInfo(merkleTree);
2943
- if (!merkleTreeAccount) {
2944
- throw new Error("Failed to fetch merkle tree account");
2945
- }
2946
- const nextIndex = Number(merkleTreeAccount.data.readBigUInt64LE(32));
2947
- leafIndex = nextIndex - 1;
2948
- const rootBytes = merkleTreeAccount.data.slice(1064, 1096);
2949
- root = Buffer.from(rootBytes).toString("hex");
2950
- }
2951
- if (leafIndex === null || root === null) {
2952
- throw new Error("Failed to get leaf index and root from transaction logs or on-chain state");
2953
- }
2954
- let merkleProof;
2955
- try {
2956
- const programId2 = this.config.programId || CLOAK_PROGRAM_ID;
2957
- const mintForSOL = NATIVE_SOL_MINT;
2958
- const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId2, mintForSOL);
2959
- const chainProof = await computeProofFromChain(connection, merkleTreePDA, leafIndex);
2960
- merkleProof = {
2961
- pathElements: chainProof.pathElements,
2962
- pathIndices: chainProof.pathIndices
2963
- };
2964
- root = chainProof.root;
2965
- } catch (proofError) {
2966
- console.warn("[SDK] Could not compute proof from chain, will fetch at withdrawal:", proofError);
2967
- }
2968
- const updatedNote = updateNoteWithDeposit(note, {
2969
- signature,
2970
- slot: depositSlot,
2971
- leafIndex,
2972
- root,
2973
- merkleProof
2974
- });
2975
- await this.storage.updateNote(note.commitment, {
2976
- depositSignature: signature,
2977
- depositSlot,
2978
- leafIndex,
2979
- root,
2980
- merkleProof
2981
- });
2982
- return {
2983
- note: updatedNote,
2984
- signature,
2985
- leafIndex,
2986
- root
2987
- };
2988
- } catch (error) {
2989
- throw this.wrapError(error, "Deposit failed");
2990
- }
2991
- }
2992
- /**
2993
- * Private transfer with up to 5 recipients
2994
- *
2995
- * Handles the complete private transfer flow:
2996
- * 1. If note is not deposited, deposits it first and waits for confirmation
2997
- * 2. Generates a zero-knowledge proof
2998
- * 3. Submits the withdrawal via relay service to recipients
2999
- *
3000
- * This is the main method for performing private transfers - it handles everything!
3001
- *
3002
- * @param connection - Solana connection (required for deposit if not already deposited)
3003
- * @param payer - Payer wallet (required for deposit if not already deposited)
3004
- * @param note - Note to spend (can be deposited or not)
3005
- * @param recipients - Array of 1-5 recipients with amounts
3006
- * @param options - Optional configuration
3007
- * @returns Transfer result with signature and outputs
3008
- *
3009
- * @example
3010
- * ```typescript
3011
- * // Create a note (not deposited yet)
3012
- * const note = client.generateNote(1_000_000_000);
3013
- *
3014
- * // privateTransfer handles the full flow: deposit + withdraw
3015
- * const result = await client.privateTransfer(
3016
- * connection,
3017
- * wallet,
3018
- * note,
3019
- * [
3020
- * { recipient: new PublicKey("..."), amount: 500_000_000 },
3021
- * { recipient: new PublicKey("..."), amount: 492_500_000 }
3022
- * ],
3023
- * {
3024
- * relayFeeBps: 50, // 0.5%
3025
- * onProgress: (status) => console.log(status),
3026
- * onProofProgress: (pct) => console.log(`Proof: ${pct}%`)
3027
- * }
3028
- * );
3029
- * console.log(`Success! TX: ${result.signature}`);
3030
- * ```
3031
- */
3032
- async privateTransfer(connection, note, recipients, options) {
3033
- if (!isWithdrawable(note)) {
3034
- const depositResult = await this.deposit(connection, note, {
3035
- skipPreflight: false
3036
- });
3037
- note = depositResult.note;
3038
- }
3039
- const protocolFee = note.amount - getDistributableAmount(note.amount);
3040
- const feeBps = Math.ceil(protocolFee * 1e4 / note.amount);
3041
- const distributableAmount = getDistributableAmount(note.amount);
3042
- validateTransfers(recipients, distributableAmount);
3043
- if (!note.leafIndex && note.leafIndex !== 0) {
3044
- throw new Error("Note must have a leaf index (note must be deposited)");
3045
- }
3046
- if (!isValidHex(note.r, 32)) {
3047
- throw new Error("Note r must be 64 hex characters (32 bytes)");
3048
- }
3049
- if (!isValidHex(note.sk_spend, 32)) {
3050
- throw new Error("Note sk_spend must be 64 hex characters (32 bytes)");
3051
- }
3052
- const nullifier = await computeNullifierAsync(note.sk_spend, note.leafIndex);
3053
- const nullifierHex = nullifier.toString(16).padStart(64, "0");
3054
- const outputsHash = await computeOutputsHashAsync(recipients);
3055
- const outputsHashHex = outputsHash.toString(16).padStart(64, "0");
3056
- const circuitsPath2 = await getDefaultCircuitsPath();
3057
- const useDirectProof = await areCircuitsAvailable(circuitsPath2);
3058
- if (!useDirectProof) {
3059
- throw new Error(
3060
- `Circuits not available at ${circuitsPath2}. Make sure circuits are built and accessible. Circuit artifacts are pinned to S3 and must be reachable from this environment.`
3061
- );
3062
- }
3063
- const MAX_PROOF_RETRIES = 3;
3064
- let lastError = null;
3065
- for (let attempt = 0; attempt < MAX_PROOF_RETRIES; attempt++) {
3066
- try {
3067
- if (attempt > 0) {
3068
- options?.onProgress?.(`proof_refresh_${attempt + 1}`);
3069
- await new Promise((resolve) => setTimeout(resolve, 1e3));
3070
- }
3071
- let merkleProof;
3072
- let merkleRoot;
3073
- const hasStoredProof = note.merkleProof && note.root && note.merkleProof.pathElements && note.merkleProof.pathElements.length > 0;
3074
- if (attempt === 0 && hasStoredProof) {
3075
- merkleProof = note.merkleProof;
3076
- merkleRoot = note.root;
3077
- } else {
3078
- const programId = this.config.programId || CLOAK_PROGRAM_ID;
3079
- const mintForSOL = NATIVE_SOL_MINT;
3080
- const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId, mintForSOL);
3081
- const chainProof = await computeProofFromChain(
3082
- connection,
3083
- merkleTreePDA,
3084
- note.leafIndex
3085
- );
3086
- merkleProof = {
3087
- pathElements: chainProof.pathElements,
3088
- pathIndices: chainProof.pathIndices,
3089
- root: chainProof.root
3090
- };
3091
- merkleRoot = chainProof.root;
3092
- note.merkleProof = merkleProof;
3093
- note.root = merkleRoot;
3094
- }
3095
- if (!merkleProof || !merkleRoot) {
3096
- throw new Error("Failed to get Merkle proof from chain");
3097
- }
3098
- if (!merkleProof.pathElements || merkleProof.pathElements.length === 0) {
3099
- throw new Error("Merkle proof is invalid: missing path elements");
3100
- }
3101
- if (merkleProof.pathElements.length !== merkleProof.pathIndices.length) {
3102
- throw new Error("Merkle proof is invalid: path elements and indices length mismatch");
3103
- }
3104
- for (let i = 0; i < merkleProof.pathIndices.length; i++) {
3105
- const idx = merkleProof.pathIndices[i];
3106
- if (idx !== 0 && idx !== 1) {
3107
- throw new Error(`Merkle proof path index at position ${i} must be 0 or 1, got ${idx}`);
3108
- }
3109
- }
3110
- if (!isValidHex(merkleRoot, 32)) {
3111
- throw new Error("Merkle root must be 64 hex characters (32 bytes)");
3112
- }
3113
- for (let i = 0; i < merkleProof.pathElements.length; i++) {
3114
- const element = merkleProof.pathElements[i];
3115
- if (typeof element !== "string" || !isValidHex(element, 32)) {
3116
- throw new Error(`Merkle proof path element at position ${i} must be 64 hex characters (32 bytes)`);
3117
- }
3118
- }
3119
- const sk_spend_bigint = BigInt("0x" + note.sk_spend);
3120
- const r_bigint = BigInt("0x" + note.r);
3121
- const root_bigint = BigInt("0x" + merkleRoot);
3122
- const nullifier_bigint = BigInt("0x" + nullifierHex);
3123
- const outputs_hash_bigint = BigInt("0x" + outputsHashHex);
3124
- const amount_bigint = BigInt(note.amount);
3125
- const pathElements = merkleProof.pathElements.map((p) => BigInt("0x" + p));
3126
- const outAddr = [];
3127
- for (let i = 0; i < 5; i++) {
3128
- if (i < recipients.length) {
3129
- const [lo, hi] = pubkeyToLimbs(recipients[i].recipient.toBytes());
3130
- outAddr.push([lo, hi]);
3131
- } else {
3132
- outAddr.push([0n, 0n]);
3133
- }
3134
- }
3135
- const t = amount_bigint * 3n;
3136
- const varFee = t / 1000n;
3137
- const rem = t % 1000n;
3138
- const outAmount = [];
3139
- const outFlags = [];
3140
- for (let i = 0; i < 5; i++) {
3141
- if (i < recipients.length) {
3142
- outAmount.push(BigInt(recipients[i].amount));
3143
- outFlags.push(1);
3144
- } else {
3145
- outAmount.push(0n);
3146
- outFlags.push(0);
3147
- }
3148
- }
3149
- const sk = splitTo2Limbs(sk_spend_bigint);
3150
- const r = splitTo2Limbs(r_bigint);
3151
- if (attempt === 0) {
3152
- const computedCommitment = await computeCommitment2(amount_bigint, r_bigint, sk_spend_bigint);
3153
- const noteCommitment = BigInt("0x" + note.commitment);
3154
- if (computedCommitment !== noteCommitment) {
3155
- throw new Error(
3156
- `Commitment mismatch! Computed: ${computedCommitment.toString(16)}, Note: ${note.commitment}. This means the note's sk_spend, r, or amount doesn't match what was deposited.`
3157
- );
3158
- }
3159
- }
3160
- const proofInputs = {
3161
- root: root_bigint,
3162
- nullifier: nullifier_bigint,
3163
- outputs_hash: outputs_hash_bigint,
3164
- public_amount: amount_bigint,
3165
- amount: amount_bigint,
3166
- leaf_index: BigInt(note.leafIndex),
3167
- sk,
3168
- r,
3169
- pathElements,
3170
- pathIndices: merkleProof.pathIndices,
3171
- num_outputs: recipients.length,
3172
- out_addr: outAddr,
3173
- out_amount: outAmount,
3174
- out_flags: outFlags,
3175
- var_fee: varFee,
3176
- rem
3177
- };
3178
- options?.onProgress?.("proof_generating");
3179
- const proofResult = await generateWithdrawRegularProof(proofInputs, circuitsPath2);
3180
- options?.onProgress?.("proof_complete");
3181
- const proofHex = Buffer.from(proofResult.proofBytes).toString("hex");
3182
- const finalPublicInputs = {
3183
- root: merkleRoot,
3184
- nf: nullifierHex,
3185
- outputs_hash: outputsHashHex,
3186
- amount: note.amount
3187
- };
3188
- const metadataBundle = options?.metadataBundle;
3189
- const signature = await this.relay.submitWithdraw(
3190
- {
3191
- proof: proofHex,
3192
- publicInputs: finalPublicInputs,
3193
- outputs: recipients.map((r2) => ({
3194
- recipient: r2.recipient.toBase58(),
3195
- amount: r2.amount
3196
- })),
3197
- feeBps,
3198
- // Use calculated protocol fee BPS
3199
- metadataBundle
3200
- },
3201
- options?.onProgress
3202
- );
3203
- return {
3204
- signature,
3205
- outputs: recipients.map((r2) => ({
3206
- recipient: r2.recipient.toBase58(),
3207
- amount: r2.amount
3208
- })),
3209
- nullifier: nullifierHex,
3210
- root: merkleRoot
3211
- };
3212
- } catch (error) {
3213
- lastError = error instanceof Error ? error : new Error(String(error));
3214
- if (error instanceof RootNotFoundError) {
3215
- console.warn(
3216
- `[Cloak SDK] Merkle root expired (attempt ${attempt + 1}/${MAX_PROOF_RETRIES}). Regenerating proof with fresh root...`
3217
- );
3218
- continue;
3219
- }
3220
- throw error;
3221
- }
3222
- }
3223
- throw new Error(
3224
- `Withdrawal failed after ${MAX_PROOF_RETRIES} proof refresh attempts. The Merkle tree is updating too rapidly. Last error: ${lastError?.message}`
3225
- );
3226
- }
3227
- /**
3228
- * Withdraw to a single recipient
3229
- *
3230
- * Convenience method for withdrawing to one address.
3231
- * Handles the complete flow: deposits if needed, then withdraws.
3232
- *
3233
- * @param connection - Solana connection
3234
- * @param payer - Payer wallet
3235
- * @param note - Note to spend
3236
- * @param recipient - Recipient address
3237
- * @param options - Optional configuration
3238
- * @returns Transfer result
3239
- *
3240
- * @example
3241
- * ```typescript
3242
- * const note = client.generateNote(1_000_000_000);
3243
- * const result = await client.withdraw(
3244
- * connection,
3245
- * wallet,
3246
- * note,
3247
- * new PublicKey("..."),
3248
- * { withdrawAll: true }
3249
- * );
3250
- * ```
3251
- */
3252
- async withdraw(connection, note, recipient, options) {
3253
- const withdrawAll = options?.withdrawAll ?? true;
3254
- const amount = withdrawAll ? getDistributableAmount(note.amount) : options?.amount || note.amount;
3255
- if (!withdrawAll && !options?.amount) {
3256
- throw new Error("Must specify amount or set withdrawAll: true");
3257
- }
3258
- return this.privateTransfer(
3259
- connection,
3260
- note,
3261
- [{ recipient, amount }],
3262
- options
3263
- );
3264
- }
3265
- /**
3266
- * Send SOL privately to multiple recipients
3267
- *
3268
- * Convenience method that wraps privateTransfer with a simpler API.
3269
- * Handles the complete flow: deposits if needed, then sends to recipients.
3270
- *
3271
- * @param connection - Solana connection
3272
- * @param note - Note to spend
3273
- * @param recipients - Array of 1-5 recipients with amounts
3274
- * @param options - Optional configuration
3275
- * @returns Transfer result
3276
- *
3277
- * @example
3278
- * ```typescript
3279
- * const note = client.generateNote(1_000_000_000);
3280
- * const result = await client.send(
3281
- * connection,
3282
- * note,
3283
- * [
3284
- * { recipient: new PublicKey("..."), amount: 500_000_000 },
3285
- * { recipient: new PublicKey("..."), amount: 492_500_000 }
3286
- * ]
3287
- * );
3288
- * ```
3289
- */
3290
- async send(connection, note, recipients, options) {
3291
- return this.privateTransfer(connection, note, recipients, options);
3292
- }
3293
- /**
3294
- * Swap SOL for tokens privately
3295
- *
3296
- * Withdraws SOL from a note and swaps it for tokens via the relay service.
3297
- * Handles the complete flow: deposits if needed, generates proof, and submits swap.
3298
- *
3299
- * @param connection - Solana connection
3300
- * @param note - Note to spend
3301
- * @param recipient - Recipient address (will receive tokens)
3302
- * @param options - Swap configuration
3303
- * @returns Swap result with transaction signature
3304
- *
3305
- * @example
3306
- * ```typescript
3307
- * const note = client.generateNote(1_000_000_000);
3308
- * const result = await client.swap(
3309
- * connection,
3310
- * note,
3311
- * new PublicKey("..."), // recipient
3312
- * {
3313
- * outputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
3314
- * slippageBps: 100, // 1%
3315
- * getQuote: async (amount, mint, slippage) => {
3316
- * // Fetch quote from your swap API
3317
- * const quote = await fetchSwapQuote(amount, mint, slippage);
3318
- * return {
3319
- * outAmount: quote.outAmount,
3320
- * minOutputAmount: quote.minOutputAmount
3321
- * };
3322
- * }
3323
- * }
3324
- * );
3325
- * ```
3326
- */
3327
- async swap(connection, note, recipient, options) {
3328
- try {
3329
- if (!isWithdrawable(note)) {
3330
- const depositResult = await this.deposit(connection, note, {
3331
- skipPreflight: false
3332
- });
3333
- note = depositResult.note;
3334
- }
3335
- const variableFee = Math.floor(note.amount * 3 / 1e3);
3336
- const feeBps = note.amount === 0 ? 0 : Math.min(Math.floor((variableFee * 1e4 + note.amount - 1) / note.amount), 65535);
3337
- const withdrawAmountLamports = getDistributableAmount(note.amount);
3338
- if (withdrawAmountLamports <= 0) {
3339
- throw new Error("Amount too small after fees");
3340
- }
3341
- let minOutputAmount;
3342
- if (options.minOutputAmount !== void 0) {
3343
- minOutputAmount = options.minOutputAmount;
3344
- } else if (options.getQuote) {
3345
- const quote = await options.getQuote(
3346
- withdrawAmountLamports,
3347
- options.outputMint,
3348
- options.slippageBps || 100
3349
- );
3350
- minOutputAmount = quote.minOutputAmount;
3351
- } else {
3352
- throw new Error(
3353
- "Must provide either minOutputAmount or getQuote function"
3354
- );
3355
- }
3356
- let recipientAta;
3357
- if (options.recipientAta) {
3358
- recipientAta = new PublicKey4(options.recipientAta);
3359
- } else {
3360
- try {
3361
- const splTokenModule = await import("@solana/spl-token");
3362
- const getAssociatedTokenAddress = splTokenModule.getAssociatedTokenAddress;
3363
- if (!getAssociatedTokenAddress) {
3364
- throw new Error("getAssociatedTokenAddress not found");
3365
- }
3366
- const outputMint2 = new PublicKey4(options.outputMint);
3367
- recipientAta = await getAssociatedTokenAddress(outputMint2, recipient);
3368
- } catch (error) {
3369
- throw new Error(
3370
- `Failed to get associated token account: ${error instanceof Error ? error.message : String(error)}. Please install @solana/spl-token or provide recipientAta in options.`
3371
- );
3372
- }
3373
- }
3374
- if (!note.leafIndex && note.leafIndex !== 0) {
3375
- throw new Error("Note must have a leaf index (note must be deposited)");
3376
- }
3377
- const nullifier = await computeNullifierAsync(note.sk_spend, note.leafIndex);
3378
- const nullifierHex = nullifier.toString(16).padStart(64, "0");
3379
- const inputMint = new PublicKey4("11111111111111111111111111111111");
3380
- const outputMint = new PublicKey4(options.outputMint);
3381
- const outputsHash = await computeSwapOutputsHashAsync(
3382
- inputMint,
3383
- outputMint,
3384
- recipientAta,
3385
- minOutputAmount,
3386
- note.amount
3387
- );
3388
- const outputsHashHex = outputsHash.toString(16).padStart(64, "0");
3389
- const circuitsPath2 = await getDefaultCircuitsPath();
3390
- const useDirectProof = await areCircuitsAvailable(circuitsPath2);
3391
- if (!useDirectProof) {
3392
- throw new Error(
3393
- `Circuits not available at ${circuitsPath2}. Make sure circuits are built and accessible. Circuit artifacts are pinned to S3 and must be reachable from this environment.`
3394
- );
3395
- }
3396
- const MAX_PROOF_RETRIES = 3;
3397
- let lastError = null;
3398
- for (let attempt = 0; attempt < MAX_PROOF_RETRIES; attempt++) {
3399
- try {
3400
- if (attempt > 0) {
3401
- options?.onProgress?.(`proof_refresh_${attempt + 1}`);
3402
- await new Promise((resolve) => setTimeout(resolve, 1e3));
3403
- }
3404
- const programId = this.config.programId || CLOAK_PROGRAM_ID;
3405
- const mintForSOL = NATIVE_SOL_MINT;
3406
- const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId, mintForSOL);
3407
- const chainProof = await computeProofFromChain(connection, merkleTreePDA, note.leafIndex);
3408
- const merkleProof = {
3409
- pathElements: chainProof.pathElements,
3410
- pathIndices: chainProof.pathIndices
3411
- };
3412
- const merkleRoot = chainProof.root;
3413
- if (!merkleRoot) {
3414
- throw new Error("Failed to get Merkle root from chain");
3415
- }
3416
- if (!merkleProof.pathElements || merkleProof.pathElements.length === 0) {
3417
- throw new Error("Merkle proof is invalid: missing path elements");
3418
- }
3419
- const sk_spend_bigint = BigInt("0x" + note.sk_spend);
3420
- const r_bigint = BigInt("0x" + note.r);
3421
- const root_bigint = BigInt("0x" + merkleRoot);
3422
- const nullifier_bigint = BigInt("0x" + nullifierHex);
3423
- const outputs_hash_bigint = BigInt("0x" + outputsHashHex);
3424
- const amount_bigint = BigInt(note.amount);
3425
- const pathElements = merkleProof.pathElements.map((p) => BigInt("0x" + p));
3426
- const inputMintLimbs = pubkeyToLimbs(inputMint.toBytes());
3427
- const outputMintLimbs = pubkeyToLimbs(outputMint.toBytes());
3428
- const recipientAtaLimbs = pubkeyToLimbs(recipientAta.toBytes());
3429
- if (attempt === 0) {
3430
- const computedCommitment = await computeCommitment2(amount_bigint, r_bigint, sk_spend_bigint);
3431
- const noteCommitment = BigInt("0x" + note.commitment);
3432
- if (computedCommitment !== noteCommitment) {
3433
- throw new Error(
3434
- `Commitment mismatch! Computed: ${computedCommitment.toString(16)}, Note: ${note.commitment}. This means the note's sk_spend, r, or amount doesn't match what was deposited.`
3435
- );
3436
- }
3437
- }
3438
- const t = amount_bigint * 3n;
3439
- const varFee = t / 1000n;
3440
- const rem = t % 1000n;
3441
- const proofInputs = {
3442
- sk_spend: sk_spend_bigint,
3443
- r: r_bigint,
3444
- amount: amount_bigint,
3445
- leaf_index: BigInt(note.leafIndex),
3446
- path_elements: pathElements,
3447
- path_indices: merkleProof.pathIndices,
3448
- root: root_bigint,
3449
- nullifier: nullifier_bigint,
3450
- outputs_hash: outputs_hash_bigint,
3451
- public_amount: amount_bigint,
3452
- input_mint: inputMintLimbs,
3453
- output_mint: outputMintLimbs,
3454
- recipient_ata: recipientAtaLimbs,
3455
- min_output_amount: BigInt(minOutputAmount),
3456
- var_fee: varFee,
3457
- rem
3458
- };
3459
- options?.onProgress?.("proof_generating");
3460
- const proofResult = await generateWithdrawSwapProof(proofInputs, circuitsPath2);
3461
- options?.onProgress?.("proof_complete");
3462
- const proofHex = Buffer.from(proofResult.proofBytes).toString("hex");
3463
- const finalPublicInputs = {
3464
- root: merkleRoot,
3465
- nf: nullifierHex,
3466
- outputs_hash: outputsHashHex,
3467
- amount: note.amount
3468
- };
3469
- const metadataBundle = options?.metadataBundle;
3470
- const signature = await this.relay.submitSwap(
3471
- {
3472
- proof: proofHex,
3473
- publicInputs: finalPublicInputs,
3474
- outputs: [
3475
- {
3476
- recipient: recipient.toBase58(),
3477
- amount: withdrawAmountLamports
3478
- }
3479
- ],
3480
- feeBps,
3481
- swap: {
3482
- output_mint: options.outputMint,
3483
- slippage_bps: options.slippageBps || 100,
3484
- min_output_amount: minOutputAmount
3485
- },
3486
- metadataBundle
3487
- },
3488
- options.onProgress
3489
- );
3490
- return {
3491
- signature,
3492
- outputs: [
3493
- {
3494
- recipient: recipient.toBase58(),
3495
- amount: withdrawAmountLamports
3496
- }
3497
- ],
3498
- nullifier: nullifierHex,
3499
- root: merkleRoot,
3500
- outputMint: options.outputMint,
3501
- minOutputAmount
3502
- };
3503
- } catch (error) {
3504
- lastError = error instanceof Error ? error : new Error(String(error));
3505
- if (error instanceof RootNotFoundError) {
3506
- console.warn(
3507
- `[Cloak SDK] Merkle root expired (attempt ${attempt + 1}/${MAX_PROOF_RETRIES}). Regenerating swap proof with fresh root...`
3508
- );
3509
- continue;
3510
- }
3511
- throw error;
3512
- }
3513
- }
3514
- throw new Error(
3515
- `Swap failed after ${MAX_PROOF_RETRIES} proof refresh attempts. The Merkle tree is updating too rapidly. Last error: ${lastError?.message}`
3516
- );
3517
- } catch (error) {
3518
- throw this.wrapError(error, "Swap failed");
3519
- }
3520
- }
3521
- /**
3522
- * Generate a new note without depositing
3523
- *
3524
- * @param amountLamports - Amount for the note
3525
- * @param useWalletKeys - Whether to use wallet keys (v2.0 recommended)
3526
- * @returns New note (not yet deposited)
3527
- */
3528
- async generateNote(amountLamports, useWalletKeys = false) {
3529
- if (useWalletKeys && this.cloakKeys) {
3530
- return await generateNoteFromWallet(amountLamports, this.cloakKeys, this.config.network);
3531
- } else if (useWalletKeys) {
3532
- const keys = generateCloakKeys();
3533
- this.cloakKeys = keys;
3534
- const result = this.storage.saveKeys(keys);
3535
- if (result instanceof Promise) {
3536
- result.catch(() => {
3537
- });
3538
- }
3539
- return await generateNoteFromWallet(amountLamports, keys, this.config.network);
3540
- }
3541
- return await generateNote(amountLamports, this.config.network);
3542
- }
3543
- /**
3544
- * Parse a note from JSON string
3545
- *
3546
- * @param jsonString - JSON representation
3547
- * @returns Parsed note
3548
- */
3549
- parseNote(jsonString) {
3550
- return parseNote2(jsonString);
3551
- }
3552
- /**
3553
- * Export a note to JSON string
3554
- *
3555
- * @param note - Note to export
3556
- * @param pretty - Format with indentation
3557
- * @returns JSON string
3558
- */
3559
- exportNote(note, pretty = false) {
3560
- return pretty ? JSON.stringify(note, null, 2) : JSON.stringify(note);
3561
- }
3562
- /**
3563
- * Check if a note is ready for withdrawal
3564
- *
3565
- * @param note - Note to check
3566
- * @returns True if withdrawable
3567
- */
3568
- isWithdrawable(note) {
3569
- return isWithdrawable(note);
3570
- }
3571
- /**
3572
- * Get Merkle proof for a leaf index directly from on-chain state
3573
- *
3574
- * @param connection - Solana connection
3575
- * @param leafIndex - Leaf index in tree
3576
- * @returns Merkle proof computed from on-chain data
2049
+ * Compute a Merkle proof for `leafIndex` from on-chain state. No relay /
2050
+ * indexer round-trip required.
3577
2051
  */
3578
2052
  async getMerkleProof(connection, leafIndex) {
3579
2053
  const programId = this.config.programId || CLOAK_PROGRAM_ID;
3580
- const mintForSOL = NATIVE_SOL_MINT;
3581
- const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId, mintForSOL);
2054
+ const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId, NATIVE_SOL_MINT);
3582
2055
  const chainProof = await computeProofFromChain(connection, merkleTreePDA, leafIndex);
3583
2056
  return {
3584
- pathElements: chainProof.pathElements,
3585
- pathIndices: chainProof.pathIndices,
3586
- root: chainProof.root
3587
- };
3588
- }
3589
- /**
3590
- * Get current Merkle root directly from on-chain state
3591
- *
3592
- * @param connection - Solana connection
3593
- * @returns Current root hash from on-chain tree
3594
- */
3595
- async getCurrentRoot(connection) {
3596
- const programId = this.config.programId || CLOAK_PROGRAM_ID;
3597
- const mintForSOL = NATIVE_SOL_MINT;
3598
- const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId, mintForSOL);
3599
- const state = await readMerkleTreeState(connection, merkleTreePDA);
3600
- return state.root;
3601
- }
3602
- /**
3603
- * Get transaction status from relay service
3604
- *
3605
- * @param requestId - Request ID from previous submission
3606
- * @returns Current status
3607
- */
3608
- async getTransactionStatus(requestId) {
3609
- return this.relay.getStatus(requestId);
3610
- }
3611
- /**
3612
- * Fetch and decrypt this user's transaction metadata history.
3613
- * DEPRECATED: This functionality is no longer supported
3614
- */
3615
- async getTransactionMetadata(_options) {
3616
- throw new Error("getTransactionMetadata is deprecated - use compliance export API instead");
3617
- }
3618
- /**
3619
- * Load all notes from storage
3620
- *
3621
- * @returns Array of saved notes
3622
- */
3623
- async loadNotes() {
3624
- const notes = this.storage.loadAllNotes();
3625
- return Array.isArray(notes) ? notes : await notes;
3626
- }
3627
- /**
3628
- * Save a note to storage
3629
- *
3630
- * @param note - Note to save
3631
- */
3632
- async saveNote(note) {
3633
- const result = this.storage.saveNote(note);
3634
- if (result instanceof Promise) {
3635
- await result;
3636
- }
2057
+ pathElements: chainProof.pathElements,
2058
+ pathIndices: chainProof.pathIndices,
2059
+ root: chainProof.root
2060
+ };
3637
2061
  }
3638
- /**
3639
- * Find a note by its commitment
3640
- *
3641
- * @param commitment - Commitment hash
3642
- * @returns Note if found
3643
- */
3644
- async findNote(commitment) {
3645
- const notes = await this.loadNotes();
3646
- return findNoteByCommitment(notes, commitment);
2062
+ /** Read the current SOL-pool Merkle root from on-chain state. */
2063
+ async getCurrentRoot(connection) {
2064
+ const programId = this.config.programId || CLOAK_PROGRAM_ID;
2065
+ const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId, NATIVE_SOL_MINT);
2066
+ const state = await readMerkleTreeState(connection, merkleTreePDA);
2067
+ return state.root;
3647
2068
  }
3648
- /**
3649
- * Import wallet keys from JSON
3650
- *
3651
- * @param keysJson - JSON string containing keys
3652
- */
2069
+ /** Poll relay for the status of a previously-submitted request. */
2070
+ async getTransactionStatus(requestId) {
2071
+ return this.relay.getStatus(requestId);
2072
+ }
2073
+ /** Import wallet keys from JSON; persists to the configured storage adapter. */
3653
2074
  async importWalletKeys(keysJson) {
3654
- const keys = importWalletKeys(keysJson);
2075
+ const keys = importKeys(keysJson);
3655
2076
  this.cloakKeys = keys;
3656
2077
  const result = this.storage.saveKeys(keys);
3657
2078
  if (result instanceof Promise) {
@@ -3659,134 +2080,32 @@ var CloakSDK = class {
3659
2080
  }
3660
2081
  }
3661
2082
  /**
3662
- * Export wallet keys to JSON
3663
- *
3664
- * WARNING: This exports secret keys! Store securely.
3665
- *
3666
- * @returns JSON string with keys
2083
+ * Export wallet keys to JSON.
2084
+ *
2085
+ * WARNING: this exports secret keys. Store securely.
3667
2086
  */
3668
2087
  exportWalletKeys() {
3669
2088
  if (!this.cloakKeys) {
3670
2089
  throw new CloakError("No wallet keys available", "wallet", false);
3671
2090
  }
3672
- return exportWalletKeys(this.cloakKeys);
2091
+ return exportKeys(this.cloakKeys);
3673
2092
  }
3674
2093
  /**
3675
- * Get the configuration
2094
+ * Snapshot of the active SDK configuration.
2095
+ *
2096
+ * The return type ({@link CloakConfig}) is structurally narrower than the
2097
+ * constructor input ({@link CloakSDKOptions}): `keypairBytes`, `wallet`,
2098
+ * and `storage` are intentionally absent from the snapshot. The secrets
2099
+ * are consumed into private SDK state at construction time and are not
2100
+ * re-exposable through this method — the type system enforces it. To
2101
+ * sign, pass a `Keypair` / `WalletAdapter` directly to the standalone
2102
+ * `transact` / `partialWithdraw` / `fullWithdraw` / `swapUtxo` helpers.
3676
2103
  */
3677
2104
  getConfig() {
3678
2105
  return { ...this.config };
3679
2106
  }
3680
- // Note: scanNotes was removed - it required the indexer to store encrypted outputs.
3681
- // Users should maintain their own note storage. This is more privacy-preserving.
3682
- /**
3683
- * Wrap errors with better categorization and user-friendly messages
3684
- *
3685
- * @private
3686
- */
3687
- wrapError(error, context) {
3688
- if (error instanceof CloakError) {
3689
- return error;
3690
- }
3691
- const errorMessage = error instanceof Error ? error.message : String(error);
3692
- if (errorMessage.includes("duplicate key") || errorMessage.includes("already deposited")) {
3693
- return new CloakError(
3694
- "This note was already deposited. Generate a new note to continue.",
3695
- "validation",
3696
- false,
3697
- error instanceof Error ? error : void 0
3698
- );
3699
- }
3700
- if (errorMessage.includes("insufficient funds") || errorMessage.includes("insufficient lamports")) {
3701
- return new CloakError(
3702
- "Insufficient funds for this transaction. Please check your wallet balance.",
3703
- "wallet",
3704
- false,
3705
- error instanceof Error ? error : void 0
3706
- );
3707
- }
3708
- if (errorMessage.includes("Merkle tree") || errorMessage.includes("merkle")) {
3709
- return new CloakError(
3710
- "Failed to read on-chain Merkle tree state. Please try again.",
3711
- "network",
3712
- true,
3713
- error instanceof Error ? error : void 0
3714
- );
3715
- }
3716
- if (errorMessage.includes("timeout") || errorMessage.includes("timed out")) {
3717
- return new CloakError(
3718
- "Network timeout. Please check your connection and try again.",
3719
- "network",
3720
- true,
3721
- error instanceof Error ? error : void 0
3722
- );
3723
- }
3724
- if (errorMessage.includes("not connected") || errorMessage.includes("wallet")) {
3725
- return new CloakError(
3726
- "Wallet not connected. Please connect your wallet first.",
3727
- "wallet",
3728
- false,
3729
- error instanceof Error ? error : void 0
3730
- );
3731
- }
3732
- if (errorMessage.includes("proof") && (errorMessage.includes("failed") || errorMessage.includes("error"))) {
3733
- return new CloakError(
3734
- "Zero-knowledge proof generation failed. This is usually temporary - please try again.",
3735
- "prover",
3736
- true,
3737
- error instanceof Error ? error : void 0
3738
- );
3739
- }
3740
- if (errorMessage.includes("relay") || errorMessage.includes("withdraw")) {
3741
- return new CloakError(
3742
- "Relay service error. Please try again later.",
3743
- "relay",
3744
- true,
3745
- error instanceof Error ? error : void 0
3746
- );
3747
- }
3748
- return new CloakError(
3749
- `${context}: ${errorMessage}`,
3750
- "network",
3751
- false,
3752
- error instanceof Error ? error : void 0
3753
- );
3754
- }
3755
2107
  };
3756
2108
 
3757
- // src/core/note.ts
3758
- function serializeNote(note, pretty = false) {
3759
- validateNote(note);
3760
- return pretty ? JSON.stringify(note, null, 2) : JSON.stringify(note);
3761
- }
3762
- function downloadNote(note, filename) {
3763
- const g = globalThis;
3764
- const doc = g?.document;
3765
- const URL_ = g?.URL;
3766
- const Blob_ = g?.Blob;
3767
- if (!doc || !URL_ || !Blob_) {
3768
- throw new Error("downloadNote is only available in browser environments");
3769
- }
3770
- const json = serializeNote(note, true);
3771
- const blob = new Blob_([json], { type: "application/json" });
3772
- const url = URL_.createObjectURL(blob);
3773
- const defaultFilename = `cloak-note-${note.commitment.slice(0, 8)}.json`;
3774
- const link = doc.createElement("a");
3775
- link.href = url;
3776
- link.download = filename || defaultFilename;
3777
- link.click();
3778
- URL_.revokeObjectURL(url);
3779
- }
3780
- async function copyNoteToClipboard(note) {
3781
- const g = globalThis;
3782
- const nav = g?.navigator;
3783
- if (!nav || !nav.clipboard) {
3784
- throw new Error("Clipboard API not available");
3785
- }
3786
- const json = serializeNote(note, true);
3787
- await nav.clipboard.writeText(json);
3788
- }
3789
-
3790
2109
  // src/core/compliance-keys.ts
3791
2110
  import nacl2 from "tweetnacl";
3792
2111
  import { blake3 as blake32 } from "@noble/hashes/blake3";
@@ -4173,6 +2492,164 @@ async function decryptComplianceMetadataWithMasterKey(encrypted, masterComplianc
4173
2492
  return decryptWithSharedSecret(payload, sharedSecret);
4174
2493
  }
4175
2494
 
2495
+ // src/utils/fees.ts
2496
+ var LAMPORTS_PER_SOL = 1e9;
2497
+ var FIXED_FEE_LAMPORTS = 5e6;
2498
+ var VARIABLE_FEE_NUMERATOR = 3;
2499
+ var VARIABLE_FEE_DENOMINATOR = 1e3;
2500
+ var VARIABLE_FEE_RATE = VARIABLE_FEE_NUMERATOR / VARIABLE_FEE_DENOMINATOR;
2501
+ var MIN_DEPOSIT_LAMPORTS = 1e7;
2502
+ function calculateFee(amountLamports) {
2503
+ const variableFee = Math.floor(amountLamports * VARIABLE_FEE_NUMERATOR / VARIABLE_FEE_DENOMINATOR);
2504
+ return FIXED_FEE_LAMPORTS + variableFee;
2505
+ }
2506
+ function calculateFeeBigint(amountLamports) {
2507
+ const fixed = BigInt(FIXED_FEE_LAMPORTS);
2508
+ const variable = amountLamports * BigInt(VARIABLE_FEE_NUMERATOR) / BigInt(VARIABLE_FEE_DENOMINATOR);
2509
+ return fixed + variable;
2510
+ }
2511
+ function isWithdrawAmountSufficient(amountLamports) {
2512
+ return amountLamports > calculateFeeBigint(amountLamports);
2513
+ }
2514
+ function getDistributableAmount(amountLamports) {
2515
+ return amountLamports - calculateFee(amountLamports);
2516
+ }
2517
+ function formatAmount(lamports, decimals = 9) {
2518
+ return (lamports / LAMPORTS_PER_SOL).toFixed(decimals);
2519
+ }
2520
+ function parseAmount(sol) {
2521
+ const num = parseFloat(sol);
2522
+ if (isNaN(num) || num < 0) {
2523
+ throw new Error(`Invalid SOL amount: ${sol}`);
2524
+ }
2525
+ return Math.floor(num * LAMPORTS_PER_SOL);
2526
+ }
2527
+ function validateOutputsSum(outputs, expectedTotal) {
2528
+ const sum = outputs.reduce((acc, out) => acc + out.amount, 0);
2529
+ return sum === expectedTotal;
2530
+ }
2531
+ function calculateRelayFee(amountLamports, feeBps) {
2532
+ if (feeBps < 0 || feeBps > 1e4) {
2533
+ throw new Error("Fee basis points must be between 0 and 10000");
2534
+ }
2535
+ return Math.floor(amountLamports * feeBps / 1e4);
2536
+ }
2537
+
2538
+ // src/utils/validation.ts
2539
+ import { PublicKey as PublicKey3 } from "@solana/web3.js";
2540
+ function isValidSolanaAddress(address) {
2541
+ try {
2542
+ new PublicKey3(address);
2543
+ return true;
2544
+ } catch {
2545
+ return false;
2546
+ }
2547
+ }
2548
+
2549
+ // src/utils/network.ts
2550
+ function detectNetworkFromRpcUrl(rpcUrl) {
2551
+ const url = rpcUrl || process.env.NEXT_PUBLIC_SOLANA_RPC_URL || "";
2552
+ const lowerUrl = url.toLowerCase();
2553
+ if (lowerUrl.includes("mainnet") || lowerUrl.includes("api.mainnet-beta") || lowerUrl.includes("mainnet-beta")) {
2554
+ return "mainnet";
2555
+ }
2556
+ if (lowerUrl.includes("testnet") || lowerUrl.includes("api.testnet")) {
2557
+ return "testnet";
2558
+ }
2559
+ if (lowerUrl.includes("devnet") || lowerUrl.includes("api.devnet")) {
2560
+ return "devnet";
2561
+ }
2562
+ if (lowerUrl.includes("localhost") || lowerUrl.includes("127.0.0.1") || lowerUrl.includes("local")) {
2563
+ return "localnet";
2564
+ }
2565
+ return "mainnet";
2566
+ }
2567
+ function getRpcUrlForNetwork(network) {
2568
+ switch (network) {
2569
+ case "mainnet":
2570
+ return "https://api.mainnet-beta.solana.com";
2571
+ case "devnet":
2572
+ return "https://api.devnet.solana.com";
2573
+ case "localnet":
2574
+ return "http://localhost:8899";
2575
+ default:
2576
+ return "https://api.mainnet-beta.solana.com";
2577
+ }
2578
+ }
2579
+ function isValidRpcUrl(url) {
2580
+ try {
2581
+ const parsed = new URL(url);
2582
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
2583
+ } catch {
2584
+ return false;
2585
+ }
2586
+ }
2587
+ function getExplorerUrl(signature, network = "devnet") {
2588
+ const cluster = network === "mainnet" ? "" : `?cluster=${network}`;
2589
+ return `https://explorer.solana.com/tx/${signature}${cluster}`;
2590
+ }
2591
+ function getAddressExplorerUrl(address, network = "devnet") {
2592
+ const cluster = network === "mainnet" ? "" : `?cluster=${network}`;
2593
+ return `https://explorer.solana.com/address/${address}${cluster}`;
2594
+ }
2595
+
2596
+ // src/utils/verify-utxos.ts
2597
+ async function verifyUtxos(utxos, connection, programId, commitment = "confirmed") {
2598
+ const checkable = [];
2599
+ const skipped = [];
2600
+ for (const utxo of utxos) {
2601
+ if (utxo.commitment === void 0 || utxo.mintAddress === void 0 || utxo.keypair === void 0 || utxo.keypair.privateKey === void 0) {
2602
+ skipped.push(utxo);
2603
+ continue;
2604
+ }
2605
+ try {
2606
+ const nullifierBig = await computeNullifier(utxo);
2607
+ const nullifierHex = nullifierBig.toString(16).padStart(64, "0");
2608
+ const nullifierBytes = Buffer.from(nullifierHex, "hex");
2609
+ const { pool } = getShieldPoolPDAs(programId, utxo.mintAddress);
2610
+ const [pda] = getNullifierPDA(pool, nullifierBytes, programId);
2611
+ checkable.push({ utxo, pda });
2612
+ } catch {
2613
+ skipped.push(utxo);
2614
+ }
2615
+ }
2616
+ if (checkable.length === 0) {
2617
+ return { spent: [], unspent: [], skipped };
2618
+ }
2619
+ const CHUNK = 100;
2620
+ const infos = [];
2621
+ for (let i = 0; i < checkable.length; i += CHUNK) {
2622
+ const page = checkable.slice(i, i + CHUNK).map((e) => e.pda);
2623
+ const pageInfos = await connection.getMultipleAccountsInfo(page, commitment);
2624
+ infos.push(...pageInfos);
2625
+ }
2626
+ const spent = [];
2627
+ const unspent = [];
2628
+ for (let i = 0; i < checkable.length; i++) {
2629
+ if (infos[i] !== null) {
2630
+ spent.push(checkable[i].utxo);
2631
+ } else {
2632
+ unspent.push(checkable[i].utxo);
2633
+ }
2634
+ }
2635
+ return { spent, unspent, skipped };
2636
+ }
2637
+ async function preflightNullifiers(utxos, connection, programId, commitment = "confirmed") {
2638
+ if (utxos.length === 0) return;
2639
+ const result = await verifyUtxos(utxos, connection, programId, commitment);
2640
+ if (result.spent.length > 0) {
2641
+ const allCommitments = result.spent.map(
2642
+ (u) => u.commitment !== void 0 ? u.commitment.toString(16).padStart(64, "0") : "<unknown>"
2643
+ );
2644
+ const preview = allCommitments.slice(0, 5);
2645
+ const extra = allCommitments.length > preview.length ? ` (+${allCommitments.length - preview.length} more)` : "";
2646
+ throw new UtxoAlreadySpentError(
2647
+ `Pre-flight detected ${allCommitments.length} already-spent UTXO(s). First spent commitment(s): ${preview.join(", ")}${extra}. Call verifyUtxos() and remove these from local state before retrying.`,
2648
+ allCommitments
2649
+ );
2650
+ }
2651
+ }
2652
+
4176
2653
  // src/services/risk-oracle.ts
4177
2654
  function buildRangeRiskScoreJob(OracleJob, walletAddress) {
4178
2655
  return OracleJob.fromObject({
@@ -4244,45 +2721,6 @@ async function fetchRiskQuoteIx(connection, wallet, rangeApiKey) {
4244
2721
  };
4245
2722
  }
4246
2723
 
4247
- // src/helpers/encrypted-output.ts
4248
- function prepareEncryptedOutput(note, cloakKeys) {
4249
- const noteData = {
4250
- amount: note.amount,
4251
- r: note.r,
4252
- sk_spend: note.sk_spend,
4253
- commitment: note.commitment
4254
- };
4255
- const encrypted = encryptNoteForRecipient(noteData, cloakKeys.view.pvk);
4256
- return btoa(JSON.stringify(encrypted));
4257
- }
4258
- function prepareEncryptedOutputForRecipient(note, recipientPvkHex) {
4259
- const noteData = {
4260
- amount: note.amount,
4261
- r: note.r,
4262
- sk_spend: note.sk_spend,
4263
- commitment: note.commitment
4264
- };
4265
- const recipientPvk = hexToBytes(recipientPvkHex);
4266
- const encrypted = encryptNoteForRecipient(noteData, recipientPvk);
4267
- return btoa(JSON.stringify(encrypted));
4268
- }
4269
- function encodeNoteSimple(note) {
4270
- const data = {
4271
- amount: note.amount,
4272
- r: note.r,
4273
- sk_spend: note.sk_spend,
4274
- commitment: note.commitment
4275
- };
4276
- const json = JSON.stringify(data);
4277
- if (typeof Buffer !== "undefined") {
4278
- return Buffer.from(json).toString("base64");
4279
- } else if (typeof btoa !== "undefined") {
4280
- return btoa(json);
4281
- } else {
4282
- throw new Error("No base64 encoding method available");
4283
- }
4284
- }
4285
-
4286
2724
  // src/helpers/wallet-integration.ts
4287
2725
  import {
4288
2726
  Keypair as Keypair2
@@ -4521,10 +2959,15 @@ import _bs58 from "bs58";
4521
2959
  var bs58 = _bs58.default || _bs58;
4522
2960
  var TRANSACT_TAG = 0;
4523
2961
  var TRANSACT_SWAP_TAG = 1;
4524
- var DEPOSIT_TAG = 1;
4525
2962
  var PROOF_LEN = 256;
4526
2963
  var PUBLIC_INPUTS_LEN = 264;
4527
2964
  var COMMITMENTS_OFFSET = 168;
2965
+ function isAllZero(b) {
2966
+ for (let i = 0; i < b.length; i++) {
2967
+ if (b[i] !== 0) return false;
2968
+ }
2969
+ return true;
2970
+ }
4528
2971
  var DEFAULT_TIMEOUT_MS = 3e4;
4529
2972
  var DEFAULT_MAX_RETRIES = 3;
4530
2973
  async function fetchWithRetry(url, options = {}) {
@@ -4662,24 +3105,20 @@ async function buildMerkleTreeFromRelay(relayUrl, options) {
4662
3105
  function extractCommitmentsFromInstruction(data) {
4663
3106
  if (data.length === 0) return [];
4664
3107
  const tag = data[0];
4665
- if (tag === TRANSACT_TAG || tag === TRANSACT_SWAP_TAG) {
4666
- const publicInputsStart = 1 + PROOF_LEN;
4667
- const publicInputsEnd = publicInputsStart + PUBLIC_INPUTS_LEN;
4668
- if (data.length < publicInputsEnd) return [];
4669
- const publicInputs = data.slice(publicInputsStart, publicInputsEnd);
4670
- const commitments = [];
4671
- const c0 = publicInputs.slice(COMMITMENTS_OFFSET, COMMITMENTS_OFFSET + 32);
4672
- const c1 = publicInputs.slice(COMMITMENTS_OFFSET + 32, COMMITMENTS_OFFSET + 64);
4673
- commitments.push(c0);
4674
- commitments.push(c1);
4675
- return commitments;
4676
- }
4677
- if (tag === DEPOSIT_TAG) {
4678
- if (data.length < 1 + 8 + 32) return [];
4679
- const commitment = data.slice(1 + 8, 1 + 8 + 32);
4680
- return [commitment];
4681
- }
4682
- return [];
3108
+ if (tag !== TRANSACT_TAG && tag !== TRANSACT_SWAP_TAG) return [];
3109
+ const publicInputsStart = 1 + PROOF_LEN;
3110
+ const publicInputsEnd = publicInputsStart + PUBLIC_INPUTS_LEN;
3111
+ if (data.length < publicInputsEnd) return [];
3112
+ const publicInputs = data.slice(publicInputsStart, publicInputsEnd);
3113
+ const c0 = publicInputs.slice(COMMITMENTS_OFFSET, COMMITMENTS_OFFSET + 32);
3114
+ const c1 = publicInputs.slice(
3115
+ COMMITMENTS_OFFSET + 32,
3116
+ COMMITMENTS_OFFSET + 64
3117
+ );
3118
+ const out = [];
3119
+ if (!isAllZero(c0)) out.push(c0);
3120
+ if (!isAllZero(c1)) out.push(c1);
3121
+ return out;
4683
3122
  }
4684
3123
  function isBrowser() {
4685
3124
  return typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
@@ -4793,181 +3232,14 @@ async function preflightCheck(relayUrl, rootHex) {
4793
3232
  };
4794
3233
  }
4795
3234
 
4796
- // src/utils/pending-operations.ts
4797
- var PENDING_DEPOSITS_KEY = "cloak_pending_deposits";
4798
- var PENDING_WITHDRAWALS_KEY = "cloak_pending_withdrawals";
4799
- function getStorage() {
4800
- if (typeof globalThis !== "undefined" && globalThis.localStorage) {
4801
- return globalThis.localStorage;
4802
- }
4803
- return null;
4804
- }
4805
- function savePendingDeposit(deposit) {
4806
- const storage = getStorage();
4807
- if (!storage) {
4808
- console.warn("localStorage not available - pending deposit not persisted");
4809
- return;
4810
- }
4811
- const deposits = loadPendingDeposits();
4812
- const index = deposits.findIndex((d) => d.note.commitment === deposit.note.commitment);
4813
- if (index >= 0) {
4814
- deposits[index] = deposit;
4815
- } else {
4816
- deposits.push(deposit);
4817
- }
4818
- storage.setItem(PENDING_DEPOSITS_KEY, JSON.stringify(deposits));
4819
- }
4820
- function loadPendingDeposits() {
4821
- const storage = getStorage();
4822
- if (!storage) return [];
4823
- const stored = storage.getItem(PENDING_DEPOSITS_KEY);
4824
- if (!stored) return [];
4825
- try {
4826
- return JSON.parse(stored);
4827
- } catch {
4828
- return [];
4829
- }
4830
- }
4831
- function updatePendingDeposit(commitment, updates) {
4832
- const storage = getStorage();
4833
- if (!storage) return;
4834
- const deposits = loadPendingDeposits();
4835
- const index = deposits.findIndex((d) => d.note.commitment === commitment);
4836
- if (index >= 0) {
4837
- deposits[index] = { ...deposits[index], ...updates };
4838
- storage.setItem(PENDING_DEPOSITS_KEY, JSON.stringify(deposits));
4839
- }
4840
- }
4841
- function removePendingDeposit(commitment) {
4842
- const storage = getStorage();
4843
- if (!storage) return;
4844
- const deposits = loadPendingDeposits();
4845
- const filtered = deposits.filter((d) => d.note.commitment !== commitment);
4846
- storage.setItem(PENDING_DEPOSITS_KEY, JSON.stringify(filtered));
4847
- }
4848
- function clearPendingDeposits() {
4849
- const storage = getStorage();
4850
- if (storage) {
4851
- storage.removeItem(PENDING_DEPOSITS_KEY);
4852
- }
4853
- }
4854
- function savePendingWithdrawal(withdrawal) {
4855
- const storage = getStorage();
4856
- if (!storage) {
4857
- console.warn("localStorage not available - pending withdrawal not persisted");
4858
- return;
4859
- }
4860
- const withdrawals = loadPendingWithdrawals();
4861
- const index = withdrawals.findIndex((w) => w.requestId === withdrawal.requestId);
4862
- if (index >= 0) {
4863
- withdrawals[index] = withdrawal;
4864
- } else {
4865
- withdrawals.push(withdrawal);
4866
- }
4867
- storage.setItem(PENDING_WITHDRAWALS_KEY, JSON.stringify(withdrawals));
4868
- }
4869
- function loadPendingWithdrawals() {
4870
- const storage = getStorage();
4871
- if (!storage) return [];
4872
- const stored = storage.getItem(PENDING_WITHDRAWALS_KEY);
4873
- if (!stored) return [];
4874
- try {
4875
- return JSON.parse(stored);
4876
- } catch {
4877
- return [];
4878
- }
4879
- }
4880
- function updatePendingWithdrawal(requestId, updates) {
4881
- const storage = getStorage();
4882
- if (!storage) return;
4883
- const withdrawals = loadPendingWithdrawals();
4884
- const index = withdrawals.findIndex((w) => w.requestId === requestId);
4885
- if (index >= 0) {
4886
- withdrawals[index] = { ...withdrawals[index], ...updates };
4887
- storage.setItem(PENDING_WITHDRAWALS_KEY, JSON.stringify(withdrawals));
4888
- }
4889
- }
4890
- function removePendingWithdrawal(requestId) {
4891
- const storage = getStorage();
4892
- if (!storage) return;
4893
- const withdrawals = loadPendingWithdrawals();
4894
- const filtered = withdrawals.filter((w) => w.requestId !== requestId);
4895
- storage.setItem(PENDING_WITHDRAWALS_KEY, JSON.stringify(filtered));
4896
- }
4897
- function clearPendingWithdrawals() {
4898
- const storage = getStorage();
4899
- if (storage) {
4900
- storage.removeItem(PENDING_WITHDRAWALS_KEY);
4901
- }
4902
- }
4903
- function hasPendingOperations() {
4904
- const deposits = loadPendingDeposits();
4905
- const withdrawals = loadPendingWithdrawals();
4906
- const pendingDeposits = deposits.filter(
4907
- (d) => d.status === "pending" || d.status === "tx_sent"
4908
- );
4909
- const pendingWithdrawals = withdrawals.filter(
4910
- (w) => w.status === "pending" || w.status === "processing"
4911
- );
4912
- return pendingDeposits.length > 0 || pendingWithdrawals.length > 0;
4913
- }
4914
- function getPendingOperationsSummary() {
4915
- const deposits = loadPendingDeposits().filter(
4916
- (d) => d.status === "pending" || d.status === "tx_sent"
4917
- );
4918
- const withdrawals = loadPendingWithdrawals().filter(
4919
- (w) => w.status === "pending" || w.status === "processing"
4920
- );
4921
- return {
4922
- deposits,
4923
- withdrawals,
4924
- totalPending: deposits.length + withdrawals.length
4925
- };
4926
- }
4927
- function cleanupStalePendingOperations(maxAgeMs = 24 * 60 * 60 * 1e3) {
4928
- const now = Date.now();
4929
- let removedDeposits = 0;
4930
- let removedWithdrawals = 0;
4931
- const deposits = loadPendingDeposits();
4932
- const activeDeposits = deposits.filter((d) => {
4933
- const age = now - d.startedAt;
4934
- const isStale = age > maxAgeMs;
4935
- const isTerminal = d.status === "confirmed" || d.status === "failed";
4936
- if (isStale || isTerminal) {
4937
- removedDeposits++;
4938
- return false;
4939
- }
4940
- return true;
4941
- });
4942
- const storage = getStorage();
4943
- if (storage) {
4944
- storage.setItem(PENDING_DEPOSITS_KEY, JSON.stringify(activeDeposits));
4945
- }
4946
- const withdrawals = loadPendingWithdrawals();
4947
- const activeWithdrawals = withdrawals.filter((w) => {
4948
- const age = now - w.startedAt;
4949
- const isStale = age > maxAgeMs;
4950
- const isTerminal = w.status === "completed" || w.status === "failed";
4951
- if (isStale || isTerminal) {
4952
- removedWithdrawals++;
4953
- return false;
4954
- }
4955
- return true;
4956
- });
4957
- if (storage) {
4958
- storage.setItem(PENDING_WITHDRAWALS_KEY, JSON.stringify(activeWithdrawals));
4959
- }
4960
- return { removedDeposits, removedWithdrawals };
4961
- }
4962
-
4963
3235
  // src/core/transact.ts
4964
3236
  import {
4965
- PublicKey as PublicKey6,
4966
- Transaction as Transaction3,
4967
- TransactionInstruction as TransactionInstruction3,
3237
+ PublicKey as PublicKey5,
3238
+ Transaction as Transaction2,
3239
+ TransactionInstruction,
4968
3240
  sendAndConfirmTransaction,
4969
- SystemProgram as SystemProgram2,
4970
- ComputeBudgetProgram as ComputeBudgetProgram2,
3241
+ SystemProgram,
3242
+ ComputeBudgetProgram,
4971
3243
  SYSVAR_INSTRUCTIONS_PUBKEY,
4972
3244
  SYSVAR_SLOT_HASHES_PUBKEY,
4973
3245
  VersionedTransaction,
@@ -4980,7 +3252,7 @@ import {
4980
3252
  getAssociatedTokenAddressSync,
4981
3253
  TOKEN_PROGRAM_ID
4982
3254
  } from "@solana/spl-token";
4983
- import * as snarkjs2 from "snarkjs";
3255
+ import * as snarkjs from "snarkjs";
4984
3256
 
4985
3257
  // src/core/chain-note.ts
4986
3258
  var NOTE_VERSION = 2;
@@ -5130,7 +3402,7 @@ function chainNoteFromBase64(base64) {
5130
3402
  // src/core/transact.ts
5131
3403
  import { buildPoseidon as buildPoseidon3 } from "circomlibjs";
5132
3404
  import nacl4 from "tweetnacl";
5133
- var IS_BROWSER2 = typeof window !== "undefined" || typeof globalThis.document !== "undefined";
3405
+ var IS_BROWSER = typeof window !== "undefined" || typeof globalThis.document !== "undefined";
5134
3406
  var DEFAULT_TRANSACTION_CIRCUITS_URL = "https://cloak-circuits.s3.us-east-1.amazonaws.com/circuits/0.1.0";
5135
3407
  var DEFAULT_CLOAK_RELAY_URL = "https://api.cloak.ag";
5136
3408
  function resolveDefaultTransactionCircuitsPath() {
@@ -5143,7 +3415,7 @@ async function resolveTransactionCircuitFiles() {
5143
3415
  const baseNorm = base.endsWith("/") ? base : `${base}/`;
5144
3416
  const wasmUrl = `${baseNorm}transaction_js/transaction.wasm`;
5145
3417
  const zkeyUrl = `${baseNorm}transaction_final.zkey`;
5146
- if (IS_BROWSER2) {
3418
+ if (IS_BROWSER) {
5147
3419
  return { wasmPath: wasmUrl, zkeyPath: zkeyUrl };
5148
3420
  }
5149
3421
  if (_remoteTxCircuitsCache?.base === baseNorm) {
@@ -5280,7 +3552,7 @@ async function resolveAddressLookupTableAccounts(connection, relayUrl, altAddres
5280
3552
  const fetched = await Promise.all(
5281
3553
  altAddresses.map(async (addr) => {
5282
3554
  try {
5283
- const pubkey = new PublicKey6(addr);
3555
+ const pubkey = new PublicKey5(addr);
5284
3556
  const result = await connection.getAddressLookupTable(pubkey);
5285
3557
  return result.value ?? null;
5286
3558
  } catch {
@@ -5442,7 +3714,7 @@ async function generateTransactionProof(inputs, onProgress) {
5442
3714
  onProgress?.(10);
5443
3715
  const { wasmPath: wasmInput, zkeyPath: zkeyInput } = await resolveTransactionCircuitFiles();
5444
3716
  onProgress?.(30);
5445
- const { proof, publicSignals } = await snarkjs2.groth16.fullProve(
3717
+ const { proof, publicSignals } = await snarkjs.groth16.fullProve(
5446
3718
  inputs,
5447
3719
  wasmInput,
5448
3720
  zkeyInput
@@ -5522,7 +3794,7 @@ function buildPublicInputsBytesFromSignals(publicSignals) {
5522
3794
  }
5523
3795
  var TRANSACT_DISCRIMINATOR = 0;
5524
3796
  function deriveNullifierPDA(programId, poolPDA, nullifier) {
5525
- return PublicKey6.findProgramAddressSync(
3797
+ return PublicKey5.findProgramAddressSync(
5526
3798
  [Buffer.from("nullifier"), poolPDA.toBuffer(), Buffer.from(nullifier)],
5527
3799
  programId
5528
3800
  );
@@ -5565,7 +3837,7 @@ function buildTransactInstruction(programId, payer, poolPDA, treasuryPDA, merkle
5565
3837
  // 4. nullifier PDA 0 (writable)
5566
3838
  { pubkey: nullifierPDA1, isSigner: false, isWritable: true },
5567
3839
  // 5. nullifier PDA 1 (writable)
5568
- { pubkey: SystemProgram2.programId, isSigner: false, isWritable: false }
3840
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
5569
3841
  // 6. system_program
5570
3842
  ];
5571
3843
  if (splAccounts) {
@@ -5590,7 +3862,7 @@ function buildTransactInstruction(programId, payer, poolPDA, treasuryPDA, merkle
5590
3862
  accounts.push({ pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false });
5591
3863
  }
5592
3864
  }
5593
- return new TransactionInstruction3({
3865
+ return new TransactionInstruction({
5594
3866
  programId,
5595
3867
  keys: accounts,
5596
3868
  data
@@ -5598,10 +3870,10 @@ function buildTransactInstruction(programId, payer, poolPDA, treasuryPDA, merkle
5598
3870
  }
5599
3871
  function getCommonALTAddresses() {
5600
3872
  return [
5601
- SystemProgram2.programId,
3873
+ SystemProgram.programId,
5602
3874
  SYSVAR_SLOT_HASHES_PUBKEY,
5603
3875
  SYSVAR_INSTRUCTIONS_PUBKEY,
5604
- ComputeBudgetProgram2.programId
3876
+ ComputeBudgetProgram.programId
5605
3877
  ];
5606
3878
  }
5607
3879
  function dedupePubkeys(addresses) {
@@ -5662,7 +3934,7 @@ async function createEphemeralALT(connection, depositor, onProgress, additionalA
5662
3934
  lookupTable: altAddress,
5663
3935
  addresses: addressesForCreate
5664
3936
  });
5665
- const tx = new Transaction3().add(createIx).add(extendIx);
3937
+ const tx = new Transaction2().add(createIx).add(extendIx);
5666
3938
  const { blockhash } = await connection.getLatestBlockhash();
5667
3939
  tx.recentBlockhash = blockhash;
5668
3940
  tx.feePayer = depositor.publicKey;
@@ -5828,8 +4100,8 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
5828
4100
  if (riskQuoteIx) {
5829
4101
  baseInstructions.unshift(riskQuoteIx);
5830
4102
  }
5831
- const cuLimitIx = ComputeBudgetProgram2.setComputeUnitLimit({ units: 4e5 });
5832
- const cuPriceIx = ComputeBudgetProgram2.setComputeUnitPrice({ microLamports: 1e5 });
4103
+ const cuLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 4e5 });
4104
+ const cuPriceIx = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1e5 });
5833
4105
  const fullInstructions = [...baseInstructions, cuLimitIx, cuPriceIx, transactIx];
5834
4106
  const compactInstructions = [...baseInstructions, cuLimitIx, transactIx];
5835
4107
  const minimalInstructions = [...baseInstructions, transactIx];
@@ -6038,7 +4310,7 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
6038
4310
  let legacyTransportAttempt = 0;
6039
4311
  while (true) {
6040
4312
  try {
6041
- const tx = new Transaction3();
4313
+ const tx = new Transaction2();
6042
4314
  for (const ix of instructions) {
6043
4315
  tx.add(ix);
6044
4316
  }
@@ -6220,7 +4492,7 @@ async function fetchRiskQuoteInstruction(riskQuoteUrl, wallet, options) {
6220
4492
  const signerB58 = signerKey;
6221
4493
  const signatureBuf = Buffer.from(signatureB64, "base64");
6222
4494
  const messageArr = hexToBytes2(messageHex);
6223
- const publicKeyBytes = new PublicKey6(signerB58).toBytes();
4495
+ const publicKeyBytes = new PublicKey5(signerB58).toBytes();
6224
4496
  if (signatureBuf.length !== 64 || messageArr.length !== 33 && messageArr.length !== 41 && messageArr.length !== 50 || publicKeyBytes.length !== 32) {
6225
4497
  throw new Error(
6226
4498
  `Invalid risk quote response: invalid lengths (sig=${signatureBuf.length}, msg=${messageArr.length}, pk=${publicKeyBytes.length})`
@@ -6242,10 +4514,10 @@ async function fetchRiskQuoteInstruction(riskQuoteUrl, wallet, options) {
6242
4514
  `Invalid risk quote response: expected relay format { signature, message, signer_pubkey } or { instruction }. Received keys: ${keys}`
6243
4515
  );
6244
4516
  }
6245
- return new TransactionInstruction3({
6246
- programId: new PublicKey6(raw.programId),
4517
+ return new TransactionInstruction({
4518
+ programId: new PublicKey5(raw.programId),
6247
4519
  keys: raw.keys.map((k) => ({
6248
- pubkey: new PublicKey6(k.pubkey),
4520
+ pubkey: new PublicKey5(k.pubkey),
6249
4521
  isSigner: k.isSigner,
6250
4522
  isWritable: k.isWritable
6251
4523
  })),
@@ -6299,6 +4571,7 @@ async function transact(params, options) {
6299
4571
  if (outputUtxos.length > 2) {
6300
4572
  throw new Error("Maximum 2 output UTXOs allowed");
6301
4573
  }
4574
+ await preflightNullifiers(inputUtxos, connection, programId);
6302
4575
  const mint = inputUtxos.find((u) => u.amount > 0)?.mintAddress ?? outputUtxos.find((u) => u.amount > 0)?.mintAddress ?? NATIVE_SOL_MINT;
6303
4576
  const paddedInputs = [...inputUtxos];
6304
4577
  while (paddedInputs.length < 2) {
@@ -7126,10 +5399,46 @@ async function transact(params, options) {
7126
5399
  }
7127
5400
  }
7128
5401
  if (lastError) {
5402
+ const classified = classifyRelayError(lastError.message);
5403
+ const isMerkleClass = !(classified instanceof UtxoAlreadySpentError) && !(classified instanceof SanctionsQuoteError) && !isRootNotFoundError2(lastError);
5404
+ if (!IS_BROWSER && isMerkleClass && relayUrl) {
5405
+ onProgress?.(
5406
+ "All relay-tree attempts failed. Rebuilding merkle tree from chain as last resort..."
5407
+ );
5408
+ try {
5409
+ const [merkleTreePda] = PublicKey5.findProgramAddressSync(
5410
+ [Buffer.from("merkle_tree"), mint.toBuffer()],
5411
+ programId
5412
+ );
5413
+ const chainTree = await buildMerkleTreeFromChain(
5414
+ connection,
5415
+ programId,
5416
+ merkleTreePda,
5417
+ onProgress
5418
+ );
5419
+ const chainRootHex = chainTree.root().toString(16).padStart(64, "0");
5420
+ onProgress?.(
5421
+ `Chain-rebuilt tree root: ${chainRootHex.substring(0, 16)}... \u2014 attached to error for caller retry`
5422
+ );
5423
+ throw new RelayInternalError(
5424
+ `All relay-tree attempts failed. Chain-rebuilt tree available (root ${chainRootHex.substring(0, 16)}...). Retry this call with options.cachedMerkleTree = err.cachedTreeFromChain. Original error: ${lastError.message}`,
5425
+ lastError.message,
5426
+ void 0,
5427
+ chainTree
5428
+ );
5429
+ } catch (e) {
5430
+ if (e instanceof RelayInternalError) throw e;
5431
+ onProgress?.(
5432
+ `Chain-replay fallback failed: ${e instanceof Error ? e.message : String(e)}`
5433
+ );
5434
+ }
5435
+ }
7129
5436
  if (isRootNotFoundError2(lastError)) {
7130
- throw new Error(`Transaction failed after ${maxRootRetries + 1} retries: ${lastError.message}`);
5437
+ throw new Error(
5438
+ `Transaction failed after ${maxRootRetries + 1} retries: ${lastError.message}`
5439
+ );
7131
5440
  }
7132
- throw new Error(`Transaction failed: ${lastError.message}`);
5441
+ throw classified;
7133
5442
  }
7134
5443
  if (relayUrl && outputCommitments.length >= 2) {
7135
5444
  const hasOnChainIndices = commitmentIndices[0] >= 0 && commitmentIndices[1] >= 0;
@@ -7433,6 +5742,7 @@ async function swapUtxo(params, options) {
7433
5742
  if (inputUtxos.length > 2) {
7434
5743
  throw new Error("Maximum 2 input UTXOs allowed");
7435
5744
  }
5745
+ await preflightNullifiers(inputUtxos, connection, programId);
7436
5746
  const outputMintAccount = await connection.getAccountInfo(outputMint, {
7437
5747
  commitment: "confirmed"
7438
5748
  });
@@ -8165,7 +6475,7 @@ async function swapWithChange(inputUtxos, swapAmount, outputMint, recipientAta,
8165
6475
  // src/core/scanner.ts
8166
6476
  import _bs582 from "bs58";
8167
6477
  import {
8168
- PublicKey as PublicKey7
6478
+ PublicKey as PublicKey6
8169
6479
  } from "@solana/web3.js";
8170
6480
  import { getAssociatedTokenAddressSync as getAssociatedTokenAddressSync2 } from "@solana/spl-token";
8171
6481
  var bs582 = _bs582.default || _bs582;
@@ -8253,7 +6563,7 @@ function parseSwapOutputMint(data) {
8253
6563
  const mintBytes = data.slice(SWAP_OUTPUT_MINT_OFFSET, end);
8254
6564
  if (mintBytes.every((byte) => byte === 0)) return void 0;
8255
6565
  try {
8256
- return new PublicKey7(mintBytes).toBase58();
6566
+ return new PublicKey6(mintBytes).toBase58();
8257
6567
  } catch {
8258
6568
  return void 0;
8259
6569
  }
@@ -8343,7 +6653,7 @@ function parseSwapRecipientAta(data) {
8343
6653
  const recipientAtaBytes = data.slice(recipientAtaStart, recipientAtaEnd);
8344
6654
  if (recipientAtaBytes.every((byte) => byte === 0)) return void 0;
8345
6655
  try {
8346
- return new PublicKey7(recipientAtaBytes).toBase58();
6656
+ return new PublicKey6(recipientAtaBytes).toBase58();
8347
6657
  } catch {
8348
6658
  return void 0;
8349
6659
  }
@@ -8658,8 +6968,8 @@ async function scanTransactions(opts) {
8658
6968
  if (onChainAta) {
8659
6969
  try {
8660
6970
  const expectedAta = getAssociatedTokenAddressSync2(
8661
- new PublicKey7(asset.mint),
8662
- new PublicKey7(walletPublicKey)
6971
+ new PublicKey6(asset.mint),
6972
+ new PublicKey6(walletPublicKey)
8663
6973
  ).toBase58();
8664
6974
  if (onChainAta === expectedAta) {
8665
6975
  isOurs = true;
@@ -8962,7 +7272,7 @@ function formatComplianceCsv(report) {
8962
7272
  }
8963
7273
 
8964
7274
  // src/core/wallet.ts
8965
- import { PublicKey as PublicKey8 } from "@solana/web3.js";
7275
+ import { PublicKey as PublicKey7 } from "@solana/web3.js";
8966
7276
  var UtxoWallet = class _UtxoWallet {
8967
7277
  constructor(viewingKey) {
8968
7278
  this.wallets = /* @__PURE__ */ new Map();
@@ -9174,7 +7484,7 @@ var UtxoWallet = class _UtxoWallet {
9174
7484
  data.viewingKey ? new Uint8Array(data.viewingKey) : void 0
9175
7485
  );
9176
7486
  for (const w of data.wallets) {
9177
- const mint = new PublicKey8(w.mint);
7487
+ const mint = new PublicKey7(w.mint);
9178
7488
  for (const u of w.utxos) {
9179
7489
  wallet.addUtxo({
9180
7490
  amount: BigInt(u.amount),
@@ -9257,15 +7567,13 @@ var SimpleWallet = class {
9257
7567
  };
9258
7568
 
9259
7569
  // src/index.ts
9260
- var VERSION = "1.0.0";
7570
+ var VERSION = "0.1.6";
9261
7571
  var SCANNER_SUPPORTS_TRANSACT_SWAP = true;
9262
7572
  export {
9263
7573
  CLOAK_PROGRAM_ID,
9264
7574
  CloakError,
9265
7575
  CloakSDK,
9266
- DEFAULT_CIRCUITS_URL,
9267
7576
  DEFAULT_TRANSACTION_CIRCUITS_URL,
9268
- EXPECTED_CIRCUIT_HASHES,
9269
7577
  FIXED_FEE_LAMPORTS,
9270
7578
  LAMPORTS_PER_SOL,
9271
7579
  LocalStorageAdapter,
@@ -9274,18 +7582,20 @@ export {
9274
7582
  MemoryStorageAdapter,
9275
7583
  MerkleTree,
9276
7584
  NATIVE_SOL_MINT,
7585
+ RelayInternalError,
9277
7586
  RelayService,
9278
7587
  RootNotFoundError,
9279
7588
  SCANNER_SUPPORTS_TRANSACT_SWAP,
9280
7589
  SIGN_IN_MESSAGE,
7590
+ SanctionsQuoteError,
9281
7591
  ShieldPoolErrors,
9282
7592
  SimpleWallet,
7593
+ UtxoAlreadySpentError,
9283
7594
  UtxoWallet,
9284
7595
  VARIABLE_FEE_DENOMINATOR,
9285
7596
  VARIABLE_FEE_NUMERATOR,
9286
7597
  VARIABLE_FEE_RATE,
9287
7598
  VERSION,
9288
- areCircuitsAvailable,
9289
7599
  bigintToBytes322 as bigintToBytes32,
9290
7600
  bigintToHex,
9291
7601
  buildMerkleTree,
@@ -9298,30 +7608,16 @@ export {
9298
7608
  calculateRelayFee,
9299
7609
  chainNoteFromBase64,
9300
7610
  chainNoteToBase64,
9301
- cleanupStalePendingOperations,
9302
- clearPendingDeposits,
9303
- clearPendingWithdrawals,
7611
+ classifyRelayError,
9304
7612
  computeChainNoteHash,
9305
- computeCommitment2 as computeCommitment,
9306
7613
  computeExtDataHash,
9307
7614
  computeMerkleRoot,
9308
- computeNullifier2 as computeNullifier,
9309
- computeNullifierAsync,
9310
- computeNullifierSync,
9311
- computeOutputsHash,
9312
- computeOutputsHashAsync,
9313
- computeOutputsHashSync,
9314
7615
  computeProofForLatestDeposit,
9315
7616
  computeProofFromChain,
9316
7617
  computeSignature,
9317
- computeSwapOutputsHash,
9318
- computeSwapOutputsHashAsync,
9319
- computeSwapOutputsHashSync,
9320
7618
  computeCommitment as computeUtxoCommitment,
9321
7619
  computeNullifier as computeUtxoNullifier,
9322
- copyNoteToClipboard,
9323
7620
  createCloakError,
9324
- createDepositInstruction,
9325
7621
  createLogger,
9326
7622
  createUtxo,
9327
7623
  createZeroUtxo,
@@ -9341,77 +7637,52 @@ export {
9341
7637
  deriveViewingKeyFromUtxoPrivateKey,
9342
7638
  deserializeUtxo,
9343
7639
  detectNetworkFromRpcUrl,
9344
- downloadNote,
9345
- encodeNoteSimple,
9346
7640
  encryptCompactChainNote,
9347
7641
  encryptNoteForRecipient,
9348
7642
  encryptTransactionMetadata,
9349
7643
  encryptTransactionMetadataBundle,
9350
7644
  expandSpendKey,
9351
7645
  exportKeys,
9352
- exportNote,
9353
- exportWalletKeys,
9354
7646
  fetchCommitments,
9355
7647
  fetchRiskQuoteInstruction,
9356
7648
  fetchRiskQuoteIx,
9357
- filterNotesByNetwork,
9358
- filterWithdrawableNotes,
9359
- findNoteByCommitment,
9360
7649
  formatAmount,
9361
7650
  formatComplianceCsv,
9362
7651
  formatErrorForLogging,
9363
7652
  formatSol,
9364
7653
  fullWithdraw,
9365
7654
  generateCloakKeys,
9366
- generateCommitment,
9367
- generateCommitmentAsync,
9368
7655
  generateMasterSeed,
9369
- generateNote,
9370
- generateNoteFromWallet,
9371
7656
  generateUtxoKeypair,
9372
7657
  generateViewingKeyPair,
9373
- generateWithdrawRegularProof,
9374
- generateWithdrawSwapProof,
9375
7658
  getAddressExplorerUrl,
9376
7659
  getCircuitsPath,
9377
- getDefaultCircuitsPath,
9378
7660
  getDistributableAmount,
9379
7661
  getExplorerUrl,
9380
7662
  getNkFromUtxoPrivateKey,
9381
7663
  getNullifierPDA,
9382
- getPendingOperationsSummary,
9383
7664
  getPublicKey,
9384
- getPublicViewKey,
9385
- getRecipientAmount,
9386
7665
  getRpcUrlForNetwork,
9387
7666
  getShieldPoolPDAs,
9388
7667
  getSwapStatePDA,
9389
- getViewKey,
9390
- hasPendingOperations,
9391
7668
  hexToBigint2 as hexToBigint,
9392
7669
  hexToBytes,
9393
7670
  importKeys,
9394
- importWalletKeys,
9395
7671
  isDebugEnabled,
9396
7672
  isRootNotFoundError,
9397
7673
  isValidHex,
9398
7674
  isValidRpcUrl,
9399
7675
  isValidSolanaAddress,
9400
7676
  isWithdrawAmountSufficient,
9401
- isWithdrawable,
9402
7677
  keypairToAdapter,
9403
- loadPendingDeposits,
9404
- loadPendingWithdrawals,
9405
7678
  parseAmount,
9406
7679
  parseError,
9407
- parseNote,
9408
7680
  parseRelayErrorResponse,
9409
7681
  parseTransactionError,
9410
7682
  partialWithdraw,
9411
7683
  poseidonHash,
9412
7684
  preflightCheck,
9413
- prepareEncryptedOutput,
9414
- prepareEncryptedOutputForRecipient,
7685
+ preflightNullifiers,
9415
7686
  proofToBytes,
9416
7687
  pubkeyToFieldElement,
9417
7688
  pubkeyToLimbs,
@@ -9419,16 +7690,11 @@ export {
9419
7690
  randomFieldElement,
9420
7691
  readMerkleTreeState,
9421
7692
  registerViewingKey,
9422
- removePendingDeposit,
9423
- removePendingWithdrawal,
9424
- savePendingDeposit,
9425
- savePendingWithdrawal,
9426
7693
  scanNotesForWallet,
9427
7694
  scanTransactions,
9428
7695
  sdkLogger,
9429
7696
  selectUtxos,
9430
7697
  sendTransaction,
9431
- serializeNote,
9432
7698
  serializeUtxo,
9433
7699
  setCircuitsPath,
9434
7700
  setDebugMode,
@@ -9442,21 +7708,13 @@ export {
9442
7708
  transfer,
9443
7709
  truncate,
9444
7710
  tryDecryptNote,
9445
- updateNoteWithDeposit,
9446
- updatePendingDeposit,
9447
- updatePendingWithdrawal,
9448
7711
  bigintToBytes32 as utxoBigintToBytes32,
9449
7712
  utxoEquals,
9450
7713
  hexToBigint as utxoHexToBigint,
9451
- validateDepositParams,
9452
- validateNote,
9453
7714
  validateOutputsSum,
9454
7715
  validateRoot,
9455
- validateTransfers,
9456
7716
  validateWalletConnected,
9457
- validateWithdrawableNote,
9458
- verifyAllCircuits,
9459
- verifyCircuitIntegrity,
7717
+ verifyUtxos,
9460
7718
  waitForRoot,
9461
7719
  withTiming
9462
7720
  };