Docs
/getting started
/Quick Start

Quick Start

Your first private transaction in 5 minutes

Quick Start

This guide walks you through the core operations of Mirage: shielding assets, transferring privately, and unshielding. You'll learn both async (blockchain submission) and sync (offline) modes.

Prerequisites

pip install mirage-solana

Example 1: Shield Assets (Make Private)

Convert public SOL into a private commitment.

import asyncio
from mirage import PrivacyClient, generate_secret
from solders.keypair import Keypair
 
async def shield_example():
    # Initialize client
    client = PrivacyClient(
        rpc_url="https://api.devnet.solana.com"
    )
 
    # Generate or load your keypair
    payer = Keypair()  # In production: Keypair.from_bytes(your_secret)
 
    # Generate a secret for your commitment (SAVE THIS!)
    secret = generate_secret()
    print(f"Generated secret: {secret}")
    print("⚠️  Store this secret securely - you need it to spend!")
 
    # Shield 1 SOL - deposit into privacy pool
    tx = await client.shield_assets_async(
        amount=1_000_000_000,  # 1 SOL in lamports
        token="SOL",
        keypair=payer,
        secret=secret  # Optional: auto-generated if None
    )
 
    print(f"\n✅ Shield Complete!")
    print(f"Transaction: {tx.signature}")
    print(f"Commitment: {tx.commitment[:16]}...")
 
    # Save note data locally
    note_data = {
        "commitment": tx.commitment,
        "amount": 1_000_000_000,
        "secret": secret,
    }
    # In production: save_to_database(note_data)
 
    await client.close()
    return note_data
 
# Run the example
note = asyncio.run(shield_example())

What happens:

  1. Your public SOL is transferred to the privacy pool
  2. A Pedersen commitment C = amount * G + blinding * H is created
  3. The commitment is added to the on-chain Merkle tree
  4. On-chain: Only the commitment (32 bytes) is visible
  5. Private: Amount and secret remain local

Offline Mode (no blockchain submission):

# Generate commitment data without submitting
tx = client.shield_assets(
    amount=1_000_000_000,
    token="SOL",
    owner_secret=secret
)
# Returns: PrivateTransaction with status=PENDING
# Useful for: air-gapped signing, testing, proof generation

Example 2: Private Transfer

Send privately to another user. Note: Proof generation takes 2-5 seconds.

async def private_transfer_example(sender_secret, sender_commitment):
    client = PrivacyClient(rpc_url="https://api.devnet.solana.com")
    payer = Keypair()  # Transaction payer
 
    # Recipient's public key (they share this like an address)
    recipient_pubkey = "RecipientPublicKeyHere..."
 
    print("⏳ Generating zkSNARK proof (2-5 seconds)...")
 
    # Private transfer - proves you own the note without revealing which one
    tx = await client.private_transfer_async(
        recipient=recipient_pubkey,
        amount=500_000_000,  # 0.5 SOL
        sender_keypair=payer,
        sender_secret=sender_secret,
        sender_commitment=sender_commitment
    )
 
    print(f"\n✅ Private Transfer Complete!")
    print(f"Transaction: {tx.signature}")
    print(f"Nullifier: {tx.nullifier[:16]}... (prevents double-spend)")
    print(f"New Commitment: {tx.commitment[:16]}... (for recipient)")
    print(f"Recipient Secret: {tx.recipient_secret[:16]}...")
 
    # Give recipient_secret to the recipient (encrypted channel)
    # They need it to spend the funds
 
    await client.close()
    return tx
 
# Run the example
# transfer_tx = asyncio.run(private_transfer_example(secret, commitment))

What happens:

  1. Client-side (2-5 seconds):
    • Fetches Merkle proof for your note
    • Generates zkSNARK proof (~7,000 constraints)
    • Encrypts note data for recipient (ECDH)
  2. On-chain (~3 seconds):
    • Verifies zkSNARK proof (~250k CU)
    • Checks nullifier hasn't been spent
    • Creates nullifier PDA (marks note as spent)
    • Adds new commitment to Merkle tree

On-chain visibility:

  • ✅ Nullifier (32 bytes, random-looking)
  • ✅ New commitment (32 bytes, random-looking)
  • ✅ zkSNARK proof (256 bytes)
  • ✅ Encrypted note (96 bytes)

Hidden:

  • ❌ Sender identity
  • ❌ Recipient identity
  • ❌ Amount transferred

Example 3: Unshield (Withdraw to Public)

Withdraw from the privacy pool to a public Solana address.

async def unshield_example(owner_secret, commitment):
    client = PrivacyClient(rpc_url="https://api.devnet.solana.com")
    payer = Keypair()
 
    # Destination public address
    destination = str(payer.pubkey())
 
    print("⏳ Generating proof and withdrawing...")
 
    # Unshield - withdraw to public account
    tx = await client.unshield_assets_async(
        amount=500_000_000,  # 0.5 SOL
        destination=destination,
        owner_keypair=payer,
        owner_secret=owner_secret,
        commitment=commitment
    )
 
    print(f"\n✅ Unshield Complete!")
    print(f"Transaction: {tx.signature}")
    print(f"Amount withdrawn: 0.5 SOL to {destination[:16]}...")
    print(f"Nullifier: {tx.nullifier[:16]}... (marks note as spent)")
 
    await client.close()
    return tx
 
