@cloak.dev/sdk 0.1.7 → 0.1.8

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,7 +24,10 @@ import {
24
24
  // src/core/CloakSDK.ts
25
25
  import {
26
26
  Keypair,
27
- PublicKey as PublicKey2
27
+ PublicKey as PublicKey4,
28
+ Transaction,
29
+ TransactionInstruction as TransactionInstruction2,
30
+ ComputeBudgetProgram
28
31
  } from "@solana/web3.js";
29
32
 
30
33
  // src/core/types.ts
@@ -82,6 +85,79 @@ function hexToBigint2(hex) {
82
85
  const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
83
86
  return BigInt("0x" + cleanHex);
84
87
  }
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
+ }
85
161
  function bigintToBytes322(n) {
86
162
  const hex = n.toString(16).padStart(64, "0");
87
163
  const bytes = new Uint8Array(32);
@@ -354,11 +430,223 @@ function importKeys(exported) {
354
430
  return generateCloakKeys(masterSeed);
355
431
  }
356
432
 
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
+
357
626
  // src/core/storage.ts
358
627
  var MemoryStorageAdapter = class {
359
628
  constructor() {
629
+ this.notes = /* @__PURE__ */ new Map();
360
630
  this.keys = null;
361
631
  }
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
+ }
362
650
  saveKeys(keys) {
363
651
  this.keys = keys;
364
652
  }
@@ -369,10 +657,10 @@ var MemoryStorageAdapter = class {
369
657
  this.keys = null;
370
658
  }
371
659
  };
372
- var _LocalStorageAdapter = class _LocalStorageAdapter {
373
- constructor(keysKey = "cloak_wallet_keys") {
660
+ var LocalStorageAdapter = class {
661
+ constructor(notesKey = "cloak_notes", keysKey = "cloak_wallet_keys") {
662
+ this.notesKey = notesKey;
374
663
  this.keysKey = keysKey;
375
- _LocalStorageAdapter.runLegacyPurgeOnce();
376
664
  }
377
665
  getStorage() {
378
666
  if (typeof globalThis !== "undefined" && globalThis.localStorage) {
@@ -380,6 +668,47 @@ var _LocalStorageAdapter = class _LocalStorageAdapter {
380
668
  }
381
669
  return null;
382
670
  }
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
+ }
383
712
  saveKeys(keys) {
384
713
  const storage = this.getStorage();
385
714
  if (!storage) throw new Error("localStorage not available");
@@ -402,64 +731,126 @@ var _LocalStorageAdapter = class _LocalStorageAdapter {
402
731
  storage.removeItem(this.keysKey);
403
732
  }
404
733
  }
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;
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}`);
414
754
  }
415
- const storage = globalThis.localStorage;
416
- if (storage.getItem(_LocalStorageAdapter.LEGACY_PURGE_SENTINEL_KEY)) {
417
- return;
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");
418
786
  }
419
- storage.removeItem(_LocalStorageAdapter.LEGACY_NOTES_KEY);
420
- try {
421
- storage.setItem(_LocalStorageAdapter.LEGACY_PURGE_SENTINEL_KEY, "1");
422
- } catch {
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");
794
+ }
795
+ if (!Array.isArray(note.merkleProof.pathIndices)) {
796
+ throw new Error("Merkle proof pathIndices must be an array");
797
+ }
798
+ if (note.merkleProof.pathElements.length !== note.merkleProof.pathIndices.length) {
799
+ throw new Error("Merkle proof pathElements and pathIndices must have same length");
423
800
  }
424
801
  }
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;
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`);
445
845
  }
446
- globalThis.localStorage.removeItem(notesKey);
447
846
  }
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";
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
+ }
463
854
 
464
855
  // src/utils/errors.ts
