@veilo/sdk-core 0.1.17 → 0.3.3

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.
Files changed (52) hide show
  1. package/README.md +1257 -272
  2. package/dist/cjs/client.d.ts +407 -0
  3. package/dist/cjs/client.js +914 -0
  4. package/dist/cjs/config.d.ts +82 -0
  5. package/dist/cjs/config.js +57 -0
  6. package/dist/cjs/events.d.ts +77 -0
  7. package/dist/cjs/events.js +167 -0
  8. package/dist/cjs/idl/privacy_pool.json +10313 -0
  9. package/dist/cjs/index.d.ts +13 -0
  10. package/dist/cjs/index.js +57 -0
  11. package/dist/cjs/merkle.d.ts +64 -0
  12. package/dist/cjs/merkle.js +133 -0
  13. package/dist/cjs/poseidon.d.ts +29 -0
  14. package/dist/cjs/poseidon.js +100 -0
  15. package/dist/cjs/program.d.ts +26 -0
  16. package/dist/cjs/program.js +38 -0
  17. package/dist/cjs/proof.d.ts +183 -0
  18. package/dist/cjs/proof.js +292 -0
  19. package/dist/cjs/prover.d.ts +54 -0
  20. package/dist/cjs/prover.js +112 -0
  21. package/dist/cjs/relayer.d.ts +295 -0
  22. package/dist/cjs/relayer.js +246 -0
  23. package/dist/cjs/retry.d.ts +32 -0
  24. package/dist/cjs/retry.js +75 -0
  25. package/dist/cjs/utxo.d.ts +215 -0
  26. package/dist/cjs/utxo.js +394 -0
  27. package/dist/esm/client.js +887 -0
  28. package/dist/esm/config.js +51 -0
  29. package/dist/esm/events.js +129 -0
  30. package/dist/esm/idl/privacy_pool.json +10313 -0
  31. package/dist/esm/index.js +22 -0
  32. package/dist/esm/merkle.js +129 -0
  33. package/dist/esm/poseidon.js +87 -0
  34. package/dist/esm/program.js +31 -0
  35. package/dist/esm/proof.js +281 -0
  36. package/dist/esm/prover.js +75 -0
  37. package/dist/esm/relayer.js +238 -0
  38. package/dist/esm/retry.js +71 -0
  39. package/dist/esm/utxo.js +372 -0
  40. package/package.json +47 -11
  41. package/src/client.ts +0 -352
  42. package/src/config.ts +0 -13
  43. package/src/index.ts +0 -6
  44. package/src/merkle.ts +0 -178
  45. package/src/note.ts +0 -193
  46. package/src/poseidon.ts +0 -62
  47. package/src/proof.ts +0 -170
  48. package/test/script.js +0 -0
  49. package/test-tsconfig.json +0 -19
  50. package/tests/note.test.ts +0 -50
  51. package/tests/sdk.integration.test.ts +0 -210
  52. package/tsconfig.json +0 -18
package/README.md CHANGED
@@ -1,35 +1,797 @@
1
- # @zkprivacysol/sdk-core
1
+ # @veilo/sdk-core
2
2
 
3
- Tiny TypeScript SDK for the `privacy-pool` Anchor program.
3
+ TypeScript SDK for the Veilo Privacy Pool Anchor program.
4
4
 
5
- This package wraps the on-chain program with a small set of ergonomic helpers for:
5
+ This package provides a complete UTXO-based privacy protocol implementation on Solana with:
6
6
 
7
- - Deriving PDAs (`config`, `vault`, `note_tree`, `nullifiers`)
8
- - Initializing the pool with fixed SOL denominations
9
- - Depositing using a note commitment + **off-chain Merkle root**
10
- - Withdrawing via an authorized relayer (with fee + TVL accounting)
11
- - Building simple Merkle roots off-chain (for demo / testing)
12
- - Handling note commitments (`createRandomNote`, `commitNote`, etc.)
7
+ - **UTXO Model**: Full support for unspent transaction outputs with Poseidon commitments
8
+ - **Multi-Tree Support**: Multiple concurrent Merkle trees for improved scalability
9
+ - **ZK Proofs**: Integration with Circom circuits for private transactions
10
+ - **Private Swaps**: Atomic cross-pool swaps via Jupiter (native SOL + SPL tokens)
11
+ - **Note Encryption**: NaCl-based encrypted UTXO notes and blind mailbox delivery
12
+ - **Event Scanning**: Reconstruct Merkle trees from on-chain events
13
+ - **Poseidon Hashing**: BN254-curve compatible hashing using circomlibjs
14
+ - **Relayer Support**: Built-in relayer infrastructure for private withdrawals and swaps
13
15
 
14
- > **Status:** internal/dev SDK. No production guarantees.
15
- > ZK verification is expected to happen **off-chain in a relayer service**.
16
- > On-chain, `proof: Vec<u8>` is treated as opaque bytes (hook for a future verifier).
16
+ > **Status:** Active development. The SDK supports full transaction privacy with ZK-SNARK proofs.
17
+ > Proofs are generated off-chain and verified on-chain using Groth16.
17
18
 
18
19
  ---
19
20
 
20
- ## 1. Install
21
+ ## 1. Installation
21
22
 
22
- From the monorepo root (or inside the package folder):
23
+ ```bash
24
+ npm install @veilo/sdk-core
25
+ ```
26
+
27
+ ---
28
+
29
+ ## 2. Prerequisites
30
+
31
+ - A running Solana validator (localnet/devnet/mainnet)
32
+ - The `privacy-pool` program deployed to the network
33
+ - A funded keypair
23
34
 
24
35
  ```bash
25
- cd packages/sdk-core
26
- npm install
36
+ export ANCHOR_PROVIDER_URL=https://api.devnet.solana.com
37
+ export ANCHOR_WALLET=$HOME/.config/solana/id.json
27
38
  ```
28
39
 
29
- If you publish it somewhere later:
40
+ ---
41
+
42
+ ## 3. Build
30
43
 
