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