465
856
  var RootNotFoundError = class extends Error {
@@ -519,7 +910,8 @@ function classifyRelayError(responseText, httpStatus) {
519
910
  inner = responseText;
520
911
  }
521
912
  const haystack = inner.toLowerCase();
522
- if (haystack.includes("0x1020") || haystack.includes("doublespend")) {
913
+ if (haystack.includes("0x1020") || // 0x1020 as decimal in a JSON-stringified confirmTransaction err (`"Custom":4128`).
914
+ haystack.includes('"custom":4128') || haystack.includes("doublespend")) {
523
915
  return new UtxoAlreadySpentError(
524
916
  "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
917
  );
@@ -1355,6 +1747,7 @@ function formatErrorForLogging(error) {
1355
1747
  }
1356
1748
 
1357
1749
  // src/services/RelayService.ts
1750
+ import { sha256 } from "@noble/hashes/sha2";
1358
1751
  var RelayService = class {
1359
1752
  /**
1360
1753
  * Create a new Relay Service client
@@ -1723,23 +2116,91 @@ var RelayService = class {
1723
2116
  }
1724
2117
  };
1725
2118
 
2119
+ // src/solana/instructions.ts
2120
+ import {
2121
+ PublicKey as PublicKey2,
2122
+ SystemProgram,
2123
+ TransactionInstruction
2124
+ } from "@solana/web3.js";
2125
+ function createDepositInstruction(params) {
2126
+ if (params.commitment.length !== 32) {
2127
+ throw new Error(
2128
+ `Invalid commitment length: ${params.commitment.length} (expected 32 bytes)`
2129
+ );
2130
+ }
2131
+ if (params.amount <= 0) {
2132
+ throw new Error("Amount must be positive");
2133
+ }
2134
+ const discriminant = new Uint8Array([1]);
2135
+ const amountBytes = new Uint8Array(8);
2136
+ new DataView(amountBytes.buffer).setBigUint64(
2137
+ 0,
2138
+ BigInt(params.amount),
2139
+ true
2140
+ // little-endian
2141
+ );
2142
+ const data = new Uint8Array(41);
2143
+ data.set(discriminant, 0);
2144
+ data.set(amountBytes, 1);
2145
+ data.set(params.commitment, 9);
2146
+ return new TransactionInstruction({
2147
+ programId: params.programId,
2148
+ keys: [
2149
+ // Account 0: Payer (signer, writable) - pays for transaction
2150
+ { pubkey: params.payer, isSigner: true, isWritable: true },
2151
+ // Account 1: Pool (writable) - receives SOL
2152
+ { pubkey: params.pool, isSigner: false, isWritable: true },
2153
+ // Account 2: System Program (readonly) - for transfers
2154
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
2155
+ // Account 3: Merkle Tree (writable) - stores on-chain Merkle tree
2156
+ { pubkey: params.merkleTree, isSigner: false, isWritable: true }
2157
+ ],
2158
+ data: Buffer.from(data)
2159
+ });
2160
+ }
2161
+ function validateDepositParams(params) {
2162
+ if (!(params.programId instanceof PublicKey2)) {
2163
+ throw new Error("programId must be a PublicKey");
2164
+ }
2165
+ if (!(params.payer instanceof PublicKey2)) {
2166
+ throw new Error("payer must be a PublicKey");
2167
+ }
2168
+ if (!(params.pool instanceof PublicKey2)) {
2169
+ throw new Error("pool must be a PublicKey");
2170
+ }
2171
+ if (!(params.merkleTree instanceof PublicKey2)) {
2172
+ throw new Error("merkleTree must be a PublicKey");
2173
+ }
2174
+ if (typeof params.amount !== "number" || params.amount <= 0) {
2175
+ throw new Error("amount must be a positive number");
2176
+ }
2177
+ if (!(params.commitment instanceof Uint8Array)) {
2178
+ throw new Error("commitment must be a Uint8Array");
2179
+ }
2180
+ if (params.commitment.length !== 32) {
2181
+ throw new Error(
2182
+ `commitment must be 32 bytes (got ${params.commitment.length})`
2183
+ );
2184
+ }
2185
+ }
2186
+
1726
2187
  // src/utils/pda.ts
1727
- import { PublicKey } from "@solana/web3.js";
2188
+ import { PublicKey as PublicKey3 } from "@solana/web3.js";
1728
2189
  function getShieldPoolPDAs(programId, mint = NATIVE_SOL_MINT) {
1729
2190
  const pid = programId || CLOAK_PROGRAM_ID;
1730
- const [pool] = PublicKey.findProgramAddressSync(
2191
+ const [pool] = PublicKey3.findProgramAddressSync(
1731
2192
  [Buffer.from("pool"), mint.toBuffer()],
1732
2193
  pid
1733
2194
  );
1734
- const [merkleTree] = PublicKey.findProgramAddressSync(
2195
+ const [merkleTree] = PublicKey3.findProgramAddressSync(
1735
2196
  [Buffer.from("merkle_tree"), mint.toBuffer()],
1736
2197
  pid
1737
2198
  );
1738
- const [treasury] = PublicKey.findProgramAddressSync(
2199
+ const [treasury] = PublicKey3.findProgramAddressSync(
1739
2200
  [Buffer.from("treasury"), mint.toBuffer()],
1740
2201
  pid
1741
2202
  );
1742
- const [vaultAuthority] = PublicKey.findProgramAddressSync(
2203
+ const [vaultAuthority] = PublicKey3.findProgramAddressSync(
1743
2204
  [Buffer.from("vault_authority"), mint.toBuffer()],
1744
2205
  pid
1745
2206
  );
@@ -1755,7 +2216,7 @@ function getNullifierPDA(poolPubkey, nullifier, programId) {
1755
2216
  if (nullifier.length !== 32) {
1756
2217
  throw new Error(`Nullifier must be 32 bytes, got ${nullifier.length}`);
1757
2218
  }
1758
- return PublicKey.findProgramAddressSync(
2219
+ return PublicKey3.findProgramAddressSync(
1759
2220
  [Buffer.from("nullifier"), poolPubkey.toBuffer(), Buffer.from(nullifier)],
1760
2221
  pid
1761
2222
  );
@@ -1765,29 +2226,273 @@ function getSwapStatePDA(poolPubkey, nullifier, programId) {
1765
2226
  if (nullifier.length !== 32) {
1766
2227
  throw new Error(`Nullifier must be 32 bytes, got ${nullifier.length}`);
1767
2228
  }
1768
- return PublicKey.findProgramAddressSync(
2229
+ return PublicKey3.findProgramAddressSync(
1769
2230
  [Buffer.from("swap_state"), poolPubkey.toBuffer(), Buffer.from(nullifier)],
1770
2231
  pid
1771
2232
  );
1772
2233
  }
1773
2234
 
1774
- // src/utils/logger.ts
1775
- var globalDebugEnabled = false;
1776
- function checkEnvDebug() {
1777
- if (typeof process !== "undefined" && process.env) {
1778
- return !!(process.env.CLOAK_DEBUG === "1" || process.env.CLOAK_DEBUG === "true" || process.env.DEBUG?.includes("cloak"));
2235
+ // src/utils/proof-generation.ts
2236
+ import * as snarkjs from "snarkjs";
2237
+ var IS_REACT_NATIVE = typeof navigator !== "undefined" && navigator.product === "ReactNative";
2238
+ var IS_BROWSER = IS_REACT_NATIVE || typeof window !== "undefined" || typeof globalThis !== "undefined" && typeof globalThis.document !== "undefined";
2239
+ var DEFAULT_CIRCUITS_URL = "https://storage.googleapis.com/cloak-circuits/circuits/0.1.0";
2240
+ var SKIP_CIRCUIT_VERIFY_ENV = typeof process !== "undefined" && process.env && process.env.CLOAK_SKIP_CIRCUIT_INTEGRITY_CHECK === "1";
2241
+ function isUrl(path) {
2242
+ return path.startsWith("http://") || path.startsWith("https://");
2243
+ }
2244
+ function resolveCircuitsUrl(circuitsPath2) {
2245
+ if (circuitsPath2.trim() !== "" && circuitsPath2.trim() !== DEFAULT_CIRCUITS_URL) {
2246
+ return DEFAULT_CIRCUITS_URL;
1779
2247
  }
1780
- return false;
2248
+ return DEFAULT_CIRCUITS_URL;
1781
2249
  }
1782
- globalDebugEnabled = checkEnvDebug();
1783
- function setDebugMode(enabled) {
1784
- globalDebugEnabled = enabled;
2250
+ async function fetchFileAsBuffer(url) {
2251
+ const response = await fetch(url);
2252
+ if (!response.ok) {
2253
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
2254
+ }
2255
+ const arrayBuffer = await response.arrayBuffer();
2256
+ return new Uint8Array(arrayBuffer);
1785
2257
  }
1786
- function isDebugEnabled() {
1787
- return globalDebugEnabled;
2258
+ async function sha256Hex(data) {
2259
+ if (IS_BROWSER) {
2260
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
2261
+ const buffer = bytes.buffer.slice(
2262
+ bytes.byteOffset,
2263
+ bytes.byteOffset + bytes.byteLength
2264
+ );
2265
+ const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
2266
+ return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
2267
+ }
2268
+ const nodeCrypto = await getNodeCrypto();
2269
+ return nodeCrypto.createHash("sha256").update(data).digest("hex");
1788
2270
  }
1789
- var LOG_COLORS = {
1790
- DEBUG: "\x1B[90m",
2271
+ var _nodeCrypto = null;
2272
+ function nodeRequire(moduleName) {
2273
+ if (IS_BROWSER) {
2274
+ throw new Error(`Node.js ${moduleName} module not available in browser/React Native`);
2275
+ }
2276
+ try {
2277
+ const requireFunc = new Function("moduleName", "return require(moduleName)");
2278
+ return requireFunc(moduleName);
2279
+ } catch {
2280
+ throw new Error(`Cannot load Node.js module ${moduleName} synchronously in ESM.`);
2281
+ }
2282
+ }
2283
+ async function getNodeCrypto() {
2284
+ if (_nodeCrypto) return _nodeCrypto;
2285
+ _nodeCrypto = nodeRequire("crypto");
2286
+ return _nodeCrypto;
2287
+ }
2288
+ async function generateWithdrawRegularProof(inputs, circuitsPath2) {
2289
+ const circuitsUrl = resolveCircuitsUrl(circuitsPath2);
2290
+ let wasmInput;
2291
+ let zkeyInput;
2292
+ if (IS_BROWSER) {
2293
+ wasmInput = `${circuitsUrl}/withdraw_regular_js/withdraw_regular.wasm`;
2294
+ zkeyInput = `${circuitsUrl}/withdraw_regular_final.zkey`;
2295
+ } else {
2296
+ const wasmUrl = `${circuitsUrl}/withdraw_regular_js/withdraw_regular.wasm`;
2297
+ const zkeyUrl = `${circuitsUrl}/withdraw_regular_final.zkey`;
2298
+ const [wasmData, zkeyData] = await Promise.all([
2299
+ fetchFileAsBuffer(wasmUrl),
2300
+ fetchFileAsBuffer(zkeyUrl)
2301
+ ]);
2302
+ wasmInput = wasmData;
2303
+ zkeyInput = zkeyData;
2304
+ }
2305
+ const verification = await verifyCircuitIntegrity(circuitsUrl, "withdraw_regular");
2306
+ if (!verification.valid) {
2307
+ throw new Error(
2308
+ `withdraw_regular circuit integrity verification failed: ${verification.error ?? "hash mismatch"}`
2309
+ );
2310
+ }
2311
+ const circuitInputs = {
2312
+ // Public signals
2313
+ root: inputs.root.toString(),
2314
+ nullifier: inputs.nullifier.toString(),
2315
+ outputs_hash: inputs.outputs_hash.toString(),
2316
+ public_amount: inputs.public_amount.toString(),
2317
+ // Private common inputs
2318
+ amount: inputs.amount.toString(),
2319
+ leaf_index: inputs.leaf_index.toString(),
2320
+ sk: inputs.sk.map((x) => x.toString()),
2321
+ r: inputs.r.map((x) => x.toString()),
2322
+ pathElements: inputs.pathElements.map((x) => x.toString()),
2323
+ pathIndices: inputs.pathIndices,
2324
+ // Outputs
2325
+ num_outputs: inputs.num_outputs,
2326
+ out_addr: inputs.out_addr.map((addr) => addr.map((x) => x.toString())),
2327
+ out_amount: inputs.out_amount.map((x) => x.toString()),
2328
+ out_flags: inputs.out_flags,
2329
+ // Fee calculation
2330
+ var_fee: inputs.var_fee.toString(),
2331
+ rem: inputs.rem.toString()
2332
+ };
2333
+ const { proof, publicSignals } = await snarkjs.groth16.fullProve(circuitInputs, wasmInput, zkeyInput);
2334
+ const proofBytes = proofToBytes(proof);
2335
+ return {
2336
+ proof,
2337
+ publicSignals,
2338
+ proofBytes,
2339
+ publicInputsBytes: new Uint8Array(0)
2340
+ // Not used, kept for compatibility
2341
+ };
2342
+ }
2343
+ async function generateWithdrawSwapProof(inputs, circuitsPath2) {
2344
+ const circuitsUrl = resolveCircuitsUrl(circuitsPath2);
2345
+ let wasmInput;
2346
+ let zkeyInput;
2347
+ if (IS_BROWSER) {
2348
+ wasmInput = `${circuitsUrl}/withdraw_swap_js/withdraw_swap.wasm`;
2349
+ zkeyInput = `${circuitsUrl}/withdraw_swap_final.zkey`;
2350
+ } else {
2351
+ const wasmUrl = `${circuitsUrl}/withdraw_swap_js/withdraw_swap.wasm`;
2352
+ const zkeyUrl = `${circuitsUrl}/withdraw_swap_final.zkey`;
2353
+ const [wasmData, zkeyData] = await Promise.all([
2354
+ fetchFileAsBuffer(wasmUrl),
2355
+ fetchFileAsBuffer(zkeyUrl)
2356
+ ]);
2357
+ wasmInput = wasmData;
2358
+ zkeyInput = zkeyData;
2359
+ }
2360
+ const verification = await verifyCircuitIntegrity(circuitsUrl, "withdraw_swap");
2361
+ if (!verification.valid) {
2362
+ throw new Error(
2363
+ `withdraw_swap circuit integrity verification failed: ${verification.error ?? "hash mismatch"}`
2364
+ );
2365
+ }
2366
+ const sk = splitTo2Limbs(inputs.sk_spend);
2367
+ const r = splitTo2Limbs(inputs.r);
2368
+ const circuitInputs = {
2369
+ // Public signals (must be provided for witness generation)
2370
+ root: inputs.root.toString(),
2371
+ nullifier: inputs.nullifier.toString(),
2372
+ outputs_hash: inputs.outputs_hash.toString(),
2373
+ public_amount: inputs.public_amount.toString(),
2374
+ // Private inputs
2375
+ sk: sk.map((x) => x.toString()),
2376
+ r: r.map((x) => x.toString()),
2377
+ amount: inputs.amount.toString(),
2378
+ leaf_index: inputs.leaf_index.toString(),
2379
+ pathElements: inputs.path_elements.map((x) => x.toString()),
2380
+ pathIndices: inputs.path_indices,
2381
+ input_mint: inputs.input_mint.map((x) => x.toString()),
2382
+ output_mint: inputs.output_mint.map((x) => x.toString()),
2383
+ recipient_ata: inputs.recipient_ata.map((x) => x.toString()),
2384
+ min_output_amount: inputs.min_output_amount.toString(),
2385
+ // Note: fee is computed off-chain and validated on-chain:
2386
+ // - fixed_fee is 5000000 (0.005 SOL)
2387
+ // - var_fee and rem are computed from amount * 3 / 1000 (0.3%)
2388
+ var_fee: inputs.var_fee.toString(),
2389
+ rem: inputs.rem.toString()
2390
+ };
2391
+ const { proof, publicSignals } = await snarkjs.groth16.fullProve(
2392
+ circuitInputs,
2393
+ wasmInput,
2394
+ zkeyInput
2395
+ );
2396
+ const proofBytes = proofToBytes(proof);
2397
+ return {
2398
+ proof,
2399
+ publicSignals,
2400
+ proofBytes,
2401
+ publicInputsBytes: new Uint8Array(0)
2402
+ // Not used, kept for compatibility
2403
+ };
2404
+ }
2405
+ async function areCircuitsAvailable(circuitsPath2) {
2406
+ if (!circuitsPath2 || circuitsPath2 === "") {
2407
+ return false;
2408
+ }
2409
+ return isUrl(resolveCircuitsUrl(circuitsPath2));
2410
+ }
2411
+ async function getDefaultCircuitsPath() {
2412
+ return DEFAULT_CIRCUITS_URL;
2413
+ }
2414
+ var EXPECTED_CIRCUIT_HASHES = {
2415
+ withdraw_regular_wasm: "b80c364c926332111945ef437b07a71720fae774e08206cb11b6171f2a2420a3",
2416
+ withdraw_regular_zkey: "6dbca2612f7cc257b897d93384ae9dd09c1617fd1668f8287efb1b124488d144",
2417
+ withdraw_swap_wasm: "a296badc0047c211a4724c96a4e64a77cd75fbde7cfa4cf23af3640a723e5686",
2418
+ withdraw_swap_zkey: "95251954feaac80396651d12a0df3288630fa75db9410aebecb8aaee17a4f6ea"
2419
+ };
2420
+ async function verifyCircuitIntegrity(circuitsPath2, circuit) {
2421
+ const circuitsUrl = resolveCircuitsUrl(circuitsPath2);
2422
+ if (SKIP_CIRCUIT_VERIFY_ENV) {
2423
+ return {
2424
+ valid: true,
2425
+ circuit,
2426
+ error: "Verification skipped (CLOAK_SKIP_CIRCUIT_INTEGRITY_CHECK=1)"
2427
+ };
2428
+ }
2429
+ const expected = circuit === "withdraw_regular" ? {
2430
+ wasm: EXPECTED_CIRCUIT_HASHES.withdraw_regular_wasm,
2431
+ zkey: EXPECTED_CIRCUIT_HASHES.withdraw_regular_zkey
2432
+ } : {
2433
+ wasm: EXPECTED_CIRCUIT_HASHES.withdraw_swap_wasm,
2434
+ zkey: EXPECTED_CIRCUIT_HASHES.withdraw_swap_zkey
2435
+ };
2436
+ const wasmPath = `${circuitsUrl}/${circuit}_js/${circuit}.wasm`;
2437
+ const zkeyPath = `${circuitsUrl}/${circuit}_final.zkey`;
2438
+ try {
2439
+ const [wasmBytes, zkeyBytes] = await Promise.all([
2440
+ fetchFileAsBuffer(wasmPath),
2441
+ fetchFileAsBuffer(zkeyPath)
2442
+ ]);
2443
+ const computed = {
2444
+ wasm: await sha256Hex(wasmBytes),
2445
+ zkey: await sha256Hex(zkeyBytes)
2446
+ };
2447
+ if (computed.wasm === expected.wasm && computed.zkey === expected.zkey) {
2448
+ return {
2449
+ valid: true,
2450
+ circuit,
2451
+ computed,
2452
+ expected
2453
+ };
2454
+ }
2455
+ return {
2456
+ valid: false,
2457
+ circuit,
2458
+ error: `Circuit artifact hash mismatch for ${circuit}`,
2459
+ computed,
2460
+ expected
2461
+ };
2462
+ } catch (error) {
2463
+ return {
2464
+ valid: false,
2465
+ circuit,
2466
+ error: `Circuit verification failed: ${error instanceof Error ? error.message : String(error)}`
2467
+ };
2468
+ }
2469
+ }
2470
+ async function verifyAllCircuits(circuitsPath2) {
2471
+ const circuitsUrl = resolveCircuitsUrl(circuitsPath2);
2472
+ const results = await Promise.all([
2473
+ verifyCircuitIntegrity(circuitsUrl, "withdraw_regular"),
2474
+ verifyCircuitIntegrity(circuitsUrl, "withdraw_swap")
2475
+ ]);
2476
+ return results;
2477
+ }
2478
+
2479
+ // src/utils/logger.ts
2480
+ var globalDebugEnabled = false;
2481
+ function checkEnvDebug() {
2482
+ if (typeof process !== "undefined" && process.env) {
2483
+ return !!(process.env.CLOAK_DEBUG === "1" || process.env.CLOAK_DEBUG === "true" || process.env.DEBUG?.includes("cloak"));
2484
+ }
2485
+ return false;
2486
+ }
2487
+ globalDebugEnabled = checkEnvDebug();
2488
+ function setDebugMode(enabled) {
2489
+ globalDebugEnabled = enabled;
2490
+ }
2491
+ function isDebugEnabled() {
2492
+ return globalDebugEnabled;
2493
+ }
2494
+ var LOG_COLORS = {
2495
+ DEBUG: "\x1B[90m",
1791
2496
  // gray
1792
2497
  INFO: "\x1B[32m",
1793
2498
  // green
@@ -1987,16 +2692,44 @@ async function computeProofInternal(leafIndex, _nextIndex, subtrees) {
1987
2692
  }
1988
2693
 
1989
2694
  // src/core/CloakSDK.ts
1990
- var CLOAK_PROGRAM_ID = new PublicKey2("zh1eLd6rSphLejbFfJEneUwzHRfMKxgzrgkfwA6qRkW");
2695
+ var COMPUTE_BUDGET_PROGRAM_ID = new PublicKey4("ComputeBudget111111111111111111111111111111");
2696
+ function createSetLoadedAccountsDataSizeLimitInstruction(bytes) {
2697
+ const data = Buffer.alloc(5);
2698
+ data.writeUInt8(4, 0);
2699
+ data.writeUInt32LE(bytes, 1);
2700
+ return new TransactionInstruction2({
2701
+ keys: [],
2702
+ programId: COMPUTE_BUDGET_PROGRAM_ID,
2703
+ data
2704
+ });
2705
+ }
2706
+ var CLOAK_PROGRAM_ID = new PublicKey4("zh1eLd6rSphLejbFfJEneUwzHRfMKxgzrgkfwA6qRkW");
1991
2707
  var CLOAK_API_URL = "https://api.cloak.ag";
1992
2708
  var CloakSDK = class {
2709
+ /**
2710
+ * Create a new Cloak SDK client
2711
+ *
2712
+ * @param config - Client configuration
2713
+ *
2714
+ * @example Node.js mode (with keypair)
2715
+ * ```typescript
2716
+ * const sdk = new CloakSDK({
2717
+ * keypairBytes: keypair.secretKey,
2718
+ * network: "devnet"
2719
+ * });
2720
+ * ```
2721
+ *
2722
+ * @example Browser mode (with wallet adapter)
2723
+ * ```typescript
2724
+ * const sdk = new CloakSDK({
2725
+ * wallet: walletAdapter,
2726
+ * network: "devnet"
2727
+ * });
2728
+ * ```
2729
+ */
1993
2730
  constructor(config) {
1994
2731
  if (!config.keypairBytes && !config.wallet) {
1995
- throw new CloakError(
1996
- "Must provide either keypairBytes (Node.js) or wallet (Browser)",
1997
- "validation",
1998
- false
1999
- );
2732
+ throw new Error("Must provide either keypairBytes (Node.js) or wallet (Browser)");
2000
2733
  }
2001
2734
  if (config.debug) {
2002
2735
  setDebugMode(true);
@@ -2016,9 +2749,13 @@ var CloakSDK = class {
2016
2749
  }
2017
2750
  }
2018
2751
  const programId = config.programId || CLOAK_PROGRAM_ID;
2019
- const { pool, merkleTree, treasury } = getShieldPoolPDAs(programId, NATIVE_SOL_MINT);
2752
+ const { pool, merkleTree, treasury } = getShieldPoolPDAs(
2753
+ programId,
2754
+ NATIVE_SOL_MINT
2755
+ );
2020
2756
  this.config = {
2021
2757
  network: config.network || "mainnet",
2758
+ keypairBytes: config.keypairBytes,
2022
2759
  cloakKeys: config.cloakKeys,
2023
2760
  programId,
2024
2761
  poolAddress: pool,
@@ -2027,7 +2764,9 @@ var CloakSDK = class {
2027
2764
  debug: config.debug
2028
2765
  };
2029
2766
  }
2030
- /** Public key of the configured signer (keypair or wallet). */
2767
+ /**
2768
+ * Get the public key for deposits (from keypair or wallet)
2769
+ */
2031
2770
  getPublicKey() {
2032
2771
  if (this.keypair) {
2033
2772
  return this.keypair.publicKey;
@@ -2035,23 +2774,877 @@ var CloakSDK = class {
2035
2774
  if (this.wallet?.publicKey) {
2036
2775
  return this.wallet.publicKey;
2037
2776
  }
2038
- throw new CloakError(
2039
- "No public key available - wallet not connected and no keypair provided",
2040
- "wallet",
2041
- false
2777
+ throw new Error("No public key available - wallet not connected or keypair not provided");
2778
+ }
2779
+ /**
2780
+ * Check if the SDK is using a wallet adapter
2781
+ */
2782
+ isWalletMode() {
2783
+ return !!this.wallet && !this.keypair;
2784
+ }
2785
+ /**
2786
+ * Deposit SOL into the Cloak protocol
2787
+ *
2788
+ * Creates a new note (or uses a provided one), submits a deposit transaction,
2789
+ * and registers with the indexer.
2790
+ *
2791
+ * @param connection - Solana connection
2792
+ * @param payer - Payer wallet with sendTransaction method
2793
+ * @param amountOrNote - Amount in lamports OR an existing note to deposit
2794
+ * @param options - Optional configuration
2795
+ * @returns Deposit result with note and transaction info
2796
+ *
2797
+ * @example
2798
+ * ```typescript
2799
+ * // Generate and deposit in one step
2800
+ * const result = await client.deposit(
2801
+ * connection,
2802
+ * wallet,
2803
+ * 1_000_000_000,
2804
+ * {
2805
+ * onProgress: (status) => console.log(status)
2806
+ * }
2807
+ * );
2808
+ *
2809
+ * // Or deposit a pre-generated note
2810
+ * const note = client.generateNote(1_000_000_000);
2811
+ * const result = await client.deposit(connection, wallet, note);
2812
+ * ```
2813
+ */
2814
+ async deposit(connection, amountOrNote, options) {
2815
+ try {
2816
+ let note;
2817
+ let isNewNote = false;
2818
+ if (typeof amountOrNote === "number") {
2819
+ options?.onProgress?.("generating_note", { message: "Generating note with secrets..." });
2820
+ note = await generateNote(amountOrNote, this.config.network);
2821
+ isNewNote = true;
2822
+ } else {
2823
+ note = amountOrNote;
2824
+ if (note.depositSignature) {
2825
+ throw new Error("Note has already been deposited");
2826
+ }
2827
+ }
2828
+ if (isNewNote) {
2829
+ await this.storage.saveNote(note);
2830
+ if (options?.onNoteGenerated) {
2831
+ options?.onProgress?.("awaiting_note_acknowledgment", {
2832
+ message: "Waiting for note to be saved before proceeding with deposit..."
2833
+ });
2834
+ try {
2835
+ await options.onNoteGenerated(note);
2836
+ } catch (error) {
2837
+ throw new Error(
2838
+ `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.`
2839
+ );
2840
+ }
2841
+ }
2842
+ options?.onProgress?.("note_saved", {
2843
+ message: "Note saved. Proceeding with on-chain deposit..."
2844
+ });
2845
+ }
2846
+ const payerPubkey = this.getPublicKey();
2847
+ const balance = await connection.getBalance(payerPubkey);
2848
+ const requiredAmount = note.amount + 5e3;
2849
+ if (balance < requiredAmount) {
2850
+ throw new Error(
2851
+ `Insufficient balance. Required: ${requiredAmount} lamports (${note.amount} + fees), Available: ${balance} lamports`
2852
+ );
2853
+ }
2854
+ const commitmentBytes = hexToBytes(note.commitment);
2855
+ const programId = this.config.programId || CLOAK_PROGRAM_ID;
2856
+ const depositIx = createDepositInstruction({
2857
+ programId,
2858
+ payer: payerPubkey,
2859
+ pool: this.config.poolAddress,
2860
+ merkleTree: this.config.merkleTreeAddress,
2861
+ amount: note.amount,
2862
+ commitment: commitmentBytes
2863
+ });
2864
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
2865
+ const priorityFee = options?.priorityFee ?? 1e4;
2866
+ const cuPriceIx = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFee });
2867
+ const dataSizeLimit = options?.loadedAccountsDataSizeLimit ?? 256 * 1024;
2868
+ const dataSizeLimitIx = dataSizeLimit > 0 ? createSetLoadedAccountsDataSizeLimitInstruction(dataSizeLimit) : null;
2869
+ let computeUnits;
2870
+ if (options?.optimizeCU && this.keypair) {
2871
+ options?.onProgress?.("simulating", { message: "Simulating transaction for optimal CU..." });
2872
+ const simCuLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 2e5 });
2873
+ const simTransaction = new Transaction({
2874
+ feePayer: payerPubkey,
2875
+ recentBlockhash: blockhash
2876
+ }).add(simCuLimitIx).add(cuPriceIx);
2877
+ if (dataSizeLimitIx) {
2878
+ simTransaction.add(dataSizeLimitIx);
2879
+ }
2880
+ simTransaction.add(depositIx);
2881
+ try {
2882
+ const simulation = await connection.simulateTransaction(simTransaction, [this.keypair]);
2883
+ if (simulation.value.err) {
2884
+ console.warn("Simulation failed, using default CU:", simulation.value.err);
2885
+ computeUnits = 4e4;
2886
+ } else {
2887
+ const simulatedCU = simulation.value.unitsConsumed ?? 3e4;
2888
+ computeUnits = Math.ceil(simulatedCU * 1.2);
2889
+ console.log(`CU optimization: ${simulatedCU} simulated \u2192 ${computeUnits} limit`);
2890
+ }
2891
+ } catch (simError) {
2892
+ console.warn("Simulation RPC error, using default CU:", simError);
2893
+ computeUnits = 4e4;
2894
+ }
2895
+ } else if (options?.optimizeCU && this.wallet) {
2896
+ console.warn("CU optimization via simulation not available in wallet mode (would require double signing). Using default.");
2897
+ computeUnits = options?.computeUnits ?? 4e4;
2898
+ } else {
2899
+ computeUnits = options?.computeUnits ?? 4e4;
2900
+ }
2901
+ const cuLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits });
2902
+ const transaction = new Transaction({
2903
+ feePayer: payerPubkey,
2904
+ recentBlockhash: blockhash
2905
+ }).add(cuLimitIx).add(cuPriceIx);
2906
+ if (dataSizeLimitIx) {
2907
+ transaction.add(dataSizeLimitIx);
2908
+ }
2909
+ transaction.add(depositIx);
2910
+ if (!transaction.feePayer) {
2911
+ throw new Error("Transaction feePayer is not set");
2912
+ }
2913
+ if (!transaction.recentBlockhash) {
2914
+ throw new Error("Transaction recentBlockhash is not set");
2915
+ }
2916
+ let signature;
2917
+ if (this.wallet) {
2918
+ if (!this.wallet.publicKey) {
2919
+ throw new Error("Wallet not connected - publicKey is null");
2920
+ }
2921
+ if (this.wallet.signTransaction) {
2922
+ try {
2923
+ const signedTransaction = await this.wallet.signTransaction(transaction);
2924
+ const rawTransaction = signedTransaction.serialize();
2925
+ signature = await connection.sendRawTransaction(rawTransaction, {
2926
+ skipPreflight: options?.skipPreflight || false,
2927
+ preflightCommitment: "confirmed",
2928
+ maxRetries: 3
2929
+ });
2930
+ } catch (signError) {
2931
+ if (this.wallet.sendTransaction) {
2932
+ signature = await this.wallet.sendTransaction(transaction, connection, {
2933
+ skipPreflight: options?.skipPreflight || false,
2934
+ preflightCommitment: "confirmed",
2935
+ maxRetries: 3
2936
+ });
2937
+ } else {
2938
+ throw signError;
2939
+ }
2940
+ }
2941
+ } else if (this.wallet.sendTransaction) {
2942
+ signature = await this.wallet.sendTransaction(transaction, connection, {
2943
+ skipPreflight: options?.skipPreflight || false,
2944
+ preflightCommitment: "confirmed",
2945
+ maxRetries: 3
2946
+ });
2947
+ } else {
2948
+ throw new Error("Wallet adapter must provide either sendTransaction or signTransaction");
2949
+ }
2950
+ } else if (this.keypair) {
2951
+ signature = await connection.sendTransaction(transaction, [this.keypair], {
2952
+ skipPreflight: options?.skipPreflight || false,
2953
+ preflightCommitment: "confirmed",
2954
+ maxRetries: 3
2955
+ });
2956
+ } else {
2957
+ throw new Error("No signing method available - provide keypair or wallet");
2958
+ }
2959
+ const confirmation = await connection.confirmTransaction({
2960
+ signature,
2961
+ blockhash,
2962
+ lastValidBlockHeight
2963
+ });
2964
+ if (confirmation.value.err) {
2965
+ throw new Error(
2966
+ `Transaction failed: ${JSON.stringify(confirmation.value.err)}`
2967
+ );
2968
+ }
2969
+ const txDetails = await connection.getTransaction(signature, {
2970
+ commitment: "confirmed",
2971
+ maxSupportedTransactionVersion: 0
2972
+ });
2973
+ const depositSlot = txDetails?.slot ?? 0;
2974
+ let leafIndex = null;
2975
+ let root = null;
2976
+ try {
2977
+ if (txDetails?.meta?.logMessages) {
2978
+ for (const log of txDetails.meta.logMessages) {
2979
+ if (log.includes("Program data:")) {
2980
+ try {
2981
+ const parts = log.split("Program data:");
2982
+ if (parts.length > 1) {
2983
+ const base64Data = parts[1].trim();
2984
+ const eventData = Buffer.from(base64Data, "base64");
2985
+ if (eventData.length >= 81 && eventData[0] === 1) {
2986
+ const parsedLeafIndex = Number(eventData.readBigUInt64LE(1));
2987
+ const loggedCommitment = eventData.slice(9, 41);
2988
+ if (Buffer.compare(loggedCommitment, Buffer.from(commitmentBytes)) === 0) {
2989
+ leafIndex = parsedLeafIndex;
2990
+ const loggedRoot = eventData.slice(49, 81);
2991
+ root = Buffer.from(loggedRoot).toString("hex");
2992
+ break;
2993
+ }
2994
+ }
2995
+ }
2996
+ } catch (e) {
2997
+ continue;
2998
+ }
2999
+ }
3000
+ }
3001
+ }
3002
+ } catch (e) {
3003
+ }
3004
+ if (leafIndex === null) {
3005
+ const programId2 = this.config.programId || CLOAK_PROGRAM_ID;
3006
+ const mintForSOL = NATIVE_SOL_MINT;
3007
+ const { merkleTree } = getShieldPoolPDAs(programId2, mintForSOL);
3008
+ const merkleTreeAccount = await connection.getAccountInfo(merkleTree);
3009
+ if (!merkleTreeAccount) {
3010
+ throw new Error("Failed to fetch merkle tree account");
3011
+ }
3012
+ const nextIndex = Number(merkleTreeAccount.data.readBigUInt64LE(32));
3013
+ leafIndex = nextIndex - 1;
3014
+ const rootBytes = merkleTreeAccount.data.slice(1064, 1096);
3015
+ root = Buffer.from(rootBytes).toString("hex");
3016
+ }
3017
+ if (leafIndex === null || root === null) {
3018
+ throw new Error("Failed to get leaf index and root from transaction logs or on-chain state");
3019
+ }
3020
+ let merkleProof;
3021
+ try {
3022
+ const programId2 = this.config.programId || CLOAK_PROGRAM_ID;
3023
+ const mintForSOL = NATIVE_SOL_MINT;
3024
+ const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId2, mintForSOL);
3025
+ const chainProof = await computeProofFromChain(connection, merkleTreePDA, leafIndex);
3026
+ merkleProof = {
3027
+ pathElements: chainProof.pathElements,
3028
+ pathIndices: chainProof.pathIndices
3029
+ };
3030
+ root = chainProof.root;
3031
+ } catch (proofError) {
3032
+ console.warn("[SDK] Could not compute proof from chain, will fetch at withdrawal:", proofError);
3033
+ }
3034
+ const updatedNote = updateNoteWithDeposit(note, {
3035
+ signature,
3036
+ slot: depositSlot,
3037
+ leafIndex,
3038
+ root,
3039
+ merkleProof
3040
+ });
3041
+ await this.storage.updateNote(note.commitment, {
3042
+ depositSignature: signature,
3043
+ depositSlot,
3044
+ leafIndex,
3045
+ root,
3046
+ merkleProof
3047
+ });
3048
+ return {
3049
+ note: updatedNote,
3050
+ signature,
3051
+ leafIndex,
3052
+ root
3053
+ };
3054
+ } catch (error) {
3055
+ throw this.wrapError(error, "Deposit failed");
3056
+ }
3057
+ }
3058
+ /**
3059
+ * Private transfer with up to 5 recipients
3060
+ *
3061
+ * Handles the complete private transfer flow:
3062
+ * 1. If note is not deposited, deposits it first and waits for confirmation
3063
+ * 2. Generates a zero-knowledge proof
3064
+ * 3. Submits the withdrawal via relay service to recipients
3065
+ *
3066
+ * This is the main method for performing private transfers - it handles everything!
3067
+ *
3068
+ * @param connection - Solana connection (required for deposit if not already deposited)
3069
+ * @param payer - Payer wallet (required for deposit if not already deposited)
3070
+ * @param note - Note to spend (can be deposited or not)
3071
+ * @param recipients - Array of 1-5 recipients with amounts
3072
+ * @param options - Optional configuration
3073
+ * @returns Transfer result with signature and outputs
3074
+ *
3075
+ * @example
3076
+ * ```typescript
3077
+ * // Create a note (not deposited yet)
3078
+ * const note = client.generateNote(1_000_000_000);
3079
+ *
3080
+ * // privateTransfer handles the full flow: deposit + withdraw
3081
+ * const result = await client.privateTransfer(
3082
+ * connection,
3083
+ * wallet,
3084
+ * note,
3085
+ * [
3086
+ * { recipient: new PublicKey("..."), amount: 500_000_000 },
3087
+ * { recipient: new PublicKey("..."), amount: 492_500_000 }
3088
+ * ],
3089
+ * {
3090
+ * relayFeeBps: 50, // 0.5%
3091
+ * onProgress: (status) => console.log(status),
3092
+ * onProofProgress: (pct) => console.log(`Proof: ${pct}%`)
3093
+ * }
3094
+ * );
3095
+ * console.log(`Success! TX: ${result.signature}`);
3096
+ * ```
3097
+ */
3098
+ async privateTransfer(connection, note, recipients, options) {
3099
+ if (!isWithdrawable(note)) {
3100
+ const depositResult = await this.deposit(connection, note, {
3101
+ skipPreflight: false
3102
+ });
3103
+ note = depositResult.note;
3104
+ }
3105
+ const protocolFee = note.amount - getDistributableAmount(note.amount);
3106
+ const feeBps = Math.ceil(protocolFee * 1e4 / note.amount);
3107
+ const distributableAmount = getDistributableAmount(note.amount);
3108
+ validateTransfers(recipients, distributableAmount);
3109
+ if (!note.leafIndex && note.leafIndex !== 0) {
3110
+ throw new Error("Note must have a leaf index (note must be deposited)");
3111
+ }
3112
+ if (!isValidHex(note.r, 32)) {
3113
+ throw new Error("Note r must be 64 hex characters (32 bytes)");
3114
+ }
3115
+ if (!isValidHex(note.sk_spend, 32)) {
3116
+ throw new Error("Note sk_spend must be 64 hex characters (32 bytes)");
3117
+ }
3118
+ const nullifier = await computeNullifierAsync(note.sk_spend, note.leafIndex);
3119
+ const nullifierHex = nullifier.toString(16).padStart(64, "0");
3120
+ const outputsHash = await computeOutputsHashAsync(recipients);
3121
+ const outputsHashHex = outputsHash.toString(16).padStart(64, "0");
3122
+ const circuitsPath2 = await getDefaultCircuitsPath();
3123
+ const useDirectProof = await areCircuitsAvailable(circuitsPath2);
3124
+ if (!useDirectProof) {
3125
+ throw new Error(
3126
+ `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.`
3127
+ );
3128
+ }
3129
+ const MAX_PROOF_RETRIES = 3;
3130
+ let lastError = null;
3131
+ for (let attempt = 0; attempt < MAX_PROOF_RETRIES; attempt++) {
3132
+ try {
3133
+ if (attempt > 0) {
3134
+ options?.onProgress?.(`proof_refresh_${attempt + 1}`);
3135
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
3136
+ }
3137
+ let merkleProof;
3138
+ let merkleRoot;
3139
+ const hasStoredProof = note.merkleProof && note.root && note.merkleProof.pathElements && note.merkleProof.pathElements.length > 0;
3140
+ if (attempt === 0 && hasStoredProof) {
3141
+ merkleProof = note.merkleProof;
3142
+ merkleRoot = note.root;
3143
+ } else {
3144
+ const programId = this.config.programId || CLOAK_PROGRAM_ID;
3145
+ const mintForSOL = NATIVE_SOL_MINT;
3146
+ const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId, mintForSOL);
3147
+ const chainProof = await computeProofFromChain(
3148
+ connection,
3149
+ merkleTreePDA,
3150
+ note.leafIndex
3151
+ );
3152
+ merkleProof = {
3153
+ pathElements: chainProof.pathElements,
3154
+ pathIndices: chainProof.pathIndices,
3155
+ root: chainProof.root
3156
+ };
3157
+ merkleRoot = chainProof.root;
3158
+ note.merkleProof = merkleProof;
3159
+ note.root = merkleRoot;
3160
+ }
3161
+ if (!merkleProof || !merkleRoot) {
3162
+ throw new Error("Failed to get Merkle proof from chain");
3163
+ }
3164
+ if (!merkleProof.pathElements || merkleProof.pathElements.length === 0) {
3165
+ throw new Error("Merkle proof is invalid: missing path elements");
3166
+ }
3167
+ if (merkleProof.pathElements.length !== merkleProof.pathIndices.length) {
3168
+ throw new Error("Merkle proof is invalid: path elements and indices length mismatch");
3169
+ }
3170
+ for (let i = 0; i < merkleProof.pathIndices.length; i++) {
3171
+ const idx = merkleProof.pathIndices[i];
3172
+ if (idx !== 0 && idx !== 1) {
3173
+ throw new Error(`Merkle proof path index at position ${i} must be 0 or 1, got ${idx}`);
3174
+ }
3175
+ }
3176
+ if (!isValidHex(merkleRoot, 32)) {
3177
+ throw new Error("Merkle root must be 64 hex characters (32 bytes)");
3178
+ }
3179
+ for (let i = 0; i < merkleProof.pathElements.length; i++) {
3180
+ const element = merkleProof.pathElements[i];
3181
+ if (typeof element !== "string" || !isValidHex(element, 32)) {
3182
+ throw new Error(`Merkle proof path element at position ${i} must be 64 hex characters (32 bytes)`);
3183
+ }
3184
+ }
3185
+ const sk_spend_bigint = BigInt("0x" + note.sk_spend);
3186
+ const r_bigint = BigInt("0x" + note.r);
3187
+ const root_bigint = BigInt("0x" + merkleRoot);
3188
+ const nullifier_bigint = BigInt("0x" + nullifierHex);
3189
+ const outputs_hash_bigint = BigInt("0x" + outputsHashHex);
3190
+ const amount_bigint = BigInt(note.amount);
3191
+ const pathElements = merkleProof.pathElements.map((p) => BigInt("0x" + p));
3192
+ const outAddr = [];
3193
+ for (let i = 0; i < 5; i++) {
3194
+ if (i < recipients.length) {
3195
+ const [lo, hi] = pubkeyToLimbs(recipients[i].recipient.toBytes());
3196
+ outAddr.push([lo, hi]);
3197
+ } else {
3198
+ outAddr.push([0n, 0n]);
3199
+ }
3200
+ }
3201
+ const t = amount_bigint * 3n;
3202
+ const varFee = t / 1000n;
3203
+ const rem = t % 1000n;
3204
+ const outAmount = [];
3205
+ const outFlags = [];
3206
+ for (let i = 0; i < 5; i++) {
3207
+ if (i < recipients.length) {
3208
+ outAmount.push(BigInt(recipients[i].amount));
3209
+ outFlags.push(1);
3210
+ } else {
3211
+ outAmount.push(0n);
3212
+ outFlags.push(0);
3213
+ }
3214
+ }
3215
+ const sk = splitTo2Limbs(sk_spend_bigint);
3216
+ const r = splitTo2Limbs(r_bigint);
3217
+ if (attempt === 0) {
3218
+ const computedCommitment = await computeCommitment2(amount_bigint, r_bigint, sk_spend_bigint);
3219
+ const noteCommitment = BigInt("0x" + note.commitment);
3220
+ if (computedCommitment !== noteCommitment) {
3221
+ throw new Error(
3222
+ `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.`
3223
+ );
3224
+ }
3225
+ }
3226
+ const proofInputs = {
3227
+ root: root_bigint,
3228
+ nullifier: nullifier_bigint,
3229
+ outputs_hash: outputs_hash_bigint,
3230
+ public_amount: amount_bigint,
3231
+ amount: amount_bigint,
3232
+ leaf_index: BigInt(note.leafIndex),
3233
+ sk,
3234
+ r,
3235
+ pathElements,
3236
+ pathIndices: merkleProof.pathIndices,
3237
+ num_outputs: recipients.length,
3238
+ out_addr: outAddr,
3239
+ out_amount: outAmount,
3240
+ out_flags: outFlags,
3241
+ var_fee: varFee,
3242
+ rem
3243
+ };
3244
+ options?.onProgress?.("proof_generating");
3245
+ const proofResult = await generateWithdrawRegularProof(proofInputs, circuitsPath2);
3246
+ options?.onProgress?.("proof_complete");
3247
+ const proofHex = Buffer.from(proofResult.proofBytes).toString("hex");
3248
+ const finalPublicInputs = {
3249
+ root: merkleRoot,
3250
+ nf: nullifierHex,
3251
+ outputs_hash: outputsHashHex,
3252
+ amount: note.amount
3253
+ };
3254
+ const metadataBundle = options?.metadataBundle;
3255
+ const signature = await this.relay.submitWithdraw(
3256
+ {
3257
+ proof: proofHex,
3258
+ publicInputs: finalPublicInputs,
3259
+ outputs: recipients.map((r2) => ({
3260
+ recipient: r2.recipient.toBase58(),
3261
+ amount: r2.amount
3262
+ })),
3263
+ feeBps,
3264
+ // Use calculated protocol fee BPS
3265
+ metadataBundle
3266
+ },
3267
+ options?.onProgress
3268
+ );
3269
+ return {
3270
+ signature,
3271
+ outputs: recipients.map((r2) => ({
3272
+ recipient: r2.recipient.toBase58(),
3273
+ amount: r2.amount
3274
+ })),
3275
+ nullifier: nullifierHex,
3276
+ root: merkleRoot
3277
+ };
3278
+ } catch (error) {
3279
+ lastError = error instanceof Error ? error : new Error(String(error));
3280
+ if (error instanceof RootNotFoundError) {
3281
+ console.warn(
3282
+ `[Cloak SDK] Merkle root expired (attempt ${attempt + 1}/${MAX_PROOF_RETRIES}). Regenerating proof with fresh root...`
3283
+ );
3284
+ continue;
3285
+ }
3286
+ throw error;
3287
+ }
3288
+ }
3289
+ throw new Error(
3290
+ `Withdrawal failed after ${MAX_PROOF_RETRIES} proof refresh attempts. The Merkle tree is updating too rapidly. Last error: ${lastError?.message}`
2042
3291
  );
2043
3292
  }
