Quick Start
Your first private transaction in 5 minutes
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.
pip install mirage-solanaConvert 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:
- Your public SOL is transferred to the privacy pool
- A Pedersen commitment
C = amount * G + blinding * His created - The commitment is added to the on-chain Merkle tree
- On-chain: Only the commitment (32 bytes) is visible
- 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 generationSend 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:
- Client-side (2-5 seconds):
- Fetches Merkle proof for your note
- Generates zkSNARK proof (~7,000 constraints)
- Encrypts note data for recipient (ECDH)
- 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
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
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
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:
-
"Nullifier already spent"
- You're trying to spend a note twice
- Solution: Track which notes you've spent
-
"Invalid Merkle root"
- Your proof is for an old Merkle root (>30 updates ago)
- Solution: Regenerate proof with current root
-
"Proof verification failed"
- Invalid proof or mismatched public inputs
- Solution: Check that secret, amount, and commitment are correct
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 thisError 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)- API Reference - Complete PrivacyClient documentation
- x402 Integration - Chain-agnostic payments
- Architecture - Understand how it works
- Privacy Model - Learn about privacy guarantees and limitations