31
44
  ```bash
32
- npm install @zkprivacysol/sdk-core
45
+ npm run build
46
+ ```
47
+
48
+ ---
49
+
50
+ ## 4. SDK API
51
+
52
+ ### 4.1 PDA Helpers
53
+
54
+ ```ts
55
+ import {
56
+ getPoolPdas,
57
+ getNoteTreePda,
58
+ getGlobalConfigPda,
59
+ getNullifierMarkerPda,
60
+ getSwapExecutorPda,
61
+ } from "@veilo/sdk-core";
62
+ import { PublicKey } from "@solana/web3.js";
63
+
64
+ const programId = new PublicKey(
65
+ "YourProgram1111111111111111111111111111111111",
66
+ );
67
+ const mintAddress = new PublicKey(
68
+ "So11111111111111111111111111111111111111112",
69
+ );
70
+
71
+ // Pool PDAs (config, vault, nullifiers)
72
+ const { config, vault, nullifiers } = getPoolPdas(programId, mintAddress);
73
+
74
+ // Note tree PDA for tree ID 0
75
+ const noteTree = getNoteTreePda(programId, mintAddress, 0);
76
+
77
+ // Global config PDA (one per program)
78
+ const globalConfig = getGlobalConfigPda(programId);
79
+
80
+ // Nullifier marker PDA (prevents double-spend)
81
+ const nullifier = new Uint8Array(32);
82
+ const marker = getNullifierMarkerPda(programId, mintAddress, nullifier);
83
+
84
+ // Swap executor PDA (for cross-pool swaps)
85
+ const relayerPubkey = new PublicKey("...");
86
+ const executor = getSwapExecutorPda(
87
+ programId,
88
+ sourceMint,
89
+ destMint,
90
+ inputNullifier0, // Uint8Array[32]
91
+ relayerPubkey,
92
+ );
93
+ ```
94
+
95
+ **PDA seeds (v3):**
96
+
97
+ | Account | Seeds |
98
+ | ---------------- | ------------------------------------------------------------------- |
99
+ | Config | `["privacy_config_v3", mint]` |
100
+ | Vault | `["privacy_vault_v3", mint]` |
101
+ | Note Tree | `["privacy_note_tree_v3", mint, tree_id]` |
102
+ | Nullifiers | `["privacy_nullifiers_v3", mint]` |
103
+ | Nullifier Marker | `["nullifier_v3", mint, nullifier]` |
104
+ | Global Config | `["global_config_v1"]` |
105
+ | Swap Executor | `["swap_executor_v1", source_mint, dest_mint, nullifier0, relayer]` |
106
+
107
+ ---
108
+
109
+ ### 4.2 Pool Initialization
110
+
111
+ ```ts
112
+ import {
113
+ initializeGlobalConfig,
114
+ initializePool,
115
+ updatePoolConfig,
116
+ addMerkleTree,
117
+ getPoolConfig,
118
+ updateGlobalConfig,
119
+ } from "@veilo/sdk-core";
120
+ import { NATIVE_SOL_MINT, sol } from "@veilo/sdk-core/config";
121
+
122
+ // Initialize global config (once per program)
123
+ await initializeGlobalConfig({ program, admin: adminKeypair });
124
+
125
+ // Update global config
126
+ await updateGlobalConfig({
127
+ program,
128
+ admin: adminKeypair,
129
+ newAdmin: newAdminPubkey, // optional
130
+ paused: false, // optional
131
+ });
132
+
133
+ // Initialize a pool for native SOL
134
+ await initializePool({
135
+ program,
136
+ payer: adminKeypair,
137
+ admin: adminKeypair,
138
+ mintAddress: NATIVE_SOL_MINT,
139
+ minDepositAmount: sol(0.1),
140
+ maxDepositAmount: sol(100),
141
+ minWithdrawAmount: sol(0.1),
142
+ maxWithdrawAmount: sol(100),
143
+ feeBps: 50, // 0.5%
144
+ feeErrorMarginBps: 10, // 0.1% margin
145
+ minWithdrawalFee: 1_000_000n,
146
+ });
147
+
148
+ // Add Merkle tree (tree ID 0)
149
+ await addMerkleTree({
150
+ program,
151
+ payer: adminKeypair,
152
+ mintAddress: NATIVE_SOL_MINT,
153
+ treeId: 0,
154
+ });
155
+
156
+ // Update pool fee
157
+ await updatePoolConfig({
158
+ program,
159
+ admin: adminKeypair,
160
+ mintAddress: NATIVE_SOL_MINT,
161
+ feeBps: 30,
162
+ });
163
+
164
+ // Read pool configuration
165
+ const poolConfig = await getPoolConfig(program, NATIVE_SOL_MINT);
166
+ console.log("TVL:", poolConfig.totalTvl, " Fee:", poolConfig.feeBps, "bps");
167
+ ```
168
+
169
+ ---
170
+
171
+ ### 4.3 On-Chain Account Queries
172
+
173
+ ```ts
174
+ import {
175
+ fetchPoolConfig,
176
+ checkNullifierSpent,
177
+ getTreeInfo,
178
+ getAllTreeInfo,
179
+ getBestTreeForDeposit,
180
+ } from "@veilo/sdk-core";
181
+
182
+ // Fetch decoded PrivacyConfig account
183
+ const config = await fetchPoolConfig(program, NATIVE_SOL_MINT);
184
+
185
+ // Check if a nullifier has been spent
186
+ const spent = await checkNullifierSpent(program, NATIVE_SOL_MINT, nullifier);
187
+
188
+ // Info for a single tree
189
+ const info = await getTreeInfo(program, NATIVE_SOL_MINT, 0);
190
+ // info = { treeId, leafCount, root, isFull }
191
+
192
+ // Info for all trees in a pool
193
+ const allInfo = await getAllTreeInfo(program, NATIVE_SOL_MINT);
194
+
195
+ // Pick the best tree for depositing (least full with capacity)
196
+ const best = await getBestTreeForDeposit(program, NATIVE_SOL_MINT);
197
+ // best = { treeId, leafCount, root }
198
+ ```
199
+
200
+ ---
201
+
202
+ ### 4.4 UTXO Management
203
+
204
+ ```ts
205
+ import {
206
+ generateKeypair,
207
+ keypairFromPrivateKey,
208
+ createUTXO,
209
+ createOwnedUTXO,
210
+ createOwnedZeroUTXO,
211
+ deriveNullifier,
212
+ type Keypair,
213
+ type UTXO,
214
+ type SerializedUTXO,
215
+ type InputUTXO,
216
+ } from "@veilo/sdk-core";
217
+ import { NATIVE_SOL_MINT } from "@veilo/sdk-core/config";
218
+ import { pubkeyToField } from "@veilo/sdk-core";
219
+
220
+ // Generate a random UTXO keypair
221
+ const keypair: Keypair = generateKeypair();
222
+ // keypair = { privateKey: bigint, publicKey: bigint }
223
+
224
+ // Restore from private key
225
+ const restored = keypairFromPrivateKey(privateKeyBigInt);
226
+
227
+ // Create an owned UTXO (includes private key)
228
+ const ownedUtxo: SerializedUTXO = createOwnedUTXO({
229
+ amount: 1_000_000_000n,
230
+ privateKey: keypair.privateKey,
231
+ mintAddress: NATIVE_SOL_MINT,
232
+ });
233
+
234
+ // Create a zero-value UTXO (for unused inputs/outputs)
235
+ const zeroUtxo = createOwnedZeroUTXO(pubkeyToField(NATIVE_SOL_MINT), keypair);
236
+
237
+ // Derive nullifier for spending
238
+ const pathIndex = 0; // leaf index in Merkle tree
239
+ const treeId = 0;
240
+ const nullifier = deriveNullifier(
241
+ ownedUtxo.privateKey,
242
+ ownedUtxo.commitment,
243
+ pathIndex,
244
+ treeId,
245
+ );
246
+ ```
247
+
248
+ **UTXO commitment:** `Poseidon(amount, pubkey, blinding, mintAddress)`
249
+
250
+ **Nullifier:** `Poseidon(privateKey, commitment, pathIndex, treeId)`
251
+
252
+ ---
253
+
254
+ ### 4.5 UTXO Encryption
255
+
256
+ The SDK provides NaCl-based encryption for UTXO notes (relayer storage) and blind mailbox delivery (wallet-to-wallet).
257
+
258
+ ```ts
259
+ import {
260
+ deriveEncryptionKeypair,
261
+ encryptUTXONote,
262
+ decryptUTXONote,
263
+ encryptBlindMailboxNote,
264
+ decryptBlindMailboxNote,
265
+ fetchAndDecryptNotes,
266
+ computeSignature,
267
+ inputUTXOToCircuitFormat,
268
+ type EncryptedNote,
269
+ type BlindMailboxNote,
270
+ type BlindMailboxNoteData,
271
+ type DecryptedNote,
272
+ } from "@veilo/sdk-core";
273
+ import { Keypair as SolanaKeypair } from "@solana/web3.js";
274
+
275
+ // Derive a NaCl encryption keypair from a UTXO private key
276
+ const encKeypair = deriveEncryptionKeypair(utxoKeypair.privateKey);
277
+ // encKeypair = { publicKey: Uint8Array[32], secretKey: Uint8Array[32] }
278
+
279
+ // Encrypt a UTXO note for relay storage
280
+ const encryptedNote: EncryptedNote = encryptUTXONote(
281
+ serializedUtxo, // SerializedUTXO
282
+ encKeypair.publicKey, // NaCl public key (Uint8Array[32])
283
+ );
284
+
285
+ // Decrypt a UTXO note
286
+ const decrypted: SerializedUTXO | null = decryptUTXONote(
287
+ encryptedNote,
288
+ encKeypair.secretKey, // NaCl secret key (Uint8Array[32])
289
+ );
290
+
291
+ // Blind mailbox: encrypt for a Solana wallet keypair
292
+ const recipientKeypair = SolanaKeypair.generate();
293
+
294
+ const mailboxNote: BlindMailboxNote = encryptBlindMailboxNote(
295
+ serializedUtxo,
296
+ recipientKeypair.publicKey.toBytes(), // ed25519 public key → X25519 DH
297
+ );
298
+
299
+ // Recipient decrypts using their Solana secret key (64-byte)
300
+ const mailboxDecrypted: DecryptedNote | null = decryptBlindMailboxNote(
301
+ mailboxNote,
302
+ recipientKeypair.secretKey, // 64-byte Solana secret key
303
+ );
304
+
305
+ // Compute UTXO signature (used in circuit)
306
+ const sig = computeSignature(utxoKeypair.privateKey, utxo.commitment);
307
+
308
+ // Format an InputUTXO for snarkjs circuit
309
+ const circuitFormat = inputUTXOToCircuitFormat(inputUtxo);
310
+ ```
311
+
312
+ ---
313
+
314
+ ### 4.6 Merkle Tree Operations
315
+
316
+ ```ts
317
+ import { MerkleTree } from "@veilo/sdk-core";
318
+
319
+ // Create a new Merkle tree (default depth: 22)
320
+ const tree = new MerkleTree();
321
+
322
+ // Insert commitments
323
+ const index = tree.insert(ownedUtxo.commitment);
324
+
325
+ // Current root
326
+ const root = tree.root();
327
+
328
+ // Merkle path for proof generation
329
+ const path = tree.path(index);
330
+ // path = { pathElements: Uint8Array[], pathIndices: number[] }
331
+
332
+ // Total leaves inserted
333
+ const count = tree.totalLeaves;
334
+ ```
335
+
336
+ ---
337
+
338
+ ### 4.7 Transaction Operations
339
+
340
+ All transaction functions return `Promise<string>` (the transaction signature).
341
+
342
+ #### Deposits
343
+
344
+ ```ts
345
+ import { deposit, type DepositResult } from "@veilo/sdk-core";
346
+
347
+ const result: DepositResult = await deposit({
348
+ program,
349
+ depositor: depositorKeypair,
350
+ mintAddress: NATIVE_SOL_MINT,
351
+ amount: 1_000_000_000n, // 1 SOL
352
+ recipientPubkey: pubkeyToField(depositorKeypair.publicKey), // bigint
353
+ tree,
354
+ proofBuilder,
355
+ treeId: 0,
356
+ });
357
+ // result = { outputUTXOs, leafIndices, root }
358
+ ```
359
+
360
+ #### Withdrawals
361
+
362
+ ```ts
363
+ import { withdraw, type WithdrawResult } from "@veilo/sdk-core";
364
+
365
+ const result: WithdrawResult = await withdraw({
366
+ program,
367
+ relayer: relayerKeypair,
368
+ mintAddress: NATIVE_SOL_MINT,
369
+ amount: 900_000_000n, // 0.9 SOL
370
+ fee: 100_000_000n, // 0.1 SOL to relayer
371
+ inputs: [inputUtxo1, zeroInputUtxo],
372
+ outputs: [changeUtxo, zeroOutputUtxo],
373
+ recipient: recipientKeypair.publicKey,
374
+ tree,
375
+ proofBuilder,
376
+ treeId: 0,
377
+ });
378
+ ```
379
+
380
+ #### Private Transfers
381
+
382
+ ```ts
383
+ import { privateTransfer, type TransferResult } from "@veilo/sdk-core";
384
+
385
+ const result: TransferResult = await privateTransfer({
386
+ program,
387
+ relayer: relayerKeypair,
388
+ mintAddress: NATIVE_SOL_MINT,
389
+ inputs: [inputUtxo1, zeroInputUtxo],
390
+ outputs: [recipientUtxo, changeUtxo],
391
+ recipient: relayerKeypair.publicKey,
392
+ tree,
393
+ proofBuilder,
394
+ treeId: 0,
395
+ });
396
+ ```
397
+
398
+ #### Low-Level `transact`
399
+
400
+ ```ts
401
+ import { transact } from "@veilo/sdk-core";
402
+
403
+ const signature: string = await transact({
404
+ program,
405
+ relayer: relayerKeypair,
406
+ mintAddress: NATIVE_SOL_MINT,
407
+ inputTreeId: 0,
408
+ outputTreeId: 0,
409
+ root: tree.root(),
410
+ publicAmount: 0n,
411
+ inputNullifiers: [nullifier1, nullifier2],
412
+ outputCommitments: [output1.commitment, output2.commitment],
413
+ extData: { recipient, relayer: relayerPubkey, fee: 0n, refund: 0n },
414
+ proof,
415
+ });
416
+ ```
417
+
418
+ ---
419
+
420
+ ### 4.8 Private Swaps
421
+
422
+ Cross-pool private swaps via Jupiter/Raydium. For native SOL source pools the SDK automatically composes the required `fund_native_source` + `transact_swap` instructions into a single atomic transaction.
423
+
424
+ ```ts
425
+ import {
426
+ transactSwap,
427
+ getSwapExecutorPda,
428
+ fundNativeSource,
429
+ type SwapParams,
430
+ type SwapProofStruct,
431
+ } from "@veilo/sdk-core";
432
+
433
+ // Execute a private swap (returns transaction signature)
434
+ const signature: string = await transactSwap({
435
+ program,
436
+ relayer: relayerKeypair,
437
+ sourceMint, // NATIVE_SOL_MINT or SPL token mint
438
+ destMint,
439
+ sourceRoot: tree.root(),
440
+ sourceTreeId: 0,
441
+ destTreeId: 0,
442
+ inputNullifiers: [nullifier0, nullifier1],
443
+ outputCommitments: [changeCommitment, destCommitment],
444
+ proof, // SwapProofStruct
445
+ swapParams: {
446
+ minAmountOut: 950_000_000n,
447
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 60),
448
+ sourceMint,
449
+ destMint,
450
+ destAmount: 950_000_000n,
451
+ swapDataHash: new Uint8Array(32), // SHA-256 of DEX ix bytes, or zeros
452
+ },
453
+ swapAmount: 1_000_000_000n,
454
+ swapData: jupiterInstructionBytes, // Buffer
455
+ extData: { recipient, relayer: relayerPubkey, fee, refund: 0n },
456
+ sourceVaultTokenAccount,
457
+ sourceMintAccount,
458
+ destVaultTokenAccount,
459
+ destMintAccount,
460
+ relayerTokenAccount,
461
+ swapProgram: JUPITER_PROGRAM_ID,
462
+ jupiterEventAuthority,
463
+ });
464
+
465
+ // Build the fund_native_source instruction standalone (advanced)
466
+ const fundIx = await fundNativeSource({
467
+ program,
468
+ relayer: relayerKeypair,
469
+ sourceMint: NATIVE_SOL_MINT,
470
+ destMint,
471
+ inputNullifier0: nullifier0,
472
+ swapAmount: 1_000_000_000n,
473
+ });
474
+ // Returns TransactionInstruction — must be first ix in same tx as transact_swap
475
+ ```
476
+
477
+ > **Note:** `transactSwap` handles the atomicity requirement automatically. Only use `fundNativeSource` directly if you are building transactions manually.
478
+
479
+ ---
480
+
481
+ ### 4.9 Event Scanning & Tree Reconstruction
482
+
483
+ ```ts
484
+ import {
485
+ scanCommitmentEvents,
486
+ scanNullifierEvents,
487
+ buildTreeFromEvents,
488
+ type CommitmentEvent,
489
+ type NullifierSpentEvent,
490
+ } from "@veilo/sdk-core";
491
+
492
+ // Scan all commitment events for a mint (paginated by signature)
493
+ const { events, latestSignature } = await scanCommitmentEvents({
494
+ program,
495
+ mintAddress: NATIVE_SOL_MINT,
496
+ treeId: 0,
497
+ beforeSignature: undefined, // or last known signature for pagination
498
+ limit: 1000,
499
+ });
500
+ // events: CommitmentEvent[]
501
+ // CommitmentEvent = { commitment, leafIndex, newRoot, timestamp, mintAddress, treeId }
502
+
503
+ // Scan spent nullifier events
504
+ const { events: nullEvents } = await scanNullifierEvents({
505
+ program,
506
+ mintAddress: NATIVE_SOL_MINT,
507
+ beforeSignature: undefined,
508
+ limit: 1000,
509
+ });
510
+ // NullifierSpentEvent = { nullifier, mintAddress, treeId }
511
+
512
+ // Reconstruct a Merkle tree from on-chain events
513
+ const {
514
+ tree,
515
+ events: allEvents,
516
+ latestSignature: sig,
517
+ } = await buildTreeFromEvents({
518
+ program,
519
+ mintAddress: NATIVE_SOL_MINT,
520
+ treeId: 0,
521
+ });
522
+ ```
523
+
524
+ ---
525
+
526
+ ### 4.10 Proof Generation
527
+
528
+ ```ts
529
+ import {
530
+ prepareTransactionInputs,
531
+ formatInputsForSnarkjs,
532
+ computeExtDataHash,
533
+ encodeSnarkjsProofToTransactionProof,
534
+ packProofToBytes,
535
+ computeSwapParamsHash,
536
+ computeSwapDataHash,
537
+ type ExtData,
538
+ type TransactionCircuitInputs,
539
+ } from "@veilo/sdk-core";
540
+
541
+ // Prepare inputs for the transaction circuit
542
+ const circuitInputs: TransactionCircuitInputs = prepareTransactionInputs({
543
+ root: tree.root(),
544
+ publicAmount: 1_000_000_000n,
545
+ extData: { recipient, relayer: relayerPubkey, fee: 0n, refund: 0n },
546
+ mintAddress: NATIVE_SOL_MINT,
547
+ inputs: [input1, input2],
548
+ outputs: [output1, output2],
549
+ inputTreeId: 0,
550
+ outputTreeId: 0,
551
+ });
552
+
553
+ // Format for snarkjs (converts bigints / Uint8Arrays to strings)
554
+ const snarkjsInputs = formatInputsForSnarkjs(circuitInputs);
555
+
556
+ // After proof generation:
557
+ // const { proof } = await snarkjs.groth16.fullProve(snarkjsInputs, wasmPath, zkeyPath);
558
+ // const transactionProof = encodeSnarkjsProofToTransactionProof(proof);
559
+
560
+ // Compute ext data hash (matches on-chain computation)
561
+ const extDataHash = computeExtDataHash({
562
+ recipient,
563
+ relayer,
564
+ fee: 0n,
565
+ refund: 0n,
566
+ });
567
+
568
+ // Compute swap param/data hashes (for swap circuit inputs)
569
+ const swapParamsHash = computeSwapParamsHash(swapParams);
570
+ const swapDataHash = computeSwapDataHash(jupiterInstructionBytes);
571
+ ```
572
+
573
+ ---
574
+
575
+ ### 4.11 Fee Utilities
576
+
577
+ ```ts
578
+ import {
579
+ computeWithdrawalFee,
580
+ computeSwapFee,
581
+ DEFAULT_FEE_BPS,
582
+ } from "@veilo/sdk-core/config";
583
+
584
+ // Compute protocol fee for a withdrawal
585
+ const fee = computeWithdrawalFee(amount, feeBps, minWithdrawalFee);
586
+
587
+ // Compute protocol fee for a swap
588
+ const swapFee = computeSwapFee(swapAmount, feeBps);
589
+ ```
590
+
591
+ ---
592
+
593
+ ### 4.12 Relayer & Admin Management
594
+
595
+ ```ts
596
+ import { addRelayer, setPaused } from "@veilo/sdk-core";
597
+
598
+ // Authorise a new relayer for a pool
599
+ await addRelayer({
600
+ program,
601
+ admin: adminKeypair,
602
+ mintAddress: NATIVE_SOL_MINT,
603
+ newRelayer: relayerPubkey,
604
+ });
605
+
606
+ // Pause or unpause a pool
607
+ await setPaused({
608
+ program,
609
+ admin: adminKeypair,
610
+ mintAddress: NATIVE_SOL_MINT,
611
+ paused: true,
612
+ });
613
+ ```
614
+
615
+ ---
616
+
617
+ ### 4.13 Poseidon Utilities
618
+
619
+ ```ts
620
+ import {
621
+ initPoseidon,
622
+ poseidon1,
623
+ poseidon2,
624
+ poseidon3,
625
+ poseidon4,
626
+ pubkeyToField,
627
+ bytesToBigIntBE,
628
+ bigIntToBytesBE,
629
+ BN254_FR_MODULUS,
630
+ } from "@veilo/sdk-core";
631
+
632
+ // Must be called once before using hash functions
633
+ await initPoseidon();
634
+
635
+ const h1 = poseidon1(12345n);
636
+ const h2 = poseidon2(12345n, 67890n);
637
+ const h3 = poseidon3(12345n, 67890n, 11111n);
638
+ const h4 = poseidon4(12345n, 67890n, 11111n, 22222n);
639
+
640
+ // Convert a Solana PublicKey to a BN254 field element
641
+ const field = pubkeyToField(NATIVE_SOL_MINT);
642
+
643
+ // Byte ↔ bigint helpers (big-endian)
644
+ const n = bytesToBigIntBE(bytes32);
645
+ const b = bigIntToBytesBE(someField, 32);
646
+ ```
647
+
648
+ ---
649
+
650
+ ### 4.14 Error Utilities
651
+
652
+ ```ts
653
+ import { parseOnChainError } from "@veilo/sdk-core";
654
+
655
+ try {
656
+ await transactSwap({ ... });
657
+ } catch (err) {
658
+ // Returns a human-readable error string from Anchor/program errors
659
+ const msg = parseOnChainError(err);
660
+ console.error("Swap failed:", msg);
661
+ }
662
+ ```
663
+
664
+ ---
665
+
666
+ ## 5. Architecture
667
+
668
+ ### Transaction Model
669
+
670
+ Veilo uses a UTXO privacy model inspired by Zcash and Tornado Cash Nova:
671
+
672
+ - **Inputs**: 2 UTXOs (zero-value for deposits)
673
+ - **Outputs**: 2 UTXOs (zero-value for withdrawals)
674
+ - **Public Amount**: net pool change — positive = deposit, negative = withdrawal, zero = private transfer or swap
675
+
676
+ Each transaction consumes 2 input UTXOs (Merkle proofs), creates 2 output UTXOs (commitments inserted to tree), and generates 2 nullifiers to prevent double-spend.
677
+
678
+ ### Swap Architecture
679
+
680
+ Private swaps require two instructions in a **single atomic transaction**:
681
+
682
+ 1. `fund_native_source` — pre-funds the swap executor with SOL from the vault (native SOL pools only)
683
+ 2. `transact_swap` — verifies the ZK proof, spends input UTXOs, creates output UTXOs, and executes the DEX swap
684
+
685
+ `transactSwap()` handles this automatically. The on-chain program validates atomicity via the instructions sysvar.
686
+
687
+ ### Privacy Guarantees
688
+
689
+ - **Commitment hiding**: amount, owner, blinding are hidden via Poseidon
690
+ - **Nullifier uniqueness**: each UTXO can only be spent once
691
+ - **Unlinkability**: no public link between inputs and outputs
692
+ - **ZK proofs**: Groth16 verified on-chain
693
+
694
+ ### Constants
695
+
696
+ ```ts
697
+ import {
698
+ NATIVE_SOL_MINT, // PublicKey.default — native SOL pools
699
+ MERKLE_TREE_DEPTH, // 22
700
+ ROOT_HISTORY_SIZE, // 256
701
+ DEFAULT_FEE_BPS, // 50 (0.5%)
702
+ sol, // sol(1) === 1_000_000_000n
703
+ } from "@veilo/sdk-core/config";
704
+ ```
705
+
706
+ ### Type Exports
707
+
708
+ ```ts
709
+ import type {
710
+ // UTXO
711
+ Keypair,
712
+ UTXO,
713
+ SerializedUTXO,
714
+ InputUTXO,
715
+ // Encryption
716
+ EncryptedNote,
717
+ BlindMailboxNote,
718
+ BlindMailboxNoteData,
719
+ DecryptedNote,
720
+ // Proof
721
+ ExtData,
722
+ TransactionCircuitInputs,
723
+ TransactionProofStruct,
724
+ RawProof,
725
+ TransactionProofBuilder,
726
+ // Swap
727
+ SwapProofStruct,
728
+ SwapParams,
729
+ // Tree / events
730
+ MerklePath,
731
+ CircuitMerklePath,
732
+ TreeInfo,
733
+ CommitmentEvent,
734
+ NullifierSpentEvent,
735
+ // Results
736
+ DepositResult,
737
+ WithdrawResult,
738
+ TransferResult,
739
+ // Config
740
+ PoolInitConfig,
741
+ PrivacyConfigAccount,
742
+ GlobalConfigAccount,
743
+ } from "@veilo/sdk-core";
744
+ ```
745
+
746
+ ---
747
+
748
+ ## 6. Development
749
+
750
+ ```bash
751
+ # Run tests (requires devnet or local validator)
752
+ npm test
753
+ ```
754
+
755
+ ---
756
+
757
+ ## 7. Resources
758
+
759
+ - **Repository**: https://github.com/VeiloSolana/veilo-sdk
760
+ - **Circomlibjs**: https://github.com/iden3/circomlibjs
761
+ - **Poseidon Hash**: https://www.poseidon-hash.info/
762
+
763
+ ---
764
+
765
+ ## License
766
+
767
+ ISC
768
+
769
+ This package provides a complete UTXO-based privacy protocol implementation on Solana with:
770
+
771
+ - **UTXO Model**: Full support for unspent transaction outputs with Poseidon commitments
772
+ - **Multi-Tree Support**: Multiple concurrent Merkle trees for improved scalability
773
+ - **ZK Proofs**: Integration with Circom circuits for private transactions
774
+ - **Flexible Operations**: Deposits, withdrawals, and private transfers
775
+ - **Poseidon Hashing**: BN254-curve compatible hashing using circomlibjs
776
+ - **Merkle Trees**: Off-chain Merkle tree management with proof generation
777
+ - **Relayer Support**: Built-in relayer infrastructure for private withdrawals
778
+
779
+ > **Status:** Active development. The SDK supports full transaction privacy with ZK-SNARK proofs.
780
+ > Proofs are generated off-chain and verified on-chain using Groth16.
781
+
782
+ ---
783
+
784
+ ## 1. Installation
785
+
786
+ ```bash
787
+ npm install @veilo/sdk-core
788
+ ```
789
+
790
+ Or from source:
791
+
792
+ ```bash
793
+ cd core-sdk
794
+ npm install
33
795
  ```