2044
- /** True when the SDK was constructed with a wallet adapter (browser). */
2045
- isWalletMode() {
2046
- return !!this.wallet && !this.keypair;
3293
+ /**
3294
+ * Withdraw to a single recipient
3295
+ *
3296
+ * Convenience method for withdrawing to one address.
3297
+ * Handles the complete flow: deposits if needed, then withdraws.
3298
+ *
3299
+ * @param connection - Solana connection
3300
+ * @param payer - Payer wallet
3301
+ * @param note - Note to spend
3302
+ * @param recipient - Recipient address
3303
+ * @param options - Optional configuration
3304
+ * @returns Transfer result
3305
+ *
3306
+ * @example
3307
+ * ```typescript
3308
+ * const note = client.generateNote(1_000_000_000);
3309
+ * const result = await client.withdraw(
3310
+ * connection,
3311
+ * wallet,
3312
+ * note,
3313
+ * new PublicKey("..."),
3314
+ * { withdrawAll: true }
3315
+ * );
3316
+ * ```
3317
+ */
3318
+ async withdraw(connection, note, recipient, options) {
3319
+ const withdrawAll = options?.withdrawAll ?? true;
3320
+ const amount = withdrawAll ? getDistributableAmount(note.amount) : options?.amount || note.amount;
3321
+ if (!withdrawAll && !options?.amount) {
3322
+ throw new Error("Must specify amount or set withdrawAll: true");
3323
+ }
3324
+ return this.privateTransfer(
3325
+ connection,
3326
+ note,
3327
+ [{ recipient, amount }],
3328
+ options
3329
+ );
3330
+ }
3331
+ /**
3332
+ * Send SOL privately to multiple recipients
3333
+ *
3334
+ * Convenience method that wraps privateTransfer with a simpler API.
3335
+ * Handles the complete flow: deposits if needed, then sends to recipients.
3336
+ *
3337
+ * @param connection - Solana connection
3338
+ * @param note - Note to spend
3339
+ * @param recipients - Array of 1-5 recipients with amounts
3340
+ * @param options - Optional configuration
3341
+ * @returns Transfer result
3342
+ *
3343
+ * @example
3344
+ * ```typescript
3345
+ * const note = client.generateNote(1_000_000_000);
3346
+ * const result = await client.send(
3347
+ * connection,
3348
+ * note,
3349
+ * [
3350
+ * { recipient: new PublicKey("..."), amount: 500_000_000 },
3351
+ * { recipient: new PublicKey("..."), amount: 492_500_000 }
3352
+ * ]
3353
+ * );
3354
+ * ```
3355
+ */
3356
+ async send(connection, note, recipients, options) {
3357
+ return this.privateTransfer(connection, note, recipients, options);
3358
+ }
3359
+ /**
3360
+ * Swap SOL for tokens privately
3361
+ *
3362
+ * Withdraws SOL from a note and swaps it for tokens via the relay service.
3363
+ * Handles the complete flow: deposits if needed, generates proof, and submits swap.
3364
+ *
3365
+ * @param connection - Solana connection
3366
+ * @param note - Note to spend
3367
+ * @param recipient - Recipient address (will receive tokens)
3368
+ * @param options - Swap configuration
3369
+ * @returns Swap result with transaction signature
3370
+ *
3371
+ * @example
3372
+ * ```typescript
3373
+ * const note = client.generateNote(1_000_000_000);
3374
+ * const result = await client.swap(
3375
+ * connection,
3376
+ * note,
3377
+ * new PublicKey("..."), // recipient
3378
+ * {
3379
+ * outputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
3380
+ * slippageBps: 100, // 1%
3381
+ * getQuote: async (amount, mint, slippage) => {
3382
+ * // Fetch quote from your swap API
3383
+ * const quote = await fetchSwapQuote(amount, mint, slippage);
3384
+ * return {
3385
+ * outAmount: quote.outAmount,
3386
+ * minOutputAmount: quote.minOutputAmount
3387
+ * };
3388
+ * }
3389
+ * }
3390
+ * );
3391
+ * ```
3392
+ */
3393
+ async swap(connection, note, recipient, options) {
3394
+ try {
3395
+ if (!isWithdrawable(note)) {
3396
+ const depositResult = await this.deposit(connection, note, {
3397
+ skipPreflight: false
3398
+ });
3399
+ note = depositResult.note;
3400
+ }
3401
+ const variableFee = Math.floor(note.amount * 3 / 1e3);
3402
+ const feeBps = note.amount === 0 ? 0 : Math.min(Math.floor((variableFee * 1e4 + note.amount - 1) / note.amount), 65535);
3403
+ const withdrawAmountLamports = getDistributableAmount(note.amount);
3404
+ if (withdrawAmountLamports <= 0) {
3405
+ throw new Error("Amount too small after fees");
3406
+ }
3407
+ let minOutputAmount;
3408
+ if (options.minOutputAmount !== void 0) {
3409
+ minOutputAmount = options.minOutputAmount;
3410
+ } else if (options.getQuote) {
3411
+ const quote = await options.getQuote(
3412
+ withdrawAmountLamports,
3413
+ options.outputMint,
3414
+ options.slippageBps || 100
3415
+ );
3416
+ minOutputAmount = quote.minOutputAmount;
3417
+ } else {
3418
+ throw new Error(
3419
+ "Must provide either minOutputAmount or getQuote function"
3420
+ );
3421
+ }
3422
+ let recipientAta;
3423
+ if (options.recipientAta) {
3424
+ recipientAta = new PublicKey4(options.recipientAta);
3425
+ } else {
3426
+ try {
3427
+ const splTokenModule = await import("@solana/spl-token");
3428
+ const getAssociatedTokenAddress = splTokenModule.getAssociatedTokenAddress;
3429
+ if (!getAssociatedTokenAddress) {
3430
+ throw new Error("getAssociatedTokenAddress not found");
3431
+ }
3432
+ const outputMint2 = new PublicKey4(options.outputMint);
3433
+ recipientAta = await getAssociatedTokenAddress(outputMint2, recipient);
3434
+ } catch (error) {
3435
+ throw new Error(
3436
+ `Failed to get associated token account: ${error instanceof Error ? error.message : String(error)}. Please install @solana/spl-token or provide recipientAta in options.`
3437
+ );
3438
+ }
3439
+ }
3440
+ if (!note.leafIndex && note.leafIndex !== 0) {
3441
+ throw new Error("Note must have a leaf index (note must be deposited)");
3442
+ }
3443
+ const nullifier = await computeNullifierAsync(note.sk_spend, note.leafIndex);
3444
+ const nullifierHex = nullifier.toString(16).padStart(64, "0");
3445
+ const inputMint = new PublicKey4("11111111111111111111111111111111");
3446
+ const outputMint = new PublicKey4(options.outputMint);
3447
+ const outputsHash = await computeSwapOutputsHashAsync(
3448
+ inputMint,
3449
+ outputMint,
3450
+ recipientAta,
3451
+ minOutputAmount,
3452
+ note.amount
3453
+ );
3454
+ const outputsHashHex = outputsHash.toString(16).padStart(64, "0");
3455
+ const circuitsPath2 = await getDefaultCircuitsPath();
3456
+ const useDirectProof = await areCircuitsAvailable(circuitsPath2);
3457
+ if (!useDirectProof) {
3458
+ throw new Error(
3459
+ `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.`
3460
+ );
3461
+ }
3462
+ const MAX_PROOF_RETRIES = 3;
3463
+ let lastError = null;
3464
+ for (let attempt = 0; attempt < MAX_PROOF_RETRIES; attempt++) {
3465
+ try {
3466
+ if (attempt > 0) {
3467
+ options?.onProgress?.(`proof_refresh_${attempt + 1}`);
3468
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
3469
+ }
3470
+ const programId = this.config.programId || CLOAK_PROGRAM_ID;
3471
+ const mintForSOL = NATIVE_SOL_MINT;
3472
+ const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId, mintForSOL);
3473
+ const chainProof = await computeProofFromChain(connection, merkleTreePDA, note.leafIndex);
3474
+ const merkleProof = {
3475
+ pathElements: chainProof.pathElements,
3476
+ pathIndices: chainProof.pathIndices
3477
+ };
3478
+ const merkleRoot = chainProof.root;
3479
+ if (!merkleRoot) {
3480
+ throw new Error("Failed to get Merkle root from chain");
3481
+ }
3482
+ if (!merkleProof.pathElements || merkleProof.pathElements.length === 0) {
3483
+ throw new Error("Merkle proof is invalid: missing path elements");
3484
+ }
3485
+ const sk_spend_bigint = BigInt("0x" + note.sk_spend);
3486
+ const r_bigint = BigInt("0x" + note.r);
3487
+ const root_bigint = BigInt("0x" + merkleRoot);
3488
+ const nullifier_bigint = BigInt("0x" + nullifierHex);
3489
+ const outputs_hash_bigint = BigInt("0x" + outputsHashHex);
3490
+ const amount_bigint = BigInt(note.amount);
3491
+ const pathElements = merkleProof.pathElements.map((p) => BigInt("0x" + p));
3492
+ const inputMintLimbs = pubkeyToLimbs(inputMint.toBytes());
3493
+ const outputMintLimbs = pubkeyToLimbs(outputMint.toBytes());
3494
+ const recipientAtaLimbs = pubkeyToLimbs(recipientAta.toBytes());
3495
+ if (attempt === 0) {
3496
+ const computedCommitment = await computeCommitment2(amount_bigint, r_bigint, sk_spend_bigint);
3497
+ const noteCommitment = BigInt("0x" + note.commitment);
3498
+ if (computedCommitment !== noteCommitment) {
3499
+ throw new Error(
3500
+ `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.`
3501
+ );
3502
+ }
3503
+ }
3504
+ const t = amount_bigint * 3n;
3505
+ const varFee = t / 1000n;
3506
+ const rem = t % 1000n;
3507
+ const proofInputs = {
3508
+ sk_spend: sk_spend_bigint,
3509
+ r: r_bigint,
3510
+ amount: amount_bigint,
3511
+ leaf_index: BigInt(note.leafIndex),
3512
+ path_elements: pathElements,
3513
+ path_indices: merkleProof.pathIndices,
3514
+ root: root_bigint,
3515
+ nullifier: nullifier_bigint,
3516
+ outputs_hash: outputs_hash_bigint,
3517
+ public_amount: amount_bigint,
3518
+ input_mint: inputMintLimbs,
3519
+ output_mint: outputMintLimbs,
3520
+ recipient_ata: recipientAtaLimbs,
3521
+ min_output_amount: BigInt(minOutputAmount),
3522
+ var_fee: varFee,
3523
+ rem
3524
+ };
3525
+ options?.onProgress?.("proof_generating");
3526
+ const proofResult = await generateWithdrawSwapProof(proofInputs, circuitsPath2);
3527
+ options?.onProgress?.("proof_complete");
3528
+ const proofHex = Buffer.from(proofResult.proofBytes).toString("hex");
3529
+ const finalPublicInputs = {
3530
+ root: merkleRoot,
3531
+ nf: nullifierHex,
3532
+ outputs_hash: outputsHashHex,
3533
+ amount: note.amount
3534
+ };
3535
+ const metadataBundle = options?.metadataBundle;
3536
+ const signature = await this.relay.submitSwap(
3537
+ {
3538
+ proof: proofHex,
3539
+ publicInputs: finalPublicInputs,
3540
+ outputs: [
3541
+ {
3542
+ recipient: recipient.toBase58(),
3543
+ amount: withdrawAmountLamports
3544
+ }
3545
+ ],
3546
+ feeBps,
3547
+ swap: {
3548
+ output_mint: options.outputMint,
3549
+ slippage_bps: options.slippageBps || 100,
3550
+ min_output_amount: minOutputAmount
3551
+ },
3552
+ metadataBundle
3553
+ },
3554
+ options.onProgress
3555
+ );
3556
+ return {
3557
+ signature,
3558
+ outputs: [
3559
+ {
3560
+ recipient: recipient.toBase58(),
3561
+ amount: withdrawAmountLamports
3562
+ }
3563
+ ],
3564
+ nullifier: nullifierHex,
3565
+ root: merkleRoot,
3566
+ outputMint: options.outputMint,
3567
+ minOutputAmount
3568
+ };
3569
+ } catch (error) {
3570
+ lastError = error instanceof Error ? error : new Error(String(error));
3571
+ if (error instanceof RootNotFoundError) {
3572
+ console.warn(
3573
+ `[Cloak SDK] Merkle root expired (attempt ${attempt + 1}/${MAX_PROOF_RETRIES}). Regenerating swap proof with fresh root...`
3574
+ );
3575
+ continue;
3576
+ }
3577
+ throw error;
3578
+ }
3579
+ }
3580
+ throw new Error(
3581
+ `Swap failed after ${MAX_PROOF_RETRIES} proof refresh attempts. The Merkle tree is updating too rapidly. Last error: ${lastError?.message}`
3582
+ );
3583
+ } catch (error) {
3584
+ throw this.wrapError(error, "Swap failed");
3585
+ }
3586
+ }
3587
+ /**
3588
+ * Generate a new note without depositing
3589
+ *
3590
+ * @param amountLamports - Amount for the note
3591
+ * @param useWalletKeys - Whether to use wallet keys (v2.0 recommended)
3592
+ * @returns New note (not yet deposited)
3593
+ */
3594
+ async generateNote(amountLamports, useWalletKeys = false) {
3595
+ if (useWalletKeys && this.cloakKeys) {
3596
+ return await generateNoteFromWallet(amountLamports, this.cloakKeys, this.config.network);
3597
+ } else if (useWalletKeys) {
3598
+ const keys = generateCloakKeys();
3599
+ this.cloakKeys = keys;
3600
+ const result = this.storage.saveKeys(keys);
3601
+ if (result instanceof Promise) {
3602
+ result.catch(() => {
3603
+ });
3604
+ }
3605
+ return await generateNoteFromWallet(amountLamports, keys, this.config.network);
3606
+ }
3607
+ return await generateNote(amountLamports, this.config.network);
3608
+ }
3609
+ /**
3610
+ * Parse a note from JSON string
3611
+ *
3612
+ * @param jsonString - JSON representation
3613
+ * @returns Parsed note
3614
+ */
3615
+ parseNote(jsonString) {
3616
+ return parseNote2(jsonString);
3617
+ }
3618
+ /**
3619
+ * Export a note to JSON string
3620
+ *
3621
+ * @param note - Note to export
3622
+ * @param pretty - Format with indentation
3623
+ * @returns JSON string
3624
+ */
3625
+ exportNote(note, pretty = false) {
3626
+ return pretty ? JSON.stringify(note, null, 2) : JSON.stringify(note);
3627
+ }
3628
+ /**
3629
+ * Check if a note is ready for withdrawal
3630
+ *
3631
+ * @param note - Note to check
3632
+ * @returns True if withdrawable
3633
+ */
3634
+ isWithdrawable(note) {
3635
+ return isWithdrawable(note);
2047
3636
  }
