@cloak.dev/sdk 0.1.5 → 0.1.7

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 ADDED
@@ -0,0 +1,173 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@cloak.dev/sdk` are documented here. The format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
+ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.6] — 2026-04-27
8
+
9
+ ### Removed (BREAKING)
10
+
11
+ - **`CloakSDK.deposit / privateTransfer / withdraw / send / swap`** —
12
+ these methods built a legacy `[discriminator: 1, amount: u64,
13
+ commitment: 32]` deposit instruction. The deployed shield-pool program
14
+ has repurposed discriminator `1` to `TransactSwap` (which requires
15
+ `>= 8` accounts), so every external invocation hit `0x1063
16
+ MissingAccounts` at ~112 CU. Reported by the cloak-squads hackathon
17
+ team on 2026-04-26.
18
+ - **`createDepositInstruction` / `validateDepositParams` /
19
+ `DepositInstructionParams`** — the disc-1 instruction builder and its
20
+ validators.
21
+ - **`CloakNote` model and helpers** — `generateNote`, `parseNote`,
22
+ `exportNote`, `findNoteByCommitment`, `filterNotesByNetwork`,
23
+ `filterWithdrawableNotes`, `isWithdrawable`, `updateNoteWithDeposit`,
24
+ `getRecipientAmount`, `generateNoteFromWallet`. The OLD
25
+ `withdraw_regular.circom` model used a 3-input
26
+ `Poseidon(amount, r0, r1, pk_spend)` commitment that is incompatible
27
+ with the deployed `transaction.circom` UTXO circuit (4-input
28
+ `Poseidon(amount, pubkey, blinding, mintAddress)`).
29
+ - **`CloakSDK.loadNotes / saveNote / findNote / getTransactionMetadata`**
30
+ and the `StorageAdapter.notes*` methods. The adapter is now keys-only.
31
+ - **Encrypted-note helpers** — `prepareEncryptedOutput`,
32
+ `prepareEncryptedOutputForRecipient`, `encodeNoteSimple` (legacy
33
+ `CloakNote`-based variants).
34
+ - **Pending-operations helpers** — `savePendingDeposit`/`Withdrawal`
35
+ and friends, plus `PendingDeposit` / `PendingWithdrawal` types.
36
+ - **Legacy proof generation** — `src/utils/proof-generation.ts`
37
+ (`generateWithdrawRegularProof`, `generateWithdrawSwapProof`,
38
+ `verifyCircuitIntegrity`, `verifyAllCircuits`,
39
+ `areCircuitsAvailable`, `getDefaultCircuitsPath`,
40
+ `EXPECTED_CIRCUIT_HASHES`, `DEFAULT_CIRCUITS_URL`).
41
+ - **Legacy bigint hash helpers** in `src/utils/crypto.ts` —
42
+ `computeCommitment` (3-arg), `generateCommitmentAsync`,
43
+ `computeNullifier{,Async,Sync}`, `computeOutputsHash{,Async,Sync,FromLimbs}`,
44
+ `computeSwapOutputsHash{,Async,Sync}`. The UTXO-model
45
+ `computeCommitment(utxo)` / `computeNullifier(utxo)` live in
46
+ `core/utxo` and are re-exported as `computeUtxoCommitment` /
47
+ `computeUtxoNullifier`.
48
+ - **OLD types** in `src/core/types.ts` — `CloakNote`, `Transfer`,
49
+ `MaxLengthArray`, `TransferResult`, `DepositResult`, `DepositOptions`,
50
+ `TransferOptions`, `WithdrawOptions`, `DepositStatus`, `SwapOptions`,
51
+ `SwapParams`, `SwapResult`. Plus `proofTimeout` (was unused on the
52
+ snapshot).
53
+ - **`withdraw_regular_final.zkey`** — the ~10 MB legacy circuit asset.
54
+
55
+ ### Changed (BREAKING)
56
+
57
+ - **`CloakConfig` is now a snapshot-only type.** It is the structurally
58
+ narrow value returned by `CloakSDK.getConfig()`: exactly seven
59
+ fields — `network`, `cloakKeys`, `programId`, `poolAddress`,
60
+ `merkleTreeAddress`, `treasuryAddress`, `debug`. The constructor
61
+ input is the new exported `CloakSDKOptions` interface (which still
62
+ accepts `keypairBytes`, `wallet`, and `storage` — they are consumed
63
+ into private SDK state and are *never* persisted on the snapshot).
64
+ `sdk.getConfig().keypairBytes` is now a `TS2339` compile-time error.
65
+ - `CloakConfig.cloakKeys` is now typed as `CloakKeyPair | undefined`
66
+ (was `any`).
67
+ - `LocalStorageAdapter` constructor signature is `(keysKey?: string)`;
68
+ the second `notesKey` parameter is gone.
69
+ - `CloakSDK` constructor now throws `CloakError(_, "validation", false)`
70
+ instead of plain `Error` when no signer is provided; `getPublicKey()`
71
+ throws `CloakError(_, "wallet", false)` when neither keypair nor
72
+ wallet adapter is configured. Consumers' `parseError()` /
73
+ `classifyRelayError()` flows now handle these uniformly.
74
+
75
+ ### Added
76
+
77
+ - **`LocalStorageAdapter` auto-purges legacy plaintext secrets.** On
78
+ the first construction in any browser origin, the adapter removes the
79
+ pre-0.1.6 `cloak_notes` localStorage entry (which contained `r`
80
+ randomness and `sk_spend` hex in plaintext) and writes a sentinel at
81
+ `cloak_purged_v0_1_5` so subsequent constructions skip the work.
82
+ Closes the upgrade-time security gap without requiring consumers to
83
+ call any helper. Consumers using a non-default legacy key can still
84
+ call `LocalStorageAdapter.purgeLegacyNoteStorage(customKey)`
85
+ explicitly.
86
+ - `LocalStorageAdapter.LEGACY_PURGE_SENTINEL_KEY` and
87
+ `LocalStorageAdapter.LEGACY_NOTES_KEY` static constants for callers
88
+ that want to inspect / reset the purge state.
89
+ - `LocalStorageAdapter.runLegacyPurgeOnce()` static helper for callers
90
+ that want to force the auto-purge before the first adapter
91
+ instantiation (e.g. during an upgrade boot sequence).
92
+
93
+ ### Security
94
+
95
+ - `getConfig()` no longer leaks `keypairBytes`. The constructor
96
+ consumed the secret bytes into a `Keypair` and used to spread the
97
+ whole input onto `this.config`; `getConfig()`'s shallow-copy return
98
+ exposed the original `Uint8Array` by reference. `this.config` is now
99
+ built explicitly without `keypairBytes`/`wallet`/`storage`, and the
100
+ type system enforces the asymmetry (see "Changed" above).
101
+ - `LocalStorageAdapter` upgrade-time auto-purge (see "Added") removes
102
+ the now-unredeemable plaintext secrets that pre-0.1.6 versions left
103
+ in browser localStorage.
104
+
105
+ ### Migration guide
106
+
107
+ The supported flow for transaction emission is the functional UTXO API.
108
+
109
+ ```ts
110
+ import {
111
+ transact,
112
+ fullWithdraw,
113
+ partialWithdraw,
114
+ transfer,
115
+ swapUtxo,
116
+ createUtxo,
117
+ createZeroUtxo,
118
+ generateUtxoKeypair,
119
+ CLOAK_PROGRAM_ID,
120
+ NATIVE_SOL_MINT,
121
+ } from "@cloak.dev/sdk";
122
+ import { Connection, Keypair } from "@solana/web3.js";
123
+
124
+ const connection = new Connection("https://api.mainnet-beta.solana.com");
125
+ const signer = Keypair.fromSecretKey(/* ... */);
126
+
127
+ // Deposit (positive externalAmount).
128
+ const owner = await generateUtxoKeypair();
129
+ const output = await createUtxo(10_000_000n, owner, NATIVE_SOL_MINT);
130
+ const deposited = await transact(
131
+ {
132
+ inputUtxos: [
133
+ await createZeroUtxo(NATIVE_SOL_MINT),
134
+ await createZeroUtxo(NATIVE_SOL_MINT),
135
+ ],
136
+ outputUtxos: [output, await createZeroUtxo(NATIVE_SOL_MINT)],
137
+ externalAmount: 10_000_000n,
138
+ depositor: signer.publicKey,
139
+ },
140
+ {
141
+ connection,
142
+ programId: CLOAK_PROGRAM_ID,
143
+ relayUrl: "https://api.cloak.ag",
144
+ depositorKeypair: signer,
145
+ },
146
+ );
147
+
148
+ // Reclaim to a public address.
149
+ await fullWithdraw(deposited.outputUtxos, signer.publicKey, {
150
+ connection,
151
+ programId: CLOAK_PROGRAM_ID,
152
+ relayUrl: "https://api.cloak.ag",
153
+ depositorKeypair: signer,
154
+ cachedMerkleTree: deposited.merkleTree,
155
+ });
156
+ ```
157
+
158
+ The `CloakSDK` class is retained as a thin config + read-only helper
159
+ (`getPublicKey`, `getCurrentRoot`, `getMerkleProof`,
160
+ `getTransactionStatus`, `importWalletKeys`, `exportWalletKeys`,
161
+ `getConfig`). Construction is unchanged for new fields.
162
+
163
+ ### Verification
164
+
165
+ - Mainnet end-to-end smoke (deposit + withdraw cycle) on the patched
166
+ SDK: deposit signature
167
+ `5iGmrMYoSucYJ5PzEo5BCWioqTof4KNfjowq14ZYrbQACicaP2tJdJfRUy2CctsQLyNu6xpYXn8gYBg3EefsmXrv`
168
+ (slot 416038846, leaf 318); withdraw signature
169
+ `37A4T1KNiZLkmRvLAtui5ZN2VCmzjsZMzzGNwMosFA2UM1NGn7kP28p5APTvKRb51rNaLmWF42TJqK9GSZM5xCxn`
170
+ (slot 416055377). Net delta on the test wallet matches the expected
171
+ fee schedule (0.005 fixed + 0.3% var).
172
+
173
+ [0.1.6]: https://github.com/cloak-ag/sdk/pull/11
package/README.md CHANGED
@@ -4,12 +4,12 @@ TypeScript SDK for the Cloak Protocol - Private transactions on Solana using zer
4
4
 
5
5
  ## Features
6
6
 
7
- - 🔒 **Private Transfers**: Send SOL privately using zero-knowledge proofs
8
- - 👥 **Multi-Recipient**: Support for 1-5 recipients in a single transaction
9
- - 💱 **Token Swaps**: Swap SOL for SPL tokens privately (SOL → USDC, etc.)
10
- - 🔐 **Type-Safe**: Full TypeScript support with comprehensive types
11
- - 🌐 **Cross-Platform**: Works in browser (React, Next.js) and Node.js
12
- - ⚡ **Simple API**: Easy-to-use high-level client with wallet adapter support
7
+ - 🔒 **Private Transfers**: Shield, send, and reclaim SOL or SPL tokens via the UTXO model and zero-knowledge proofs
8
+ - 🔁 **Shield-to-shield**: Move funds privately between UTXOs (`transfer`) without ever de-shielding
9
+ - 💱 **Token Swaps**: Swap shielded SOL for SPL tokens via Jupiter (`swapUtxo`) — SOL → USDC, BRZ, etc.
10
+ - 🔐 **Type-Safe**: Full TypeScript types; the `getConfig()` snapshot is structurally narrower than the constructor input so secrets cannot be re-exposed by accident
11
+ - 🌐 **Cross-Platform**: Browser (wallet adapter) and Node.js (`Keypair`) modes
12
+ - ⚡ **Functional API**: `transact` / `transfer` / `partialWithdraw` / `fullWithdraw` / `swapUtxo` — pass connection + program + relay + signer per call
13
13
 
14
14
  ## Installation
15
15
 
@@ -155,146 +155,215 @@ npm run test:examples
155
155
  ### Node.js (with Keypair)
156
156
 
157
157
  ```typescript
