# 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