2048
3637
  /**
2049
- * Compute a Merkle proof for `leafIndex` from on-chain state. No relay /
2050
- * indexer round-trip required.
3638
+ * Get Merkle proof for a leaf index directly from on-chain state
3639
+ *
3640
+ * @param connection - Solana connection
3641
+ * @param leafIndex - Leaf index in tree
3642
+ * @returns Merkle proof computed from on-chain data
2051
3643
  */
2052
3644
  async getMerkleProof(connection, leafIndex) {
2053
3645
  const programId = this.config.programId || CLOAK_PROGRAM_ID;
2054
- const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId, NATIVE_SOL_MINT);
3646
+ const mintForSOL = NATIVE_SOL_MINT;
3647
+ const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId, mintForSOL);
2055
3648
  const chainProof = await computeProofFromChain(connection, merkleTreePDA, leafIndex);
2056
3649
  return {
2057
3650
  pathElements: chainProof.pathElements,
@@ -2059,20 +3652,72 @@ var CloakSDK = class {
2059
3652
  root: chainProof.root
2060
3653
  };
2061
3654
  }
2062
- /** Read the current SOL-pool Merkle root from on-chain state. */
3655
+ /**
3656
+ * Get current Merkle root directly from on-chain state
3657
+ *
3658
+ * @param connection - Solana connection
3659
+ * @returns Current root hash from on-chain tree
3660
+ */
2063
3661
  async getCurrentRoot(connection) {
2064
3662
  const programId = this.config.programId || CLOAK_PROGRAM_ID;
2065
- const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId, NATIVE_SOL_MINT);
3663
+ const mintForSOL = NATIVE_SOL_MINT;
3664
+ const { merkleTree: merkleTreePDA } = getShieldPoolPDAs(programId, mintForSOL);
2066
3665
  const state = await readMerkleTreeState(connection, merkleTreePDA);
