# Encrypted Balances Source: https://sdk.umbraprivacy.com/concepts/encrypted-balances EncryptedTokenAccounts (ETAs): MXE-only mode (Arcium network decryptable) vs Shared mode (user X25519 decryptable). Covers nonces, ciphertext structure, and encryption upgrades. ## ATA vs. ETA Solana uses **AssociatedTokenAccounts (ATAs)** to hold SPL and Token-2022 tokens. These are fully public; anyone can query your balance. Umbra introduces **EncryptedTokenAccounts (ETAs)** — on-chain PDA accounts that hold your token balance in encrypted form. **ATA (standard public account)** * Balance visible on-chain. * Supports standard SPL transfers. * No registration required. * Works with any SPL or Token-2022 token. **ETA (Umbra encrypted account)** * Balance hidden on-chain. * Use Umbra deposit / withdraw / convert / burn instead of standard SPL transfers. * One-time user registration required. * Works with any [supported](/supported-tokens) mint. ## Depositing: ATA → ETA When you call the depositor `getATAIntoETADirectDepositorFunction`, your tokens move from your ATA into the on-chain pool custody account. The program records the encrypted balance in the ETA PDA for your `(wallet, mint)` pair. ``` Your ATA ──deposit──► Pool custody (on-chain SPL) │ ▼ Your ETA (encrypted balance stored here) ``` The pool custody account holds the real tokens. The ETA stores the cryptographic proof of how much of those tokens belong to you. ## Withdrawing: ETA → ATA `getETAIntoATAWithdrawerFunction` reverses the deposit. Arcium MPC verifies that your encrypted balance is sufficient and authorises the transfer from the pool custody account back to your ATA. ## The Two Encryption Modes ### MXE-Only In MXE-only mode, your balance is encrypted under the **Arcium MXE (Multi-party Exchange) public key**. Only the Arcium network can decrypt it. * Withdrawals require Arcium to perform the decryption computation. * You cannot query your own balance client-side without Arcium. * This is the default mode for users who have not registered an [X25519](https://www.rfc-editor.org/rfc/rfc7748) key. ### Shared Mode In Shared mode, your balance is encrypted under **two keys simultaneously**: the Arcium MXE key and your personal **X25519 public key**. * You can decrypt and read your own balance locally, without a network call. * Withdrawals still use Arcium's MPC for the on-chain operation. * Available after completing the X25519 key registration sub-step (part of the standard `register({ confidential: true })` flow). To upgrade an existing MXE-only ETA to Shared mode, use the **converter**: ```typescript theme={null} import { getNetworkEncryptionToSharedEncryptionConverterFunction } from "@umbra-privacy/sdk/conversion"; const convert = getNetworkEncryptionToSharedEncryptionConverterFunction({ client }); await convert([USDC_MINT]); ``` If you register with `confidential: true` (the default), your deposits will automatically use Shared mode. This is strongly recommended — it lets you call the encrypted-balance querier locally instead of round-tripping through Arcium. ## Account Lifecycle An ETA is created on first deposit and exists for the lifetime of the `(wallet, mint)` pair. Subsequent deposits update the encrypted balance in place. ```typescript theme={null} import { getATAIntoETADirectDepositorFunction } from "@umbra-privacy/sdk/deposit"; const deposit = getATAIntoETADirectDepositorFunction({ client }); // First deposit for a given mint → creates the ETA. await deposit(signer.address, USDC_MINT, 1_000_000n); // Second deposit → adds to the existing ETA balance. await deposit(signer.address, USDC_MINT, 500_000n); ``` ## Nonces and Replay Protection Each ETA has a **nonce** — a monotonically increasing counter used to prevent replay attacks. The nonce is derived from the account's `generationIndex` field combined with on-chain entropy. You don't manage nonces directly; the SDK handles them. ## Viewing Your Balance If you are registered in Shared mode, you can query your current encrypted balance: ```typescript theme={null} import { getEncryptedBalanceQuerierFunction } from "@umbra-privacy/sdk/query"; const query = getEncryptedBalanceQuerierFunction({ client }); const balances = await query([USDC_MINT]); const result = balances.get(USDC_MINT); switch (result?.state) { case "shared": console.log("Balance:", result.balance); // decrypted MathU64 break; case "mxe": console.log("Account is in MXE mode — convert to Shared to decrypt locally."); break; case "uninitialized": console.log("Account exists but balance not initialised."); break; case "non_existent": console.log("No ETA for this mint."); break; } ``` Only Shared-mode ETAs can be decrypted client-side. MXE-mode ETAs return `{ state: "mxe" }` without a balance. Use the converter (above) to move to Shared mode. ## Protocol Fees Deposits and withdrawals subtract a small protocol fee from your balance. The fee has two components: * **Base fee** — a fixed amount in the token's native units. * **Commission** — a percentage of the amount in basis points (current rate: 35 bps with a 16,384 divisor — see [Pricing](/pricing)). Fees are deducted automatically. For Token-2022 mints with a transfer fee extension, the Token-2022 transfer fee is subtracted *before* protocol fees are applied — see [Token-2022 Support](/advanced/token-2022). ## Umbra Confidential SPL Token The **Umbra Confidential SPL token** adds a loyalty/lottery-point tracking layer to every `EncryptedTokenAccount`. It is live on-chain and in active SDK integration. ### What changes in the ETA state Each `EncryptedTokenAccount` now carries two additional Rescue-cipher ciphertext fields alongside the encrypted balance: * `points_short_epoch` — encrypted point accumulator for the current short epoch * `points_long_epoch` — encrypted point accumulator for the current long epoch A boolean flag `is_points_initialised` indicates whether these fields contain valid encrypted-zero ciphertexts (true) or just zero-byte placeholders (false — pre-migration accounts). ### How points accumulate Every time a Stealth Pool Note is burned **into** an ETA, the Arcium MPC computes a lottery-ticket delta and adds it to the ETA's running totals. The relayer includes this delta in the burn-batch API response as a `rescue_encrypted_lottery_ticket_delta` field on each batch. The `BurnBatchResult` SDK type does not yet expose this field — it is currently accessible only at the raw relayer API layer. ### Account migration ETAs deployed before the Umbra Confidential SPL token feature have zero-byte placeholders for the two new fields. To enable points accrual, each such account must go through a two-step migration: 1. **`migrate_for_points`** — a pure on-chain account realloc (no MPC). Expands the account from the old layout to the new size and shifts existing data forward. 2. **`initialise_points_for_network_balance_v15`** or **`initialise_points_for_shared_balance_v15`** — an Arcium MPC instruction that overwrites the placeholder bytes with valid encrypted-zero ciphertexts and sets `is_points_initialised = true`. SDK TypeScript factory helpers for the migration steps are **not yet shipped**. To run the migration from code, use the Codama-generated client (`@umbra-privacy/umbra-codama`) directly until higher-level wrappers land in a future SDK release. ### SDK support status | Feature | Status | | --------------------------------------------------------------- | ------- | | On-chain ETA points fields | Live | | On-chain migration + initialisation instructions | Live | | `rescue_encrypted_lottery_ticket_delta` in relayer API response | Live | | `BurnBatchResult.lotteryTicketDelta` SDK field | Planned | | SDK migration helper factories | Planned | | SDK points-balance query | Planned | # How Umbra Works Source: https://sdk.umbraprivacy.com/concepts/how-umbra-works How Umbra works: EncryptedTokenAccounts via Arcium MPC dual-instruction pattern (queue_computation + arcium_callback), a Stealth Pool with an Indexed Merkle Tree, and compliance grants. ## The Problem with Public Blockchains Every transaction on Solana is publicly readable. Anyone can look up your wallet address and see exactly how much of any token you hold and every transfer you've ever made. For many use cases — payroll, business payments, personal finance — this is unacceptable. Umbra fixes this without requiring you to move your tokens off Solana or trust a centralized custodian. ## The Two Privacy Modes Umbra provides two distinct privacy tools that can be used independently or together: Your token balance is stored on-chain, but the amount is encrypted. Transfers still happen on-chain — what's hidden is *how much*. Tokens are written as **Stealth Pool Notes** into a shared mixer tree, then **burned** from any wallet with no on-chain link between sender and recipient. *Who paid whom* is hidden. You can use the EncryptedTokenAccount alone (hide amounts), the Stealth Pool alone (hide the transfer path), or both together for the strongest privacy guarantees. ## How EncryptedTokenAccounts Work When you deposit tokens into your ETA: 1. Your tokens move from your **AssociatedTokenAccount (ATA)** to the on-chain pool custody account. 2. The program initialises an `EncryptedTokenAccount` PDA for your `(wallet, mint)` pair if one does not already exist. 3. The balance is encrypted and stored in that ETA using **Arcium MPC**. The encryption means no one — not Umbra, not Arcium, not Solana validators — can read your balance without the decryption key. ### What is Arcium MPC? Arcium is a network of nodes that perform computation on encrypted data using **[Multi-Party Computation (MPC)](https://dl.acm.org/doi/10.1145/62212.62213)**. You don't need to understand the details, but the key property is: > No single node can decrypt your data. The computation is split across multiple independent parties so that learning your balance requires compromising a majority of them simultaneously. From a developer's perspective: Arcium is the computation backend. The SDK handles all communication with it automatically. ### Encryption Modes When your balance is encrypted, it can be in one of two modes: * **MXE-only** — Only the Arcium MPC network can decrypt it. Withdrawals require MPC to perform the decryption. You cannot decrypt it locally. * **Shared** — Encrypted under both the Arcium MPC key *and* your personal [X25519](https://www.rfc-editor.org/rfc/rfc7748) key. You can decrypt your own balance client-side without MPC. Available after you register your X25519 key. After [registering](/sdk/registration) with `confidential: true`, your deposits automatically use Shared mode so you can query your balance locally. ## How the Stealth Pool Works The Stealth Pool uses a cryptographic structure called an **[Indexed Merkle Tree](https://link.springer.com/chapter/10.1007/3-540-48184-2_32)** combined with **[zero-knowledge proofs](https://dl.acm.org/doi/10.1145/22145.22178)**. Here's the simplified version: You create a **commitment** — a cryptographic hash of `(amount, recipient, secret)` — and insert it as a leaf into the Merkle tree. The commitment is public; the contents are not. More users write notes into the same tree. Your commitment is now one of many thousands of leaves. The larger the set, the stronger the anonymity. You present a **zero-knowledge proof** that you know the secret behind one of the leaves in the tree without revealing *which* leaf. The proof is verified on-chain, the **nullifier** is burned to prevent double-spending, and the tokens are released to the destination balance. The deposit address and the burn destination need not be related. An observer sees a note being written and a burn happening, but cannot link them. ### Self-burnable vs receiver-burnable There are two ways to write a Stealth Pool Note: * **Self-burnable** — the writer keeps the unlocker. Useful for "stage funds in the pool, burn them out later from a fresh wallet" patterns. Requires zero on-chain state from the destination. * **Receiver-burnable** — the writer encrypts the unlocker against the **recipient's** `userCommitment`. The recipient scans, decrypts, and burns. Requires the recipient to have completed all three registration sub-steps on-chain (account init, X25519 key, user commitment). ## The Dual-Instruction Pattern Every confidential MPC operation in Umbra involves two on-chain transactions: 1. **Handler** — your wallet signs a transaction that validates inputs and queues a computation request with Arcium. 2. **Callback** — Arcium completes the off-chain computation and posts the result back on-chain, triggering the program's callback instruction to update state. The SDK waits for both transactions to confirm before returning. You don't need to manage this yourself. This is why Umbra MPC operations take a few seconds longer than a standard token transfer — there is an off-chain MPC round-trip between the two on-chain instructions. Pure ZK operations (e.g. an ATA-source note write) are single-tx and finish in normal Solana confirmation time. ## Privacy Guarantees Summary * **Token balance amount** — hidden by EncryptedTokenAccounts. Encrypted with MPC; auditors can be granted selective access via compliance grants. * **Transfer counterparty** — hidden by the Stealth Pool. Requires a sufficient anonymity set (other notes in the same tree at the same denomination). * **Transaction history** — strongest guarantees when the Stealth Pool and EncryptedTokenAccounts are used together. ## What is NOT Hidden * **That you are using Umbra** — on-chain interactions with the Umbra program are visible. * **Note amounts** — the amount is committed at note creation and revealed at burn. * **Timing** — if you write a note and immediately burn it from a fresh wallet, temporal analysis can correlate them. ## Compliance Umbra includes a built-in compliance system. Users can create **viewing grants** that allow specific parties (auditors, regulators) to decrypt their balances. See the [Compliance](/sdk/compliance) guide for details. # Stealth Pool Notes Source: https://sdk.umbraprivacy.com/concepts/utxos-and-mixer Stealth Pool internals: Indexed Merkle Tree of note commitments, nullifier treap for double-spend prevention, X25519 ciphertext discovery, and Groth16 ZK proof verification on burn. ## What is a Stealth Pool Note? A **Stealth Pool Note** is a [UTXO](https://bitcoin.org/bitcoin.pdf)-style commitment leaf in the mixer's Indexed Merkle Tree. The SDK surfaces these commitments as "notes", and the act of consuming one is a "burn". A note encodes: * **Amount** — how many tokens are locked. * **Destination address** — who is authorised to burn. * **Secret randomness** — private entropy known only to the writer (and, for receiver-burnable notes, the recipient). Only the **[Poseidon](https://eprint.iacr.org/2019/458.pdf) hash** of these values (the *commitment*) is stored on-chain. The inputs remain private. ## The Stealth Pool: How It Works The Stealth Pool is a shared **[Indexed Merkle Tree](https://link.springer.com/chapter/10.1007/3-540-48184-2_32)** stored on-chain. Each leaf in the tree is a note commitment. You call one of the four `…StealthPoolNote…Creator` factory functions (see below). The SDK computes a commitment from `(amount, destination, randomness)` and inserts it as a new leaf into the tree. Your tokens are locked in the pool custody account. At this point, anyone can see that *a note was written* and *the tree grew by one leaf* — but cannot see the amount, destination, or any other detail. As more users write notes into the same tree, your commitment becomes one of many. The larger the set, the harder it is to link the write to the eventual burn. Trees hold up to **1,048,576 leaves** (depth-20 Indexed Merkle Tree). The SDK queries the indexer for note ciphertexts, attempts to decrypt each with your viewing keys, and groups successful decrypts by `(burn kind, source)`. The scanner is **zero-arg** — it walks every active tree and tracks its own cursor in `client.utxoDataStore`. See [Scanning](/sdk/mixer/fetching-utxos). You present a **[zero-knowledge proof](https://dl.acm.org/doi/10.1145/22145.22178)** that proves: * You know the secret inputs behind a commitment that exists in the tree. * You haven't burnt it before (nullifier is unspent). Without revealing *which* commitment it is. The on-chain program verifies the proof, burns the nullifier, and releases the tokens to the destination ETA or ATA. ## The Four Note Variants A note is parameterised on two axes — **unlocker** (who can burn it) × **source** (where the funds came from): * **Self-burnable from ATA** — `getATAIntoSelfBurnableStealthPoolNoteCreatorFunction` — sender keeps the unlocker; funded from the writer's ATA. Single tx, no MPC. * **Self-burnable from ETA** — `getETAIntoSelfBurnableStealthPoolNoteCreatorFunction` — sender keeps the unlocker; funded from the writer's ETA. Two-tx MPC pipeline. * **Receiver-burnable from ATA** — `getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction` — unlocker encrypted against the recipient's `userCommitment`; funded from the writer's ATA. * **Receiver-burnable from ETA** — `getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction` — same recipient model, funded from the writer's ETA. Two-tx MPC pipeline. The "self-burnable" pattern is useful when you want to move funds through the pool yourself. The "receiver-burnable" pattern lets you send tokens anonymously — you write for a recipient, and they burn the note without you having direct access. ## Registration prerequisites differ by variant This is one of the most common integration footguns. * **Self-burnable creates** encrypt the unlocker against the **sender's** master-seed-derived key. Sender needs only `isUserAccountX25519KeyRegistered`. Recipient needs nothing. * **Receiver-burnable creates** encrypt against the **recipient's** `userCommitment`. The recipient **must** have completed **all three** registration sub-steps on-chain (account init, X25519 key, user commitment). Otherwise the create simulation fails before any tx is broadcast. **Always pre-check the recipient before a receiver-burnable create.** Run `getUserAccountQuerierFunction` on the recipient address; if any flag is missing, either abort with a clear "ask the recipient to register on Umbra" error, **or** fall back to a self-burnable create (the sender stays the unlocker; recipient needs zero on-chain state — useful for one-shot transfers). ```typescript theme={null} import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk/query"; const query = getUserAccountQuerierFunction({ client }); const r = await query(recipient); const ready = r.state === "exists" && r.data?.isInitialised && r.data?.isUserAccountX25519KeyRegistered && r.data?.isUserCommitmentRegistered; if (!ready) { // Fall back to a self-burnable note, or ask the recipient to register. } ``` ## Nullifiers: Preventing Double-Spends Each note has a corresponding **[nullifier](https://eprint.iacr.org/2014/349.pdf)** — a deterministic hash derived from its private inputs. When a note is burnt, its nullifier is stored in an on-chain **[treap](https://dl.acm.org/doi/10.1145/324133.324247)** (a self-balancing sorted tree). Before allowing a burn, the on-chain program checks that: * The nullifier has not been seen before. * The ZK proof is valid for a commitment in the current Merkle tree. This prevents any note from being burnt twice, even if the burn transaction is replayed. The relayer reports `NullifierAlreadyBurnt` in the failure reason if you re-submit a burn that has already landed — the SDK's burner factory treats this as success and surfaces a clean "Note already burnt" string. ## Ciphertext Discovery After a note is written, an **X25519 + AES-GCM ciphertext** is published on-chain (and indexed off-chain). For self-burnable notes the ciphertext is addressed to the writer; for receiver-burnable notes it is addressed to the recipient. The ciphertext payload contains: * Amount (8 bytes). * Destination address (32 bytes). * Generation index (16 bytes). * Domain separator identifying the note variant (12 bytes). The Umbra indexer stores all ciphertexts and serves them for efficient querying. Your X25519 private key is used locally by the scanner to try decrypting each one — successful decryptions are your burnable notes. Your private key never leaves your device. Decryption happens in the SDK using locally derived X25519 keys. ## Anonymity Set Size The privacy guarantee of the pool depends on how many other notes exist in the same tree at the time you burn. A tree with only one leaf offers no privacy — it's obvious which commitment is being burnt. In practice: * Wait for more users to write notes before burning. * Burning into a different address from the write address increases privacy. * Combining a burn into an ETA (rather than an ATA) hides the destination amount further. ## Trees Fill Up Each Merkle tree has a maximum of 1,048,576 leaves. When a tree is full, the on-chain write service starts a new tree at the next sequential index. Notes from different trees have separate anonymity sets. The scanner walks every active tree automatically — you do not need to specify a tree index. (The old `(treeIndex, start, end)` arguments are gone; cursor state lives in `client.utxoDataStore` and is per-tree under the hood.) # Batch Merkle Proofs Source: https://sdk.umbraprivacy.com/indexer/api-reference/batch-proofs Retrieve multiple Merkle inclusion proofs atomically under the same tree root. ## Endpoint `POST /v1/trees/{tree_index}/proofs` Returns multiple Merkle inclusion proofs for the specified insertion indices, all guaranteed to be computed against the same tree root. This is what the V18 burner factory calls per batch — burns that bundle multiple Stealth Pool Notes need all proofs to share a root. ## Path Parameters * `tree_index` (u64) -- Zero-based index of the Merkle tree. ## Request Body JSON object with a single field: * `insertion_indices` (array of u64) -- Leaf positions to generate proofs for. Maximum 8 indices per request. **Example:** ```json theme={null} { "insertion_indices": [42, 108, 255] } ``` ## Response Protobuf `BatchProofResponse` containing an array of proof objects. Each proof has the same structure as the single proof endpoint: * `root` (string) -- 64-character little-endian hex-encoded Poseidon root hash. Identical across all proofs in the batch. * `tree_index` (int64) -- Echo of the path parameter. * `insertion_index` (int64) -- The leaf position this proof corresponds to. * `proof` (array of 20 strings) -- Sibling hashes from leaf to root, each 64-character hex. * `leaf` (string) -- 64-character hex-encoded final commitment. ## Why Batch? Single proof requests (`GET /v1/trees/{tree_index}/proof/{insertion_index}`) do not guarantee a consistent root across multiple calls — the tree may grow between requests. The batch endpoint acquires a read lock on the tree, ensuring all returned proofs share the same root. This is required for Groth16 ZK proofs that reference multiple note commitments. The SDK's burner factories call this endpoint internally per batch. Receiver-burnable → ETA burns chunk to ≤5 notes per proof; self-burnable burners loop one note at a time. # Health Source: https://sdk.umbraprivacy.com/indexer/api-reference/health/basic GET /health Basic health check for the indexer service. # Health (Detailed) Source: https://sdk.umbraprivacy.com/indexer/api-reference/health/detailed GET /health/detailed Detailed health status including database and dependency checks. # Liveness Source: https://sdk.umbraprivacy.com/indexer/api-reference/health/liveness GET /health/liveness Kubernetes-style liveness probe. Returns 200 if the process is alive. # Readiness Source: https://sdk.umbraprivacy.com/indexer/api-reference/health/readiness GET /health/readiness Kubernetes-style readiness probe. Returns 200 only when the service is ready to serve traffic. # Merkle Proof Source: https://sdk.umbraprivacy.com/indexer/api-reference/merkle-proofs GET /v1/trees/{tree_index}/proof/{insertion_index} Generate a Merkle inclusion proof for a specific leaf in a Merkle tree. Returns an authentication path of **20 sibling hashes** — one per level — ordered from the leaf level (index 0) up to the root level (index 19). All hash values are little-endian 64-character hex strings (32 bytes each). Merkle proofs become **stale** when new leaves are inserted into the tree (because the root changes). The V18 burner factories fetch a per-batch proof immediately before submitting the burn, so simply re-run the burner on a stale-root failure — it refetches the proof. Never cache proofs across operations. ## Verifying a Proof Hash the target leaf with each sibling in order and compare the final result to the tree's current root hash (fetch the root via [Tree Metadata](/indexer/api-reference/tree-metadata)). ## Usage in the SDK The Umbra SDK does **not** use this single-proof endpoint when burning. Burner factories use the [batch endpoint](/indexer/api-reference/batch-proofs) so all proofs in a burn batch share the same root. The single-proof endpoint exists for analytics, debugging, and custom integrations. ## Performance Proof generation traverses all 20 tree levels. Under normal conditions, proofs are generated in under 100 ms. Operations exceeding 500 ms emit a slow-operation warning server-side but still return a valid proof. # Stats Source: https://sdk.umbraprivacy.com/indexer/api-reference/stats GET /v1/stats Aggregate statistics for the entire Stealth Pool Note index across all Merkle trees. Use this endpoint to get a quick snapshot before deciding which tree and index range to scan. Response encoding is always `application/x-protobuf` -- content negotiation is not supported for this endpoint. # Tree Metadata Source: https://sdk.umbraprivacy.com/indexer/api-reference/tree-metadata GET /v1/trees/{tree_index} Returns metadata for a specific Indexed Merkle Tree: leaf count, current root hash, and Stealth Pool Note record count. The root hash changes every time a new leaf is inserted. Use this endpoint to check the current state of a tree before fetching notes or generating Merkle proofs. `utxo_count` may differ from `num_leaves` if some leaves were inserted without Stealth Pool Note ciphertext data (e.g. padding leaves). # Tree Notes Source: https://sdk.umbraprivacy.com/indexer/api-reference/tree-utxos GET /v1/trees/{tree_index}/utxos Returns a paginated list of Stealth Pool Note records for a specific Merkle tree, ordered by insertion index ascending. Use this endpoint when you know which tree you want to scan. For global scans across all trees, use [Global Notes](/indexer/api-reference/utxos). The path retains the legacy `/v1/trees/{tree_index}/utxos` segment. The records are V18 Stealth Pool Notes. ## Response Layouts The `X-Response-Layout` header controls how records are packed in the protobuf response: * **Row-oriented** (default, omit header): each note is a self-contained `UtxoDataItem`. Easy to iterate record by record. * **Columnar** (set header to `"columnar"`): all fields across notes are packed into parallel arrays. Significantly smaller on the wire — preferred for data pipelines and bulk processing. The Umbra SDK uses row-oriented layout internally. Use columnar layout only if you are building a custom bulk processing pipeline. ## Pagination Initialise with `cursor = tree_index × 1,048,576`. Each response includes a `next_cursor` field. Repeat requests using `cursor = next_cursor` until the response contains fewer records than `limit`. ```bash theme={null} # First page of tree 0. curl "https://utxo-indexer.api.umbraprivacy.com/v1/trees/0/utxos?cursor=0&limit=1000" # Next page (using next_cursor from previous response). curl "https://utxo-indexer.api.umbraprivacy.com/v1/trees/0/utxos?cursor=1000&limit=1000" ``` # Single Note Source: https://sdk.umbraprivacy.com/indexer/api-reference/utxo-single GET /v1/utxos/{absolute_index} Returns the single Stealth Pool Note record at the specified global absolute index. The absolute index uniquely identifies a note across all trees: ``` absolute_index = tree_index × 1,048,576 + insertion_index ``` For example, the 5th leaf of tree 2 has absolute index `2 × 1,048,576 + 4 = 2,097,156`. Response is a `SingleUtxoResponse` wrapper containing one `UtxoDataItem`. The `X-Response-Layout` header has no effect on this endpoint. The path retains the legacy `/v1/utxos/{absolute_index}` segment. The record is a V18 Stealth Pool Note. # Global Notes Source: https://sdk.umbraprivacy.com/indexer/api-reference/utxos GET /v1/utxos Returns a paginated list of Stealth Pool Note records across all Merkle trees, ordered by absolute index ascending. Use this endpoint when you need to scan notes globally without knowing which tree they belong to. For tree-specific queries, prefer [Tree Notes](/indexer/api-reference/tree-utxos). The path retains the legacy `/v1/utxos` segment. The records it serves are V18 Stealth Pool Notes. ## Pagination Use the `next_cursor` from each response as the `start` parameter for the next request. Repeat until the response contains fewer records than `limit`. ```bash theme={null} # First page curl "https://utxo-indexer.api.umbraprivacy.com/v1/utxos?start=0&limit=1000" # Next page curl "https://utxo-indexer.api.umbraprivacy.com/v1/utxos?start=1000&limit=1000" # Fetch a specific range (e.g. all notes in tree 1: absolute indices 1,048,576 to 2,097,151) curl "https://utxo-indexer.api.umbraprivacy.com/v1/utxos?start=1048576&end=2097151&limit=5000" ``` ## Response Layouts Set `X-Response-Layout: columnar` for columnar layout (`UtxoColumnarResponse`). Omit for row-oriented layout (`UtxoResponse`). # Indexer Overview Source: https://sdk.umbraprivacy.com/indexer/overview Stealth Pool Note indexer REST API at utxo-indexer.api.umbraprivacy.com. Protobuf responses, paginated notes, Merkle proofs, batch proofs. Used by getBurnableStealthPoolNoteScannerFunction. ## What is the Indexer? The Umbra indexer is an off-chain service that continuously watches the Solana chain for Umbra program transactions, extracts Stealth Pool Note commitments and encrypted ciphertexts, and stores them in a queryable database. It acts as the data layer that bridges on-chain Merkle tree state with the SDK and any other client that needs to interact with the pool. It exposes two primary capabilities: * **Note data** — fetch encrypted note ciphertexts by tree or absolute index. The SDK uses this to scan for notes addressable by your [X25519](https://www.rfc-editor.org/rfc/rfc7748) + viewing keys by trying to decrypt each ciphertext. * **Merkle proofs** — generate a Merkle inclusion proof (authentication path) for any leaf. Required to construct a valid burn transaction on-chain. The V18 burner factories fetch a **per-batch** proof immediately before submission, so the scanner does not pre-bundle proofs. Beyond the SDK, the indexer is useful for analytics, monitoring, and custom integrations such as building your own wallet or scanning pipeline. The service is named `utxo-indexer` for historical reasons. The records it serves are V18 Stealth Pool Notes; the on-chain commitments are still called UTXOs at the protocol level. ## Base URLs * **Mainnet** — `https://utxo-indexer.api.umbraprivacy.com`. * **Devnet** — `https://utxo-indexer.api-devnet.umbraprivacy.com`. ## Response Format All endpoints return **[Protobuf](https://protobuf.dev/)** (`application/x-protobuf`) regardless of the `Accept` header. Protobuf is used by design — binary data like 32-byte hash arrays and ciphertext payloads compress significantly better than JSON. The health endpoints are the exception: they support both JSON (default) and Protobuf via `Accept` header negotiation. ## Rate Limiting All endpoints are subject to rate limiting. Exceeded limits return `429 Too Many Requests`. Contact the Umbra team if you need higher rate limit allowances for production workloads. ## Tree Structure Note commitments are organised into **Indexed Merkle Trees**, each holding up to **1,048,576 leaves** (depth-20). When a tree is full, the indexer begins a new tree at the next sequential index. * Tree 0 holds leaves at absolute indices `0` to `1,048,575`. * Tree 1 holds leaves at absolute indices `1,048,576` to `2,097,151`. * And so on. The absolute index of any leaf is: ``` absolute_index = tree_index × 1_048_576 + insertion_index ``` ## SDK Integration When you pass `indexerApiEndpoint` to `getUmbraClient`, the SDK automatically constructs four internal providers on the client: * `client.fetchMerkleProof` — single-leaf Merkle proof fetcher (`GET /v1/trees/{tree_index}/proof/{insertion_index}`). * `client.fetchBatchMerkleProof` — per-batch Merkle proof fetcher (`POST /v1/trees/{tree_index}/proofs`). Pass to burner factories. * `client.fetchUtxoData` — Stealth Pool Note data fetcher (`GET /v1/utxos/...`). Field name retains the legacy spelling for backwards compatibility; the V18 scanner deps override is `fetchStealthPoolNoteData`. * `client.fetchTreeSummary` — tree summary fetcher. The scanner factory reads `client.fetchUtxoData` and `client.fetchTreeSummary` internally. The burner factories consume `fetchBatchMerkleProof` (pass it via `deps.fetchBatchMerkleProof = client.fetchBatchMerkleProof!`). You do not need to call the indexer directly when using the SDK. ## Planned Improvements The SDK currently calls the indexer directly using the `indexerApiEndpoint` you provide. We are planning to migrate this to an **IP Obfuscation Service** that will proxy indexer requests through an anonymising relay, so that fetching your notes does not leak your IP address to the indexer. This will be a transparent upgrade — the SDK interface will not change. ## API Reference Aggregate statistics for the entire Stealth Pool Note index. Basic, detailed, liveness, and readiness health checks. Current root hash, leaf count, and note count for a specific tree. Paginated Stealth Pool Note records for a specific tree. Generate an inclusion proof for a specific leaf. Retrieve multiple proofs atomically under the same tree root — what the burner factory uses. Paginated note queries spanning all trees. Fetch a single note by absolute index. # Introduction Source: https://sdk.umbraprivacy.com/introduction Umbra privacy protocol for Solana: shield SPL/Token-2022 balances via Arcium MPC EncryptedTokenAccounts, transfer anonymously through a Stealth Pool, and grant selective compliance access. ## What is Umbra? Umbra adds a privacy layer on top of standard Solana [SPL and Token-2022 tokens](https://spl.solana.com/token). It lets you: * **Shield balances** — move tokens from a public Associated Token Account (ATA) into an **EncryptedTokenAccount (ETA)** where the balance is hidden from everyone except authorized viewers. * **Transfer anonymously** — write a **Stealth Pool Note** into a shared mixer tree, then **burn** it from any wallet with no on-chain linkage to the sender. * **Control who sees what** — grant selective viewing access to auditors or compliance systems without exposing your full history. Everything runs on Solana using a combination of on-chain programs and [Arcium](https://arcium.com) multi-party computation (MPC) for the confidential arithmetic. You do **not** need to understand MPC or zero-knowledge proofs to use Umbra. The SDK handles all cryptographic operations for you. ## Key terms The SDK and these docs use this vocabulary throughout: * **ATA** — `AssociatedTokenAccount`. A standard SPL Associated Token Account holding plaintext tokens. * **ETA** — `EncryptedTokenAccount`. An Umbra-managed account whose balance is encrypted. Two modes: **MXE-only** (Arcium decrypts) and **Shared** (the user can also decrypt locally). * **Stealth Pool Note** — a leaf in the shared mixer tree. On-chain it is a commitment in the Indexed Merkle Tree; the SDK surfaces it as a "note". * **Burn** — consuming a note: it reveals the note's nullifier on-chain and credits the destination balance. The on-chain instruction is `burn_*` and the relayer endpoint is `/v1/claims` — the wire protocol uses these lower-level nouns, and a handful of TS aliases bridge them so the relayer client plugs into the burner factories unchanged. You should not need to touch any of that as an SDK consumer. ## Core Concepts at a Glance Shield any SPL or Token-2022 token balance in an EncryptedTokenAccount. Only you — and anyone you explicitly grant access to — can see the amount. Break the on-chain link between sender and recipient using a shared Merkle tree of Stealth Pool Notes and zero-knowledge burn proofs. One TypeScript package, `@umbra-privacy/sdk`. The ZK prover, indexer client, and relayer client all ship as subpath modules — no separate `@umbra-privacy/web-zk-prover` package. A read-only REST API for Stealth Pool Note records and Merkle proofs — used internally by the SDK and available for direct integration. ## How It Fits Into Your Stack Umbra is designed to slot into an existing Solana application. You bring your wallet adapter; Umbra handles the rest. ``` Your App └── Wallet (Phantom, Solflare, etc.) └── Umbra SDK (@umbra-privacy/sdk) ├── Solana RPC — sends and confirms transactions ├── Arcium MPC — performs confidential computation off-chain ├── Umbra Indexer — indexes Stealth Pool Notes + Merkle proofs ├── Umbra Relayer — submits burn transactions for the user └── ZK prover — Groth16 (snarkjs), runs in a Web Worker ``` ## Program IDs The Umbra on-chain program is deployed at different addresses per network: * **Mainnet:** `UMBRAD2ishebJTcgCLkTkNUx1v3GyoAgpTRPeWoLykh` * **Devnet:** `DSuKkyqGVGgo4QtPABfxKJKygUDACbUhirnuv63mEpAJ` The SDK resolves the correct program address automatically based on the `network` parameter. ## Supported Networks * `mainnet` — Production. wSOL, USDC, USDT, UMBRA, CASH, ZINC. * `devnet` — Development and integration testing. wSOL only. * `localnet` — Local validator. wSOL only. ## Next Steps Install the SDK and run your first deposit + burn in under 5 minutes. Understand the privacy model before you start building. # Migration: V4 → V5 Source: https://sdk.umbraprivacy.com/migrations/v4-to-v5 Step-by-step migration from @umbra-privacy/sdk@4.x to @umbra-privacy/sdk@5.x: package consolidation, subpath imports, factory renames, zero-arg scanner, burner relayer-dep shape, store adapters. V5 is a vocabulary + ergonomics overhaul. The on-chain protocol and wire formats are **unchanged** — relayer paths are still `/v1/claims`, the on-chain instruction is still `burn_*` under the hood, and a handful of TypeScript aliases bridge the two namespaces so most callers can rename without rewriting logic. This page walks you through every breaking change. Work top-to-bottom; nothing later depends on something earlier you skipped. **Current release: `@umbra-privacy/sdk@5.0.0-rc.6`** (pins `@umbra-privacy/umbra-codama@3.0.0-rc.6`). Nothing extra to wire up — the earlier `rc.3`/`rc.4` workarounds are gone: the codama PDA bug (`ConstraintSeeds 2006` on deposit) and the columnar `scan()` BigInt bug are **both fixed in rc.6**. No package-manager override, no `postinstall` dist patch. ## TL;DR 1. Bump the package: `@umbra-privacy/sdk@4.x → @umbra-privacy/sdk@5.0.0-rc.6`. Pin an **exact** version (codama is pinned by the SDK — no override needed). 2. Delete `@umbra-privacy/web-zk-prover` from `package.json`. The prover ships inside the SDK at `@umbra-privacy/sdk/zk-prover`. 3. Move every operation factory import from the main barrel to its **named subpath** (`/registration`, `/deposit`, `/withdrawal`, `/burn`, `/query`, `/conversion`, `/compliance`, `/account`). 4. Rename factories per the table below (UTXO → Stealth Pool Note, claim → burn, PublicBalance → ATA, encrypted balance → ETA). 5. Rewrite the scanner call: zero-arg in V5, results bucketed by `(kind, source)` under long-form keys. 6. Burner factories take a new `relayer` dep — `{ submitBurn, pollBurnStatus, getRelayerAddress }` — these are TypeScript aliases of the relayer client's existing `submitClaim` / `pollClaimStatus` / `getRelayerAddress`, so plug in the same client unchanged. 7. (Browser) Wire `utxoDataStore` + `nullifierStore` from `@umbra-privacy/sdk/store-adapters`. Without them every `scan()` walks every active tree from genesis. 8. (Mainnet) Mint list adds `CASH`. (Devnet) Mint list drops to wSOL only. The rest of this page expands each step with before/after code. *** ## 1. Package consolidation **V4** ```jsonc theme={null} // package.json { "dependencies": { "@umbra-privacy/sdk": "^4.0.0", "@umbra-privacy/web-zk-prover": "^2.0.0" } } ``` ```typescript theme={null} // app code import { getCreateReceiverClaimableUtxoFromPublicBalanceProver } from "@umbra-privacy/web-zk-prover"; import { getCdnZkAssetProvider } from "@umbra-privacy/web-zk-prover/cdn"; ``` **V5** ```jsonc theme={null} { "dependencies": { "@umbra-privacy/sdk": "5.0.0-rc.6" } } ``` ```typescript theme={null} // Creator prover factories were renamed in V5: import { getATAIntoStealthPoolNoteCreatorProver } from "@umbra-privacy/sdk/zk-prover"; // Burner prover factories kept V4 spellings: import { getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver } from "@umbra-privacy/sdk/zk-prover"; import { getCdnZkAssetProvider } from "@umbra-privacy/sdk/zk-prover/cdn"; ``` **Why pin?** ZK assets are versioned in lockstep with the package. A floating version (`^5.0.0`) can resolve a manifest that points at a different circuit hash than the one your wallet's master seed was set up for. **Prover naming:** The **burner** prover factories (`getClaim…`) retained their V4 spellings. The **creator** prover factories were renamed — `getATAIntoStealthPoolNoteCreatorProver` (ATA-source) and `getETAIntoStealthPoolNoteCreatorProver` (ETA-source). If you also imported the package's CJS build paths or had a webpack alias for `web-zk-prover`, remove them. If your scaffold has a Next.js `transpilePackages` block, remove the `web-zk-prover` entry: ```typescript theme={null} // next.config.ts — before transpilePackages: ["@umbra-privacy/sdk", "@umbra-privacy/web-zk-prover"] // after transpilePackages: ["@umbra-privacy/sdk"] ``` *** ## 2. Subpath imports The V5 main barrel re-exports only: * `getUmbraClient`, the four signer factories, the error hierarchy, `Result` helpers. * Everything from `infrastructure/{arcium, indexer, relayer, solana, zk-prover}` — including `getUmbraRelayer`, `getUmbraIndexer`, RPC providers, transaction forwarders. **Every operation factory now lives behind a named subpath.** | What you import | V4 path | V5 path | | ------------------------------------------------------ | ---------------------------------- | ------------------------------------------------------------------- | | `getUmbraClient`, signer factories, `getUmbraRelayer` | `@umbra-privacy/sdk` | `@umbra-privacy/sdk` (unchanged) | | `getUserRegistrationFunction` | `@umbra-privacy/sdk` | `@umbra-privacy/sdk/registration` | | Direct depositor + 4 note creators | `@umbra-privacy/sdk` | `@umbra-privacy/sdk/deposit` | | Direct withdrawer | `@umbra-privacy/sdk` | `@umbra-privacy/sdk/withdrawal` | | Scanner + 3 burner factories + `enrichWithMerkleProof` | `@umbra-privacy/sdk` | `@umbra-privacy/sdk/burn` | | Account + balance queriers | `@umbra-privacy/sdk` | `@umbra-privacy/sdk/query` | | MXE→Shared converter | `@umbra-privacy/sdk` | `@umbra-privacy/sdk/conversion` | | Compliance grant + reencrypt + querier | `@umbra-privacy/sdk` | `@umbra-privacy/sdk/compliance` | | Staged-token recoverers, key rotators, maintenance | `@umbra-privacy/sdk` | `@umbra-privacy/sdk/account` | | ZK provers | `@umbra-privacy/web-zk-prover` | `@umbra-privacy/sdk/zk-prover` | | CDN asset provider | `@umbra-privacy/web-zk-prover/cdn` | `@umbra-privacy/sdk/zk-prover/cdn` | | Browser store adapters | (new) | `@umbra-privacy/sdk/store-adapters` | | `UMBRA_MESSAGE_TO_SIGN` | `@umbra-privacy/sdk` | `@umbra-privacy/sdk/shared` | | Branded type helpers (`createU64`, etc.) | `@umbra-privacy/sdk` | `@umbra-privacy/sdk/types` | | `generateRandomNonce` | `@umbra-privacy/sdk/utils` | `@umbra-privacy/sdk/arcium` (also re-exported from the main barrel) | | Function-type aliases | `@umbra-privacy/sdk/interfaces` | the same subpath as the factory | | Error classes + `is*` guards | `@umbra-privacy/sdk/errors` | `@umbra-privacy/sdk/errors` (unchanged) | | `BPS_DIVISOR`, mint lists | `@umbra-privacy/sdk` | `@umbra-privacy/sdk/constants` | There is no `@umbra-privacy/sdk/interfaces` re-export in V5. Function-type aliases (`SelfBurnableStealthPoolNoteFromEncryptedTokenAccountCreatorFunction`, etc.) ship alongside their factories — import them from the same subpath. *** ## 3. Factory renames Names lengthened to spell out the data flow end-to-end. The vocabulary swap: * **`UTXO`** → **`Stealth Pool Note`** (in factory names + result types; on-chain commitments and the indexer route are still called UTXOs). * **`Claim`** → **`Burn`** (in factory names + lifecycle hooks; the wire endpoint is still `/v1/claims`). * **`PublicBalance`** → **`AssociatedTokenAccount` (ATA)** in factory names. * **`EncryptedBalance`** → **`EncryptedTokenAccount` (ETA)** in factory names. ### Deposits | V4 | V5 (subpath `/deposit`) | | ----------------------------------------------------------- | ---------------------------------------------------------- | | `getPublicBalanceToEncryptedBalanceDirectDepositorFunction` | `getATAIntoETADirectDepositorFunction` | | `getPublicBalanceToSelfClaimableUtxoCreatorFunction` | `getATAIntoSelfBurnableStealthPoolNoteCreatorFunction` | | `getPublicBalanceToReceiverClaimableUtxoCreatorFunction` | `getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction` | | `getEncryptedBalanceToSelfClaimableUtxoCreatorFunction` | `getETAIntoSelfBurnableStealthPoolNoteCreatorFunction` | | `getEncryptedBalanceToReceiverClaimableUtxoCreatorFunction` | `getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction` | ### Withdrawal | V4 | V5 (subpath `/withdrawal`) | | ------------------------------------------------------------ | --------------------------------- | | `getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction` | `getETAIntoATAWithdrawerFunction` | ### Scan + burn | V4 | V5 (subpath `/burn`) | | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `getClaimableUtxoScannerFunction` | `getBurnableStealthPoolNoteScannerFunction` | | `getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction` | `getReceiverBurnableStealthPoolNoteIntoETABurnerFunction` | | `getSelfClaimableUtxoToEncryptedBalanceClaimerFunction` | `getSelfBurnableStealthPoolNoteIntoETABurnerFunction` | | `getSelfClaimableUtxoToPublicBalanceClaimerFunction` | `getSelfBurnableStealthPoolNoteIntoATABurnerFunction` | | *(new in V5)* | `getTransferorFunction` (in `@umbra-privacy/sdk/transfer`) — ETA-to-ETA direct transfer, 8 variants | The `getReceiverClaimableUtxoToPublicBalanceClaimerFunction` variant from V4 is **not yet shipped in V5** — the on-chain instruction exists, but the SDK factory has not been written. Use receiver-into-ETA, then a regular withdrawal, until it lands. ### Conversion + key rotation | V4 | V5 | | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | `getNetworkEncryptionToSharedEncryptionConverterFunction` (main barrel) | `getNetworkEncryptionToSharedEncryptionConverterFunction` (now `@umbra-privacy/sdk/conversion`) | | `getMintEncryptionKeyRotatorFunction` (main barrel) | `getMintEncryptionKeyRotatorFunction` (name unchanged; moved to `@umbra-privacy/sdk/account`) | | `getRotateUserAccountX25519KeyFunction` | `getUserEncryptionKeyRotatorFunction` (in `@umbra-privacy/sdk/account`) | | `getRotateMvkX25519KeyFunction` | `getMasterViewingKeyRotatorFunction` (in `@umbra-privacy/sdk/account`) | ### Account recovery + maintenance | V4 | V5 (subpath `/account`) | | --------------------------------------------------- | ------------------------------------ | | `getClaimStagedSolFromPoolFunction` | `getStagedSolRecovererFunction` | | `getClaimStagedSplFromPoolFunction` | `getStagedSplRecovererFunction` | | `getUpdateRandomGenerationSeedFunction` | `getUserEntropySeedRotatorFunction` | | `getUpdateTokenAccountRandomGenerationSeedFunction` | `getTokenEntropySeedRotatorFunction` | ### Compliance | V4 | V5 (subpath `/compliance`) | | ------------------------------------------------------- | ------------------------------------------------------------------- | | `getComplianceGrantIssuerFunction` | `getComplianceGrantIssuerFunction` (unchanged) | | `getComplianceGrantRevokerFunction` | `getComplianceGrantRevokerFunction` (unchanged) | | `getUserComplianceGrantQuerierFunction` | `getUserComplianceGrantQuerierFunction` (unchanged) | | `getQueryNetworkMxeComplianceGrantFunction` | `getNetworkComplianceGrantQuerierFunction` | | `getQueryNetworkSharedComplianceGrantFunction` | `getSharedComplianceGrantQuerierFunction` | | `getReencryptMxeCiphertextsNetworkGrantFunction` | `getNetworkCiphertextReencryptorForNetworkGrantFunction` | | `getSharedCiphertextReencryptorForUserGrantFunction` | `getSharedCiphertextReencryptorForUserGrantFunction` (unchanged) | | `getSharedCiphertextReencryptorForNetworkGrantFunction` | `getSharedCiphertextReencryptorForNetworkGrantFunction` (unchanged) | *** ## 4. Scanner API rewrite V4's scanner took three positional args (`treeIndex`, `start`, `end?`) and returned proof-bundled UTXO data under four short-form keys. V5's scanner is **zero-arg**: it walks every active tree automatically, with the cursor persisted in `client.utxoDataStore`. The scanner **no longer bundles Merkle proofs**. 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. The burner factory handles the per-batch fetch internally; you do not need to call `enrichWithMerkleProof` yourself unless you are building a custom burn pipeline. **V4** ```typescript theme={null} import { getClaimableUtxoScannerFunction } from "@umbra-privacy/sdk"; const scan = getClaimableUtxoScannerFunction({ client }); const result = await scan(0, 0); // (treeIndex, startInsertionIndex, endInsertionIndex?) console.log("Self-burnable from ETA:", result.selfBurnable.length); console.log("Receiver from ETA:", result.received.length); console.log("Self-burnable from ATA:", result.publicSelfBurnable.length); console.log("Receiver from ATA:", result.publicReceived.length); // Pass directly to a V4 claimer — the UTXOs come with Merkle proofs attached. await claim(result.received); ``` **V5** ```typescript theme={null} import { getBurnableStealthPoolNoteScannerFunction } from "@umbra-privacy/sdk/burn"; const scan = getBurnableStealthPoolNoteScannerFunction({ client }); const result = await scan(); // zero-arg console.log("Self-burnable from ETA: ", result.etaToStealthPoolSelfBurnable.length); console.log("Receiver from ETA: ", result.etaToStealthPoolReceiverBurnable.length); console.log("Self-burnable from ATA: ", result.ataToStealthPoolSelfBurnable.length); console.log("Receiver from ATA: ", result.ataToStealthPoolReceiverBurnable.length); console.log("Scanned trees: ", result.scannedTrees.length); // Pass directly to a V5 burner — it fetches per-batch proofs internally. await burn(result.etaToStealthPoolReceiverBurnable); ``` Bucket key rename map: | V4 result key | V5 result key | | -------------------- | ---------------------------------- | | `selfBurnable` | `etaToStealthPoolSelfBurnable` | | `received` | `etaToStealthPoolReceiverBurnable` | | `publicSelfBurnable` | `ataToStealthPoolSelfBurnable` | | `publicReceived` | `ataToStealthPoolReceiverBurnable` | The result object also gains `scannedTrees: readonly ScannedTreeProgress[]` so you can display per-tree progress in your UI. *** ## 5. Burner factory `relayer` dep V5's burner factories accept a new `relayer` dep — a three-property adapter built from the relayer client. The property names use the V5 vocabulary but are **TypeScript aliases** of the claim equivalents, so the relayer client's existing methods plug in directly. **V4** ```typescript theme={null} import { getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction, getUmbraRelayer, } from "@umbra-privacy/sdk"; import { getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver } from "@umbra-privacy/web-zk-prover"; const relayer = getUmbraRelayer({ apiEndpoint }); const zkProver = getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(); const claim = getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction( { client }, { zkProver, relayer }, // V4: pass the relayer object as-is ); await claim(utxos); ``` **V5** ```typescript theme={null} import { getReceiverBurnableStealthPoolNoteIntoETABurnerFunction } from "@umbra-privacy/sdk/burn"; import { getUmbraRelayer } from "@umbra-privacy/sdk"; import { getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver } from "@umbra-privacy/sdk/zk-prover"; const r = getUmbraRelayer({ apiEndpoint }); const zkProver = getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(); const burn = getReceiverBurnableStealthPoolNoteIntoETABurnerFunction( { client }, { fetchBatchMerkleProof: client.fetchBatchMerkleProof!, zkProver, relayer: { submitBurn: r.submitClaim, // alias: BurnSubmitter ≡ ClaimSubmitter pollBurnStatus: r.pollClaimStatus, // alias: BurnStatusPoller ≡ ClaimStatusPoller getRelayerAddress: r.getRelayerAddress, }, }, ); await burn(notes); ``` Two new required deps to be aware of: * **`fetchBatchMerkleProof`** — pass `client.fetchBatchMerkleProof` (auto-wired when `indexerApiEndpoint` is set on the client). Replaces V4's pre-fetched proof bundle. * **`relayer: { submitBurn, pollBurnStatus, getRelayerAddress }`** — explicit three-property adapter. V4 accepted the full relayer client; V5 wants only these three methods. ### Burn result shape V4 claimers returned `{ signatures: Record }`. V5 burners return `BurnStealthPoolNoteIntoETAResult` (or `BurnStealthPoolNoteIntoATAResult` for the ATA variant): ```typescript theme={null} interface BurnStealthPoolNoteIntoETAResult { readonly signatures: readonly TransactionSignature[]; // from OperationOutcome — every tx submitted readonly batches: Map; // keyed by batch index } interface BurnBatchResult { readonly requestId: string; // relayer-assigned tracking ID readonly status: BurnStatus; readonly txSignature?: string; // on-chain burn tx (when landed) readonly callbackSignature?: string; // MPC callback tx (when finalised) readonly resolvedVariant?: string; // e.g. "claim_into_existing_shared_balance_v18" readonly failureReason?: string | null; readonly stealthPoolNoteIds?: readonly string[]; // "treeIndex:leafIndex" pairs } type BurnStatus = | "received" | "validating" | "offsets_reserved" | "building_tx" | "tx_built" | "submitting" | "submitted" | "awaiting_callback" | "callback_received" | "finalizing" | "completed" | "failed" | "timed_out" | "refunded"; ``` `completed` / `callback_received` indicate success. The signature to surface to the user is `callbackSignature ?? txSignature`. A `failureReason` containing `NullifierAlreadyBurnt` is **idempotent success** — the burner factory handles this internally. ### Burner function signature V5 burners take **three** positional args (one more than V4): ```typescript theme={null} type ReceiverBurnerFn = ( stealthPoolNotes: readonly (DecryptedStealthPoolNoteData & { kind: "receiver-burnable" })[], optionalData?: OptionalData32, microLamportsPerAcu?: MicroLamportsPerAcu, // NEW in V5 — per-call priority fee ) => Promise; ``` ### Batching is unchanged conceptually * **Receiver-burnable → ETA** batches **natively**: groups notes by `destinationAddress`, chunks to ≤5 per proof. Pass the whole array. * **Self-burnable → ETA / ATA** has `MAX_NOTES_PER_PROOF = 1`. The SDK loops internally — caller still passes an array. *** ## 6. Store adapters (browser) V4 had no opinionated cursor / nullifier store — every scanner call walked every tree from genesis. V5 ships `createBrowserStorageBackend`, `createShardedUtxoDataStore`, and `createShardedNullifierStore` for browser persistence; and `createFileStorageBackend` for Node.js file-backed persistence. **On the browser, wire these.** Without them every `scan()` re-scans every active tree from genesis — fine on devnet, crippling on mainnet. ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; import { createBrowserStorageBackend, createShardedUtxoDataStore, createShardedNullifierStore, } from "@umbra-privacy/sdk/store-adapters"; const storageBackend = createBrowserStorageBackend({ dbName: `umbra-${walletPubkey}` }); const utxoDataStore = createShardedUtxoDataStore({ storageBackend }); const nullifierStore = createShardedNullifierStore({ storageBackend }); const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl, indexerApiEndpoint, relayerApiEndpoint, utxoDataStore, // NEW in V5 nullifierStore, // NEW in V5 }); ``` Key the IndexedDB database **per-wallet** (e.g. `dbName: \`umbra-\$\`\`). A wallet swap mid-session must not load the previous wallet's stores. On Node.js or short-lived scripts, you can omit the stores — the scanner falls back to in-memory state. *** ## 7. Mint list changes * **Mainnet** gained `CASH` (`CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH`). Mainnet list (5): wSOL, USDC, USDT, UMBRA, CASH. * **Devnet** dropped to **wSOL only**. * **Localnet** stayed wSOL only. If your code hard-codes a mint allowlist, update it. The authoritative source at runtime is `relayer.getSupportedMints()` — call it once at app boot and cache for the session. *** ## 8. `UMBRA_MESSAGE_TO_SIGN` moved The deterministic consent message used for master-seed derivation now ships under the `/shared` subpath: ```typescript theme={null} // V4 import { UMBRA_MESSAGE_TO_SIGN } from "@umbra-privacy/sdk"; // V5 import { UMBRA_MESSAGE_TO_SIGN } from "@umbra-privacy/sdk/shared"; ``` The **bytes are unchanged** — your existing users' master seeds re-derive identically. Only the import path changed. *** ## 9. Registration hooks rewrite V5 registration replaces V4's `callbacks: { pre, post }` shape with `hooks: RegistrationHooks` — named per-step slots with `{ onPreSend, onPostSend, onSkipped }` plus top-level phase hooks. The on-chain instructions also changed: V4 emitted up to **three** transactions (account init, X25519 key registration, user commitment registration). V5 has the same three logical steps, but the underlying instructions now `init_if_needed` the user account, so the init step is usually folded into the X25519 step when account creation and key registration happen together. V5 registration hooks expose **three** skippable slots — each fires `onSkipped` when on-chain state already satisfies it: * `initUserAccount` — base `EncryptedUserAccount` PDA. * `registerX25519PublicKey` — X25519 token-encryption key. * `registerAnonymousUsage` — user-commitment registration via MPC + ZK. ```typescript theme={null} // V4 — callbacks: { pre, post } shape await register({ confidential: true, anonymous: true, callbacks: { userAccountInitialisation: { pre: async (tx) => …, post: async (tx, sig) => … }, registerX25519PublicKey: { pre: async (tx) => …, post: async (tx, sig) => … }, registerUserForAnonymousUsage: { pre: async (tx) => …, post: async (tx, sig) => … }, }, }); // V5 — hooks: RegistrationHooks shape (renamed slot + new hook event shape) await register({ confidential: true, anonymous: true, hooks: { // Top-level phase hooks (new in V5) onValidationStart: async () => …, onAccountFetchComplete: async ({ userAccountExists, hasX25519Key, isAnonymous }) => …, onKeyDerivationComplete: async ({ elapsedMs }) => …, // Per-step slots — { onPreSend, onPostSend, onSkipped } initUserAccount: { onSkipped: async ({ reason }) => …, onPreSend: async ({ signedTransaction }) => …, onPostSend: async ({ signature }) => …, }, registerX25519PublicKey: { onSkipped: async ({ reason }) => …, onPreSend: async () => …, onPostSend: async ({ signature }) => …, }, registerAnonymousUsage: { onSkipped: async ({ reason }) => …, onPreSend: async () => …, onPostSend: async ({ signature }) => …, }, onComplete: async (result) => …, onError: async ({ phase, error }) => …, }, }); ``` V4 → V5 slot rename map: | V4 callbacks slot | V5 hooks slot | | ------------------------------- | ------------------------------------- | | `userAccountInitialisation` | `initUserAccount` | | `registerX25519PublicKey` | `registerX25519PublicKey` (unchanged) | | `registerUserForAnonymousUsage` | `registerAnonymousUsage` | Event-shape changes: * `pre(tx)` → `onPreSend({ signedTransaction })`. * `post(tx, sig)` → `onPostSend({ signature })`. * New: `onSkipped({ reason })` fires when a step is a no-op because on-chain state already satisfies it. * New: anonymous-usage step is an MPC step (`SkippableMpcTransactionStepHooks`) and additionally exposes `onMonitorStarted`, `onProgress`, `onFinalized`, `onRentReclaimSubmitted`, `onRentReclaimError` (see `MpcTransactionStepHooks`). *** ## 10. Options-shape change: `callbacks: TransactionCallbacks` → `hooks` (per-operation) V4 deposits, withdrawals, conversions, and registrations all took a generic `callbacks: { pre, post }` object. V5 replaces it with a per-operation **`hooks`** object whose shape is specific to that operation — named per-phase / per-step slots, each with a typed event object. V4 → V5 option-field changes on the **direct deposit / withdraw**: | V4 option | V5 | | ---------------------------------- | ----------------------------------------------------------------------------------------------- | | `priorityFees?: U64` | removed (priority fees are factory-time config) | | `purpose?: number` | removed | | `awaitCallback?: boolean` | removed (callback waiting is always on; the result's `callback?` field is populated on success) | | `skipPreflight?: boolean` | removed | | `maxRetries?: number` | removed (configure `deps.transactionForwarder` at factory time) | | `epochInfoCommitment?: Commitment` | retained on deposit; removed on withdrawal | | `callbacks?: TransactionCallbacks` | replaced by `hooks?: …DirectDepositHooks` / `…DirectWithdrawHooks` | | (new) | `computationOffset?: U64` (withdrawal) | | (new) | `mpcCallbackDataOffset?: U128` (withdrawal) | | (new) | `feeVaultOffset?: U128` (withdrawal) | | (new) | `destinationProgram?: Address` (withdrawal observer-CPI) | V4 → V5 option-field changes on **conversion**: | V4 | V5 | | ----------------------------------------- | ------------------------------------------------------------ | | `convert(mints, optionalData, callbacks)` | `convert(mints, optionalData, hooks?, microLamportsPerAcu?)` | V4 → V5 result-shape change on **deposit / withdraw**: | V4 result field | V5 result field | | --------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | `queueSignature: TransactionSignature` | `queueSignature: TransactionSignature` (unchanged) | | `callbackStatus?: "finalized" \| "pruned" \| "timed-out"` | flattened into `callback?: { status, signature?, elapsedMs }` (discriminated) | | `callbackSignature?: TransactionSignature` | inside `callback.signature` (when `status === "finalized"`) | | `callbackElapsedMs?: number` | inside `callback.elapsedMs` | | `rentClaimSignature?: TransactionSignature` | inside `rentClaim?: { claimed: true, signature } \| { claimed: false, reason }` | | `rentClaimError?: string` | inside `rentClaim.reason` (when `claimed: false`) | | (new) | `signatures: readonly TransactionSignature[]` — full submission list from `OperationOutcome` | *** ## 11. Error class names Error class names are **unchanged** for backwards compatibility — even though `ClaimUtxoError` is now thrown by burner factories that operate on "Stealth Pool Notes", the class kept its V4 spelling so existing `isClaimUtxoError(err)` guards still work. The `FetchUtxosStage` enum gained two values in V5 (no V4 equivalents): * `"proof-fetch"` — the burner's per-batch Merkle proof fetch failed. * `"proof-enrichment"` — `enrichWithMerkleProof` failed to attach a proof entry to a scanned note. All other operation `stage` enums are unchanged. *** ## End-to-end before/after A complete "scan → burn into ETA" flow. **V4** ```typescript theme={null} import { getUmbraClient, getClaimableUtxoScannerFunction, getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction, getUmbraRelayer, } from "@umbra-privacy/sdk"; import { getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver } from "@umbra-privacy/web-zk-prover"; const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl, indexerApiEndpoint, }); const scan = getClaimableUtxoScannerFunction({ client }); const result = await scan(0, 0); const relayer = getUmbraRelayer({ apiEndpoint }); const claim = getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction( { client }, { zkProver: getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(), relayer }, ); if (result.received.length > 0) { const out = await claim(result.received); console.log("Signatures:", out.signatures); } ``` **V5** ```typescript theme={null} import { getUmbraClient, getUmbraRelayer } from "@umbra-privacy/sdk"; import { createBrowserStorageBackend, createShardedUtxoDataStore, createShardedNullifierStore, } from "@umbra-privacy/sdk/store-adapters"; import { getBurnableStealthPoolNoteScannerFunction, getReceiverBurnableStealthPoolNoteIntoETABurnerFunction, } from "@umbra-privacy/sdk/burn"; import { getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver } from "@umbra-privacy/sdk/zk-prover"; const storageBackend = createBrowserStorageBackend({ dbName: `umbra-${signer.address}` }); const utxoDataStore = createShardedUtxoDataStore({ storageBackend }); const nullifierStore = createShardedNullifierStore({ storageBackend }); const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl, indexerApiEndpoint, relayerApiEndpoint, utxoDataStore, nullifierStore, }); const scan = getBurnableStealthPoolNoteScannerFunction({ client }); const result = await scan(); const r = getUmbraRelayer({ apiEndpoint: relayerApiEndpoint }); const burn = getReceiverBurnableStealthPoolNoteIntoETABurnerFunction( { client }, { fetchBatchMerkleProof: client.fetchBatchMerkleProof!, zkProver: getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(), relayer: { submitBurn: r.submitClaim, pollBurnStatus: r.pollClaimStatus, getRelayerAddress: r.getRelayerAddress, }, }, ); const notes = result.etaToStealthPoolReceiverBurnable; if (notes.length > 0) { const out = await burn(notes); for (const [batchIndex, b] of out.batches) { console.log( "Batch", batchIndex, "status:", b.status, "id:", b.requestId, "sig:", b.callbackSignature ?? b.txSignature, ); } } ``` *** ## Compatibility checklist After migrating, verify each: * [ ] `package.json` pins `@umbra-privacy/sdk` to an exact version; `@umbra-privacy/web-zk-prover` is removed. * [ ] No imports from `@umbra-privacy/web-zk-prover` (replaced with `@umbra-privacy/sdk/zk-prover`). * [ ] No imports of `@umbra-privacy/sdk/interfaces` or `@umbra-privacy/sdk/utils` (V4-only subpaths). * [ ] Every V4 factory name above has been renamed to its V5 counterpart. * [ ] `getUmbraClient(...)` includes `relayerApiEndpoint` and, on browser, `utxoDataStore` + `nullifierStore`. * [ ] Scanner is invoked with no arguments and result keys are dereferenced with the long-form names. * [ ] Burner factories receive `fetchBatchMerkleProof: client.fetchBatchMerkleProof!` and a `relayer: { submitBurn, pollBurnStatus, getRelayerAddress }` adapter. * [ ] Mainnet mint allowlist includes `CASH`; devnet allowlist is wSOL-only. * [ ] `UMBRA_MESSAGE_TO_SIGN` is imported from `@umbra-privacy/sdk/shared`. * [ ] `getEncryptedBalanceQuerierFunction` result `switch` covers the discriminator strings `"shared"`, `"mxe"`, `"uninitialized"`, `"non_existent"` (was `{ state: "exists", data: { mode } }` in V4 — flattened in V5). * [ ] Removed `callbacks: { pre, post }` from every call site. Deposits / withdrawals / conversions use `options.hooks` (typed per-operation hooks interface); registration uses `options.hooks: RegistrationHooks` with `{ onPreSend, onPostSend, onSkipped }` per-step slots. * [ ] Removed dead options from deposit / withdraw call sites: `priorityFees`, `purpose`, `awaitCallback`, `skipPreflight`, `maxRetries`. * [ ] Update `DepositResult` / `WithdrawResult` consumers: flat `callbackSignature` / `callbackStatus` / `rentClaimSignature` fields no longer exist — read `result.callback?.signature` (only present when `result.callback?.status === "finalized"`) and `result.rentClaim?.signature` (only when `result.rentClaim?.claimed === true`). * [ ] Run the SDK against devnet and verify a full register → deposit → write note → scan → burn round-trip succeeds before deploying to mainnet. *** ## Forward-compatibility note The V5 surface is the **target shape** going forward. Future minor releases will add factories (e.g. receiver-burnable → ATA), additional store backends, and ergonomic helpers — but the existing V5 factory names, subpath layout, and `relayer` dep shape are stable. # Pricing Source: https://sdk.umbraprivacy.com/pricing Fee structure: protocol fees (BPS_DIVISOR=16384), relayer fees on burns, Stealth Pool SOL fee paid when writing a note. Covers fee calculation formulas and Token-2022 transfer fee interaction. ## Overview Umbra charges three distinct fees depending on the operation: * **Protocol fee** — a percentage of the SPL token amount, collected by the protocol on most operations. * **Relayer fee** — a percentage of the SPL token amount, collected by the relayer on burn operations. Currently 0. * **Stealth Pool SOL fee** — a fixed SOL amount paid when writing a Stealth Pool Note, used to pre-fund the eventual burn so neither the recipient nor the relayer pays SOL out of pocket at burn time. *** ## Protocol Fee The protocol fee is deducted from the SPL or Token-2022 token amount involved in each operation. **Current rates:** * Fixed base: 0 token micro-units * Proportional: 35 bps **Formula:** Umbra uses a power-of-two BPS divisor (`2^14 = 16,384`) rather than the traditional `10,000`. This means 35 bps corresponds to `35 / 16,384 ≈ 0.2136%`. ``` protocol_fee = floor(amount × 35 / 16_384) ``` ### Example — 1,000 USDC ETA → ATA withdrawal ``` 1,000 USDC = 1,000,000,000 µUSDC protocol_fee = floor(1,000,000,000 × 35 / 16,384) = 2,136,230 µUSDC ≈ 2.14 USDC net received = 1,000,000,000 - 2,136,230 = 997,863,770 µUSDC ≈ 997.86 USDC ``` ### Estimating programmatically ```typescript theme={null} import { BPS_DIVISOR } from "@umbra-privacy/sdk/constants"; const bps = 35n; function estimateProtocolFee(amount: bigint): bigint { return (amount * bps) / BPS_DIVISOR; // BPS_DIVISOR = 16_384n } const fee = estimateProtocolFee(1_000_000n); // 1 USDC (6 decimals) // fee = 1_000_000n × 35n / 16_384n = 2_136n µUSDC ``` *** ## Relayer Fee The relayer submits burn transactions on your behalf so your wallet never appears on-chain as the fee payer. In exchange, relayers can charge a base SPL fee plus a BPS rate on the burned amount. **Current rates:** both are set to 0. Burning a Stealth Pool Note incurs no SPL relayer fee today. When relayer fees are non-zero, they are applied on top of the protocol fee and deducted from the same token amount: ``` relayer_fee = RELAYER_BASE_FEE + floor(amount × RELAYER_BPS / 16_384) net = amount - protocol_fee - relayer_fee ``` *** ## Stealth Pool SOL Fee When you write a Stealth Pool Note into the mixer tree, you pay a one-time SOL fee upfront. This pre-funds the eventual burn so neither the recipient nor the relayer pays SOL out of pocket at burn time. The SOL fee covers two costs: * **Treap node rent** — Solana requires a minimum SOL balance to keep the nullifier account (a `TreapNode`) rent-exempt on-chain. Roughly the minimum rent for a 48-byte account at current Solana rates. * **Costliest burn path** — enough SOL to cover the on-chain compute and transaction costs of the most expensive possible burn route out of the pool. The fee is dynamically calculated at note-write time from the current Solana rent schedule. It is non-refundable — once inserted, the commitment is in the tree and the SOL is committed. The Stealth Pool SOL fee is denominated in SOL (lamports), not in the token being transferred. It is separate from and in addition to the protocol fee and relayer fee, which are denominated in the transferred token. *** ## Which Operations Carry Which Fees **EncryptedTokenAccount operations:** * Self-deposit (ATA → own ETA): 0 protocol fee for standard self-shielding. * Withdrawal (ETA → ATA): protocol fee applies. * Conversion (MXE-only → Shared): protocol fee applies on the converted balance. **Stealth Pool operations:** * Note creation (ETA → Stealth Pool Note): protocol fee on SPL amount + Stealth Pool SOL fee. * Note creation (ATA → Stealth Pool Note): protocol fee on SPL amount + Stealth Pool SOL fee. * Burn (note → ETA): protocol fee applies, relayer fee applies (currently 0). * Burn (note → ATA): protocol fee applies, relayer fee applies (currently 0). (Only the self-burnable → ATA variant is shipped in V18; receiver-burnable → ATA is on-chain but not yet exposed in the SDK.) *** ## Token-2022 Transfer Fees If the token mint has a Token-2022 transfer fee extension configured, that fee is deducted by the SPL program before tokens reach the Umbra pool. Umbra then measures the actual amount received and applies its protocol fee on top of that — it never charges protocol fees on the portion taken by the Token-2022 mechanism. ``` User sends: transfer_amount T22 deducts: token_transfer_fee (set on the mint, outside Umbra's control) Pool receives: actual_received = transfer_amount - token_transfer_fee Umbra deducts: protocol_fee = floor(actual_received × 35 / 16,384) EncryptedTokenAccount: actual_received - protocol_fee ``` *** ## Fee Configuration Protocol and relayer fee rates are stored in on-chain `ProtocolFeesConfiguration` and `RelayerFeesConfiguration` accounts, configurable by the pool admin and relayer operators respectively. The values above reflect the current live configuration. If you are building a production integration and need exact fee amounts for display or pre-flight checks, fetch the on-chain fee configuration accounts directly rather than hardcoding the SDK defaults. # Quickstart Source: https://sdk.umbraprivacy.com/quickstart Build directly on @umbra-privacy/sdk: register, deposit, create a Stealth Pool Note, scan, burn. ## 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`. ```bash pnpm theme={null} pnpm add @umbra-privacy/sdk@5.0.0-rc.6 snarkjs ``` ```bash npm theme={null} npm install @umbra-privacy/sdk@5.0.0-rc.6 snarkjs ``` ```bash yarn theme={null} yarn add @umbra-privacy/sdk@5.0.0-rc.6 snarkjs ``` 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](/sdk/wallet-adapters). ```typescript theme={null} import { createInMemorySigner, createSignerFromPrivateKeyBytes, } from "@umbra-privacy/sdk"; // Ephemeral — disappears when the process exits. Test/dev only. const signer = await createInMemorySigner(); // Reusable — pass your own secret bytes (e.g. from a Solana CLI keypair file). // const secret = Uint8Array.from(JSON.parse(readFileSync("keypair.json", "utf8"))); // const signer = await createSignerFromPrivateKeyBytes(secret); console.log("Wallet address:", signer.address); ``` 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). ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl: "https://api.mainnet-beta.solana.com", rpcSubscriptionsUrl: "wss://api.mainnet-beta.solana.com", indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com", // NOTE: there is NO relayerApiEndpoint here — see 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](/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`](https://github.com/umbra-defi/examples/blob/main/private-payments-nextjs/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`. ```typescript theme={null} import { getUserRegistrationFunction } from "@umbra-privacy/sdk/registration"; import { getUserRegistrationProver } from "@umbra-privacy/sdk/zk-prover"; const register = getUserRegistrationFunction( { client }, { zkProver: getUserRegistrationProver() }, ); const signatures = await register({ confidential: true, // register the X25519 key (Shared-mode balances + receiver-burnable notes) anonymous: true, // register the user commitment (anonymous mixer transfers) — needs the zkProver above }); console.log(`Registered in ${signatures.length} transaction(s)`); ``` 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. ```typescript theme={null} import { getATAIntoETADirectDepositorFunction } from "@umbra-privacy/sdk/deposit"; import { createU64 } from "@umbra-privacy/sdk/types"; import { address } from "@solana/kit"; const deposit = getATAIntoETADirectDepositorFunction({ client }); const MINT = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); // USDC (mainnet) const amount = createU64({ value: 1_000_000n }); // 1 USDC (6 decimals) const result = await deposit( signer.address, // credit your own ETA MINT, amount, ); console.log("Deposit queued:", result.queueSignature); if (result.callback?.status === "finalized") { console.log("Callback confirmed:", result.callback.signature); } ``` ## 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.) ```typescript theme={null} import { getETAIntoATAWithdrawerFunction } from "@umbra-privacy/sdk/withdrawal"; const withdraw = getETAIntoATAWithdrawerFunction({ client }); const result = await withdraw( signer.address, // destination ATA owner MINT, amount, ); console.log("Withdraw queued:", result.queueSignature); if (result.callback?.status === "finalized") { console.log("Callback confirmed:", result.callback.signature); } ``` ## 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](/concepts/utxos-and-mixer#registration-prerequisites-differ-by-variant) for the full pre-check rubric. ```typescript theme={null} import { getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction } from "@umbra-privacy/sdk/deposit"; import { getATAIntoStealthPoolNoteCreatorProver } from "@umbra-privacy/sdk/zk-prover"; import { address } from "@solana/kit"; const zkProver = getATAIntoStealthPoolNoteCreatorProver(); const createNote = getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction( { client }, { zkProver }, ); const RECIPIENT = address("RecipientWalletAddressHere"); const signatures = await createNote({ destinationAddress: RECIPIENT, mint: MINT, amount, }); console.log("Stealth Pool Note created:", signatures[0]); ``` **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. ```typescript theme={null} import { getBurnableStealthPoolNoteScannerFunction } from "@umbra-privacy/sdk/burn"; const scan = getBurnableStealthPoolNoteScannerFunction({ client }); // Zero-arg. No (treeIndex, start, end) — the cursor is managed by the SDK. const result = await scan(); console.log("Self-burnable from ETA: ", result.etaToStealthPoolSelfBurnable.length); console.log("Receiver-burnable from ETA: ", result.etaToStealthPoolReceiverBurnable.length); console.log("Self-burnable from ATA: ", result.ataToStealthPoolSelfBurnable.length); console.log("Receiver-burnable from ATA: ", result.ataToStealthPoolReceiverBurnable.length); console.log("Scanned trees:", result.scannedTrees.length); ``` 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. ```typescript theme={null} import { getReceiverBurnableStealthPoolNoteIntoETABurnerFunction, } from "@umbra-privacy/sdk/burn"; import { getUmbraRelayer } from "@umbra-privacy/sdk"; import { getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver } from "@umbra-privacy/sdk/zk-prover"; const zkProver = getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(); const r = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com" }); const burn = getReceiverBurnableStealthPoolNoteIntoETABurnerFunction( { client }, { fetchBatchMerkleProof: client.fetchBatchMerkleProof!, zkProver, relayer: { submitBurn: r.submitClaim, // alias: BurnSubmitter ≡ ClaimSubmitter pollBurnStatus: r.pollClaimStatus, // alias: BurnStatusPoller ≡ ClaimStatusPoller getRelayerAddress: r.getRelayerAddress, }, }, ); // Tag the scanned notes with their burn kind (the scanner does not). const notes = result.etaToStealthPoolReceiverBurnable.map( (n) => ({ ...n, kind: "receiver-burnable" as const }), ); if (notes.length > 0) { const out = await burn([notes[0]]); for (const [batchIndex, batch] of out.batches) { console.log( "Batch", batchIndex, "status:", batch.status, "id:", batch.requestId, "sig:", batch.callbackSignature ?? batch.txSignature, ); } } ``` 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. ```typescript theme={null} import { createInMemorySigner, getUmbraClient, getUmbraRelayer, } from "@umbra-privacy/sdk"; import { createU64 } from "@umbra-privacy/sdk/types"; import { address } from "@solana/kit"; import { getUserRegistrationFunction } from "@umbra-privacy/sdk/registration"; import { getATAIntoETADirectDepositorFunction, getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction, } from "@umbra-privacy/sdk/deposit"; import { getETAIntoATAWithdrawerFunction } from "@umbra-privacy/sdk/withdrawal"; import { getBurnableStealthPoolNoteScannerFunction, getReceiverBurnableStealthPoolNoteIntoETABurnerFunction, } from "@umbra-privacy/sdk/burn"; import { getUserRegistrationProver, getATAIntoStealthPoolNoteCreatorProver, getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver, } from "@umbra-privacy/sdk/zk-prover"; async function main() { // 1. Signer (use your wallet adapter / a persisted keypair in production). const signer = await createInMemorySigner(); // 2. Client (relayer is built separately, below). const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl: "https://api.mainnet-beta.solana.com", rpcSubscriptionsUrl: "wss://api.mainnet-beta.solana.com", indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com", }); const MINT = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); // USDC // 3. Register (idempotent; anonymous mode needs the registration prover). const register = getUserRegistrationFunction( { client }, { zkProver: getUserRegistrationProver() }, ); await register({ confidential: true, anonymous: true }); // 4. Deposit (ATA → ETA). const deposit = getATAIntoETADirectDepositorFunction({ client }); await deposit(signer.address, MINT, createU64({ value: 1_000_000n })); // 5. Withdraw (ETA → ATA). const withdraw = getETAIntoATAWithdrawerFunction({ client }); await withdraw(signer.address, MINT, createU64({ value: 1_000_000n })); // 6. ZK provers (Groth16). const createProver = getATAIntoStealthPoolNoteCreatorProver(); const burnProver = getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(); // 7. Create a receiver-burnable Stealth Pool Note. const createNote = getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction( { client }, { zkProver: createProver }, ); const RECIPIENT = address("RecipientWalletAddressHere"); await createNote({ destinationAddress: RECIPIENT, mint: MINT, amount: createU64({ value: 500_000n }) }); // 8. Scan (zero-arg, cursor in client.utxoDataStore). const scan = getBurnableStealthPoolNoteScannerFunction({ client }); const result = await scan(); // 9. Burn the first receiver-burnable note into the recipient's ETA. const r = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com" }); const burn = getReceiverBurnableStealthPoolNoteIntoETABurnerFunction( { client }, { fetchBatchMerkleProof: client.fetchBatchMerkleProof!, zkProver: burnProver, relayer: { submitBurn: r.submitClaim, pollBurnStatus: r.pollClaimStatus, getRelayerAddress: r.getRelayerAddress, }, }, ); // Tag scanned notes with their burn kind before passing to the burner. const candidates = result.etaToStealthPoolReceiverBurnable.map( (n) => ({ ...n, kind: "receiver-burnable" as const }), ); if (candidates.length > 0) { const out = await burn([candidates[0]]); for (const [, b] of out.batches) { console.log("Burn status:", b.status, "sig:", b.callbackSignature ?? b.txSignature); } } } main().catch(console.error); ``` ## Next Steps Connect Phantom, Solflare, or any Solana wallet. Anonymous transfers via Stealth Pool Notes. Read encrypted balances and account state on-chain. Recover from dropped relayer callbacks and partial burns. # Client Source: https://sdk.umbraprivacy.com/reference/client API reference: getUmbraClient(args, deps?) returns IUmbraClient. Args: signer, network, rpcUrl, offsets. No client-level commitment — per-call overrides only. ## getUmbraClient **Import:** `@umbra-privacy/sdk` ```typescript theme={null} async function getUmbraClient( args: GetUmbraClientArgs, deps?: GetUmbraClientDeps, ): Promise ``` The primary SDK entry point. Call this once per session with a connected wallet to construct the `IUmbraClient` that all factory functions consume. By default this immediately prompts the wallet to sign the master seed derivation message. Pass `deferMasterSeedSignature: true` to skip that prompt at construction time - it will be requested on the first operation that requires key material. There is no client-level `commitment` parameter. Each SDK function accepts per-call commitment overrides via its `options` argument (e.g., `accountInfoCommitment`, `epochInfoCommitment`, or `commitment`). Both default to `"confirmed"`. *** ### GetUmbraClientArgs * `signer: IUmbraSigner` — connected wallet. Must implement `signMessage` and `signTransaction`. * `network: Network` — `"mainnet"` | `"devnet"` | `"localnet"`. * `rpcUrl: string` — HTTPS RPC endpoint URL for the Solana cluster. * `rpcSubscriptionsUrl: string` — WebSocket RPC endpoint URL for transaction confirmation. * `indexerApiEndpoint?: string` — base URL for the Umbra indexer (e.g. `"https://utxo-indexer.api.umbraprivacy.com"`). Required for Stealth Pool operations (`getBurnableStealthPoolNoteScannerFunction` and all burner factories). * `relayerApiEndpoint?: string` — base URL for the Umbra relayer. Required for burning. The burner factory's `relayer` adapter is built from a relayer client constructed against this endpoint via `getUmbraRelayer({ apiEndpoint })`. * `utxoDataStore?: IUtxoDataStore` — persistent storage for scanner cursor + decrypted notes. Strongly recommended on the browser. Build via `createShardedUtxoDataStore({ storageBackend })` from `@umbra-privacy/sdk/store-adapters`. * `nullifierStore?: INullifierStore` — persistent storage for discovered nullifiers. Same source as `utxoDataStore`; use `createShardedNullifierStore({ storageBackend })`. * `deferMasterSeedSignature?: boolean` — skip the master seed signature prompt at construction time. Defaults to `false`. * `versions?` - Override version specifiers. Leave unset to use SDK defaults. * `protocol?: ProtocolVersionSpecifierFunction` * `algorithm?: AlgorithmVersionSpecifierFunction` * `scheme?: SchemeVersionSpecifierFunction` * `offsets?` - Per-key derivation path offsets (`U512`). Used for key rotation - leave unset unless rotating. * `masterViewingKey?: U512` * `poseidonPrivateKey?: U512` * `x25519UserAccountPrivateKey?: U512` * `x25519MasterViewingKeyEncryptingPrivateKey?: U512` * `mintX25519PrivateKey?: U512` * `rescueCommitmentBlindingFactor?: U512` * `randomCommitmentFactor?: U512` *** ### GetUmbraClientDeps All fields are optional. Provide overrides for custom infrastructure or testing. * `accountInfoProvider?: AccountInfoProviderFunction` - Override the default RPC-based account fetcher. * `blockhashProvider?: GetLatestBlockhash` - Override the default blockhash fetcher. * `transactionForwarder?: TransactionForwarder` - Override transaction broadcasting (e.g. for Jito bundles or custom retry logic). * `epochInfoProvider?: GetEpochInfo` - Override the epoch info fetcher (used for Token-2022 fee schedules). * `masterSeedStorage?` - Override master seed persistence. * `load?: MasterSeedLoaderFunction` - Load the cached master seed (e.g. from encrypted local storage). * `store?: MasterSeedStorerFunction` - Persist the master seed after derivation. * `generate?: MasterSeedGeneratorFunction` - Derive the master seed from a wallet signature (override the default KMAC256-based derivation). *** ### Returns `Promise` - The client configuration object. Pass `client` to every factory function. *** ### Example ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; const client = await getUmbraClient({ signer: walletAdapter, network: "mainnet", rpcUrl: "https://api.mainnet-beta.solana.com", rpcSubscriptionsUrl: "wss://api.mainnet-beta.solana.com", indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com", }); ``` *** ## IUmbraClient The read-only configuration object produced by `getUmbraClient`. All factory functions accept it as `args.client`. Do not call methods on it directly. * `signer: IUmbraSigner` - The connected wallet. * `network: Network` - The configured network. * `networkConfig: NetworkConfig` - On-chain program IDs and addresses for the configured network. * `versions: ResolvedVersions` - Resolved protocol, algorithm, and scheme version identifiers. * `offsets: ResolvedOffsets` - Resolved key derivation offsets. Commitment is not stored on the client - each SDK function accepts per-call commitment overrides. * `accountInfoProvider: AccountInfoProviderFunction` - Account data fetcher (RPC-based or overridden). * `blockhashProvider: GetLatestBlockhash` - Blockhash fetcher (RPC-based or overridden). * `transactionForwarder: TransactionForwarder` - Transaction broadcaster (WebSocket-based or overridden). * `epochInfoProvider: GetEpochInfo` - Epoch info fetcher (RPC-based or overridden). * `fetchMerkleProof?: MerkleProofFetcherFunction` — indexer-based single-leaf Merkle proof fetcher. Present only when `indexerApiEndpoint` is set. * `fetchBatchMerkleProof?: BatchMerkleProofFetcherFunction` — indexer-based per-batch Merkle proof fetcher. Pass to burner factories. Present only when `indexerApiEndpoint` is set. * `fetchUtxoData?: UtxoDataFetcherFunction` — indexer-based Stealth Pool Note data fetcher used internally by the scanner. (Field name retains its legacy spelling for backwards compatibility — the V18 scanner deps override is named `fetchStealthPoolNoteData`, see `/reference/mixer`.) Present only when `indexerApiEndpoint` is set. * `fetchTreeSummary?: TreeSummaryFetcherFunction` — indexer-based tree summary fetcher. Present only when `indexerApiEndpoint` is set. * `utxoDataStore?` / `nullifierStore?` — same instances supplied at construction, when present. * `masterSeed: { load, store, generate, getMasterSeed }` — master seed storage operations. `load` attempts to load an existing seed, `store` persists it, `generate` derives a new seed via signature, and `getMasterSeed` is a convenience method combining load and generate. *** ## IUmbraSigner The wallet interface the SDK requires. Any connected wallet that can sign messages and transactions satisfies this interface. ```typescript theme={null} interface IUmbraSigner { readonly address: Address; signTransaction(transaction: SignableTransaction): Promise; signTransactions(transactions: readonly SignableTransaction[]): Promise; signMessage(message: Uint8Array): Promise; } ``` ### address The base58-encoded Ed25519 public key that uniquely identifies this signer on the Solana network. Used as the account owner, fee payer identifier, and key lookup in signed transactions. * `address: Address` - Read-only public address. ### signTransaction Signs a compiled Solana versioned transaction. * `transaction: SignableTransaction` - The versioned transaction to sign. * Returns: `Promise` - The signed transaction ready for network submission. ### signTransactions Signs multiple transactions in a batch. Returns them in the same order with signatures added. * `transactions: readonly SignableTransaction[]` - Array of transactions to sign. * Returns: `Promise` - The signed transactions in the same order as input. ### signMessage Signs an arbitrary byte sequence. Used once per session to derive the master seed - this is the only non-transaction signature the user sees. * `message: Uint8Array` - The bytes to sign. The SDK passes a deterministic human-readable message encoding the derivation intent. * Returns: `Promise` - Contains the original message bytes, the 64-byte Ed25519 signature, and the signer's address. *** ## Network ```typescript theme={null} type Network = "mainnet" | "devnet" | "localnet"; ``` Selects the set of on-chain program IDs and addresses the SDK uses. Pass this in the `network` field of `getUmbraClient` args. # Compliance Source: https://sdk.umbraprivacy.com/reference/compliance API reference: getComplianceGrantIssuerFunction, getComplianceGrantRevokerFunction, getUserComplianceGrantQuerierFunction, re-encryption for user/network grants. Compliance grants allow authorized third parties (regulators, compliance tools) to read encrypted data. Grants are stored on-chain and come in two kinds: * **User grants** - Authorized by the user. Grants a third party access to the user's shared-mode ciphertexts. * **Network grants** - Authorized by the Arcium network. Grants access to MXE-mode or shared-mode ciphertexts without requiring user interaction. *** ## getComplianceGrantIssuerFunction **Import:** `@umbra-privacy/sdk/compliance` ```typescript theme={null} function getComplianceGrantIssuerFunction( args: GetCreateUserGrantedComplianceGrantFunctionArgs, deps?: GetCreateUserGrantedComplianceGrantFunctionDeps, ): CreateUserGrantedComplianceGrantFunction ``` Creates an on-chain compliance grant signed by the user, authorizing a third-party receiver to re-encrypt and read the user's shared-mode ciphertexts. ### GetCreateUserGrantedComplianceGrantFunctionArgs * `client: IUmbraClient` ### GetCreateUserGrantedComplianceGrantFunctionDeps * `getLatestBlockhash?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` * `masterViewingKeyX25519KeypairGenerator?: MasterViewingKeyX25519KeypairGeneratorFunction` ### Returns `CreateUserGrantedComplianceGrantFunction` ```typescript theme={null} type CreateUserGrantedComplianceGrantFunction = ( receiver: Address, granterX25519: X25519PublicKey, receiverX25519: X25519PublicKey, nonce: RescueCipherEncryptionNonce, optionalData?: OptionalData32, hooks?: ComplianceTransactionHooks, ) => Promise ``` * `receiver: Address` - The third-party account that will be authorized to call re-encryption. * `granterX25519: X25519PublicKey` - The granter's X25519 public key (MVK encrypting key). * `receiverX25519: X25519PublicKey` - The receiver's X25519 public key (the target re-encryption key). * `nonce: RescueCipherEncryptionNonce` - Grant nonce, used to differentiate multiple grants to the same receiver. ### Example ```typescript theme={null} import { getComplianceGrantIssuerFunction } from "@umbra-privacy/sdk/compliance"; const createGrant = getComplianceGrantIssuerFunction({ client }); const signature = await createGrant( receiverAddress, granterX25519PublicKey, receiverX25519PublicKey, grantNonce, ); ``` *** ## getComplianceGrantRevokerFunction **Import:** `@umbra-privacy/sdk/compliance` ```typescript theme={null} function getComplianceGrantRevokerFunction( args: GetDeleteUserGrantedComplianceGrantFunctionArgs, deps?: GetDeleteUserGrantedComplianceGrantFunctionDeps, ): DeleteUserGrantedComplianceGrantFunction ``` Revokes an existing user-granted compliance grant, removing the third-party's authorization to re-encrypt. ### GetDeleteUserGrantedComplianceGrantFunctionArgs * `client: IUmbraClient` ### GetDeleteUserGrantedComplianceGrantFunctionDeps * `getLatestBlockhash?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` * `masterViewingKeyX25519KeypairGenerator?: MasterViewingKeyX25519KeypairGeneratorFunction` ### Returns `DeleteUserGrantedComplianceGrantFunction` ```typescript theme={null} type DeleteUserGrantedComplianceGrantFunction = ( receiver: Address, granterX25519: X25519PublicKey, receiverX25519: X25519PublicKey, nonce: RescueCipherEncryptionNonce, optionalData?: OptionalData32, hooks?: ComplianceTransactionHooks, ) => Promise ``` Parameters are identical to `CreateUserGrantedComplianceGrantFunction` - pass the same values used when creating the grant. *** ## getUserComplianceGrantQuerierFunction **Import:** `@umbra-privacy/sdk/compliance` ```typescript theme={null} function getUserComplianceGrantQuerierFunction( args: GetQueryUserComplianceGrantFunctionArgs, deps?: GetQueryUserComplianceGrantFunctionDeps, ): QueryUserComplianceGrantFunction ``` Checks whether a specific user-granted compliance grant exists on-chain. ### GetQueryUserComplianceGrantFunctionArgs * `client: IUmbraClient` ### GetQueryUserComplianceGrantFunctionDeps * `accountInfoProvider?: AccountInfoProviderFunction` ### Returns `QueryUserComplianceGrantFunction` ```typescript theme={null} type QueryUserComplianceGrantFunction = ( granterX25519: X25519PublicKey, nonce: RescueCipherEncryptionNonce, receiverX25519: X25519PublicKey, ) => Promise ``` *** ## getNetworkComplianceGrantQuerierFunction **Import:** `@umbra-privacy/sdk/compliance` ```typescript theme={null} function getNetworkComplianceGrantQuerierFunction( args: { client: IUmbraClient }, deps?: { accountInfoProvider?: AccountInfoProviderFunction }, ): NetworkComplianceGrantQuerierFunction; ``` Checks whether a **network MXE** compliance grant exists for the given nonce and receiver key. Network MXE grants allow the Arcium network to re-encrypt MXE-mode ciphertexts. ### Returns ```typescript theme={null} type NetworkComplianceGrantQuerierFunction = ( nonce: RescueCipherEncryptionNonce, receiverX25519: X25519PublicKey, ) => Promise; ``` *** ## getSharedComplianceGrantQuerierFunction **Import:** `@umbra-privacy/sdk/compliance` ```typescript theme={null} function getSharedComplianceGrantQuerierFunction( args: { client: IUmbraClient }, deps?: { accountInfoProvider?: AccountInfoProviderFunction }, ): SharedComplianceGrantQuerierFunction; ``` Checks whether a **network shared** compliance grant exists. Allows the Arcium network to re-encrypt Shared-mode ciphertexts. ### Returns ```typescript theme={null} type SharedComplianceGrantQuerierFunction = ( granterX25519: X25519PublicKey, nonce: RescueCipherEncryptionNonce, receiverX25519: X25519PublicKey, ) => Promise; ``` ### QueryComplianceGrantResult Returned by all three query functions: * `{ state: "exists" }` - The grant is present on-chain. * `{ state: "non_existent" }` - No grant exists for the given parameters. *** ## getNetworkCiphertextReencryptorForNetworkGrantFunction **Import:** `@umbra-privacy/sdk/compliance` ```typescript theme={null} function getNetworkCiphertextReencryptorForNetworkGrantFunction( args: { client: IUmbraClient }, deps?: GetNetworkCiphertextReencryptorForNetworkGrantFunctionDeps, ): NetworkCiphertextReencryptorForNetworkGrantFunction; ``` Re-encrypts MXE-mode ciphertexts under a network grant, making them readable by the grant receiver. Queues an Arcium MPC computation. ### GetNetworkCiphertextReencryptorForNetworkGrantFunctionArgs * `client: IUmbraClient` ### GetNetworkCiphertextReencryptorForNetworkGrantFunctionDeps * `getLatestBlockhash?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` ### Returns `NetworkCiphertextReencryptorForNetworkGrantFunction` ```typescript theme={null} type NetworkCiphertextReencryptorForNetworkGrantFunction = ( receiverX25519Key: X25519PublicKey, nonce: RescueCipherEncryptionNonce, inputEncryptionNonce: RescueCipherEncryptionNonce, ciphertexts: readonly Uint8Array[], optionalData?: OptionalData32, hooks?: ComplianceTransactionHooks, ) => Promise ``` * `receiverX25519Key: X25519PublicKey` - The receiver's X25519 public key. * `nonce: RescueCipherEncryptionNonce` - The grant nonce identifying which network grant to use. * `inputEncryptionNonce: RescueCipherEncryptionNonce` - The nonce used when the ciphertexts were originally encrypted. * `ciphertexts: readonly Uint8Array[]` - The MXE-encrypted ciphertexts to re-encrypt. Must contain between 1 and 6 elements. *** ## getSharedCiphertextReencryptorForNetworkGrantFunction **Import:** `@umbra-privacy/sdk/compliance` ```typescript theme={null} function getSharedCiphertextReencryptorForNetworkGrantFunction( args: GetSharedCiphertextReencryptorForNetworkGrantFunctionArgs, deps?: GetSharedCiphertextReencryptorForNetworkGrantFunctionDeps, ): SharedCiphertextReencryptorForNetworkGrantFunction ``` Re-encrypts shared-mode ciphertexts under a network shared grant. ### Returns `ReencryptSharedCiphertextsNetworkGrantFunction` ```typescript theme={null} type ReencryptSharedCiphertextsNetworkGrantFunction = ( granterX25519Key: X25519PublicKey, receiverX25519Key: X25519PublicKey, nonce: RescueCipherEncryptionNonce, inputEncryptionNonce: RescueCipherEncryptionNonce, ciphertexts: readonly Uint8Array[], optionalData?: OptionalData32, hooks?: ComplianceTransactionHooks, ) => Promise ``` * `granterX25519Key: X25519PublicKey` - The granter's X25519 public key. * `receiverX25519Key: X25519PublicKey` - The receiver's X25519 public key. * `ciphertexts: readonly Uint8Array[]` - Must contain between 1 and 6 elements. *** ## getSharedCiphertextReencryptorForUserGrantFunction **Import:** `@umbra-privacy/sdk/compliance` ```typescript theme={null} function getSharedCiphertextReencryptorForUserGrantFunction( args: GetReencryptSharedCiphertextsUserGrantFunctionArgs, deps?: GetReencryptSharedCiphertextsUserGrantFunctionDeps, ): ReencryptSharedCiphertextsUserGrantFunction ``` Re-encrypts shared-mode ciphertexts under a user-granted compliance grant. ### Returns `ReencryptSharedCiphertextsUserGrantFunction` ```typescript theme={null} type ReencryptSharedCiphertextsUserGrantFunction = ( granterX25519Key: X25519PublicKey, receiverX25519Key: X25519PublicKey, nonce: RescueCipherEncryptionNonce, inputEncryptionNonce: RescueCipherEncryptionNonce, ciphertexts: readonly Uint8Array[], optionalData?: OptionalData32, hooks?: ComplianceTransactionHooks, ) => Promise ``` Identical signature to `ReencryptSharedCiphertextsNetworkGrantFunction`. The difference is which on-chain grant account is used to authorize the re-encryption. * `ciphertexts: readonly Uint8Array[]` - Must contain between 1 and 6 elements. # Conversion Source: https://sdk.umbraprivacy.com/reference/conversion API reference: getNetworkEncryptionToSharedEncryptionConverterFunction (MXE to Shared batch) + getMintEncryptionKeyRotatorFunction (X25519 key rotation per mint). ## getNetworkEncryptionToSharedEncryptionConverterFunction **Import:** `@umbra-privacy/sdk/conversion` ```typescript theme={null} function getNetworkEncryptionToSharedEncryptionConverterFunction( args: GetConvertToSharedEncryptionFunctionArgs, deps?: GetConvertToSharedEncryptionFunctionDeps, ): ConvertToSharedEncryptionFunction ``` Converts one or more encrypted token accounts from MXE-mode (network-decryptable only) to shared-mode (user-decryptable). Conversion is per-mint and involves an on-chain Arcium MPC computation. Mints that are already in shared mode, uninitialized, or non-existent are skipped rather than erroring. *** ### GetConvertToSharedEncryptionFunctionArgs * `client: IUmbraClient` ### GetConvertToSharedEncryptionFunctionDeps * `accountInfoProvider?: AccountInfoProviderFunction` * `getLatestBlockhash?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` ### Returns `ConvertToSharedEncryptionFunction` ```typescript theme={null} type ConvertToSharedEncryptionFunction = ( mints: readonly Address[], optionalData?: OptionalData32, hooks?: ConvertToSharedHooks, microLamportsPerAcu?: MicroLamportsPerAcu, ) => Promise; ``` * `mints: readonly Address[]` — token mints to convert. Processed in parallel (eligible only). * `optionalData?: OptionalData32` — 32-byte caller metadata. Pre-hashed only. * `hooks?: ConvertToSharedHooks` — per-mint and overall lifecycle hooks. * `microLamportsPerAcu?: MicroLamportsPerAcu` — optional priority fee in micro-lamports per Arcium compute unit. ### ConvertToSharedEncryptionResult ```typescript theme={null} interface ConvertToSharedEncryptionResult { readonly signatures: readonly TransactionSignature[]; // from OperationOutcome readonly converted: Map; readonly skipped: Map; } ``` * `signatures` — every transaction signature submitted across all per-mint conversions. * `converted` — mints successfully converted, with their handler-tx signature. * `skipped` — mints not converted, with the reason. ### ConvertToSharedEncryptionSkipReason * `"non_existent"` - No encrypted token account exists for this mint. * `"not_initialised"` - The encrypted token account has not been initialised. * `"already_shared"` - This mint is already in shared mode. * `"balance_not_initialised"` - The balance state is not yet initialised (e.g. no deposits have occurred). *** ### Errors Throws `ConversionError` if any non-skippable error occurs. See [Errors](./errors#conversionerror). ### Example ```typescript theme={null} import { getNetworkEncryptionToSharedEncryptionConverterFunction } from "@umbra-privacy/sdk/conversion"; const convert = getNetworkEncryptionToSharedEncryptionConverterFunction({ client }); const result = await convert([usdcMint, solMint]); for (const [mint, sig] of result.converted) { console.log(`Converted ${mint}:`, sig); } for (const [mint, reason] of result.skipped) { console.log(`Skipped ${mint}: ${reason}`); } ``` *** ## getMintEncryptionKeyRotatorFunction **Import:** `@umbra-privacy/sdk/account` ```typescript theme={null} function getMintEncryptionKeyRotatorFunction( args: GetRotateMintX25519EncryptionKeyFunctionArgs, deps?: GetRotateMintX25519EncryptionKeyFunctionDeps, ): RotateMintX25519EncryptionKeyFunction ``` Rotates the X25519 encryption key for a specific mint's shared-mode encrypted token account. Use this when the per-mint key may have been compromised. The account must already be in shared mode. *** ### GetRotateMintX25519EncryptionKeyFunctionArgs * `client: IUmbraClient` ### GetRotateMintX25519EncryptionKeyFunctionDeps * `accountInfoProvider?: AccountInfoProviderFunction` * `getLatestBlockhash?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` * `mintX25519KeypairGenerator?: MintX25519KeypairGeneratorFunction` - Override the per-mint X25519 keypair derivation. ### Returns `RotateMintX25519EncryptionKeyFunction` ```typescript theme={null} type RotateMintX25519EncryptionKeyFunction = ( mint: Address, optionalData?: OptionalData32, hooks?: RotateMintKeyHooks, microLamportsPerAcu?: MicroLamportsPerAcu, options?: RotateMintKeyOptions, ) => Promise ``` * `mint: Address` - The token mint whose X25519 key to rotate. *** ### Errors Throws `ConversionError`. See [Errors](./errors#conversionerror). ### Example ```typescript theme={null} import { getMintEncryptionKeyRotatorFunction } from "@umbra-privacy/sdk/account"; const rotateMintKey = getMintEncryptionKeyRotatorFunction({ client }); const signature = await rotateMintKey(usdcMint); ``` *** ## ConversionError Thrown by both conversion functions. Stage values: `"initialization"` | `"account-fetch"` | `"pda-derivation"` | `"instruction-build"` | `"transaction-build"` | `"transaction-compile"` | `"transaction-sign"` | `"transaction-validate"` | `"transaction-send"` See [Errors](./errors#conversionerror) for full documentation. # Deposit Source: https://sdk.umbraprivacy.com/reference/deposit V18 API reference: getATAIntoETADirectDepositorFunction + 4 Stealth Pool Note creators. All deposit-side factories live under `@umbra-privacy/sdk/deposit`. *** ## getATAIntoETADirectDepositorFunction **Import:** `@umbra-privacy/sdk/deposit` ```typescript theme={null} function getATAIntoETADirectDepositorFunction( args: { client: IUmbraClient }, deps?: GetATAIntoETADirectDepositorFunctionDeps, ): ATAIntoETADirectDepositorFunction; ``` Transfers tokens from an ATA into a destination user's ETA. The destination must be registered on Umbra. This is the primary deposit path — no ZK proof required. Follows the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern). ### Deps * `accountInfoProvider?: AccountInfoProviderFunction`. * `getLatestBlockhash?: GetLatestBlockhash`. * `transactionForwarder?: TransactionForwarder`. * `getEpochInfo?: GetEpochInfo` — required for Token-2022 transfer-fee calculation. ### Returns ```typescript theme={null} type ATAIntoETADirectDepositorFunction = ( destinationAddress: Address, mint: Address, transferAmount: U64, options?: ATAIntoETADirectDepositorOptions, ) => Promise; ``` ### Options (`ATAIntoETADirectDepositorOptions`) ```typescript theme={null} interface ATAIntoETADirectDepositorOptions { readonly optionalData?: OptionalData32; // 32-byte caller metadata (pre-hashed / pre-encrypted) readonly accountInfoCommitment?: Commitment; // default "confirmed" readonly epochInfoCommitment?: Commitment; // default "confirmed" readonly hooks?: ATAIntoETADirectDepositHooks; } ``` * `optionalData` — 32 bytes of opaque caller metadata. **Must be pre-hashed or pre-encrypted.** Default 32 zero bytes. * `accountInfoCommitment` — per-call commitment for RPC reads. * `epochInfoCommitment` — per-call commitment for epoch-info fetches (Token-2022 transfer-fee schedule). * `hooks` — per-phase lifecycle hooks. Keys: `onValidationStart`, `onValidationComplete`, `onMintFetchStart`, `onMintFetchComplete`, `onAccountFetchStart`, `onAccountFetchComplete`, `onArciumSetupStart`, `onArciumSetupComplete`, `onInstructionBuildStart`, `onInstructionBuildComplete`, `queueComputation` (`MpcTransactionStepHooks`), `onComplete`, `onError`. There are no `priorityFees`, `awaitCallback`, `skipPreflight`, `maxRetries`, or `purpose` options at call time. Priority fees and retry policy are configured at factory time via `deps.microLamportsPerAcuPolicy` and `deps.transactionForwarder`. ### DepositResult ```typescript theme={null} interface DepositResult { readonly signatures: readonly TransactionSignature[]; readonly queueSignature: TransactionSignature; readonly callback?: CallbackOutcome; readonly rentClaim?: RentClaimOutcome; } type CallbackOutcome = | { readonly status: "finalized"; readonly signature?: TransactionSignature; readonly elapsedMs: number } | { readonly status: "pruned"; readonly elapsedMs: number } | { readonly status: "timed-out"; readonly elapsedMs: number }; type RentClaimOutcome = | { readonly claimed: true; readonly signature: TransactionSignature } | { readonly claimed: false; readonly reason: string }; ``` * `signatures` — every tx signature submitted, in submission order (handler, callback, rent claim). * `queueSignature` — handler (queue computation) signature. * `callback` — present once the callback round-trip completes (always awaited). `"finalized"` exposes the MPC callback `signature`; other statuses indicate the callback did not land. * `rentClaim` — best-effort. `claimed: false` does not invalidate the deposit. ### Errors Throws `EncryptedDepositError`. See [Errors](./errors#encrypteddepositerror). ### Example ```typescript theme={null} import { getATAIntoETADirectDepositorFunction } from "@umbra-privacy/sdk/deposit"; const deposit = getATAIntoETADirectDepositorFunction({ client }); const result = await deposit(recipientAddress, mintAddress, 1_000_000n); console.log("Queue signature:", result.queueSignature); if (result.callback?.status === "finalized") { console.log("Callback signature:", result.callback.signature); } ``` *** ## getETAIntoSelfBurnableStealthPoolNoteCreatorFunction **Import:** `@umbra-privacy/sdk/deposit` ```typescript theme={null} function getETAIntoSelfBurnableStealthPoolNoteCreatorFunction( args: { client: IUmbraClient }, deps: { zkProver: IZkProverForETAIntoStealthPoolNote; /* … */ }, ): ETAIntoSelfBurnableStealthPoolNoteCreatorFunction; ``` Writes a self-burnable Stealth Pool Note funded from the caller's ETA (ETA → pool). The note can only be burnt by the writer. Two-tx MPC pipeline. `deps.zkProver` is **required** — use `getETAIntoStealthPoolNoteCreatorProver` from `@umbra-privacy/sdk/zk-prover`. ### Returns ```typescript theme={null} type ETAIntoSelfBurnableStealthPoolNoteCreatorFunction = ( args: CreateStealthPoolNoteArgs, options?: CreateStealthPoolNoteOptions, ) => Promise; ``` ### CreateStealthPoolNoteArgs * `amount: U64` — net amount locked into the note (pre-fee). * `destinationAddress: Address` — unlocker address (must equal the caller for self-burnable). * `mint: Address` — token mint address. ### CreateStealthPoolNoteOptions (ETA-source) * `generationIndex?: U256` — deterministic generation index. Default: random `U256`. Pass the same value on retry to allow the pipeline's `closeProofAccount` step to reclaim a proof-account orphan from a prior failed attempt. * `optionalData?: OptionalData32` — caller metadata. Pre-hashed only. Default 32 zero bytes. * `accountInfoCommitment?: Commitment` — per-call RPC commitment. Default `"confirmed"`. (Per-step hooks for the ETA-source pipeline are attached at factory time via `deps.hooks: ETAIntoStealthPoolNoteCreatorHooks`, not on the per-call options object.) ### Errors Throws `CreateUtxoError`. See [Errors](./errors#createutxoerror). *** ## getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction **Import:** `@umbra-privacy/sdk/deposit` Same shape as the self variant. `deps.zkProver` is `IZkProverForETAIntoStealthPoolNote` — use `getETAIntoStealthPoolNoteCreatorProver` from `@umbra-privacy/sdk/zk-prover`. **Recipient prerequisite:** must have all three registration sub-step flags (`isInitialised`, `isUserAccountX25519KeyRegistered`, `isUserCommitmentRegistered`). Pre-check with `getUserAccountQuerierFunction` from `@umbra-privacy/sdk/query` before calling — fall back to the self-burnable creator if the recipient is not ready. *** ## getATAIntoSelfBurnableStealthPoolNoteCreatorFunction **Import:** `@umbra-privacy/sdk/deposit` ```typescript theme={null} function getATAIntoSelfBurnableStealthPoolNoteCreatorFunction( args: { client: IUmbraClient }, deps: { zkProver: IZkProverForATAIntoStealthPoolNote; /* … */ }, ): ATAIntoSelfBurnableStealthPoolNoteCreatorFunction; ``` Writes a self-burnable note funded from an ATA (ATA → pool). Single-tx, no MPC. `deps.zkProver` is **required** — use `getATAIntoStealthPoolNoteCreatorProver` from `@umbra-privacy/sdk/zk-prover`. ### Returns ```typescript theme={null} type ATAIntoSelfBurnableStealthPoolNoteCreatorFunction = ( args: CreateStealthPoolNoteArgs, options?: ATAIntoStealthPoolNoteCreatorOptions, ) => Promise; ``` ### ATAIntoStealthPoolNoteCreatorOptions (ATA-source) * `generationIndex?: U256`. * `optionalData?: OptionalData32`. * `accountInfoCommitment?: Commitment` — default `"confirmed"`. * `epochInfoCommitment?: Commitment` — default `"confirmed"`. * `hooks?: ATAIntoStealthPoolNoteCreatorHooks` — per-phase + per-step lifecycle hooks. Step slot names: `closeProofAccount`, `populateProofAccount`, `createStealthPoolNote` — each `SkippableDirectTransactionStepHooks` with `{ onPreSend, onPostSend, onSkipped }`. ### Errors Throws `CreateUtxoError`. See [Errors](./errors#createutxoerror). *** ## getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction **Import:** `@umbra-privacy/sdk/deposit` Same shape as the self-ATA variant. `deps.zkProver` is `IZkProverForATAIntoStealthPoolNote` — use `getATAIntoStealthPoolNoteCreatorProver` from `@umbra-privacy/sdk/zk-prover`. **Recipient prerequisite:** must have all three registration sub-step flags. Pre-check and fall back to self-burnable when needed. *** ## EncryptedDepositError Thrown by the ATA→ETA depositor. Stage values: `"initialization"` | `"validation"` | `"mint-fetch"` | `"fee-calculation"` | `"pda-derivation"` | `"account-fetch"` | `"instruction-build"` | `"transaction-build"` | `"transaction-compile"` | `"transaction-sign"` | `"transaction-validate"` | `"transaction-send"`. See [Errors](./errors#encrypteddepositerror). *** ## CreateUtxoError Thrown by all four Stealth Pool Note creators. Stage values: `"initialization"` | `"validation"` | `"account-fetch"` | `"mint-fetch"` | `"fee-calculation"` | `"key-derivation"` | `"zk-proof-generation"` | `"pda-derivation"` | `"instruction-build"` | `"transaction-build"` | `"transaction-compile"` | `"transaction-sign"` | `"transaction-validate"` | `"transaction-send"`. See [Errors](./errors#createutxoerror). # Errors Source: https://sdk.umbraprivacy.com/reference/errors UmbraError hierarchy: EncryptedDepositError, ClaimUtxoError, RegistrationError etc. with stage fields for precise failure location. Retry guidance per stage. ## Import Path ```typescript theme={null} import { // Staged errors EncryptedDepositError, EncryptedWithdrawalError, RegistrationError, ConversionError, CreateUtxoError, FetchUtxosError, ClaimUtxoError, QueryError, AccountError, ComplianceError, isEncryptedDepositError, isEncryptedWithdrawalError, isRegistrationError, isConversionError, isCreateUtxoError, isFetchUtxosError, isClaimUtxoError, isQueryError, isAccountError, isComplianceError, // Base primitives UmbraError, CryptographyError, InstructionError, RpcError, TransactionError, TransactionSigningError, MasterSeedSigningRejectedError, isUmbraError, isCryptographyError, isInstructionError, isRpcError, isTransactionError, isTransactionSigningError, } from "@umbra-privacy/sdk/errors"; ``` *** ## Error Hierarchy ``` Error └── UmbraError ├── CryptographyError ├── InstructionError ├── RpcError ├── TransactionError │ └── TransactionSigningError │ └── MasterSeedSigningRejectedError ├── EncryptedDepositError ├── EncryptedWithdrawalError ├── RegistrationError ├── ConversionError ├── CreateUtxoError ├── FetchUtxosError ├── ClaimUtxoError ├── QueryError ├── AccountError └── ComplianceError ``` *** ## Staged Errors Every major SDK operation throws a dedicated staged error. The `stage` field pinpoints exactly where in the pipeline the failure occurred. *** ### EncryptedDepositError Thrown by `getATAIntoETADirectDepositorFunction`. ```typescript theme={null} class EncryptedDepositError extends UmbraError { readonly stage: EncryptedDepositStage; } function isEncryptedDepositError(error: unknown): error is EncryptedDepositError; ``` **Stage values:** * `"initialization"` - Factory construction failed. Missing required arguments. * `"validation"` - Invalid mint address, zero amount, or unregistered destination. * `"mint-fetch"` - Failed to fetch the mint account. Check RPC and mint address. * `"fee-calculation"` - Token-2022 transfer fee calculation failed. Epoch info issue. * `"pda-derivation"` - Failed to derive required PDAs. * `"account-fetch"` - Failed to fetch the destination user or token account. * `"instruction-build"` - Failed to construct the instruction. Protocol state mismatch. * `"transaction-build"` - Failed to assemble the transaction. Blockhash fetch failure. * `"transaction-compile"` - Failed to compile the transaction. Lookup table mismatch. * `"transaction-sign"` - Wallet rejected signing. * `"transaction-validate"` - Pre-flight simulation failed. Insufficient funds or account state mismatch. * `"transaction-send"` - Transaction sent but confirmation timed out. May still have landed. *** ### EncryptedWithdrawalError Thrown by `getETAIntoATAWithdrawerFunction`. ```typescript theme={null} class EncryptedWithdrawalError extends UmbraError { readonly stage: EncryptedWithdrawalStage; } function isEncryptedWithdrawalError(error: unknown): error is EncryptedWithdrawalError; ``` **Stage values:** * `"initialization"` - Factory construction failed. * `"validation"` - Zero amount or no encrypted balance exists for this mint. * `"mint-fetch"` - Failed to fetch the mint account. * `"pda-derivation"` - Failed to derive required PDAs. * `"instruction-build"` - Failed to construct the instruction. * `"transaction-build"` - Blockhash fetch failure. * `"transaction-compile"` - Failed to compile the transaction. * `"transaction-sign"` - Wallet rejected signing. * `"transaction-send"` - Confirmation timed out. May still have landed. *** ### RegistrationError Thrown by `getUserRegistrationFunction`. ```typescript theme={null} class RegistrationError extends UmbraError { readonly stage: RegistrationStage; } function isRegistrationError(error: unknown): error is RegistrationError; ``` **Stage values:** * `"initialization"` - Factory construction failed. * `"master-seed-derivation"` - User declined to sign the master seed derivation message. * `"account-fetch"` - Failed to fetch on-chain account state. RPC connectivity issue. * `"key-derivation"` - Cryptographic key derivation from master seed failed. * `"zk-proof-generation"` - Groth16 proof generation failed (anonymous step only). * `"pda-derivation"` - Failed to derive required PDAs. * `"instruction-build"` - Failed to construct an instruction. * `"transaction-build"` - Blockhash fetch failure. * `"transaction-compile"` - Failed to compile the transaction. * `"transaction-sign"` - Wallet rejected signing. * `"transaction-validate"` - Pre-flight simulation failed. * `"transaction-send"` - Confirmation timed out. May still have landed. *** ### ConversionError Thrown by `getNetworkEncryptionToSharedEncryptionConverterFunction` and `getMintEncryptionKeyRotatorFunction`. ```typescript theme={null} class ConversionError extends UmbraError { readonly stage: ConversionStage; } function isConversionError(error: unknown): error is ConversionError; ``` **Stage values:** * `"initialization"` - Factory construction failed. * `"account-fetch"` - Failed to fetch token account state. * `"pda-derivation"` - Failed to derive required PDAs. * `"instruction-build"` - Failed to construct the conversion instruction. * `"transaction-build"` - Blockhash fetch or transaction assembly failed. * `"transaction-compile"` - Failed to compile the transaction. * `"transaction-sign"` - Wallet rejected a per-mint transaction. * `"transaction-validate"` - Pre-flight simulation failed. * `"transaction-send"` - Confirmation timed out. Already-converted mints remain on-chain. *** ### CreateUtxoError Thrown by all four Stealth Pool Note creator factory functions in `@umbra-privacy/sdk/deposit`. ```typescript theme={null} class CreateUtxoError extends UmbraError { readonly stage: CreateUtxoStage; } function isCreateUtxoError(error: unknown): error is CreateUtxoError; ``` **Stage values:** * `"initialization"` - Factory construction failed. * `"validation"` - Invalid recipient, mint, or amount. * `"account-fetch"` - Failed to fetch the recipient's on-chain account. * `"mint-fetch"` - Failed to fetch mint account data. * `"fee-calculation"` - Token-2022 transfer fee calculation failed. * `"key-derivation"` - Key derivation from master seed failed. * `"zk-proof-generation"` - Groth16 proof generation failed. * `"pda-derivation"` - Failed to derive required PDAs. * `"instruction-build"` - Failed to construct the instruction. * `"transaction-build"` - Blockhash fetch or transaction assembly failed. * `"transaction-compile"` - Failed to compile the transaction. * `"transaction-sign"` - Wallet rejected signing. * `"transaction-validate"` - Pre-flight simulation failed. * `"transaction-send"` - Confirmation timed out. May still have landed - check before retrying. *** ### FetchUtxosError Thrown by `getBurnableStealthPoolNoteScannerFunction` in `@umbra-privacy/sdk/burn`. ```typescript theme={null} class FetchUtxosError extends UmbraError { readonly stage: FetchUtxosStage; } function isFetchUtxosError(error: unknown): error is FetchUtxosError; ``` **Stage values:** * `"initialization"` — factory construction failed. `indexerApiEndpoint` not configured. * `"validation"` — invalid scanner-level input. * `"key-derivation"` — X25519 private key derivation from master seed failed. * `"indexer-fetch"` — indexer HTTP call failed (unreachable, rate-limited, or error response). * `"proof-fetch"` — Merkle proof HTTP call failed (from the burner's per-batch proof fetch). * `"proof-enrichment"` — `enrichWithMerkleProof` failed to attach a proof entry to a scanned note (missing proof for the note's insertion index). *** ### ClaimUtxoError Thrown by all three burner factory functions in `@umbra-privacy/sdk/burn`. ```typescript theme={null} class ClaimUtxoError extends UmbraError { readonly stage: ClaimUtxoStage; } function isClaimUtxoError(error: unknown): error is ClaimUtxoError; ``` **Stage values:** * `"initialization"` - Factory construction failed. * `"validation"` — invalid Stealth Pool Note data or parameters. * `"key-derivation"` - Key derivation from master seed failed. * `"zk-proof-generation"` - Groth16 proof generation failed. * `"pda-derivation"` - Failed to derive required PDAs. * `"instruction-build"` - Failed to construct the instruction. * `"transaction-build"` - Blockhash fetch or transaction assembly failed. * `"transaction-compile"` - Failed to compile the transaction. * `"transaction-sign"` - Wallet rejected signing. * `"transaction-validate"` — pre-flight simulation failed. Often indicates a stale Merkle proof — the burner refetches the proof on a retry, so just re-run the burn. * `"transaction-send"` - Confirmation timed out. Verify on-chain before retrying - the nullifier may have been burned. *** ### QueryError Thrown by `getUserAccountQuerierFunction` and `getEncryptedBalanceQuerierFunction`. ```typescript theme={null} class QueryError extends UmbraError { readonly stage: QueryStage; } function isQueryError(error: unknown): error is QueryError; ``` **Stage values:** * `"initialization"` — factory-level validation failed. * `"pda-derivation"` — PDA address generation failed. * `"account-fetch"` — RPC fetch failed (node unreachable or returned an error). * `"account-decode"` — on-chain account data could not be decoded. Unexpected on-chain state. * `"key-derivation"` — X25519 key derivation failed (encrypted balance query only). * `"decryption"` — Rescue cipher decryption failed (encrypted balance query only). *** ### AccountError Thrown by every factory in `@umbra-privacy/sdk/account` — staged-token recoverers (`getStagedSolRecovererFunction`, `getStagedSplRecovererFunction`), key rotators (`getUserEncryptionKeyRotatorFunction`, `getMasterViewingKeyRotatorFunction`, `getMintEncryptionKeyRotatorFunction`), and entropy-seed rotators (`getUserEntropySeedRotatorFunction`, `getTokenEntropySeedRotatorFunction`). ```typescript theme={null} class AccountError extends UmbraError { readonly stage: AccountStage; } function isAccountError(error: unknown): error is AccountError; ``` **Stage values:** `"initialization"` | `"validation"` | `"account-fetch"` | `"account-decode"` | `"key-derivation"` | `"zk-proof-generation"` | `"pda-derivation"` | `"instruction-build"` | `"transaction-build"` | `"transaction-compile"` | `"transaction-sign"` | `"transaction-validate"` | `"transaction-send"`. `"zk-proof-generation"` is only reachable from `getMasterViewingKeyRotatorFunction` (the only account operation that runs a ZK proof). *** ### ComplianceError Thrown by every factory in `@umbra-privacy/sdk/compliance` — grant issuer / revoker / queriers / re-encryptors. ```typescript theme={null} class ComplianceError extends UmbraError { readonly stage: ComplianceStage; } function isComplianceError(error: unknown): error is ComplianceError; ``` **Stage values:** `"initialization"` | `"validation"` | `"key-derivation"` | `"pda-derivation"` | `"account-fetch"` | `"account-decode"` | `"decryption"` | `"reencryption"` | `"instruction-build"` | `"transaction-build"` | `"transaction-compile"` | `"transaction-sign"` | `"transaction-validate"` | `"transaction-send"`. `"decryption"` / `"reencryption"` are only reachable from the re-encryptor factories (`getSharedCiphertextReencryptorForUserGrantFunction`, etc.). The grant lifecycle factories (issue / revoke) never hit those stages. *** ## Base Error Primitives These are the underlying error types. All staged errors extend `UmbraError`. *** ### UmbraError Base class for all SDK errors. ```typescript theme={null} class UmbraError extends Error { constructor( message: string, options?: { code?: string; context?: Record; cause?: unknown; }, ); readonly code: string; // Machine-readable code. Defaults to "UMBRA_ERROR". readonly context?: Record; override readonly cause?: unknown; } function isUmbraError(error: unknown): error is UmbraError; ``` *** ### CryptographyError Thrown when a cryptographic operation fails - most commonly ZK proof generation or Rescue cipher encryption/decryption. ```typescript theme={null} class CryptographyError extends UmbraError { constructor( message: string, options?: { code?: string; operation?: string; context?: Record; cause?: unknown; }, ); readonly operation?: string; // Name of the failed operation, e.g. "groth16-prove". } function isCryptographyError(error: unknown): error is CryptographyError; ``` *** ### InstructionError Thrown when instruction construction fails - typically a protocol state mismatch or missing on-chain account. ```typescript theme={null} class InstructionError extends UmbraError { constructor( message: string, options?: { code?: string; instructionName?: string; context?: Record; cause?: unknown; }, ); readonly instructionName?: string; // Name of the instruction that failed to build. } function isInstructionError(error: unknown): error is InstructionError; ``` *** ### RpcError Thrown when an RPC or indexer HTTP call fails. ```typescript theme={null} class RpcError extends UmbraError { constructor( message: string, options?: { code?: string; endpoint?: string; statusCode?: number; rpcErrorCode?: number; context?: Record; cause?: unknown; }, ); readonly endpoint?: string; // The URL that was called. readonly statusCode?: number; // HTTP status code (e.g. 429 for rate limit, 503 for unavailable). readonly rpcErrorCode?: number; // JSON-RPC error code from the node. } function isRpcError(error: unknown): error is RpcError; ``` *** ### TransactionError Thrown when a transaction fails on-chain (simulation rejection or network rejection). ```typescript theme={null} class TransactionError extends UmbraError { constructor( message: string, options?: { code?: string; signature?: string; simulationLogs?: string[]; context?: Record; cause?: unknown; }, ); readonly signature?: string; // Base58 tx signature. Present even on failure - tx may have landed. readonly simulationLogs?: string[] // Program log output from pre-flight simulation. } function isTransactionError(error: unknown): error is TransactionError; ``` *** ### TransactionSigningError Thrown when a wallet rejects a transaction signing request. Extends `TransactionError`. ```typescript theme={null} class TransactionSigningError extends TransactionError { constructor( message: string, options?: { code?: string; wasRejected?: boolean; signerAddress?: string; signature?: string; context?: Record; cause?: unknown; }, ); readonly wasRejected: boolean; // true = user explicitly cancelled; false = wallet error. readonly signerAddress?: string; // Address of the wallet that rejected. } function isTransactionSigningError(error: unknown): error is TransactionSigningError; ``` *** ### MasterSeedSigningRejectedError Thrown when the user declines to sign the master seed derivation message. Extends `TransactionSigningError`. Always has `wasRejected: true`. Error code: `"MASTER_SEED_SIGNING_REJECTED"`. This surfaces during the first operation that needs key material in a session - typically `register()`, or `getUmbraClient()` in non-deferred mode. ```typescript theme={null} class MasterSeedSigningRejectedError extends TransactionSigningError { constructor( message?: string, options?: { signerAddress?: string; context?: Record; cause?: unknown; }, ); } ``` *** ## Retry Strategy * `"transaction-sign"` / `MasterSeedSigningRejectedError` - **Do not retry.** User cancelled. Never auto-retry a wallet prompt rejection. * `"transaction-send"` with a `signature` present - **Check first.** The transaction may have landed. Verify on-chain before submitting again to avoid double submissions. * `"transaction-validate"` - **Fix inputs first.** Pre-flight simulation failed. Diagnose the root cause before retrying. * `"transaction-validate"` for burns — may indicate a stale Merkle proof. Re-run the burner; it fetches a fresh per-batch proof on each attempt. * `isRpcError` - **Safe to retry with exponential backoff.** Transient network issue. * All other stages - **Safe to retry with exponential backoff.** # Stealth Pool (Mixer) Source: https://sdk.umbraprivacy.com/reference/mixer V18 API reference: getBurnableStealthPoolNoteScannerFunction (zero-arg) + 3 burner factories (receiver→ETA, self→ETA, self→ATA) with fetchBatchMerkleProof, ZK prover, and relayer deps. All Stealth Pool factories live under `@umbra-privacy/sdk/burn`. *** ## getBurnableStealthPoolNoteScannerFunction **Import:** `@umbra-privacy/sdk/burn` ```typescript theme={null} function getBurnableStealthPoolNoteScannerFunction( args: { client: IUmbraClient }, deps?: GetBurnableStealthPoolNoteScannerFunctionDeps, ): BurnableStealthPoolNoteScannerFunction; ``` Returns a **zero-arg** scanner. When called, it discovers every active Stealth Pool tree, loads scan progress from `client.utxoDataStore`, fetches missing ranges from the indexer, decrypts ciphertexts addressable by your viewing keys, persists progress, and returns the result grouped by `(kind, source)`. `client.indexerApiEndpoint` must be set (pass it to `getUmbraClient`). ### Deps * `fetchStealthPoolNoteData?: StealthPoolNoteDataFetcherFunction` — override the default indexer note-data fetcher. * `fetchMerkleProof?: MerkleProofFetcherFunction` — override the single-leaf Merkle proof fetcher used internally for sanity checks. The burner factory uses its own `fetchBatchMerkleProof` dep at submit time. * `aesDecryptor?: AesDecryptorFunction` — override the AES-GCM decryptor used to decode receiver-burnable note ciphertexts. * `x25519GetPublicKey?` / `x25519GetSharedSecret?` — override X25519 ECDH primitives. * `additionalX25519PrivateKeys?: readonly X25519PrivateKey[]` — multi-key wallets can scan against extra X25519 keys. ### Returns ```typescript theme={null} type BurnableStealthPoolNoteScannerFunction = () => Promise; ``` ### ScannedStealthPoolNoteResult ```typescript theme={null} interface ScannedStealthPoolNoteResult { readonly etaToStealthPoolSelfBurnable: readonly DecryptedStealthPoolNoteData[]; readonly etaToStealthPoolReceiverBurnable: readonly DecryptedStealthPoolNoteData[]; readonly ataToStealthPoolSelfBurnable: readonly DecryptedStealthPoolNoteData[]; readonly ataToStealthPoolReceiverBurnable: readonly DecryptedStealthPoolNoteData[]; readonly scannedTrees: readonly ScannedTreeProgress[]; } interface ScannedTreeProgress { readonly treeIndex: U128; readonly scannedRange: { start: U32; end: U32 } | null; readonly totalLeaves: U32; readonly fullyScanned: boolean; } ``` Notes are **not** proof-bundled — proofs are fetched per batch at burn time. ### Errors Throws `FetchUtxosError` (named with the legacy noun; class name retained for backwards compatibility — the error itself covers V18 Stealth Pool Note scanning). See [Errors](./errors#fetchutxoserror). ### Example ```typescript theme={null} import { getBurnableStealthPoolNoteScannerFunction } from "@umbra-privacy/sdk/burn"; const scan = getBurnableStealthPoolNoteScannerFunction({ client }); const result = await scan(); console.log("Self-burnable from ETA:", result.etaToStealthPoolSelfBurnable.length); ``` *** ## getReceiverBurnableStealthPoolNoteIntoETABurnerFunction **Import:** `@umbra-privacy/sdk/burn` ```typescript theme={null} function getReceiverBurnableStealthPoolNoteIntoETABurnerFunction( args: { client: IUmbraClient }, deps: { fetchBatchMerkleProof: BatchMerkleProofFetcherFunction; zkProver: IZkProverForBurnReceiverBurnableStealthPoolNoteIntoEncryptedTokenAccount; relayer: { submitBurn: BurnSubmitterFunction; pollBurnStatus: BurnStatusPollerFunction; getRelayerAddress: () => Promise
; }; hooks?: BurnHooks; awaitCompletion?: boolean; // default true pollingIntervalMs?: number; // default 3000 timeoutMs?: number; // default 120_000 /* additional optional crypto + RPC + key-derivation overrides */ }, ): ReceiverBurnableStealthPoolNoteIntoETABurnerFunction; ``` Burns receiver-burnable notes into the caller's ETA. **Natively batches**: groups by `destinationAddress`, chunks to ≤5 per proof. ### Required deps * **`fetchBatchMerkleProof`** — typically `client.fetchBatchMerkleProof` (auto-wired when `indexerApiEndpoint` is set on the client). * **`zkProver`** — `getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver()` from `@umbra-privacy/sdk/zk-prover`. (The interface type is aliased internally as `IZkProverForBurnReceiverBurnableStealthPoolNoteIntoEncryptedTokenAccount` — the underlying type is identical.) * **`relayer`** — adapter built from `getUmbraRelayer({ apiEndpoint })`. `BurnSubmitterFunction` and `BurnStatusPollerFunction` are V18 TypeScript aliases of `ClaimSubmitterFunction` and `ClaimStatusPollerFunction`, so the relayer client's `submitClaim` / `pollClaimStatus` plug in directly under the renamed property names. ### Returns ```typescript theme={null} type ReceiverBurnableStealthPoolNoteIntoETABurnerFunction = ( stealthPoolNotes: readonly (DecryptedStealthPoolNoteData & { kind: "receiver-burnable" })[], optionalData?: OptionalData32, microLamportsPerAcu?: MicroLamportsPerAcu, ) => Promise; interface BurnStealthPoolNoteIntoETAResult { readonly signatures: readonly TransactionSignature[]; // every tx submitted (from OperationOutcome) readonly batches: Map; // keyed by batch index } interface BurnBatchResult { readonly requestId: string; // relayer-assigned tracking ID readonly status: BurnStatus; readonly txSignature?: string; // on-chain burn tx (when landed) readonly callbackSignature?: string; // MPC callback tx (when finalised) readonly resolvedVariant?: string; // e.g. "claim_into_existing_shared_balance_v18" readonly failureReason?: string | null; readonly stealthPoolNoteIds?: readonly string[]; // "treeIndex:leafIndex" pairs } type BurnStatus = | "received" | "validating" | "offsets_reserved" | "building_tx" | "tx_built" | "submitting" | "submitted" | "awaiting_callback" | "callback_received" | "finalizing" | "completed" | "failed" | "timed_out" | "refunded"; ``` `completed` / `callback_received` indicate success. The signature to display is `callbackSignature ?? txSignature`. `failureReason` containing `"NullifierAlreadyBurnt"` means the note was already burnt — the SDK treats this as idempotent success. ### Errors Throws `ClaimUtxoError`. See [Errors](./errors#claimutxoerror). The `"transaction-validate"` stage often indicates a stale Merkle proof — the burner refetches on retry. *** ## getSelfBurnableStealthPoolNoteIntoETABurnerFunction **Import:** `@umbra-privacy/sdk/burn` Same shape as the receiver variant. `MAX_NOTES_PER_PROOF = 1` — the SDK loops internally; caller still passes an array. `zkProver` is `IZkProverForClaimSelfClaimableUtxoIntoEncryptedBalance` — use `getClaimSelfClaimableUtxoIntoEncryptedBalanceProver` from `@umbra-privacy/sdk/zk-prover`. *** ## getSelfBurnableStealthPoolNoteIntoATABurnerFunction **Import:** `@umbra-privacy/sdk/burn` Same shape; tokens released to the destination ATA instead of an ETA. `zkProver` is `IZkProverForClaimSelfClaimableUtxoIntoPublicBalance` — use `getClaimSelfClaimableUtxoIntoPublicBalanceProver` from `@umbra-privacy/sdk/zk-prover`. Receiver-burnable → ATA exists on-chain but is not yet shipped in the SDK. Use receiver-burnable → ETA then a regular withdrawal until the SDK adds the direct path. *** ## Full scan + burn example ```typescript theme={null} import { getBurnableStealthPoolNoteScannerFunction, getReceiverBurnableStealthPoolNoteIntoETABurnerFunction, getSelfBurnableStealthPoolNoteIntoETABurnerFunction, } from "@umbra-privacy/sdk/burn"; import { getUmbraRelayer } from "@umbra-privacy/sdk"; import { getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver, getClaimSelfClaimableUtxoIntoEncryptedBalanceProver, } from "@umbra-privacy/sdk/zk-prover"; const r = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com" }); const relayerDeps = { submitBurn: r.submitClaim, pollBurnStatus: r.pollClaimStatus, getRelayerAddress: r.getRelayerAddress, }; const scan = getBurnableStealthPoolNoteScannerFunction({ client }); const burnReceiverIntoEta = getReceiverBurnableStealthPoolNoteIntoETABurnerFunction( { client }, { fetchBatchMerkleProof: client.fetchBatchMerkleProof!, zkProver: getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(), relayer: relayerDeps, }, ); const burnSelfIntoEta = getSelfBurnableStealthPoolNoteIntoETABurnerFunction( { client }, { fetchBatchMerkleProof: client.fetchBatchMerkleProof!, zkProver: getClaimSelfClaimableUtxoIntoEncryptedBalanceProver(), relayer: relayerDeps, }, ); const result = await scan(); if (result.ataToStealthPoolReceiverBurnable.length > 0) { const out = await burnReceiverIntoEta(result.ataToStealthPoolReceiverBurnable); for (const [batchIndex, batch] of out.batches) { console.log("Batch", batchIndex, "status:", batch.status, "id:", batch.requestId); } } if (result.etaToStealthPoolSelfBurnable.length > 0) { await burnSelfIntoEta(result.etaToStealthPoolSelfBurnable); } ``` *** ## FetchUtxosError Thrown by `getBurnableStealthPoolNoteScannerFunction`. Stage values: `"initialization"` | `"validation"` | `"key-derivation"` | `"indexer-fetch"` | `"proof-fetch"` | `"proof-enrichment"`. See [Errors](./errors#fetchutxoserror). *** ## ClaimUtxoError Thrown by all burner factories. Stage values: `"initialization"` | `"validation"` | `"key-derivation"` | `"zk-proof-generation"` | `"pda-derivation"` | `"instruction-build"` | `"transaction-build"` | `"transaction-compile"` | `"transaction-sign"` | `"transaction-validate"` | `"transaction-send"`. See [Errors](./errors#claimutxoerror). Before retrying after `"transaction-send"`, verify on-chain — the nullifier may have been burnt. The burner itself treats `NullifierAlreadyBurnt` as idempotent success in `BurnBatchOutcome.failureReason`. # SDK Reference Source: https://sdk.umbraprivacy.com/reference/overview Complete V18 SDK reference index: all factory functions and the subpath each lives under (registration, deposit, withdrawal, burn, query, conversion, compliance, account, zk-prover, store-adapters). This reference covers the full public API surface of `@umbra-privacy/sdk@5.x` (V18). Pages are organised by feature area — each section documents factory functions alongside their parameter types, result types, and associated errors. ## Main barrel + subpaths The main `@umbra-privacy/sdk` barrel re-exports a small, stable surface — `getUmbraClient`, the four signer factories, the full error hierarchy, the `Result` helpers, and everything from `infrastructure/{arcium, indexer, relayer, solana, zk-prover}`. **Operation factories require named subpath imports.** | Subpath | Contents | | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | | `@umbra-privacy/sdk` | `getUmbraClient`, signers, errors, `Result`, indexer + relayer + zk-prover + solana + arcium infra | | `@umbra-privacy/sdk/client` | `IUmbraClient`, `IUmbraSigner` interfaces | | `@umbra-privacy/sdk/shared` | `UMBRA_MESSAGE_TO_SIGN` and protocol-wide constants | | `@umbra-privacy/sdk/registration` | `getUserRegistrationFunction`, executors, build helpers | | `@umbra-privacy/sdk/deposit` | Direct ATA→ETA deposit + 4 Stealth Pool Note creators (ATA/ETA × self/receiver) | | `@umbra-privacy/sdk/withdrawal` | ETA→ATA withdrawer | | `@umbra-privacy/sdk/transfer` | `getTransferorFunction` — ETA-to-ETA direct transfer, 8 variants | | `@umbra-privacy/sdk/burn` | Note scanner + 3 burner factories + `enrichWithMerkleProof` | | `@umbra-privacy/sdk/query` | Encrypted-balance + user-account queriers | | `@umbra-privacy/sdk/conversion` | MXE-only → Shared converter | | `@umbra-privacy/sdk/compliance` | Grant issuer / revoker / 3 reencryptors / 3 queriers | | `@umbra-privacy/sdk/account` | Staged-token recoverers, key rotators, maintenance | | `@umbra-privacy/sdk/zk-prover` | One factory per Groth16 circuit | | `@umbra-privacy/sdk/zk-prover/cdn` | `getCdnZkAssetProvider` | | `@umbra-privacy/sdk/store-adapters` | `createBrowserStorageBackend`, `createFileStorageBackend`, sharded + in-memory UTXO + nullifier stores | | `@umbra-privacy/sdk/store` | Base store interfaces | | `@umbra-privacy/sdk/indexer`, `…/indexer/utxo`, `…/indexer/nullifier` | Typed indexer clients | | `@umbra-privacy/sdk/errors` | Error classes + `is*` type guards | | `@umbra-privacy/sdk/types` | Branded primitives (U64, Address, …) + `create*` helpers | | `@umbra-privacy/sdk/constants` | `BPS_DIVISOR`, mint lists, domain separators | | `@umbra-privacy/sdk/crypto`, `…/crypto/poseidon`, `…/crypto/rescue`, `…/crypto/aes`, `…/crypto/key-derivation`, `…/crypto/challenges`, `…/crypto/commitment` | Cryptographic primitives | | `@umbra-privacy/sdk/converters` | Encoding / byte-conversion utilities | | `@umbra-privacy/sdk/arcium` | Arcium MPC types + helpers + `generateRandomNonce` (also re-exported from main barrel) | | `@umbra-privacy/sdk/temporal`, `…/validation`, `…/hooks`, `…/pipeline`, `…/fee-provider`, `…/math` | Utilities | | `@umbra-privacy/sdk/pda`, `…/solana`, `…/token` | Solana / PDA / token helpers | | `@umbra-privacy/sdk/arcium` | Arcium types + helpers | *** ## Factory Function Pattern Every SDK operation follows the same two-step pattern: ```typescript theme={null} // 1. Build once (at setup time). const operation = getOperationFunction({ client }, deps?); // 2. Call per-operation at runtime. const result = await operation(/* runtime arguments */); ``` The factory (`getOperationFunction`) captures configuration and optional dependency overrides. The optional `deps` argument accepts injectable overrides for RPC providers, key generators, ZK provers, and (for burners) relayer adapters — useful for unit testing or custom infrastructure (e.g. Jito bundles, hardware key storage). *** ## Common Types Types referenced across all pages: * `Address` — Base58-encoded Solana public key string (`string`-branded). * `TransactionSignature` — Base58-encoded transaction signature string. * `U64` — Branded `bigint` constrained to the unsigned 64-bit range. * `U128` — Branded `bigint` for unsigned 128-bit values. * `U256` — Branded `bigint` for unsigned 256-bit values. * `U512` — Branded `bigint` for unsigned 512-bit values. * `X25519PublicKey` — 32-byte `Uint8Array` Diffie-Hellman public key. * `RescueCipherEncryptionNonce` — Rescue cipher encryption nonce (branded `Uint8Array`). * `OptionalData32` — 32-byte `Uint8Array` for caller-defined metadata attached to on-chain instructions. Defaults to 32 zero bytes when omitted. **Must be pre-hashed or pre-encrypted — never store plaintext identifiers.** * `IUmbraClient` — The client configuration object produced by `getUmbraClient`. Passed to every factory as `args.client`. * `Network` — `"mainnet" | "devnet" | "localnet"`. * `DecryptedStealthPoolNoteData` — A decrypted note returned by the scanner. Passed directly to a burner factory. * `ScannedStealthPoolNoteResult` — The scanner's return value; four buckets of notes by `(kind, source)` plus `scannedTrees` progress. *** ## Pages in this reference * [Client](/reference/client) — `getUmbraClient`, `IUmbraClient`, `IUmbraSigner`. * [Registration](/reference/registration) — `getUserRegistrationFunction`, sub-step executors. * [Deposit](/reference/deposit) — Direct ATA→ETA deposit + 4 Stealth Pool Note creators. * [Withdrawal](/reference/withdraw) — Direct ETA→ATA withdrawal. * [Query](/reference/query) — User-account and encrypted-balance queriers. * [Conversion](/reference/conversion) — MXE-only → Shared converter. * [Stealth Pool (Mixer)](/reference/mixer) — Note scanner + 3 burner factories. * [Compliance](/reference/compliance) — Grants, re-encryption, viewing-key derivers. * [Errors](/reference/errors) — All error classes, stage enums, type guards, and the error hierarchy. # Query Source: https://sdk.umbraprivacy.com/reference/query API reference: getEncryptedBalanceQuerierFunction (Rescue cipher decryption for shared-mode) + getUserAccountQuerierFunction (EncryptedUserAccount fields). ## getUserAccountQuerierFunction **Import:** `@umbra-privacy/sdk/query` ```typescript theme={null} function getUserAccountQuerierFunction( args: GetUserAccountQuerierFunctionArgs, deps?: GetUserAccountQuerierFunctionDeps, ): UserAccountQuerierFunction ``` Returns a function that fetches and decodes the on-chain user account for any Umbra address. *** ### GetUserAccountQuerierFunctionArgs * `client: IUmbraClient` ### GetUserAccountQuerierFunctionDeps * `accountInfoProvider?: AccountInfoProviderFunction` ### Returns `UserAccountQuerierFunction` ```typescript theme={null} type UserAccountQuerierFunction = ( userAddress: Address, options?: { commitment?: Commitment }, ) => Promise ``` ### QueryUserAccountResult A discriminated union: * `{ state: "exists"; data: EncryptedUserAccount }` - The user is registered and the account was decoded. * `{ state: "non_existent" }` - No user account exists at this address. ### EncryptedUserAccount Fields of the decoded on-chain account: * `versionByte: U8` - Schema version. * `canonicalBump: U8` - PDA canonical bump. * `isInitialised: boolean` - Whether the account has been initialised. * `isActiveForAnonymousUsage: boolean` - Whether anonymous (mixer) usage is enabled. * `isUserCommitmentRegistered: boolean` - Whether the Poseidon commitment has been registered. * `isUserAccountX25519KeyRegistered: boolean` - Whether the X25519 public key has been registered. * `x25519PublicKey: X25519PublicKey` — the user's X25519 public key (used to encrypt Stealth Pool Notes addressed to this account). * `userCommitment: PoseidonHash` - The user's on-chain Poseidon commitment. * `generationIndex: U128` - Current generation index used in key derivation. * `randomGenerationSeed: U256LeBytes` - Current random generation seed. *** ### Errors Throws `QueryError`. See [Errors](./errors#queryerror). ### Example ```typescript theme={null} import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk/query"; const queryUserAccount = getUserAccountQuerierFunction({ client }); const result = await queryUserAccount(userAddress); if (result.state === "exists") { console.log("X25519 key:", result.data.x25519PublicKey); console.log("Registered for anonymous usage:", result.data.isActiveForAnonymousUsage); } else { console.log("User not registered."); } ``` *** ## getEncryptedBalanceQuerierFunction **Import:** `@umbra-privacy/sdk/query` ```typescript theme={null} function getEncryptedBalanceQuerierFunction( args: GetEncryptedBalanceQuerierFunctionArgs, deps?: GetEncryptedBalanceQuerierFunctionDeps, ): EncryptedBalanceQuerierFunction ``` Returns a function that fetches and decrypts encrypted token balances for a set of mints. For shared-mode accounts, the balance is decrypted locally using the caller's X25519 private key. For MXE-mode accounts, the ciphertext cannot be decrypted client-side. *** ### GetEncryptedBalanceQuerierFunctionArgs * `client: IUmbraClient` ### GetEncryptedBalanceQuerierFunctionDeps * `accountInfoProvider?: AccountInfoProviderFunction` * `rcDecryptor?: RcDecryptorFunction` - Override the Rescue cipher decryptor used for shared-mode balance decryption. ### Returns `EncryptedBalanceQuerierFunction` ```typescript theme={null} type EncryptedBalanceQuerierFunction = ( mints: readonly Address[], options?: { commitment?: Commitment }, ) => Promise> ``` Returns a `Map` keyed by mint address. Each entry is one of: ### QueryEncryptedBalanceResult A discriminated union per mint: * `{ state: "shared"; balance: U64 }` - Shared-mode account. `balance` is the decrypted token amount in base units. * `{ state: "mxe" }` - MXE-mode account. Balance is encrypted for the Arcium network and cannot be read client-side. * `{ state: "uninitialized" }` - The encrypted token account exists but has not been initialised for this mint. * `{ state: "non_existent" }` - No encrypted token account exists for this mint. *** ### Errors Throws `QueryError`. See [Errors](./errors#queryerror). ### Example ```typescript theme={null} import { getEncryptedBalanceQuerierFunction } from "@umbra-privacy/sdk/query"; const queryBalances = getEncryptedBalanceQuerierFunction({ client }); const balances = await queryBalances([usdcMint, solMint]); for (const [mint, result] of balances) { if (result.state === "shared") { console.log(`${mint}: ${result.balance} base units`); } else if (result.state === "mxe") { console.log(`${mint}: MXE-encrypted, balance not readable client-side`); } else { console.log(`${mint}: ${result.state}`); } } ``` *** ## QueryError Thrown by both query functions. Stage values: `"initialization"` | `"pda-derivation"` | `"account-fetch"` | `"account-decode"` | `"key-derivation"` | `"decryption"` * `"key-derivation"` and `"decryption"` are only reachable from `getEncryptedBalanceQuerierFunction` (shared-mode decryption path). See [Errors](./errors#queryerror) for full documentation. # Registration Source: https://sdk.umbraprivacy.com/reference/registration API reference: getUserRegistrationFunction (3-step: account init, X25519 key, user commitment ZK proof), key rotators, entropy seed updaters, staged fund recovery. ## getUserRegistrationFunction **Import:** `@umbra-privacy/sdk/registration` ```typescript theme={null} function getUserRegistrationFunction( args: GetUserRegistrationFunctionArgs, deps?: GetUserRegistrationFunctionDeps, ): UserRegistrationFunction ``` Registers the caller on-chain. Registration is a multi-step idempotent flow that enables confidential token accounts (encrypted balances) and anonymous usage (mixer eligibility). Steps already completed in a previous session are skipped. *** ### GetUserRegistrationFunctionArgs * `client: IUmbraClient` - The configured client. *** ### GetUserRegistrationFunctionDeps All fields are optional overrides for SDK defaults. RPC overrides: * `accountInfoProvider?: AccountInfoProviderFunction` * `getLatestBlockhash?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` Key derivation overrides: * `userAccountX25519KeypairGenerator?: Curve25519KeypairGeneratorFunction` * `masterViewingKeyEncryptingX25519KeypairGenerator?: Curve25519KeypairGeneratorFunction` * `mintX25519KeypairGenerator?: MintX25519KeypairGeneratorFunction` * `masterViewingKeyGenerator?: MasterViewingKeyGeneratorFunction` * `masterViewingKeyBlindingFactorGenerator?: MasterViewingKeyBlindingFactorGeneratorFunction` * `poseidonPrivateKeyGenerator?: PoseidonPrivateKeyGeneratorFunction` * `poseidonBlindingFactorGenerator?: PoseidonBlindingFactorGeneratorFunction` * `rescueCommitmentBlindingFactorGenerator?: RescueEncryptionCommitmentBlindingFactorGeneratorFunction` * `randomFactorGenerator?: RandomFactorForPolynomialCommitmentGeneratorFunction` Rescue cipher overrides: * `getRcKeyGenerator?: (privateKey: X25519PrivateKey) => RcKeyGeneratorFunction` * `getRcEncryptor?: (privateKey: X25519PrivateKey) => RcEncryptorWithNonceFunction` * `rescueCommitmentGenerator?: RescueEncryptionCommitmentGeneratorFunction` Commitment overrides: * `userCommitmentGenerator?: UserCommitmentGeneratorFunction` Challenge / polynomial overrides: * `fiatShamirChallengeGenerator?: FiatShamirChallengeGeneratorFunction` * `challengePowersFunction?: ChallengePowersFunction` * `polynomialEvaluator?: PolynomialEvaluatorFunction` Poseidon / BN254 overrides: * `poseidonAggregator?: PoseidonAggregatorHashFunction` * `bn254ModInverter?: ModuleInvFunction` * `computeLimbwiseSumInverse?: (limbs: Base85LimbTuple) => Bn254FieldElement` ZK prover: * `zkProver?: IZkProverForUserRegistration` — ZK proof generator for the anonymous registration sub-step. Required only when `options.anonymous` is `true`. Use `getUserRegistrationProver()` from `@umbra-privacy/sdk/zk-prover` — see [ZK Provers](/sdk/advanced/zk-provers). *** ### Returns `UserRegistrationFunction` ```typescript theme={null} type UserRegistrationFunction = ( options?: UserRegistrationOptions, ) => Promise ``` Returns the signatures of all transactions submitted during the registration flow. Already-completed steps produce no transactions. ### UserRegistrationOptions * `confidential?: boolean` - Register for confidential (encrypted) token account usage. Default: `true`. * `anonymous?: boolean` - Register for anonymous (mixer) usage. Requires a ZK proof. Default: `true`. * `hooks?: RegistrationHooks` - Per-step lifecycle hooks. Slots: `initUserAccount`, `registerX25519PublicKey`, `registerAnonymousUsage` (each `SkippableDirectTransactionStepHooks` or `SkippableMpcTransactionStepHooks`), plus top-level `onValidationStart/Complete`, `onAccountFetchStart/Complete`, `onKeyDerivationStart/Complete`, `onComplete`, `onError`. * `optionalData?: OptionalData32` - 32-byte caller metadata attached to on-chain instructions. *** ### Errors Throws `RegistrationError`. See [Errors](./errors#registrationerror) for all stages. ### Example ```typescript theme={null} import { getUserRegistrationFunction } from "@umbra-privacy/sdk"; import { getUserRegistrationProver } from "@umbra-privacy/sdk/zk-prover"; const zkProver = getUserRegistrationProver(); const register = getUserRegistrationFunction({ client }, { zkProver }); const signatures = await register({ confidential: true, anonymous: true }); ``` *** ## getUserEncryptionKeyRotatorFunction **Import:** `@umbra-privacy/sdk/account` ```typescript theme={null} function getUserEncryptionKeyRotatorFunction( args: { client: IUmbraClient }, deps?: GetUserRegistrationFunctionDeps, ): () => Promise; ``` Rotates the X25519 public key stored in the on-chain user account. Synchronous, no MPC. Use this when the existing key may have been compromised. Accepts the same shape `args` / `deps` as `getUserRegistrationFunction`. Returns a zero-argument function. Call it with no arguments to execute the rotation. *** ## getMasterViewingKeyRotatorFunction **Import:** `@umbra-privacy/sdk/account` ```typescript theme={null} function getMasterViewingKeyRotatorFunction( args: { client: IUmbraClient }, deps?: GetUserRegistrationFunctionDeps, ): () => Promise; ``` Rotates the Master Viewing Key (MVK) X25519 encryption key. **MPC + ZK** — also rotates the on-chain user commitment. Accepts the same `args` / `deps` shape as `getUserRegistrationFunction`. Returns a zero-argument function. Call it with no arguments to execute the rotation. *** ## getUserEntropySeedRotatorFunction **Import:** `@umbra-privacy/sdk/account` ```typescript theme={null} function getUserEntropySeedRotatorFunction( args: { client: IUmbraClient }, deps?: GetUserEntropySeedRotatorFunctionDeps, ): UserEntropySeedRotatorFunction; ``` Rotates the random generation seed stored in the on-chain user account. This seed contributes to commitment randomness in Stealth Pool Note operations. Synchronous, no MPC. ### Deps * `accountInfoProvider?: AccountInfoProviderFunction`. * `getLatestBlockhash?: GetLatestBlockhash`. * `transactionForwarder?: TransactionForwarder`. ### Returns ```typescript theme={null} type UserEntropySeedRotatorFunction = (args: { newSeed: Uint8Array; optionalData?: OptionalData32; }) => Promise; ``` * `newSeed: Uint8Array` — the 32-byte replacement seed. *** ## getTokenEntropySeedRotatorFunction **Import:** `@umbra-privacy/sdk/account` ```typescript theme={null} function getTokenEntropySeedRotatorFunction( args: { client: IUmbraClient }, deps?: GetTokenEntropySeedRotatorFunctionDeps, ): TokenEntropySeedRotatorFunction; ``` Rotates the random generation seed on a per-mint EncryptedTokenAccount. Synchronous, no MPC. ### Deps * `accountInfoProvider?: AccountInfoProviderFunction`. * `getLatestBlockhash?: GetLatestBlockhash`. * `transactionForwarder?: TransactionForwarder`. ### Returns ```typescript theme={null} type TokenEntropySeedRotatorFunction = (args: { mint: Address; newSeed: Uint8Array; optionalData?: OptionalData32; hooks?: DirectTransactionStepHooks; }) => Promise; ``` * `mint: Address` — the token mint whose account seed to rotate. * `newSeed: Uint8Array` — the 32-byte replacement seed. *** ## getStagedSolRecovererFunction **Import:** `@umbra-privacy/sdk/account` ```typescript theme={null} function getStagedSolRecovererFunction( args: { client: IUmbraClient }, deps?: GetStagedSolRecovererFunctionDeps, ): StagedSolRecovererFunction; ``` Recovers wSOL that was staged in the protocol's pool custody during a failed MPC callback. Call this if a deposit / withdraw / convert callback never landed and you need to reclaim the staged SOL. Synchronous — no MPC, no ZK proof. ### Deps * `accountInfoProvider?: AccountInfoProviderFunction`. * `getLatestBlockhash?: GetLatestBlockhash`. * `transactionForwarder?: TransactionForwarder`. ### Returns ```typescript theme={null} type StagedSolRecovererFunction = (args: { mint: Address; optionalData?: OptionalData32; hooks?: DirectTransactionStepHooks; }) => Promise; ``` * `mint: Address` — the mint of the failed operation (typically the wSOL mint). *** ## getStagedSplRecovererFunction **Import:** `@umbra-privacy/sdk/account` ```typescript theme={null} function getStagedSplRecovererFunction( args: { client: IUmbraClient }, deps?: GetStagedSplRecovererFunctionDeps, ): StagedSplRecovererFunction; ``` Recovers SPL tokens that were staged in the protocol's pool custody during a failed MPC callback. Synchronous — no MPC, no ZK proof. ### Deps * `accountInfoProvider?: AccountInfoProviderFunction`. * `getLatestBlockhash?: GetLatestBlockhash`. * `transactionForwarder?: TransactionForwarder`. ### Returns ```typescript theme={null} type StagedSplRecovererFunction = (args: { mint: Address; optionalData?: OptionalData32; hooks?: DirectTransactionStepHooks; }) => Promise; ``` * `mint: Address` — the token mint of the staged SPL tokens. *** ## RegistrationError Thrown by `getUserRegistrationFunction`. See [Errors](./errors#registrationerror). Stage values: `"initialization"` | `"master-seed-derivation"` | `"account-fetch"` | `"key-derivation"` | `"zk-proof-generation"` | `"pda-derivation"` | `"instruction-build"` | `"transaction-build"` | `"transaction-compile"` | `"transaction-sign"` | `"transaction-validate"` | `"transaction-send"` # Withdrawal Source: https://sdk.umbraprivacy.com/reference/withdraw V18 API reference: getETAIntoATAWithdrawerFunction returns WithdrawResult with queueSignature and callbackSignature via Arcium MPC. ## getETAIntoATAWithdrawerFunction **Import:** `@umbra-privacy/sdk/withdrawal` ```typescript theme={null} function getETAIntoATAWithdrawerFunction( args: { client: IUmbraClient }, deps?: GetETAIntoATAWithdrawerFunctionDeps, ): ETAIntoATAWithdrawerFunction; ``` Withdraws tokens from the caller's ETA to a destination ATA. This is the primary withdrawal path — no ZK proof required. The withdrawal queues an Arcium MPC computation; the network processes it and calls back on-chain to execute the transfer. Follows the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern). ### Deps All fields are optional. * `accountInfoProvider?: AccountInfoProviderFunction`. * `getLatestBlockhash?: GetLatestBlockhash`. * `transactionForwarder?: TransactionForwarder`. ### Returns ```typescript theme={null} type ETAIntoATAWithdrawerFunction = ( destinationAddress: Address, mint: Address, withdrawalAmount: U64, options?: ETAIntoATAWithdrawerOptions, ) => Promise; ``` * `destinationAddress: Address` — wallet whose ATA receives the tokens. * `mint: Address` — token mint address. * `withdrawalAmount: U64` — amount in base token units to withdraw from the ETA. ### Options (`ETAIntoATAWithdrawerOptions`) ```typescript theme={null} interface ETAIntoATAWithdrawerOptions { readonly optionalData?: OptionalData32; readonly accountInfoCommitment?: Commitment; readonly computationOffset?: U64; readonly mpcCallbackDataOffset?: U128; readonly feeVaultOffset?: U128; // default 0n (sentinel: no fee-vault interaction) readonly destinationProgram?: Address; // default System Program (sentinel: no observer CPI) // plus observer-CPI passthrough accounts and a `hooks?: WithdrawalHooks` } ``` * `optionalData` — 32-byte caller metadata. **Pre-hashed only.** Default 32 zero bytes. * `accountInfoCommitment` — per-call commitment for RPC account reads. * `computationOffset`, `mpcCallbackDataOffset` — PDA offsets. Default to fresh CSPRNG values per call. * `feeVaultOffset`, `destinationProgram` — observer-forward CPI configuration. Leave at defaults unless you are intentionally routing post-callback into another program. * `hooks` — per-phase lifecycle hooks (`onValidationStart`, `onAccountFetchComplete`, `queueComputation`, `onComplete`, `onError`, etc.). There are no `priorityFees`, `awaitCallback`, `skipPreflight`, `maxRetries`, or `epochInfoCommitment` options on the V18 withdrawer. ### WithdrawResult ```typescript theme={null} interface WithdrawResult { readonly signatures: readonly TransactionSignature[]; readonly queueSignature: TransactionSignature; readonly callback?: CallbackOutcome; readonly rentClaim?: RentClaimOutcome; } type CallbackOutcome = | { readonly status: "finalized"; readonly signature?: TransactionSignature; readonly elapsedMs: number } | { readonly status: "pruned"; readonly elapsedMs: number } | { readonly status: "timed-out"; readonly elapsedMs: number }; type RentClaimOutcome = | { readonly claimed: true; readonly signature: TransactionSignature } | { readonly claimed: false; readonly reason: string }; ``` ### Errors Throws `EncryptedWithdrawalError`. See [Errors](./errors#encryptedwithdrawalerror). ### Example ```typescript theme={null} import { getETAIntoATAWithdrawerFunction } from "@umbra-privacy/sdk/withdrawal"; const withdraw = getETAIntoATAWithdrawerFunction({ client }); const result = await withdraw( destinationWallet, // tokens land in this wallet's ATA mintAddress, 500_000n, ); console.log("Queue signature:", result.queueSignature); if (result.callback?.status === "finalized") { console.log("Callback signature:", result.callback.signature); } ``` ### Dropped-callback recovery If the handler succeeds but the Arcium callback never lands, tokens stay staged in the pool ATA. Reclaim them with `getStagedSplRecovererFunction` / `getStagedSolRecovererFunction` from `@umbra-privacy/sdk/account` — synchronous, no MPC, no ZK proof. *** ## EncryptedWithdrawalError Thrown by the withdrawer factory. Stage values: `"initialization"` | `"validation"` | `"mint-fetch"` | `"pda-derivation"` | `"instruction-build"` | `"transaction-build"` | `"transaction-compile"` | `"transaction-sign"` | `"transaction-send"`. See [Errors](./errors#encryptedwithdrawalerror). # Burn Status Source: https://sdk.umbraprivacy.com/relayer/api-reference/claim-status openapi-relayer.yaml GET /v1/claims/{request_id} Poll the status of a submitted burn request. The wire path is /v1/claims/{request_id} (legacy). The SDK polls this endpoint automatically with a 3-second interval and a 120-second timeout. You only need to call it directly if building a custom integration. The wire path retains the legacy `/v1/claims/{request_id}` segment. In V18 SDK code you call it via the relayer client's `pollClaimStatus` method (or its `pollBurnStatus` alias when plugged into a burner factory's `relayer` dep). ## Polling recommendations * Poll every **3 seconds** (the SDK default). * Stop polling when status reaches a terminal state: `completed`, `failed`, or `timed_out`. * Maximum recommended polling duration: **120 seconds**. * If status is `timed_out`, the MPC callback was not received within the expected block window — the callback may still arrive later. **Verify the nullifier on-chain before re-submitting.** * If `failure_reason` contains `NullifierAlreadyBurnt`, treat the request as **idempotent success** — the SDK's burner factory does this automatically. ## Burn lifecycle A burn request progresses through these statuses in order: * `received` — accepted by the relayer API. * `validating` — preflight checks running (Merkle root, nullifiers, account state). * `offsets_reserved` — nullifier offsets reserved for deduplication. * `building_tx` — transactions being constructed. * `tx_built` — transactions ready. * `submitting` — sending to Solana. * `submitted` — confirmed on-chain. * `awaiting_callback` — waiting for Arcium MPC callback. * `callback_received` — MPC callback detected. * `finalizing` — running cleanup transactions. * `completed` — burn successful. * `failed` — error occurred (see `failure_reason` field). * `timed_out` — MPC callback deadline exceeded. # Relayer Health Source: https://sdk.umbraprivacy.com/relayer/api-reference/health openapi-relayer.yaml GET /v1/health Check if the relayer service is operational. Used by load balancers and monitoring systems to verify the relayer is running. # Relayer Info Source: https://sdk.umbraprivacy.com/relayer/api-reference/info openapi-relayer.yaml GET /v1/relayer/info Get relayer identity, supported mints, and active stealth pool indices. Use this endpoint to verify the relayer is operational and supports the mint you want to burn. The `mints` field is the authoritative runtime source of truth — the SDK's hardcoded mint list is a fallback only. The SDK calls this internally via `getUmbraRelayer()` to cache the relayer's fee payer address. # Submit Burn Source: https://sdk.umbraprivacy.com/relayer/api-reference/submit-claim openapi-relayer.yaml POST /v1/claims Submit a Stealth Pool Note burn request to the relayer for processing. The wire path is /v1/claims (legacy). This endpoint is designed to be called by the SDK's burner factories, not directly. The request body contains cryptographic proof data that must be generated correctly by the ZK prover. Use `getSelfBurnableStealthPoolNoteIntoETABurnerFunction`, `getReceiverBurnableStealthPoolNoteIntoETABurnerFunction`, or `getSelfBurnableStealthPoolNoteIntoATABurnerFunction` from `@umbra-privacy/sdk/burn` instead. The relayer validates the request, builds the on-chain transactions, and submits them asynchronously. Poll `GET /v1/claims/{request_id}` with the returned UUID to track progress. The wire path retains the legacy `/v1/claims` segment. In V18 SDK code you call it via the relayer client's `submitClaim` method (or its `submitBurn` alias when plugged into a burner factory's `relayer` dep). ## Burn variants * `encrypted_balance` — burn into an `EncryptedTokenAccount`. Requires additional Rescue-encrypted fee fields in `proof_account_data`. Each note slot has 6 linker encryptions and 6 key commitments. * `public_balance` — burn into an `AssociatedTokenAccount`. Requires `fee_proof_data` with the client's expected basis-points rates (`protocol_fee_basis_points`, `relayer_fee_basis_points`) and allowed-address PDA slots — the relayer verifies these against the on-chain `FeeSchedule` / `RelayerFeeSchedule` PDAs before submission, catching stale client config without wasting a transaction. Each note slot has 5 linker encryptions and 5 key commitments. ## Validation rules * All base58 pubkeys must decode to exactly 32 bytes. * All nullifiers within a request must be unique (duplicates return `409 DUPLICATE_OFFSET`). * `max_utxo_capacity` must be greater than 0. * `fee_proof_data` is required for `public_balance` variant only. ## Idempotency / `DUPLICATE_OFFSET` If a burn callback drops and you re-submit with the same `request_id` while the nullifier is still reserved upstream, the relayer returns **HTTP 409 with code `DUPLICATE_OFFSET`**. Wait, re-check on-chain that the nullifier has not landed, and retry only if still unspent. The SDK's burner factories handle this internally. # Relayer Overview Source: https://sdk.umbraprivacy.com/relayer/overview Burn relay at relayer.api.umbraprivacy.com and relayer.api-devnet.umbraprivacy.com. Async POST /v1/claims, poll GET /v1/claims/{id}. Lifecycle: received → completed. SDK uses via getUmbraRelayer. ## What is the Relayer? The Umbra relayer is a transaction submission service that builds, signs, and submits burn transactions for users. When you burn a Stealth Pool Note, the relayer pays the Solana transaction fees and submits the burn on your behalf — ensuring your wallet address never appears as the fee payer, preserving your privacy. The relayer is used internally by the SDK's burner factories. You do not need to interact with the relayer API directly unless you are building a custom integration. The wire protocol retains the legacy `claim` noun: the endpoint is `/v1/claims`, the JSON shape is `{ request_id, status, … }`, and the SDK's relayer client method names are `submitClaim` / `pollClaimStatus`. In V18 the burner factory's `relayer` dep uses the renamed property names `submitBurn` / `pollBurnStatus`, which are TypeScript aliases of the claim equivalents — the relayer client's existing methods plug in directly. **There is no breaking wire change.** ## Base URL * **Mainnet:** `https://relayer.api.umbraprivacy.com`. * **Devnet:** `https://relayer.api-devnet.umbraprivacy.com`. ## How It Works 1. You call a burner factory in the SDK (e.g. `getSelfBurnableStealthPoolNoteIntoETABurnerFunction`). 2. The SDK fetches per-batch Merkle proofs, generates the Groth16 proof, and constructs the burn-request payload locally. 3. The SDK submits the request to the relayer via `POST /v1/claims`. 4. The relayer validates, builds the on-chain transactions, and submits them. 5. The SDK polls `GET /v1/claims/{request_id}` until the request reaches a terminal status. 6. The burned tokens appear in the destination ETA or ATA. ## SDK Integration ```typescript theme={null} import { getUmbraRelayer } from "@umbra-privacy/sdk"; const relayer = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com", }); ``` The relayer client is wrapped into the burner factory's `relayer` dep: ```typescript theme={null} const burn = getSelfBurnableStealthPoolNoteIntoETABurnerFunction( { client }, { fetchBatchMerkleProof: client.fetchBatchMerkleProof!, zkProver, relayer: { submitBurn: relayer.submitClaim, // alias: BurnSubmitter ≡ ClaimSubmitter pollBurnStatus: relayer.pollClaimStatus, // alias: BurnStatusPoller ≡ ClaimStatusPoller getRelayerAddress: relayer.getRelayerAddress, }, }, ); ``` ## Burn Request Lifecycle A burn request progresses through these statuses (terminal values are bolded in the SDK's `BurnBatchOutcome.status`): * `received` — accepted by the relayer API. * `validating` — preflight checks running (Merkle root, nullifiers, account state). * `offsets_reserved` — nullifier offsets reserved for deduplication. * `building_tx` — transactions being constructed. * `tx_built` — transactions ready. * `submitting` — sending to Solana. * `submitted` — confirmed on-chain. * `awaiting_callback` — waiting for Arcium MPC callback. * `callback_received` — MPC callback detected. * `finalizing` — running cleanup transactions. * **`completed`** — burn successful. * **`failed`** — error occurred (see `failure_reason`). * **`timed_out`** — MPC callback deadline exceeded. * **`refunded`** — terminal recovery state. The MPC callback did not finalise and the relayer reclaimed any staged funds back to the user. A `failure_reason` containing `NullifierAlreadyBurnt` is **idempotent success** — the SDK's burner factory surfaces it as such and returns a clean status. ## Idempotency If the relayer's callback drops, re-submitting the same `request_id` while the nullifier is still reserved upstream returns **HTTP 409 with code `DUPLICATE_OFFSET`**. The burner factory polls to terminal state and verifies on-chain before retry; custom pipelines must implement the same contract. ## Response Format All relayer endpoints return JSON. ## Rate Limiting All endpoints are subject to rate limiting. Exceeded limits return `429 Too Many Requests`. # Account State Source: https://sdk.umbraprivacy.com/sdk/account-state getUserAccountQuerierFunction: read EncryptedUserAccount fields (X25519 key, user commitment, generation index, registration sub-step flags) for any address without submitting transactions. ## Checking Registration Status Use `getUserAccountQuerierFunction` from `@umbra-privacy/sdk/query` to inspect an account's current registration state without triggering any transactions: ```typescript theme={null} import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk/query"; const query = getUserAccountQuerierFunction({ client }); const result = await query(client.signer.address); if (result.state === "non_existent") { console.log("Not registered — call register() first"); } else { const { data } = result; console.log("Account initialised:", data.isInitialised); console.log("X25519 key registered:", data.isUserAccountX25519KeyRegistered); console.log("User commitment registered:", data.isUserCommitmentRegistered); console.log("Anonymous usage active:", data.isActiveForAnonymousUsage); } ``` ## When to Register Register once, at account setup time. Check state before calling `register()` so you avoid unnecessary transaction prompts for users who are already fully set up: ```typescript theme={null} import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk/query"; import { getUserRegistrationFunction } from "@umbra-privacy/sdk/registration"; const query = getUserAccountQuerierFunction({ client }); const result = await query(client.signer.address); const isFullyRegistered = result.state === "exists" && result.data.isInitialised && result.data.isUserAccountX25519KeyRegistered && result.data.isUserCommitmentRegistered; if (!isFullyRegistered) { const register = getUserRegistrationFunction({ client }); await register({ confidential: true, anonymous: true }); } ``` ## Pre-check the **recipient** before a receiver-burnable create This is the most common integration footgun. A receiver-burnable Stealth Pool Note encrypts the unlocker against the recipient's `userCommitment`, so the **recipient** must have all three flags (`isInitialised`, `isUserAccountX25519KeyRegistered`, `isUserCommitmentRegistered`) set before you write the note. Pre-check at the API boundary and either error with a clear message or fall back to a self-burnable create: ```typescript theme={null} const r = await query(recipientAddress); const ready = r.state === "exists" && r.data?.isInitialised && r.data?.isUserAccountX25519KeyRegistered && r.data?.isUserCommitmentRegistered; if (!ready) { // Either ask the recipient to register, or use the self-burnable creator // (sender keeps the unlocker; recipient needs zero on-chain state). } ``` ## Registration state fields The `data` object returned when `result.state === "exists"`: * `isInitialised` — the base `EncryptedUserAccount` PDA has been created. * `isUserAccountX25519KeyRegistered` — the X25519 token-encryption pubkey has been stored on-chain (Confidential-usage sub-step complete). Required for Shared-mode ETAs. * `isUserCommitmentRegistered` — the Poseidon user commitment has been stored (Anonymous-usage sub-step complete). Required to receive receiver-burnable notes. * `isActiveForAnonymousUsage` — the account is active and cleared for anonymous usage (both sub-steps complete and valid). * `x25519PublicKey` — the registered X25519 public key bytes, if the confidential sub-step is complete. * `userCommitment` — the registered Poseidon commitment, if the anonymous sub-step is complete. * `generationIndex` — monotonic counter used for nonce derivation. **Use this to derive the next note nonce** — never invent your own. * `randomGenerationSeed` — entropy bytes mixed into nonces. For the full type reference see [Query](/reference/query). # Compliance Source: https://sdk.umbraprivacy.com/sdk/compliance Two independent compliance mechanisms: hierarchical viewing keys (MVK + Poseidon-derived temporal keys) for Stealth Pool Notes, and X25519 compliance grants for EncryptedTokenAccount re-encryption. ## Overview Umbra's compliance system is entirely voluntary and user-initiated. Two independent mechanisms cover different parts of the protocol: Derive scoped cryptographic keys from your master viewing key and share them with auditors. Viewing keys give read access to Stealth Pool Note activity within a specific time window or token scope — without exposing anything outside that scope. Pure off-chain Poseidon hierarchy. Create on-chain grants that authorise Arcium MPC to re-encrypt your EncryptedTokenAccount balances under a grantee's X25519 key. The grantee then decrypts the re-encrypted ciphertexts with their own private key. The two mechanisms cover **different data**. Stealth Pool viewing keys only decrypt the Poseidon `pc_encrypted_*` fields on a note (the mixer pool ciphertext). X25519 grants only cover the Rescue `rc_encrypted_*` ciphertext on an ETA. **Viewing keys cannot decrypt ETA balances; X25519 grants cannot decrypt notes.** Pick the right tool for the data you are auditing. No party can access your data without your explicit action. Neither mechanism is reversible for data the grantee has already received — revoking a grant prevents future access but does not invalidate ciphertexts already re-encrypted. *** ## Stealth Pool viewing keys The **master viewing key (MVK)** is a BN254 field element derived from your master seed. It is the root of an off-chain key hierarchy you can share selectively: ``` Master Viewing Key └── Mint Viewing Key { mint } └── Yearly Viewing Key { year } └── Monthly Viewing Key { month } └── Daily Viewing Key { day } └── Hourly Viewing Key { hour } └── Minute Viewing Key { minute } └── Second Viewing Key { second } ← decrypts pc_encrypted_* ``` Each level is derived from the level above using a Poseidon hash. Sharing a key at any level grants read access to all note activity within that scope and nothing outside it. The Second Viewing Key (a.k.a. the transaction viewing key — TVK) is the one that actually decrypts a note's Poseidon stream cipher (`pc_encrypted_*`). Eight derivers ship in the SDK, all from the key-derivation primitives: ```typescript theme={null} import { getMasterViewingKeyDeriver, getMintViewingKeyDeriver, getYearlyViewingKeyDeriver, getMonthlyViewingKeyDeriver, getDailyViewingKeyDeriver, getHourlyViewingKeyDeriver, getMinuteViewingKeyDeriver, getSecondViewingKeyDeriver, } from "@umbra-privacy/sdk/crypto/key-derivation"; const mintVk = await getMintViewingKeyDeriver({ client })(MINT); // Export to hex for out-of-band sharing with auditors: const hex = mintVk.toString(16); ``` There is no shipped `getViewingKeyClaimableStealthPoolNoteScannerFunction` factory — compose one from the shipped primitives (\~50 lines). See [Stealth Pool Viewing Keys](/sdk/compliance-viewing-keys) for the canonical recipe. *** ## X25519 compliance grants X25519 compliance grants use on-chain PDAs to authorise Arcium MPC to re-encrypt your ETA ciphertexts under a grantee's X25519 key. You create the grant voluntarily, choose the receiver and a unique nonce, and can revoke it at any time. ```typescript theme={null} import { getComplianceGrantIssuerFunction } from "@umbra-privacy/sdk/compliance"; const createGrant = getComplianceGrantIssuerFunction({ client }); // Create a grant allowing `receiver` to re-encrypt your shared ETA ciphertexts. const result = await createGrant({ receiver, // receiver's wallet address (Address) granterX25519, // your MVK X25519 public key (X25519PublicKey) receiverX25519, // receiver's X25519 public key (X25519PublicKey) nonce, // u128 nonce (RescueCipherEncryptionNonce) }); ``` See [X25519 Compliance Grants](/sdk/compliance-x25519-grants) for the complete workflow — deriving the granter's MVK X25519 keypair, generating a random nonce, re-encrypting ciphertexts under the grant, and querying grant status. *** ## Compliance factory inventory All under `@umbra-privacy/sdk/compliance`: * `getComplianceGrantIssuerFunction` — create a grant. * `getComplianceGrantRevokerFunction` — revoke (deletes the grant PDA). * `getUserComplianceGrantQuerierFunction` — query a user-issued grant. * `getNetworkComplianceGrantQuerierFunction` — query a network-issued grant. * `getSharedComplianceGrantQuerierFunction` — query a Shared-balance grant. * `getSharedCiphertextReencryptorForUserGrantFunction` — re-encrypt under a user grant. * `getNetworkCiphertextReencryptorForNetworkGrantFunction` — re-encrypt under a network grant. * `getSharedCiphertextReencryptorForNetworkGrantFunction` — re-encrypt a Shared-mode ciphertext for a network grantee. ## Privacy and Trust Model * Compliance grants are stored on-chain as marker PDAs — their existence is transparent and auditable. * A viewing key at scope X gives access only to data within scope X — no access to anything outside that scope. * Revoking a compliance grant (deleting the PDA) prevents future re-encryption requests, but does not invalidate ciphertexts the grantee has already obtained. * The master viewing key and master seed are never shared directly through either mechanism. Rescue is a **stream cipher**. Never reuse a grant nonce — doing so leaks the XOR of two plaintexts. Use `generateRandomNonce` from `@umbra-privacy/sdk/arcium` for fresh nonces, and persist them per `(granter, receiver, mint)` if you need to re-issue with the same parameters. # Stealth Pool Viewing Keys Source: https://sdk.umbraprivacy.com/sdk/compliance-viewing-keys MVK hierarchy: master viewing key (BN254) derived via KMAC256, Poseidon-derived temporal and per-mint scoped keys. Grant auditors read access to specific Stealth Pool Note time windows or tokens. ## What Are Viewing Keys? The **master viewing key** (MVK) is a 252-bit BN254 field element derived deterministically from your master seed. It sits at the root of a hierarchical key system that lets you grant scoped read access to your Stealth Pool activity without ever exposing the root key or any data outside the defined scope. Viewing keys are designed for compliance and audit use cases: * Provide an auditor or regulator access to transactions within a specific time window * Grant a compliance officer access to all activity for a specific token * Share full visibility at the master level with a tax advisor or legal counterparty * Prove to an exchange that specific Stealth Pool Notes originated from you. Viewing keys are read-only credentials. Possessing a viewing key does not allow burning Stealth Pool Notes or withdrawing balances. They can only be used to decrypt ciphertext data that was encrypted under the associated key — specifically the `pc_encrypted_*` (Poseidon stream cipher) fields on a note. They cannot decrypt the AES-GCM or Rescue ciphertexts. ## The Viewing Key Hierarchy All keys in the hierarchy are BN254 field elements. Each level is derived from its parent using the Poseidon hash function. A key at any level grants access to all data within its scope - and nothing above or adjacent to it in the tree. ``` Master Viewing Key └── Mint Viewing Key { e.g. USDC · USDT · wSOL · ... } └── Yearly Viewing Key { e.g. 2024 · 2025 · ... } ├── Monthly Viewing Key { Jan } │ ├── Daily Viewing Key { Jan 01 } │ │ ├── Hourly Viewing Key { 00h } │ │ │ ├── Minute Viewing Key { 00m } │ │ │ │ └── Second Viewing Key { 00s } │ │ │ └── ... │ │ └── ... │ └── ... ├── Monthly Viewing Key { Feb } ├── Monthly Viewing Key { Mar } └── ... { Apr – Dec } ``` **Example scopes:** * Monthly key for `2025-02`: sees all USDC mixer activity in February 2025 * Yearly key for `2025`: sees all USDC mixer activity across all of 2025 * Mint key for USDC: sees all USDC mixer activity, all time * Master viewing key: sees everything across all tokens and all time ## Deriving Viewing Keys Each level of the hierarchy has a corresponding deriver factory function. Pass `{ client }` to get a deriver, then call it with the appropriate parameters. ### Master Viewing Key ```typescript theme={null} import { getMasterViewingKeyDeriver } from "@umbra-privacy/sdk/crypto/key-derivation"; // Derives the root of the hierarchy const deriveMvk = getMasterViewingKeyDeriver({ client }); const mvk = await deriveMvk(); // mvk is a bigint (BN254 field element) console.log("Master Viewing Key:", mvk.toString()); ``` The master viewing key gives unrestricted read access to all your mixer activity - past and future. Share it only with fully trusted parties. For audits with a limited scope, always use a scoped sub-key. ### Mint Viewing Key ```typescript theme={null} import { getMintViewingKeyDeriver } from "@umbra-privacy/sdk/crypto/key-derivation"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // Derives the mint-specific viewing key const deriveMintVk = getMintViewingKeyDeriver({ client }); const mintVk = await deriveMintVk(USDC); // Scope: all time, but only USDC activity console.log("USDC Viewing Key:", mintVk.toString()); ``` ### Yearly Viewing Key ```typescript theme={null} import { getYearlyViewingKeyDeriver } from "@umbra-privacy/sdk/crypto/key-derivation"; import type { Year } from "@umbra-privacy/sdk/types"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // Derives the yearly viewing key for 2025 under the USDC mint key const deriveYearlyVk = getYearlyViewingKeyDeriver({ client }); const yearlyVk = await deriveYearlyVk(USDC, 2025n as Year); // Scope: all USDC activity in 2025 only console.log("2025 USDC Viewing Key:", yearlyVk.toString()); ``` ### Monthly Viewing Key ```typescript theme={null} import { getMonthlyViewingKeyDeriver } from "@umbra-privacy/sdk/crypto/key-derivation"; import type { Year, Month } from "@umbra-privacy/sdk/types"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // Derives the monthly viewing key for February 2025 const deriveMonthlyVk = getMonthlyViewingKeyDeriver({ client }); const monthlyVk = await deriveMonthlyVk(USDC, 2025n as Year, 2n as Month); // Scope: USDC activity in February 2025 only console.log("Feb 2025 USDC Viewing Key:", monthlyVk.toString()); ``` ### Daily Viewing Key ```typescript theme={null} import { getDailyViewingKeyDeriver } from "@umbra-privacy/sdk/crypto/key-derivation"; import type { Year, Month, Day } from "@umbra-privacy/sdk/types"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // Derives the daily viewing key for February 14, 2025 const deriveDailyVk = getDailyViewingKeyDeriver({ client }); const dailyVk = await deriveDailyVk(USDC, 2025n as Year, 2n as Month, 14n as Day); // Scope: USDC activity on Feb 14, 2025 only console.log("Feb 14 2025 USDC Viewing Key:", dailyVk.toString()); ``` ## Exporting and Sharing a Viewing Key Viewing keys are `bigint` values (BN254 field elements). Export them as decimal or hex strings for out-of-band sharing with an auditor: ```typescript theme={null} // Export as decimal string (lossless, unambiguous) function exportViewingKey(key: bigint): string { return key.toString(); } // Export as hex string (compact) function exportViewingKeyHex(key: bigint): string { return "0x" + key.toString(16).padStart(64, "0"); } // Import back from decimal string function importViewingKey(decimal: string): bigint { return BigInt(decimal); } // Import back from hex string function importViewingKeyHex(hex: string): bigint { return BigInt(hex); } ``` ### Sharing Example ```typescript theme={null} import { getMonthlyViewingKeyDeriver } from "@umbra-privacy/sdk/crypto/key-derivation"; import type { Year, Month } from "@umbra-privacy/sdk/types"; // 1. Choose a scope - e.g., monthly key for an audit of Q1 2025 const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const deriveMonthlyVk = getMonthlyViewingKeyDeriver({ client }); const q1Keys = await Promise.all([ deriveMonthlyVk(USDC, 2025n as Year, 1n as Month), // January deriveMonthlyVk(USDC, 2025n as Year, 2n as Month), // February deriveMonthlyVk(USDC, 2025n as Year, 3n as Month), // March ]); // 2. Export as strings const q1Strings = q1Keys.map((key) => key.toString()); // 3. Share with auditor out-of-band (email, secure document, etc.) const auditPackage = { granterAddress: client.signer.address, token: USDC, period: "Q1 2025", viewingKeys: { "2025-01": q1Strings[0], "2025-02": q1Strings[1], "2025-03": q1Strings[2], }, }; console.log(JSON.stringify(auditPackage, null, 2)); ``` ## How Key Derivation Works Viewing keys are derived using the Poseidon hash function over BN254. Starting from the master viewing key: ``` MintViewingKey(mint) = Poseidon(MVK, mint_low_u128, mint_high_u128) YearlyViewingKey(mint, year) = Poseidon(MintViewingKey(mint), year) MonthlyViewingKey(mint, year, month) = Poseidon(YearlyViewingKey(mint, year), month) DailyViewingKey(mint, year, month, day) = Poseidon(MonthlyViewingKey(mint, year, month), day) ``` The mint address is split into two 128-bit halves before being passed to Poseidon, as BN254 field elements are 252-bit values and a 256-bit pubkey would exceed the field size. This structure means: * You cannot derive a parent key from a child key - the tree is one-directional * Child keys can always be re-derived from the parent at any time * Two separate mint keys cannot be linked without the master viewing key ## Security Properties * **One-directional** - a grantee who holds a monthly key cannot derive the yearly key, the master viewing key, or any key for another mint or time period * **Unlinkable** - viewing keys for different months or mints are computationally unrelated without the master key * **Deterministic** - the same wallet on the same network always produces the same viewing key hierarchy; keys can be re-derived at any time * **Non-transactional** - sharing a viewing key does not create any on-chain state; it is a purely off-chain credential ## What Auditors Can Do With a Viewing Key An auditor who holds a viewing key can: * Scan the Stealth Pool (Indexed Merkle Tree) for notes that were encrypted under the corresponding key. * Decrypt the note `pc_encrypted_*` payloads (Poseidon stream cipher) to learn the amount and recipient. * Verify that specific transactions occurred within the defined scope. An auditor cannot: * Access any data outside the scope of the key they hold. * Burn Stealth Pool Notes — viewing keys are read-only, not spending keys. * Link the activity to any adjacent scope without the parent key. * Decrypt the X25519 + AES-GCM ciphertext (`aes_encrypted_data`) or the Rescue ciphertext (`rc_encrypted_*`) — those use independent keys. No opinionated `getViewingKey…NoteScannerFunction` factory is shipped — auditors compose one from the shipped primitives. Pattern (\~50 lines): for each note, call `getSecondViewingKeyDeriver` for the granted scope, then call `getPoseidonDecryptor` with the Second Viewing Key as the cipher key and the keystream `Poseidon([transactionViewingKey, counter, 2n])` to recover the plaintext. # X25519 Compliance Grants Source: https://sdk.umbraprivacy.com/sdk/compliance-x25519-grants getComplianceGrantIssuerFunction and getComplianceGrantRevokerFunction: create/revoke on-chain PDA grants that authorize Arcium MPC to re-encrypt ETA ciphertexts under a grantee's X25519 key. ## How X25519 Compliance Grants Work Encrypted token account balances in Umbra are stored as ciphertexts on-chain. By default, only you can read your own balance - Arcium MPC will not re-encrypt your ciphertexts for a third party without explicit authorization. An **X25519 compliance grant** is an on-chain PDA that grants that authorization. Once the PDA exists, Arcium MPC is permitted to re-encrypt your ciphertexts under the grantee's X25519 key. The grantee can then decrypt the output locally using their own private key - without you needing to be online. ``` Without grant: Ciphertext (on-chain) ──► [grantee has no access - MPC will not re-encrypt] With X25519 compliance grant: Ciphertext (on-chain) ──► Arcium MPC re-encrypts ──► Re-encrypted under grantee's key └─► Grantee decrypts locally ``` The grant PDA is a **marker account** - its presence on-chain is the authorization. No data is stored in it beyond the discriminator. Once a re-encryption instruction is executed, the grantee receives a ciphertext encrypted under their own X25519 key. **They hold this permanently.** More critically: because Rescue is a stream cipher, possessing a re-encrypted ciphertext for a given nonce allows the grantee to derive the full keystream for that nonce. This means **all past and future encryptions produced under the same nonce are also permanently readable by the grantee** - not just the single ciphertext that was re-encrypted. Revoking the grant stops future re-encryption requests but cannot undo this. Treat each nonce as a scope of permanent disclosure once any re-encryption instruction for it has been executed. *** ## Prerequisites ### 1. Your X25519 Public Key (Granter Key) For user-granted compliance grants, the `granterX25519` parameter is **your MVK (master viewing key) X25519 public key** - the Curve25519 key derived from your master seed's MVK X25519 keypair. This is distinct from the user account X25519 key used for encrypting token balances. The MVK X25519 key is used to prove ownership of the master viewing key during grant creation. The SDK derives it internally via the `masterViewingKeyX25519KeypairGenerator` dependency. You can derive and read your MVK X25519 public key from the client: ```typescript theme={null} import { getMasterViewingKeyX25519KeypairGenerator } from "@umbra-privacy/sdk/crypto/key-derivation"; // The SDK derives this internally during grant creation. // You can also derive it explicitly if you need to share it with a grantee // so they can look up the grant PDA. const generateMvkKeypair = getMasterViewingKeyX25519KeypairGenerator({ client }); const mvkKeypairResult = await generateMvkKeypair(); const granterX25519 = mvkKeypairResult.x25519Keypair.publicKey; ``` ### 2. Receiver's X25519 Public Key The `receiverX25519` is the grantee's X25519 public key. This is the key that Arcium MPC will re-encrypt the ciphertext under. The receiver derives it from their own master seed. The receiver can share their X25519 public key with you out-of-band, or you can look it up from their registered user account: ```typescript theme={null} import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk/query"; const queryAccount = getUserAccountQuerierFunction({ client }); const receiverAccount = await queryAccount(receiverAddress); if (receiverAccount.state !== "exists" || !receiverAccount.data.isUserAccountX25519KeyRegistered) { throw new Error("Receiver has not registered an X25519 key"); } // receiverX25519 is a Uint8Array (32 bytes) const receiverX25519 = receiverAccount.data.x25519PublicKey; ``` ### 3. Nonce A compliance grant authorizes re-encryption of any Rescue cipher encryption scoped to a specific **X25519 public key + nonce** combination. Only ciphertexts encrypted under that exact pubkey and nonce are covered - anything encrypted under a different nonce is outside the scope of the grant. The nonce is also part of the on-chain PDA seed, so multiple independent grants (e.g. to different parties or different nonces) can coexist simultaneously. Generate a random nonce using the SDK's utility: ```typescript theme={null} import { generateRandomNonce } from "@umbra-privacy/sdk/arcium"; const nonce = generateRandomNonce(); // Random u128 bigint ``` Store the nonce - you will need it to delete the grant later, to look up the PDA, and to pass as the `inputEncryptionNonce` when triggering re-encryption. Because Rescue is a stream cipher, a grantee who obtains a re-encrypted ciphertext for a given nonce can derive the keystream for that nonce and read all encryptions produced under it - not just the one that was re-encrypted. Use a fresh nonce for each grant and each disclosure scope. Never reuse a nonce across grants you intend to keep independent. *** ## Creating a User-Granted Compliance Grant ```typescript theme={null} import { getComplianceGrantIssuerFunction, } from "@umbra-privacy/sdk/compliance"; import { getMasterViewingKeyX25519KeypairGenerator } from "@umbra-privacy/sdk/crypto/key-derivation"; import { generateRandomNonce } from "@umbra-privacy/sdk/arcium"; const createGrant = getComplianceGrantIssuerFunction({ client }); // Gather required keys const generateMvkKeypair = getMasterViewingKeyX25519KeypairGenerator({ client }); const mvkKeypairResult = await generateMvkKeypair(); const granterX25519 = mvkKeypairResult.x25519Keypair.publicKey; const nonce = generateRandomNonce(); // Create the on-chain grant PDA const signature = await createGrant( receiver, // Address - receiver's wallet address granterX25519, // X25519PublicKey - your MVK X25519 public key receiverX25519, // X25519PublicKey - receiver's X25519 public key nonce, // RcEncryptionNonce - 16-byte u128 ); console.log("Grant created:", signature); console.log("Save this nonce for later revocation:", nonce.toString()); ``` The transaction includes an Ed25519 signature over the grant parameters, produced using your MVK X25519 keypair's Ed25519 component. This proves to the on-chain program that the granter controls the master viewing key - without revealing the key itself. *** ## Querying a Grant Check whether a grant is active before attempting re-encryption: ```typescript theme={null} import { getUserComplianceGrantQuerierFunction } from "@umbra-privacy/sdk/compliance"; const queryGrant = getUserComplianceGrantQuerierFunction({ client }); const result = await queryGrant( granterX25519, // X25519PublicKey - granter's MVK X25519 key nonce, // RcEncryptionNonce - the same nonce used at creation receiverX25519, // X25519PublicKey - receiver's X25519 key ); if (result.state === "exists") { console.log("Grant is active - re-encryption is authorized"); } else { console.log("Grant not found - either never created or already revoked"); } ``` The grant PDA is derived deterministically from these three values, so the query is a pure account existence check with no network round-trip beyond the RPC call. *** ## Re-Encrypting Ciphertexts (Grantee Workflow) Once the grant is active, the **grantee** calls the re-encryption function. This triggers an Arcium MPC computation that decrypts the granter's Shared-mode ciphertexts and re-encrypts them under the receiver's X25519 key. ```typescript theme={null} import { getSharedCiphertextReencryptorForUserGrantFunction } from "@umbra-privacy/sdk/compliance"; // The grantee calls this (using their own client, not the granter's) const reencrypt = getSharedCiphertextReencryptorForUserGrantFunction({ client: granteeClient }); const signature = await reencrypt( granterX25519Key, // X25519PublicKey - granter's MVK X25519 key receiverX25519Key, // X25519PublicKey - receiver's (grantee's own) X25519 key nonce, // RcEncryptionNonce - the nonce used when creating the grant inputEncryptionNonce, // RcEncryptionNonce - the nonce of the ciphertext to re-encrypt ciphertexts, // readonly Uint8Array[] - 1 to 6 ciphertexts (32 bytes each) ); console.log("Re-encryption triggered:", signature); ``` ### Parameters The granter's MVK X25519 public key. Must match the key used when the grant was created. The receiver's (grantee's) X25519 public key. Arcium MPC will re-encrypt the output under this key. The 128-bit nonce from the grant creation. Used to locate the grant PDA on-chain. The nonce of the specific ciphertext you want re-encrypted. This is the nonce associated with the encrypted token account state at a specific point in time. An array of 1 to 6 ciphertext values (32 bytes each). Unused slots are padded with zero bytes. These are the raw encrypted balance ciphertexts fetched from the on-chain encrypted token account. Re-encryption follows the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern). The SDK submits the handler, waits for Arcium MPC to produce the re-encrypted output, then waits for the callback to confirm. The re-encrypted ciphertext is written on-chain in an MPC callback data PDA. *** ## Revoking a Grant Delete the grant PDA to prevent future re-encryption requests. The grant parameters must exactly match the original creation. ```typescript theme={null} import { getComplianceGrantRevokerFunction } from "@umbra-privacy/sdk/compliance"; const deleteGrant = getComplianceGrantRevokerFunction({ client }); const signature = await deleteGrant( receiver, // Address - receiver's wallet address granterX25519, // X25519PublicKey - your MVK X25519 key receiverX25519, // X25519PublicKey - receiver's X25519 key nonce, // RcEncryptionNonce - must be the original nonce ); console.log("Grant revoked:", signature); ``` Deleting the grant closes the PDA and returns the rent lamports to the fee payer. Revoking a grant stops future re-encryption requests but does not affect anything the grantee has already received. Any ciphertext they obtained before revocation remains permanently accessible to them - there is no mechanism to claw it back. Revocation is a forward-only control. *** ## Full End-to-End Workflow Example ```typescript theme={null} import { getComplianceGrantIssuerFunction, getComplianceGrantRevokerFunction, getUserComplianceGrantQuerierFunction, } from "@umbra-privacy/sdk/compliance"; import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk/query"; import { getMasterViewingKeyX25519KeypairGenerator } from "@umbra-privacy/sdk/crypto/key-derivation"; import { generateRandomNonce } from "@umbra-privacy/sdk/arcium"; // === GRANTER SIDE === // 1. Derive your MVK X25519 public key const generateMvkKeypair = getMasterViewingKeyX25519KeypairGenerator({ client: granterClient }); const mvkKeypairResult = await generateMvkKeypair(); const granterX25519 = mvkKeypairResult.x25519Keypair.publicKey; // 2. Look up the receiver's X25519 key const RECEIVER_ADDRESS = "GsbwXfJraMomNxBcpR3DBFyKCCmN9SKGzKFJBNKxRFkT"; const queryAccount = getUserAccountQuerierFunction({ client: granterClient }); const receiverAccount = await queryAccount(RECEIVER_ADDRESS); if (receiverAccount.state !== "exists") { throw new Error("Receiver is not registered"); } const receiverX25519 = receiverAccount.data.x25519PublicKey; // 3. Generate a nonce and save it const nonce = generateRandomNonce(); console.log("Nonce (save this):", nonce.toString()); // 4. Create the on-chain grant const createGrant = getComplianceGrantIssuerFunction({ client: granterClient }); const grantSig = await createGrant( RECEIVER_ADDRESS, granterX25519, receiverX25519, nonce, ); console.log("Grant created:", grantSig); // === GRANTEE SIDE === // (The grantee now calls re-encryption using their own client) // 5. (Optional) Grantee verifies the grant is active const queryGrant = getUserComplianceGrantQuerierFunction({ client: granteeClient }); const grantStatus = await queryGrant(granterX25519, nonce, receiverX25519); console.log("Grant active:", grantStatus.state === "exists"); // 6. Grantee triggers re-encryption of the granter's ciphertexts // (They provide: the ciphertexts from the on-chain encrypted balance account, the input nonce, etc.) // const reencrypt = getSharedCiphertextReencryptorForUserGrantFunction({ client: granteeClient }); // const reencryptSig = await reencrypt(granterX25519, receiverX25519, nonce, inputNonce, ciphertexts); // === GRANTER SIDE - Revoking === // 7. Later: revoke the grant const deleteGrant = getComplianceGrantRevokerFunction({ client: granterClient }); const deleteSig = await deleteGrant(RECEIVER_ADDRESS, granterX25519, receiverX25519, nonce); console.log("Grant revoked:", deleteSig); ``` *** ## PDA Structure For reference, the grant PDA is derived from these seeds (using Arcium-encoded byte representations): ``` seeds = [ SHA256("ArciumComplianceGrant"), SHA256("UserGrant"), granterX25519 (Arcium-encoded, 32 bytes), nonce (Arcium-encoded u128, 16 bytes), receiverX25519 (Arcium-encoded, 32 bytes), ] ``` The PDA's existence is the authorization - it contains no data beyond the account discriminator. # Conversion Source: https://sdk.umbraprivacy.com/sdk/conversion getNetworkEncryptionToSharedEncryptionConverterFunction: upgrade MXE-only ETAs to Shared mode. getMintEncryptionKeyRotatorFunction: rotate per-mint X25519 encryption keys (MPC). ## Overview When a user registers their X25519 key after already having encrypted token accounts in MXE-only mode, those existing balances remain in MXE-only mode. `getNetworkEncryptionToSharedEncryptionConverterFunction` upgrades them to **Shared mode** - re-encrypting the balance under both the Arcium MPC key and the user's X25519 key, enabling local balance queries in the future. This step is only necessary if you deposited tokens before registering your X25519 key (i.e., before calling `register({ confidential: true })`). Deposits made after X25519 registration automatically use Shared mode. ## Usage ```typescript theme={null} import { getNetworkEncryptionToSharedEncryptionConverterFunction } from "@umbra-privacy/sdk/conversion"; const convert = getNetworkEncryptionToSharedEncryptionConverterFunction({ client }); const result = await convert( mints, // readonly Address[] optionalData?, // OptionalData32 — pre-hashed only hooks?, // ConvertToSharedHooks microLamportsPerAcu?, // optional Arcium priority fee ); ``` ### Parameters An array of SPL or Token-2022 mint addresses to convert. Only accounts in MXE-only mode with an initialized balance will be converted. Others are automatically skipped and reported in `result.skipped`. 32 bytes of optional metadata stored with each conversion transaction. Defaults to all zeros. Optional per-mint + overall lifecycle hooks. Each per-mint conversion is an MPC step (`onPreSend`, `onPostSend`, `onMonitorStarted`, `onFinalized`). Optional Arcium compute-unit priority fee. ### Return Value ```typescript theme={null} type ConvertToSharedEncryptionResult = { converted: Map; skipped: Map; }; ``` * `converted` - mints that were successfully upgraded, mapped to their transaction signature * `skipped` - mints that were not processed, mapped to the reason they were skipped Skip reasons: * `"non_existent"` - No encrypted token account exists for this mint * `"not_initialised"` - The token account exists but is not initialized * `"already_shared"` - The token account is already in Shared mode (no-op) * `"balance_not_initialised"` - The Arcium balance has not been initialized yet ## Example ```typescript theme={null} import { getNetworkEncryptionToSharedEncryptionConverterFunction } from "@umbra-privacy/sdk/conversion"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const USDT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"; const convert = getNetworkEncryptionToSharedEncryptionConverterFunction({ client }); const result = await convert([USDC, USDT]); for (const [mint, signature] of result.converted) { console.log(`Converted ${mint}: ${signature}`); } for (const [mint, reason] of result.skipped) { console.log(`Skipped ${mint}: ${reason}`); } ``` ## Rotate Mint Encryption Key If a per-mint X25519 key may have been compromised, use the mint X25519 rotator from `@umbra-privacy/sdk/account`: ```typescript theme={null} import { getMintEncryptionKeyRotatorFunction } from "@umbra-privacy/sdk/account"; const rotateMintKey = getMintEncryptionKeyRotatorFunction({ client }); const result = await rotateMintKey({ mint }); ``` This is an MPC operation — it re-encrypts the on-chain balance under the new per-mint key. The account must already be in Shared mode. See [Recovery & Rotation](/sdk/advanced/recovery) for details on when and how to rotate keys. ## When to Call This A typical onboarding flow checks whether conversion is needed after completing registration: ```typescript theme={null} import { getUserRegistrationFunction } from "@umbra-privacy/sdk/registration"; import { getNetworkEncryptionToSharedEncryptionConverterFunction } from "@umbra-privacy/sdk/conversion"; import { getEncryptedBalanceQuerierFunction } from "@umbra-privacy/sdk/query"; // 1. Register (ensures X25519 key is on-chain). const register = getUserRegistrationFunction({ client }); await register({ confidential: true, anonymous: true }); // 2. Check which mints need conversion. const queryBalance = getEncryptedBalanceQuerierFunction({ client }); const MINTS_TO_CHECK = [USDC, USDT]; const balances = await queryBalance(MINTS_TO_CHECK); const mxeOnlyMints = [...balances.entries()] .filter(([, result]) => result.state === "mxe") .map(([mint]) => mint); if (mxeOnlyMints.length > 0) { // 3. Upgrade MXE-only balances to Shared mode. const convert = getNetworkEncryptionToSharedEncryptionConverterFunction({ client }); const conversionResult = await convert(mxeOnlyMints); console.log(`Converted ${conversionResult.converted.size} balance(s) to Shared mode`); } ``` ## Error Handling Conversion processes each mint sequentially. If a transaction fails or is cancelled mid-way, any mints already converted will have their signatures in `result.converted` - they are not rolled back. ```typescript theme={null} import { isConversionError } from "@umbra-privacy/sdk/errors"; try { const result = await convert([USDC, USDT, WSOL]); console.log(`Converted ${result.converted.size} balance(s)`); console.log(`Skipped ${result.skipped.size} balance(s)`); } catch (err) { if (isConversionError(err)) { switch (err.stage) { case "transaction-sign": // User rejected a per-mint transaction in their wallet. // Any mints converted before this point are already confirmed on-chain. // Re-call convert() with the remaining mints to resume. showNotification("Conversion cancelled. Progress has been saved."); break; case "account-fetch": // RPC connectivity issue while fetching token account state. console.error("RPC error during conversion:", err.message); break; case "transaction-send": // Transaction submitted but confirmation timed out. // Any mints already converted in this batch are confirmed on-chain. console.warn("Confirmation timeout. Check on-chain before retrying."); break; default: // Other stages: initialization, pda-derivation, instruction-build, // transaction-build, transaction-compile, transaction-validate. console.error("Conversion failed at stage:", err.stage, err); } } else { throw err; } } ``` Conversion is idempotent for mints that are already in Shared mode - they are returned in `result.skipped` with reason `"already_shared"`. It is safe to call `convert()` repeatedly with the same mint list. See [Error Handling](/reference/errors) for a full reference of all error types. ## How It Works The conversion follows the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern). For each eligible token account: 1. The handler instruction queues a re-encryption computation on Arcium 2. Arcium MPC decrypts the MXE-only balance and re-encrypts it under both the MPC key and the user's X25519 key 3. The callback instruction updates the on-chain account to Shared mode After conversion, future balance queries will be able to decrypt locally using the user's X25519 private key. # Creating a Client Source: https://sdk.umbraprivacy.com/sdk/creating-a-client Call getUmbraClient(args: {signer, network, rpcUrl}) to create an IUmbraClient. Covers per-call commitment override model, deps injection, and client reuse across factory functions. ## Overview `IUmbraClient` is the entry point to the SDK. It is a plain configuration object (not a class) that holds your wallet, network settings, and all pre-constructed Solana infrastructure providers. You create one client at startup and pass it to every service factory function throughout your application. ## `getUmbraClient` ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; const client = await getUmbraClient(args, deps?); ``` `getUmbraClient` is async. It returns a `Promise`. By default the wallet is prompted to sign the master seed derivation message at construction time - the client resolves only after the seed is cached. Pass `deferMasterSeedSignature: true` to defer this prompt until the first operation that needs key material instead - see [Master Seed Derivation](#master-seed-derivation) below. ### Required Arguments Your wallet signer. Must implement `signTransaction`, `signTransactions`, `signMessage`, and expose an `address` property. See [Wallet Adapters](/sdk/wallet-adapters). The Solana network to connect to. Determines which program addresses and Arcium cluster endpoints are used. HTTP endpoint for your Solana JSON-RPC node. Used to fetch account data, submit transactions, and query blockhashes. ``` https://api.mainnet-beta.solana.com # Solana public node https://my-node.quiknode.pro/abc123/ # QuikNode https://rpc.helius.xyz/?api-key=abc123 # Helius ``` WebSocket endpoint for the same RPC node. Used by the transaction forwarder to subscribe to signature confirmations instead of polling. ``` wss://api.mainnet-beta.solana.com wss://my-node.quiknode.pro/abc123/ ``` Base URL for the Umbra indexer. Required for Stealth Pool Note discovery and Merkle proof generation when using the Stealth Pool. Omit if you are only using EncryptedTokenAccounts (deposit / withdraw / convert) and not the pool. ``` https://utxo-indexer.api.umbraprivacy.com # mainnet ``` Base URL for the Umbra relayer. Required for burning Stealth Pool Notes — the burner factory's `relayer` dep is built from a client constructed against this endpoint via `getUmbraRelayer({ apiEndpoint })`. Omit if you only need to read state or do non-burn operations. ``` https://relayer.api.umbraprivacy.com # mainnet ``` Persistent storage for scan cursors and decrypted Stealth Pool Note data. **Strongly recommended on the browser** — without it the scanner re-scans every active tree from genesis on every call. Use `createShardedUtxoDataStore({ storageBackend })` from `@umbra-privacy/sdk/store-adapters` with `createBrowserStorageBackend({ dbName })`. Persistent storage for nullifiers discovered during scanning. Same provenance and recommendation as `utxoDataStore` — use `createShardedNullifierStore({ storageBackend })`. There is no client-level `commitment` parameter. Commitment is configured per-call via options such as `accountInfoCommitment` and `epochInfoCommitment` on each service function. All default to `"confirmed"` when not specified. Controls when the wallet is prompted to sign the master seed derivation message. * `false` (default) - eager derivation. `getUmbraClient` awaits the wallet signature before resolving. The seed is cached before the function returns. Use this to surface the prompt at a predictable point in your onboarding flow (e.g., immediately after the user clicks "Connect Wallet"). * `true` - lazy derivation. The client is constructed instantly with no wallet prompt. The prompt fires on the first operation that needs cryptographic key material (typically `register()`). ```typescript theme={null} // Default - wallet prompt fires at construction const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl, }); // Seed is already cached - no prompt during any subsequent operation await register({ confidential: true, anonymous: true }); ``` U512 key rotation offsets. All default to `0n`. Increment a specific offset to rotate that key without changing your wallet. See the [Key Derivation](/sdk/advanced/key-derivation) guide for details. Available keys: `masterViewingKey`, `poseidonPrivateKey`, `x25519UserAccountPrivateKey`, `x25519MasterViewingKeyEncryptingPrivateKey`, `mintX25519PrivateKey`, `rescueCommitmentBlindingFactor`, `randomCommitmentFactor`. ### Optional Dependency Overrides The second argument, `deps`, lets you override individual infrastructure providers. This is primarily used for testing - you can inject mocks without a live RPC node or wallet. Override the function used to fetch on-chain account data. Defaults to an RPC-based implementation constructed from `rpcUrl`. Override the function used to fetch the latest blockhash for transaction lifetime. Defaults to an RPC-based implementation. Override how transactions are broadcast and confirmed. Defaults to a WebSocket-based forwarder. Swap this out to use Jito bundles or a custom priority fee strategy. Override the epoch info provider used for Token-2022 transfer fee calculations. Defaults to an RPC-based implementation. Custom persistence for the master seed. Defaults to in-memory storage (lost on reload). Override `load`, `store`, and `generate` to persist the seed in secure storage. See [Wallet Adapters - Persisting the Master Seed](/sdk/wallet-adapters#persisting-the-master-seed). ## What the Client Stores Once constructed, the client exposes these properties (read-only): * `client.signer` — your wallet signer. * `client.network` — `"mainnet"` | `"devnet"` | `"localnet"`. * `client.networkConfig` — resolved program addresses and cluster config. * `client.accountInfoProvider` — pre-built RPC account fetcher. * `client.blockhashProvider` — pre-built blockhash fetcher. * `client.transactionForwarder` — pre-built transaction broadcaster. * `client.epochInfoProvider` — pre-built epoch info provider. * `client.masterSeed.getMasterSeed()` — async function that derives (and caches) the master seed. * `client.fetchBatchMerkleProof` — present when `indexerApiEndpoint` was supplied; used by the burner factories to fetch the per-batch Merkle proof at submit time. * `client.utxoDataStore`, `client.nullifierStore` — present when the stores were supplied; consulted by the scanner for cursor state and known nullifiers. ## Master Seed Derivation The master seed is a 64-byte root secret derived from a deterministic wallet signature. It is the root of Umbra's key hierarchy - all encryption keys and commitments flow from it. The signer is critical to this process. The master seed is generated by having the signer sign a deterministic message; the resulting signature is passed through KMAC256 to produce the seed. This means the signer's identity directly determines every derived key - the viewing keys, nullifier keys, X25519 keypairs, and all cryptographic commitments for that wallet. This requirement can be bypassed entirely by overriding `deps.masterSeedStorage.generate` with a custom implementation. If you supply your own `generate` function, the signer's `signMessage` is never called for seed derivation - your function is responsible for returning the master seed. In that case the signer is still used only for transaction signing. See [Understanding the SDK](/sdk/understanding-the-sdk/overview) for details. ### Eager mode (default) With `deferMasterSeedSignature: false` (the default), the wallet prompt fires at `getUmbraClient` call time. The seed is cached before the function resolves. All subsequent operations use the cached seed with no further prompts. ```typescript theme={null} // Wallet prompt fires here const client = await getUmbraClient({ signer, network, rpcUrl, rpcSubscriptionsUrl }); // No prompt - seed is already cached const register = getUserRegistrationFunction({ client }); await register({ confidential: true, anonymous: true }); // All subsequent operations reuse the cached seed await deposit(signer.address, USDC_MINT, 1_000_000n); ``` ### Lazy mode With `deferMasterSeedSignature: true`, the client is constructed instantly with no wallet prompt. The seed is derived on demand - the first time any operation needs cryptographic key material. For most users, this happens during `register()`. ```typescript theme={null} // Instant - no wallet prompt const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl, deferMasterSeedSignature: true, }); // Wallet signs here for the first time await register({ confidential: true, anonymous: true }); ``` After the first derivation, the seed is cached in memory for the lifetime of the client object. The user is not prompted again unless the client is recreated. ## Example: Full Client Setup (browser, recommended) ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; import { createBrowserStorageBackend, createShardedUtxoDataStore, createShardedNullifierStore, } from "@umbra-privacy/sdk/store-adapters"; const storageBackend = createBrowserStorageBackend({ dbName: "umbra" }); const utxoDataStore = createShardedUtxoDataStore({ storageBackend }); const nullifierStore = createShardedNullifierStore({ storageBackend }); const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl: "https://rpc.helius.xyz/?api-key=YOUR_KEY", rpcSubscriptionsUrl: "wss://rpc.helius.xyz/?api-key=YOUR_KEY", indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com", relayerApiEndpoint: "https://relayer.api.umbraprivacy.com", utxoDataStore, nullifierStore, }); ``` ## Example: Node.js (no browser persistence) ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl: "https://api.mainnet-beta.solana.com", rpcSubscriptionsUrl: "wss://api.mainnet-beta.solana.com", indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com", relayerApiEndpoint: "https://relayer.api.umbraprivacy.com", }); ``` Omitting `utxoDataStore` and `nullifierStore` is acceptable for short-lived scripts or tests, but means every `scan()` walks every active tree from scratch. For any long-running process, wire a Node-side store (you can implement `IUtxoDataStore` / `INullifierStore` against any KV — disk, Redis, Postgres). ## Example: Testing with Mocks ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; const client = await getUmbraClient( { signer: mockSigner, network: "localnet", rpcUrl: "http://127.0.0.1:8899", rpcSubscriptionsUrl: "ws://127.0.0.1:8900", }, { accountInfoProvider: mockAccountInfoProvider, transactionForwarder: mockTransactionForwarder, masterSeedStorage: { generate: async () => fixedTestSeed, // deterministic seed for tests }, } ); ``` # Deposit Source: https://sdk.umbraprivacy.com/sdk/deposit getATAIntoETADirectDepositorFunction: transfer SPL/Token-2022 tokens from an ATA to an EncryptedTokenAccount. Returns DepositResult with net amount and tx signatures. ## Overview The `deposit` operation moves tokens from a user's `AssociatedTokenAccount (ATA)` into their `EncryptedTokenAccount (ETA)`. After a successful deposit, the balance is hidden on-chain. Tokens are moved from the user's `AssociatedTokenAccount` into the pool custody account — an on-chain SPL or Token-2022 account controlled by the Umbra program. Protocol fees and any Token-2022 transfer fees are subtracted from the incoming amount. The net amount is what gets credited to the ETA. The net amount is added to the destination user's ETA. The balance is hidden on-chain — it can only be decrypted by the Arcium MPC network and, in Shared mode, by the user's own X25519 key. The user must be [registered](/sdk/registration) before depositing. An `EncryptedUserAccount` PDA must exist for the destination wallet address. Deposit transactions are publicly visible on-chain. The depositor's wallet address, the destination address, and the gross transfer amount are all readable from the transaction. Only the resulting ETA balance is hidden — the act of shielding itself is not private. ## Usage ```typescript theme={null} import { getATAIntoETADirectDepositorFunction } from "@umbra-privacy/sdk/deposit"; const deposit = getATAIntoETADirectDepositorFunction({ client }); const result = await deposit( destinationAddress, // whose encrypted balance to credit mint, // SPL or Token-2022 mint address transferAmount, // amount in native token units (U64) options?, // optional ); ``` ## Parameters The wallet address whose ETA is credited. This is usually `client.signer.address` (depositing to yourself), but you can deposit into any registered user's account — the funds will be encrypted under *their* X25519 key. The SPL token mint address. Works with any standard SPL token and Token-2022 mints (see [Token-2022 Support](/advanced/token-2022) for transfer fee handling). The gross amount to transfer, in the token's native units (accounting for decimals). For example, `1_000_000n` for 1 USDC (6 decimals). Protocol fees and Token-2022 transfer fees are **subtracted** from this amount before the encrypted balance is credited. 32 bytes of caller metadata stored with the deposit. Defaults to 32 zero bytes. **Must be a pre-hashed or pre-encrypted 32-byte value** — never store plaintext identifiers. Commitment level used for RPC account reads during deposit preparation. Commitment level used for fetching epoch info (Token-2022 transfer-fee schedule). Per-phase lifecycle hooks. Keys: `onValidationStart`, `onValidationComplete`, `onMintFetchStart`, `onMintFetchComplete`, `onAccountFetchStart`, `onAccountFetchComplete`, `onArciumSetupStart`, `onArciumSetupComplete`, `onInstructionBuildStart`, `onInstructionBuildComplete`, `queueComputation` (`MpcTransactionStepHooks` — `onPreSend`, `onPostSend`, `onMonitorStarted`, `onProgress`, `onFinalized`, `onRentReclaimSubmitted`, `onRentReclaimError`), `onComplete`, `onError`. Each receives a typed event object. There are no `priorityFees`, `awaitCallback`, `skipPreflight`, `maxRetries`, or `purpose` options on the depositor. Priority fees are configured at factory time via `deps.microLamportsPerAcuPolicy`. Callback waiting is always-on for direct deposits — the result's `callback` field is populated on success. ## Return Value Returns a `Promise`. The `DepositResult` object contains: ```typescript theme={null} interface DepositResult { readonly signatures: readonly TransactionSignature[]; // every tx signature submitted, in order readonly queueSignature: TransactionSignature; // the handler (queue computation) signature readonly callback?: CallbackOutcome; // present once the callback round-trip completes (always awaited) readonly rentClaim?: RentClaimOutcome; // rent reclamation outcome (best-effort) } type CallbackOutcome = | { readonly status: "finalized"; readonly signature?: TransactionSignature; readonly elapsedMs: number } | { readonly status: "pruned"; readonly elapsedMs: number } | { readonly status: "timed-out"; readonly elapsedMs: number }; type RentClaimOutcome = | { readonly claimed: true; readonly signature: TransactionSignature } | { readonly claimed: false; readonly reason: string }; ``` * `signatures` — every transaction signature submitted by this call, in submission order (handler, callback, rent claim). * `queueSignature` — the handler (queue computation) transaction signature. * `callback` — present once the callback round-trip completes (always awaited) (the default). Discriminated on `status`: `"finalized"` means the callback landed and `signature` is the MPC callback tx signature; `"pruned"` / `"timed-out"` indicate the callback never landed. * `rentClaim` — present once rent reclamation has been attempted. `{ claimed: true }` returns the rent-reclaim signature; `{ claimed: false }` returns the reason. Rent reclamation is best-effort — the deposit itself still succeeded even when `claimed: false`. Deposits follow the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern) - the SDK submits the handler transaction, waits for Arcium to complete the MPC computation, then waits for the callback transaction to confirm. ## Encryption Mode Selection The SDK automatically selects the correct instruction variant based on the state of the destination user's account: * **New MXE-only balance** — destination has no X25519 key registered; first deposit. * **Existing MXE-only balance** — destination has no X25519 key; balance already exists. * **New Shared balance** — destination has X25519 key registered; first deposit. * **Existing Shared balance** — destination has X25519 key; balance already exists. You do not need to specify the mode manually. The destination address's `isUserAccountX25519KeyRegistered` flag drives the selection. ## Protocol Fees Fees are deducted from the `transferAmount` before crediting the ETA. With the BPS divisor of 16,384: ``` credited = transferAmount - baseFee - floor((transferAmount - baseFee) * commissionBps / 16384) ``` Protocol-fee configuration is set on-chain by the pool admin and may vary per pool. The current default is a small fixed base fee plus a 35-bps commission. See [Pricing](/pricing) for full details. In most cases, direct deposits from your own ATA to your own ETA carry **zero protocol fees** — the `baseFee` and `commissionBps` are both set to 0 for standard self-shielding. Fees may apply for deposits routed through relayers or for specific pool configurations. ## Example: Self-Deposit ```typescript theme={null} import { getATAIntoETADirectDepositorFunction } from "@umbra-privacy/sdk/deposit"; const deposit = getATAIntoETADirectDepositorFunction({ client }); // Deposit 100 USDC into your own ETA. const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const ONE_HUNDRED_USDC = 100_000_000n; // 6 decimals const result = await deposit(client.signer.address, USDC, ONE_HUNDRED_USDC); console.log("Queue signature:", result.queueSignature); if (result.callback?.status === "finalized") { console.log("Callback signature:", result.callback.signature); } ``` ## Example: Deposit to Another User You can shield tokens directly into another registered user's ETA: ```typescript theme={null} const RECIPIENT = "GsbwXfJraMomNxBcpR3DBFyKCCmN9SKGzKFJBNKxRFkT"; const result = await deposit(RECIPIENT, USDC, ONE_HUNDRED_USDC); ``` The tokens are encrypted under the recipient's X25519 key. Only they can withdraw them. ## Error Handling Deposit errors include a `stage` field identifying where the failure occurred: ```typescript theme={null} import { isEncryptedDepositError } from "@umbra-privacy/sdk/errors"; try { await deposit(destinationAddress, mint, amount); } catch (err) { if (isEncryptedDepositError(err)) { switch (err.stage) { case "validation": // Invalid arguments - check destinationAddress, mint, amount break; case "mint-fetch": // Could not fetch mint account - check RPC connectivity and mint address break; case "fee-calculation": // Token-2022 transfer fee calculation failed break; case "account-fetch": // Could not fetch the destination user account or token account break; case "transaction-send": // Transaction submitted but confirmation failed - may still have landed break; default: // Other stages: pda-derivation, instruction-build, transaction-build, // transaction-compile, transaction-sign, transaction-validate console.error("Deposit failed at stage:", err.stage, err); } } } ``` See [Error Handling](/reference/errors) for a full reference. # Installation Source: https://sdk.umbraprivacy.com/sdk/installation Install @umbra-privacy/sdk@5.x. The SDK ships everything in one package — operations are imported from named subpaths (/registration, /deposit, /burn, /query, …); the ZK prover lives at /zk-prover. ## Package The SDK is a single package — `@umbra-privacy/sdk`. The ZK prover, indexer client, relayer client, store adapters, and Solana primitives are all subpaths of the same package. **There is no separate `@umbra-privacy/web-zk-prover`** — it is folded into `@umbra-privacy/sdk/zk-prover`. ```bash pnpm theme={null} pnpm add @umbra-privacy/sdk@5.0.0-rc.6 snarkjs ``` ```bash npm theme={null} npm install @umbra-privacy/sdk@5.0.0-rc.6 snarkjs ``` ```bash yarn theme={null} yarn add @umbra-privacy/sdk@5.0.0-rc.6 snarkjs ``` `snarkjs` is a **required peer dependency** — the SDK imports it for Groth16 proving. Install it alongside the SDK (as shown above) or the first prover import throws `ERR_MODULE_NOT_FOUND`. Pin the SDK to an exact version (no `^`, no `~`). The ZK assets are versioned in lockstep with the package; a floating version can resolve a manifest that points at a different circuit hash than the one your wallet's master seed was set up for. ## TypeScript The SDK is written in TypeScript and ships full type declarations. No additional `@types` packages are needed. Minimum TypeScript version: **5.0**. For Node.js: ```json theme={null} { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true } } ``` ## Subpath imports The main `@umbra-privacy/sdk` barrel only re-exports: * `getUmbraClient`, the four signer factories (`createInMemorySigner`, `createSignerFromPrivateKeyBytes`, `createSignerFromKeyPair`, `createSignerFromWalletAccount`). * The full error hierarchy and the `Result` helpers. * Everything from `infrastructure/{arcium, indexer, relayer, solana, zk-prover}` — including `getUmbraRelayer`, `getUmbraIndexer`, RPC providers, transaction forwarders. **Every operation factory lives behind a named subpath.** This keeps the main barrel small and helps bundlers tree-shake aggressively. The subpath map: * `@umbra-privacy/sdk/client` — `IUmbraClient`, `IUmbraSigner` interfaces. * `@umbra-privacy/sdk/shared` — protocol-wide constants. **`UMBRA_MESSAGE_TO_SIGN` lives here**, not on the main barrel. * `@umbra-privacy/sdk/registration` — `getUserRegistrationFunction` + executors. * `@umbra-privacy/sdk/deposit` — direct depositor + the four Stealth Pool Note creators. * `@umbra-privacy/sdk/withdrawal` — ETA → ATA withdrawer. * `@umbra-privacy/sdk/burn` — note scanner + three burner factories + `enrichWithMerkleProof`. * `@umbra-privacy/sdk/query` — encrypted-balance + user-account queriers. * `@umbra-privacy/sdk/conversion` — MXE-only → Shared converter. * `@umbra-privacy/sdk/compliance` — grant issue / revoke / re-encrypt / query. * `@umbra-privacy/sdk/account` — staged-token recoverers, key rotation, maintenance. * `@umbra-privacy/sdk/zk-prover` — Groth16 provers (one factory per circuit) + `getCdnZkAssetProvider`. * `@umbra-privacy/sdk/zk-prover/cdn` — direct access to the CDN asset provider. * `@umbra-privacy/sdk/store-adapters` — `createBrowserStorageBackend`, `createShardedUtxoDataStore`, `createShardedNullifierStore`. * `@umbra-privacy/sdk/store` — base store interfaces (for custom storage backends). * `@umbra-privacy/sdk/indexer`, `@umbra-privacy/sdk/indexer/utxo`, `@umbra-privacy/sdk/indexer/nullifier` — typed indexer clients. * `@umbra-privacy/sdk/relayer` — typed relayer client (re-exported from main barrel). * `@umbra-privacy/sdk/solana`, `@umbra-privacy/sdk/pda`, `@umbra-privacy/sdk/token` — Solana primitives. * `@umbra-privacy/sdk/arcium` — Arcium MPC types + helpers. * `@umbra-privacy/sdk/constants` — `BPS_DIVISOR`, mint lists, etc. * `@umbra-privacy/sdk/types` — branded primitives (`U64`, `Address`, …). * `@umbra-privacy/sdk/crypto`, `…/crypto/poseidon`, `…/crypto/rescue`, `…/crypto/aes`, `…/crypto/key-derivation`, `…/crypto/challenges`, `…/crypto/commitment` — cryptographic primitives. * `@umbra-privacy/sdk/errors` — error classes + `is*` type guards. * `@umbra-privacy/sdk/converters`, `@umbra-privacy/sdk/temporal`, `@umbra-privacy/sdk/validation`, `@umbra-privacy/sdk/hooks`, `@umbra-privacy/sdk/pipeline`, `@umbra-privacy/sdk/fee-provider`, `@umbra-privacy/sdk/math` — utilities. A typical app import block: ```typescript theme={null} // Main barrel — client + signer + infrastructure import { getUmbraClient, createSignerFromWalletAccount, getUmbraRelayer, } from "@umbra-privacy/sdk"; // Operation subpaths import { getUserRegistrationFunction } from "@umbra-privacy/sdk/registration"; import { getATAIntoETADirectDepositorFunction } from "@umbra-privacy/sdk/deposit"; import { getETAIntoATAWithdrawerFunction } from "@umbra-privacy/sdk/withdrawal"; import { getBurnableStealthPoolNoteScannerFunction, getReceiverBurnableStealthPoolNoteIntoETABurnerFunction, } from "@umbra-privacy/sdk/burn"; // ZK prover (same package, named subpath) import { getATAIntoStealthPoolNoteCreatorProver, getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver, } from "@umbra-privacy/sdk/zk-prover"; // Browser store adapters (recommended for any scanning workload) import { createBrowserStorageBackend, createShardedUtxoDataStore, createShardedNullifierStore, } from "@umbra-privacy/sdk/store-adapters"; // Error classification import { isEncryptedDepositError } from "@umbra-privacy/sdk/errors"; ``` The **burner** ZK prover factories are spelled `getClaim…ClaimableUtxo…`. The **creator** prover factories are `getETAIntoStealthPoolNoteCreatorProver` (ETA-source) and `getATAIntoStealthPoolNoteCreatorProver` (ATA-source). ## Runtime Requirements The SDK works in both Node.js (18+) and modern browser environments. It has no native dependencies — all cryptography is pure TypeScript / WebAssembly. If you are using a bundler (Vite, webpack, Next.js), no special configuration is needed for the SDK itself. For Next.js, add `@umbra-privacy/sdk` to `transpilePackages` in `next.config.ts` if you hit ESM/CJS interop issues. # Burning Notes Source: https://sdk.umbraprivacy.com/sdk/mixer/claiming-utxos 3 V18 burner factory functions: receiver-burnable → ETA, self-burnable → ETA, self-burnable → ATA. Each needs a fetchBatchMerkleProof, a circuit-specific zkProver, and a relayer adapter ({submitBurn, pollBurnStatus, getRelayerAddress}). ## Overview Burning a Stealth Pool Note presents a ZK proof on-chain that you know the secret inputs behind a Merkle tree commitment, burns the nullifier to prevent double-spending, and releases the tokens. You choose where the tokens go: * **Into an EncryptedTokenAccount** — tokens remain private after burning. Default for receiver-burnable. * **Into an AssociatedTokenAccount** — tokens become visible in the destination ATA. Self-burnable only in V18. ## V18 burner factories All three live under `@umbra-privacy/sdk/burn`. ### Receiver-burnable → ETA ```typescript theme={null} import { getReceiverBurnableStealthPoolNoteIntoETABurnerFunction, } from "@umbra-privacy/sdk/burn"; import { getUmbraRelayer } from "@umbra-privacy/sdk"; import { getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver } from "@umbra-privacy/sdk/zk-prover"; const zkProver = getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(); const r = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com" }); const burn = getReceiverBurnableStealthPoolNoteIntoETABurnerFunction( { client }, { fetchBatchMerkleProof: client.fetchBatchMerkleProof!, zkProver, relayer: { submitBurn: r.submitClaim, // alias: BurnSubmitter ≡ ClaimSubmitter pollBurnStatus: r.pollClaimStatus, // alias: BurnStatusPoller ≡ ClaimStatusPoller getRelayerAddress: r.getRelayerAddress, }, }, ); const result = await burn(notes); // array of DecryptedStealthPoolNoteData ``` ### Self-burnable → ETA ```typescript theme={null} import { getSelfBurnableStealthPoolNoteIntoETABurnerFunction, } from "@umbra-privacy/sdk/burn"; import { getUmbraRelayer } from "@umbra-privacy/sdk"; import { getClaimSelfClaimableUtxoIntoEncryptedBalanceProver } from "@umbra-privacy/sdk/zk-prover"; const zkProver = getClaimSelfClaimableUtxoIntoEncryptedBalanceProver(); const r = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com" }); const burn = getSelfBurnableStealthPoolNoteIntoETABurnerFunction( { client }, { fetchBatchMerkleProof: client.fetchBatchMerkleProof!, zkProver, relayer: { submitBurn: r.submitClaim, pollBurnStatus: r.pollClaimStatus, getRelayerAddress: r.getRelayerAddress, }, }, ); const result = await burn(notes); ``` ### Self-burnable → ATA ```typescript theme={null} import { getSelfBurnableStealthPoolNoteIntoATABurnerFunction, } from "@umbra-privacy/sdk/burn"; import { getUmbraRelayer } from "@umbra-privacy/sdk"; import { getClaimSelfClaimableUtxoIntoPublicBalanceProver } from "@umbra-privacy/sdk/zk-prover"; const zkProver = getClaimSelfClaimableUtxoIntoPublicBalanceProver(); const r = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com" }); const burn = getSelfBurnableStealthPoolNoteIntoATABurnerFunction( { client }, { fetchBatchMerkleProof: client.fetchBatchMerkleProof!, zkProver, relayer: { submitBurn: r.submitClaim, pollBurnStatus: r.pollClaimStatus, getRelayerAddress: r.getRelayerAddress, }, }, ); const result = await burn(notes); ``` Receiver-burnable → ATA is supported on-chain (`claim_to_receiver` in the anchor program) but is not yet exposed in the SDK. Use receiver-burnable → ETA, then a regular withdrawal, until the SDK ships the direct path. ## Function signature All three burners share the same call signature: ```typescript theme={null} type BurnerFn = ( stealthPoolNotes: readonly (DecryptedStealthPoolNoteData & { kind: "self-burnable" | "receiver-burnable" })[], optionalData?: OptionalData32, microLamportsPerAcu?: MicroLamportsPerAcu, ) => Promise; ``` ### Parameters Notes returned by the scanner — pick them out of `result.{etaToStealthPoolSelfBurnable, etaToStealthPoolReceiverBurnable, ataToStealthPoolSelfBurnable, ataToStealthPoolReceiverBurnable}`. The burner fetches per-batch Merkle proofs internally; do **not** pre-attach proofs. 32 bytes of opaque metadata stored with the burn. Defaults to all zeros. **Must be a pre-hashed or pre-encrypted 32-byte value.** Optional priority fee in micro-lamports per Arcium computation unit, applied to the relayer-submitted transactions. ### Deps Per-batch Merkle proof fetcher. Use `client.fetchBatchMerkleProof` (auto-built when `indexerApiEndpoint` is set on the client). Circuit-specific Groth16 prover (one per burn variant). Provers ship at `@umbra-privacy/sdk/zk-prover`. Relayer adapter. The three property names are V18 — they are TypeScript aliases (`BurnSubmitterFunction = ClaimSubmitterFunction`, etc.), so a relayer client built via `getUmbraRelayer({ apiEndpoint })` plugs in directly under the renamed property names. ## Return Value Receiver-into-ETA and self-into-ETA both return `BurnStealthPoolNoteIntoETAResult`. Self-into-ATA returns `BurnStealthPoolNoteIntoATAResult`. Both shapes: ```typescript theme={null} interface BurnStealthPoolNoteIntoETAResult { readonly signatures: readonly TransactionSignature[]; // from OperationOutcome — every tx submitted readonly batches: Map; // keyed by batch index } interface BurnBatchResult { readonly requestId: string; // relayer-assigned tracking ID readonly status: BurnStatus; readonly txSignature?: string; // on-chain burn tx (when landed) readonly callbackSignature?: string; // MPC callback tx (when finalised) readonly resolvedVariant?: string; // relayer-chosen variant, e.g. "claim_into_existing_shared_balance_v18" readonly failureReason?: string | null; readonly stealthPoolNoteIds?: readonly string[]; // "treeIndex:leafIndex" pairs } type BurnStatus = | "received" | "validating" | "offsets_reserved" | "building_tx" | "tx_built" | "submitting" | "submitted" | "awaiting_callback" | "callback_received" | "finalizing" | "completed" | "failed" | "timed_out" | "refunded"; ``` `completed` / `callback_received` indicate success. The signature you want to surface to the user is `callbackSignature ?? txSignature`. `failureReason` containing `"NullifierAlreadyBurnt"` means the note was already burnt — the SDK treats this as idempotent success. **Umbra Confidential SPL token — lottery points:** When burning into an ETA, the Arcium MPC generates a `rescue_encrypted_lottery_ticket_delta` and adds it to your ETA's running point totals. This field is present in the raw relayer batch response but is not yet surfaced in `BurnBatchResult`. See [Umbra Confidential SPL Token](/concepts/encrypted-balances#umbra-confidential-spl-token) for the current SDK status. ## Batching behaviour * **Receiver-burnable → ETA** batches **natively**: groups notes by `destinationAddress`, chunks to ≤5 per proof. Pass the whole array; do not reimplement chunking. * **Self-burnable → ETA / ATA** has `MAX_NOTES_PER_PROOF = 1`. The SDK loops internally — caller still passes an array; the result has one batch per note. ## Full example: scan + burn ```typescript theme={null} import { getBurnableStealthPoolNoteScannerFunction } from "@umbra-privacy/sdk/burn"; // ...the burner setup from above const scan = getBurnableStealthPoolNoteScannerFunction({ client }); const result = await scan(); if (result.ataToStealthPoolReceiverBurnable.length > 0) { const out = await burnReceiverIntoEta(result.ataToStealthPoolReceiverBurnable); for (const [batchIndex, batch] of out.batches) { console.log( "Batch", batchIndex, "status:", batch.status, "sig:", batch.callbackSignature ?? batch.txSignature, "id:", batch.requestId, ); } } ``` ## ZK Proof Generation Burns generate a **[Groth16](https://eprint.iacr.org/2016/260.pdf) ZK proof** that proves: * You know the secret inputs to one of the commitments in the Merkle tree. * The corresponding nullifier has not been burnt before. The proof is verified on-chain by the Umbra program. Invalid proofs are rejected; valid proofs trigger the callback instruction that releases the tokens. Proof generation is CPU-intensive. Plan for 2–8 seconds in the browser per batch. Run the prover in a Web Worker to avoid blocking the main thread (see [ZK Provers](/sdk/advanced/zk-provers)). ## Stale Merkle Proofs Merkle proofs can become stale if the tree root changes between when the proof is fetched and when the burn is submitted. In V18 the burner fetches the proof **per batch, immediately before submission**, so this is much less likely than under V13's scan-time proof bundling. If a burn fails with a root mismatch, simply re-run it — the burner refetches the proof on each attempt. Do **not** call `enrichWithMerkleProof` yourself in normal use. The burner handles it. The helper is exported from `@umbra-privacy/sdk/burn` only for callers building a fully custom burn pipeline that bypasses the orchestrator factories. ## Dropped relayer callback The relayer's MPC callback can drop (network partition, compute-budget exhaustion, Arcium downtime). If you re-submit the same `request_id` while the nullifier is still reserved upstream, the relayer returns **HTTP 409 with code `DUPLICATE_OFFSET`** — wait, re-check on-chain that the nullifier has not landed, and retry. The burner factory handles this automatically: polling, detecting `NullifierAlreadyBurnt` as idempotent success, and surfacing a clean status. If you are building a custom burn pipeline, see [Errors — Idempotent burn retry](/reference/errors). ## Error Handling ```typescript theme={null} import { isClaimUtxoError } from "@umbra-privacy/sdk/errors"; try { const result = await burn(notes); } catch (err) { if (isClaimUtxoError(err)) { switch (err.stage) { case "zk-proof-generation": console.error("Proof generation failed:", err.message); break; case "transaction-validate": // Pre-flight failed — Merkle proof may be stale. Re-run the burner; it refetches. console.warn("Pre-flight failed:", err.message); break; case "transaction-send": // Submitted but confirmation timed out. Verify on-chain before retrying — // the nullifier may already be burnt. console.warn("Confirmation timeout. Check on-chain before retrying."); break; default: console.error("Burn failed at stage:", err.stage, err); } } else { throw err; } } ``` On `transaction-send`, do **not** immediately retry. The transaction may have landed; the burner factory checks on-chain before re-submission, but a fresh manual retry without that check can attempt to spend a nullifier that has already been burnt — wasting fees on a rejected tx. ## Nullifier Reuse Once a note is burnt, its nullifier is recorded in an on-chain treap. Any attempt to burn the same note again will be rejected by the program with `NullifierAlreadyBurnt`. You do not need to track this client-side — the on-chain check is authoritative, and the SDK's burner treats this status as idempotent success. See [Error Handling](/reference/errors) for the full error reference. # Writing Stealth Pool Notes Source: https://sdk.umbraprivacy.com/sdk/mixer/creating-utxos 4 V18 note-creator factory functions: get{ATA|ETA}IntoSelfBurnableStealthPoolNoteCreatorFunction and get{ATA|ETA}IntoReceiverBurnableStealthPoolNoteCreatorFunction. Merkle insertion + on-chain ciphertext. ## Overview Writing a Stealth Pool Note inserts a Poseidon commitment into the on-chain Indexed Merkle Tree and locks the corresponding tokens in the pool. The SDK also publishes an X25519 + AES-GCM ciphertext on-chain so the unlocker can later discover the note with their viewing keys. Choose the factory function that matches your source (`ATA` or `ETA`) and unlocker (self-burnable or receiver-burnable). All four factories live under `@umbra-privacy/sdk/deposit`. ## Registration prerequisites differ by variant This is the most common integration footgun. * **Self-burnable creates** encrypt the unlocker against the **sender's** master-seed-derived key. Sender needs `isUserAccountX25519KeyRegistered`. Recipient needs nothing. * **Receiver-burnable creates** encrypt against the **recipient's** `userCommitment`. The recipient must have completed **all three** registration sub-step flags (`isInitialised`, `isUserAccountX25519KeyRegistered`, `isUserCommitmentRegistered`) on-chain. **Always pre-check the recipient before a receiver-burnable create:** ```typescript theme={null} import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk/query"; const queryAccount = getUserAccountQuerierFunction({ client }); const r = await queryAccount(recipient); const ready = r.state === "exists" && r.data?.isInitialised && r.data?.isUserAccountX25519KeyRegistered && r.data?.isUserCommitmentRegistered; if (!ready) { // Either abort with a clear "ask the recipient to register" error, // or fall back to a self-burnable create — see "Anonymous payment fallback" below. } ``` ## Factory Functions ### Self-burnable from ATA Single-tx, no MPC. Funds locked directly from your ATA. You unlock. ```typescript theme={null} import { getATAIntoSelfBurnableStealthPoolNoteCreatorFunction } from "@umbra-privacy/sdk/deposit"; import { getATAIntoStealthPoolNoteCreatorProver } from "@umbra-privacy/sdk/zk-prover"; const zkProver = getATAIntoStealthPoolNoteCreatorProver(); const createNote = getATAIntoSelfBurnableStealthPoolNoteCreatorFunction( { client }, { zkProver }, ); const signatures = await createNote({ destinationAddress: client.signer.address, // you unlock mint, amount, }); ``` ### Receiver-burnable from ATA Single-tx, no MPC. Funds locked from your ATA. **Recipient must be fully registered.** ```typescript theme={null} import { getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction } from "@umbra-privacy/sdk/deposit"; import { getATAIntoStealthPoolNoteCreatorProver } from "@umbra-privacy/sdk/zk-prover"; const zkProver = getATAIntoStealthPoolNoteCreatorProver(); const createNote = getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction( { client }, { zkProver }, ); const RECIPIENT = "GsbwXfJraMomNxBcpR3DBFyKCCmN9SKGzKFJBNKxRFkT"; const signatures = await createNote({ destinationAddress: RECIPIENT, mint, amount, }); ``` ### Self-burnable from ETA Two-tx pipeline (`createProofAccount` → `createUtxo`), MPC. Funds drawn from your ETA. You unlock. ```typescript theme={null} import { getETAIntoSelfBurnableStealthPoolNoteCreatorFunction } from "@umbra-privacy/sdk/deposit"; import { getETAIntoStealthPoolNoteCreatorProver } from "@umbra-privacy/sdk/zk-prover"; const zkProver = getETAIntoStealthPoolNoteCreatorProver(); const createNote = getETAIntoSelfBurnableStealthPoolNoteCreatorFunction( { client }, { zkProver }, ); const signatures = await createNote({ destinationAddress: client.signer.address, mint, amount, }); ``` ### Receiver-burnable from ETA Two-tx pipeline, MPC. Funds drawn from your ETA. **Recipient must be fully registered.** ```typescript theme={null} import { getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction } from "@umbra-privacy/sdk/deposit"; import { getETAIntoStealthPoolNoteCreatorProver } from "@umbra-privacy/sdk/zk-prover"; const zkProver = getETAIntoStealthPoolNoteCreatorProver(); const createNote = getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction( { client }, { zkProver }, ); const signatures = await createNote({ destinationAddress: RECIPIENT, mint, amount, }); ``` ## Parameters All four creator factories accept a `CreateUtxoArgs` object as the first argument: The wallet address that can unlock this note. For self-burnable, use `client.signer.address`. For receiver-burnable, use the recipient's address. SPL or Token-2022 mint address. Must be a supported mint. Amount in native token units. Protocol fees and Token-2022 transfer fees are subtracted from this amount before the commitment is written. Optional deterministic generation index. Defaults to a random `U256` (CSPRNG). Pass the same `generationIndex` on retry to allow the V18 pipeline's `closeProofAccount` step to reclaim a proof-account orphan from a prior failed attempt. 32 bytes of opaque metadata stored alongside the note. **Must be pre-hashed or pre-encrypted** — never store plaintext identifiers. Commitment level for RPC account reads during note construction. (ATA-source only) Commitment level for epoch-info fetches (Token-2022 transfer-fee schedule). Per-phase + per-step lifecycle hooks. Per-step slots use `{ onPreSend, onPostSend, onSkipped }`. Step slot names: `closeProofAccount` (fires only when a stale proof account from a prior failed attempt is reclaimed), `populateProofAccount`, `createStealthPoolNote` (ATA-source pipeline only; ETA-source uses `queueComputation` instead). Circuit-specific Groth16 prover. **Required** for every variant — there is no built-in default. Provers ship at `@umbra-privacy/sdk/zk-prover`. V4 fields `priorityFees`, `purpose`, `awaitCallback`, `skipPreflight`, `maxRetries` do not exist on V5 note creators. The V4 `callbacks: { createProofAccount, createUtxo, closeProofAccount }` shape has been replaced by `options.hooks` (ATA-source) or `deps.hooks` (ETA-source) with `onPreSend`/`onPostSend`/`onSkipped` per-step events. ## Return Value The return type depends on the source: * **From ATA (ZK-only):** `Promise` of length 1 — `[noteWriteSig]`. * **From ETA (MPC pipeline):** `Promise` of length 2 — `[proofAccountSig, noteWriteSig]`. ## The ZK prover dependency Note creation requires a ZK prover function (`zkProver` in `deps`). This function generates a Groth16 proof that the commitment was constructed correctly. The prover is CPU-intensive — generating a proof takes 2–8 seconds in the browser and 1–3 seconds in Node.js. For browser applications, run the prover in a Web Worker to avoid blocking the main thread. See [ZK Provers](/sdk/advanced/zk-provers) for the canonical comlink pattern. The `zkProver` dependency is **required** and cannot be omitted. There is no built-in default. Use `@umbra-privacy/sdk/zk-prover` for the CDN-backed implementation. ## Anonymous payment fallback (receiver-burnable → self-burnable) For one-shot payments to a recipient who may not be registered, prefer the **self-burnable ATA** variant + share a recovery secret out of band (or use the relayer's escrow flow). This avoids the "recipient must register first" UX trap: ```typescript theme={null} import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk/query"; import { getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction, getATAIntoSelfBurnableStealthPoolNoteCreatorFunction, } from "@umbra-privacy/sdk/deposit"; import { getATAIntoStealthPoolNoteCreatorProver, } from "@umbra-privacy/sdk/zk-prover"; const queryAccount = getUserAccountQuerierFunction({ client }); const r = await queryAccount(RECIPIENT); const recipientReady = r.state === "exists" && r.data?.isInitialised && r.data?.isUserAccountX25519KeyRegistered && r.data?.isUserCommitmentRegistered; if (recipientReady) { const create = getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction( { client }, { zkProver: getATAIntoStealthPoolNoteCreatorProver() }, ); await create({ destinationAddress: RECIPIENT, mint, amount }); } else { // Fallback: sender stays the unlocker, hand over the secret out of band. const create = getATAIntoSelfBurnableStealthPoolNoteCreatorFunction( { client }, { zkProver: getATAIntoStealthPoolNoteCreatorProver() }, ); await create({ destinationAddress: client.signer.address, mint, amount }); } ``` ## Error Handling Create-side failures have two distinguishing modes — ZK proof generation and stale on-chain state. Use `isCreateUtxoError` from `@umbra-privacy/sdk/errors`: ```typescript theme={null} import { isCreateUtxoError } from "@umbra-privacy/sdk/errors"; try { const signatures = await createNote({ destinationAddress: recipient, mint, amount }); } catch (err) { if (isCreateUtxoError(err)) { switch (err.stage) { case "zk-proof-generation": console.error("Proof generation failed:", err.message); break; case "transaction-sign": console.log("Cancelled."); break; case "account-fetch": console.error("RPC error:", err.message); break; case "transaction-send": // May have landed. Re-run the scanner to check whether the commitment was inserted. console.warn("Confirmation timeout. Check on-chain before retrying."); break; default: console.error("Create failed at:", err.stage, err); } } else { throw err; } } ``` On `transaction-send`, do **not** immediately retry. Run the scanner first — the commitment may already be in the tree. If retrying an ETA-source create, you must pass the same `generationIndex` via `options.generationIndex` so the SDK's `closeProofAccount` step can reclaim the orphan from your prior attempt (see [Recovery](/sdk/advanced/recovery)). See [Error Handling](/reference/errors) for a full reference. ## Concurrency rule Never run note creates concurrently from the same client. The SDK auto-derives `generationIndex` from on-chain account state during each call; two parallel `Promise.all`-style creates read the same `generationIndex` before either has incremented it, derive identical ephemeral keys, and collide silently (fund loss / scan failure). Serialise creates per `(signer, network)`. ## Protocol Fees Fees are deducted from `amount` before the commitment is written. The net amount committed (= what the unlocker receives at burn time) is `amount − fees`. See [Pricing](/pricing) for current rates. # Scanning for Notes Source: https://sdk.umbraprivacy.com/sdk/mixer/fetching-utxos getBurnableStealthPoolNoteScannerFunction (V18, zero-arg): walks every active tree, decrypts X25519-AES ciphertexts addressable by your viewing keys, persists cursor in client.utxoDataStore. Returns ScannedStealthPoolNoteResult grouped by (kind, source). ## Overview After a Stealth Pool Note is written, the SDK publishes an X25519-AES ciphertext on-chain addressed either to the sender (self-burnable) or to the recipient (receiver-burnable). To find your burnable notes, the scanner: 1. Discovers every active stealth-pool tree. 2. Loads scan progress from `client.utxoDataStore` and computes the unscanned ranges. 3. Fetches the encrypted note data for those ranges from the indexer. 4. Tries to decrypt each ciphertext with your X25519 + viewing keys. 5. Persists progress. 6. Returns the successful decryptions, grouped by `(kind, source)`. The V18 scanner is **zero-arg** — the cursor is fully SDK-managed. The scanner requires the indexer. Ensure `indexerApiEndpoint` is set when constructing the client. The browser scanner is several orders of magnitude faster when `utxoDataStore` and `nullifierStore` are wired — without them, every call re-scans every active tree from genesis. ## Usage ```typescript theme={null} import { getBurnableStealthPoolNoteScannerFunction } from "@umbra-privacy/sdk/burn"; const scan = getBurnableStealthPoolNoteScannerFunction({ client }); const result = await scan(); // zero-arg in V18 ``` ## Return Value ```typescript theme={null} interface ScannedStealthPoolNoteResult { readonly etaToStealthPoolSelfBurnable: readonly DecryptedStealthPoolNoteData[]; readonly etaToStealthPoolReceiverBurnable: readonly DecryptedStealthPoolNoteData[]; readonly ataToStealthPoolSelfBurnable: readonly DecryptedStealthPoolNoteData[]; readonly ataToStealthPoolReceiverBurnable: readonly DecryptedStealthPoolNoteData[]; readonly scannedTrees: readonly ScannedTreeProgress[]; } interface ScannedTreeProgress { readonly treeIndex: U128; readonly scannedRange: { start: U32; end: U32 } | null; readonly totalLeaves: U32; readonly fullyScanned: boolean; } ``` The four note buckets are exhaustive — every burnable note maps onto exactly one. Within each bucket, the notes are **not** Merkle-proof-bundled: proofs are fetched per batch at burn time so a single proof set is shared across an entire burn batch (much cheaper than fetching one proof per note). ## Example: Run the scanner ```typescript theme={null} import { getBurnableStealthPoolNoteScannerFunction } from "@umbra-privacy/sdk/burn"; const scan = getBurnableStealthPoolNoteScannerFunction({ client }); const result = await scan(); console.log("Self-burnable from ETA: ", result.etaToStealthPoolSelfBurnable.length); console.log("Receiver-burnable from ETA: ", result.etaToStealthPoolReceiverBurnable.length); console.log("Self-burnable from ATA: ", result.ataToStealthPoolSelfBurnable.length); console.log("Receiver-burnable from ATA: ", result.ataToStealthPoolReceiverBurnable.length); for (const tree of result.scannedTrees) { console.log( `Tree ${tree.treeIndex}: scanned ${tree.scannedRange?.start}–${tree.scannedRange?.end} of ${tree.totalLeaves}${tree.fullyScanned ? " (full)" : ""}`, ); } ``` ## Idempotent — call as often as you like The scanner is idempotent: subsequent calls only fetch and decrypt new leaves since the last call. The cursor lives in `client.utxoDataStore.getScanProgress(treeIndex)` / `client.utxoDataStore.addScannedRange(treeIndex, start, end)`. Re-mounting the scanner factory does not reset state. A typical browser app calls the scanner on every page load and on a 30s timer while the page is open. ## How decryption works The SDK derives your X25519 private key from the master seed, then for each note ciphertext: 1. Extracts the writer's ephemeral X25519 public key from the ciphertext header. 2. Computes an [X25519 ECDH](https://www.rfc-editor.org/rfc/rfc7748) shared secret. 3. Derives an [AES-GCM](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf) key. 4. Attempts to decrypt the payload. 5. If decryption succeeds, reads the 12-byte domain separator and routes the note into the matching `(kind, source)` bucket. Your private key never leaves your device. ## Custom indexer fetchers If you are running a private indexer mirror, override the data fetchers via `deps`: ```typescript theme={null} const scan = getBurnableStealthPoolNoteScannerFunction( { client }, { fetchStealthPoolNoteData: async (treeIndex, range) => mirror.fetchNotes(treeIndex, range), fetchMerkleProof: async (treeIndex, leafIndex) => mirror.fetchProof(treeIndex, leafIndex), // Multi-key wallets can scan with extra viewing keys: additionalX25519PrivateKeys: [otherPrivKey], }, ); ``` (The burner factory's per-batch `fetchBatchMerkleProof` is a separate dep on the burner — see [Burning](/sdk/mixer/claiming-utxos).) ## Error Handling ```typescript theme={null} import { isFetchUtxosError } from "@umbra-privacy/sdk/errors"; try { const result = await scan(); } catch (err) { if (isFetchUtxosError(err)) { switch (err.stage) { case "initialization": // The scanner was constructed without an indexer endpoint. console.error("Indexer not configured:", err.message); break; case "key-derivation": console.error("Viewing-key derivation failed:", err.message); break; case "indexer-fetch": // The indexer was unreachable or returned an error. console.error("Indexer fetch failed:", err.message); break; case "proof-fetch": // The burner's per-batch Merkle proof fetch failed. console.error("Merkle proof fetch failed:", err.message); break; case "proof-enrichment": // enrichWithMerkleProof failed to attach a proof entry to a scanned note. console.error("Proof enrichment failed:", err.message); break; } } else { throw err; } } ``` An empty result (all buckets empty) is **not** an error — it means no notes addressable by your viewing keys exist in the trees the scanner walked. Errors are reserved for infrastructure failures. ## Burn the result Pass the scanned buckets directly to the matching burner factory — the burner fetches per-batch proofs internally: ```typescript theme={null} import { getReceiverBurnableStealthPoolNoteIntoETABurnerFunction, getSelfBurnableStealthPoolNoteIntoETABurnerFunction, getSelfBurnableStealthPoolNoteIntoATABurnerFunction, } from "@umbra-privacy/sdk/burn"; // Burner factory setup omitted — see /sdk/mixer/claiming-utxos. // Burn all receiver-burnable notes from ATA-source writes into your ETA. await burnReceiverIntoEta(result.ataToStealthPoolReceiverBurnable); ``` See [Burning](/sdk/mixer/claiming-utxos) for full burner factory setup, batching behaviour, and dropped-callback recovery. # Stealth Pool Overview Source: https://sdk.umbraprivacy.com/sdk/mixer/overview Stealth Pool overview (V18): 4 note-creation variants (ATA/ETA × self/receiver-burnable), 3 shipped burn variants (receiver→ETA, self→ETA, self→ATA). Self-burnable vs receiver-burnable flows. ## The Most Powerful Shielded Pool on Solana Umbra's Stealth Pool is the most powerful shielded pool on Solana today — and it is built to fit into any flow. Tokens can enter from a public ATA or an EncryptedTokenAccount, exit to a fresh address or stay private inside another ETA, and be unlocked by you or by any registered recipient. Every combination is a first-class path. The pool requires [registration](/sdk/registration) with `anonymous: true` **on the recipient side** for receiver-burnable notes. Self-burnable notes require no recipient registration — the sender is the unlocker. ## Note structure The pool operates on **Stealth Pool Notes** — commitments inserted into an on-chain Indexed Merkle Tree that represent locked tokens. Every note encodes three distinct roles: * **Sender** — the address that funded the note, locked the tokens into the pool, and fixed the recipient at creation time. * **Unlocker** — the address authorised to burn the note's nullifier on-chain and release the tokens, choosing whether they exit to an ATA or an ETA. * **Recipient** — the final destination address, set by the sender and not modifiable by the unlocker. The sender controls who receives. The unlocker controls when and how the tokens exit. Separating these two roles is what makes deep mixing possible — the sender's involvement ends at note-write time. ## Note variants You are both the sender and the unlocker. Burn the note on your own schedule and choose whether tokens exit to an ATA or stay in an ETA. **Recipient needs zero on-chain state.** The recipient is the unlocker. They burn the note themselves and decide the exit. **Recipient must be fully registered** (all three sub-step flags) — the unlocker is encrypted against their `userCommitment`. Receiver-burnable notes produce stronger anonymity sets. Because the sender's actions and the exit are fully decoupled, timing correlation between write and burn becomes significantly harder. ## Source options Each variant can be funded from two sources: * **From an ATA** — tokens are transferred directly from your public ATA. Single tx, no MPC. * **From an ETA** — tokens are drawn from your existing EncryptedTokenAccount before entering the pool. Two-tx pipeline (proof account → note write), MPC under the hood. This gives four creator factories — all under `@umbra-privacy/sdk/deposit`: * `getATAIntoSelfBurnableStealthPoolNoteCreatorFunction` — ATA source, you unlock. * `getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction` — ATA source, recipient unlocks. * `getETAIntoSelfBurnableStealthPoolNoteCreatorFunction` — ETA source (MPC), you unlock. * `getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction` — ETA source (MPC), recipient unlocks. See [Writing notes](/sdk/mixer/creating-utxos) for full API details and the recipient pre-check rubric. ## Burn variants (V18) Three burn factories are shipped, under `@umbra-privacy/sdk/burn`: * `getReceiverBurnableStealthPoolNoteIntoETABurnerFunction` — receiver-burnable → ETA. Natively batches: groups notes by `destinationAddress`, chunks ≤5 per proof. * `getSelfBurnableStealthPoolNoteIntoETABurnerFunction` — self-burnable → ETA. `MAX_NOTES_PER_PROOF = 1`; SDK loops internally. * `getSelfBurnableStealthPoolNoteIntoATABurnerFunction` — self-burnable → ATA. `MAX_NOTES_PER_PROOF = 1`. (Receiver-burnable → ATA is supported on-chain but is not yet shipped in the SDK — open issue.) ## The Stealth Pool flow Choose a creator factory, supply the recipient pre-check / fallback (for receiver-burnable creates), and submit. The SDK locks tokens in the pool, inserts a commitment into the Merkle tree, and publishes the X25519-AES ciphertext on-chain so the unlocker can discover it. See [Writing notes](/sdk/mixer/creating-utxos). The more other users write notes into the same tree, the stronger your privacy guarantee. There is no enforced waiting period — this is a trade-off you manage in your application. Run `getBurnableStealthPoolNoteScannerFunction({ client })()`. The zero-arg scanner walks every active tree, decrypts every ciphertext addressable by your viewing keys, persists progress in `client.utxoDataStore`, and returns the notes grouped by `(kind, source)`. See [Scanning](/sdk/mixer/fetching-utxos). Pass the notes to the matching burner factory. The burner fetches a per-batch Merkle proof, generates a Groth16 ZK proof, submits to the relayer, and polls until the on-chain burn lands. The nullifier is burned to prevent double-spending; tokens are released to the chosen destination. See [Burning](/sdk/mixer/claiming-utxos). ## Infrastructure requirements * **Indexer** — required for note discovery and Merkle proof fetching. Pass `indexerApiEndpoint` to `getUmbraClient`. * **Relayer** — required for burning. Pass `relayerApiEndpoint` to `getUmbraClient`, then build the burner factory's `relayer` dep from `getUmbraRelayer({ apiEndpoint })`. * **Store adapters** — strongly recommended on the browser. Without `utxoDataStore` / `nullifierStore`, every `scan()` re-scans every active tree from genesis. ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; import { createBrowserStorageBackend, createShardedUtxoDataStore, createShardedNullifierStore, } from "@umbra-privacy/sdk/store-adapters"; const storageBackend = createBrowserStorageBackend({ dbName: "umbra" }); const utxoDataStore = createShardedUtxoDataStore({ storageBackend }); const nullifierStore = createShardedNullifierStore({ storageBackend }); const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl: "...", rpcSubscriptionsUrl: "...", indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com", relayerApiEndpoint: "https://relayer.api.umbraprivacy.com", utxoDataStore, nullifierStore, }); ``` ## Privacy Considerations **Burn into an ETA by default.** When a note is burnt into an ETA, the burned amount is hidden, the destination is not revealed, and no observable exit event is produced on-chain. At burn time, the sender is completely unlinkable — there is no visible amount, no destination address, and no signal that can tie the burn back to any specific note write. ATA burns should be the opt-out, not the default. **Use round, pool-common amounts when burning publicly.** If a burn must exit to an ATA, the amount becomes visible on-chain. Unusual or fragmented amounts let an observer eliminate most of the tree and narrow in on the matching commitment. Standardise on amounts that are common in the pool (e.g. exactly 100 USDC) to stay indistinguishable within the largest possible set. **Burn to a fresh address when going public.** Burning into an address with existing on-chain history re-establishes a link. Use an address that has never appeared on-chain before. For a complete breakdown of all source × destination combinations — visible vs hidden state, timing risk, recommended use cases — see [Privacy Analysis](/sdk/mixer/privacy-analysis). # Privacy Analysis Source: https://sdk.umbraprivacy.com/sdk/mixer/privacy-analysis 3-tier privacy model: ETA → ETA (strongest), mixed flows (partial), ATA → ATA (weakest). Covers timing analysis, amount correlation, anonymity set sizing, and self-burnable vs receiver-burnable trade-offs. ## Summary The Stealth Pool's privacy strength is determined by three independent choices: where tokens come from (ATA or ETA), where they land (ATA or ETA), and who controls the burn (self-burnable or receiver-burnable). * Shielding both ends (ETA → ETA) eliminates amount correlation entirely and makes the sender completely unlinkable at burn time — the strongest possible configuration. * Shielding one end (mixed flows) partially limits what an observer can correlate, depending on which side is visible. * Public flows (ATA → ATA) expose amounts at both ends; the only remaining privacy property is the absence of a direct on-chain link between the two addresses. * Receiver-burnable notes are cryptographically equivalent to self-burnable notes with the same source and destination, but are stronger in practice — recipients naturally burn later and independently, widening the anonymity set without any deliberate effort. For a complete breakdown of all eight combinations see the [Privacy Ranking](#privacy-ranking) below. *** ## How to read this page Every Stealth Pool flow has two independent axes that determine its privacy profile: * **Source** — where the tokens come from: an `AssociatedTokenAccount (ATA)` or an `EncryptedTokenAccount (ETA)`. * **Destination** — where the tokens land after the note is burnt: an ATA or an ETA. * **Unlocker** — whether the sender (self-burnable) or the recipient (receiver-burnable) controls the burn. Writing a note from an ETA hides the deposited amount — no observer can see how much entered the pool. Burning into an ETA hides the released amount — no observer can see how much left the pool. When both ends are shielded, amounts cannot be correlated at all, and the sender is completely unlinkable at burn time. ### Self-burnable vs receiver-burnable Cryptographically, self-burnable and receiver-burnable notes with the same source and destination are equivalent — the same information is visible or hidden on-chain in both cases. The practical difference is behavioural. A sender burning their own note tends to do so relatively promptly. A recipient, acting independently, will typically burn at a time of their own choosing — often much later, and without coordination. This natural timing gap widens the anonymity set and makes temporal correlation significantly harder. Receiver-burnable is therefore the stronger choice — but only when the recipient is fully registered (see [Writing notes — registration prerequisites](/sdk/mixer/creating-utxos)). Each of the eight combinations below is assessed on: * **Visible on-chain** — what any observer can read from the chain. * **Hidden** — what an observer cannot determine. * **Timing risk** — how susceptible the flow is to correlation by time. * **Best for** — the use case this combination suits. *** ## Privacy Ranking Pairs share the same cryptographic strength. Within each pair, receiver-burnable is stronger in practice due to natural timing behaviour. * **Tier 1** — ETA → ETA (both ends shielded, no amounts observable) * ETA → receiver-burnable → ETA. * ETA → self-burnable → ETA. * **Tier 2** — Mixed (one end shielded) * ETA → receiver-burnable → ATA *(not yet shipped in V18 SDK; on-chain only)*. * ETA → self-burnable → ATA. * ATA → receiver-burnable → ETA. * ATA → self-burnable → ETA. * **Tier 3** — ATA → ATA (both ends public) * ATA → receiver-burnable → ATA *(not yet shipped in V18 SDK)*. * ATA → self-burnable → ATA. *** ## Tier 1 — ETA → ETA Both the write and the burn are shielded. No amounts are visible at either end. ### ETA → receiver-burnable → ETA The strongest possible flow. No amounts are visible at either end, and the exit happens entirely on the recipient's timeline. * **Visible on-chain** — a note commitment was inserted into the tree; a nullifier was burnt at some later time. * **Hidden** — the written amount, the released amount, the sender's identity, the recipient's identity, and any temporal relationship between the two events. * **Timing risk** — minimal. No amounts are observable at either end. The recipient burns at a time of their choosing with no signal from the sender. * **Best for** — anonymous payments to a fully registered recipient where maximum privacy is required for both parties. ### ETA → self-burnable → ETA Cryptographically identical to the receiver-burnable variant. The same party controls both events, so the timing gap is down to the sender's own discipline. * **Visible on-chain** — note commitment was inserted; nullifier was burnt. * **Hidden** — written amount, released amount, link between the two events. * **Timing risk** — low. Amounts cannot be correlated. The sender controls burn timing and can delay it to widen the anonymity set, though this requires deliberate effort rather than happening naturally. * **Best for** — moving your own funds through the pool with no on-chain amount trail at either end. *** ## Tier 2 — Mixed One side is visible, one is hidden. Amounts can be partially observed but not fully correlated across the pool boundary. ### ETA → self-burnable → ATA The write is hidden but the burn exits publicly. No amount is visible at entry, so the visible exit cannot be tied back to any specific note with certainty. * **Visible on-chain** — burned amount and destination ATA at burn time. * **Hidden** — written amount and any quantitative link from the burn back to the write. * **Timing risk** — medium. The burn amount is visible but no matching write amount can be observed. Same party controls both events, so timing correlation is possible if you burn promptly after writing. * **Best for** — surfacing funds into a public wallet where hiding the deposit amount matters more than hiding the exit. ### ATA → receiver-burnable → ETA The write is public but the burn is hidden. An observer can see who wrote and how much, but the burn produces no visible exit. The recipient acts independently. * **Visible on-chain** — written amount and the writing ATA at note-write time. * **Hidden** — burned amount, burn destination, link from the visible write to the eventual burn. * **Timing risk** — low-medium. The exit is invisible, so even though the write is known, there is no observable burn event to correlate it against. The recipient controls timing. * **Best for** — scenarios where the sender's identity is not sensitive but the recipient's privacy must be protected. ### ATA → self-burnable → ETA Cryptographically identical to the receiver-burnable variant; the same party controls both events. * **Visible on-chain** — written amount and writing ATA at note-write time. * **Hidden** — burned amount and destination. * **Timing risk** — medium. The write is fully visible. The exit leaves no trace, but the sender burning their own note may do so close to the write time. * **Best for** — moving funds into a private ETA from a known public wallet, where the source is not sensitive but the destination must be hidden. *** ## Tier 3 — ATA → ATA Both ends are public. Amounts are fully observable at write and burn. The only privacy property is the absence of a direct on-chain link. ### ATA → self-burnable → ATA The weakest flow. Both events are fully visible and the same party controls both, making timing and amount correlation straightforward. * **Visible on-chain** — written amount, writing ATA, burned amount, destination ATA. * **Hidden** — the direct on-chain link between the two addresses; a ZK proof still prevents anyone from proving which write maps to which burn without the secret inputs. * **Timing risk** — high. Amounts are visible at both ends. Matching equal amounts written and burnt within a short window is easy when the anonymity set is small. * **Best for** — the minimum viable case where address unlinking is the only goal and amount / timing privacy are not a concern. Use round, pool-common amounts to maximise the anonymity set. # Query State Source: https://sdk.umbraprivacy.com/sdk/query getEncryptedBalanceQuerierFunction and getUserAccountQuerierFunction: read-only queries for encrypted balance metadata (per-mint) and user account registration state. ## Overview Two query functions let you inspect on-chain state without modifying it: * **`getUserAccountQuerierFunction`** - reads registration status and account metadata for any address * **`getEncryptedBalanceQuerierFunction`** - reads encrypted balance metadata for the calling user across multiple mints Both are read-only and do not require a wallet signing prompt. ## Query User Account ```typescript theme={null} import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk/query"; const query = getUserAccountQuerierFunction({ client }); const result = await query(userAddress, options?); ``` ### Parameters The wallet address to query registration status for. Commitment level used for the RPC account read. Overrides the default on a per-call basis. ### Return Value The result is a discriminated union: ```typescript theme={null} type QueryUserAccountResult = | { state: "non_existent" } | { state: "exists"; data: EncryptedUserAccount }; ``` When `state === "exists"`, `data` contains: Whether the base account has been created on-chain. This is `true` after the first registration step. Whether the X25519 public key has been registered. Required for Shared-mode ETAs and for receiving Stealth Pool Notes addressed to this account. Whether the user commitment (Poseidon hash) has been registered. Required to receive **receiver-burnable** Stealth Pool Notes. Whether both X25519 and commitment registration are complete and the account is ready for all features. The user's registered X25519 public key (32 bytes). Used by other users to encrypt Stealth Pool Notes addressed to this account. Entropy bytes mixed into nonce derivation. Combined with `generationIndex` to derive the next note's `generationIndex` — never invent your own. Monotonic counter incremented on each deposit/withdrawal. Used internally for nonce derivation. ### Example: Check Registration Status ```typescript theme={null} const result = await query(client.signer.address); if (result.state === "non_existent") { // User is not registered - prompt them to register console.log("Not registered"); return; } const { data } = result; if (!data.isUserAccountX25519KeyRegistered) { console.log("X25519 key not registered - re-run registration"); } if (!data.isUserCommitmentRegistered) { console.log("Commitment not registered - re-run registration with anonymous: true"); } if (data.isActiveForAnonymousUsage) { console.log("Fully registered - all features available"); } ``` *** ## Query Encrypted Balance ```typescript theme={null} import { getEncryptedBalanceQuerierFunction } from "@umbra-privacy/sdk/query"; const query = getEncryptedBalanceQuerierFunction({ client }); // Query your encrypted balances for one or more mints const balances = await query([USDC_MINT, USDT_MINT], options?); // Returns Map ``` The function queries the calling user's own encrypted balances. Pass an array of mint addresses to check multiple mints in a single call. ### Parameters An array of SPL or Token-2022 mint addresses to query. The SDK fetches the encrypted token account PDA for each mint and returns a result for every mint in the array. Commitment level used for the RPC account reads. Overrides the default on a per-call basis. ### Return Value ```typescript theme={null} type QueryEncryptedBalanceResult = | { readonly state: "non_existent" } | { readonly state: "uninitialized" } | { readonly state: "mxe" } | { readonly state: "shared"; readonly balance: MathU64 }; ``` The four states represent: * `"non_existent"` — No ETA exists for this mint. The user needs to deposit first. * `"uninitialized"` — The ETA PDA exists on-chain but the Arcium balance has not been initialised yet. * `"mxe"` — The ETA is in MXE-only mode. The balance is encrypted under the network key and cannot be decrypted client-side. * `"shared"` — The ETA is in Shared mode. The SDK automatically decrypts the balance using the user's X25519 private key and returns the plaintext `balance` as a `MathU64`. ### Example ```typescript theme={null} import { getEncryptedBalanceQuerierFunction } from "@umbra-privacy/sdk/query"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const USDT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"; const query = getEncryptedBalanceQuerierFunction({ client }); const balances = await query([USDC, USDT]); for (const [mint, result] of balances) { switch (result.state) { case "shared": console.log(`Mint ${mint}: balance=${result.balance}`); break; case "mxe": console.log(`Mint ${mint}: MXE mode (cannot decrypt client-side)`); break; case "uninitialized": console.log(`Mint ${mint}: ETA exists but balance not initialised`); break; case "non_existent": console.log(`No ETA for mint ${mint} — deposit first`); break; } } ``` Shared-mode accounts return the decrypted balance automatically. MXE-mode accounts cannot be decrypted client-side - convert to Shared mode first using [Conversion](/sdk/conversion). *** ## Common Patterns ### Check Before Depositing Before depositing to an external address, verify the recipient is registered: ```typescript theme={null} const recipientQuery = getUserAccountQuerierFunction({ client }); const recipientResult = await recipientQuery(recipientAddress); if ( recipientResult.state === "non_existent" || !recipientResult.data.isUserAccountX25519KeyRegistered ) { throw new Error("Recipient is not registered for encrypted deposits"); } // Safe to deposit await deposit(recipientAddress, mint, amount); ``` ### Poll Until Registered For UI flows where you start registration and want to confirm it landed: ```typescript theme={null} async function waitForRegistration(address: Address, maxAttempts = 10) { const query = getUserAccountQuerierFunction({ client }); for (let attempt = 0; attempt < maxAttempts; attempt++) { const result = await query(address); if (result.state === "exists" && result.data.isActiveForAnonymousUsage) { return result.data; } await new Promise((r) => setTimeout(r, 2000)); // wait 2s between polls } throw new Error("Registration not confirmed after polling"); } ``` ### Query Multiple Balances at Once Use the array-based query to check several token balances in a single round-trip: ```typescript theme={null} const MINTS = [ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", // USDT "So11111111111111111111111111111111111111112", // wSOL ]; const query = getEncryptedBalanceQuerierFunction({ client }); const results = await query(MINTS); const activeBalances = [...results.entries()] .filter(([, result]) => result.state === "shared" || result.state === "mxe") .map(([mint]) => mint); console.log("Mints with active encrypted balances:", activeBalances); ``` ## Error Handling Both query functions are read-only - they never submit transactions. Use `isQueryError` from `@umbra-privacy/sdk/errors` and `switch` on `err.stage` to handle each failure point. ```typescript theme={null} import { isQueryError } from "@umbra-privacy/sdk/errors"; try { const result = await query(userAddress); // or: const balances = await queryBalance([USDC, USDT]); } catch (err) { if (isQueryError(err)) { switch (err.stage) { case "pda-derivation": // Could not derive the account's program address - unexpected on-chain state. console.error("PDA derivation failed:", err.message); break; case "account-fetch": // RPC node unreachable, returned an HTTP error, or returned a JSON-RPC error. console.error("RPC error fetching account:", err.message); showNotification("Could not reach the network. Please check your connection."); break; case "account-decode": // Account data exists on-chain but could not be decoded. // This indicates unexpected on-chain state - not a transient error. console.error("Account data could not be decoded:", err.message); break; case "key-derivation": // X25519 key derivation failed (encrypted balance query only). console.error("Key derivation failed:", err.message); break; case "decryption": // Rescue cipher decryption failed (encrypted balance query only). console.error("Balance decryption failed:", err.message); break; default: // Other stages: initialization. console.error("Query failed at stage:", err.stage, err); } } else { throw err; } } ``` Query functions do not throw when an account does not exist - they return `{ state: "non_existent" }` instead. Errors are reserved for infrastructure failures (RPC unreachable, malformed data). See [Error Handling](/reference/errors) for a full reference of all error types. # Registration Source: https://sdk.umbraprivacy.com/sdk/registration getUserRegistrationFunction: idempotent flow that creates the EncryptedUserAccount, registers the X25519 encryption key (confidential), and stores the user commitment via MPC + ZK (anonymous). ## Overview Before a user can deposit into an EncryptedTokenAccount or interact with the Stealth Pool, they need an on-chain **Umbra user account**. Registration creates this account and optionally registers the cryptographic keys needed for ETAs and anonymous transfers. Registration is idempotent — it checks on-chain state before each step and skips any sub-steps already completed (handling key rotation when keys have changed). Each call may submit on-chain transactions with SOL costs, so in practice you should [check whether the account is already registered](./account-state) before calling it. ## Usage ```typescript theme={null} import { getUserRegistrationFunction } from "@umbra-privacy/sdk/registration"; const register = getUserRegistrationFunction({ client }); const signatures = await register({ confidential: true, // register the X25519 key (Shared ETAs + receiver-burnable notes) anonymous: true, // register the user commitment (Stealth Pool transfers as the recipient) }); console.log(`Completed ${signatures.length} registration transaction(s)`); // 0 if already fully registered. // 1–2 depending on which sub-steps were needed (registration splits into two independent on-chain instructions). ``` ## Options Register the X25519 token-encryption pubkey for Shared-mode ETAs. Without this, deposits stay in MXE-only mode and you cannot query your balance locally. Register the on-chain user commitment via MPC + ZK. Required to receive **receiver-burnable** Stealth Pool Notes. Can be used independently of `confidential`. Per-step lifecycle hooks plus top-level phase hooks. Skippable step slots take `{ onPreSend, onPostSend, onSkipped }`. ```typescript theme={null} await register({ confidential: true, anonymous: true, hooks: { // Top-level phase hooks onValidationStart: async () => console.log("Validating..."), onAccountFetchComplete: async ({ userAccountExists, hasX25519Key, isAnonymous }) => console.log("Account state:", { userAccountExists, hasX25519Key, isAnonymous }), onKeyDerivationComplete: async ({ elapsedMs }) => console.log("Keys ready in", elapsedMs, "ms"), // Per-step hooks (skippable; onSkipped fires when state is already correct) initUserAccount: { onSkipped: async ({ reason }) => console.log("Account already exists:", reason), onPreSend: async ({ signedTransaction }) => console.log("Creating account..."), onPostSend: async ({ signature }) => console.log("Account created:", signature), }, registerX25519PublicKey: { onSkipped: async ({ reason }) => console.log("X25519 key already registered:", reason), onPreSend: async () => console.log("Registering X25519 key..."), onPostSend: async ({ signature }) => console.log("X25519 registered:", signature), }, registerAnonymousUsage: { onSkipped: async ({ reason }) => console.log("Anonymous usage already enabled:", reason), onPreSend: async () => console.log("Registering user commitment..."), onPostSend: async ({ signature }) => console.log("Commitment registered:", signature), }, // Terminal onComplete: async (result) => console.log("Registration complete:", result), onError: async ({ phase, error }) => console.error("Failed in phase", phase, ":", error), }, }); ``` The hook slot names are **`initUserAccount`, `registerX25519PublicKey`, `registerAnonymousUsage`**. Each is `SkippableDirectTransactionStepHooks` or `SkippableMpcTransactionStepHooks` — `{ onPreSend, onPostSend, onSkipped }`. ## The registration flow Registration is **two independent on-chain instructions**. Both `init_if_needed` the `EncryptedUserAccount` PDA, so either can run first. Synchronous. Initialises the `EncryptedUserAccount` PDA (if needed) and stores the X25519 token-encryption pubkey derived from your master seed. Enables **Shared encryption mode** — deposits get encrypted under both the Arcium MPC key and your key, so you can decrypt your own balance locally. Run when `confidential: true`. MPC + ZK. Initialises the user account (if needed), stages master-viewing-key material, runs Fiat-Shamir + [Groth16](https://eprint.iacr.org/2016/260.pdf), queues the Arcium MPC computation, and on callback stores the on-chain user commitment. Enables you to **receive** receiver-burnable Stealth Pool Notes. Run when `anonymous: true`. Both sub-steps require the master seed to be derived (overridable via [dependency injection](/sdk/understanding-the-sdk/dependency-injection#master-seed-storage)). The wallet signing prompt fires the first time the seed is needed if it has not been derived yet in this session. ## On-Chain Account After registration, the user's `EncryptedUserAccount` PDA stores: * X25519 public key (for Shared mode deposits addressed to this user) * Poseidon user commitment (for ZK proof generation) * Generation index (monotonic counter used for nonce derivation) * Random generation seed (entropy mixed into nonces) * Status flags for each registration step This account is the persistent on-chain identity for the wallet address. It does not hold any tokens. ## Error Handling Registration can fail at several distinct points. Use `isRegistrationError` from `@umbra-privacy/sdk/errors` and `switch` on `err.stage` to handle each one. ```typescript theme={null} import { isRegistrationError } from "@umbra-privacy/sdk/errors"; try { await register({ confidential: true, anonymous: true }); } catch (err) { if (isRegistrationError(err)) { switch (err.stage) { case "master-seed-derivation": // User declined to sign the master seed derivation message. // Without the seed, no keys can be derived and registration cannot proceed. showNotification("Please sign the master seed message to set up your account."); break; case "transaction-sign": // User rejected one of the registration transactions in their wallet. showNotification("Registration cancelled."); break; case "zk-proof-generation": // ZK proof generation failed. Only applies to the commitment step (anonymous: true). console.error("Proof generation failed:", err.message); showNotification("Failed to generate proof. Please try again."); break; case "account-fetch": // RPC connectivity issue while fetching on-chain account state. console.error("RPC error during registration:", err.message); break; case "transaction-send": // Transaction submitted but confirmation timed out. // The transaction may have landed - check on-chain before retrying. console.warn("Confirmation timeout. Check on-chain before retrying."); break; default: console.error("Registration failed at stage:", err.stage, err); } } else { throw err; } } ``` **Partial completion**: Registration submits up to two separate transactions. If the second transaction fails or is cancelled, the first is already confirmed on-chain. Call `register()` again — it checks on-chain state and skips the sub-step that already completed. See [Error Handling](/reference/errors) for a full reference of all error types. # Transfers Source: https://sdk.umbraprivacy.com/sdk/transfers An overview of the transfer models in Umbra — unlinkable transfers via Stealth Pool Notes, confidential direct transfers via getTransferorFunction (ETA-to-ETA), and amount-hiding ETA operations. ## Overview Umbra supports two distinct models for transferring tokens privately. They differ in what they hide, what infrastructure they require, and how strong their privacy guarantees are. ## Unlinkable transfers (via the Stealth Pool) The strongest privacy model. The on-chain link between the sender's note write and the recipient's burn is broken entirely — an observer cannot connect the two events by examining the chain. Tokens enter the pool as a **Stealth Pool Note** commitment inserted into an on-chain Indexed Merkle Tree. The note is later **burned** with a Groth16 ZK proof that proves ownership of the note without revealing which commitment is being spent. The note write and the burn are indistinguishable from every other note write and burn in the same tree. Note writes have four variants — `(source × unlocker)`: * **From an ATA** — tokens enter the pool directly from an `AssociatedTokenAccount`. Single tx, no MPC. * **From an ETA** — tokens are drawn from an `EncryptedTokenAccount` before entering the pool. Two-tx pipeline (proof account → note write), MPC under the hood. Each source has both a **self-burnable** variant (sender keeps the unlocker; useful for "stage funds and burn out from a fresh wallet" flows — recipient needs no on-chain state) and a **receiver-burnable** variant (unlocker encrypted against the recipient's `userCommitment` — recipient must have completed all three registration sub-steps). Burns have three shipped variants: * **Receiver-burnable → ETA** — anonymous payment into the recipient's encrypted balance. * **Self-burnable → ETA** — write a note, then burn it back into an encrypted balance from any wallet. * **Self-burnable → ATA** — write a note, then burn it out to a plaintext ATA from any wallet. (Receiver-burnable → ATA exists on-chain but is not yet exposed in the SDK.) See the [Stealth Pool](/sdk/mixer/overview) section for the full API and per-variant pre-checks. Receiver-burnable notes require the **recipient** to have completed full registration (`confidential: true` **and** `anonymous: true`). Self-burnable notes have no recipient-side prerequisites — they're the right choice for one-shot transfers when the recipient hasn't onboarded yet. ## Confidential-only transfers (via EncryptedTokenAccounts) A lighter-weight privacy model that hides token amounts and balances on-chain, but does not break the link between sender and recipient. The transfer is confidential — amounts are hidden under MPC encryption — but the participating addresses remain observable. This covers scenarios where amount privacy is the goal and linkability is an acceptable trade-off: for example, moving tokens between accounts you control without exposing the amounts, or settling a payment where both parties know each other but want the value hidden. The on-chain program supports several confidential-transfer variants. For direct ETA-to-ETA transfers, use `getTransferorFunction` documented below. For moving funds between ATAs and your own ETAs, use [deposit](/sdk/deposit) / [withdraw](/sdk/withdraw) / [convert](/sdk/conversion). ## ETA-to-ETA Direct Transfer `getTransferorFunction` from `@umbra-privacy/sdk/transfer` performs a direct confidential transfer between two `EncryptedTokenAccount`s. A single MPC instruction moves the amount from the sender's encrypted balance to the receiver's, creating or initialising the receiver's token account if needed. **When to use this vs the Stealth Pool:** Transfer settles immediately (one MPC instruction) but the on-chain event is linkable — an observer can see that two Umbra users interacted. The Stealth Pool breaks that link entirely but requires a note write followed by a separate burn, which takes longer and involves the relayer. Use Transfer when speed matters and the relationship between sender and receiver is already known; use the Stealth Pool when on-chain anonymity is the goal. ### Import ```typescript theme={null} import { getTransferorFunction } from "@umbra-privacy/sdk/transfer"; import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk/query"; ``` ### Factory pattern ```typescript theme={null} const transfer = getTransferorFunction({ client }, deps?); // deps?: { accountInfoProvider?, executorConfig? } ``` ### Recipient pre-check The recipient must have an `EncryptedTokenAccount` (or the transfer will create one, provided they have an `x25519PublicKey` registered). Verify before calling: ```typescript theme={null} const queryAccount = getUserAccountQuerierFunction({ client }); const receiverAccount = await queryAccount(receiverAddress); if (receiverAccount.state !== "exists" || !receiverAccount.data?.isUserAccountX25519KeyRegistered) { throw new Error("Recipient has not registered an X25519 key on Umbra"); } ``` ### Calling the function ```typescript theme={null} const result = await transfer({ receiverAddress, mint, transferAmount, // U64 — amount in mint's smallest unit optionalData?, // OptionalData32 — pre-hashed, never plaintext microLamportsPerAcu?, accountInfoCommitment?, }); ``` ### TransferVariant — the 8 on-chain variants The SDK auto-selects the correct variant by fetching all four accounts in one RPC round-trip: ```typescript theme={null} type TransferVariant = | "network_to_existing_network" | "network_to_existing_shared" | "network_to_new_network" | "network_to_new_shared" | "shared_to_existing_network" | "shared_to_existing_shared" | "shared_to_new_network" | "shared_to_new_shared"; ``` The two dimensions are: * **Sender mode** — `network` (MXE-only balance) or `shared` (user-decryptable shared balance) * **Receiver state** — `existing_network`, `existing_shared`, `new_network`, `new_shared` Two helper guards are exported: ```typescript theme={null} import { isSharedSenderVariant, isNewReceiverVariant } from "@umbra-privacy/sdk/transfer"; if (isSharedSenderVariant(result.preparation.variant)) { // SDK submitted the transaction; result.kind === "submitted" } if (isNewReceiverVariant(result.preparation.variant)) { // SDK created the receiver's token account as part of the transfer } ``` ### TransferResult The result is a discriminated union on `kind`: ```typescript theme={null} type TransferResult = | { kind: "prepared"; preparation: TransferPreparation } | { kind: "submitted"; preparation: TransferPreparation; signature: TransactionSignature }; ``` * **`"submitted"`** — returned for `shared_to_*` variants; the SDK ran the full queue-computation pipeline. * **`"prepared"`** — returned for `network_to_*` variants; the SDK built and returned the preparation bundle and the caller decides what to do with it (typically for advanced flows or batching). ### Privacy note All 8 variants conceal the transferred amount. However, the on-chain instruction reveals that the two wallet addresses are both Umbra users and that a transfer occurred between them at a given slot. If you need to hide the relationship entirely, use a Stealth Pool note instead. # Branded Types Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/branded-types Branded types with phantom symbol branding: createU64, createU128, createBn254FieldElement, createX25519PublicKey. Prevents silent bigint/Uint8Array misuse at compile time, zero runtime cost. ## Why Branded Types Exist TypeScript uses **structural typing** — two types with the same underlying representation are treated as identical by the compiler. This means a raw `bigint` that represents an encrypted balance ciphertext is structurally the same as one that represents a Poseidon encryption key, or a U64 token amount. Without extra guardrails, you can pass the wrong value and TypeScript will not warn you. The SDK uses **branded types** (also called nominal types) to prevent this class of silent bug. Every cryptographic and numeric value in the SDK carries a phantom brand that makes it distinct from every other value with the same base type — at compile time, with zero runtime overhead. ```typescript theme={null} // Without branding — TypeScript accepts both, one is silently wrong: function encryptBalance(key: bigint, plaintext: bigint) { ... } encryptBalance(myBalance, myKey); // Oops — args are swapped, no error // With branding — the compiler catches the swap: function encryptBalance(key: RcKey, plaintext: RcPlaintext) { ... } encryptBalance(myBalance, myKey); // Error: RcPlaintext is not assignable to RcKey ``` *** ## The Type Hierarchy The SDK's branded types form a hierarchy. A more specific type is always assignable to its parent, but a parent is never assignable to a more specific child without an explicit helper call. **Integer types** (base: `bigint`): * `U64` — 64-bit unsigned integer (`0` to `2^64 - 1`). Used for token amounts and instruction parameters. * `U128` — 128-bit unsigned integer. Used for instruction seeds and generation indices. * `U256` — 256-bit unsigned integer. Used for Stealth Pool Note commitment preimages and random generation seeds. **Cryptographic field elements** (base: `bigint`, extend `U256`): * `Bn254FieldElement` — BN254 scalar field element. Strict upper bound: the BN254 field prime. * `Curve25519FieldElement` — Curve25519 field element. Strict upper bound: `2^255 - 19`. **Poseidon types** (extend `Bn254FieldElement`): * `PoseidonKey` — Encryption key for the Poseidon cipher scheme. Derived from the user's master seed. **Rescue Cipher types** (extend `Curve25519FieldElement`): * `RcPlaintext` — An unencrypted balance value. * `RcCiphertext` — An encrypted balance value stored on-chain. * `RcKey` — A session encryption key derived from the Poseidon key and account nonce. * `RcEncryptionNonce` — The monotonically increasing counter (derived from `generationIndex`) that ensures each encryption produces a distinct ciphertext. **Protocol types** (base: `Uint8Array` or `bigint`): * `OptionalData32` — A 32-byte opaque payload stored alongside deposits and burns. Must be encrypted or hashed — never store plaintext identifiers. * `MicroLamportsPerAcu` — Priority fee in micro-lamports per Arcium Computation Unit. **Byte array types** (base: `Uint8Array`): * `X25519PublicKey`, `X25519PrivateKey`, `SharedSecret` — 32-byte X25519 values. * Sized little-endian/big-endian variants (`U64LeBytes`, `U256BeBytes`, etc.) for serialisation. *** ## The `create*` Helper Functions Raw `bigint` or `Uint8Array` values you read from on-chain accounts, compute locally, or receive from external sources are unbranded. Before passing them to any SDK function that expects a branded type, run them through the corresponding `create*` helper. Each helper: 1. **Validates** the value at runtime (range check, type check, length check). 2. **Brands** the value — returns it typed as the specific branded type. 3. **Throws** a descriptive error (never silently returns `undefined`) if validation fails. Import helpers from `@umbra-privacy/sdk/types`: ```typescript theme={null} import { createU64, createU128, createU256, createBn254FieldElement, createCurve25519FieldElement, createPoseidonKey, createRcPlaintext, createRcCiphertext, createRcKey, createRcEncryptionNonce, createBase85Limb, createOptionalData32, createMicroLamportsPerAcu, } from "@umbra-privacy/sdk/types"; ``` ### Mathematics Helpers These are the helpers you will use most often. Token amounts, offsets, and protocol-level integers all go through here. ```typescript theme={null} // createU64 — validate and brand a bigint as a 64-bit unsigned integer. // Use for token amounts, fee values, and any U64 parameter. const amount: U64 = createU64(1_000_000n, "amount"); // Throws MathematicsAssertionError for negative or oversized values: createU64(-1n); // MathematicsAssertionError createU64(2n ** 64n); // MathematicsAssertionError ``` ```typescript theme={null} // createU128 — validate and brand a bigint as a 128-bit unsigned integer. // Use for instruction seeds and generation-index-derived values. const seed: U128 = createU128(340282366920938463463374607431768211455n, "seed"); ``` ```typescript theme={null} // createU256 — validate and brand a bigint as a 256-bit unsigned integer. // Use for Stealth Pool Note commitment preimages, random generation seeds, and full-width scalars. const commitment: U256 = createU256(rawCommitmentBigint, "noteCommitment"); ``` ### Field Element Helpers ```typescript theme={null} // createBn254FieldElement — validates that the value is in the BN254 scalar field. // Required for Groth16 ZK proof public inputs and Poseidon hash inputs. const publicInput: Bn254FieldElement = createBn254FieldElement(leafHash, "leafHash"); ``` ```typescript theme={null} // createCurve25519FieldElement — validates that the value is in the Curve25519 field. // Required for X25519 key derivation. const privKey: Curve25519FieldElement = createCurve25519FieldElement(rawKey, "x25519PrivateKey"); ``` ```typescript theme={null} // createPoseidonKey — validates a BN254 field element as a Poseidon cipher key. // Keys are derived from the user's master seed; call this after derivation. const encKey: PoseidonKey = createPoseidonKey(derivedKeyBigint); ``` ### Rescue Cipher Helpers The Rescue Cipher encrypts on-chain encrypted balances. Keeping plaintexts, ciphertexts, keys, and nonces as distinct types prevents the most dangerous class of encryption bug — passing a ciphertext where the decryptor expects a key. ```typescript theme={null} // createRcPlaintext — the unencrypted balance value before encryption. const balance: RcPlaintext = createRcPlaintext(rawBalance, "mxeBalance"); // createRcCiphertext — the encrypted balance stored on-chain. const ct: RcCiphertext = createRcCiphertext(rawCiphertext, "encryptedBalance"); // createRcKey — the session key passed to the Rescue Cipher. const sessionKey: RcKey = createRcKey(derivedKey, "rcKey"); // createRcEncryptionNonce — the per-account counter derived from generationIndex. const nonce: RcEncryptionNonce = createRcEncryptionNonce(generationIndex, "encryptionNonce"); ``` ### Protocol Helpers ```typescript theme={null} // createOptionalData32 — validates a Uint8Array as a 32-byte optional metadata payload. // Used as the optionalData field in deposit, withdraw, and Stealth Pool Note operations. // MUST be a pre-hashed or pre-encrypted 32-byte value — never a plaintext orderId or identifier. const noData: OptionalData32 = createOptionalData32(new Uint8Array(32)); // createMicroLamportsPerAcu — validates a bigint as a priority fee (>= 0). // Pass as the `microLamportsPerAcu` option on operation calls to speed up processing under network load. const fee: MicroLamportsPerAcu = createMicroLamportsPerAcu(1000n); ``` ### Limb Helper ```typescript theme={null} // createBase85Limb — validates a bigint as a 85-bit limb (0 to 2^85 - 1). // Used internally when decomposing 256-bit values into three limbs for ZK circuit inputs. const limb0: Base85Limb = createBase85Limb(value & ((1n << 85n) - 1n), "limb0"); const limb1: Base85Limb = createBase85Limb((value >> 85n) & ((1n << 85n) - 1n), "limb1"); const limb2: Base85Limb = createBase85Limb(value >> 170n, "limb2"); ``` *** ## Common Patterns ### Converting a user-supplied number to U64 User-facing forms and external APIs often produce plain `number` or `string`. Always validate before passing to the SDK: ```typescript theme={null} import { createU64 } from "@umbra-privacy/sdk/types"; // User input from a form field (string) const rawInput = "1000000"; // 1 USDC (6 decimals) const amount: U64 = createU64(BigInt(rawInput), "depositAmount"); // Call the SDK function — amount is now type-safe const signature = await deposit("DestinationAddress", mint, amount); ``` ### Reading on-chain data and re-branding Account data fetched via RPC arrives as raw `bigint`. Validate before any cryptographic operation: ```typescript theme={null} import { createRcCiphertext } from "@umbra-privacy/sdk/types"; // rawAccount.ciphertext is bigint from on-chain deserialization const ciphertext: RcCiphertext = createRcCiphertext( rawAccount.ciphertext, "onChainCiphertext", ); ``` ### The `assert*` vs `create*` distinction The SDK exports both families. Use `create*` (not `assert*`) in expression contexts — `assert*` returns `void` and forces a two-step pattern: ```typescript theme={null} // ❌ Verbose — requires a temporary variable and an unsafe cast assertU64(rawAmount, "amount"); const amount = rawAmount as unknown as U64; // ✓ Concise — validates and brands in a single expression const amount = createU64(rawAmount, "amount"); ``` ### Error handling All helpers throw a typed error on failure. Catch at the boundary where you receive external input: ```typescript theme={null} import { createU64 } from "@umbra-privacy/sdk/types"; import { MathematicsAssertionError } from "@umbra-privacy/sdk/errors"; try { const amount = createU64(BigInt(userInput), "amount"); await deposit(destination, mint, amount); } catch (err) { if (err instanceof MathematicsAssertionError) { // Show a validation message to the user setError(`Invalid amount: ${err.message}`); } else { throw err; } } ``` ### Passing branded values through the SDK Once a value is branded, you can pass it directly to any SDK function that accepts that type — no additional wrapping needed: ```typescript theme={null} import { createU64, createU256 } from "@umbra-privacy/sdk/types"; const amount: U64 = createU64(depositAmountBigint, "amount"); const seed: U256 = createU256(randomSeedBigint, "randomSeed"); // Both are now type-safe for use in SDK operations const signature = await createSelfBurnableNote({ amount, mint, destinationAddress }, { generationIndex: seed }); ``` *** ## What the SDK Does Not Accept You cannot pass a plain `bigint` where a branded type is required — the TypeScript compiler will reject the call: ```typescript theme={null} const raw: bigint = 1_000_000n; // Error: Argument of type 'bigint' is not assignable to parameter of type 'U64' await deposit(destination, mint, raw); // Correct: await deposit(destination, mint, createU64(raw, "amount")); ``` This is intentional. The validation the `create*` helpers perform is not optional — it is the mechanism that guarantees protocol invariants hold before values reach the on-chain program. # Transaction Callbacks Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/callbacks V18 per-operation hooks interfaces: DirectTransactionStepHooks, MpcTransactionStepHooks, RegistrationHooks, BurnHooks. Named per-phase/per-step slots replace the V13 generic TransactionCallbacks. ## Overview V18 uses per-operation **hooks** objects (not a single generic `TransactionCallbacks`). Each factory's `deps.hooks` or `options.hooks` slot accepts a named interface specific to that operation, with named per-phase / per-step hook keys. Hooks observe the operation as it moves through validation, fetching, ZK proving, transaction submission, MPC callback waiting, and completion. The two transaction-level building blocks every hooks interface uses: ```typescript theme={null} // Direct (non-MPC) transaction step — used for the synchronous registration sub-steps // and for any single-tx operation. interface DirectTransactionStepHooks { readonly interceptInstructions?: (event: InstructionsReadyEvent) => Promise | readonly Instruction[]; readonly onTransactionBuilt?: (event: TransactionBuiltEvent) => Promise; readonly onTransactionSigned?: (event: TransactionSignedEvent) => Promise; readonly onPreSend?: (event: PreSendEvent) => Promise; readonly onPostSend?: (event: PostSendEvent) => Promise; } // Same, but with an extra `onSkipped` slot fired when the step is a no-op // because the on-chain state already satisfies it (e.g. initUserAccount when // the account already exists). interface SkippableDirectTransactionStepHooks extends DirectTransactionStepHooks { readonly onSkipped?: (event: { readonly reason: string }) => Promise; } // MPC transaction step — extends DirectTransactionStepHooks with Arcium MPC // progress events and rent-reclaim outcome hooks. interface MpcTransactionStepHooks extends DirectTransactionStepHooks { readonly onMonitorStarted?: (event: { readonly computationAccount: Address }) => Promise; readonly onProgress?: (event: ComputationMonitorProgressEvent) => Promise; readonly onFinalized?: (event: ComputationFinalizedEvent) => Promise; readonly onRentReclaimSubmitted?: (event: { readonly signature: TransactionSignature }) => Promise; readonly onRentReclaimError?: (event: { readonly error: string }) => Promise; } ``` **Skipped steps fire `onSkipped`, not `onPreSend` / `onPostSend`.** This is the V18 way to observe "the step was a no-op because the on-chain state was already correct." *** ## Usage — Registration V18 registration uses a single `hooks` option (`RegistrationHooks`) with named per-step slots and top-level phase hooks. Each skippable step slot takes `{ onPreSend, onPostSend, onSkipped }`. ```typescript theme={null} import { getUserRegistrationFunction } from "@umbra-privacy/sdk/registration"; const register = getUserRegistrationFunction({ client }); await register({ confidential: true, anonymous: true, hooks: { onValidationStart: async () => setStatus("Validating..."), onAccountFetchComplete: async ({ userAccountExists }) => setStatus(userAccountExists ? "Account exists" : "Will create account"), initUserAccount: { onSkipped: async ({ reason }) => setStatus("Account already initialised: " + reason), onPreSend: async () => setStatus("Creating account..."), onPostSend: async ({ signature }) => setProgress(33), }, registerX25519PublicKey: { onSkipped: async ({ reason }) => setStatus("X25519 key already registered: " + reason), onPreSend: async () => setStatus("Registering encryption key..."), onPostSend: async ({ signature }) => setProgress(66), }, registerAnonymousUsage: { onSkipped: async ({ reason }) => setStatus("Anonymous usage already enabled: " + reason), onPreSend: async () => setStatus("Enabling anonymous mode..."), onPostSend: async ({ signature }) => { setProgress(100); setStatus("Done!"); }, }, onComplete: async (result) => setStatus(`Registered with ${result.signatures.length} signatures`), onError: async ({ phase, error }) => console.error("Registration failed at", phase, error), }, }); ``` Step slot names: `initUserAccount`, `registerX25519PublicKey`, `registerAnonymousUsage`. Each fires `onSkipped` when the on-chain state already satisfies the step, `onPreSend` before the transaction is submitted, and `onPostSend` after it confirms. *** ## Usage — Stealth Pool Note creation Creating a Stealth Pool Note involves up to three transactions. Each has its own callbacks slot inside the `options` argument: ```typescript theme={null} import { getATAIntoSelfBurnableStealthPoolNoteCreatorFunction } from "@umbra-privacy/sdk/deposit"; import { getATAIntoStealthPoolNoteCreatorProver } from "@umbra-privacy/sdk/zk-prover"; const createNote = getATAIntoSelfBurnableStealthPoolNoteCreatorFunction( { client }, { zkProver: getATAIntoStealthPoolNoteCreatorProver() }, ); await createNote( { destinationAddress: recipient, mint, amount }, { createUtxo: { pre: async (tx) => console.log("Submitting note write..."), post: async (tx, sig) => console.log("Note written:", sig), }, createProofAccount: { pre: async (tx) => console.log("Creating proof account..."), post: async (tx, sig) => console.log("Proof account:", sig), }, closeProofAccount: { // Only fires if a stale proof account was found and closed. pre: async (tx) => console.log("Closing stale proof account..."), post: async (tx, sig) => console.log("Closed:", sig), }, }, ); ``` *** ## Usage — Single-transaction operations Deposits, withdrawals, conversions, and most other operations take an `options.hooks` field whose shape is operation-specific. There is no single global `TransactionCallbacks { pre, post }` shape — each operation defines its own hooks interface with named per-phase slots: ```typescript theme={null} import { getATAIntoETADirectDepositorFunction } from "@umbra-privacy/sdk/deposit"; const deposit = getATAIntoETADirectDepositorFunction({ client }); await deposit(destinationAddress, mint, amount, { hooks: { onValidationStart: async () => console.log("Validating..."), onMintFetchComplete: async ({ tokenProgram, hasTransferFee }) => console.log("Mint:", tokenProgram, "T22 fee:", hasTransferFee), onAccountFetchComplete: async ({ userAccountExists, isSharedMode }) => console.log("Account:", { userAccountExists, isSharedMode }), queueComputation: { onPreSend: async ({ signedTransaction }) => console.log("Sending deposit..."), onPostSend: async ({ signature }) => console.log("Queued:", signature), onMonitorStarted: async ({ computationAccount }) => console.log("MPC monitoring:", computationAccount), onFinalized: async ({ callbackSignature }) => console.log("MPC callback:", callbackSignature), }, onComplete: async (result) => console.log("Deposit done:", result.queueSignature), onError: async ({ phase, error }) => console.error("Failed at", phase, error), }, }); ``` The exact hook slot names per operation: * **Deposit** (`ATAIntoETADirectDepositHooks`) — top-level: `onValidationStart/Complete`, `onMintFetchStart/Complete`, `onAccountFetchStart/Complete`, `onArciumSetupStart/Complete`, `onInstructionBuildStart/Complete`, `onComplete`, `onError`. Per-step: `queueComputation: MpcTransactionStepHooks` — slots: `onPreSend`, `onPostSend`, `onMonitorStarted`, `onProgress`, `onFinalized`, `onRentReclaimSubmitted`, `onRentReclaimError`. * **Withdrawal** (`WithdrawalHooks`) — analogous shape with the same MpcTransactionStepHooks slot. * **Conversion** (`ConvertToSharedHooks`) — per-mint hooks plus `onComplete` / `onError`. *** ## Usage — Burn lifecycle hooks (`BurnHooks`) Burns are relayer-managed: ZK proof generation, relayer submission, polling, and the on-chain callback all happen behind a single `burn()` call. The burner factory exposes per-stage **lifecycle hooks** on the `deps.hooks` slot. The hooks are named per-event (not generic `onPhase*`): ```typescript theme={null} import { getReceiverBurnableStealthPoolNoteIntoETABurnerFunction } from "@umbra-privacy/sdk/burn"; const burn = getReceiverBurnableStealthPoolNoteIntoETABurnerFunction( { client }, { fetchBatchMerkleProof: client.fetchBatchMerkleProof!, zkProver, relayer: { submitBurn: r.submitClaim, pollBurnStatus: r.pollClaimStatus, getRelayerAddress: r.getRelayerAddress }, hooks: { // Setup phase onKeyDerivationStart: async () => console.log("Deriving keys..."), onKeyDerivationComplete: async ({ elapsedMs }) => console.log("Keys ready in", elapsedMs, "ms"), // Batch assembly onBatchAssemblyStart: async () => console.log("Assembling batches..."), onBatchAssemblyComplete: async ({ batchCount, totalStealthPoolNotes }) => console.log("Assembled", batchCount, "batches for", totalStealthPoolNotes, "notes"), // Per-batch lifecycle onBatchStart: async ({ batchIndex, batchCount }) => console.log(`Batch ${batchIndex}/${batchCount} starting`), onBatchZkProofGenerationStart: async ({ batchIndex }) => console.log("Batch", batchIndex, "proving..."), onBatchZkProofGenerationComplete: async ({ batchIndex, elapsedMs }) => console.log("Batch", batchIndex, "proof done in", elapsedMs, "ms"), onBatchSubmitted: async ({ batchIndex, requestId }) => console.log("Batch", batchIndex, "submitted:", requestId), onBatchProgress: async ({ batchIndex, status }) => console.log("Batch", batchIndex, "status:", status), onBatchComplete: async ({ batchIndex, status, signatures }) => console.log("Batch", batchIndex, "complete:", status, signatures), // Terminal onComplete: async (result) => console.log("Burn complete:", result), onError: async ({ phase, error }) => console.error("Burn error in phase", phase, ":", error), }, }, ); ``` `onError`'s `phase` is one of: `"keyDerivation"`, `"batchAssembly"`, `"zkProofGeneration"`, `"batchSubmission"`, `"batchPolling"`. # Dependency Injection Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/dependency-injection Dependency injection: args (required params) vs deps (injectable RPC, blockhash, tx forwarding, master seed). Client-level defaults with per-call commitment overrides on any factory function. ## Client-Level Defaults `getUmbraClient` accepts its own `deps` object that sets the **default** for every downstream function. Any function-level `deps` override takes precedence. ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; const client = await getUmbraClient( { signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl, indexerApiEndpoint, // optional but required for scanning + burning relayerApiEndpoint, // optional but required for burning utxoDataStore, // optional but strongly recommended on browser nullifierStore, // optional but strongly recommended on browser }, { accountInfoProvider, // override RPC account fetching blockhashProvider, // override blockhash source transactionForwarder, // override broadcast + confirmation epochInfoProvider, // override epoch info masterSeedStorage: { // control seed persistence load, store, generate, }, }, ); ``` ```typescript theme={null} interface GetUmbraClientFromSignerDeps { readonly accountInfoProvider?: AccountInfoProviderFunction; readonly blockhashProvider?: GetLatestBlockhash; readonly transactionForwarder?: TransactionForwarder; readonly epochInfoProvider?: GetEpochInfo; readonly masterSeedStorage?: { readonly load?: MasterSeedLoaderFunction; readonly store?: MasterSeedStorerFunction; readonly generate?: MasterSeedGeneratorFunction; }; } ``` *** ## Infrastructure Providers These appear as optional overrides across virtually every function's `deps` type. ### `accountInfoProvider` Fetches raw on-chain account data. Used by every function that reads on-chain state. ```typescript theme={null} type AccountInfoProviderFunction = ( addresses: readonly Address[], ) => Promise>; ``` Default: RPC batch fetch via `rpcUrl`. Override for: unit tests (no live node), custom RPC middleware, account caching, or simulated state. ### `blockhashProvider` / `getLatestBlockhash` Returns the latest confirmed [blockhash](https://solana.com/docs/terminology#blockhash) and last valid block height for transaction construction. ```typescript theme={null} type GetLatestBlockhash = () => Promise<{ blockhash: string; lastValidBlockHeight: number; }>; ``` Default: RPC call via `rpcUrl`. Override for: deterministic transaction snapshots in tests, or to fetch from an alternative source. ### `transactionForwarder` Handles transaction broadcast and confirmation. Used by every function that submits on-chain transactions. ```typescript theme={null} interface TransactionForwarder { forwardSequentially(txs: readonly SignedTransaction[]): Promise; forwardInParallel(txs: readonly SignedTransaction[]): Promise; } ``` Default: [WebSocket](https://solana.com/docs/rpc/websocket/signaturesubscribe)-based forwarder using `signatureSubscribe` for confirmation. Override for [Jito](https://www.jito.network/) bundle submission, priority fee management, dry-run recording in tests, or custom retry logic: ```typescript theme={null} const jitoForwarder: TransactionForwarder = { forwardSequentially: async (txs) => submitToJitoBundleEndpoint(txs), forwardInParallel: async (txs) => submitToJitoBundleEndpoint(txs), }; ``` ### `epochInfoProvider` Returns current Solana epoch information. Used for [Token-2022 transfer fee](/advanced/token-2022) schedule selection. ```typescript theme={null} type GetEpochInfo = () => Promise; ``` Default: RPC call via `rpcUrl`. *** ## Master Seed Storage Controls how the master seed is persisted and derived between sessions. Provided at the client level only. ```typescript theme={null} interface MasterSeedStorage { load: () => Promise<{ exists: true; seed: MasterSeed } | { exists: false }>; store: (seed: MasterSeed) => Promise; generate: () => Promise; // called when load returns exists: false } ``` Default: re-derive from `signer.signMessage(UMBRA_MESSAGE_TO_SIGN)` on every client construction. No persistence across page loads. ```typescript theme={null} // CI — fixed deterministic seed, no wallet prompt. const client = await getUmbraClient(args, { masterSeedStorage: { generate: async () => FIXED_TEST_SEED, }, }); ``` Never persist the master seed in plaintext `localStorage` / `IndexedDB`. The seed is equivalent in sensitivity to a private key. Production persistence requires encrypted-at-rest storage keyed by a user-controlled secret (WebAuthn-derived, password-derived via Argon2id, or wallet-signed challenge) **and** scoped per-wallet (key includes the wallet pubkey). The scaffold defaults to re-derive every session. See [Key Derivation](/sdk/advanced/key-derivation) for how the master seed is structured and derived from the wallet signature. # Key Generators & Crypto Operations Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/key-generators Injectable key generators: KMAC256-based MVK and blinding factor derivers, Poseidon user commitment generators, X25519 keypair generators. Override any derivation step via deps injection. ## Key Derivation Generator Overrides All cryptographic keys are derived from the master seed via [KMAC256](/sdk/advanced/cryptography/kmac256) or [Poseidon](/sdk/advanced/cryptography/poseidon). Every derivation step is injectable. The following generator types appear as optional `deps` across the functions that use them. ### Master Viewing Key Generators ```typescript theme={null} type MasterViewingKeyGeneratorFunction = () => Promise; type MasterViewingKeyBlindingFactorGeneratorFunction = () => Promise; ``` Used by: registration, Stealth Pool Note creation (self-burnable and receiver-burnable), burning. ### Poseidon Private Key Generators ```typescript theme={null} type PoseidonPrivateKeyGeneratorFunction = () => Promise; type PoseidonBlindingFactorGeneratorFunction = () => Promise; ``` Used by: registration, Stealth Pool Note creation, burning. ### X25519 / Curve25519 Keypair Generators ```typescript theme={null} // User account X25519 key - used for token account encryption type Curve25519KeypairGeneratorFunction = () => Promise; // Per-mint X25519 key - used for Stealth Pool Note ciphertext addressing type MintX25519KeypairGeneratorFunction = (mint: Address) => Promise; ``` Used by: registration, Stealth Pool Note creation. Also exposed via `getMintEncryptionKeyRotatorFunction` deps (in `@umbra-privacy/sdk/account`). ### Viewing Key Generators (Poseidon sub-hierarchy) ```typescript theme={null} type MintViewingKeyGeneratorFunction = ( mint: Address, options?: ViewingKeyGeneratorOptions, ) => Promise; type YearlyViewingKeyGeneratorFunction = ( mint: Address, year: Year, options?: ViewingKeyGeneratorOptions, ) => Promise; type MonthlyViewingKeyGeneratorFunction = ( mint: Address, year: Year, month: Month, options?: ViewingKeyGeneratorOptions, ) => Promise; type DailyViewingKeyGeneratorFunction = ( mint: Address, year: Year, month: Month, day: Day, options?: ViewingKeyGeneratorOptions, ) => Promise; type SecondViewingKeyGeneratorFunction = ( mint: Address, year: Year, month: Month, day: Day, hour: Hour, minute: Minute, second: Second, options?: ViewingKeyGeneratorOptions, ) => Promise; ``` Used by: Stealth Pool Note creation (the second viewing key scopes the note ciphertext). ### Commitment Factor Generators ```typescript theme={null} type RescueEncryptionCommitmentBlindingFactorGeneratorFunction = () => Promise; type RandomFactorForPolynomialCommitmentGeneratorFunction = () => Promise; type PoseidonKeystreamBlindingFactorGeneratorFunction = () => Promise; ``` ### Ephemeral Stealth Pool Note key generators Each note operation uses per-note ephemeral keys parameterised by the on-chain generation index. All are injectable. The TypeScript names retained their V13 spellings (`EphemeralUtxo*`) and the burn-side interfaces re-export them under V18 aliases: ```typescript theme={null} type EphemeralUtxoMasterViewingKeyDeriverFunction = (offset: U256) => Promise; type EphemeralUtxoMasterViewingKeyBlindingFactorDeriverFunction = (offset: U256) => Promise; type EphemeralUtxoPoseidonPrivateKeyDeriverFunction = (offset: U256) => Promise; type EphemeralUtxoPoseidonPrivateKeyBlindingFactorDeriverFunction = (offset: U256) => Promise; type EphemeralUtxoNullifierDeriverFunction = (offset: U256) => Promise; type EphemeralUtxoH2RandomSecretDeriverFunction = (offset: U256) => Promise; ``` Used by: all Stealth Pool Note creation and burner factories. *** ## Cryptographic Operation Overrides Beyond key generation, the low-level cryptographic operations themselves are injectable. ### Rescue Cipher (RC) - Encryption and Decryption The [Rescue cipher](/sdk/advanced/cryptography/rescue-cipher) is used to encrypt Stealth Pool Note payloads and EncryptedTokenAccount balances. ```typescript theme={null} type RcEncryptorFunction = ( plaintext: readonly RcPlaintext[], ) => Promise<{ ciphertexts: RcCiphertext[]; nonce: RcEncryptionNonce }>; type RcEncryptorWithNonceFunction = ( plaintext: readonly RcPlaintext[], nonce: RcEncryptionNonce, ) => Promise; type RcDecryptorFunction = ( ciphertext: readonly RcCiphertext[], nonce: RcEncryptionNonce, ) => Promise; type RcKeyGeneratorFunction = ( counters: readonly RcCounter[], nonce: RcEncryptionNonce, ) => Promise; ``` * `getRcEncryptor` and `getRcKeyGenerator` appear in: registration, Stealth Pool Note creation deps. * `rcDecryptor` appears in: `getEncryptedBalanceQuerierFunction` deps — override to provide a custom decryption backend. ### AES Encryption [AES-GCM](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf) is used for symmetric encryption of Stealth Pool Note commitment payloads. ```typescript theme={null} type AesEncryptorFunction = ( key: AesKey, plaintext: AesPlaintext, ) => Promise; type AesDecryptorFunction = ( key: AesKey, ciphertext: AesCiphertextWithMetadata, ) => Promise; ``` Used by: Stealth Pool Note creator factories. ### Poseidon Hash, Encryption, and Keystream See [Poseidon](/sdk/advanced/cryptography/poseidon) for the hash construction and security properties. ```typescript theme={null} type PoseidonHashFunction = ( dataPoints: readonly Bn254FieldElement[], ) => Promise; type PoseidonAggregatorHashFunction = ( dataPoints: readonly Bn254FieldElement[], ) => Promise; type PoseidonEncryptorFunction = ( plaintext: readonly PoseidonPlaintext[], key: PoseidonKey, ) => Promise; type PoseidonKeystreamGeneratorFunction = ( counters: readonly PoseidonCounter[], key: PoseidonKey, ) => Promise>; type KeystreamCommitmentFunction = ( keystream: Bn254FieldElement, blindingFactor: Bn254FieldElement, ) => Promise; ``` Used by: registration, Stealth Pool Note creation, burning. ### Fiat-Shamir Challenge and Polynomial Evaluation ```typescript theme={null} type FiatShamirChallengeGeneratorFunction = ( input: Uint8Array, ) => Curve25519FieldElement; type ChallengePowersFunction = ( challenge: Curve25519FieldElement, maxPower: number, ) => Curve25519FieldElement[]; type PolynomialEvaluatorFunction = ( coefficients: Curve25519FieldElement[], point: Curve25519FieldElement, ) => Curve25519FieldElement; ``` Used by: registration, Stealth Pool Note creation. Override to run evaluation in a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) or swap in a WASM implementation. ### User Commitment Generator ```typescript theme={null} type UserCommitmentGeneratorFunction = ( masterViewingKey: Bn254FieldElement, masterViewingKeyBlindingFactor: Bn254FieldElement, poseidonPrivateKey: Bn254FieldElement, poseidonBlindingFactor: Bn254FieldElement, ) => Promise; ``` Used by: registration, Stealth Pool Note creation. # Key Rotation & Deps Reference Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/key-rotation Offset-based key rotation: U512 offsets on getUmbraClient shift KMAC256 derivation to produce new X25519, MVK, and commitment keys for the same wallet. Complete deps map per V18 factory. ## Key Rotation via Offsets Key rotation is a distinct mechanism from dependency injection. Rather than replacing the generator function, you increment a numeric offset at client construction time. This causes the [KMAC256](/sdk/advanced/cryptography/kmac256) derivation to produce a completely different key for the same wallet, without changing any code. ```typescript theme={null} const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl, offsets: { x25519UserAccountPrivateKey: 1n, // rotate the token encryption key poseidonPrivateKey: 0n, masterViewingKey: 0n, x25519MasterViewingKeyEncryptingPrivateKey: 0n, mintX25519PrivateKey: 0n, rescueCommitmentBlindingFactor: 0n, randomCommitmentFactor: 0n, }, }); ``` Available offset keys: * `masterViewingKey` * `poseidonPrivateKey` * `x25519UserAccountPrivateKey` * `x25519MasterViewingKeyEncryptingPrivateKey` * `mintX25519PrivateKey` * `rescueCommitmentBlindingFactor` * `randomCommitmentFactor` Rotating an offset produces a different derived key. Any on-chain state — registered X25519 keys, encrypted balances, compliance grants — was created under the old key and cannot be accessed with the rotated key. Always re-register after rotating the user-account key, and update any grants that referenced the old MVK X25519 key. For active on-chain key rotation (issue a new key, re-encrypt existing state, retire the old key), use the rotators exported from `@umbra-privacy/sdk/account` — see [Recovery & Rotation](/sdk/advanced/recovery). *** ## Per-Factory Deps Reference (V18) A complete map of which injectable deps each factory accepts. Subpath in parens. ### Registration — `@umbra-privacy/sdk/registration` **`getUserRegistrationFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder`. * `masterViewingKeyDeriver`, `masterViewingKeyBlindingFactorDeriver`. * `poseidonPrivateKeyDeriver`, `poseidonBlindingFactorDeriver`. * `userAccountX25519KeypairGenerator`, `masterViewingKeyEncryptingX25519KeypairGenerator`. * `mintX25519KeypairGenerator`. * `rescueCommitmentBlindingFactorDeriver`, `randomFactorDeriver`. * `getRcKeyGenerator`, `getRcEncryptor`, `rescueCommitmentGenerator`. * `userCommitmentGenerator`. * `fiatShamirChallengeGenerator`, `challengePowersFunction`, `polynomialEvaluator`. * `poseidonAggregator`. * `bn254ModInverter`, `computeLimbwiseSumInverse`. * `zkProver` (`IZkProverForUserRegistration`) — **required**. ### Deposit — `@umbra-privacy/sdk/deposit` **`getATAIntoETADirectDepositorFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder`, `getEpochInfo`. ### Withdrawal — `@umbra-privacy/sdk/withdrawal` **`getETAIntoATAWithdrawerFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder`. ### Query — `@umbra-privacy/sdk/query` **`getUserAccountQuerierFunction`** * `accountInfoProvider`. **`getEncryptedBalanceQuerierFunction`** * `accountInfoProvider`. * `rcDecryptor` — override to provide a custom Rescue cipher decryption backend. ### Stealth Pool Note creation — `@umbra-privacy/sdk/deposit` **`getETAIntoSelfBurnableStealthPoolNoteCreatorFunction`** (ETA-source, MPC) * `accountInfoProvider`, `blockhashProvider`, `transactionForwarder`. * All MVK / Poseidon / ephemeral-note key derivers. * Cryptographic operation functions (Poseidon, Rescue, AES, Fiat-Shamir). * `zkProver` (`IZkProverForETAIntoStealthPoolNote`) — **required**. **`getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction`** * Same as above. * `zkProver` (`IZkProverForETAIntoStealthPoolNote`) — **required**. **`getATAIntoSelfBurnableStealthPoolNoteCreatorFunction`** (ATA-source, no MPC) * `accountInfoProvider`, `blockhashProvider`, `transactionForwarder`. * All MVK / Poseidon / ephemeral-note key derivers. * Cryptographic operation functions. * `zkProver` (`IZkProverForETAIntoStealthPoolNote`) — **required**. **`getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction`** * Same as above. * `zkProver` (`IZkProverForETAIntoStealthPoolNote`) — **required**. ### Stealth Pool Note scan + burn — `@umbra-privacy/sdk/burn` **`getBurnableStealthPoolNoteScannerFunction`** (zero-arg scanner) * `fetchStealthPoolNoteData` — override note-data fetcher. * `fetchMerkleProof` — override single-leaf Merkle proof fetcher used internally for sanity checks. (Per-batch proofs are a separate dep on the burner factories, not the scanner.) * `additionalX25519PrivateKeys` — extra X25519 keys to try when decrypting (multi-key wallets). **`getReceiverBurnableStealthPoolNoteIntoETABurnerFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder`. * All key derivers + cryptographic op functions. * `fetchBatchMerkleProof` — **required** (use `client.fetchBatchMerkleProof`). * `zkProver` (`IZkProverForClaimReceiverClaimableUtxoIntoEncryptedBalance`) — **required**. * `relayer: { submitBurn, pollBurnStatus, getRelayerAddress }` — **required** (TS aliases of the relayer client's `submitClaim`, `pollClaimStatus`, `getRelayerAddress`). **`getSelfBurnableStealthPoolNoteIntoETABurnerFunction`** * Same shape; `zkProver` is `IZkProverForClaimSelfClaimableUtxoIntoEncryptedBalance`. **`getSelfBurnableStealthPoolNoteIntoATABurnerFunction`** * Same shape; `zkProver` is `IZkProverForClaimSelfClaimableUtxoIntoPublicBalance`. ### Conversion — `@umbra-privacy/sdk/conversion` **`getNetworkEncryptionToSharedEncryptionConverterFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder`. ### Account — `@umbra-privacy/sdk/account` **`getStagedSolRecovererFunction`**, **`getStagedSplRecovererFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder`. Key rotators (rotation submodule): * `getUserEncryptionKeyRotator…` — sync, no MPC. * `getMintEncryptionKeyRotator…` — MPC; re-encrypts on-chain balance under the new per-mint key. * `getMasterViewingKeyRotator…` — MPC + ZK; rotates MVK X25519 + user commitment. Maintenance: * `getUserEntropySeedRotatorFunction`, `getTokenEntropySeedRotatorFunction` — sync, no MPC. ### Compliance — `@umbra-privacy/sdk/compliance` **`getComplianceGrantIssuerFunction`**, **`getComplianceGrantRevokerFunction`**, **`getSharedCiphertextReencryptorForUserGrantFunction`**, **`getNetworkCiphertextReencryptorForNetworkGrantFunction`**, **`getSharedCiphertextReencryptorForNetworkGrantFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` (those that submit transactions). **`getUserComplianceGrantQuerierFunction`**, **`getNetworkComplianceGrantQuerierFunction`**, **`getSharedComplianceGrantQuerierFunction`** * `accountInfoProvider`. # The SDK Pattern Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/overview SDK architecture (V18): factory pattern with verbose flow-based names, args (required params) vs deps (injectable providers), per-call commitment overrides, and subpath imports for every operation family. ## Design Philosophy The Umbra SDK is written in the same **functional, dependency-injected style** as [Anza's `@solana/kit`](https://github.com/anza-xyz/solana-web3.js). Every capability is either a pure function or a factory that returns one. There are no classes with internal state, no singletons, and no implicit globals. Dependencies are explicit, injectable, and testable. This mirrors the architecture of `@solana/kit` itself — functions like `getTransferSolInstruction` and `createSolanaRpc` follow the same closure-based factory pattern you will find throughout the Umbra SDK. ## The Factory Pattern Every SDK operation follows the same two-step pattern: ```typescript theme={null} import { getATAIntoETADirectDepositorFunction } from "@umbra-privacy/sdk/deposit"; // 1. Build the function once, at setup time. const deposit = getATAIntoETADirectDepositorFunction({ client }); // 2. Call it at runtime. const result = await deposit(destinationAddress, mint, amount); ``` The factory call (`get*Function`) is cheap — it binds configuration and resolves defaults. The returned function is what does the actual async work: sending transactions, generating proofs, querying the indexer, polling the relayer. This is the same pattern `@solana/kit` uses for its instruction builders: ```typescript theme={null} // @solana/kit instruction builder const ix = getTransferSolInstruction({ source, destination, amount }); // Umbra SDK factory function — same pattern, describing the data flow end-to-end. const withdraw = getETAIntoATAWithdrawerFunction({ client }); ``` ## Naming Conventions (V18) Names are long, but they spell out the exact data flow. * **Deposit family** (`@umbra-privacy/sdk/deposit`) * `getATAIntoETADirectDepositorFunction` — ATA → ETA. * `getATAIntoSelfBurnableStealthPoolNoteCreatorFunction` — ATA → self-burnable Stealth Pool Note. * `getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction` — ATA → receiver-burnable Stealth Pool Note. * `getETAIntoSelfBurnableStealthPoolNoteCreatorFunction` — ETA → self-burnable Stealth Pool Note (MPC). * `getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction` — ETA → receiver-burnable Stealth Pool Note (MPC). * **Withdrawal family** (`@umbra-privacy/sdk/withdrawal`) * `getETAIntoATAWithdrawerFunction` — ETA → ATA. * **Burn family** (`@umbra-privacy/sdk/burn`) * `getBurnableStealthPoolNoteScannerFunction` — zero-arg scanner, cursor in `client.utxoDataStore`. * `getReceiverBurnableStealthPoolNoteIntoETABurnerFunction` — receiver-burnable → ETA. * `getSelfBurnableStealthPoolNoteIntoETABurnerFunction` — self-burnable → ETA. * `getSelfBurnableStealthPoolNoteIntoATABurnerFunction` — self-burnable → ATA. * (Receiver-burnable → ATA exists on-chain but is not yet shipped in the SDK.) * **Registration** (`@umbra-privacy/sdk/registration`) — `getUserRegistrationFunction`. * **Query** (`@umbra-privacy/sdk/query`) — `getUserAccountQuerierFunction`, `getEncryptedBalanceQuerierFunction`. * **Conversion** (`@umbra-privacy/sdk/conversion`) — `getNetworkEncryptionToSharedEncryptionConverterFunction`. * **Compliance** (`@umbra-privacy/sdk/compliance`) — grant issuer / revoker / re-encryptors / queriers. * **Account** (`@umbra-privacy/sdk/account`) — staged-token recoverers (`getStagedSplRecovererFunction`, `getStagedSolRecovererFunction`), key rotators, maintenance helpers. You read each name as `[verb] from [source] (into|to) [destination]` or `[verb] [object]`. Factory names use the abbreviations `ATA` (`AssociatedTokenAccount`) and `ETA` (`EncryptedTokenAccount`) — e.g. `getATAIntoETADirectDepositorFunction` — so they stay short while remaining searchable and self-describing in code. ## V18 vocabulary * **Stealth Pool Note** replaces V13's "UTXO" in factory names and types. The wire protocol and on-chain instructions retain `claim_*` / `utxo_*` spellings for compatibility. * **Burn** replaces V13's "claim" in factory names. The relayer endpoint is still `/v1/claims`, and the burner factory's `relayer` dep takes `{ submitBurn, pollBurnStatus, getRelayerAddress }` — `submitBurn` and `pollBurnStatus` are TypeScript aliases of `submitClaim` and `pollClaimStatus`, so the relayer client's existing methods plug in unchanged under the renamed property names. ## args and deps Every factory accepts two arguments: ```typescript theme={null} const fn = getOperationFunction(args, deps?); ``` * **`args`** — required. Always contains `client: IUmbraClient` and any fixed configuration for this operation (e.g. a specific mint). * **`deps`** — optional. Contains every infrastructure provider, key generator, ZK prover, and relayer adapter used internally. All `deps` fields have sensible defaults derived from the `client`. The only exceptions are **ZK provers and the burner factory `relayer` adapter**, which have no built-in defaults and must always be supplied explicitly. ```typescript theme={null} // Using all defaults. const deposit = getATAIntoETADirectDepositorFunction({ client }); // Overriding the transaction forwarder (e.g. Jito bundles). const deposit = getATAIntoETADirectDepositorFunction( { client }, { transactionForwarder: jitoForwarder }, ); ``` Function-level `deps` override client-level `deps`. Client-level `deps` override built-in defaults. The **Advanced** section covers dependency injection internals, key generators, ZK provers, transaction callbacks, and key rotation in depth. Most applications do not need those details to get started. # Types & Interfaces Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/types SDK type system (V18): branded types U64/U128/U256, Bn254FieldElement, X25519PublicKey. Integration with @solana/kit Address and TransactionSignature. Function types via the named subpath of each operation family. ## Anza Solana Kit Types The SDK is built on top of `@solana/kit` and shares its core types directly. You do not need to import `@solana/kit` yourself — these types flow through the SDK's own types naturally — but it helps to know where they come from: * [`Address`](https://github.com/anza-xyz/solana-web3.js/blob/main/packages/addresses/src/address.ts) — a base58-encoded public key string. Every `mint`, `destinationAddress`, and user-address parameter in the SDK is an `Address`. * [`TransactionSignature`](https://github.com/anza-xyz/solana-web3.js/blob/main/packages/transaction-messages/src/transaction-message.ts) — a base58-encoded signature string. Returned by every operation that submits a transaction. * `SignableTransaction` / `SignedTransaction` — the versioned transaction types passed to `IUmbraSigner.signTransaction`. * `GetLatestBlockhash`, `GetEpochInfo` — RPC function types used in the `deps` of most factory functions. If you are writing unit tests or building a custom RPC layer, the [`@solana/kit` documentation](https://github.com/anza-xyz/solana-web3.js) is the primary reference for these types. ## Function Types Every returned function has a named type exported from the **same subpath** as its factory. There is no separate `@umbra-privacy/sdk/interfaces` re-export in V18 — types ship alongside their factories. ```typescript theme={null} import type { // Deposit family ATAIntoETADirectDepositorFunction, ATAIntoReceiverBurnableStealthPoolNoteCreatorFunction, } from "@umbra-privacy/sdk/deposit"; import type { // Withdrawal family ETAIntoATAWithdrawerFunction, } from "@umbra-privacy/sdk/withdrawal"; import type { // Burn family BurnableStealthPoolNoteScannerFunction, ReceiverBurnableStealthPoolNoteIntoETABurnerFunction, SelfBurnableStealthPoolNoteIntoETABurnerFunction, SelfBurnableStealthPoolNoteIntoATABurnerFunction, } from "@umbra-privacy/sdk/burn"; import type { UserRegistrationFunction } from "@umbra-privacy/sdk/registration"; import type { QueryUserAccountFunction, QueryEncryptedBalanceFunction, } from "@umbra-privacy/sdk/query"; ``` This is useful when you build a factory function once and store it in React state or a context: ```typescript theme={null} import { useState, useEffect } from "react"; import type { ATAIntoETADirectDepositorFunction } from "@umbra-privacy/sdk/deposit"; import { getATAIntoETADirectDepositorFunction } from "@umbra-privacy/sdk/deposit"; const [deposit, setDeposit] = useState(null); useEffect(() => { if (client) { setDeposit(() => getATAIntoETADirectDepositorFunction({ client })); } }, [client]); ``` The full inventory of factory + function types mirrors the operation families above. See the [Reference](/reference/overview) for the complete list. # ZK Prover Interfaces Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/zk-provers Six injectable Groth16 prover interfaces (one per Circom circuit). Each implements prove(inputs) returning {proofA, proofB, proofC}. Provers ship in @umbra-privacy/sdk/zk-prover. ## Overview Every operation that generates a [Groth16](/sdk/advanced/cryptography/groth16) ZK proof requires a prover passed through `deps`. **There is no built-in default** — you must always supply one. All provers follow the same `prove(inputs) → { proofA, proofB, proofC }` contract. In V18 the prover and the asset CDN live inside the main SDK at the `@umbra-privacy/sdk/zk-prover` subpath. The standalone `@umbra-privacy/web-zk-prover` package from V13 has been deleted. For instantiation, Web Worker setup, and remote proving backends, see [ZK Provers](/sdk/advanced/zk-provers). The **burner** prover factory and interface names retained their V13 spellings (`getClaim…`, `IZkProverForClaim…`). The **creator** prover factories were renamed — ETA-source: `getETAIntoStealthPoolNoteCreatorProver` / `IZkProverForETAIntoStealthPoolNote`; ATA-source: `getATAIntoStealthPoolNoteCreatorProver` / `IZkProverForATAIntoStealthPoolNote`. The high-level **factory functions** in `/deposit`, `/withdrawal`, `/burn` also use the new vocabulary. Both creator and burner prover names coexist but follow different patterns. *** ## `IZkProverForUserRegistration` ```typescript theme={null} interface IZkProverForUserRegistration { prove(inputs: UserRegistrationCircuitInputs): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` Required by: `getUserRegistrationFunction` deps (`zkProver`). *** ## `IZkProverForETAIntoStealthPoolNote` ```typescript theme={null} interface IZkProverForETAIntoStealthPoolNote { prove(inputs: ETAIntoStealthPoolNoteCircuitInputs): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` **Shared by both ETA-source creator variants** — `getETAIntoSelfBurnableStealthPoolNoteCreatorFunction` and `getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction` both take a `zkProver: IZkProverForETAIntoStealthPoolNote` dep. Use `getETAIntoStealthPoolNoteCreatorProver()` to get a default instance. For ATA-source creation, see `IZkProverForATAIntoStealthPoolNote` below. *** ## `IZkProverForClaimSelfClaimableUtxoIntoEncryptedBalance` ```typescript theme={null} interface IZkProverForClaimSelfClaimableUtxoIntoEncryptedBalance { readonly maxUtxoCapacity: 1; prove(inputs: ClaimSelfClaimableUtxoIntoEncryptedBalanceCircuitInputs): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` Required by: `getSelfBurnableStealthPoolNoteIntoETABurnerFunction` deps (`zkProver`). `maxUtxoCapacity: 1` — one note per proof, the SDK loops internally if you submit an array. *** ## `IZkProverForClaimReceiverClaimableUtxoIntoEncryptedBalance` ```typescript theme={null} interface IZkProverForClaimReceiverClaimableUtxoIntoEncryptedBalance { prove( inputs: ClaimReceiverClaimableUtxoIntoEncryptedBalanceCircuitInputs, nLeaves: ClaimBatchSize, ): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` Required by: `getReceiverBurnableStealthPoolNoteIntoETABurnerFunction` deps (`zkProver`). This prover accepts a batch size `nLeaves` (1–5 in V18 — the SDK chunks to 5 notes per proof). *** ## `IZkProverForClaimSelfClaimableUtxoIntoPublicBalance` ```typescript theme={null} interface IZkProverForClaimSelfClaimableUtxoIntoPublicBalance { readonly maxUtxoCapacity: 1; prove(inputs: ClaimSelfClaimableUtxoIntoPublicBalanceCircuitInputs): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` Required by: `getSelfBurnableStealthPoolNoteIntoATABurnerFunction` deps (`zkProver`). *** ## `IZkProverForATAIntoStealthPoolNote` ```typescript theme={null} interface IZkProverForATAIntoStealthPoolNote { prove(inputs: ATAIntoStealthPoolNoteCircuitInputs): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` **Shared by both ATA-source creator variants** — `getATAIntoSelfBurnableStealthPoolNoteCreatorFunction` and `getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction` both take a `zkProver: IZkProverForATAIntoStealthPoolNote` dep. Use `getATAIntoStealthPoolNoteCreatorProver()` to get a default instance. *** ## Quick Setup Per-circuit factories are exported from `@umbra-privacy/sdk/zk-prover`. They wrap [snarkjs](https://github.com/iden3/snarkjs) Groth16 and fetch proving keys from Umbra's CDN by default. ```typescript theme={null} import { getUserRegistrationProver, getETAIntoStealthPoolNoteCreatorProver, getATAIntoStealthPoolNoteCreatorProver, getClaimSelfClaimableUtxoIntoEncryptedBalanceProver, getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver, getClaimSelfClaimableUtxoIntoPublicBalanceProver, } from "@umbra-privacy/sdk/zk-prover"; const registrationProver = getUserRegistrationProver(); // Stealth Pool Note creators — one prover instance is shared across self/receiver variants of the same source const createFromEtaProver = getETAIntoStealthPoolNoteCreatorProver(); // both ETA self-burnable + receiver-burnable const createFromPataProver = getATAIntoStealthPoolNoteCreatorProver(); // both ATA self-burnable + receiver-burnable // Burners const burnSelfIntoEtaProver = getClaimSelfClaimableUtxoIntoEncryptedBalanceProver(); const burnReceiverIntoEtaProver = getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(); const burnSelfIntoPataProver = getClaimSelfClaimableUtxoIntoPublicBalanceProver(); ``` Each factory accepts an optional `IZkAssetProvider` argument — a simple interface with one method, `getAssetUrls(type, variant?)`. When no provider is passed, the factories default to the CDN provider (equivalent to calling `getCdnZkAssetProvider()` from `@umbra-privacy/sdk/zk-prover/cdn`), which fetches proving keys from Umbra's CDN. You can supply your own provider to customise asset resolution. See [ZK Provers](/sdk/advanced/zk-provers) for Web Worker setup, custom asset providers, and remote proving backends. # Wallet Adapters Source: https://sdk.umbraprivacy.com/sdk/wallet-adapters IUmbraSigner interface and adapters: createInMemorySigner (keypair), createSignerFromWalletAccount (Wallet Standard). Covers UMBRA_MESSAGE_TO_SIGN consent and signMessage requirements. ## The IUmbraSigner Interface The Umbra SDK requires a signer that implements `IUmbraSigner`: ```typescript theme={null} interface IUmbraSigner { readonly address: Address; signTransaction(tx: SignableTransaction): Promise; signTransactions(txs: readonly SignableTransaction[]): Promise; signMessage(message: Uint8Array): Promise; } ``` `signMessage` is critical - it is used to derive the master seed from a wallet signature. The other methods sign the Solana transactions that interact with the Umbra program. The SDK exports ready-made helper functions for every common signer source. You do not need to implement `IUmbraSigner` yourself. ## Option 1: In-Memory Keypair (Testing) Use the SDK's built-in helpers to generate or load an in-memory keypair. Best for scripts, CI, and local development. ```typescript theme={null} import { createInMemorySigner, createSignerFromPrivateKeyBytes, createSignerFromKeyPair, } from "@umbra-privacy/sdk"; // Generate a new random keypair const signer = await createInMemorySigner(); console.log("Address:", signer.address); // Load from raw bytes (64-byte keypair or 32-byte seed - @solana/kit accepts both) import { readFileSync } from "fs"; const keyFile = JSON.parse(readFileSync("/path/to/keypair.json", "utf8")); const signer = await createSignerFromPrivateKeyBytes(new Uint8Array(keyFile)); // Adapt an existing @solana/kit KeyPairSigner you already hold import { generateKeyPairSigner } from "@solana/kit"; const kps = await generateKeyPairSigner(); const signer = createSignerFromKeyPair(kps); ``` In-memory keypairs are ephemeral. If your process restarts, the keypair is gone. Only use this for testing or scripts where you control the key lifecycle. ## Option 2: Wallet Standard Browser Wallet (Production) Modern Solana wallets — Phantom, Backpack, Solflare, and others — implement the [Wallet Standard](https://github.com/wallet-standard/wallet-standard). Use `createSignerFromWalletAccount` to adapt them to `IUmbraSigner`. You obtain the `wallet` and `account` objects by discovering registered wallets with `getWallets()` from `@wallet-standard/app`, then calling the wallet's `standard:connect` feature. ```bash theme={null} pnpm add @wallet-standard/app @wallet-standard/base @wallet-standard/features ``` ```typescript theme={null} import { getWallets } from "@wallet-standard/app"; import type { Wallet, WalletAccount } from "@wallet-standard/base"; import { StandardConnect, StandardDisconnect } from "@wallet-standard/features"; import { createSignerFromWalletAccount, getUmbraClient, } from "@umbra-privacy/sdk"; // 1. Discover wallets that support Solana signing const { get } = getWallets(); const solanaWallets = get().filter((w) => { const features = Object.keys(w.features); return ( features.includes("solana:signTransaction") && features.includes("solana:signMessage") ); }); // 2. Connect to a wallet (prompts the user) const wallet = solanaWallets[0]; // e.g. Phantom const connectFeature = wallet.features[StandardConnect]; const { accounts } = await connectFeature.connect(); const account = accounts[0]; // 3. Create the Umbra signer const signer = createSignerFromWalletAccount(wallet, account); // 4. Create the Umbra client const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl: "https://api.mainnet-beta.solana.com", rpcSubscriptionsUrl: "wss://api.mainnet-beta.solana.com", indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com", deferMasterSeedSignature: true, // prompt fires on first operation, not at connect time }); ``` For React applications, you can also use the `useWallets()` hook from `@wallet-standard/react` and the `useConnect()` / `useDisconnect()` hooks for a more ergonomic integration: ```typescript theme={null} import { useWallets, useConnect } from "@wallet-standard/react"; import { createSignerFromWalletAccount } from "@umbra-privacy/sdk"; ``` Re-create the client whenever the wallet connection changes. Since the client holds a reference to the signer, stale signers will cause transactions to fail. The wallet must support both `"solana:signTransaction"` and `"solana:signMessage"` features — an error is thrown immediately if either is missing. ### How signing works 1. The `@solana/kit` transaction is serialized to wire-format bytes via `getTransactionEncoder()` 2. The bytes are passed directly to the wallet's `solana:signTransaction` feature 3. The wallet returns signed wire bytes 4. `getTransactionDecoder()` reconstructs the `@solana/kit` `Transaction`, and signatures are merged back in This path is compatible with all Wallet Standard wallets and does not require `@solana/web3.js`. ## Master Seed Derivation The first time any cryptographic operation runs (typically during `register()`), the SDK calls `signer.signMessage` with a deterministic message to derive the **master seed** (overridable via [dependency injection](/sdk/understanding-the-sdk/dependency-injection#master-seed-storage)). This produces a one-time wallet signing prompt. The message that the user signs is a multi-paragraph legal consent and acknowledgement. It is exported as `UMBRA_MESSAGE_TO_SIGN` from the `@umbra-privacy/sdk/shared` subpath: ```typescript theme={null} import { UMBRA_MESSAGE_TO_SIGN } from "@umbra-privacy/sdk/shared"; ``` The signature is then hashed with KMAC256 (dkLen=64) to produce the 512-bit master seed. The master seed is derived deterministically from this signature. If the message changed, a completely different master seed would be produced - all previously registered keys would become inaccessible. Never modify the derivation message. After derivation, the seed is cached in memory for the lifetime of the client object. The prompt does not appear again unless the client is recreated. To control exactly when the prompt fires, use the `deferMasterSeedSignature` option in `getUmbraClient`. See [Creating a Client - Master Seed Derivation](/sdk/creating-a-client#master-seed-derivation). ## Persisting the Master Seed By default, the master seed lives only in memory. If you want to avoid re-deriving it on every page load, you can persist it using the `masterSeedStorage` dependency override: ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; const client = await getUmbraClient( { signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl }, { masterSeedStorage: { load: async () => { const stored = sessionStorage.getItem("umbra:masterSeed"); if (!stored) return { exists: false }; return { exists: true, seed: new Uint8Array(JSON.parse(stored)) }; }, store: async (seed) => { sessionStorage.setItem("umbra:masterSeed", JSON.stringify(Array.from(seed))); }, }, } ); ``` The master seed is a 64-byte root secret equivalent in sensitivity to a private key. Only persist it in secure storage - never in `localStorage` in plaintext. `sessionStorage` is slightly better (cleared on tab close) but still not suitable for long-term storage. For production applications, consider deriving it on every session or using a secure enclave. # Withdraw Source: https://sdk.umbraprivacy.com/sdk/withdraw getETAIntoATAWithdrawerFunction: move tokens from an EncryptedTokenAccount to an AssociatedTokenAccount. Returns WithdrawResult with Arcium MPC callback confirmation. ## Overview The `withdraw` operation is the reverse of a deposit. It moves tokens from a user's `EncryptedTokenAccount (ETA)` back to their `AssociatedTokenAccount (ATA)`. ``` ETA (on-chain encrypted balance) │ ▼ Pool custody (on-chain SPL) ──withdraw──▶ Your ATA ``` Arcium MPC verifies the withdrawal authorization. The operation follows the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern) — the SDK waits for both the handler and the callback transactions to confirm before returning. ## Usage ```typescript theme={null} import { getETAIntoATAWithdrawerFunction } from "@umbra-privacy/sdk/withdrawal"; const withdraw = getETAIntoATAWithdrawerFunction({ client }); const result = await withdraw( destinationAddress, // where tokens go (typically client.signer.address) mint, // SPL or Token-2022 mint address amount, // amount in native token units (U64) options?, // optional ); ``` ## Parameters The Solana address whose ATA will receive the withdrawn tokens. This is typically `client.signer.address`. The SPL or Token-2022 mint address. Must match a mint for which the user has an existing ETA. The amount to withdraw, in the token's native units. Must not exceed the current ETA balance. 32 bytes of caller metadata stored with the withdrawal. Defaults to 32 zero bytes. **Must be a pre-hashed or pre-encrypted 32-byte value** — never store plaintext identifiers. Commitment level used for RPC account reads during withdrawal preparation. Optional Arcium computation-account PDA offset. Defaults to a fresh random `U64` per call (CSPRNG). Optional MpcCallbackData PDA offset (paired with the signer in seeds). Defaults to a fresh random `U128` per call. Fee-vault PDA offset. The fee-vault path is currently unused for public-ATA withdrawals — leave at the default `0n` (sentinel for "no fee-vault interaction"). Optional observer-CPI destination program invoked after the MPC callback. Defaults to the System Program (sentinel = "no observer CPI"). Per-phase lifecycle hooks (`onValidationStart`, `onAccountFetchComplete`, `queueComputation`, `onComplete`, `onError`, etc.). There are no `priorityFees`, `awaitCallback`, `skipPreflight`, `maxRetries`, or `epochInfoCommitment` options on the withdrawer. Priority fees are configured at factory time. Callback waiting is always-on — the result's `callback` field is populated on success. ## Return Value Returns a `Promise`. The shape mirrors `DepositResult`: ```typescript theme={null} interface WithdrawResult { readonly signatures: readonly TransactionSignature[]; readonly queueSignature: TransactionSignature; readonly callback?: CallbackOutcome; readonly rentClaim?: RentClaimOutcome; } type CallbackOutcome = | { readonly status: "finalized"; readonly signature?: TransactionSignature; readonly elapsedMs: number } | { readonly status: "pruned"; readonly elapsedMs: number } | { readonly status: "timed-out"; readonly elapsedMs: number }; type RentClaimOutcome = | { readonly claimed: true; readonly signature: TransactionSignature } | { readonly claimed: false; readonly reason: string }; ``` * `signatures` — every transaction signature submitted by this call, in submission order. * `queueSignature` — the handler (queue computation) transaction signature. * `callback` — present once the callback round-trip completes (always awaited). `"finalized"` means the MPC callback landed (and exposes `signature`); `"pruned"` / `"timed-out"` indicate the callback did not land — recover via the staged recoverer (below). * `rentClaim` — best-effort rent reclamation outcome. The withdrawal itself still succeeded even when `claimed: false`. ## Example ```typescript theme={null} import { getETAIntoATAWithdrawerFunction } from "@umbra-privacy/sdk/withdrawal"; const withdraw = getETAIntoATAWithdrawerFunction({ client }); const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const result = await withdraw(client.signer.address, USDC, 50_000_000n); // 50 USDC console.log("Queue signature:", result.queueSignature); if (result.callback?.status === "finalized") { console.log("Callback signature:", result.callback.signature); } ``` ## Withdrawal destination The tokens are sent to the **ATA** associated with the `destinationAddress` for the given mint. If that ATA does not exist, the transaction will fail with an account-not-found error. Create the ATA first using standard token tooling if needed. ## Callback failure recovery If the handler succeeds but the Arcium callback never lands (network partition, compute budget, Arcium outage), the tokens stay **staged** in the pool ATA. Reclaim them with the recoverer factories from `@umbra-privacy/sdk/account`: ```typescript theme={null} import { getStagedSplRecovererFunction, getStagedSolRecovererFunction } from "@umbra-privacy/sdk/account"; const recover = getStagedSplRecovererFunction({ client }); await recover({ mint }); ``` No MPC, no ZK proof — a synchronous recovery transaction. Use `getStagedSolRecovererFunction` for wSOL. ## Error Handling Withdrawal errors are typed with a `stage` field identifying exactly where in the pipeline the failure occurred: ```typescript theme={null} import { isEncryptedWithdrawalError } from "@umbra-privacy/sdk/errors"; try { const result = await withdraw(client.signer.address, mint, amount); } catch (err) { if (isEncryptedWithdrawalError(err)) { switch (err.stage) { case "validation": // Zero amount, or no ETA exists for this mint. console.error("Withdrawal validation failed:", err.message); break; case "mint-fetch": // Could not fetch the mint account — check RPC connectivity and mint address. break; case "pda-derivation": // Could not derive required program addresses — unexpected on-chain state. break; case "instruction-build": // Could not construct the instruction — protocol state mismatch. break; case "transaction-sign": // User rejected the transaction in their wallet. showNotification("Withdrawal cancelled."); break; case "transaction-send": // Transaction submitted but confirmation timed out. // The transaction may have landed — check on-chain before retrying. console.warn("Confirmation timeout. Check signature on-chain:", err.cause); break; default: console.error("Withdrawal failed at stage:", err.stage, err); } } else { throw err; } } ``` See [Error Handling](/reference/errors) for retry guidance and a full stage reference. ## Timing Withdrawals follow the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern). The SDK submits the handler transaction, waits for Arcium to complete the MPC computation, then waits for the callback transaction to confirm. Total wall-clock time is typically a few seconds on a well-connected network. # Supported Tokens Source: https://sdk.umbraprivacy.com/supported-tokens V18 mint list: wSOL, USDC, USDT, UMBRA, CASH, ZINC on mainnet; wSOL only on devnet. Each pool supports both EncryptedTokenAccount operations and the Stealth Pool mixer, including Token-2022 transfer fees. ## Overview Each Umbra pool is deployed per token mint. A pool supports both the **EncryptedTokenAccount** flow (deposit / withdraw / convert) and the **Stealth Pool** flow (Stealth Pool Note create + burn). Token-2022 mints with a transfer fee extension are fully supported — the SDK accounts for the transfer fee before applying protocol fees. See [Token-2022 Support](/advanced/token-2022). The list below is a snapshot. **The authoritative source at runtime is the relayer's `getSupportedMints()` endpoint** — it returns only mints with deployed token-pool and fee-schedule PDAs, so it doubles as a "pool deployed" check. ```typescript theme={null} import { getUmbraRelayer } from "@umbra-privacy/sdk"; const relayer = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com" }); const live = await relayer.getSupportedMints(); ``` *** ## Mainnet * **wSOL** (Wrapped SOL) * Mint: `So11111111111111111111111111111111111111112` * Decimals: 9 * Token program: SPL * EncryptedTokenAccount: enabled * Stealth Pool: enabled * **USDC** * Mint: `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` * Decimals: 6 * Token program: SPL * EncryptedTokenAccount: enabled * Stealth Pool: enabled * **USDT** * Mint: `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` * Decimals: 6 * Token program: SPL * EncryptedTokenAccount: enabled * Stealth Pool: enabled * **UMBRA** * Mint: `PRVT6TB7uss3FrUd2D9xs2zqDBsa3GbMJMwCQsgmeta` * Decimals: 6 * Token program: SPL * EncryptedTokenAccount: enabled * Stealth Pool: enabled * **CASH** * Mint: `CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH` * Decimals: 6 * Token program: SPL * EncryptedTokenAccount: enabled * Stealth Pool: enabled * **ZINC** * Mint: `zinc155BS4mSPk8GXQj4R5hkVDQXcW253pTYq5SGyfi` * Decimals: 9 * Token program: SPL * EncryptedTokenAccount: enabled * Stealth Pool: enabled *** ## Devnet * **wSOL** (Wrapped SOL) * Mint: `So11111111111111111111111111111111111111112` * Decimals: 9 * Token program: SPL * EncryptedTokenAccount: enabled * Stealth Pool: enabled * **dUSDC** (Dummy USDC — mock of the USD Coin standard) * Mint: `4oG4sjmopf5MzvTHLE8rpVJ2uyczxfsw2K84SUTpNDx7` * Decimals: 6 * Token program: SPL * EncryptedTokenAccount: enabled * Stealth Pool: enabled * **dUSDT** (Dummy USDT — mock of Tether USD) * Mint: `DXQwBNGgyQ2BzGWxEriJPVmXYFQBsQbXvfvfSNTaJkL6` * Decimals: 6 * Token program: SPL * EncryptedTokenAccount: enabled * Stealth Pool: enabled Mints outside this list will fail at the account-fetch stage with `error 3012` (pool missing). ### Devnet Faucet Get test tokens at **[faucet.umbraprivacy.com](https://faucet.umbraprivacy.com/)**. The faucet distributes 1,000 tokens per wallet every 60 minutes for each of dUSDC and dUSDT — no signup, no KYC, just paste a Solana devnet wallet address. For devnet wSOL, use any standard Solana airdrop faucet (e.g. `solana airdrop`) and wrap it with the SDK. *** ## Localnet Localnet uses the same configuration as devnet — wSOL only — and runs against a local Solana validator + a locally-spun-up indexer/relayer stack. *** ## Using a Supported Mint Pass the mint address directly to any SDK function that accepts a `mint` parameter: ```typescript theme={null} import { getATAIntoETADirectDepositorFunction } from "@umbra-privacy/sdk/deposit"; import { getETAIntoATAWithdrawerFunction } from "@umbra-privacy/sdk/withdrawal"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // Deposit: ATA → ETA const deposit = getATAIntoETADirectDepositorFunction({ client }); await deposit(client.signer.address, USDC, 1_000_000n); // Withdraw: ETA → ATA const withdraw = getETAIntoATAWithdrawerFunction({ client }); await withdraw(client.signer.address, USDC, 1_000_000n); ``` The SDK fetches the pool configuration on-chain and selects the correct instruction variant automatically. Always verify the mint is supported before building a transaction. Use `relayer.getSupportedMints()` at app boot and cache the result for the session. Calling a deposit / withdraw / create with an unsupported mint fails with `error 3012` after the account-fetch stage — surface a clear user-facing error before you get there.