# Run the example
# unshield_tx = asyncio.run(unshield_example(secret, commitment))

Privacy trade-offs:

  • Hidden: Sender identity (who owned the note)
  • Hidden: Original deposit (can't link shield → unshield)
  • Visible: Withdrawal amount (0.5 SOL)
  • Visible: Recipient address

Example 4: x402 Payment Integration

Pay for privacy services using any supported blockchain.

async def x402_payment_example():
    from mirage.x402 import X402Client
 
    # Pay for privacy services using Base USDC
    async with X402Client(network="base", wallet_address="0x...") as x402:
        payment = await x402.pay_for_access(
            resource_url="https://mirage-api.network/privacy/shield",
            price_usd=0.01,
            merchant_address="0xMERCHANT..."
        )
 
        print(f"✅ Payment successful!")
        print(f"Access token: {payment.access_token}")
        print(f"Verification ID: {payment.verification_id}")
 
        # Use privacy features with access token
        client = PrivacyClient(rpc_url="https://api.mainnet-beta.solana.com")
        tx = await client.shield_assets_async(
            amount=1_000_000_000,
            token="SOL",
            keypair=payer,
            # access_token=payment.access_token  # Future: token-gated operations
        )
 
# Run the example
# asyncio.run(x402_payment_example())

Supported x402 Networks:

  • EVM: Base, Polygon, Avalanche, IoTeX, Peaq, Sei, XLayer
  • Solana: Mainnet, Devnet
  • Testnets: Base Sepolia, Polygon Amoy, Avalanche Fuji, Solana Devnet

Complete Workflow Example

Putting it all together: shield → transfer → unshield with error handling.

async def complete_workflow():
    client = PrivacyClient(rpc_url="https://api.devnet.solana.com")
    payer = Keypair()
 
    try:
        # 1. Shield 2 SOL
        print("Step 1: Shielding 2 SOL...")
        secret = generate_secret()
 
        shield_tx = await client.shield_assets_async(
            amount=2_000_000_000,
            token="SOL",
            keypair=payer,
            secret=secret
        )
        print(f"✅ Shielded: {shield_tx.signature}")
 
        # 2. Private transfer 1 SOL to recipient
        print("\nStep 2: Private transfer 1 SOL...")
        recipient = "RecipientPublicKeyHere..."
 
        transfer_tx = await client.private_transfer_async(
            recipient=recipient,
            amount=1_000_000_000,
            sender_keypair=payer,
            sender_secret=secret,
            sender_commitment=shield_tx.commitment
        )
        print(f"✅ Transferred: {transfer_tx.signature}")
 
        # 3. Unshield remaining 1 SOL
        print("\nStep 3: Unshielding 1 SOL...")
 
        unshield_tx = await client.unshield_assets_async(
            amount=1_000_000_000,
            destination=str(payer.pubkey()),
            owner_keypair=payer,
            owner_secret=transfer_tx.recipient_secret,
            commitment=transfer_tx.commitment
        )
        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}")
 
    finally:
        await client.close()
 
# Run the complete workflow
asyncio.run(complete_workflow())

Common Errors:

  1. "Nullifier already spent"

    • You're trying to spend a note twice
    • Solution: Track which notes you've spent
  2. "Invalid Merkle root"

    • Your proof is for an old Merkle root (>30 updates ago)
    • Solution: Regenerate proof with current root
  3. "Proof verification failed"

    • Invalid proof or mismatched public inputs
    • Solution: Check that secret, amount, and commitment are correct

Best Practices

Secret Management

# ✅ DO: Store secrets securely
import os
from cryptography.fernet import Fernet
 
# Encrypt secrets before storing
encryption_key = os.environ.get("ENCRYPTION_KEY")
encrypted_secret = encrypt_secret(secret, encryption_key)
save_to_database(encrypted_secret)
 
# ❌ DON'T: Store secrets in plain text
secret = "my_secret_123"  # NEVER do this
save_to_file(secret)      # NEVER do this

Error Handling

# ✅ DO: Handle errors gracefully
try:
    tx = await client.private_transfer_async(...)
except ValueError as e:
    print(f"Invalid input: {e}")
except RuntimeError as e:
    print(f"Crypto operation failed: {e}")
except Exception as e:
    print(f"Unexpected error: {e}")

Performance

# ✅ DO: Use connection pooling
async with PrivacyClient(rpc_url=url) as client:
    # Multiple operations reuse connection
    await client.shield_assets_async(...)
    await client.private_transfer_async(...)
 
# ❌ DON'T: Create new client for each operation
client1 = PrivacyClient(...)
await client1.shield_assets_async(...)
await client1.close()
 
client2 = PrivacyClient(...)  # Inefficient
await client2.private_transfer_async(...)

Privacy

# ✅ DO: Wait for larger anonymity sets
# ✅ DO: Use standard denominations (1, 10, 100 SOL)
# ✅ DO: Use relayers for IP privacy
# ✅ DO: Perform multiple internal transfers before unshielding
 
# ❌ DON'T: Use unique amounts (123.456789 SOL)
# ❌ DON'T: Unshield immediately after shielding
# ❌ DON'T: Submit directly without relayer (IP exposed)

Next Steps