2067
3666
  return state.root;
2068
3667
  }
2069
- /** Poll relay for the status of a previously-submitted request. */
3668
+ /**
3669
+ * Get transaction status from relay service
3670
+ *
3671
+ * @param requestId - Request ID from previous submission
3672
+ * @returns Current status
3673
+ */
2070
3674
  async getTransactionStatus(requestId) {
2071
3675
  return this.relay.getStatus(requestId);
2072
3676
  }
2073
- /** Import wallet keys from JSON; persists to the configured storage adapter. */
3677
+ /**
3678
+ * Fetch and decrypt this user's transaction metadata history.
3679
+ * DEPRECATED: This functionality is no longer supported
3680
+ */
3681
+ async getTransactionMetadata(_options) {
3682
+ throw new Error("getTransactionMetadata is deprecated - use compliance export API instead");
3683
+ }
3684
+ /**
3685
+ * Load all notes from storage
3686
+ *
3687
+ * @returns Array of saved notes
3688
+ */
3689
+ async loadNotes() {
3690
+ const notes = this.storage.loadAllNotes();
3691
+ return Array.isArray(notes) ? notes : await notes;
3692
+ }
3693
+ /**
3694
+ * Save a note to storage
3695
+ *
3696
+ * @param note - Note to save
3697
+ */
3698
+ async saveNote(note) {
3699
+ const result = this.storage.saveNote(note);
3700
+ if (result instanceof Promise) {
3701
+ await result;
3702
+ }
3703
+ }
3704
+ /**
3705
+ * Find a note by its commitment
3706
+ *
3707
+ * @param commitment - Commitment hash
3708
+ * @returns Note if found
3709
+ */
3710
+ async findNote(commitment) {
3711
+ const notes = await this.loadNotes();
3712
+ return findNoteByCommitment(notes, commitment);
3713
+ }
3714
+ /**
3715
+ * Import wallet keys from JSON
3716
+ *
3717
+ * @param keysJson - JSON string containing keys
3718
+ */
2074
3719
  async importWalletKeys(keysJson) {
2075
- const keys = importKeys(keysJson);
3720
+ const keys = importWalletKeys(keysJson);
2076
3721
  this.cloakKeys = keys;
2077
3722
  const result = this.storage.saveKeys(keys);
2078
3723
  if (result instanceof Promise) {
@@ -2080,32 +3725,134 @@ var CloakSDK = class {
2080
3725
  }
2081
3726
  }
2082
3727
  /**
2083
- * Export wallet keys to JSON.
2084
- *
2085
- * WARNING: this exports secret keys. Store securely.
3728
+ * Export wallet keys to JSON
3729
+ *
3730
+ * WARNING: This exports secret keys! Store securely.
3731
+ *
3732
+ * @returns JSON string with keys
2086
3733
  */
2087
3734
  exportWalletKeys() {
2088
3735
  if (!this.cloakKeys) {
2089
3736
  throw new CloakError("No wallet keys available", "wallet", false);
2090
3737
  }
2091
- return exportKeys(this.cloakKeys);
3738
+ return exportWalletKeys(this.cloakKeys);
2092
3739
  }
2093
3740
  /**
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.
3741
+ * Get the configuration
2103
3742
  */
2104
3743
  getConfig() {
2105
3744
  return { ...this.config };
2106
3745
  }
3746
+ // Note: scanNotes was removed - it required the indexer to store encrypted outputs.
3747
+ // Users should maintain their own note storage. This is more privacy-preserving.
3748
+ /**
3749
+ * Wrap errors with better categorization and user-friendly messages
3750
+ *
3751
+ * @private
3752
+ */
3753
+ wrapError(error, context) {
3754
+ if (error instanceof CloakError) {
3755
+ return error;
3756
+ }
3757
+ const errorMessage = error instanceof Error ? error.message : String(error);
3758
+ if (errorMessage.includes("duplicate key") || errorMessage.includes("already deposited")) {
3759
+ return new CloakError(
3760
+ "This note was already deposited. Generate a new note to continue.",
3761
+ "validation",
3762
+ false,
3763
+ error instanceof Error ? error : void 0
3764
+ );
3765
+ }
3766
+ if (errorMessage.includes("insufficient funds") || errorMessage.includes("insufficient lamports")) {
3767
+ return new CloakError(
3768
+ "Insufficient funds for this transaction. Please check your wallet balance.",
3769
+ "wallet",
3770
+ false,
3771
+ error instanceof Error ? error : void 0
3772
+ );
3773
+ }
3774
+ if (errorMessage.includes("Merkle tree") || errorMessage.includes("merkle")) {
3775
+ return new CloakError(
3776
+ "Failed to read on-chain Merkle tree state. Please try again.",
3777
+ "network",
3778
+ true,
3779
+ error instanceof Error ? error : void 0
3780
+ );
3781
+ }
3782
+ if (errorMessage.includes("timeout") || errorMessage.includes("timed out")) {
3783
+ return new CloakError(
3784
+ "Network timeout. Please check your connection and try again.",
3785
+ "network",
3786
+ true,
3787
+ error instanceof Error ? error : void 0
3788
+ );
3789
+ }
3790
+ if (errorMessage.includes("not connected") || errorMessage.includes("wallet")) {
3791
+ return new CloakError(
3792
+ "Wallet not connected. Please connect your wallet first.",
3793
+ "wallet",
3794
+ false,
3795
+ error instanceof Error ? error : void 0
3796
+ );
3797
+ }
3798
+ if (errorMessage.includes("proof") && (errorMessage.includes("failed") || errorMessage.includes("error"))) {
3799
+ return new CloakError(
3800
+ "Zero-knowledge proof generation failed. This is usually temporary - please try again.",
3801
+ "prover",
3802
+ true,
3803
+ error instanceof Error ? error : void 0
3804
+ );
3805
+ }
3806
+ if (errorMessage.includes("relay") || errorMessage.includes("withdraw")) {
3807
+ return new CloakError(
3808
+ "Relay service error. Please try again later.",
3809
+ "relay",
3810
+ true,
3811
+ error instanceof Error ? error : void 0
3812
+ );
3813
+ }
3814
+ return new CloakError(
3815
+ `${context}: ${errorMessage}`,
3816
+ "network",
3817
+ false,
3818
+ error instanceof Error ? error : void 0
3819
+ );
3820
+ }
2107
3821
  };
