Docs
/api
/VeilClient

VeilClient

Main entry point for the Veil SDK

VeilClient

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.

Constructor

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..."
)

Pool Initialization

initialize_pool_async()

Initialize the privacy pool on-chain (one-time setup per deployment).

async def initialize_pool_async(
    authority: Keypair
) -> str

Parameters:

  • 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 Operations

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
) -> PrivateTransaction

Parameters:

  • amount - Amount to shield in smallest unit (lamports for SOL)
  • token - Token address or "SOL" for native SOL
  • keypair - Payer keypair for transaction signing and fee payment
  • secret - Optional secret key (auto-generated if None, min 32 chars if provided)

Returns: PrivateTransaction with:

  • signature - Transaction signature
  • commitment - 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 short
  • RuntimeError - 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
) -> PrivateTransaction

Parameters:

  • amount - Amount to shield
  • token - Token address or "SOL"
  • secret - Owner's secret key (min 32 characters)

Returns: PrivateTransaction with:

  • commitment - Hex-encoded commitment
  • secret - Secret used
  • status - 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 signing

Use cases:

  • Testing commitment generation
  • Air-gapped transaction signing
  • Proof-of-concept demonstrations

Private Transfer Operations

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
) -> PrivateTransaction

Parameters:

  • 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 tree
  • recipient_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 signature
  • nullifier - 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 index
  • status - TransactionStatus.CONFIRMED

Raises:

  • ValueError - If invalid recipient address, amount <= 0, or secret too short
  • RuntimeError - 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
) -> PrivateTransaction

Parameters:

  • input_note_secret - Secret of the note you're spending
  • input_note_index - Leaf index of your note
  • recipient_pubkey - Recipient's public key
  • amount - 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 Operations

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
) -> PrivateTransaction

Parameters:

  • note_secret - Secret of the note you're spending
  • note_index - Leaf index of your note in the Merkle tree
  • amount - 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 signature
  • nullifier - Hex-encoded nullifier (marks note as spent)
  • proof - zkSNARK proof bytes
  • status - TransactionStatus.CONFIRMED

Raises:

  • ValueError - If invalid destination address or amount
  • RuntimeError - 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
) -> PrivateTransaction

Parameters:

  • note_secret - Secret of the note you're spending
  • note_index - Leaf index of your note
  • amount - Amount to withdraw
  • recipient_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}")  # PENDING

Verification and State Queries

verify_proof()

Verify a Groth16 zkSNARK proof locally.

def verify_proof(
    proof: bytes,
    public_inputs: dict[str, Any]
) -> bool

Parameters:

  • 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
) -> bool

Parameters:

  • 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() -> bytes

Returns: 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

Connection Management

close()

Close the RPC connection and clean up resources.

async def close() -> None

Example:

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 closed

Context Manager Usage

VeilClient 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 block

Error Handling

ValueError

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

Complete Example

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())

Performance Characteristics

OperationClient TimeOn-Chain TimeCost
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()<10msN/AFree (local)
is_nullifier_spent()~100msN/AFree (read-only)
get_merkle_root()~100msN/AFree (read-only)

Hardware requirements (client-side):

  • Proof generation: 2-4 GB RAM, modern CPU
  • Other operations: Minimal resources

See Also