Prerequisites
- Node.js 18+ or a modern browser
- A Solana wallet (or a generated keypair for testing)
- A Solana JSON-RPC URL (HTTP and WebSocket)
1. Install
The SDK is one package — the ZK prover, indexer client, relayer client, and Solana primitives are all subpath modules of@umbra-privacy/sdk. snarkjs is a required
peer dependency (the SDK imports it for Groth16 proving) — install it alongside the SDK
or the first import throws ERR_MODULE_NOT_FOUND.
If you connect a browser wallet (Phantom/Solflare/…) you also need the
@wallet-standard/core peer dependency. It is not required for the
createInMemorySigner / createSignerFromPrivateKeyBytes paths used below.2. Create a Signer
For quick testing, generate an in-memory keypair. For anything reusable, load a persisted keypair withcreateSignerFromPrivateKeyBytes (it accepts a 64-byte
keypair or a 32-byte seed). For production, use a wallet adapter — see
Wallet Adapters.
3. Create the Umbra Client
The client owns your signer, network configuration, and indexer endpoint. The relayer is not a client argument — you build it separately withgetUmbraRelayer (step 9).
- Testing on devnet? Use
network: "devnet",rpcUrl: "https://api.devnet.solana.com"(+wss://api.devnet.solana.com),indexerApiEndpoint: "https://utxo-indexer.api-devnet.umbraprivacy.com", the devnet relayerhttps://relayer.api-devnet.umbraprivacy.com, and the devnet dUSDC mint4oG4sjmopf5MzvTHLE8rpVJ2uyczxfsw2K84SUTpNDx7. See Supported Tokens for the full per-network mint list. - The Umbra program address differs between devnet and mainnet. The SDK resolves the correct address automatically based on the
networkparameter. - The first operation that needs the master seed (typically
register()) prompts the wallet to sign a deterministic consent message. Subsequent operations reuse the cached seed without re-prompting. - Browser: persist scan state. On Node you can omit persistent stores and the scanner falls back to in-memory state (fine for scripts). In the browser, wire
createShardedUtxoDataStore+createShardedNullifierStore(@umbra-privacy/sdk/store-adapters) — otherwise everyscan()walks every tree from genesis. Because those stores derive their keys from the client’s master seed, it’s a two-phase build: construct a bootstrap client, build the stores from it, then rebuild the client with the stores indeps. See the example app’slib/umbra-client.tsfor the full pattern.
4. Register Your Account
Registration sets up your on-chain Umbra identity. It is idempotent: skipped sub-steps cost zero SOL, so you can call it safely on every app start. Anonymous registration runs a Groth16 proof, so it requires azkProver in the
factory’s deps — omitting it throws ZK prover is required for anonymous-mode registration.
register_user_for_confidential_usage— synchronous; initialises theEncryptedUserAccountand stores the X25519 token-encryption pubkey.register_user_for_anonymous_usage_v18— MPC + ZK; stages master-viewing-key material, runs Fiat-Shamir + Groth16, queues the Arcium MPC computation, and stores the on-chain user commitment.
init_if_needed the user account, so either may run first. The orchestrator picks the right combination based on requested flags and on-chain state.
5. Deposit Tokens (ATA → ETA)
Shield an SPL or Token-2022 balance by moving it from your public Associated Token Account into your Encrypted Token Account. Amounts are brandedU64 and mints are branded Address — wrap raw values with
createU64 (@umbra-privacy/sdk/types) and address (@solana/kit) so the calls
typecheck.
6. Withdraw Tokens (ETA → ATA)
Move tokens back from your Encrypted Token Account to your public ATA. (MINT and
amount are the branded values from step 5.)
7. Create a Receiver-Burnable Stealth Pool Note
Send tokens privately to a recipient by writing a Stealth Pool Note into the mixer. The recipient can later scan and burn it with no on-chain link to you as the sender.Creating a note requires a
zkProver dependency for Groth16 proof generation. The
prover ships inside the SDK at @umbra-privacy/sdk/zk-prover — there is no separate
@umbra-privacy/web-zk-prover package any more.Spending from your shielded balance. The factory above sources tokens from your
public ATA. To create a note from your existing Encrypted Token Account balance
(the common case once you’ve deposited), use the ETA-sourced creators —
getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction /
getETAIntoSelfBurnableStealthPoolNoteCreatorFunction — same call shape, paired with
getETAIntoStealthPoolNoteCreatorProver.8. Scan for Burnable Notes
As the recipient, run the scanner. It walks every active stealth-pool tree, decrypts every commitment it can with your viewing keys, and groups the results by(burn kind, source). Cursor state lives in client.utxoDataStore — the scanner picks up exactly where the last call left off.
9. Burn the Note
Burning consumes a note, reveals its nullifier on-chain to prevent double-spend, and credits the destination balance. We’ll burn the receiver-burnable note we just created, into the recipient’s Encrypted Token Account. The burner factory needs three dependencies: afetchBatchMerkleProof (already exposed on client), a zkProver for the burn circuit, and a relayer adapter whose three methods (submitBurn, pollBurnStatus, getRelayerAddress) forward to the relayer service.
The scanner returns notes without a kind discriminator, but the burner input is
typed DecryptedStealthPoolNoteData & { kind: "receiver-burnable" } — tag each note
with kind before passing it in.
For self-burnable notes use the matching burners + provers:
getSelfBurnableStealthPoolNoteIntoETABurnerFunction /
getSelfBurnableStealthPoolNoteIntoATABurnerFunction, with
getClaimSelfClaimableUtxoIntoEncryptedBalanceProver /
getClaimSelfClaimableUtxoIntoPublicBalanceProver. Tag those notes with
kind: "self-burnable".The
submitBurn / pollBurnStatus property names map onto the relayer client’s existing submitClaim / pollClaimStatus via plain TypeScript aliases (BurnSubmitterFunction = ClaimSubmitterFunction). The wire protocol still calls the endpoint /v1/claims — nothing has changed there.Batching: pass an array of notes. The receiver-into-ETA burner groups them by
destinationAddress, chunks to ≤5 per proof, and runs each chunk through the relayer. Single-note burners (self-into-ETA, self-into-ATA) have MAX_NOTES_PER_PROOF = 1 and loop internally — caller still passes an array.Full Example
A runnable Node script (uses an ephemeral signer, so fund + reuse a real keypair to take it pastregister). Stores are omitted — on Node the scanner falls back to
in-memory state; for browser persistence see the two-phase note in step 3.
Next Steps
Wallet Adapters
Connect Phantom, Solflare, or any Solana wallet.
Stealth Pool
Anonymous transfers via Stealth Pool Notes.
Query State
Read encrypted balances and account state on-chain.
Error Handling
Recover from dropped relayer callbacks and partial burns.