2108
3822
 
3823
+ // src/core/note.ts
3824
+ function serializeNote(note, pretty = false) {
3825
+ validateNote(note);
3826
+ return pretty ? JSON.stringify(note, null, 2) : JSON.stringify(note);
3827
+ }
3828
+ function downloadNote(note, filename) {
3829
+ const g = globalThis;
3830
+ const doc = g?.document;
3831
+ const URL_ = g?.URL;
3832
+ const Blob_ = g?.Blob;
3833
+ if (!doc || !URL_ || !Blob_) {
3834
+ throw new Error("downloadNote is only available in browser environments");
3835
+ }
3836
+ const json = serializeNote(note, true);
3837
+ const blob = new Blob_([json], { type: "application/json" });
3838
+ const url = URL_.createObjectURL(blob);
3839
+ const defaultFilename = `cloak-note-${note.commitment.slice(0, 8)}.json`;
3840
+ const link = doc.createElement("a");
3841
+ link.href = url;
3842
+ link.download = filename || defaultFilename;
3843
+ link.click();
3844
+ URL_.revokeObjectURL(url);
3845
+ }
3846
+ async function copyNoteToClipboard(note) {
3847
+ const g = globalThis;
3848
+ const nav = g?.navigator;
3849
+ if (!nav || !nav.clipboard) {
3850
+ throw new Error("Clipboard API not available");
3851
+ }
3852
+ const json = serializeNote(note, true);
3853
+ await nav.clipboard.writeText(json);
3854
+ }
3855
+
2109
3856
  // src/core/compliance-keys.ts
2110
3857
  import nacl2 from "tweetnacl";
2111
3858
  import { blake3 as blake32 } from "@noble/hashes/blake3";
@@ -2492,107 +4239,6 @@ async function decryptComplianceMetadataWithMasterKey(encrypted, masterComplianc
2492
4239
  return decryptWithSharedSecret(payload, sharedSecret);
2493
4240
  }
2494
4241
 
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
4242
  // src/utils/verify-utxos.ts
2597
4243
  async function verifyUtxos(utxos, connection, programId, commitment = "confirmed") {
2598
4244
  const checkable = [];
@@ -2721,6 +4367,45 @@ async function fetchRiskQuoteIx(connection, wallet, rangeApiKey) {
2721
4367
  };
2722
4368
  }
2723
4369
 
4370
+ // src/helpers/encrypted-output.ts
4371
+ function prepareEncryptedOutput(note, cloakKeys) {
4372
+ const noteData = {
4373
+ amount: note.amount,
4374
+ r: note.r,
4375
+ sk_spend: note.sk_spend,
4376
+ commitment: note.commitment
4377
+ };
4378
+ const encrypted = encryptNoteForRecipient(noteData, cloakKeys.view.pvk);
4379
+ return btoa(JSON.stringify(encrypted));
4380
+ }
4381
+ function prepareEncryptedOutputForRecipient(note, recipientPvkHex) {
4382
+ const noteData = {
4383
+ amount: note.amount,
4384
+ r: note.r,
4385
+ sk_spend: note.sk_spend,
4386
+ commitment: note.commitment
4387
+ };
4388
+ const recipientPvk = hexToBytes(recipientPvkHex);
4389
+ const encrypted = encryptNoteForRecipient(noteData, recipientPvk);
4390
+ return btoa(JSON.stringify(encrypted));
4391
+ }
4392
+ function encodeNoteSimple(note) {
4393
+ const data = {
4394
+ amount: note.amount,
4395
+ r: note.r,
4396
+ sk_spend: note.sk_spend,
4397
+ commitment: note.commitment
4398
+ };
4399
+ const json = JSON.stringify(data);
4400
+ if (typeof Buffer !== "undefined") {
4401
+ return Buffer.from(json).toString("base64");
4402
+ } else if (typeof btoa !== "undefined") {
4403
+ return btoa(json);
4404
+ } else {
4405
+ throw new Error("No base64 encoding method available");
4406
+ }
4407
+ }
4408
+
2724
4409
  // src/helpers/wallet-integration.ts
2725
4410
  import {
2726
4411
  Keypair as Keypair2
@@ -3232,14 +4917,181 @@ async function preflightCheck(relayUrl, rootHex) {
3232
4917
  };
3233
4918
  }
3234
4919
 
4920
+ // src/utils/pending-operations.ts
4921
+ var PENDING_DEPOSITS_KEY = "cloak_pending_deposits";
4922
+ var PENDING_WITHDRAWALS_KEY = "cloak_pending_withdrawals";
4923
+ function getStorage() {
4924
+ if (typeof globalThis !== "undefined" && globalThis.localStorage) {
4925
+ return globalThis.localStorage;
4926
+ }
4927
+ return null;
4928
+ }
4929
+ function savePendingDeposit(deposit) {
4930
+ const storage = getStorage();
4931
+ if (!storage) {
4932
+ console.warn("localStorage not available - pending deposit not persisted");
4933
+ return;
4934
+ }
4935
+ const deposits = loadPendingDeposits();
4936
+ const index = deposits.findIndex((d) => d.note.commitment === deposit.note.commitment);
4937
+ if (index >= 0) {
4938
+ deposits[index] = deposit;
4939
+ } else {
4940
+ deposits.push(deposit);
4941
+ }
4942
+ storage.setItem(PENDING_DEPOSITS_KEY, JSON.stringify(deposits));
4943
+ }
4944
+ function loadPendingDeposits() {
4945
+ const storage = getStorage();
4946
+ if (!storage) return [];
4947
+ const stored = storage.getItem(PENDING_DEPOSITS_KEY);
4948
+ if (!stored) return [];
4949
+ try {
4950
+ return JSON.parse(stored);
4951
+ } catch {
4952
+ return [];
4953
+ }
4954
+ }
4955
+ function updatePendingDeposit(commitment, updates) {
4956
+ const storage = getStorage();
4957
+ if (!storage) return;
4958
+ const deposits = loadPendingDeposits();
4959
+ const index = deposits.findIndex((d) => d.note.commitment === commitment);
4960
+ if (index >= 0) {
4961
+ deposits[index] = { ...deposits[index], ...updates };
4962
+ storage.setItem(PENDING_DEPOSITS_KEY, JSON.stringify(deposits));
4963
+ }
4964
+ }
4965
+ function removePendingDeposit(commitment) {
4966
+ const storage = getStorage();
4967
+ if (!storage) return;
4968
+ const deposits = loadPendingDeposits();
4969
+ const filtered = deposits.filter((d) => d.note.commitment !== commitment);
4970
+ storage.setItem(PENDING_DEPOSITS_KEY, JSON.stringify(filtered));
4971
+ }
4972
+ function clearPendingDeposits() {
4973
+ const storage = getStorage();
4974
+ if (storage) {
4975
+ storage.removeItem(PENDING_DEPOSITS_KEY);
4976
+ }
4977
+ }
4978
+ function savePendingWithdrawal(withdrawal) {
4979
+ const storage = getStorage();
4980
+ if (!storage) {
4981
+ console.warn("localStorage not available - pending withdrawal not persisted");
4982
+ return;
4983
+ }
4984
+ const withdrawals = loadPendingWithdrawals();
4985
+ const index = withdrawals.findIndex((w) => w.requestId === withdrawal.requestId);
4986
+ if (index >= 0) {
4987
+ withdrawals[index] = withdrawal;
4988
+ } else {
4989
+ withdrawals.push(withdrawal);
4990
+ }
4991
+ storage.setItem(PENDING_WITHDRAWALS_KEY, JSON.stringify(withdrawals));
4992
+ }
4993
+ function loadPendingWithdrawals() {
4994
+ const storage = getStorage();
4995
+ if (!storage) return [];
4996
+ const stored = storage.getItem(PENDING_WITHDRAWALS_KEY);
4997
+ if (!stored) return [];
4998
+ try {
4999
+ return JSON.parse(stored);
5000
+ } catch {
5001
+ return [];
5002
+ }
5003
+ }
5004
+ function updatePendingWithdrawal(requestId, updates) {
5005
+ const storage = getStorage();
5006
+ if (!storage) return;
5007
+ const withdrawals = loadPendingWithdrawals();
5008
+ const index = withdrawals.findIndex((w) => w.requestId === requestId);
5009
+ if (index >= 0) {
5010
+ withdrawals[index] = { ...withdrawals[index], ...updates };
5011
+ storage.setItem(PENDING_WITHDRAWALS_KEY, JSON.stringify(withdrawals));
5012
+ }
5013
+ }
5014
+ function removePendingWithdrawal(requestId) {
5015
+ const storage = getStorage();
5016
+ if (!storage) return;
5017
+ const withdrawals = loadPendingWithdrawals();
5018
+ const filtered = withdrawals.filter((w) => w.requestId !== requestId);
5019
+ storage.setItem(PENDING_WITHDRAWALS_KEY, JSON.stringify(filtered));
5020
+ }
5021
+ function clearPendingWithdrawals() {
5022
+ const storage = getStorage();
5023
+ if (storage) {
5024
+ storage.removeItem(PENDING_WITHDRAWALS_KEY);
5025
+ }
5026
+ }
5027
+ function hasPendingOperations() {
5028
+ const deposits = loadPendingDeposits();
5029
+ const withdrawals = loadPendingWithdrawals();
5030
+ const pendingDeposits = deposits.filter(
5031
+ (d) => d.status === "pending" || d.status === "tx_sent"
5032
+ );
5033
+ const pendingWithdrawals = withdrawals.filter(
5034
+ (w) => w.status === "pending" || w.status === "processing"
5035
+ );
5036
+ return pendingDeposits.length > 0 || pendingWithdrawals.length > 0;
5037
+ }
5038
+ function getPendingOperationsSummary() {
5039
+ const deposits = loadPendingDeposits().filter(
5040
+ (d) => d.status === "pending" || d.status === "tx_sent"
5041
+ );
5042
+ const withdrawals = loadPendingWithdrawals().filter(
5043
+ (w) => w.status === "pending" || w.status === "processing"
5044
+ );
5045
+ return {
5046
+ deposits,
5047
+ withdrawals,
5048
+ totalPending: deposits.length + withdrawals.length
5049
+ };
5050
+ }
5051
+ function cleanupStalePendingOperations(maxAgeMs = 24 * 60 * 60 * 1e3) {
5052
+ const now = Date.now();
5053
+ let removedDeposits = 0;
5054
+ let removedWithdrawals = 0;
5055
+ const deposits = loadPendingDeposits();
5056
+ const activeDeposits = deposits.filter((d) => {
5057
+ const age = now - d.startedAt;
5058
+ const isStale = age > maxAgeMs;
5059
+ const isTerminal = d.status === "confirmed" || d.status === "failed";
5060
+ if (isStale || isTerminal) {
5061
+ removedDeposits++;
5062
+ return false;
5063
+ }
5064
+ return true;
5065
+ });
5066
+ const storage = getStorage();
5067
+ if (storage) {
5068
+ storage.setItem(PENDING_DEPOSITS_KEY, JSON.stringify(activeDeposits));
5069
+ }
5070
+ const withdrawals = loadPendingWithdrawals();
5071
+ const activeWithdrawals = withdrawals.filter((w) => {
5072
+ const age = now - w.startedAt;
5073
+ const isStale = age > maxAgeMs;
5074
+ const isTerminal = w.status === "completed" || w.status === "failed";
5075
+ if (isStale || isTerminal) {
5076
+ removedWithdrawals++;
5077
+ return false;
5078
+ }
5079
+ return true;
5080
+ });
5081
+ if (storage) {
5082
+ storage.setItem(PENDING_WITHDRAWALS_KEY, JSON.stringify(activeWithdrawals));
5083
+ }
5084
+ return { removedDeposits, removedWithdrawals };
5085
+ }
5086
+
3235
5087
  // src/core/transact.ts