34
796
 
35
797
  ---
@@ -38,20 +800,20 @@ npm install @zkprivacysol/sdk-core
38
800
 
39
801
  You need:
40
802
 
41
- - A running Solana validator (localnet recommended):
803
+ - A running Solana validator (localnet/devnet/mainnet):
42
804
 
43
805
  ```bash
44
806
  solana-test-validator
45
807
  ```
46
808
 
47
- - The `privacy-pool` program built and deployed to that validator.
48
- - The `privacy-pool` Anchor IDL available (the SDK tests load it from):
809
+ - The `privacy-pool` program deployed to the network
810
+ - The `privacy-pool` Anchor IDL available:
49
811
 
50
812
  ```text
51
- ../../privacy-pool/target/idl/privacy_pool.json
813
+ idl/idl/privacy_pool.json
52
814
  ```
53
815
 
54
- - A funded keypair on that validator:
816
+ - A funded keypair:
55
817
 
56
818
  ```bash
57
819
  solana config set --url http://127.0.0.1:8899
@@ -59,7 +821,7 @@ You need:
59
821
  solana airdrop 10
60
822
  ```
61
823
 
62
- Environment variables for tests:
824
+ Environment variables:
63
825
 
64
826
  ```bash
65
827
  export ANCHOR_PROVIDER_URL=http://127.0.0.1:8899
@@ -68,372 +830,595 @@ export ANCHOR_WALLET=$HOME/.config/solana/id.json
68
830
 
69
831
  ---
70
832
 
71
- ## 3. Build & Test
72
-
73
- From `packages/sdk-core`:
833
+ ## 3. Build
74
834
 
75
835
  ```bash