158
- import { CloakSDK } from "@cloak.dev/sdk";
158
+ import {
159
+ CLOAK_PROGRAM_ID,
160
+ NATIVE_SOL_MINT,
161
+ createUtxo,
162
+ createZeroUtxo,
163
+ fullWithdraw,
164
+ generateUtxoKeypair,
165
+ transact,
166
+ } from "@cloak.dev/sdk";
159
167
  import { Connection, Keypair, PublicKey } from "@solana/web3.js";
160
168
 
161
- // Initialize connection and keypair
162
- const connection = new Connection("https://api.devnet.solana.com");
163
- const keypair = Keypair.fromSecretKey(/* your secret key */);
169
+ const connection = new Connection("https://api.mainnet-beta.solana.com");
170
+ const signer = Keypair.fromSecretKey(/* your secret key */);
164
171
 
165
- // Initialize SDK
166
- const sdk = new CloakSDK({
167
- keypairBytes: keypair.secretKey,
168
- network: "devnet",
169
- });
172
+ // Generate a UTXO keypair (owner of the shielded balance)
173
+ const owner = await generateUtxoKeypair();
170
174
 
171
- // Deposit SOL into the privacy pool
172
- const depositResult = await sdk.deposit(connection, 100_000_000); // 0.1 SOL
173
- console.log("Deposited! Leaf index:", depositResult.leafIndex);
175
+ // Build a 0.01 SOL output UTXO and shield it
176
+ const amountLamports = 10_000_000n;
177
+ const output = await createUtxo(amountLamports, owner, NATIVE_SOL_MINT);
174
178
 
