Quick Start
End-to-end walkthrough of a private transfer
Prerequisites#
- A Solana wallet (Phantom or Solflare) with devnet SOL
- Node.js 18+ for the relayer
- The Mini Veil devnet cluster running
Step-by-Step Flow#
1. Connect Wallet#
Open the Mini Veil dashboard and connect your wallet. The dashboard automatically checks your key registration status.
2. Register Keys (first time only)#
If prompted, approve the signMessage request to derive your protocol keys. The dashboard will register them on-chain via the register_keys instruction.
3. Enter Recipient#
In the PrivateSend card, enter the recipient's wallet address. The dashboard checks if the recipient has registered their keys (500ms debounced lookup).
Recipient Does Not Have Keys
If the recipient hasn't registered keys, the dashboard shows a warning. They need to connect to the dashboard at least once and register their keys before you can send to them.
4. Select Denomination#
Choose a pool denomination: 0.1 SOL, 1 SOL, or 10 SOL. Each denomination has a separate Merkle tree and vault.
5. Shield (if needed)#
If you don't have an unspent note in the pool, click Shield:
- The relayer provides a counter via
GET /next-counter - A note is computed:
nullifier = Poseidon(spendingKey, domain, counter),commitment = Poseidon(nullifier, secret) - The
shieldinstruction transfersdenominationlamports to the vault PDA - The relayer detects the
DepositEventviaonLogssubscription and inserts the commitment into its Merkle tree
6. Wait for Relayer#
The frontend polls GET /proof?commitment=0x...&denomination=...:
- Blocks up to 30 seconds waiting for the relayer to process the deposit
- Returns
408on timeout — the frontend retries up to 30 times with 1-second intervals - Returns
{ root, pathElements, pathIndices, leafIndex }when ready
7. Generate ZK Proof#
snarkjs generates a Groth16 proof in the browser:
public signals: merkle_root, nullifier_hash, recipient, denomination
private signals: spending_key, counter, secret, pathElements[20], pathIndices[20]
This takes approximately 2-5 seconds.
8. Unshield + Announce#
A bundled transaction is submitted:
ComputeBudgetProgram.setComputeUnitLimit({ units: 700_000 })unshieldwith packed proof (G1: 64 bytes, G2: 128 bytes, 4 public inputs)announceStealthwith ephemeral key, view tag, encrypted key
The on-chain program:
- Verifies the Groth16 proof via
alt_bn128pairing - Creates a nullifier PDA to prevent double-spending
- Transfers denomination from vault to the recipient's stealth address
9. Recipient Claims#
On the recipient's dashboard, ScanClaimCard:
- Fetches
StealthAnnouncementevents from recent transactions - Filters by view tag (fast rejection)
- Decrypts the ephemeral key using their viewing key
- Derives the stealth private key
- Builds a transaction signed by the stealth address's
ScalarSigner - Sends the claim transaction, transferring funds to their main wallet
Costs#
| Operation | Cost | |---|---| | Key registration | ~0.005 SOL | | Shield | denomination + transaction fee | | Unshield | denomination + ~0.00089 SOL (nullifier PDA rent) + transaction fee | | Sweep fee | 10,000 lamports deducted from claim amount |
Complete
That's the full flow. You've now sent a private, unlinkable transfer on Solana. The entire process fits in ~9 steps and takes about 30 seconds end-to-end.