76
- # Typecheck & build to dist/
77
836
  npm run build
78
-
79
- # Run unit + integration tests
80
- npm test
81
837
  ```
82
838
 
83
- What the tests do:
839
+ ---
84
840
 
85
- - **Unit tests (`note.test.ts`)**
86
- - `createRandomNote` / `encodeNoteToBytes` / `commitNote` / `createNoteWithCommitment`
87
- - Ensures 32-byte commitments, deterministic encoding, etc.
841
+ ## 4. SDK API
88
842
 
89
- - **Integration test (`sdk.integration.test.ts`)**
90
- - Loads the `privacy-pool` IDL from `privacy-pool/target/idl/privacy_pool.json`
91
- - Constructs an Anchor `Program` with a provider from `ANCHOR_PROVIDER_URL` / `ANCHOR_WALLET`
92
- - Runs end-to-end flow:
93
- 1. `initializePool` (configures denoms + fee)
94
- 2. `createNoteAndDeposit` (creates note, commits it, calls on-chain `depositFixed` and updates the Merkle root)
95
- 3. `addRelayer`
96
- 4. `withdrawViaRelayer` (using a Merkle root that actually contains the note + a demo nullifier, empty proof)
97
- 5. Asserts vault TVL decreased, recipient gained funds, relayer received fee.
843
+ ### 4.1 PDA Helpers
98
844
 
99
- The integration test uses an **empty proof** for now; in a real deployment your **relayer** would generate and verify a Groth16/Plonk proof off-chain, then pass the proof bytes into `withdraw`.
845
+ ```ts
846
+ import {
847
+ getPoolPdas,
848
+ getNoteTreePda,
849
+ getGlobalConfigPda,
850
+ getNullifierMarkerPda,
851
+ } from "@veilo/sdk-core";
852
+ import { PublicKey } from "@solana/web3.js";
100
853
 
