veilo-sdk-core
v0.3.2
Published
TypeScript SDK for the Veilo Privacy Pool - UTXO-based privacy protocol on Solana with ZK-SNARKs
Readme
veilo-sdk-core
TypeScript SDK for the Veilo Privacy Pool Anchor program.
This package provides a complete UTXO-based privacy protocol implementation on Solana with:
- UTXO Model: Full support for unspent transaction outputs with Poseidon commitments
- Multi-Tree Support: Multiple concurrent Merkle trees for improved scalability
- ZK Proofs: Integration with Circom circuits for private transactions
- Private Swaps: Atomic cross-pool swaps via Jupiter (native SOL + SPL tokens)
- Note Encryption: NaCl-based encrypted UTXO notes and blind mailbox delivery
- Event Scanning: Reconstruct Merkle trees from on-chain events
- Poseidon Hashing: BN254-curve compatible hashing using circomlibjs
- Relayer Support: Built-in relayer infrastructure for private withdrawals and swaps
Status: Active development. The SDK supports full transaction privacy with ZK-SNARK proofs. Proofs are generated off-chain and verified on-chain using Groth16.
1. Installation
npm install veilo-sdk-core2. Prerequisites
- A running Solana validator (localnet/devnet/mainnet)
- The
privacy-poolprogram deployed to the network - A funded keypair
export ANCHOR_PROVIDER_URL=https://api.devnet.solana.com
export ANCHOR_WALLET=$HOME/.config/solana/id.json3. Build
npm run build4. SDK API
4.1 PDA Helpers
import {
getPoolPdas,
getNoteTreePda,
getGlobalConfigPda,
getNullifierMarkerPda,
getSwapExecutorPda,
} from "veilo-sdk-core";
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey(
"YourProgram1111111111111111111111111111111111",
);
const mintAddress = new PublicKey(
"So11111111111111111111111111111111111111112",
);
// Pool PDAs (config, vault, nullifiers)
const { config, vault, nullifiers } = getPoolPdas(programId, mintAddress);
// Note tree PDA for tree ID 0
const noteTree = getNoteTreePda(programId, mintAddress, 0);
// Global config PDA (one per program)
const globalConfig = getGlobalConfigPda(programId);
// Nullifier marker PDA (prevents double-spend)
const nullifier = new Uint8Array(32);
const marker = getNullifierMarkerPda(programId, mintAddress, nullifier);
// Swap executor PDA (for cross-pool swaps)
const relayerPubkey = new PublicKey("...");
const executor = getSwapExecutorPda(
programId,
sourceMint,
destMint,
inputNullifier0, // Uint8Array[32]
relayerPubkey,
);PDA seeds (v3):
| Account | Seeds |
| ---------------- | ------------------------------------------------------------------- |
| Config | ["privacy_config_v3", mint] |
| Vault | ["privacy_vault_v3", mint] |
| Note Tree | ["privacy_note_tree_v3", mint, tree_id] |
| Nullifiers | ["privacy_nullifiers_v3", mint] |
| Nullifier Marker | ["nullifier_v3", mint, nullifier] |
| Global Config | ["global_config_v1"] |
| Swap Executor | ["swap_executor_v1", source_mint, dest_mint, nullifier0, relayer] |
4.2 Pool Initialization
import {
initializeGlobalConfig,
initializePool,
updatePoolConfig,
addMerkleTree,
getPoolConfig,
updateGlobalConfig,
} from "veilo-sdk-core";
import { NATIVE_SOL_MINT, sol } from "veilo-sdk-core/config";
// Initialize global config (once per program)
await initializeGlobalConfig({ program, admin: adminKeypair });
// Update global config
await updateGlobalConfig({
program,
admin: adminKeypair,
newAdmin: newAdminPubkey, // optional
paused: false, // optional
});
// Initialize a pool for native SOL
await initializePool({
program,
payer: adminKeypair,
admin: adminKeypair,
mintAddress: NATIVE_SOL_MINT,
minDepositAmount: sol(0.1),
maxDepositAmount: sol(100),
minWithdrawAmount: sol(0.1),
maxWithdrawAmount: sol(100),
feeBps: 50, // 0.5%
feeErrorMarginBps: 10, // 0.1% margin
minWithdrawalFee: 1_000_000n,
});
// Add Merkle tree (tree ID 0)
await addMerkleTree({
program,
payer: adminKeypair,
mintAddress: NATIVE_SOL_MINT,
treeId: 0,
});
// Update pool fee
await updatePoolConfig({
program,
admin: adminKeypair,
mintAddress: NATIVE_SOL_MINT,
feeBps: 30,
});
// Read pool configuration
const poolConfig = await getPoolConfig(program, NATIVE_SOL_MINT);
console.log("TVL:", poolConfig.totalTvl, " Fee:", poolConfig.feeBps, "bps");4.3 On-Chain Account Queries
import {
fetchPoolConfig,
checkNullifierSpent,
getTreeInfo,
getAllTreeInfo,
getBestTreeForDeposit,
} from "veilo-sdk-core";
// Fetch decoded PrivacyConfig account
const config = await fetchPoolConfig(program, NATIVE_SOL_MINT);
// Check if a nullifier has been spent
const spent = await checkNullifierSpent(program, NATIVE_SOL_MINT, nullifier);
// Info for a single tree
const info = await getTreeInfo(program, NATIVE_SOL_MINT, 0);
// info = { treeId, leafCount, root, isFull }
// Info for all trees in a pool
const allInfo = await getAllTreeInfo(program, NATIVE_SOL_MINT);
// Pick the best tree for depositing (least full with capacity)
const best = await getBestTreeForDeposit(program, NATIVE_SOL_MINT);
// best = { treeId, leafCount, root }4.4 UTXO Management
import {
generateKeypair,
keypairFromPrivateKey,
createUTXO,
createOwnedUTXO,
createOwnedZeroUTXO,
deriveNullifier,
type Keypair,
type UTXO,
type SerializedUTXO,
type InputUTXO,
} from "veilo-sdk-core";
import { NATIVE_SOL_MINT } from "veilo-sdk-core/config";
import { pubkeyToField } from "veilo-sdk-core";
// Generate a random UTXO keypair
const keypair: Keypair = generateKeypair();
// keypair = { privateKey: bigint, publicKey: bigint }
// Restore from private key
const restored = keypairFromPrivateKey(privateKeyBigInt);
// Create an owned UTXO (includes private key)
const ownedUtxo: SerializedUTXO = createOwnedUTXO({
amount: 1_000_000_000n,
privateKey: keypair.privateKey,
mintAddress: NATIVE_SOL_MINT,
});
// Create a zero-value UTXO (for unused inputs/outputs)
const zeroUtxo = createOwnedZeroUTXO(pubkeyToField(NATIVE_SOL_MINT), keypair);
// Derive nullifier for spending
const pathIndex = 0; // leaf index in Merkle tree
const treeId = 0;
const nullifier = deriveNullifier(
ownedUtxo.privateKey,
ownedUtxo.commitment,
pathIndex,
treeId,
);UTXO commitment: Poseidon(amount, pubkey, blinding, mintAddress)
Nullifier: Poseidon(privateKey, commitment, pathIndex, treeId)
4.5 UTXO Encryption
The SDK provides NaCl-based encryption for UTXO notes (relayer storage) and blind mailbox delivery (wallet-to-wallet).
import {
deriveEncryptionKeypair,
encryptUTXONote,
decryptUTXONote,
encryptBlindMailboxNote,
decryptBlindMailboxNote,
fetchAndDecryptNotes,
computeSignature,
inputUTXOToCircuitFormat,
type EncryptedNote,
type BlindMailboxNote,
type BlindMailboxNoteData,
type DecryptedNote,
} from "veilo-sdk-core";
import { Keypair as SolanaKeypair } from "@solana/web3.js";
// Derive a NaCl encryption keypair from a UTXO private key
const encKeypair = deriveEncryptionKeypair(utxoKeypair.privateKey);
// encKeypair = { publicKey: Uint8Array[32], secretKey: Uint8Array[32] }
// Encrypt a UTXO note for relay storage
const encryptedNote: EncryptedNote = encryptUTXONote(
serializedUtxo, // SerializedUTXO
encKeypair.publicKey, // NaCl public key (Uint8Array[32])
);
// Decrypt a UTXO note
const decrypted: SerializedUTXO | null = decryptUTXONote(
encryptedNote,
encKeypair.secretKey, // NaCl secret key (Uint8Array[32])
);
// Blind mailbox: encrypt for a Solana wallet keypair
const recipientKeypair = SolanaKeypair.generate();
const mailboxNote: BlindMailboxNote = encryptBlindMailboxNote(
serializedUtxo,
recipientKeypair.publicKey.toBytes(), // ed25519 public key → X25519 DH
);
// Recipient decrypts using their Solana secret key (64-byte)
const mailboxDecrypted: DecryptedNote | null = decryptBlindMailboxNote(
mailboxNote,
recipientKeypair.secretKey, // 64-byte Solana secret key
);
// Compute UTXO signature (used in circuit)
const sig = computeSignature(utxoKeypair.privateKey, utxo.commitment);
// Format an InputUTXO for snarkjs circuit
const circuitFormat = inputUTXOToCircuitFormat(inputUtxo);4.6 Merkle Tree Operations
import { MerkleTree } from "veilo-sdk-core";
// Create a new Merkle tree (default depth: 22)
const tree = new MerkleTree();
// Insert commitments
const index = tree.insert(ownedUtxo.commitment);
// Current root
const root = tree.root();
// Merkle path for proof generation
const path = tree.path(index);
// path = { pathElements: Uint8Array[], pathIndices: number[] }
// Total leaves inserted
const count = tree.totalLeaves;4.7 Transaction Operations
All transaction functions return Promise<string> (the transaction signature).
Deposits
import { deposit, type DepositResult } from "veilo-sdk-core";
const result: DepositResult = await deposit({
program,
depositor: depositorKeypair,
mintAddress: NATIVE_SOL_MINT,
amount: 1_000_000_000n, // 1 SOL
recipientPubkey: pubkeyToField(depositorKeypair.publicKey), // bigint
tree,
proofBuilder,
treeId: 0,
});
// result = { outputUTXOs, leafIndices, root }Withdrawals
import { withdraw, type WithdrawResult } from "veilo-sdk-core";
const result: WithdrawResult = await withdraw({
program,
relayer: relayerKeypair,
mintAddress: NATIVE_SOL_MINT,
amount: 900_000_000n, // 0.9 SOL
fee: 100_000_000n, // 0.1 SOL to relayer
inputs: [inputUtxo1, zeroInputUtxo],
outputs: [changeUtxo, zeroOutputUtxo],
recipient: recipientKeypair.publicKey,
tree,
proofBuilder,
treeId: 0,
});Private Transfers
import { privateTransfer, type TransferResult } from "veilo-sdk-core";
const result: TransferResult = await privateTransfer({
program,
relayer: relayerKeypair,
mintAddress: NATIVE_SOL_MINT,
inputs: [inputUtxo1, zeroInputUtxo],
outputs: [recipientUtxo, changeUtxo],
recipient: relayerKeypair.publicKey,
tree,
proofBuilder,
treeId: 0,
});Low-Level transact
import { transact } from "veilo-sdk-core";
const signature: string = await transact({
program,
relayer: relayerKeypair,
mintAddress: NATIVE_SOL_MINT,
inputTreeId: 0,
outputTreeId: 0,
root: tree.root(),
publicAmount: 0n,
inputNullifiers: [nullifier1, nullifier2],
outputCommitments: [output1.commitment, output2.commitment],
extData: { recipient, relayer: relayerPubkey, fee: 0n, refund: 0n },
proof,
});4.8 Private Swaps
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.
import {
transactSwap,
getSwapExecutorPda,
fundNativeSource,
type SwapParams,
type SwapProofStruct,
} from "veilo-sdk-core";
// Execute a private swap (returns transaction signature)
const signature: string = await transactSwap({
program,
relayer: relayerKeypair,
sourceMint, // NATIVE_SOL_MINT or SPL token mint
destMint,
sourceRoot: tree.root(),
sourceTreeId: 0,
destTreeId: 0,
inputNullifiers: [nullifier0, nullifier1],
outputCommitments: [changeCommitment, destCommitment],
proof, // SwapProofStruct
swapParams: {
minAmountOut: 950_000_000n,
deadline: BigInt(Math.floor(Date.now() / 1000) + 60),
sourceMint,
destMint,
destAmount: 950_000_000n,
swapDataHash: new Uint8Array(32), // SHA-256 of DEX ix bytes, or zeros
},
swapAmount: 1_000_000_000n,
swapData: jupiterInstructionBytes, // Buffer
extData: { recipient, relayer: relayerPubkey, fee, refund: 0n },
sourceVaultTokenAccount,
sourceMintAccount,
destVaultTokenAccount,
destMintAccount,
relayerTokenAccount,
swapProgram: JUPITER_PROGRAM_ID,
jupiterEventAuthority,
});
// Build the fund_native_source instruction standalone (advanced)
const fundIx = await fundNativeSource({
program,
relayer: relayerKeypair,
sourceMint: NATIVE_SOL_MINT,
destMint,
inputNullifier0: nullifier0,
swapAmount: 1_000_000_000n,
});
// Returns TransactionInstruction — must be first ix in same tx as transact_swapNote:
transactSwaphandles the atomicity requirement automatically. Only usefundNativeSourcedirectly if you are building transactions manually.
4.9 Event Scanning & Tree Reconstruction
import {
scanCommitmentEvents,
scanNullifierEvents,
buildTreeFromEvents,
type CommitmentEvent,
type NullifierSpentEvent,
} from "veilo-sdk-core";
// Scan all commitment events for a mint (paginated by signature)
const { events, latestSignature } = await scanCommitmentEvents({
program,
mintAddress: NATIVE_SOL_MINT,
treeId: 0,
beforeSignature: undefined, // or last known signature for pagination
limit: 1000,
});
// events: CommitmentEvent[]
// CommitmentEvent = { commitment, leafIndex, newRoot, timestamp, mintAddress, treeId }
// Scan spent nullifier events
const { events: nullEvents } = await scanNullifierEvents({
program,
mintAddress: NATIVE_SOL_MINT,
beforeSignature: undefined,
limit: 1000,
});
// NullifierSpentEvent = { nullifier, mintAddress, treeId }
// Reconstruct a Merkle tree from on-chain events
const {
tree,
events: allEvents,
latestSignature: sig,
} = await buildTreeFromEvents({
program,
mintAddress: NATIVE_SOL_MINT,
treeId: 0,
});4.10 Proof Generation
import {
prepareTransactionInputs,
formatInputsForSnarkjs,
computeExtDataHash,
encodeSnarkjsProofToTransactionProof,
packProofToBytes,
computeSwapParamsHash,
computeSwapDataHash,
type ExtData,
type TransactionCircuitInputs,
} from "veilo-sdk-core";
// Prepare inputs for the transaction circuit
const circuitInputs: TransactionCircuitInputs = prepareTransactionInputs({
root: tree.root(),
publicAmount: 1_000_000_000n,
extData: { recipient, relayer: relayerPubkey, fee: 0n, refund: 0n },
mintAddress: NATIVE_SOL_MINT,
inputs: [input1, input2],
outputs: [output1, output2],
inputTreeId: 0,
outputTreeId: 0,
});
// Format for snarkjs (converts bigints / Uint8Arrays to strings)
const snarkjsInputs = formatInputsForSnarkjs(circuitInputs);
// After proof generation:
// const { proof } = await snarkjs.groth16.fullProve(snarkjsInputs, wasmPath, zkeyPath);
// const transactionProof = encodeSnarkjsProofToTransactionProof(proof);
// Compute ext data hash (matches on-chain computation)
const extDataHash = computeExtDataHash({
recipient,
relayer,
fee: 0n,
refund: 0n,
});
// Compute swap param/data hashes (for swap circuit inputs)
const swapParamsHash = computeSwapParamsHash(swapParams);
const swapDataHash = computeSwapDataHash(jupiterInstructionBytes);4.11 Fee Utilities
import {
computeWithdrawalFee,
computeSwapFee,
DEFAULT_FEE_BPS,
} from "veilo-sdk-core/config";
// Compute protocol fee for a withdrawal
const fee = computeWithdrawalFee(amount, feeBps, minWithdrawalFee);
// Compute protocol fee for a swap
const swapFee = computeSwapFee(swapAmount, feeBps);4.12 Relayer & Admin Management
import { addRelayer, setPaused } from "veilo-sdk-core";
// Authorise a new relayer for a pool
await addRelayer({
program,
admin: adminKeypair,
mintAddress: NATIVE_SOL_MINT,
newRelayer: relayerPubkey,
});
// Pause or unpause a pool
await setPaused({
program,
admin: adminKeypair,
mintAddress: NATIVE_SOL_MINT,
paused: true,
});4.13 Poseidon Utilities
import {
initPoseidon,
poseidon1,
poseidon2,
poseidon3,
poseidon4,
pubkeyToField,
bytesToBigIntBE,
bigIntToBytesBE,
BN254_FR_MODULUS,
} from "veilo-sdk-core";
// Must be called once before using hash functions
await initPoseidon();
const h1 = poseidon1(12345n);
const h2 = poseidon2(12345n, 67890n);
const h3 = poseidon3(12345n, 67890n, 11111n);
const h4 = poseidon4(12345n, 67890n, 11111n, 22222n);
// Convert a Solana PublicKey to a BN254 field element
const field = pubkeyToField(NATIVE_SOL_MINT);
// Byte ↔ bigint helpers (big-endian)
const n = bytesToBigIntBE(bytes32);
const b = bigIntToBytesBE(someField, 32);4.14 Error Utilities
import { parseOnChainError } from "veilo-sdk-core";
try {
await transactSwap({ ... });
} catch (err) {
// Returns a human-readable error string from Anchor/program errors
const msg = parseOnChainError(err);
console.error("Swap failed:", msg);
}5. Architecture
Transaction Model
Veilo uses a UTXO privacy model inspired by Zcash and Tornado Cash Nova:
- Inputs: 2 UTXOs (zero-value for deposits)
- Outputs: 2 UTXOs (zero-value for withdrawals)
- Public Amount: net pool change — positive = deposit, negative = withdrawal, zero = private transfer or swap
Each transaction consumes 2 input UTXOs (Merkle proofs), creates 2 output UTXOs (commitments inserted to tree), and generates 2 nullifiers to prevent double-spend.
Swap Architecture
Private swaps require two instructions in a single atomic transaction:
fund_native_source— pre-funds the swap executor with SOL from the vault (native SOL pools only)transact_swap— verifies the ZK proof, spends input UTXOs, creates output UTXOs, and executes the DEX swap
transactSwap() handles this automatically. The on-chain program validates atomicity via the instructions sysvar.
Privacy Guarantees
- Commitment hiding: amount, owner, blinding are hidden via Poseidon
- Nullifier uniqueness: each UTXO can only be spent once
- Unlinkability: no public link between inputs and outputs
- ZK proofs: Groth16 verified on-chain
Constants
import {
NATIVE_SOL_MINT, // PublicKey.default — native SOL pools
MERKLE_TREE_DEPTH, // 22
ROOT_HISTORY_SIZE, // 256
DEFAULT_FEE_BPS, // 50 (0.5%)
sol, // sol(1) === 1_000_000_000n
} from "veilo-sdk-core/config";Type Exports
import type {
// UTXO
Keypair,
UTXO,
SerializedUTXO,
InputUTXO,
// Encryption
EncryptedNote,
BlindMailboxNote,
BlindMailboxNoteData,
DecryptedNote,
// Proof
ExtData,
TransactionCircuitInputs,
TransactionProofStruct,
RawProof,
TransactionProofBuilder,
// Swap
SwapProofStruct,
SwapParams,
// Tree / events
MerklePath,
CircuitMerklePath,
TreeInfo,
CommitmentEvent,
NullifierSpentEvent,
// Results
DepositResult,
WithdrawResult,
TransferResult,
// Config
PoolInitConfig,
PrivacyConfigAccount,
GlobalConfigAccount,
} from "veilo-sdk-core";6. Development
# Run tests (requires devnet or local validator)
npm test7. Resources
- Repository: https://github.com/VeiloSolana/veilo-sdk
- Circomlibjs: https://github.com/iden3/circomlibjs
- Poseidon Hash: https://www.poseidon-hash.info/
License
ISC
This package provides a complete UTXO-based privacy protocol implementation on Solana with:
- UTXO Model: Full support for unspent transaction outputs with Poseidon commitments
- Multi-Tree Support: Multiple concurrent Merkle trees for improved scalability
- ZK Proofs: Integration with Circom circuits for private transactions
- Flexible Operations: Deposits, withdrawals, and private transfers
- Poseidon Hashing: BN254-curve compatible hashing using circomlibjs
- Merkle Trees: Off-chain Merkle tree management with proof generation
- Relayer Support: Built-in relayer infrastructure for private withdrawals
Status: Active development. The SDK supports full transaction privacy with ZK-SNARK proofs.
Proofs are generated off-chain and verified on-chain using Groth16.
1. Installation
npm install veilo-sdk-coreOr from source:
cd core-sdk
npm install2. Prerequisites
You need:
A running Solana validator (localnet/devnet/mainnet):
solana-test-validatorThe
privacy-poolprogram deployed to the networkThe
privacy-poolAnchor IDL available:idl/idl/privacy_pool.jsonA funded keypair:
solana config set --url http://127.0.0.1:8899 solana-keygen new --outfile ~/.config/solana/id.json solana airdrop 10
Environment variables:
export ANCHOR_PROVIDER_URL=http://127.0.0.1:8899
export ANCHOR_WALLET=$HOME/.config/solana/id.json3. Build
npm run build4. SDK API
4.1 PDA Helpers
import {
getPoolPdas,
getNoteTreePda,
getGlobalConfigPda,
getNullifierMarkerPda,
} from "veilo-sdk-core";
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey(
"YourProgram1111111111111111111111111111111111",
);
const mintAddress = new PublicKey(
"So11111111111111111111111111111111111111112",
); // Native SOL
// Get pool PDAs
const { config, vault, nullifiers } = getPoolPdas(programId, mintAddress);
// Get note tree PDA for tree ID 0
const noteTree = getNoteTreePda(programId, mintAddress, 0);
// Get global config
const globalConfig = getGlobalConfigPda(programId);
// Get nullifier marker PDA
const nullifier = new Uint8Array(32);
const marker = getNullifierMarkerPda(programId, mintAddress, 0, nullifier);PDA seeds (v3):
- Config:
["privacy_config_v3", mint_address] - Vault:
["privacy_vault_v3", mint_address] - Note Tree:
["privacy_note_tree_v3", mint_address, tree_id] - Nullifiers:
["privacy_nullifiers_v3", mint_address] - Nullifier Marker:
["privacy_nullifier_v3", mint_address, tree_id, nullifier] - Global Config:
["global_config_v1"]
4.2 Pool Initialization
import * as anchor from "@coral-xyz/anchor";
import {
initializeGlobalConfig,
initializePool,
updatePoolConfig,
addMerkleTree,
getPoolConfig,
} from "veilo-sdk-core";
import { NATIVE_SOL_MINT, sol } from "veilo-sdk-core/config";
// Initialize global config (once per program)
await initializeGlobalConfig({
program,
admin: adminKeypair,
});
// Initialize a pool for native SOL
await initializePool({
program,
payer: adminKeypair,
admin: adminKeypair,
mintAddress: NATIVE_SOL_MINT,
minDepositAmount: sol(0.1), // 0.1 SOL
maxDepositAmount: sol(100), // 100 SOL
minWithdrawAmount: sol(0.1),
maxWithdrawAmount: sol(100),
feeBps: 50, // 0.5%
feeErrorMarginBps: 10, // 0.1% margin
minWithdrawalFee: 1_000_000n, // 0.001 SOL minimum
});
// Add first Merkle tree (tree ID 0)
await addMerkleTree({
program,
payer: adminKeypair,
mintAddress: NATIVE_SOL_MINT,
treeId: 0,
});
// Update pool configuration
await updatePoolConfig({
program,
admin: adminKeypair,
mintAddress: NATIVE_SOL_MINT,
feeBps: 30, // Change to 0.3%
});
// Read pool configuration
const poolConfig = await getPoolConfig(program, NATIVE_SOL_MINT);
console.log("TVL:", poolConfig.totalTvl);
console.log("Fee:", poolConfig.feeBps, "bps");
console.log("Num Trees:", poolConfig.numTrees);4.3 UTXO Management
The SDK uses a UTXO (Unspent Transaction Output) model with Poseidon commitments:
import {
generateKeypair,
keypairFromPrivateKey,
createUTXO,
createOwnedUTXO,
createOwnedZeroUTXO,
deriveNullifier,
type Keypair,
type UTXO,
type SerializedUTXO,
type InputUTXO,
} from "veilo-sdk-core";
import { NATIVE_SOL_MINT } from "veilo-sdk-core/config";
import { pubkeyToField } from "veilo-sdk-core";
// Generate a random keypair
const keypair: Keypair = generateKeypair();
// keypair = { privateKey: bigint, publicKey: bigint }
// Restore keypair from private key
const restored = keypairFromPrivateKey(privateKeyBigInt);
// Create an owned UTXO (includes private key)
const ownedUtxo: SerializedUTXO = createOwnedUTXO({
amount: 1_000_000_000n,
privateKey: keypair.privateKey,
mintAddress: NATIVE_SOL_MINT,
});
// Create a zero UTXO (for unused inputs/outputs)
const zeroUtxo = createOwnedZeroUTXO(pubkeyToField(NATIVE_SOL_MINT), keypair);
// Derive nullifier for spending
const nullifier = deriveNullifier(
ownedUtxo.privateKey,
ownedUtxo.commitment,
0, // pathIndex in Merkle tree
0, // treeId
);UTXO commitment formula:
commitment = Poseidon(amount, pubkey, blinding, mintAddress)Nullifier formula:
nullifier = Poseidon(privateKey, commitment, pathIndex, treeId)4.4 Merkle Tree Operations
import { MerkleTree } from "veilo-sdk-core";
// Create a new Merkle tree (default depth: 20)
const tree = new MerkleTree();
// Insert commitments
const index1 = tree.insert(ownedUtxo1.commitment);
const index2 = tree.insert(ownedUtxo2.commitment);
// Get current root
const root = tree.root();
// Get Merkle path for proof generation
const path = tree.path(index1);
// path = { pathElements: Uint8Array[], pathIndices: number[] }
// Get number of leaves
const numLeaves = tree.totalLeaves;
// Custom tree depth
const deepTree = new MerkleTree(25); // 25 levelsThe Merkle tree uses Poseidon hash for all internal nodes.
4.5 Transaction Operations
The SDK supports three types of transactions:
Deposits (publicAmount > 0)
import { deposit } from "veilo-sdk-core";
// Create output UTXO
const outputUtxo = createOwnedUTXO({
amount: 1_000_000_000n, // 1 SOL
mintAddress: pubkeyToField(NATIVE_SOL_MINT),
keypair: utxoKeypair,
});
// Zero inputs for deposit
const input1 = createOwnedZeroUTXO(pubkeyToField(NATIVE_SOL_MINT), utxoKeypair);
const input2 = createOwnedZeroUTXO(pubkeyToField(NATIVE_SOL_MINT), utxoKeypair);
const output2 = createOwnedZeroUTXO(
pubkeyToField(NATIVE_SOL_MINT),
utxoKeypair,
);
await deposit({
program,
depositor: depositorKeypair,
mintAddress: NATIVE_SOL_MINT,
inputTreeId: 0,
outputTreeId: 0,
root: tree.root(),
publicAmount: 1_000_000_000n, // Positive = deposit
inputs: [input1, input2],
outputs: [outputUtxo, output2],
recipient: depositorKeypair.publicKey,
fee: 0n,
refund: 0n,
proof: mockProof,
});
// Insert outputs into tree
tree.insert(outputUtxo.commitment);
tree.insert(output2.commitment);Withdrawals (publicAmount < 0)
import { withdraw } from "veilo-sdk-core";
// Prepare input with Merkle path
const input1: InputUTXO = {
...ownedUtxo1,
pathIndex: 0,
pathElements: tree.path(0).pathElements,
};
const zeroInput2: InputUTXO = {
...createOwnedZeroUTXO(pubkeyToField(NATIVE_SOL_MINT), utxoKeypair),
pathIndex: 0,
pathElements: tree.path(0).pathElements,
};
// Zero outputs
const output1 = createOwnedZeroUTXO(
pubkeyToField(NATIVE_SOL_MINT),
utxoKeypair,
);
const output2 = createOwnedZeroUTXO(
pubkeyToField(NATIVE_SOL_MINT),
utxoKeypair,
);
await withdraw({
program,
relayer: relayerKeypair,
mintAddress: NATIVE_SOL_MINT,
inputTreeId: 0,
outputTreeId: 0,
root: tree.root(),
publicAmount: -900_000_000n, // Negative = withdrawal (0.9 SOL)
inputs: [input1, zeroInput2],
outputs: [output1, output2],
recipient: recipientKeypair.publicKey,
fee: 100_000_000n, // 0.1 SOL to relayer
refund: 0n,
proof,
});Private Transfers (publicAmount = 0)
import {
privateTransfer,
generateKeypair,
createOwnedUTXO,
} from "veilo-sdk-core";
// Create new outputs for recipient
const recipientKeypair = generateKeypair();
const output1 = createOwnedUTXO({
amount: 1_000_000_000n,
privateKey: recipientKeypair.privateKey,
mintAddress: NATIVE_SOL_MINT,
});
await privateTransfer({
program,
relayer: relayerKeypair,
mintAddress: NATIVE_SOL_MINT,
inputTreeId: 0,
outputTreeId: 0,
root: tree.root(),
publicAmount: 0n, // Zero = private transfer
inputs: [input1, zeroInput2],
outputs: [output1, output2],
recipient: relayerKeypair.publicKey,
fee: 0n,
refund: 0n,
proof,
});
// Insert new outputs
tree.insert(output1.commitment);
tree.insert(output2.commitment);Low-Level Transaction Function
For advanced use cases, use the unified transact function directly:
import { transact } from "veilo-sdk-core";
await transact({
program,
relayer: relayerKeypair,
mintAddress: NATIVE_SOL_MINT,
inputTreeId: 0,
outputTreeId: 0,
root: tree.root(),
publicAmount: 0n,
inputNullifiers: [nullifier1, nullifier2],
outputCommitments: [output1.commitment, output2.commitment],
extData: {
recipient: recipientPubkey,
relayer: relayerPubkey,
fee: 0n,
refund: 0n,
},
proof,
});4.6 Proof Generation
The SDK provides utilities for preparing circuit inputs:
import {
prepareTransactionInputs,
formatInputsForSnarkjs,
computeExtDataHash,
encodeSnarkjsProofToTransactionProof,
packProofToBytes,
type ExtData,
type TransactionCircuitInputs,
} from "veilo-sdk-core";
// Prepare inputs for the circuit
const circuitInputs: TransactionCircuitInputs = prepareTransactionInputs({
root: tree.root(),
publicAmount: 1_000_000_000n,
extData: {
recipient: recipientPubkey,
relayer: relayerPubkey,
fee: 0n,
refund: 0n,
},
mintAddress: NATIVE_SOL_MINT,
inputs: [input1, input2],
outputs: [output1, output2],
inputTreeId: 0,
outputTreeId: 0,
});
// Format for snarkjs (converts Uint8Array to string representations)
const snarkjsInputs = formatInputsForSnarkjs(circuitInputs);
// Use with snarkjs to generate proof
// const { proof, publicSignals } = await snarkjs.groth16.fullProve(
// snarkjsInputs,
// wasmPath,
// zkeyPath
// );
// Convert snarkjs proof to on-chain format
// const transactionProof = encodeSnarkjsProofToTransactionProof(proof);4.7 Relayer Management
import { addRelayer, setPaused } from "veilo-sdk-core";
// Add a relayer
await addRelayer({
program,
admin: adminKeypair,
mintAddress: NATIVE_SOL_MINT,
newRelayer: relayerPubkey,
});
// Pause/unpause the pool
await setPaused({
program,
admin: adminKeypair,
mintAddress: NATIVE_SOL_MINT,
paused: true,
});4.8 Poseidon Utilities
import {
initPoseidon,
poseidon1,
poseidon2,
poseidon3,
poseidon4,
pubkeyToField,
} from "veilo-sdk-core";
// Initialize Poseidon (required once before using hash functions)
await initPoseidon();
// Hash functions
const hash1 = poseidon1(12345n);
const hash2 = poseidon2(12345n, 67890n);
const hash3 = poseidon3(12345n, 67890n, 11111n);
const hash4 = poseidon4(12345n, 67890n, 11111n, 22222n);
// Convert Solana PublicKey to field element
const fieldElement = pubkeyToField(NATIVE_SOL_MINT);5. Architecture
Transaction Model
Veilo uses a UTXO-based privacy model inspired by Zcash and Tornado Cash Nova:
- Inputs: 2 UTXOs (can be zero for deposits)
- Outputs: 2 UTXOs (can be zero for withdrawals)
- Public Amount: Net change (positive = deposit, negative = withdrawal, zero = private transfer)
Each transaction:
- Consumes 2 input UTXOs (proven via Merkle paths)
- Creates 2 output UTXOs (commitments added to tree)
- Generates 2 nullifiers (prevents double-spending)
- Optionally transfers funds in/out of the pool
Privacy Guarantees
- Commitment hiding: Amount, owner, and blinding factor are hidden via Poseidon hash
- Nullifier uniqueness: Each UTXO can only be spent once
- Unlinkability: No public link between inputs and outputs
- Zero-knowledge proofs: Transactions proven valid without revealing private data
Multi-Tree Support
The protocol supports multiple concurrent Merkle trees per pool:
- Improves scalability by reducing tree depth
- Allows parallel insertions
- Each tree has independent state
Constants
import {
NATIVE_SOL_MINT, // PublicKey.default (all zeros) for native SOL
MERKLE_TREE_DEPTH, // 22 levels
ROOT_HISTORY_SIZE, // 256 historical roots
DEFAULT_FEE_BPS, // 50 (0.5%)
sol, // Helper: sol(1) = 1_000_000_000n lamports
} from "veilo-sdk-core/config";
import { BN254_FR_MODULUS } from "veilo-sdk-core";Type Exports
The SDK exports the following types for TypeScript users:
import type {
// UTXO types
Keypair,
UTXO,
SerializedUTXO,
InputUTXO,
// Proof types
ExtData,
TransactionCircuitInputs,
TransactionProofStruct,
RawProof,
TransactionProofBuilder,
// Merkle types
MerklePath,
CircuitMerklePath,
// Config types
PoolInitConfig,
PrivacyConfigAccount,
GlobalConfigAccount,
} from "veilo-sdk-core";SPL Token Support
The SDK supports both native SOL and SPL tokens:
import { PublicKey } from "@solana/web3.js";
// For native SOL
const solMint = NATIVE_SOL_MINT; // PublicKey.default
// For SPL tokens
const usdcMint = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
// Initialize pool for SPL token
await initializePool({
program,
payer: adminKeypair,
admin: adminKeypair,
mintAddress: usdcMint, // Use SPL token mint
minDepositAmount: 1_000_000n, // 1 USDC (6 decimals)
// ... other params
});When using SPL tokens, the SDK automatically handles associated token accounts.
6. Development
Environment Variables
export ANCHOR_PROVIDER_URL=http://127.0.0.1:8899
export ANCHOR_WALLET=$HOME/.config/solana/id.json7. Limitations
- Off-chain tree management: Merkle trees must be maintained by relayers/clients
- Proof generation not included: You must integrate your own circuit/prover
- Development status: Active development, APIs may change
8. Resources
- Repository: https://github.com/VeiloSolana/veilo-sdk
- Circomlibjs: https://github.com/iden3/circomlibjs
- Poseidon Hash: https://www.poseidon-hash.info/
License
ISC
