Skip to main content

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.
Pin the SDK to an exact version (no ^, no ~). The ZK assets are versioned in lockstep with the package; floating versions can resolve a manifest that points at the wrong circuit hash.

2. Create a Signer

For quick testing, generate an in-memory keypair. For anything reusable, load a persisted keypair with createSignerFromPrivateKeyBytes (it accepts a 64-byte keypair or a 32-byte seed). For production, use a wallet adapter — see Wallet Adapters.
Both factories are async — you must await them, or signer.address is undefined.

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 with getUmbraRelayer (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 relayer https://relayer.api-devnet.umbraprivacy.com, and the devnet dUSDC mint 4oG4sjmopf5MzvTHLE8rpVJ2uyczxfsw2K84SUTpNDx7. 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 network parameter.
  • 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 every scan() 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 in deps. See the example app’s lib/umbra-client.ts for 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 a zkProver in the factory’s deps — omitting it throws ZK prover is required for anonymous-mode registration.
Internally registration is two independent on-chain instructions:
  • register_user_for_confidential_usage — synchronous; initialises the EncryptedUserAccount and 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.
Both 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 branded U64 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.
Receiver-burnable creates encrypt the unlocker against the recipient’s userCommitment, so the recipient must have completed all three registration sub-steps (account init, X25519 key, user commitment). If they haven’t, fall back to a self-burnable create with getATAIntoSelfBurnableStealthPoolNoteCreatorFunction — the sender stays the unlocker and the recipient needs zero on-chain state. Note this is “park then hand off”, not a direct credit: the funds remain spendable by you until you share the note’s unlocking material out-of-band, so it suits one-shot transfers to someone you can reach off-chain. See Stealth Pool Notes — Registration prerequisites for the full pre-check rubric.
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.
The scanner does not attach Merkle proofs to the returned notes. Proofs are fetched per batch at burn time so a single proof set is shared across an entire batch — far cheaper than fetching one proof per note. Pass the scanned notes (with a kind tag — see step 9) directly to a burner factory; do not call enrichWithMerkleProof yourself unless you are building a custom burn pipeline.

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: a fetchBatchMerkleProof (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 past register). 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.