3236
5088
  import {
3237
- PublicKey as PublicKey5,
3238
- Transaction as Transaction2,
3239
- TransactionInstruction,
5089
+ PublicKey as PublicKey6,
5090
+ Transaction as Transaction3,
5091
+ TransactionInstruction as TransactionInstruction3,
3240
5092
  sendAndConfirmTransaction,
3241
- SystemProgram,
3242
- ComputeBudgetProgram,
5093
+ SystemProgram as SystemProgram2,
5094
+ ComputeBudgetProgram as ComputeBudgetProgram2,
3243
5095
  SYSVAR_INSTRUCTIONS_PUBKEY,
3244
5096
  SYSVAR_SLOT_HASHES_PUBKEY,
3245
5097
  VersionedTransaction,
@@ -3252,7 +5104,7 @@ import {
3252
5104
  getAssociatedTokenAddressSync,
3253
5105
  TOKEN_PROGRAM_ID
3254
5106
  } from "@solana/spl-token";
3255
- import * as snarkjs from "snarkjs";
5107
+ import * as snarkjs2 from "snarkjs";
3256
5108
 
3257
5109
  // src/core/chain-note.ts
3258
5110
  var NOTE_VERSION = 2;
@@ -3402,7 +5254,7 @@ function chainNoteFromBase64(base64) {
3402
5254
  // src/core/transact.ts
3403
5255
  import { buildPoseidon as buildPoseidon3 } from "circomlibjs";
3404
5256
  import nacl4 from "tweetnacl";
3405
- var IS_BROWSER = typeof window !== "undefined" || typeof globalThis.document !== "undefined";
5257
+ var IS_BROWSER2 = typeof window !== "undefined" || typeof globalThis.document !== "undefined";
3406
5258
  var DEFAULT_TRANSACTION_CIRCUITS_URL = "https://storage.googleapis.com/cloak-circuits/circuits/0.1.0";
3407
5259
  var DEFAULT_CLOAK_RELAY_URL = "https://api.cloak.ag";
3408
5260
  function resolveDefaultTransactionCircuitsPath() {
@@ -3415,7 +5267,7 @@ async function resolveTransactionCircuitFiles() {
3415
5267
  const baseNorm = base.endsWith("/") ? base : `${base}/`;
3416
5268
  const wasmUrl = `${baseNorm}transaction_js/transaction.wasm`;
3417
5269
  const zkeyUrl = `${baseNorm}transaction_final.zkey`;
3418
- if (IS_BROWSER) {
5270
+ if (IS_BROWSER2) {
3419
5271
  return { wasmPath: wasmUrl, zkeyPath: zkeyUrl };
3420
5272
  }
3421
5273
  if (_remoteTxCircuitsCache?.base === baseNorm) {
@@ -3552,7 +5404,7 @@ async function resolveAddressLookupTableAccounts(connection, relayUrl, altAddres
3552
5404
  const fetched = await Promise.all(
3553
5405
  altAddresses.map(async (addr) => {
3554
5406
  try {
3555
- const pubkey = new PublicKey5(addr);
5407
+ const pubkey = new PublicKey6(addr);
3556
5408
  const result = await connection.getAddressLookupTable(pubkey);
3557
5409
  return result.value ?? null;
3558
5410
  } catch {
@@ -3714,7 +5566,7 @@ async function generateTransactionProof(inputs, onProgress) {
3714
5566
  onProgress?.(10);
3715
5567
  const { wasmPath: wasmInput, zkeyPath: zkeyInput } = await resolveTransactionCircuitFiles();
3716
5568
  onProgress?.(30);
3717
- const { proof, publicSignals } = await snarkjs.groth16.fullProve(
5569
+ const { proof, publicSignals } = await snarkjs2.groth16.fullProve(
3718
5570
  inputs,
3719
5571
  wasmInput,
3720
5572
  zkeyInput
@@ -3794,7 +5646,7 @@ function buildPublicInputsBytesFromSignals(publicSignals) {
3794
5646
  }
3795
5647
  var TRANSACT_DISCRIMINATOR = 0;
3796
5648
  function deriveNullifierPDA(programId, poolPDA, nullifier) {
3797
- return PublicKey5.findProgramAddressSync(
5649
+ return PublicKey6.findProgramAddressSync(
3798
5650
  [Buffer.from("nullifier"), poolPDA.toBuffer(), Buffer.from(nullifier)],
3799
5651
  programId
3800
5652
  );
@@ -3837,7 +5689,7 @@ function buildTransactInstruction(programId, payer, poolPDA, treasuryPDA, merkle
3837
5689
  // 4. nullifier PDA 0 (writable)
3838
5690
  { pubkey: nullifierPDA1, isSigner: false, isWritable: true },
3839
5691
  // 5. nullifier PDA 1 (writable)
3840
- { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
5692
+ { pubkey: SystemProgram2.programId, isSigner: false, isWritable: false }
3841
5693
  // 6. system_program
3842
5694
  ];
3843
5695
  if (splAccounts) {
@@ -3862,7 +5714,7 @@ function buildTransactInstruction(programId, payer, poolPDA, treasuryPDA, merkle
3862
5714
  accounts.push({ pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false });
3863
5715
  }
3864
5716
  }
3865
- return new TransactionInstruction({
5717
+ return new TransactionInstruction3({
3866
5718
  programId,
3867
5719
  keys: accounts,
3868
5720
  data
@@ -3870,10 +5722,10 @@ function buildTransactInstruction(programId, payer, poolPDA, treasuryPDA, merkle
3870
5722
  }
3871
5723
  function getCommonALTAddresses() {
3872
5724
  return [
3873
- SystemProgram.programId,
5725
+ SystemProgram2.programId,
3874
5726
  SYSVAR_SLOT_HASHES_PUBKEY,
3875
5727
  SYSVAR_INSTRUCTIONS_PUBKEY,
3876
- ComputeBudgetProgram.programId
5728
+ ComputeBudgetProgram2.programId
3877
5729
  ];
3878
5730
  }
3879
5731
  function dedupePubkeys(addresses) {
@@ -3934,7 +5786,7 @@ async function createEphemeralALT(connection, depositor, onProgress, additionalA
3934
5786
  lookupTable: altAddress,
3935
5787
  addresses: addressesForCreate
3936
5788
  });
3937
- const tx = new Transaction2().add(createIx).add(extendIx);
5789
+ const tx = new Transaction3().add(createIx).add(extendIx);
3938
5790
  const { blockhash } = await connection.getLatestBlockhash();
3939
5791
  tx.recentBlockhash = blockhash;
3940
5792
  tx.feePayer = depositor.publicKey;
@@ -4100,8 +5952,8 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
4100
5952
  if (riskQuoteIx) {
4101
5953
  baseInstructions.unshift(riskQuoteIx);
4102
5954
  }
4103
- const cuLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 4e5 });
4104
- const cuPriceIx = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1e5 });
5955
+ const cuLimitIx = ComputeBudgetProgram2.setComputeUnitLimit({ units: 4e5 });
5956
+ const cuPriceIx = ComputeBudgetProgram2.setComputeUnitPrice({ microLamports: 1e5 });
4105
5957
  const fullInstructions = [...baseInstructions, cuLimitIx, cuPriceIx, transactIx];
4106
5958
  const compactInstructions = [...baseInstructions, cuLimitIx, transactIx];
4107
5959
  const minimalInstructions = [...baseInstructions, transactIx];
@@ -4163,6 +6015,38 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
4163
6015
  const sendV0 = async (ixs, options) => {
4164
6016
  const maxBlockhashRetries = 2;
4165
6017
  const maxTransportRetries = 3;
6018
+ const alreadyLanded = async (sig) => {
6019
+ if (!sig) return false;
6020
+ for (let i = 0; i < 6; i++) {
6021
+ try {
6022
+ const { value } = await connection.getSignatureStatus(sig, {
6023
+ searchTransactionHistory: true
6024
+ });
6025
+ if (value) {
6026
+ if (value.err) return false;
6027
+ if (value.confirmationStatus === "confirmed" || value.confirmationStatus === "finalized") {
6028
+ return true;
6029
+ }
6030
+ }
6031
+ } catch {
6032
+ }
6033
+ await new Promise((r) => setTimeout(r, 400));
6034
+ }
6035
+ return false;
6036
+ };
6037
+ const looksLikeDoubleSpend = (error) => {
6038
+ const m = error instanceof Error ? error.message : String(error);
6039
+ return m.includes("0x1020") || // confirmTransaction().value.err is JSON-stringified, so 0x1020 appears as
6040
+ // its decimal form ("Custom":4128) rather than the hex "0x1020".
6041
+ m.includes('"Custom":4128') || m.includes("already been processed") || m.toLowerCase().includes("doublespend");
6042
+ };
6043
+ const sentSigs = [];
6044
+ const anyLanded = async () => {
6045
+ for (const s of sentSigs) {
6046
+ if (await alreadyLanded(s)) return s;
6047
+ }
6048
+ return void 0;
6049
+ };
4166
6050
  for (let blockhashRetry = 0; blockhashRetry <= maxBlockhashRetries; blockhashRetry++) {
4167
6051
  if (blockhashRetry > 0) {
4168
6052
  ({ blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash());
@@ -4185,6 +6069,7 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
4185
6069
  signature = await connection.sendRawTransaction(versionedTx.serialize(), {
4186
6070
  skipPreflight: options?.skipPreflight ?? false
4187
6071
  });
6072
+ sentSigs.push(signature);
4188
6073
  onProgress?.("Confirming transaction...");
4189
6074
  const confirmation = await connection.confirmTransaction({
4190
6075
  signature,
@@ -4201,6 +6086,7 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
4201
6086
  signature = await connection.sendRawTransaction(signedTx.serialize(), {
4202
6087
  skipPreflight: options?.skipPreflight ?? false
4203
6088
  });
6089
+ sentSigs.push(signature);
4204
6090
  onProgress?.("Confirming transaction...");
4205
6091
  const confirmation = await connection.confirmTransaction({
4206
6092
  signature,
@@ -4215,8 +6101,10 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
4215
6101
  }
4216
6102
  return signature;
4217
6103
  } catch (error) {
4218
- if (isBlockhashExpiryError(error) && blockhashRetry < maxBlockhashRetries) {
4219
- break;
6104
+ if (isBlockhashExpiryError(error)) {
6105
+ const landed = await anyLanded();
6106
+ if (landed) return landed;
6107
+ if (blockhashRetry < maxBlockhashRetries) break;
4220
6108
  }
4221
6109
  if (isTransientTransportError(error) && transportAttempt < maxTransportRetries) {
4222
6110
  transportAttempt++;
@@ -4228,6 +6116,10 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
4228
6116
  ({ blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash());
4229
6117
  continue;
4230
6118
  }
6119
+ if (looksLikeDoubleSpend(error)) {
6120
+ const landed = await anyLanded();
6121
+ if (landed) return landed;
6122
+ }
4231
6123
  throw error;
4232
6124
  }
4233
6125
  }
@@ -4310,7 +6202,7 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
4310
6202
  let legacyTransportAttempt = 0;
4311
6203
  while (true) {
4312
6204
  try {
4313
- const tx = new Transaction2();
6205
+ const tx = new Transaction3();
4314
6206
  for (const ix of instructions) {
4315
6207
  tx.add(ix);
4316
6208
  }
@@ -4492,7 +6384,7 @@ async function fetchRiskQuoteInstruction(riskQuoteUrl, wallet, options) {
4492
6384
  const signerB58 = signerKey;
4493
6385
  const signatureBuf = Buffer.from(signatureB64, "base64");
4494
6386
  const messageArr = hexToBytes2(messageHex);
4495
- const publicKeyBytes = new PublicKey5(signerB58).toBytes();
6387
+ const publicKeyBytes = new PublicKey6(signerB58).toBytes();
4496
6388
  if (signatureBuf.length !== 64 || messageArr.length !== 33 && messageArr.length !== 41 && messageArr.length !== 50 || publicKeyBytes.length !== 32) {
4497
6389
  throw new Error(
4498
6390
  `Invalid risk quote response: invalid lengths (sig=${signatureBuf.length}, msg=${messageArr.length}, pk=${publicKeyBytes.length})`
@@ -4514,10 +6406,10 @@ async function fetchRiskQuoteInstruction(riskQuoteUrl, wallet, options) {
4514
6406
  `Invalid risk quote response: expected relay format { signature, message, signer_pubkey } or { instruction }. Received keys: ${keys}`
4515
6407
  );
4516
6408
  }
4517
- return new TransactionInstruction({
4518
- programId: new PublicKey5(raw.programId),
6409
+ return new TransactionInstruction3({
6410
+ programId: new PublicKey6(raw.programId),
4519
6411
  keys: raw.keys.map((k) => ({
4520
- pubkey: new PublicKey5(k.pubkey),
6412
+ pubkey: new PublicKey6(k.pubkey),
4521
6413
  isSigner: k.isSigner,
4522
6414
  isWritable: k.isWritable
4523
6415
  })),
@@ -5401,12 +7293,12 @@ async function transact(params, options) {
5401
7293
  if (lastError) {
5402
7294
  const classified = classifyRelayError(lastError.message);
5403
7295
  const isMerkleClass = !(classified instanceof UtxoAlreadySpentError) && !(classified instanceof SanctionsQuoteError) && !isRootNotFoundError2(lastError);
5404
- if (!IS_BROWSER && isMerkleClass && relayUrl) {
7296
+ if (!IS_BROWSER2 && isMerkleClass && relayUrl) {
5405
7297
  onProgress?.(
5406
7298
  "All relay-tree attempts failed. Rebuilding merkle tree from chain as last resort..."
5407
7299
  );
5408
7300
  try {
5409
- const [merkleTreePda] = PublicKey5.findProgramAddressSync(
7301
+ const [merkleTreePda] = PublicKey6.findProgramAddressSync(
5410
7302
  [Buffer.from("merkle_tree"), mint.toBuffer()],
5411
7303
  programId
5412
7304
  );
@@ -6475,7 +8367,7 @@ async function swapWithChange(inputUtxos, swapAmount, outputMint, recipientAta,
6475
8367
  // src/core/scanner.ts
6476
8368
  import _bs582 from "bs58";
6477
8369
  import {
6478
- PublicKey as PublicKey6
8370
+ PublicKey as PublicKey7
6479
8371
  } from "@solana/web3.js";
6480
8372
  import { getAssociatedTokenAddressSync as getAssociatedTokenAddressSync2 } from "@solana/spl-token";
6481
8373
  var bs582 = _bs582.default || _bs582;
@@ -6563,7 +8455,7 @@ function parseSwapOutputMint(data) {
6563
8455
  const mintBytes = data.slice(SWAP_OUTPUT_MINT_OFFSET, end);
6564
8456
  if (mintBytes.every((byte) => byte === 0)) return void 0;
6565
8457
  try {
6566
- return new PublicKey6(mintBytes).toBase58();
8458
+ return new PublicKey7(mintBytes).toBase58();
6567
8459
  } catch {
6568
8460
  return void 0;
6569
8461
  }
@@ -6653,7 +8545,7 @@ function parseSwapRecipientAta(data) {
6653
8545
  const recipientAtaBytes = data.slice(recipientAtaStart, recipientAtaEnd);
6654
8546
  if (recipientAtaBytes.every((byte) => byte === 0)) return void 0;
6655
8547
  try {
6656
- return new PublicKey6(recipientAtaBytes).toBase58();
8548
+ return new PublicKey7(recipientAtaBytes).toBase58();
6657
8549
  } catch {
6658
8550
  return void 0;
6659
8551
  }
@@ -6968,8 +8860,8 @@ async function scanTransactions(opts) {
6968
8860
  if (onChainAta) {
6969
8861
  try {
6970
8862
  const expectedAta = getAssociatedTokenAddressSync2(
6971
- new PublicKey6(asset.mint),
6972
- new PublicKey6(walletPublicKey)
8863
+ new PublicKey7(asset.mint),
8864
+ new PublicKey7(walletPublicKey)
6973
8865
  ).toBase58();
6974
8866
  if (onChainAta === expectedAta) {
6975
8867
  isOurs = true;
@@ -7272,7 +9164,7 @@ function formatComplianceCsv(report) {
7272
9164
  }
7273
9165
 
7274
9166
  // src/core/wallet.ts
7275
- import { PublicKey as PublicKey7 } from "@solana/web3.js";
9167
+ import { PublicKey as PublicKey8 } from "@solana/web3.js";
7276
9168
  var UtxoWallet = class _UtxoWallet {
7277
9169
  constructor(viewingKey) {
7278
9170
  this.wallets = /* @__PURE__ */ new Map();
@@ -7484,7 +9376,7 @@ var UtxoWallet = class _UtxoWallet {
7484
9376
  data.viewingKey ? new Uint8Array(data.viewingKey) : void 0
7485
9377
  );
7486
9378
  for (const w of data.wallets) {
7487
- const mint = new PublicKey7(w.mint);
9379
+ const mint = new PublicKey8(w.mint);
7488
9380
  for (const u of w.utxos) {
7489
9381
  wallet.addUtxo({
7490
9382
  amount: BigInt(u.amount),
@@ -7567,13 +9459,15 @@ var SimpleWallet = class {
7567
9459
  };
7568
9460
 
7569
9461
  // src/index.ts
7570
- var VERSION = "0.1.6";
9462
+ var VERSION = "0.1.5";
7571
9463
  var SCANNER_SUPPORTS_TRANSACT_SWAP = true;
7572
9464
  export {
7573
9465
  CLOAK_PROGRAM_ID,
7574
9466
  CloakError,
7575
9467
  CloakSDK,
9468
+ DEFAULT_CIRCUITS_URL,
7576
9469
  DEFAULT_TRANSACTION_CIRCUITS_URL,
9470
+ EXPECTED_CIRCUIT_HASHES,
7577
9471
  FIXED_FEE_LAMPORTS,
7578
9472
  LAMPORTS_PER_SOL,
7579
9473
  LocalStorageAdapter,
@@ -7596,6 +9490,7 @@ export {
7596
9490
  VARIABLE_FEE_NUMERATOR,
7597
9491
  VARIABLE_FEE_RATE,
7598
9492
  VERSION,
9493
+ areCircuitsAvailable,
7599
9494
  bigintToBytes322 as bigintToBytes32,
7600
9495
  bigintToHex,
7601
9496
  buildMerkleTree,
@@ -7609,15 +9504,30 @@ export {
7609
9504
  chainNoteFromBase64,
7610
9505
  chainNoteToBase64,
7611
9506
  classifyRelayError,
9507
+ cleanupStalePendingOperations,
9508
+ clearPendingDeposits,
9509
+ clearPendingWithdrawals,
7612
9510
  computeChainNoteHash,
9511
+ computeCommitment2 as computeCommitment,
7613
9512
  computeExtDataHash,
7614
9513
  computeMerkleRoot,
9514
+ computeNullifier2 as computeNullifier,
9515
+ computeNullifierAsync,
9516
+ computeNullifierSync,
9517
+ computeOutputsHash,
9518
+ computeOutputsHashAsync,
9519
+ computeOutputsHashSync,
7615
9520
  computeProofForLatestDeposit,
7616
9521
  computeProofFromChain,
7617
9522
  computeSignature,
9523
+ computeSwapOutputsHash,
9524
+ computeSwapOutputsHashAsync,
9525
+ computeSwapOutputsHashSync,
7618
9526
  computeCommitment as computeUtxoCommitment,
7619
9527
  computeNullifier as computeUtxoNullifier,
9528
+ copyNoteToClipboard,
7620
9529
  createCloakError,
9530
+ createDepositInstruction,
7621
9531
  createLogger,
7622
9532
  createUtxo,
7623
9533
  createZeroUtxo,
@@ -7637,52 +9547,78 @@ export {
7637
9547
  deriveViewingKeyFromUtxoPrivateKey,
7638
9548
  deserializeUtxo,
7639
9549
  detectNetworkFromRpcUrl,
9550
+ downloadNote,
9551
+ encodeNoteSimple,
7640
9552
  encryptCompactChainNote,
7641
9553
  encryptNoteForRecipient,
7642
9554
  encryptTransactionMetadata,
7643
9555
  encryptTransactionMetadataBundle,
7644
9556
  expandSpendKey,
7645
9557
  exportKeys,
9558
+ exportNote,
9559
+ exportWalletKeys,
7646
9560
  fetchCommitments,
7647
9561
  fetchRiskQuoteInstruction,
7648
9562
  fetchRiskQuoteIx,
9563
+ filterNotesByNetwork,
9564
+ filterWithdrawableNotes,
9565
+ findNoteByCommitment,
7649
9566
  formatAmount,
7650
9567
  formatComplianceCsv,
7651
9568
  formatErrorForLogging,
7652
9569
  formatSol,
7653
9570
  fullWithdraw,
7654
9571
  generateCloakKeys,
9572
+ generateCommitment,
9573
+ generateCommitmentAsync,
7655
9574
  generateMasterSeed,
9575
+ generateNote,
9576
+ generateNoteFromWallet,
7656
9577
  generateUtxoKeypair,
7657
9578
  generateViewingKeyPair,
9579
+ generateWithdrawRegularProof,
9580
+ generateWithdrawSwapProof,
7658
9581
  getAddressExplorerUrl,
7659
9582
  getCircuitsPath,
9583
+ getDefaultCircuitsPath,
7660
9584
  getDistributableAmount,
7661
9585
  getExplorerUrl,
7662
9586
  getNkFromUtxoPrivateKey,
7663
9587
  getNullifierPDA,
9588
+ getPendingOperationsSummary,
7664
9589
  getPublicKey,
9590
+ getPublicViewKey,
9591
+ getRecipientAmount,
7665
9592
  getRpcUrlForNetwork,
7666
9593
  getShieldPoolPDAs,
7667
9594
  getSwapStatePDA,
9595
+ getViewKey,
9596
+ hasPendingOperations,
7668
9597
  hexToBigint2 as hexToBigint,
7669
9598
  hexToBytes,
7670
9599
  importKeys,
9600
+ importWalletKeys,
7671
9601
  isDebugEnabled,
7672
9602
  isRootNotFoundError,
7673
9603
  isValidHex,
7674
9604
  isValidRpcUrl,
7675
9605
  isValidSolanaAddress,
7676
9606
  isWithdrawAmountSufficient,
9607
+ isWithdrawable,
7677
9608
  keypairToAdapter,
9609
+ loadPendingDeposits,
9610
+ loadPendingWithdrawals,
7678
9611
  parseAmount,
7679
9612
  parseError,
9613
+ parseNote,
7680
9614
  parseRelayErrorResponse,
7681
9615
  parseTransactionError,
7682
9616
  partialWithdraw,
7683
9617
  poseidonHash,
7684
9618
  preflightCheck,
7685
9619
  preflightNullifiers,
9620
+ prepareEncryptedOutput,
9621
+ prepareEncryptedOutputForRecipient,
7686
9622
  proofToBytes,
7687
9623
  pubkeyToFieldElement,
7688
9624
  pubkeyToLimbs,
@@ -7690,11 +9626,16 @@ export {
7690
9626
  randomFieldElement,
7691
9627
  readMerkleTreeState,
7692
9628
  registerViewingKey,
9629
+ removePendingDeposit,
9630
+ removePendingWithdrawal,
9631
+ savePendingDeposit,
9632
+ savePendingWithdrawal,
7693
9633
  scanNotesForWallet,
7694
9634
  scanTransactions,
7695
9635
  sdkLogger,
7696
9636
  selectUtxos,
7697
9637
  sendTransaction,
9638
+ serializeNote,
7698
9639
  serializeUtxo,
7699
9640
  setCircuitsPath,
7700
9641
  setDebugMode,
@@ -7708,12 +9649,21 @@ export {
7708
9649
  transfer,
7709
9650
  truncate,
7710
9651
  tryDecryptNote,
9652
+ updateNoteWithDeposit,
9653
+ updatePendingDeposit,
9654
+ updatePendingWithdrawal,
7711
9655
  bigintToBytes32 as utxoBigintToBytes32,
7712
9656
  utxoEquals,
7713
9657
  hexToBigint as utxoHexToBigint,
9658
+ validateDepositParams,
9659
+ validateNote,
7714
9660
  validateOutputsSum,
7715
9661
  validateRoot,
9662
+ validateTransfers,
7716
9663
  validateWalletConnected,
9664
+ validateWithdrawableNote,
9665
+ verifyAllCircuits,
9666
+ verifyCircuitIntegrity,
7717
9667
  verifyUtxos,
7718
9668
  waitForRoot,
7719
9669
  withTiming