VeilClient
Main entry point for the Veil SDK
The VeilClient is the primary interface for interacting with the Veil privacy protocol on Solana. It provides both async (blockchain submission) and sync (offline computation) methods for all operations.
VeilClient(
rpc_url: str = "https://api.devnet.solana.com",
program_id: Optional[str] = None
)Parameters:
rpc_url- Solana RPC endpoint URL (default: devnet)program_id- Privacy program ID (optional, uses default if None)
Example:
from veil import VeilClient
# Devnet (default)
client = VeilClient()
# Mainnet
client = VeilClient(rpc_url="https://api.mainnet-beta.solana.com")
# Custom program
client = VeilClient(
rpc_url="https://api.devnet.solana.com",
program_id="VeiL..."
)initialize_pool_async()
Initialize the privacy pool on-chain (one-time setup per deployment).
async def initialize_pool_async(
authority: Keypair
) -> strParameters:
authority- Keypair with authority to initialize the pool
Returns: Transaction signature (string)
Raises:
RuntimeError- If pool already initialized or transaction fails
Example:
from solders.keypair import Keypair
authority = Keypair() # Load your authority keypair
signature = await client.initialize_pool_async(authority)
print(f"Pool initialized: {signature}")Note: This only needs to be called once per program deployment. Most users don't need to call this.
shield_async()
Shield assets to make them private by submitting to the blockchain.
async def shield_async(
amount: int,
token: str,
keypair: Keypair,
secret: Optional[str] = None
) -> PrivateTransactionParameters:
amount- Amount to shield in smallest unit (lamports for SOL)token- Token address or"SOL"for native SOLkeypair- Payer keypair for transaction signing and fee paymentsecret- Optional secret key (auto-generated if None, min 32 chars if provided)
Returns: PrivateTransaction with:
signature- Transaction signaturecommitment- Hex-encoded commitment (32 bytes)secret- Secret used (save this!)leaf_index- Merkle tree leaf index (save this!)status-TransactionStatus.CONFIRMED
Raises:
ValueError- If amount <= 0 or secret too shortRuntimeError- If transaction fails or pool not initialized
Example:
from solders.keypair import Keypair
from veil import generate_secret
payer = Keypair() # Load your keypair
secret = generate_secret() # Or provide your own (min 32 chars)
tx = await client.shield_async(
amount=1_000_000_000, # 1 SOL in lamports
token="SOL",
keypair=payer,
secret=secret
)
print(f"✅ Shielded 1 SOL")
print(f"Transaction: {tx.signature}")
print(f"Commitment: {tx.commitment}")
print(f"Leaf Index: {tx.leaf_index}")
print(f"Secret: {tx.secret}") # SAVE THIS!
# Save note data for spending later
note_data = {
"commitment": tx.commitment,
"amount": 1_000_000_000,
"secret": tx.secret,
"leaf_index": tx.leaf_index,
"asset_id": 0 # SOL
}On-chain cost: ~50,000 CU (~0.00005 SOL)
Privacy guarantees:
- ✅ Amount is hidden (Pedersen commitment)
- ✅ Only commitment (32 bytes) is visible on-chain
- ✅ Secret and amount remain local
shield()
Generate commitment data without blockchain submission (offline mode).
def shield(
amount: int,
token: str,
secret: str
) -> PrivateTransactionParameters:
amount- Amount to shieldtoken- Token address or"SOL"secret- Owner's secret key (min 32 characters)
Returns: PrivateTransaction with:
commitment- Hex-encoded commitmentsecret- Secret usedstatus-TransactionStatus.PENDING(not submitted)
Example:
tx = client.shield(
amount=1_000_000_000,
token="SOL",
secret="my_super_secret_key_at_least_32_chars"
)
print(f"Commitment: {tx.commitment}")
print(f"Status: {tx.status}") # PENDING (not on-chain)
# Use for testing or air-gapped signingUse cases:
- Testing commitment generation
- Air-gapped transaction signing
- Proof-of-concept demonstrations
private_transfer_async()
Transfer assets privately without revealing amounts, sender, or recipient.
⏳ Note: Proof generation takes 5-10 seconds on the client side.
async def private_transfer_async(
input_note_secret: str,
input_note_index: int,
recipient_pubkey: str,
amount: int,
keypair: Keypair
) -> PrivateTransactionParameters:
input_note_secret- Secret of the note you're spending (from shield or previous transfer)input_note_index- Leaf index of your note in the Merkle treerecipient_pubkey- Recipient's Solana public key (base58 string)amount- Amount to transfer (must be <= note amount)keypair- Payer keypair for transaction fees
Returns: PrivateTransaction with:
signature- Transaction signaturenullifier- Hex-encoded nullifier (32 bytes, marks input note as spent)commitment- Hex-encoded new commitment (32 bytes, for recipient)recipient_secret- Secret for recipient to spend (share securely!)proof- zkSNARK proof bytes (256 bytes)leaf_index- New commitment's leaf indexstatus-TransactionStatus.CONFIRMED
Raises:
ValueError- If invalid recipient address, amount <= 0, or secret too shortRuntimeError- If transaction fails, nullifier already spent, or invalid Merkle root
Example:
import time
print("⏳ Generating zkSNARK proof (5-10 seconds)...")
start_time = time.time()
tx = await client.private_transfer_async(
input_note_secret=sender_note["secret"],
input_note_index=sender_note["leaf_index"],
recipient_pubkey="RecipientPublicKeyHere...",
amount=500_000_000, # 0.5 SOL
keypair=payer
)
elapsed = time.time() - start_time
print(f"✅ Proof generated in {elapsed:.1f} seconds")
print(f"Transaction: {tx.signature}")
print(f"Nullifier: {tx.nullifier}")
print(f"New Commitment: {tx.commitment}")
print(f"Recipient Secret: {tx.recipient_secret}") # Share with recipient!
# Save new note for recipient (encrypted channel)
recipient_note = {
"commitment": tx.commitment,
"amount": 500_000_000,
"secret": tx.recipient_secret,
"leaf_index": tx.leaf_index,
"asset_id": 0
}On-chain cost: ~200,000 CU (~0.0002 SOL) + optional relayer fee (0.3%)
Privacy guarantees:
- ✅ Sender identity hidden (unlinkable nullifier)
- ✅ Recipient identity hidden (unlinkable commitment)
- ✅ Amount hidden (encrypted note)
- ✅ Transaction graph completely broken
- ❌ Transaction timestamp visible
- ❌ IP address exposed (use relayers!)
Important: After this transaction, the input note is spent. You must use the new recipient_secret for subsequent operations.
private_transfer()
Generate transfer transaction data without blockchain submission (offline mode).
def private_transfer(
input_note_secret: str,
input_note_index: int,
recipient_pubkey: str,
amount: int
) -> PrivateTransactionParameters:
input_note_secret- Secret of the note you're spendinginput_note_index- Leaf index of your noterecipient_pubkey- Recipient's public keyamount- Amount to transfer
Returns: PrivateTransaction with nullifier, commitment, proof, and recipient_secret (status=PENDING)
Example:
tx = client.private_transfer(
input_note_secret="my_secret",
input_note_index=42,
recipient_pubkey="RecipientPublicKey...",
amount=500_000_000
)
print(f"Nullifier: {tx.nullifier}")
print(f"New Commitment: {tx.commitment}")
print(f"Proof size: {len(tx.proof)} bytes")
print(f"Status: {tx.status}") # PENDING (not submitted)Use cases:
- Testing proof generation
- Preparing transactions for batch submission
- Air-gapped signing workflows
unshield_async()
Withdraw assets from the privacy pool to a public Solana address.
⏳ Note: Proof generation takes 5-10 seconds on the client side.
async def unshield_async(
note_secret: str,
note_index: int,
amount: int,
recipient_pubkey: str,
keypair: Keypair
) -> PrivateTransactionParameters:
note_secret- Secret of the note you're spendingnote_index- Leaf index of your note in the Merkle treeamount- Amount to withdraw (must be <= note amount)recipient_pubkey- Destination public Solana address (base58)keypair- Payer keypair for transaction fees
Returns: PrivateTransaction with:
signature- Transaction signaturenullifier- Hex-encoded nullifier (marks note as spent)proof- zkSNARK proof bytesstatus-TransactionStatus.CONFIRMED
Raises:
ValueError- If invalid destination address or amountRuntimeError- If transaction fails, nullifier already spent, or insufficient pool balance
Example:
print("⏳ Generating proof and withdrawing...")
tx = await client.unshield_async(
note_secret=sender_note["secret"],
note_index=sender_note["leaf_index"],
amount=500_000_000, # 0.5 SOL
recipient_pubkey=str(payer.pubkey()),
keypair=payer
)
print(f"✅ Unshielded 0.5 SOL")
print(f"Transaction: {tx.signature}")
print(f"Withdrawn to: {recipient_pubkey}")
print(f"Nullifier: {tx.nullifier}")On-chain cost: ~150,000 CU (~0.00015 SOL)
Privacy trade-offs:
- ✅ Hidden: Sender identity (who owned the note)
- ✅ Hidden: Original deposit (can't link shield → unshield)
- ❌ Visible: Withdrawal amount (public on-chain)
- ❌ Visible: Recipient address (public on-chain)
When to use:
- Converting private funds back to public
- Paying merchants/services requiring public addresses
- Cashing out to exchanges
When NOT to use:
- Keeping funds private → use
private_transfer_async()instead - Minimizing graph analysis → wait for larger anonymity set
unshield()
Generate unshield transaction data without blockchain submission (offline mode).
def unshield(
note_secret: str,
note_index: int,
amount: int,
recipient_pubkey: str
) -> PrivateTransactionParameters:
note_secret- Secret of the note you're spendingnote_index- Leaf index of your noteamount- Amount to withdrawrecipient_pubkey- Destination public address
Returns: PrivateTransaction with nullifier and proof (status=PENDING)
Example:
tx = client.unshield(
note_secret="my_secret",
note_index=42,
amount=500_000_000,
recipient_pubkey="MyPublicWallet..."
)
print(f"Nullifier: {tx.nullifier}")
print(f"Status: {tx.status}") # PENDINGverify_proof()
Verify a Groth16 zkSNARK proof locally.
def verify_proof(
proof: bytes,
public_inputs: dict[str, Any]
) -> boolParameters:
proof- Proof bytes (256 bytes for Groth16)public_inputs- Dictionary with public inputs:merkle_root- Merkle root (32 bytes)nullifier- Nullifier (32 bytes)new_commitment- New commitment (32 bytes)
Returns: True if proof is valid, False otherwise
Example:
from veil import hex_to_bytes
valid = client.verify_proof(
proof=tx.proof,
public_inputs={
"merkle_root": await client.get_merkle_root(),
"nullifier": hex_to_bytes(tx.nullifier),
"new_commitment": hex_to_bytes(tx.commitment)
}
)
if valid:
print("✅ Proof is valid")
else:
print("❌ Proof is invalid")Use cases:
- Validating proofs before submission
- Testing proof generation
- Auditing transactions
is_nullifier_spent()
Check if a nullifier has been spent on-chain (prevents double-spending).
async def is_nullifier_spent(
nullifier: bytes
) -> boolParameters:
nullifier- 32-byte nullifier to check
Returns: True if nullifier has been spent (PDA exists), False otherwise
Example:
from veil import hex_to_bytes
nullifier_bytes = hex_to_bytes(tx.nullifier)
is_spent = await client.is_nullifier_spent(nullifier_bytes)
if is_spent:
print("❌ Nullifier already spent (double-spend attempt)")
else:
print("✅ Nullifier not yet spent (safe to submit)")Use cases:
- Preventing double-spend attempts
- Validating note status before spending
- Monitoring for spent notes
Technical details:
- Checks for PDA existence with seeds:
[b"nullifier", pool_id, nullifier_bytes] - Nullifier PDAs are created during transfer/unshield operations
- Once spent, nullifiers cannot be reused (permanent)
get_merkle_root()
Get the current Merkle root from on-chain privacy pool state.
async def get_merkle_root() -> bytesReturns: 32-byte Merkle root
Example:
root = await client.get_merkle_root()
print(f"Current Merkle root: {root.hex()}")
# Use in proof verification
valid = client.verify_proof(
proof=tx.proof,
public_inputs={
"merkle_root": root,
"nullifier": nullifier_bytes,
"new_commitment": commitment_bytes
}
)Use cases:
- Proof generation (must prove membership in current tree)
- Verifying proof validity
- Monitoring pool state
Technical details:
- Root is updated after each shield/transfer operation
- Pool maintains 30-root history for front-running protection
- Proofs valid for ~1 minute (30 tree updates)
get_commitment_index()
Get the leaf index of a commitment in the Merkle tree.
async def get_commitment_index(
commitment: bytes
) -> Optional[int]Parameters:
commitment- 32-byte commitment to search for
Returns: Leaf index (int) if found, None otherwise
Example:
from veil import hex_to_bytes
commitment_bytes = hex_to_bytes(tx.commitment)
index = await client.get_commitment_index(commitment_bytes)
if index is not None:
print(f"Commitment found at index: {index}")
else:
print("Commitment not found in tree")Use cases:
- Finding leaf index for spending notes
- Verifying shield transaction success
- Note database synchronization
close()
Close the RPC connection and clean up resources.
async def close() -> NoneExample:
await client.close()Best practice: Always close connections when done, or use context manager:
# Manual close
client = VeilClient()
try:
await client.shield_async(...)
finally:
await client.close()
# Context manager (recommended)
async with VeilClient() as client:
await client.shield_async(...)
await client.private_transfer_async(...)
# Automatically closedVeilClient supports async context managers for automatic resource cleanup.
async with VeilClient(rpc_url="https://api.devnet.solana.com") as client:
# Shield
tx1 = await client.shield_async(
amount=1_000_000_000,
token="SOL",
keypair=payer,
secret=secret
)
# Private transfer
tx2 = await client.private_transfer_async(
input_note_secret=tx1.secret,
input_note_index=tx1.leaf_index,
recipient_pubkey="Recipient...",
amount=500_000_000,
keypair=payer
)
# Connection automatically closed after blockValueError
Raised for invalid input parameters.
try:
tx = await client.shield_async(
amount=-1000, # Invalid: negative amount
token="SOL",
keypair=payer
)
except ValueError as e:
print(f"Invalid input: {e}")
# Example: "Amount must be greater than 0"Common causes:
- Negative or zero amounts
- Invalid addresses (not base58)
- Secrets too short (< 32 characters)
- Invalid token addresses
RuntimeError
Raised for cryptographic or transaction failures.
try:
tx = await client.private_transfer_async(
input_note_secret=secret,
input_note_index=42,
recipient_pubkey=recipient,
amount=1_000_000_000,
keypair=payer
)
except RuntimeError as e:
print(f"Transaction failed: {e}")
# Examples:
# - "Nullifier already spent"
# - "Invalid Merkle root"
# - "Proof verification failed"Common causes:
- Double-spend attempts (nullifier already spent)
- Stale Merkle proofs (root too old)
- Invalid proof generation
- Insufficient pool balance (unshield)
- Network errors during submission
NetworkError
Raised for RPC connection issues.
try:
tx = await client.shield_async(...)
except NetworkError as e:
print(f"Network error: {e}")
# Examples:
# - "Connection timeout"
# - "RPC endpoint unavailable"Common causes:
- RPC endpoint down or rate-limited
- Network connectivity issues
- Invalid RPC URL
Full workflow with error handling:
import asyncio
from solders.keypair import Keypair
from veil import VeilClient, generate_secret
async def complete_workflow():
"""Shield, transfer, and unshield with proper error handling."""
async with VeilClient(rpc_url="https://api.devnet.solana.com") as client:
payer = Keypair() # Load your keypair
try:
# 1. Shield 2 SOL
print("Step 1: Shielding 2 SOL...")
secret = generate_secret()
shield_tx = await client.shield_async(
amount=2_000_000_000,
token="SOL",
keypair=payer,
secret=secret
)
print(f"✅ Shielded: {shield_tx.signature}")
# 2. Private transfer 1 SOL
print("\nStep 2: Private transfer 1 SOL...")
recipient = "RecipientPublicKeyHere..."
transfer_tx = await client.private_transfer_async(
input_note_secret=shield_tx.secret,
input_note_index=shield_tx.leaf_index,
recipient_pubkey=recipient,
amount=1_000_000_000,
keypair=payer
)
print(f"✅ Transferred: {transfer_tx.signature}")
# 3. Unshield remaining 1 SOL
print("\nStep 3: Unshielding 1 SOL...")
unshield_tx = await client.unshield_async(
note_secret=transfer_tx.recipient_secret, # New note's secret!
note_index=transfer_tx.leaf_index,
amount=1_000_000_000,
recipient_pubkey=str(payer.pubkey()),
keypair=payer
)
print(f"✅ Unshielded: {unshield_tx.signature}")
print("\n🎉 Complete workflow successful!")
except ValueError as e:
print(f"❌ Invalid input: {e}")
except RuntimeError as e:
print(f"❌ Transaction failed: {e}")
except Exception as e:
print(f"❌ Unexpected error: {e}")
# Run
asyncio.run(complete_workflow())| Operation | Client Time | On-Chain Time | Cost |
|---|---|---|---|
| shield_async() | <100ms | ~1s | ~50,000 CU (~0.00005 SOL) |
| private_transfer_async() | 5-10s (proof) | ~3s | ~200,000 CU (~0.0002 SOL) |
| unshield_async() | 5-10s (proof) | ~3s | ~150,000 CU (~0.00015 SOL) |
| verify_proof() | <10ms | N/A | Free (local) |
| is_nullifier_spent() | ~100ms | N/A | Free (read-only) |
| get_merkle_root() | ~100ms | N/A | Free (read-only) |
Hardware requirements (client-side):
- Proof generation: 2-4 GB RAM, modern CPU
- Other operations: Minimal resources
- RelayerClient - IP privacy via relayer network
- Types - Data structures and enums
- Crypto - Low-level cryptographic functions
- Quick Start - Complete examples
- Privacy Model - Privacy guarantees and limitations
On This Page
- VeilClient
- Constructor
- Pool Initialization
- initialize_pool_async()
- Shield Operations
- shield_async()
- shield()
- Private Transfer Operations
- private_transfer_async()
- private_transfer()
- Unshield Operations
- unshield_async()
- unshield()
- Verification and State Queries
- verify_proof()
- is_nullifier_spent()
- get_merkle_root()
- get_commitment_index()
- Connection Management
- close()
- Context Manager Usage
- Error Handling
- ValueError
- RuntimeError
- NetworkError
- Complete Example
- Performance Characteristics
- See Also