101
- ---
854
+ const programId = new PublicKey(
855
+ "YourProgram1111111111111111111111111111111111",
856
+ );
857
+ const mintAddress = new PublicKey(
858
+ "So11111111111111111111111111111111111111112",
859
+ ); // Native SOL
102
860
 
103
- ## 4. SDK Surface
861
+ // Get pool PDAs
862
+ const { config, vault, nullifiers } = getPoolPdas(programId, mintAddress);
104
863
 
105
- ### 4.1 PDA helpers
864
+ // Get note tree PDA for tree ID 0
865
+ const noteTree = getNoteTreePda(programId, mintAddress, 0);
106
866
 
107
- ```ts
108
- import { getPoolPdas } from "@zkprivacysol/sdk-core";
109
- import { PublicKey } from "@solana/web3.js";
867
+ // Get global config
868
+ const globalConfig = getGlobalConfigPda(programId);
110
869
 
111
- const programId = new PublicKey("YourProgram1111111111111111111111111111111111");
112
- const { config, vault, noteTree, nullifiers } = getPoolPdas(programId);
870
+ // Get nullifier marker PDA
871
+ const nullifier = new Uint8Array(32);
872
+ const marker = getNullifierMarkerPda(programId, mintAddress, 0, nullifier);
113
873
  ```
114
874
 
115
- These must match the on-chain seeds (v3 layout):
875
+ **PDA seeds (v3):**
116
876
 
117
- - `["privacy_config_v3"]`
118
- - `["privacy_vault_v3"]`
119
- - `["privacy_note_tree_v3"]`
120
- - `["privacy_nullifiers_v3"]`
877
+ - Config: `["privacy_config_v3", mint_address]`
878
+ - Vault: `["privacy_vault_v3", mint_address]`
879
+ - Note Tree: `["privacy_note_tree_v3", mint_address, tree_id]`
880
+ - Nullifiers: `["privacy_nullifiers_v3", mint_address]`
881
+ - Nullifier Marker: `["privacy_nullifier_v3", mint_address, tree_id, nullifier]`
882
+ - Global Config: `["global_config_v1"]`
121
883
 
122
884
  ---
123
885
 
124
- ### 4.2 Pool init / configuration
886
+ ### 4.2 Pool Initialization
125
887
 
126
888
  ```ts
127
889
  import * as anchor from "@coral-xyz/anchor";
128
- import { initializePool } from "@zkprivacysol/sdk-core";
129
- import { sol } from "@zkprivacysol/sdk-core/config";
130
- import type { Program, Idl } from "@coral-xyz/anchor";
131
-
132
- async function initPool(program: Program<Idl>, adminWallet: anchor.Wallet) {
133
- await initializePool({
134
- program,
135
- admin: adminWallet,
136
- denomsLamports: [sol(1), sol(5)], // 1 SOL & 5 SOL
137
- feeBps: 50, // 0.5% fee
138
- });
139
- }
140
- ```
890
+ import {
891
+ initializeGlobalConfig,
892
+ initializePool,
893
+ updatePoolConfig,
894
+ addMerkleTree,
895
+ getPoolConfig,
896
+ } from "@veilo/sdk-core";
897
+ import { NATIVE_SOL_MINT, sol } from "@veilo/sdk-core/config";
898
+
899
+ // Initialize global config (once per program)
900
+ await initializeGlobalConfig({
901
+ program,
902
+ admin: adminKeypair,
903
+ });
141
904
 
142
- This calls the on-chain `initialize` instruction and sets:
905
+ // Initialize a pool for native SOL
906
+ await initializePool({
907
+ program,
908
+ payer: adminKeypair,
909
+ admin: adminKeypair,
910
+ mintAddress: NATIVE_SOL_MINT,
911
+ minDepositAmount: sol(0.1), // 0.1 SOL
912
+ maxDepositAmount: sol(100), // 100 SOL
913
+ minWithdrawAmount: sol(0.1),
914
+ maxWithdrawAmount: sol(100),
915
+ feeBps: 50, // 0.5%
916
+ feeErrorMarginBps: 10, // 0.1% margin
917
+ minWithdrawalFee: 1_000_000n, // 0.001 SOL minimum
918
+ });
143
919
 
144
- - fixed denominations (in lamports)
145
- - vault + note tree + nullifier set PDAs
146
- - fee in basis points
147
- - initial TVL = 0
920
+ // Add first Merkle tree (tree ID 0)
921
+ await addMerkleTree({
922
+ program,
923
+ payer: adminKeypair,
924
+ mintAddress: NATIVE_SOL_MINT,
925
+ treeId: 0,
926
+ });
927
+
928
+ // Update pool configuration
929
+ await updatePoolConfig({
930
+ program,
931
+ admin: adminKeypair,
932
+ mintAddress: NATIVE_SOL_MINT,
933
+ feeBps: 30, // Change to 0.3%
934
+ });
935
+
936
+ // Read pool configuration
937
+ const poolConfig = await getPoolConfig(program, NATIVE_SOL_MINT);
938
+ console.log("TVL:", poolConfig.totalTvl);
939
+ console.log("Fee:", poolConfig.feeBps, "bps");
940
+ console.log("Num Trees:", poolConfig.numTrees);
941
+ ```
148
942
 
149
943
  ---
150
944
 
151
- ### 4.3 Notes & commitments
945
+ ### 4.3 UTXO Management
152
946
 
153
- `src/note.ts` is a small “note” helper module. It **does not** implement real zk-friendly Poseidon hashing yet; it’s just using SHA-256 to get 32-byte commitments that the on-chain program treats as opaque.
947
+ The SDK uses a UTXO (Unspent Transaction Output) model with Poseidon commitments:
154
948
 
155
949
  ```ts