175
- // Withdraw to a recipient
176
- const withdrawResult = await sdk.withdraw(
177
- connection,
178
- depositResult.note,
179
- new PublicKey("RECIPIENT_ADDRESS"),
180
- { withdrawAll: true }
179
+ const deposited = await transact(
180
+ {
181
+ inputUtxos: [await createZeroUtxo(NATIVE_SOL_MINT), await createZeroUtxo(NATIVE_SOL_MINT)],
182
+ outputUtxos: [output, await createZeroUtxo(NATIVE_SOL_MINT)],
183
+ externalAmount: amountLamports, // positive = deposit
184
+ depositor: signer.publicKey,
185
+ },
186
+ {
187
+ connection,
188
+ programId: CLOAK_PROGRAM_ID,
189
+ relayUrl: "https://api.cloak.ag",
190
+ depositorKeypair: signer,
191
+ },
181
192
  );
182
- console.log("Withdrawn! TX:", withdrawResult.signature);
193
+ console.log("Deposit signature:", deposited.signature);
194
+
195
+ // Reclaim the full balance to a public address
196
+ const recipient = new PublicKey("RECIPIENT_ADDRESS");
197
+ const withdrawn = await fullWithdraw(deposited.outputUtxos, recipient, {
198
+ connection,
199
+ programId: CLOAK_PROGRAM_ID,
200
+ relayUrl: "https://api.cloak.ag",
201
+ depositorKeypair: signer,
202
+ cachedMerkleTree: deposited.merkleTree,
203
+ });
204
+ console.log("Withdraw signature:", withdrawn.signature);
183
205
  ```
184
206
 
185
207
  ### React/Next.js (with Wallet Adapter)
186
208
 
187
209
  ```typescript
188
- import { CloakSDK } from "@cloak.dev/sdk";
189
- import { useWallet, useConnection } from "@solana/wallet-adapter-react";
190
- import { PublicKey } from "@solana/web3.js";
210
+ import {
211
+ CLOAK_PROGRAM_ID,
212
+ NATIVE_SOL_MINT,
213
+ createUtxo,
214
+ createZeroUtxo,
215
+ generateUtxoKeypair,
216
+ transact,
217
+ } from "@cloak.dev/sdk";
218
+ import { useConnection, useWallet } from "@solana/wallet-adapter-react";
219
+ import { useCallback } from "react";
191
220
 
192
- function PrivateTransfer() {
193
- const { publicKey, signTransaction, sendTransaction } = useWallet();
221
+ export function DepositButton() {
194
222
  const { connection } = useConnection();
223
+ const wallet = useWallet();
195
224
 
196
- // Initialize SDK with wallet adapter
197
- const sdk = useMemo(() => {
198
- if (!publicKey) return null;
199
- return new CloakSDK({
200
- network: "devnet",
201
- wallet: {
202
- publicKey,
203
- signTransaction: (tx) => signTransaction!(tx),
204
- sendTransaction: (tx, conn, opts) => sendTransaction(tx, conn, opts),
205
- },
206
- });
207
- }, [publicKey, signTransaction, sendTransaction]);
225
+ const onClick = useCallback(async () => {
226
+ if (!wallet.publicKey || !wallet.signTransaction) return;
208
227
 
209
- const handleDeposit = async () => {
210
- const result = await sdk.deposit(connection, 100_000_000);
211
- console.log("Deposited!", result.signature);
212
- };
228
+ const owner = await generateUtxoKeypair();
229
+ const amount = 10_000_000n; // 0.01 SOL — SDK-enforced minimum
230
+ const output = await createUtxo(amount, owner, NATIVE_SOL_MINT);
213
231
 
214
- return <button onClick={handleDeposit}>Deposit 0.1 SOL</button>;
232
+ const result = await transact(
233
+ {
234
+ inputUtxos: [await createZeroUtxo(NATIVE_SOL_MINT), await createZeroUtxo(NATIVE_SOL_MINT)],
235
+ outputUtxos: [output, await createZeroUtxo(NATIVE_SOL_MINT)],
236
+ externalAmount: amount,
237
+ depositor: wallet.publicKey,
238
+ },
239
+ {
240
+ connection,
241
+ programId: CLOAK_PROGRAM_ID,
242
+ relayUrl: "https://api.cloak.ag",
243
+ wallet: {
244
+ publicKey: wallet.publicKey,
245
+ signTransaction: wallet.signTransaction,
246
+ },
247
+ },
248
+ );
249
+ console.log("Deposited:", result.signature);
250
+ }, [connection, wallet]);
251
+
252
+ return <button onClick={onClick}>Deposit 0.01 SOL</button>;
215
253
  }
216
254
  ```
217
255
 
218
- ## Core Methods
256
+ The `CloakSDK` class is kept as a thin config + read-only chain helper
257
+ (`getPublicKey`, `getCurrentRoot`, `getMerkleProof`, `getTransactionStatus`,
258
+ `importWalletKeys`, `exportWalletKeys`, `getConfig`). All transaction-emitting
259
+ calls use the standalone helpers above.
219
260
 
220
- ### Deposit
261
+ ## Core Functions
221
262
 
222
- Deposit SOL into the privacy pool:
263
+ ### Deposit (`transact` with positive `externalAmount`)
223
264
 
224
265
  ```typescript
225
- const result = await sdk.deposit(connection, 100_000_000); // 0.1 SOL
226
- // Save the note securely - you need it to withdraw!
227
- console.log(result.note);
266
+ const result = await transact(
267
+ {
268
+ inputUtxos: [await createZeroUtxo(), await createZeroUtxo()],
269
+ outputUtxos: [outputUtxo, await createZeroUtxo()],
270
+ externalAmount: 10_000_000n, // positive = deposit
271
+ depositor: signer.publicKey,
272
+ },
273
+ { connection, programId: CLOAK_PROGRAM_ID, relayUrl: "https://api.cloak.ag", depositorKeypair: signer },
274
+ );
275
+ console.log("Leaf index:", result.commitmentIndices[0]);
276
+ console.log("Output UTXO:", result.outputUtxos[0]);
228
277
  ```
229
278
 
230
279
  ### Withdraw
231
280
 
232
- Withdraw to a single recipient:
233
-
234
281
  ```typescript
235
- const result = await sdk.withdraw(
282
+ import { fullWithdraw, partialWithdraw } from "@cloak.dev/sdk";
283
+
284
+ // Reclaim everything in one or more input UTXOs to a public address.
285
+ const result = await fullWithdraw(myUtxos, recipientPublicKey, {
236
286
  connection,
237
- note,
238
- recipientPublicKey,
239
- { withdrawAll: true }
240
- );
241
- ```
287
+ programId: CLOAK_PROGRAM_ID,
288
+ relayUrl: "https://api.cloak.ag",
289
+ depositorKeypair: signer,
290
+ });
242
291
 
243
- ### Send to Multiple Recipients
292
+ // Or withdraw a portion and keep the change shielded.
293
+ const partial = await partialWithdraw(myUtxos, recipientPublicKey, 5_000_000n, {
294
+ connection,
295
+ programId: CLOAK_PROGRAM_ID,
296
+ relayUrl: "https://api.cloak.ag",
297
+ depositorKeypair: signer,
298
+ });
299
+ ```
244
300
 
245
- Send to up to 5 recipients:
301
+ ### Shield-to-shield Transfer
246
302
 
247
303
  ```typescript
248
- const result = await sdk.send(connection, note, [
249
- { recipient: addr1, amount: 50_000_000 },
250
- { recipient: addr2, amount: 47_000_000 },
251
- ]);
252
- ```
304
+ import { transfer } from "@cloak.dev/sdk";
253
305
 
254
- ### Swap
306
+ const result = await transfer(myUtxos, recipientUtxoPublicKey, amountLamports, {
307
+ connection,
308
+ programId: CLOAK_PROGRAM_ID,
309
+ relayUrl: "https://api.cloak.ag",
310
+ depositorKeypair: signer,
311
+ });
312
+ ```
255
313
 
256
- Swap SOL for SPL tokens:
314
+ ### Swap (SOL SPL token)
257
315
 
258
316
  ```typescript
259
- const result = await sdk.swap(connection, note, recipientPublicKey, {
260
- outputMint: "TOKEN_MINT_ADDRESS",
261
- minOutputAmount: 1000000,
262
- });
317
+ import { swapUtxo } from "@cloak.dev/sdk";
318
+
319
+ const result = await swapUtxo(
320
+ {
321
+ inputUtxos: myUtxos,
322
+ swapAmount: 10_000_000n,
323
+ outputMint: new PublicKey("TOKEN_MINT_ADDRESS"),
324
+ recipientAta: recipientTokenAccount,
325
+ minOutputAmount: 1_000_000n,
326
+ },
327
+ {
328
+ connection,
329
+ programId: CLOAK_PROGRAM_ID,
330
+ relayUrl: "https://api.cloak.ag",
331
+ depositorKeypair: signer,
332
+ },
333
+ );
263
334
  ```
264
335
 
265
336
  ## Fee Structure
266
337
 
267
- - **Fixed Fee**: 0.005 SOL (5,000,000 lamports)
268
- - **Variable Fee**: 0.3% of deposit amount
338
+ - **Fixed Fee**: 0.005 SOL (5,000,000 lamports) on withdraw
339
+ - **Variable Fee**: 0.3% of withdrawn amount
269
340
 
270
- Use `getDistributableAmount()` to calculate the amount after fees:
341
+ Use `getDistributableAmount()` to calculate the amount a recipient receives after fees:
271
342
 
272
343
  ```typescript
273
344
  import { getDistributableAmount } from "@cloak.dev/sdk";
274
345
 
275
- const deposited = 100_000_000; // 0.1 SOL
276
- const afterFees = getDistributableAmount(deposited); // ~94,700,000 lamports
346
+ const withdrawing = 100_000_000; // 0.1 SOL
347
+ const recipientReceives = getDistributableAmount(withdrawing); // ~94,700,000 lamports
277
348
  ```
278
349
 
279
- ## Notes
350
+ ## UTXOs
280
351
 
281
- A **Cloak Note** is a cryptographic commitment representing a private amount of SOL:
352
+ A **UTXO** is a cryptographic commitment over `(amount, ownerPubkey, blinding, mintAddress)`. Each `transact` call consumes up to 2 input UTXOs and produces up to 2 outputs:
282
353
 
283
354
  ```typescript
284
- interface CloakNote {
285
- version: string;
286
- amount: number; // Amount in lamports
287
- commitment: string; // Commitment hash
288
- sk_spend: string; // Spending key (keep secret!)
289
- r: string; // Randomness
290
- timestamp: number;
291
- network: Network;
292
- leafIndex?: number; // Set after deposit
293
- depositSignature?: string;
355
+ interface Utxo {
356
+ amount: bigint; // lamports for SOL, token units for SPL
357
+ keypair: UtxoKeypair; // { privateKey, publicKey } — bigints
358
+ blinding: bigint; // randomness
359
+ mintAddress: PublicKey; // NATIVE_SOL_MINT for SOL
360
+ index?: number; // leaf index in the Merkle tree (set after the UTXO is in-tree)
361
+ commitment?: bigint;
362
+ nullifier?: bigint;
294
363
  }
295
364
  ```
296
365
 
297
- **⚠️ Important**: Save your notes securely! Without the note, you cannot withdraw your funds.
366
+ **⚠️ Important**: persist the spend `privateKey`, `blinding`, `amount`, `index`, and `mintAddress` for every output UTXO — those are the only inputs you need to spend it later. The SDK ships `LocalStorageAdapter` for browser wallet keys; UTXO persistence is the consumer's responsibility (see `lib/utxo-manager.ts` patterns in cloak-ag/web for a reference implementation).
298
367
 
299
368
  ## Compliance Chain Scanning
300
369
 
@@ -334,13 +403,31 @@ sequenceDiagram
334
403
  ## Error Handling
335
404
 
336
405
  ```typescript
337
- import { CloakError } from "@cloak.dev/sdk";
406
+ import {
407
+ CloakError,
408
+ UtxoAlreadySpentError,
409
+ RootNotFoundError,
410
+ classifyRelayError,
411
+ fullWithdraw,
412
+ CLOAK_PROGRAM_ID,
413
+ } from "@cloak.dev/sdk";
338
414
 
339
415
  try {
340
- await sdk.withdraw(connection, note, recipient);
416
+ await fullWithdraw(myUtxos, recipient, {
417
+ connection,
418
+ programId: CLOAK_PROGRAM_ID,
419
+ relayUrl: "https://api.cloak.ag",
420
+ depositorKeypair: signer,
421
+ });
341
422
  } catch (error) {
342
- if (error instanceof CloakError) {
343
- console.log("Category:", error.category); // 'wallet', 'network', 'prover', etc.
423
+ if (error instanceof UtxoAlreadySpentError) {
424
+ // Drop the spent UTXO from local storage; the structured error
425
+ // carries which nullifiers collided.
426
+ console.log("Spent nullifiers:", error.nullifiers);
427
+ } else if (error instanceof RootNotFoundError) {
428
+ // Relay/chain root mismatch; retry against a fresh tree.
429
+ } else if (error instanceof CloakError) {
430
+ console.log("Category:", error.category); // 'wallet' | 'network' | 'prover' | 'relay' | 'validation' | 'environment' | 'service' | 'indexer'
344
431
  console.log("Retryable:", error.retryable);
345
432
  }
346
433
  }