156
950
  import {
157
- createRandomNote,
158
- encodeNoteToBytes,
159
- commitNote,
160
- createNoteWithCommitment,
161
- } from "@zkprivacysol/sdk-core/note";
162
- import { Keypair } from "@solana/web3.js";
163
-
164
- const owner = Keypair.generate().publicKey;
165
-
166
- // 1. Create a note
167
- const note = createRandomNote({
168
- value: 1_000_000n, // lamports
169
- owner,
951
+ generateKeypair,
952
+ keypairFromPrivateKey,
953
+ createUTXO,
954
+ createOwnedUTXO,
955
+ createOwnedZeroUTXO,
956
+ deriveNullifier,
957
+ type Keypair,
958
+ type UTXO,
959
+ type SerializedUTXO,
960
+ type InputUTXO,
961
+ } from "@veilo/sdk-core";
962
+ import { NATIVE_SOL_MINT } from "@veilo/sdk-core/config";
963
+ import { pubkeyToField } from "@veilo/sdk-core";
964
+
965
+ // Generate a random keypair
966
+ const keypair: Keypair = generateKeypair();
967
+ // keypair = { privateKey: bigint, publicKey: bigint }
968
+
969
+ // Restore keypair from private key
970
+ const restored = keypairFromPrivateKey(privateKeyBigInt);
971
+
972
+ // Create an owned UTXO (includes private key)
973
+ const ownedUtxo: SerializedUTXO = createOwnedUTXO({
974
+ amount: 1_000_000_000n,
975
+ privateKey: keypair.privateKey,
976
+ mintAddress: NATIVE_SOL_MINT,
170
977
  });
171
978
 
172
- // 2. Encode deterministically
173
- const bytes = encodeNoteToBytes(note);
979
+ // Create a zero UTXO (for unused inputs/outputs)
980
+ const zeroUtxo = createOwnedZeroUTXO(pubkeyToField(NATIVE_SOL_MINT), keypair);
174
981
 
175
- // 3. Hash to a 32-byte commitment (placeholder)
176
- const commitment = commitNote(note);
982
+ // Derive nullifier for spending
983
+ const nullifier = deriveNullifier(
984
+ ownedUtxo.privateKey,
985
+ ownedUtxo.commitment,
986
+ 0, // pathIndex in Merkle tree
987
+ 0, // treeId
988
+ );
989
+ ```
990
+
991
+ **UTXO commitment formula:**
177
992
 
178
- // 4. Convenience combo
179
- const full = createNoteWithCommitment({
180
- value: 1_000_000n,
181
- owner,
182
- });
183
- // full.commitment is 32 bytes
993
+ ```
994
+ commitment = Poseidon(amount, pubkey, blinding, mintAddress)
184
995
  ```
185
996
 
186
- Encoding layout:
997
+ **Nullifier formula:**
187
998
 
188
- ```text
189
- value (u64 LE, 8 bytes)
190
- || owner pubkey (32 bytes)
191
- || rho (32 bytes random)
192
- || r (32 bytes random)
999
+ ```
1000
+ nullifier = Poseidon(privateKey, commitment, pathIndex, treeId)
193
1001
  ```
194
1002
 
195
- Hash:
1003
+ ---
1004
+
1005
+ ### 4.4 Merkle Tree Operations
196
1006
 
197
1007
  ```ts
198
- commitment = sha256(encodedBytes);
199
- ```
1008
+ import { MerkleTree } from "@veilo/sdk-core";
200
1009
 
201
- Later, a real implementation should swap this out for the exact hash function used inside the zk circuit (Poseidon/Rescue/etc.). The on-chain program just sees `[u8; 32]`.
1010
+ // Create a new Merkle tree (default depth: 20)
1011
+ const tree = new MerkleTree();
202
1012
 
203
- ---
1013
+ // Insert commitments
1014
+ const index1 = tree.insert(ownedUtxo1.commitment);
1015
+ const index2 = tree.insert(ownedUtxo2.commitment);
204
1016
 
205
- ### 4.4 Merkle helpers (demo-only)
1017
+ // Get current root
1018
+ const root = tree.root();
206
1019
 
207
- `src/merkle.ts` provides **very basic** Merkle helpers so callers can build roots off-chain. This is meant for demos/tests, not production.
1020
+ // Get Merkle path for proof generation
1021
+ const path = tree.path(index1);
1022
+ // path = { pathElements: Uint8Array[], pathIndices: number[] }
208
1023
 
209
- Key functions:
1024
+ // Get number of leaves
1025
+ const numLeaves = tree.totalLeaves;
210
1026
 
211
- ```ts
212
- import {
213
- merkleLeafFromCommitment,
214
- merkleHashPair,
215
- merkleRootFromLeaves,
216
- MerkleTree,
217
- } from "@zkprivacysol/sdk-core/merkle";
218
-
219
- // Stateless helpers
220
- const leaf = merkleLeafFromCommitment(commitment);
221
- const parent = merkleHashPair(left, right);
222
- const root = merkleRootFromLeaves([leaf1, leaf2, leaf3]);
223
-
224
- // Simple incremental tree (toy)
225
- const tree = new MerkleTree();
226
- const { index, root: newRoot } = tree.insert(commitment);
227
- const path = tree.getPath(index); // Merkle path for proofs
1027
+ // Custom tree depth
1028
+ const deepTree = new MerkleTree(25); // 25 levels
228
1029
  ```
229
1030
 
230
- Notes:
231
-
232
- - Uses SHA-256 under the hood, returning `Uint8Array` of length 32.
233
- - Pads with a “zero node” derived from hashing the all-zero leaf repeatedly up the tree.
234
- - This is intentionally “toy” to keep the SDK usable while the real circuit/Merkle design is still in flux.
235
- - On-chain, the program only stores the **latest Merkle root** in the `NoteTree` account (v3 layout exposes a `currentRoot` field).
1031
+ The Merkle tree uses Poseidon hash for all internal nodes.
236
1032
 
237
1033
  ---
238
1034
 
239
- ### 4.5 Deposits
1035
+ ### 4.5 Transaction Operations
240
1036
 
241
- Low-level helper (you supply both commitment + Merkle root):
1037
+ The SDK supports three types of transactions:
1038
+
1039
+ #### Deposits (publicAmount > 0)
242
1040
 
243
1041
  ```ts
244
- import * as anchor from "@coral-xyz/anchor";
245
- import { depositFixedSol } from "@zkprivacysol/sdk-core";
1042
+ import { deposit } from "@veilo/sdk-core";
246
1043
 
247
- const provider = anchor.getProvider() as anchor.AnchorProvider;
248
- const wallet = provider.wallet as anchor.Wallet;
1044
+ // Create output UTXO
1045
+ const outputUtxo = createOwnedUTXO({
1046
+ amount: 1_000_000_000n, // 1 SOL
1047
+ mintAddress: pubkeyToField(NATIVE_SOL_MINT),
1048
+ keypair: utxoKeypair,
1049
+ });
1050
+
1051
+ // Zero inputs for deposit
1052
+ const input1 = createOwnedZeroUTXO(pubkeyToField(NATIVE_SOL_MINT), utxoKeypair);
1053
+ const input2 = createOwnedZeroUTXO(pubkeyToField(NATIVE_SOL_MINT), utxoKeypair);
1054
+ const output2 = createOwnedZeroUTXO(
1055
+ pubkeyToField(NATIVE_SOL_MINT),
1056
+ utxoKeypair,
1057
+ );
249
1058
 
250
- await depositFixedSol({
1059
+ await deposit({
251
1060
  program,
252
- depositor: wallet,
253
- denomIndex: 0, // index into cfg.denoms
254
- commitment, // 32-byte note commitment
255
- newRoot, // 32-byte Merkle root (caller computed off-chain)
1061
+ depositor: depositorKeypair,
1062
+ mintAddress: NATIVE_SOL_MINT,
1063
+ inputTreeId: 0,
1064
+ outputTreeId: 0,
1065
+ root: tree.root(),
1066
+ publicAmount: 1_000_000_000n, // Positive = deposit
1067
+ inputs: [input1, input2],
1068
+ outputs: [outputUtxo, output2],
1069
+ recipient: depositorKeypair.publicKey,
1070
+ fee: 0n,
1071
+ refund: 0n,
1072
+ proof: mockProof,
256
1073
  });
1074
+
1075
+ // Insert outputs into tree
1076
+ tree.insert(outputUtxo.commitment);
1077
+ tree.insert(output2.commitment);
257
1078
  ```
258
1079
 
259
- High-level helper (with note creation, but you still feed a root):
1080
+ #### Withdrawals (publicAmount < 0)
260
1081
 
261
1082
  ```ts
262
- import * as anchor from "@coral-xyz/anchor";
263
- import { createNoteAndDeposit } from "@zkprivacysol/sdk-core";
264
- import { sol } from "@zkprivacysol/sdk-core/config";
1083
+ import { withdraw } from "@veilo/sdk-core";
1084
+
1085
+ // Prepare input with Merkle path
1086
+ const input1: InputUTXO = {
1087
+ ...ownedUtxo1,
1088
+ pathIndex: 0,
1089
+ pathElements: tree.path(0).pathElements,
1090
+ };
1091
+
1092
+ const zeroInput2: InputUTXO = {
1093
+ ...createOwnedZeroUTXO(pubkeyToField(NATIVE_SOL_MINT), utxoKeypair),
1094
+ pathIndex: 0,
1095
+ pathElements: tree.path(0).pathElements,
1096
+ };
1097
+
1098
+ // Zero outputs
1099
+ const output1 = createOwnedZeroUTXO(
1100
+ pubkeyToField(NATIVE_SOL_MINT),
1101
+ utxoKeypair,
1102
+ );
1103
+ const output2 = createOwnedZeroUTXO(
1104
+ pubkeyToField(NATIVE_SOL_MINT),
1105
+ utxoKeypair,
1106
+ );
1107
+
1108
+ await withdraw({
1109
+ program,
1110
+ relayer: relayerKeypair,
1111
+ mintAddress: NATIVE_SOL_MINT,
1112
+ inputTreeId: 0,
1113
+ outputTreeId: 0,
1114
+ root: tree.root(),
1115
+ publicAmount: -900_000_000n, // Negative = withdrawal (0.9 SOL)
1116
+ inputs: [input1, zeroInput2],
1117
+ outputs: [output1, output2],
1118
+ recipient: recipientKeypair.publicKey,
1119
+ fee: 100_000_000n, // 0.1 SOL to relayer
1120
+ refund: 0n,
1121
+ proof,
1122
+ });
1123
+ ```
265
1124
 
266
- const provider = anchor.getProvider() as anchor.AnchorProvider;
267
- const wallet = provider.wallet as anchor.Wallet;
1125
+ #### Private Transfers (publicAmount = 0)
268
1126
 
269
- const dummyRoot = new Uint8Array(32).fill(7); // replace with real Merkle root
1127
+ ```ts
1128
+ import {
1129
+ privateTransfer,
1130
+ generateKeypair,
1131
+ createOwnedUTXO,
1132
+ } from "@veilo/sdk-core";
1133
+
1134
+ // Create new outputs for recipient
1135
+ const recipientKeypair = generateKeypair();
1136
+ const output1 = createOwnedUTXO({
1137
+ amount: 1_000_000_000n,
1138
+ privateKey: recipientKeypair.privateKey,
1139
+ mintAddress: NATIVE_SOL_MINT,
1140
+ });
270
1141
 
271
- const note = await createNoteAndDeposit({
1142
+ await privateTransfer({
272
1143
  program,
273
- depositor: wallet,
274
- denomIndex: 0,
275
- valueLamports: sol(1),
276
- newRoot: dummyRoot,
1144
+ relayer: relayerKeypair,
1145
+ mintAddress: NATIVE_SOL_MINT,
1146
+ inputTreeId: 0,
1147
+ outputTreeId: 0,
1148
+ root: tree.root(),
1149
+ publicAmount: 0n, // Zero = private transfer
1150
+ inputs: [input1, zeroInput2],
1151
+ outputs: [output1, output2],
1152
+ recipient: relayerKeypair.publicKey,
1153
+ fee: 0n,
1154
+ refund: 0n,
1155
+ proof,
277
1156
  });
278
1157
 
279
- // note.commitment can later be used in your off-chain tree
1158
+ // Insert new outputs
1159
+ tree.insert(output1.commitment);
1160
+ tree.insert(output2.commitment);
280
1161
  ```
281
1162
 
282
- There’s also a higher-level helper that integrates directly with an in-memory `MerkleTree`:
1163
+ #### Low-Level Transaction Function
1164
+
1165
+ For advanced use cases, use the unified `transact` function directly:
283
1166
 
284
1167
  ```ts
285
- import { createNoteDepositWithMerkle } from "@zkprivacysol/sdk-core";
286
- import { MerkleTree } from "@zkprivacysol/sdk-core/merkle";
1168
+ import { transact } from "@veilo/sdk-core";
287
1169
 
288
- const tree = new MerkleTree();
1170
+ await transact({
1171
+ program,
1172
+ relayer: relayerKeypair,
1173
+ mintAddress: NATIVE_SOL_MINT,
1174
+ inputTreeId: 0,
1175
+ outputTreeId: 0,
1176
+ root: tree.root(),
1177
+ publicAmount: 0n,
1178
+ inputNullifiers: [nullifier1, nullifier2],
1179
+ outputCommitments: [output1.commitment, output2.commitment],
1180
+ extData: {
1181
+ recipient: recipientPubkey,
1182
+ relayer: relayerPubkey,
1183
+ fee: 0n,
1184
+ refund: 0n,
1185
+ },
1186
+ proof,
1187
+ });
1188
+ ```
289
1189
 
290
- const { note, leafIndex, root, merklePath } =
291
- await createNoteDepositWithMerkle({
292
- program,
293
- depositor: wallet,
294
- denomIndex: 0,
295
- valueLamports: sol(1),
296
- tree,
297
- });
1190
+ ---
298
1191
 
299
- // `root` is what got written to the on-chain NoteTree
300
- // `merklePath` can be used as witness for the zk circuit
301
- ```
1192
+ ### 4.6 Proof Generation
302
1193
 
303
- On-chain, the `depositFixed` instruction:
1194
+ The SDK provides utilities for preparing circuit inputs:
304
1195
 
305
- - moves SOL from `depositor` to the vault PDA,
306
- - updates TVL,
307
- - writes `new_root` into the on-chain note tree’s current root field.
1196
+ ```ts
1197
+ import {
1198
+ prepareTransactionInputs,
1199
+ formatInputsForSnarkjs,
1200
+ computeExtDataHash,
1201
+ encodeSnarkjsProofToTransactionProof,
1202
+ packProofToBytes,
1203
+ type ExtData,
1204
+ type TransactionCircuitInputs,
1205
+ } from "@veilo/sdk-core";
1206
+
1207
+ // Prepare inputs for the circuit
1208
+ const circuitInputs: TransactionCircuitInputs = prepareTransactionInputs({
1209
+ root: tree.root(),
1210
+ publicAmount: 1_000_000_000n,
1211
+ extData: {
1212
+ recipient: recipientPubkey,
1213
+ relayer: relayerPubkey,
1214
+ fee: 0n,
1215
+ refund: 0n,
1216
+ },
1217
+ mintAddress: NATIVE_SOL_MINT,
1218
+ inputs: [input1, input2],
1219
+ outputs: [output1, output2],
1220
+ inputTreeId: 0,
1221
+ outputTreeId: 0,
1222
+ });
308
1223
 
309
- ---
1224
+ // Format for snarkjs (converts Uint8Array to string representations)
1225
+ const snarkjsInputs = formatInputsForSnarkjs(circuitInputs);
310
1226
 
311
- ### 4.6 Relayers & Withdrawals
1227
+ // Use with snarkjs to generate proof
1228
+ // const { proof, publicSignals } = await snarkjs.groth16.fullProve(
1229
+ // snarkjsInputs,
1230
+ // wasmPath,
1231
+ // zkeyPath
1232
+ // );
312
1233
 
313
- Add a relayer (admin-only):
1234
+ // Convert snarkjs proof to on-chain format
1235
+ // const transactionProof = encodeSnarkjsProofToTransactionProof(proof);
1236
+ ```
314
1237
 
315
- ```ts
316
- import * as anchor from "@coral-xyz/anchor";
317
- import { addRelayer } from "@zkprivacysol/sdk-core";
318
- import { Keypair } from "@solana/web3.js";
1238
+ ---
319
1239
 
320
- const provider = anchor.getProvider() as anchor.AnchorProvider;
321
- const wallet = provider.wallet as anchor.Wallet;
1240
+ ### 4.7 Relayer Management
322
1241
 
323
- const relayer = Keypair.generate();
1242
+ ```ts
1243
+ import { addRelayer, setPaused } from "@veilo/sdk-core";
324
1244
 
1245
+ // Add a relayer
325
1246
  await addRelayer({
326
1247
  program,
327
- admin: wallet,
328
- newRelayer: relayer.publicKey,
1248
+ admin: adminKeypair,
1249
+ mintAddress: NATIVE_SOL_MINT,
1250
+ newRelayer: relayerPubkey,
1251
+ });
1252
+
1253
+ // Pause/unpause the pool
1254
+ await setPaused({
1255
+ program,
1256
+ admin: adminKeypair,
1257
+ mintAddress: NATIVE_SOL_MINT,
1258
+ paused: true,
329
1259
  });
330
1260
  ```
331
1261
 
332
- Withdraw via relayer (SDK-level helper):
1262
+ ---
1263
+
1264
+ ### 4.8 Poseidon Utilities
333
1265
 
334
1266
  ```ts
335
- import { withdrawViaRelayer } from "@zkprivacysol/sdk-core";
336
- import { Keypair } from "@solana/web3.js";
1267
+ import {
1268
+ initPoseidon,
1269
+ poseidon1,
1270
+ poseidon2,
1271
+ poseidon3,
1272
+ poseidon4,
1273
+ pubkeyToField,
1274
+ } from "@veilo/sdk-core";
1275
+
1276
+ // Initialize Poseidon (required once before using hash functions)
1277
+ await initPoseidon();
1278
+
1279
+ // Hash functions
1280
+ const hash1 = poseidon1(12345n);
1281
+ const hash2 = poseidon2(12345n, 67890n);
1282
+ const hash3 = poseidon3(12345n, 67890n, 11111n);
1283
+ const hash4 = poseidon4(12345n, 67890n, 11111n, 22222n);
1284
+
1285
+ // Convert Solana PublicKey to field element
1286
+ const fieldElement = pubkeyToField(NATIVE_SOL_MINT);
1287
+ ```
337
1288
 
338
- const relayer = Keypair.generate();
339
- const recipient = Keypair.generate();
1289
+ ---
340
1290
 
341
- const root = /* 32-byte Merkle root containing the note */;
342
- const nullifier = new Uint8Array(32).fill(3); // demo only
1291
+ ## 5. Architecture
343
1292
 
344
- // In the real world, `proof` will be zk-proof bytes coming from your prover.
345
- const proofBytes = new Uint8Array([]); // currently ignored by on-chain program
1293
+ ### Transaction Model
346
1294
 
347
- await withdrawViaRelayer({
348
- program,
349
- relayer,
350
- recipient: recipient.publicKey,
351
- denomIndex: 0,
352
- root,
353
- nullifier,
354
- proof: proofBytes,
355
- });
356
- ```
1295
+ Veilo uses a UTXO-based privacy model inspired by Zcash and Tornado Cash Nova:
357
1296
 
358
- There is also a higher-level helper (`withdrawViaRelayerWithProof`) that takes:
1297
+ 1. **Inputs**: 2 UTXOs (can be zero for deposits)
1298
+ 2. **Outputs**: 2 UTXOs (can be zero for withdrawals)
1299
+ 3. **Public Amount**: Net change (positive = deposit, negative = withdrawal, zero = private transfer)
359
1300
 
360
- - `noteData` (serialized note),
361
- - `merklePath`,
362
- - `feeBps`,
363
- - a `builder: ProofBuilder` callback
1301
+ Each transaction:
364
1302
 
365
- and lets you plug in your own proof generator. In practice, your **relayer service** will own that logic.
1303
+ - Consumes 2 input UTXOs (proven via Merkle paths)
1304
+ - Creates 2 output UTXOs (commitments added to tree)
1305
+ - Generates 2 nullifiers (prevents double-spending)
1306
+ - Optionally transfers funds in/out of the pool
366
1307
 
367
- **Production pattern:**
1308
+ ### Privacy Guarantees
368
1309
 
369
- - A **backend relayer service** (see `packages/relayer` or similar) owns:
370
- - the proving key / circuits,
371
- - a mirror view of the Merkle tree and nullifier set,
372
- - a funded relayer keypair.
373
- - The front-end sends a withdraw request to that service:
374
- - `root`, `nullifier`, `denomIndex`, `recipient`, plus any private witness data.
375
- - The relayer:
376
- 1. Builds & verifies the zk proof off-chain.
377
- 2. Packs it into bytes (e.g. via a `packProofToBytes` helper).
378
- 3. Calls the on-chain `withdraw` via Anchor, using the same program/PDAs as the SDK.
1310
+ - **Commitment hiding**: Amount, owner, and blinding factor are hidden via Poseidon hash
1311
+ - **Nullifier uniqueness**: Each UTXO can only be spent once
1312
+ - **Unlinkability**: No public link between inputs and outputs
1313
+ - **Zero-knowledge proofs**: Transactions proven valid without revealing private data
379
1314
 
380
- From the SDK’s perspective, `proof: Uint8Array` is **already-built**; this package doesn’t know how you generated it.
1315
+ ### Multi-Tree Support
381
1316
 
382
- ---
1317
+ The protocol supports multiple concurrent Merkle trees per pool:
1318
+
1319
+ - Improves scalability by reducing tree depth
1320
+ - Allows parallel insertions
1321
+ - Each tree has independent state
1322
+
1323
+ ### Constants
1324
+
1325
+ ```ts
1326
+ import {
1327
+ NATIVE_SOL_MINT, // PublicKey.default (all zeros) for native SOL
1328
+ MERKLE_TREE_DEPTH, // 22 levels
1329
+ ROOT_HISTORY_SIZE, // 256 historical roots
1330
+ DEFAULT_FEE_BPS, // 50 (0.5%)
1331
+ sol, // Helper: sol(1) = 1_000_000_000n lamports
1332
+ } from "@veilo/sdk-core/config";
1333
+
1334
+ import { BN254_FR_MODULUS } from "@veilo/sdk-core";
1335
+ ```
1336
+
1337
+ ### Type Exports
1338
+
1339
+ The SDK exports the following types for TypeScript users:
383
1340
 
384
- ## 5. Environment & Localnet
1341
+ ```ts
1342
+ import type {
1343
+ // UTXO types
1344
+ Keypair,
1345
+ UTXO,
1346
+ SerializedUTXO,
1347
+ InputUTXO,
1348
+
1349
+ // Proof types
1350
+ ExtData,
1351
+ TransactionCircuitInputs,
1352
+ TransactionProofStruct,
1353
+ RawProof,
1354
+ TransactionProofBuilder,
1355
+
1356
+ // Merkle types
1357
+ MerklePath,
1358
+ CircuitMerklePath,
1359
+
1360
+ // Config types
1361
+ PoolInitConfig,
1362
+ PrivacyConfigAccount,
1363
+ GlobalConfigAccount,
1364
+ } from "@veilo/sdk-core";
1365
+ ```
385
1366
 
386
- To run the integration tests successfully, you should:
1367
+ ### SPL Token Support
387
1368
 
388
- 1. Start local validator:
1369
+ The SDK supports both native SOL and SPL tokens:
389
1370
 
390
- ```bash
391
- solana-test-validator
392
- ```
1371
+ ```ts
1372
+ import { PublicKey } from "@solana/web3.js";
1373
+
1374
+ // For native SOL
1375
+ const solMint = NATIVE_SOL_MINT; // PublicKey.default
1376
+
1377
+ // For SPL tokens
1378
+ const usdcMint = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
1379
+
1380
+ // Initialize pool for SPL token
1381
+ await initializePool({
1382
+ program,
1383
+ payer: adminKeypair,
1384
+ admin: adminKeypair,
1385
+ mintAddress: usdcMint, // Use SPL token mint
1386
+ minDepositAmount: 1_000_000n, // 1 USDC (6 decimals)
1387
+ // ... other params
1388
+ });
1389
+ ```
393
1390
 
394
- 2. Build & deploy the `privacy-pool` Anchor program in `packages/privacy-pool`:
1391
+ When using SPL tokens, the SDK automatically handles associated token accounts.
395
1392
 
396
- ```bash
397
- cd packages/privacy-pool
398
- anchor build
399
- anchor deploy
400
- ```
1393
+ ---
401
1394
 
402
- 3. Ensure your CLI and wallet match the validator:
1395
+ ## 6. Development
403
1396
 
404
- ```bash
405
- solana config set --url http://127.0.0.1:8899
406
- solana-keygen new --outfile ~/.config/solana/id.json
407
- solana airdrop 10
408
- ```
1397
+ ### Environment Variables
409
1398
 
410
- 4. Export env vars (or inject via `npm test` script):
1399
+ ```bash
1400
+ export ANCHOR_PROVIDER_URL=http://127.0.0.1:8899
1401
+ export ANCHOR_WALLET=$HOME/.config/solana/id.json
1402
+ ```
411
1403
 
412
- ```bash
413
- export ANCHOR_PROVIDER_URL=http://127.0.0.1:8899
414
- export ANCHOR_WALLET=$HOME/.config/solana/id.json
415
- ```
1404
+ ---
416
1405
 
417
- 5. Then from `packages/sdk-core`:
1406
+ ## 7. Limitations
418
1407
 
419
- ```bash
420
- npm run build
421
- npm test
422
- ```
1408
+ - **Off-chain tree management**: Merkle trees must be maintained by relayers/clients
1409
+ - **Proof generation not included**: You must integrate your own circuit/prover
1410
+ - **Development status**: Active development, APIs may change
423
1411
 
424
1412
  ---
425
1413
 
426
- ## 6. Limitations & TODOs
1414
+ ## 8. Resources
1415
+
1416
+ - **Repository**: https://github.com/VeiloSolana/veilo-sdk
1417
+ - **Circomlibjs**: https://github.com/iden3/circomlibjs
1418
+ - **Poseidon Hash**: https://www.poseidon-hash.info/
1419
+
1420
+ ---
427
1421
 
428
- - **Merkle tree is minimal.**
429
- - Toy implementation, primarily for demos/tests.
430
- - No persisted tree; you’re expected to maintain state in your own service.
431
- - **On-chain NoteTree only stores the latest root.**
432
- - Historical roots/nullifiers must be mirrored off-chain.
433
- - **Proofs are relayer-only.**
434
- - SDK does **not** generate Groth16/Plonk proofs.
435
- - On-chain program currently only sees `Vec<u8>` and does not verify it yet.
436
- - **API is still evolving.**
437
- - Types, exports, and function signatures may change as the circuit + relayer design solidifies.
1422
+ ## License
438
1423
 
439
- This SDK is meant as a thin, hackable layer around the Anchor program while the core privacy design (circuit, proof system, Merkle layout, relayer flow) is being explored.
1424
+ ISC