# Encrypted Balances Source: https://sdk.umbraprivacy.com/concepts/encrypted-balances Encrypted Token Accounts (ETAs): MXE-only mode (Arcium network decryptable) vs Shared mode (user X25519 decryptable). Covers nonces, ciphertext structure, and encryption upgrades. ## Public vs. Encrypted Token Accounts Solana uses **Associated Token Accounts (ATAs)** to hold SPL and Token-2022 tokens. These are fully public - anyone can query your balance. Umbra introduces **Encrypted Token Accounts (ETAs)** - on-chain 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 token **ETA (Umbra encrypted account)** * Balance hidden on-chain * Use Umbra deposit/withdraw instead of standard transfers * One-time registration required * Works with any SPL token ## Depositing: ATA → ETA When you call `deposit`, your tokens move from your ATA into the shielded pool. The program records an encrypted version of your balance in an ETA tied to your `(wallet address, mint)` pair. ``` Your ATA ──deposit──► Shielded Pool (on-chain SPL) │ ▼ Your ETA (encrypted balance stored here) ``` The shielded pool holds the real tokens. The ETA holds the cryptographic proof of how much of those tokens belong to you. ## Withdrawing: ETA → ATA When you withdraw, the process reverses. Arcium MPC verifies that your encrypted balance is sufficient and authorizes the transfer from the shielded pool 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 X25519 key registration (part of the standard registration flow) If you register with `confidential: true` (the default), your deposits will automatically use Shared mode. This is strongly recommended - it lets you call `queryEncryptedBalance` to read your balance without waiting for 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} // 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 encrypted token account has a **nonce** - a monotonically increasing counter used to prevent replay attacks. The nonce is derived from the account's `generationIndex` field combined with entropy stored on-chain. 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"; 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, cannot decrypt balance client-side"); break; case "uninitialized": console.log("Account exists but balance not initialized"); break; case "non_existent": console.log("No encrypted token account for this mint"); break; } ``` Only Shared-mode accounts can be decrypted client-side. MXE-mode accounts return `{ state: "mxe" }` without a balance. Convert to Shared mode to enable local balance queries. ## Protocol Fees Deposits 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 deposit amount Fees are deducted automatically. The amount credited to your ETA is the deposit amount minus fees. For Token-2022 mints with a transfer fee extension, the transfer fee is also subtracted before protocol fees are applied - see [Token-2022 Support](/advanced/token-2022). # How Umbra Works Source: https://sdk.umbraprivacy.com/concepts/how-umbra-works How Umbra works: encrypted balances via Arcium MPC dual-instruction pattern (queue_computation + arcium_callback), UTXO mixer with 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 deposited into a shared pool and withdrawn separately, with no on-chain link between the deposit and withdrawal addresses. *Who transferred to whom* is hidden. You can use encrypted balances alone (hide amounts), the mixer alone (hide the transfer path), or both together for the strongest privacy guarantees. ## How Encrypted Balances Work When you deposit tokens into an encrypted balance: 1. Your tokens move from your public SPL or Token-2022 account to the **shielded pool** - still on-chain, still real tokens, locked in the program 2. The program creates an **Encrypted Token Account (ETA)** for your `(wallet, mint)` pair 3. Your 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 Mixer Works The mixer 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 deposit 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. The deposit address and the claim address need not be related. An observer sees a deposit and a withdrawal happening, but cannot link them. ## The Dual-Instruction Pattern Every confidential 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 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. ## Privacy Guarantees Summary * **Token balance amount** - hidden by Encrypted Balances. Encrypted with MPC; auditors can be granted selective access. * **Transfer counterparty** - hidden by the Mixer. Requires a sufficient anonymity set (other users in the same tree). * **Transaction history** - strongest guarantees when mixer + encrypted balances are used together. ## What is NOT Hidden * **That you are using Umbra** - on-chain interactions with the Umbra program are visible * **Deposit and withdrawal amounts** when using the mixer - the amount is committed at deposit and revealed at claim * **Timing** - if you deposit and immediately withdraw, 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. # UTXOs and the Mixer Source: https://sdk.umbraprivacy.com/concepts/utxos-and-mixer UTXO mixer internals: Indexed Merkle Tree for commitments, nullifier treap for double-spend prevention, X25519 ciphertext discovery, and Groth16 ZK proof verification on claim. ## What is a UTXO? A **[UTXO](https://bitcoin.org/bitcoin.pdf) (Unspent Transaction Output)** in Umbra is a cryptographic commitment to a token deposit. It represents the right to claim a specific amount of tokens - without publicly linking that right to who created it. A UTXO encodes: * **Amount** - how many tokens are locked * **Recipient address** - who is authorized to claim * **Secret randomness** - private entropy known only to the depositor 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 Mixer: How It Works The mixer 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 UTXO commitment. You call one of the `createUtxo` functions. The SDK computes a commitment from `(amount, recipient, randomness)` and inserts it as a new leaf into the tree. Your tokens are locked in the shielded pool. At this point, anyone can see that *a deposit happened* and *the tree grew by one leaf* - but cannot see the amount, recipient, or any other detail. As more users deposit into the same tree, your commitment becomes one of many. The larger the set, the harder it is to link your deposit to your eventual withdrawal. Trees hold up to **1,048,576 leaves** (depth-20 tree). The SDK queries the indexer to find UTXO ciphertexts addressed to your [X25519](https://www.rfc-editor.org/rfc/rfc7748) key, decrypts them locally, and fetches the Merkle inclusion proof for each one. See [Fetching UTXOs](/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 claimed 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 your wallet. ## UTXO Types Umbra supports four kinds of UTXOs depending on who can claim them and where the funds come from: * **Self-claimable (ephemeral)** - funded from your encrypted balance or public ATA. Claimable only by you (same wallet). * **Receiver-claimable** - funded from another user's ATA. Claimable by a specified recipient address. The "self-claimable" pattern is useful when you want to move funds through the mixer yourself. The "receiver-claimable" pattern lets you *send* tokens anonymously - you deposit for a recipient, and they claim without you having direct access. ## Nullifiers: Preventing Double-Spends Each UTXO has a corresponding **[nullifier](https://eprint.iacr.org/2014/349.pdf)** - a deterministic hash derived from its private inputs. When a UTXO is claimed, 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 claim, 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 UTXO from being claimed twice, even if the claim transaction is replayed. ## Ciphertext Discovery After you create a UTXO, the SDK publishes an **encrypted ciphertext** on-chain. This ciphertext is addressed to the recipient's X25519 public key - only the recipient can decrypt it to learn the commitment's secret inputs (amount, randomness, etc.). The ciphertext payload contains: * Amount (8 bytes) * Recipient address (32 bytes) * Generation index (16 bytes) * Domain separator identifying the UTXO type (12 bytes) The Umbra indexer stores all ciphertexts and serves them for efficient querying. Your X25519 private key is used locally to try decrypting each one - successful decryptions are your claimable UTXOs. Your private key never leaves your device. Decryption happens in the SDK using your locally derived X25519 key. ## Anonymity Set Size The privacy guarantee of the mixer depends on how many other UTXOs exist in the same tree at the time you claim. A tree with only one leaf offers no privacy - it's obvious which commitment is being claimed. In practice: * Wait for more users to deposit before claiming * Claiming into a different address from your deposit address increases privacy * Combining mixer withdrawals with encrypted balances hides the final destination further ## Trees Fill Up Each Merkle tree has a maximum of 1,048,576 leaves. When a tree is full, the write service starts a new tree at the next sequential index. UTXOs from different trees have separate anonymity sets. You specify the tree index when [fetching](/sdk/mixer/fetching-utxos) and [claiming](/sdk/mixer/claiming-utxos) UTXOs. # 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 critical for claim operations that batch multiple UTXOs into a single ZK proof. ## 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 UTXO commitments. The SDK's claim functions use this endpoint internally when claiming multiple UTXOs in a single batch. # 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). Always fetch a fresh proof immediately before submitting a claim. Never cache proofs across user sessions. ## 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 calls this endpoint automatically via `getClaimableUtxoScannerFunction`. The proof is passed directly to the claim functions -- you do not need to call this endpoint manually when using the SDK. If you are building a custom claim flow or integrating with the protocol directly, use this endpoint to fetch fresh proofs immediately before submitting a claim transaction. ## 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 UTXO 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 UTXO 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 UTXOs or generating Merkle proofs. `utxo_count` may differ from `num_leaves` if some leaves were inserted without UTXO ciphertext data (e.g. padding leaves). # Tree UTXOs Source: https://sdk.umbraprivacy.com/indexer/api-reference/tree-utxos GET /v1/trees/{tree_index}/utxos Returns a paginated list of UTXO 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 UTXOs](/indexer/api-reference/utxos). ## Response Layouts The `X-Response-Layout` header controls how records are packed in the protobuf response: * **Row-oriented** (default, omit header): Each UTXO is a self-contained `UtxoDataItem`. Easy to iterate record by record. * **Columnar** (set header to `"columnar"`): All fields across UTXOs 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 Initialize with `cursor = tree_index x 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 UTXO Source: https://sdk.umbraprivacy.com/indexer/api-reference/utxo-single GET /v1/utxos/{absolute_index} Returns the single UTXO record at the specified global absolute index. The absolute index uniquely identifies a UTXO across all trees: ``` absolute_index = tree_index x 1,048,576 + insertion_index ``` For example, the 5th leaf of tree 2 has absolute index `2 x 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. # Global UTXOs Source: https://sdk.umbraprivacy.com/indexer/api-reference/utxos GET /v1/utxos Returns a paginated list of UTXO records across all Merkle trees, ordered by absolute index ascending. Use this endpoint when you need to scan UTXOs globally without knowing which tree they belong to. For tree-specific queries, prefer [Tree UTXOs](/indexer/api-reference/tree-utxos). ## 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 UTXOs 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 UTXO indexer REST API at utxo-indexer.api.umbraprivacy.com. Protobuf responses, paginated UTXOs, Merkle proofs, batch proofs. Used by getClaimableUtxoScannerFunction. ## What is the Indexer? The Umbra indexer is an off-chain service that continuously watches the Solana chain for Umbra program transactions, extracts UTXO 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 mixer. It exposes two primary capabilities: * **UTXO data** - fetch encrypted UTXO ciphertexts by tree or absolute index. The SDK uses this to scan for UTXOs addressed to your [X25519](https://www.rfc-editor.org/rfc/rfc7748) key by trying to decrypt each ciphertext. * **Merkle proofs** - generate a Merkle inclusion proof (authentication path) for any leaf. This is required to construct a valid claim transaction on-chain. Beyond the SDK, the indexer is useful for analytics, monitoring, and custom integrations such as building your own wallet or scanning pipeline. ## 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 UTXO commitments are organized 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 x 1_048_576 + insertion_index ``` ## SDK Integration When you pass `indexerApiEndpoint` to `getUmbraClient`, the SDK automatically constructs two internal providers: * `client.fetchUtxoData` - calls `GET /v1/utxos` to retrieve ciphertext batches for decryption * `client.fetchMerkleProof` - calls `GET /v1/trees/{tree_index}/proof/{insertion_index}` for each claimable UTXO Both are consumed internally by `getClaimableUtxoScannerFunction`. 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 anonymizing relay, so that fetching your UTXOs 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 UTXO index. Basic, detailed, liveness, and readiness health checks. Current root hash, leaf count, and UTXO count for a specific tree. Paginated UTXO records for a specific tree. Generate an inclusion proof for a specific leaf. Retrieve multiple proofs atomically under the same tree root. Paginated UTXO queries spanning all trees. Fetch a single UTXO by absolute index. # Introduction Source: https://sdk.umbraprivacy.com/introduction Umbra privacy protocol for Solana: shield SPL/Token-2022 balances via Arcium MPC encrypted accounts, transfer anonymously through a UTXO mixer, 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 SPL or Token-2022 account into an encrypted account where the balance is hidden from everyone except authorized viewers * **Transfer anonymously** - move tokens in and out of a shared mixer pool with no on-chain linkage revealed between the entry and exit points * **Control who sees what** - grant selective viewing access to auditors or compliance systems without exposing your full history Everything runs on Solana mainnet 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. ## Core Concepts at a Glance Shield any SPL or Token-2022 token balance in a confidential account. Only you - and anyone you explicitly grant access to - can see the amount. Break the on-chain link between a deposit and a withdrawal using a shared Merkle tree and zero-knowledge proofs. A TypeScript SDK that wraps all protocol operations into simple async functions. Works in Node.js and browser environments. A read-only REST API for querying UTXO 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 UTXOs and generates Merkle proofs ``` ## 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 * `devnet` - Development and integration testing * `localnet` - Local validator for unit tests ## Next Steps Install the SDK and run your first deposit in under 5 minutes. Understand the privacy model before you start building. # Pricing Source: https://sdk.umbraprivacy.com/pricing Fee structure: protocol fees (BPS_DIVISOR=16384), relayer fees on claims, mixer SOL fee for UTXO creation. 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 claim operations. Currently 0. * **Mixer SOL fee** — a fixed SOL amount paid at UTXO creation time to pre-fund the eventual claim. *** ## 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 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"; 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 claim 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 claimed amount. **Current rates:** both are set to 0. Claiming a UTXO 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 ``` *** ## Mixer SOL Fee When you create a UTXO and insert it into the mixer, you pay a one-time SOL fee upfront. This pre-funds the eventual claim so neither you nor the relayer pays SOL out of pocket at claim 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. This is approximately the minimum rent for a 48-byte account at current Solana rent rates. * **Costliest claim path** — the fee includes enough SOL to cover the on-chain compute and transaction costs of the most expensive possible claim route out of the mixer. The fee is dynamically calculated at UTXO creation 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 mixer 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 **Encrypted balance operations:** * Self-deposit (public ATA → encrypted balance): 0 protocol fee for standard self-shielding * Withdrawal (encrypted balance → public ATA): protocol fee applies * Cross-account confidential transfer: protocol fee applies **Mixer operations:** * UTXO creation (encrypted balance → mixer): protocol fee on SPL amount + mixer SOL fee * UTXO creation (public ATA → mixer): protocol fee on SPL amount + mixer SOL fee * Claiming a UTXO (mixer → encrypted balance): protocol fee applies, relayer fee applies (currently 0) * Claiming a UTXO (mixer → public ATA): protocol fee applies, relayer fee applies (currently 0) *** ## 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) Encrypted balance gets: 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 End-to-end guide: install @umbra-privacy/sdk, call getUmbraClient, register, deposit into encrypted balance, create a UTXO, scan with getClaimableUtxoScannerFunction, and claim. ## Prerequisites * Node.js 18+ or a modern browser environment * A Solana wallet (or a generated keypair for testing) * An RPC endpoint - any standard Solana JSON-RPC URL works ## 1. Install ```bash pnpm theme={null} pnpm add @umbra-privacy/sdk ``` ```bash npm theme={null} npm install @umbra-privacy/sdk ``` ```bash yarn theme={null} yarn add @umbra-privacy/sdk ``` ## 2. Create a Signer For quick testing, generate an in-memory keypair. For production, see [Wallet Adapters](/sdk/wallet-adapters). ```typescript theme={null} import { createInMemorySigner } from "@umbra-privacy/sdk"; // Generate a random keypair (for testing only) const signer = await createInMemorySigner(); console.log("Wallet address:", signer.address); ``` An in-memory keypair is ephemeral - it disappears when your process exits. Use a browser wallet or a persistent keypair for anything beyond local testing. ## 3. Create the Umbra Client ```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", }); ``` 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 requires the master seed (typically `register()` or `deposit()`) will prompt the user to sign a consent message. Subsequent operations reuse the cached seed without re-prompting. ## 4. Register Your Account Registration sets up your on-chain Umbra identity. This function can be called regardless of whether the user is already registered - it handles the full setup, including key rotation when keys have changed. That said, each call submits on-chain transactions with SOL costs, so in practice you should check whether the account is already registered before calling it. ```typescript theme={null} import { getUserRegistrationFunction } from "@umbra-privacy/sdk"; const register = getUserRegistrationFunction({ client }); // This is where the wallet signing prompt appears for the first time. // The user signs once to derive the master seed; subsequent operations // reuse the cached seed without prompting again. const signatures = await register({ confidential: true, // enable encrypted balances anonymous: true, // enable mixer / anonymous transfers }); console.log(`Registered in ${signatures.length} transaction(s)`); ``` ## 5. Deposit Tokens Shield an SPL or Token-2022 token balance by moving it from your public wallet into an encrypted account. ```typescript theme={null} import { getPublicBalanceToEncryptedBalanceDirectDepositorFunction } from "@umbra-privacy/sdk"; const deposit = getPublicBalanceToEncryptedBalanceDirectDepositorFunction({ client }); const MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const amount = 1_000_000n; // 1 USDC (6 decimals) const result = await deposit( client.signer.address, // deposit into your own encrypted account MINT, amount, ); console.log("Deposit queued:", result.queueSignature); console.log("Callback confirmed:", result.callbackSignature); ``` ## 6. Withdraw Tokens Move tokens back from your encrypted account to your public wallet. ```typescript theme={null} import { getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction } from "@umbra-privacy/sdk"; const withdraw = getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction({ client }); const result = await withdraw( client.signer.address, // destination address MINT, amount, ); console.log("Withdrawal queued:", result.queueSignature); console.log("Callback confirmed:", result.callbackSignature); ``` ## 7. Create a Receiver-Claimable UTXO Send tokens privately to a recipient by depositing them into the [mixer](/sdk/mixer/overview). The recipient can later claim them with no on-chain link back to you as the sender. UTXO creation requires a `zkProver` dependency for Groth16 proof generation. Install `@umbra-privacy/web-zk-prover` for the recommended browser-based prover - see [ZK Provers](/sdk/advanced/zk-provers) for details. ```typescript theme={null} import { getPublicBalanceToReceiverClaimableUtxoCreatorFunction } from "@umbra-privacy/sdk"; import { getCreateReceiverClaimableUtxoFromPublicBalanceProver } from "@umbra-privacy/web-zk-prover"; const zkProver = getCreateReceiverClaimableUtxoFromPublicBalanceProver(); const createUtxo = getPublicBalanceToReceiverClaimableUtxoCreatorFunction( { client }, { zkProver }, ); const RECIPIENT = "RecipientWalletAddressHere"; const signatures = await createUtxo({ destinationAddress: RECIPIENT, mint: MINT, amount, }); console.log("UTXO created:", signatures[0]); ``` ## 8. Fetch Claimable UTXOs As the recipient, scan the Merkle tree for UTXOs addressed to your X25519 key. The SDK attempts to decrypt each ciphertext and returns only those belonging to you. ```typescript theme={null} import { getClaimableUtxoScannerFunction } from "@umbra-privacy/sdk"; const fetchUtxos = getClaimableUtxoScannerFunction({ client }); // Scan tree 0 from the start - pass your last seen index to resume const { received } = await fetchUtxos(0, 0); console.log("Received UTXOs:", received.length); ``` ## 9. Claim the UTXO Present a ZK proof on-chain to burn the UTXO and receive the tokens. Claiming into an encrypted balance keeps the received amount private - no on-chain trace of who received what. ```typescript theme={null} import { getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction, getUmbraRelayer, } from "@umbra-privacy/sdk"; import { getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver } from "@umbra-privacy/web-zk-prover"; const zkProver = getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(); const relayer = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com", }); const claim = getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction( { client }, { zkProver, relayer }, ); const claimResult = await claim([received[0]]); console.log("Claimed into encrypted balance:", claimResult); ``` ## Full Example ```typescript theme={null} import { createInMemorySigner, getUmbraClient, getUserRegistrationFunction, getPublicBalanceToEncryptedBalanceDirectDepositorFunction, getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction, getPublicBalanceToReceiverClaimableUtxoCreatorFunction, getClaimableUtxoScannerFunction, getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction, getUmbraRelayer, } from "@umbra-privacy/sdk"; import { getCreateReceiverClaimableUtxoFromPublicBalanceProver, getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver, } from "@umbra-privacy/web-zk-prover"; async function main() { // 1. Signer (use your wallet adapter in production) const signer = await createInMemorySigner(); // 2. Client const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl: "https://api.mainnet-beta.solana.com", rpcSubscriptionsUrl: "wss://api.mainnet-beta.solana.com", indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com", }); // 3. Register (safe to call regardless of prior registration state) const register = getUserRegistrationFunction({ client }); await register({ confidential: true, anonymous: true }); const MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // 4. Deposit into encrypted balance const deposit = getPublicBalanceToEncryptedBalanceDirectDepositorFunction({ client }); const depositResult = await deposit(signer.address, MINT, 1_000_000n); console.log("Deposited:", depositResult.queueSignature); // 5. Withdraw back to public wallet const withdraw = getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction({ client }); const withdrawResult = await withdraw(signer.address, MINT, 1_000_000n); console.log("Withdrawn:", withdrawResult.queueSignature); // 6. Set up ZK provers const utxoProver = getCreateReceiverClaimableUtxoFromPublicBalanceProver(); const claimProver = getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(); // 7. Create a receiver-claimable UTXO const createUtxo = getPublicBalanceToReceiverClaimableUtxoCreatorFunction( { client }, { zkProver: utxoProver }, ); const RECIPIENT = "RecipientWalletAddressHere"; await createUtxo({ destinationAddress: RECIPIENT, mint: MINT, amount: 500_000n, }); // 8. Fetch UTXOs addressed to this wallet (as the recipient) const fetchUtxos = getClaimableUtxoScannerFunction({ client }); const { received } = await fetchUtxos(0, 0); // 9. Claim the first received UTXO into an encrypted balance const relayer = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com", }); if (received.length > 0) { const claim = getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction( { client }, { zkProver: claimProver, relayer }, ); const claimResult = await claim([received[0]]); console.log("Claimed:", claimResult); } } main().catch(console.error); ``` ## Next Steps Connect Phantom, Solflare, or any Solana wallet. Make transfers fully anonymous using the mixer. Read encrypted balances and account state on-chain. Handle deposit and withdrawal failures gracefully. # 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 mixer operations (`getClaimableUtxoScannerFunction`). * `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?: FetchMerkleProofFunction` - Indexer-based Merkle proof fetcher. Present only when `indexerApiEndpoint` is set. * `fetchUtxoData?: FetchUtxoDataFunction` - Indexer-based UTXO discovery fetcher. Present only when `indexerApiEndpoint` is set. * `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` ```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: RcEncryptionNonce, optionalData?: OptionalData32, callbacks?: TransactionCallbacks, ) => 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: RcEncryptionNonce` - Grant nonce, used to differentiate multiple grants to the same receiver. ### Example ```typescript theme={null} import { getComplianceGrantIssuerFunction } from "@umbra-privacy/sdk"; const createGrant = getComplianceGrantIssuerFunction({ client }); const signature = await createGrant( receiverAddress, granterX25519PublicKey, receiverX25519PublicKey, grantNonce, ); ``` *** ## getComplianceGrantRevokerFunction **Import:** `@umbra-privacy/sdk` ```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: RcEncryptionNonce, optionalData?: OptionalData32, callbacks?: TransactionCallbacks, ) => Promise ``` Parameters are identical to `CreateUserGrantedComplianceGrantFunction` - pass the same values used when creating the grant. *** ## getUserComplianceGrantQuerierFunction **Import:** `@umbra-privacy/sdk` ```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: RcEncryptionNonce, receiverX25519: X25519PublicKey, ) => Promise ``` *** ## getQueryNetworkMxeComplianceGrantFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getQueryNetworkMxeComplianceGrantFunction( args: GetQueryNetworkMxeComplianceGrantFunctionArgs, deps?: GetQueryNetworkMxeComplianceGrantFunctionDeps, ): QueryNetworkMxeComplianceGrantFunction ``` 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 `QueryNetworkMxeComplianceGrantFunction` ```typescript theme={null} type QueryNetworkMxeComplianceGrantFunction = ( nonce: RcEncryptionNonce, receiverX25519: X25519PublicKey, ) => Promise ``` *** ## getQueryNetworkSharedComplianceGrantFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getQueryNetworkSharedComplianceGrantFunction( args: GetQueryNetworkSharedComplianceGrantFunctionArgs, deps?: GetQueryNetworkSharedComplianceGrantFunctionDeps, ): QueryNetworkSharedComplianceGrantFunction ``` Checks whether a network shared compliance grant exists. Network shared grants allow the Arcium network to re-encrypt shared-mode ciphertexts. ### Returns `QueryNetworkSharedComplianceGrantFunction` ```typescript theme={null} type QueryNetworkSharedComplianceGrantFunction = ( granterX25519: X25519PublicKey, nonce: RcEncryptionNonce, 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. *** ## getReencryptMxeCiphertextsNetworkGrantFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getReencryptMxeCiphertextsNetworkGrantFunction( args: GetReencryptMxeCiphertextsNetworkGrantFunctionArgs, deps?: GetReencryptMxeCiphertextsNetworkGrantFunctionDeps, ): ReencryptMxeCiphertextsNetworkGrantFunction ``` Re-encrypts MXE-mode ciphertexts under a network grant, making them readable by the grant receiver. Queues an Arcium MPC computation. ### GetReencryptMxeCiphertextsNetworkGrantFunctionArgs * `client: IUmbraClient` ### GetReencryptMxeCiphertextsNetworkGrantFunctionDeps * `getLatestBlockhash?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` ### Returns `ReencryptMxeCiphertextsNetworkGrantFunction` ```typescript theme={null} type ReencryptMxeCiphertextsNetworkGrantFunction = ( receiverX25519Key: X25519PublicKey, nonce: RcEncryptionNonce, inputEncryptionNonce: RcEncryptionNonce, ciphertexts: readonly Uint8Array[], optionalData?: OptionalData32, callbacks?: TransactionCallbacks, ) => Promise ``` * `receiverX25519Key: X25519PublicKey` - The receiver's X25519 public key. * `nonce: RcEncryptionNonce` - The grant nonce identifying which network grant to use. * `inputEncryptionNonce: RcEncryptionNonce` - 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` ```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: RcEncryptionNonce, inputEncryptionNonce: RcEncryptionNonce, ciphertexts: readonly Uint8Array[], optionalData?: OptionalData32, callbacks?: TransactionCallbacks, ) => 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` ```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: RcEncryptionNonce, inputEncryptionNonce: RcEncryptionNonce, ciphertexts: readonly Uint8Array[], optionalData?: OptionalData32, callbacks?: TransactionCallbacks, ) => 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` ```typescript theme={null} function getNetworkEncryptionToSharedEncryptionConverterFunction( args: GetNetworkEncryptionToSharedEncryptionConverterFunctionArgs, deps?: GetNetworkEncryptionToSharedEncryptionConverterFunctionDeps, ): NetworkEncryptionToSharedEncryptionConverterFunction ``` 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. *** ### GetNetworkEncryptionToSharedEncryptionConverterFunctionArgs * `client: IUmbraClient` ### GetNetworkEncryptionToSharedEncryptionConverterFunctionDeps * `accountInfoProvider?: AccountInfoProviderFunction` * `getLatestBlockhash?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` ### Returns `NetworkEncryptionToSharedEncryptionConverterFunction` ```typescript theme={null} type NetworkEncryptionToSharedEncryptionConverterFunction = ( mints: readonly Address[], optionalData?: OptionalData32, callbacks?: TransactionCallbacks, ) => Promise ``` * `mints: readonly Address[]` - Token mints to convert. Processed sequentially - each successful conversion is independent. ### ConvertToSharedEncryptionResult ```typescript theme={null} interface ConvertToSharedEncryptionResult { converted: Map; skipped: Map; } ``` * `converted` - Mints successfully converted. Value is the transaction signature. * `skipped` - Mints not converted. Value is 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"; 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` ```typescript theme={null} function getMintEncryptionKeyRotatorFunction( args: GetMintEncryptionKeyRotatorFunctionArgs, deps?: GetMintEncryptionKeyRotatorFunctionDeps, ): MintEncryptionKeyRotatorFunction ``` 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. *** ### GetMintEncryptionKeyRotatorFunctionArgs * `client: IUmbraClient` ### GetMintEncryptionKeyRotatorFunctionDeps * `accountInfoProvider?: AccountInfoProviderFunction` * `getLatestBlockhash?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` * `mintX25519KeypairGenerator?: MintX25519KeypairGeneratorFunction` - Override the per-mint X25519 keypair derivation. ### Returns `MintEncryptionKeyRotatorFunction` ```typescript theme={null} type MintEncryptionKeyRotatorFunction = ( mint: Address, optionalData?: OptionalData32, callbacks?: TransactionCallbacks, ) => 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"; 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 API reference: getPublicBalanceToEncryptedBalanceDirectDepositorFunction (DepositResult) + 4 UTXO creators for self/receiver-claimable from encrypted/public balance. ## getPublicBalanceToEncryptedBalanceDirectDepositorFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getPublicBalanceToEncryptedBalanceDirectDepositorFunction( args: GetPublicBalanceToEncryptedBalanceDirectDepositorFunctionArgs, deps?: GetPublicBalanceToEncryptedBalanceDirectDepositorFunctionDeps, ): PublicBalanceToEncryptedBalanceDirectDepositorFunction ``` Transfers tokens from a public associated token account (ATA) into a destination address's encrypted balance. 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). *** ### GetPublicBalanceToEncryptedBalanceDirectDepositorFunctionArgs * `client: IUmbraClient` ### GetPublicBalanceToEncryptedBalanceDirectDepositorFunctionDeps * `accountInfoProvider?: AccountInfoProviderFunction` * `getLatestBlockhash?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` * `getEpochInfo?: GetEpochInfo` - Required for Token-2022 transfer fee calculation. ### Returns `PublicBalanceToEncryptedBalanceDirectDepositorFunction` ```typescript theme={null} type PublicBalanceToEncryptedBalanceDirectDepositorFunction = ( destinationAddress: Address, mint: Address, transferAmount: U64, options?: DepositIntoEncryptedBalanceOptions, ) => Promise ``` * `destinationAddress: Address` - The Umbra-registered recipient. * `mint: Address` - Token mint address. * `transferAmount: U64` - Amount in base token units. ### DepositIntoEncryptedBalanceOptions * `priorityFees?: U64` - Compute unit price in micro-lamports. Default: `0n`. * `purpose?: number` - Caller-defined purpose tag stored on-chain. Default: `0`. * `optionalData?: OptionalData32` - 32-byte caller metadata. Default: 32 zero bytes. * `awaitCallback?: boolean` - Whether to wait for the MPC callback. Default: `true`. * `skipPreflight?: boolean` - Skip Solana preflight simulation. Default: `false`. * `maxRetries?: number` - Max RPC retry attempts for transaction sending. * `accountInfoCommitment?: Commitment` - Per-call commitment for RPC account reads. Default: `"confirmed"`. * `epochInfoCommitment?: Commitment` - Per-call commitment for epoch info fetches. Default: `"confirmed"`. ### DepositResult ```typescript theme={null} interface DepositResult { queueSignature: TransactionSignature; callbackStatus?: "finalized" | "pruned" | "timed-out"; callbackSignature?: TransactionSignature; callbackElapsedMs?: number; rentClaimSignature?: TransactionSignature; rentClaimError?: string; } ``` * `queueSignature` - Signature of the handler (queue computation) transaction. * `callbackStatus` - The outcome of computation monitoring: `"finalized"`, `"pruned"`, or `"timed-out"`. Present when `awaitCallback` is `true`. * `callbackSignature` - Signature of the Arcium MPC callback. Present when `callbackStatus` is `"finalized"`. * `callbackElapsedMs` - Milliseconds spent waiting for the callback. * `rentClaimSignature` - Signature for reclaiming rent from the computation account. Attempted regardless of callback outcome. * `rentClaimError` - Error from rent reclaim attempt, if any. The deposit itself still succeeded. ### Errors Throws `EncryptedDepositError`. See [Errors](./errors#encrypteddepositerror). ### Example ```typescript theme={null} import { getPublicBalanceToEncryptedBalanceDirectDepositorFunction } from "@umbra-privacy/sdk"; const deposit = getPublicBalanceToEncryptedBalanceDirectDepositorFunction({ client }); const result = await deposit(recipientAddress, mintAddress, 1_000_000n); console.log("Queue signature:", result.queueSignature); ``` *** ## getEncryptedBalanceToSelfClaimableUtxoCreatorFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getEncryptedBalanceToSelfClaimableUtxoCreatorFunction( args: GetCreateSelfClaimableUtxoFromEncryptedBalanceFunctionArgs, deps: GetCreateSelfClaimableUtxoFromEncryptedBalanceFunctionDeps, ): CreateSelfClaimableUtxoFromEncryptedBalanceFunction ``` Creates a self-claimable UTXO in the mixer funded from the caller's encrypted balance (encrypted balance -> Mixer). The created UTXO can only be claimed by the creator. Requires a ZK proof - `deps.zkProver` is **required**. *** ### GetCreateSelfClaimableUtxoFromEncryptedBalanceFunctionArgs * `client: IUmbraClient` ### GetCreateSelfClaimableUtxoFromEncryptedBalanceFunctionDeps * `zkProver: IZkProverForSelfClaimableUtxo` - **Required.** ZK proof generator for UTXO commitments. Use `getCreateSelfClaimableUtxoFromEncryptedBalanceProver` from `@umbra-privacy/web-zk-prover` -- see [ZK Provers](/sdk/advanced/zk-provers). This operation submits two transactions: a proof account creation followed by the UTXO instruction. * `blockhashProvider?: GetLatestBlockhash` * `accountInfoProvider?: AccountInfoProviderFunction` * `transactionForwarder?: TransactionForwarder` * `getEpochInfo?: GetEpochInfo` * Additional cryptographic dependency overrides (key derivation, Poseidon, AES, commitment generators - all optional, default to SDK implementations). ### Returns `CreateSelfClaimableUtxoFromEncryptedBalanceFunction` ```typescript theme={null} type CreateSelfClaimableUtxoFromEncryptedBalanceFunction = ( args: CreateUtxoArgs, options?: CreateUtxoOptions, ) => Promise ``` Returns two signatures: `[proofAccountSignature, utxoCreationSignature]`. ### CreateUtxoArgs * `amount: U64` - Token amount in base units to lock into the UTXO. * `destinationAddress: Address` - The Umbra-registered owner of the UTXO (must equal the caller for self-claimable). * `mint: Address` - Token mint address. ### CreateUtxoOptions * `generationIndex?: U256` - Override the generation index used for key derivation. Defaults to the on-chain value. * `optionalData?: OptionalData32` - 32-byte caller metadata. * `createProofAccount?: TransactionCallbacks` - Lifecycle hooks for the proof account creation transaction. * `createUtxo?: TransactionCallbacks` - Lifecycle hooks for the UTXO creation transaction. ### Errors Throws `CreateUtxoError`. See [Errors](./errors#createutxoerror). *** ## getEncryptedBalanceToReceiverClaimableUtxoCreatorFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getEncryptedBalanceToReceiverClaimableUtxoCreatorFunction( args: GetCreateReceiverClaimableUtxoFromEncryptedBalanceFunctionArgs, deps: GetCreateReceiverClaimableUtxoFromEncryptedBalanceFunctionDeps, ): CreateReceiverClaimableUtxoFromEncryptedBalanceFunction ``` Creates a receiver-claimable UTXO in the mixer funded from the caller's encrypted balance (encrypted balance -> Mixer). The UTXO is encrypted for the recipient's X25519 key - only the recipient can claim it. Requires `deps.zkProver` (**required**). *** ### GetCreateReceiverClaimableUtxoFromEncryptedBalanceFunctionArgs * `client: IUmbraClient` ### GetCreateReceiverClaimableUtxoFromEncryptedBalanceFunctionDeps * `zkProver: IZkProverForReceiverClaimableUtxo` - **Required.** Use `getCreateReceiverClaimableUtxoFromEncryptedBalanceProver` from `@umbra-privacy/web-zk-prover` -- see [ZK Provers](/sdk/advanced/zk-provers). * `blockhashProvider?: GetLatestBlockhash` * `accountInfoProvider?: AccountInfoProviderFunction` * `transactionForwarder?: TransactionForwarder` * `getEpochInfo?: GetEpochInfo` * Additional cryptographic dependency overrides (all optional). ### Returns `CreateReceiverClaimableUtxoFromEncryptedBalanceFunction` ```typescript theme={null} type CreateReceiverClaimableUtxoFromEncryptedBalanceFunction = ( args: CreateUtxoArgs, options?: CreateUtxoOptions, ) => Promise ``` See `CreateUtxoArgs` and `CreateUtxoOptions` above. ### Errors Throws `CreateUtxoError`. See [Errors](./errors#createutxoerror). *** ## getPublicBalanceToSelfClaimableUtxoCreatorFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getPublicBalanceToSelfClaimableUtxoCreatorFunction( args: GetCreateSelfClaimableUtxoFromPublicBalanceFunctionArgs, deps: GetCreateSelfClaimableUtxoFromPublicBalanceFunctionDeps, ): CreateSelfClaimableUtxoFromPublicBalanceFunction ``` Creates a self-claimable UTXO in the mixer funded from a public ATA (ATA -> Mixer). No MPC required - the ZK proof is generated client-side. `deps.zkProver` is **required**. *** ### GetCreateSelfClaimableUtxoFromPublicBalanceFunctionArgs * `client: IUmbraClient` ### GetCreateSelfClaimableUtxoFromPublicBalanceFunctionDeps * `zkProver: ZkProverForSelfClaimableUtxoFromPublicBalance` - **Required.** Use `getCreateSelfClaimableUtxoFromPublicBalanceProver` from `@umbra-privacy/web-zk-prover` -- see [ZK Provers](/sdk/advanced/zk-provers). * `blockhashProvider?: GetLatestBlockhash` * `accountInfoProvider?: AccountInfoProviderFunction` * `transactionForwarder?: TransactionForwarder` * `getEpochInfo?: GetEpochInfo` * `masterViewingKeyGenerator?: MasterViewingKeyGeneratorFunction` * `masterViewingKeyBlindingFactorGenerator?: MasterViewingKeyBlindingFactorGeneratorFunction` * `poseidonPrivateKeyGenerator?: PoseidonPrivateKeyGeneratorFunction` * `poseidonBlindingFactorGenerator?: PoseidonBlindingFactorGeneratorFunction` * `userAccountX25519KeypairGenerator?: Curve25519KeypairGeneratorFunction` * `secondViewingKeyGenerator?: SecondViewingKeyGeneratorFunction` * `ephemeralUtxoMasterViewingKeyGenerator?: EphemeralUtxoMasterViewingKeyGeneratorFunction` * `ephemeralUtxoMasterViewingKeyBlindingFactorGenerator?: EphemeralUtxoMasterViewingKeyBlindingFactorGeneratorFunction` * `ephemeralUtxoPoseidonKeyGenerator?: EphemeralUtxoPoseidonPrivateKeyGeneratorFunction` * `ephemeralUtxoPoseidonKeyBlindingFactorGenerator?: EphemeralUtxoPoseidonPrivateKeyBlindingFactorGeneratorFunction` * `poseidonKeystreamBlindingFactorGenerator?: PoseidonKeystreamBlindingFactorGeneratorFunction` * `poseidonHasher?: PoseidonHashFunction` * `aesEncryptor?: AesEncryptorFunction` * `userCommitmentGenerator?: UserCommitmentGeneratorFunction` * `h2Generator?: H2GeneratorFns` * `keystreamCommitmentGenerator?: KeystreamCommitmentFunction` * `poseidonEncryptor?: PoseidonEncryptorFunction` * `poseidonKeystreamGenerator?: PoseidonKeystreamGeneratorFunction` * `getUtcNow?: () => UtcTimestampComponents` ### Returns `CreateSelfClaimableUtxoFromPublicBalanceFunction` ```typescript theme={null} type CreateSelfClaimableUtxoFromPublicBalanceFunction = ( args: CreateUtxoArgs, options?: CreateUtxoFromPublicBalanceOptions, ) => Promise ``` ### CreateUtxoFromPublicBalanceOptions * `generationIndex?: U256` * `optionalData?: OptionalData32` * `createUtxo?: TransactionCallbacks` - Lifecycle hooks for the single UTXO creation transaction. ### Errors Throws `CreateUtxoError`. See [Errors](./errors#createutxoerror). *** ## getPublicBalanceToReceiverClaimableUtxoCreatorFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getPublicBalanceToReceiverClaimableUtxoCreatorFunction( args: GetCreateReceiverClaimableUtxoFromPublicBalanceFunctionArgs, deps: GetCreateReceiverClaimableUtxoFromPublicBalanceFunctionDeps, ): CreateReceiverClaimableUtxoFromPublicBalanceFunction ``` Creates a receiver-claimable UTXO funded from a public ATA (ATA -> Mixer). The UTXO is encrypted for the recipient - only they can claim it. `deps.zkProver` is **required**. *** ### GetCreateReceiverClaimableUtxoFromPublicBalanceFunctionArgs * `client: IUmbraClient` ### GetCreateReceiverClaimableUtxoFromPublicBalanceFunctionDeps * `zkProver: ZkProverForReceiverClaimableUtxoFromPublicBalance` - **Required.** Use `getCreateReceiverClaimableUtxoFromPublicBalanceProver` from `@umbra-privacy/web-zk-prover` -- see [ZK Provers](/sdk/advanced/zk-provers). * `blockhashProvider?: GetLatestBlockhash` * `accountInfoProvider?: AccountInfoProviderFunction` * `transactionForwarder?: TransactionForwarder` * `getEpochInfo?: GetEpochInfo` * Additional cryptographic dependency overrides (all optional). ### Returns `CreateReceiverClaimableUtxoFromPublicBalanceFunction` ```typescript theme={null} type CreateReceiverClaimableUtxoFromPublicBalanceFunction = ( args: CreateUtxoArgs, options?: CreateReceiverClaimableUtxoFromPublicBalanceOptions, ) => Promise ``` ### CreateReceiverClaimableUtxoFromPublicBalanceOptions * `generationIndex?: U256` * `optionalData?: OptionalData32` * `createUtxo?: TransactionCallbacks` ### Errors Throws `CreateUtxoError`. See [Errors](./errors#createutxoerror). *** ## EncryptedDepositError Thrown by `getPublicBalanceToEncryptedBalanceDirectDepositorFunction`. 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) for full documentation. *** ## CreateUtxoError Thrown by all four UTXO creation functions. 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) for full documentation. # 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, isEncryptedDepositError, isEncryptedWithdrawalError, isRegistrationError, isConversionError, isCreateUtxoError, isFetchUtxosError, isClaimUtxoError, isQueryError, // 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 ``` *** ## 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 `getPublicBalanceToEncryptedBalanceDirectDepositorFunction`. ```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 `getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction`. ```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 UTXO creation factory functions. ```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 `getClaimableUtxoScannerFunction`. ```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 `treeIndex` or insertion index parameters. * `"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. *** ### ClaimUtxoError Thrown by all three claim factory functions. ```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 UTXO 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 - fetch fresh UTXOs. * `"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). *** ## 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 claims - May indicate a stale Merkle proof. Fetch fresh UTXOs before retrying. * `isRpcError` - **Safe to retry with exponential backoff.** Transient network issue. * All other stages - **Safe to retry with exponential backoff.** # Mixer Source: https://sdk.umbraprivacy.com/reference/mixer API reference: getClaimableUtxoScannerFunction (ScannedUtxoResult) + 3 claim variants into encrypted or public balance with relayer and ZK prover deps. ## getClaimableUtxoScannerFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getClaimableUtxoScannerFunction( args: GetClaimableUtxoScannerFunctionArgs, deps?: GetClaimableUtxoScannerFunctionDeps, ): ClaimableUtxoScannerFunction ``` Returns a function that discovers UTXOs claimable by the caller within a given insertion index range of a mixer tree. Queries the Umbra indexer to find UTXO ciphertexts, then attempts to decrypt them using the caller's X25519 private key and AES decryption. UTXOs that decrypt successfully are returned as claimable. `client.indexerApiEndpoint` must be set (pass it to `getUmbraClient`). *** ### GetClaimableUtxoScannerFunctionArgs * `client: IUmbraClient` - Must have `indexerApiEndpoint` configured. ### GetClaimableUtxoScannerFunctionDeps * `fetchUtxoData?: FetchUtxoDataFunction` - Override the default indexer UTXO data fetcher. * `fetchMerkleProof?: FetchMerkleProofFunction` - Override the default indexer Merkle proof fetcher. * `aesDecryptor?: AesDecryptorFunction` - Override the AES-256-GCM decryptor used for receiver-claimable UTXOs. * `x25519GetSharedSecret?: (privateKey: X25519PrivateKey, publicKey: X25519PublicKey) => Uint8Array` - Override the X25519 shared secret derivation. ### Returns `ClaimableUtxoScannerFunction` ```typescript theme={null} type ClaimableUtxoScannerFunction = ( treeIndex: U32, startInsertionIndex: U32, endInsertionIndex?: U32, ) => Promise ``` * `treeIndex: U32` - The mixer tree index to scan. Branded `U32` type. * `startInsertionIndex: U32` - Inclusive start of the UTXO insertion range to scan. Branded `U32` type. * `endInsertionIndex?: U32` - Exclusive end of the range. Defaults to the current tree size. Branded `U32` type. ### ScannedUtxoResult ```typescript theme={null} interface ScannedUtxoResult { selfBurnable: ClaimableUtxoData[]; received: ClaimableUtxoData[]; publicSelfBurnable: ClaimableUtxoData[]; publicReceived: ClaimableUtxoData[]; } ``` * `selfBurnable` - UTXOs you created yourself (from encrypted balance). You can burn these. * `received` - UTXOs sent to you by others (from encrypted balance), addressed to the caller. * `publicSelfBurnable` - UTXOs you created yourself (from public balance). You can burn these. * `publicReceived` - UTXOs sent to you by others (from public balance), addressed to the caller. ### ClaimableUtxoData Fields present on every claimable UTXO: * `merkleRoot: U256LeBytes` - The Merkle root at the time this UTXO's proof was fetched (little-endian bytes). * `merklePath: U256LeBytes[]` - Sibling hashes forming the Merkle inclusion proof (little-endian bytes). * `leafIndex: U128` - The UTXO's leaf index in the Indexed Merkle Tree. * `amount: U64` - Token amount in base units. * `destinationAddress: Address` - The Umbra-registered owner who can claim this UTXO. * `depositModifiedGenerationIndex: U128` - Generation index encoded in the deposit. * `version: U64` - Protocol version of the UTXO. * `commitmentIndex: U128` - Commitment index used in key derivation. * `senderAddressLow: U128` - Low 128 bits of the sender address (for the ZK circuit). * `senderAddressHigh: U128` - High 128 bits of the sender address. * `relayerFixedSolFees: U64` - Fixed SOL fees paid to relayer (in lamports). * `mintAddressLow: U128` - Low 128 bits of the mint address. * `mintAddressHigh: U128` - High 128 bits of the mint address. * `timestamp: TimestampComponents` - UTXO creation timestamp components for TVK derivation. * `purpose: number` - Caller-defined purpose tag. * `h1CircuitProvableOnChainDataHash: U256LeBytes` - H1 hash for the ZK circuit public inputs (little-endian bytes). * `h1SmartProgramProvableOnChainDataHash: U256LeBytes` - H1 hash for the on-chain verifier (little-endian bytes). *** ### Errors Throws `FetchUtxosError`. See [Errors](./errors#fetchutxoserror). ### Example ```typescript theme={null} import { getClaimableUtxoScannerFunction } from "@umbra-privacy/sdk"; const scanUtxos = getClaimableUtxoScannerFunction({ client }); // Scan tree 0 from insertion index 0 to 1000 const result = await scanUtxos(0, 0, 1000); console.log("Self-burnable UTXOs:", result.selfBurnable.length); console.log("Received UTXOs:", result.received.length); ``` *** ## getSelfClaimableUtxoToEncryptedBalanceClaimerFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getSelfClaimableUtxoToEncryptedBalanceClaimerFunction( args: GetSelfClaimableUtxoToEncryptedBalanceClaimerFunctionArgs, deps: GetSelfClaimableUtxoToEncryptedBalanceClaimerFunctionDeps, ): SelfClaimableUtxoToEncryptedBalanceClaimerFunction ``` Claims a self-burnable UTXO (one you created yourself) and deposits its value into the caller's encrypted balance. Requires a Groth16 ZK proof - `deps.zkProver` is **required**. The nullifier is burned on-chain after a successful claim to prevent double-spending. *** ### GetSelfClaimableUtxoToEncryptedBalanceClaimerFunctionArgs * `client: IUmbraClient` ### GetSelfClaimableUtxoToEncryptedBalanceClaimerFunctionDeps * `zkProver: IZkProverForClaimSelfClaimableUtxo` - **Required.** Groth16 prover for the claim circuit. * `accountInfoProvider?: AccountInfoProviderFunction` * `blockhashProvider?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` * Additional cryptographic dependency overrides (key derivation, Poseidon, Rescue - all optional). ### Returns `SelfClaimableUtxoToEncryptedBalanceClaimerFunction` ```typescript theme={null} type SelfClaimableUtxoToEncryptedBalanceClaimerFunction = ( utxos: readonly ClaimableUtxoData[], optionalData?: OptionalData32, ) => Promise ``` * `utxos: readonly ClaimableUtxoData[]` - Array of claimable UTXOs from `getClaimableUtxoScannerFunction`. Use UTXOs from `result.selfBurnable`. * `optionalData?: OptionalData32` - Optional 32-byte caller metadata. Returns `ClaimUtxoIntoEncryptedBalanceResult` with `signatures: Record` organized by batch index. *** ### Errors Throws `ClaimUtxoError`. See [Errors](./errors#claimutxoerror). The `"transaction-validate"` stage often indicates a stale Merkle proof - fetch fresh UTXOs and retry. *** ## getSelfClaimableUtxoToPublicBalanceClaimerFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getSelfClaimableUtxoToPublicBalanceClaimerFunction( args: GetSelfClaimableUtxoToPublicBalanceClaimerFunctionArgs, deps: GetSelfClaimableUtxoToPublicBalanceClaimerFunctionDeps, ): SelfClaimableUtxoToPublicBalanceClaimerFunction ``` Claims a self-burnable UTXO (one you created yourself) and sends its value to a public ATA. Same flow as `getSelfClaimableUtxoToEncryptedBalanceClaimerFunction` but the output goes to a public account instead of an encrypted balance. `deps.zkProver` is **required**. ### Returns `SelfClaimableUtxoToPublicBalanceClaimerFunction` ```typescript theme={null} type SelfClaimableUtxoToPublicBalanceClaimerFunction = ( utxos: readonly ClaimableUtxoData[], optionalData?: OptionalData32, ) => Promise ``` * `utxos: readonly ClaimableUtxoData[]` - Array of claimable UTXOs. Use UTXOs from `result.selfBurnable` or `result.publicSelfBurnable`. * `optionalData?: OptionalData32` - Optional 32-byte caller metadata. Returns `ClaimUtxoIntoPublicBalanceResult` with `signatures: Record` organized by batch index. ### Errors Throws `ClaimUtxoError`. See [Errors](./errors#claimutxoerror). *** ## getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction( args: GetReceiverClaimableUtxoToEncryptedBalanceClaimerFunctionArgs, deps: GetReceiverClaimableUtxoToEncryptedBalanceClaimerFunctionDeps, ): ReceiverClaimableUtxoToEncryptedBalanceClaimerFunction ``` Claims a receiver-addressed UTXO (one sent to the caller by another user) and deposits its value into the caller's encrypted balance. `deps.zkProver` is **required**. ### Returns `ReceiverClaimableUtxoToEncryptedBalanceClaimerFunction` ```typescript theme={null} type ReceiverClaimableUtxoToEncryptedBalanceClaimerFunction = ( utxos: readonly ClaimableUtxoData[], optionalData?: OptionalData32, ) => Promise ``` * `utxos: readonly ClaimableUtxoData[]` - Array of claimable UTXOs. Use UTXOs from `result.received` or `result.publicReceived`. * `optionalData?: OptionalData32` - Optional 32-byte caller metadata. Returns `ClaimUtxoIntoEncryptedBalanceResult` with `signatures: Record` organized by batch index. ### Errors Throws `ClaimUtxoError`. See [Errors](./errors#claimutxoerror). *** ## Full Mixer Flow Example ```typescript theme={null} import { getClaimableUtxoScannerFunction, getSelfClaimableUtxoToEncryptedBalanceClaimerFunction, getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction, } from "@umbra-privacy/sdk"; import { getSelfClaimableUtxoToEncryptedBalanceClaimerProver, getReceiverClaimableUtxoToEncryptedBalanceClaimerProver } from "@umbra-privacy/web-zk-prover"; // Build functions (once) const scanUtxos = getClaimableUtxoScannerFunction({ client }); const claimSelf = getSelfClaimableUtxoToEncryptedBalanceClaimerFunction( { client }, { zkProver: getSelfClaimableUtxoToEncryptedBalanceClaimerProver() }, ); const claimReceived = getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction( { client }, { zkProver: getReceiverClaimableUtxoToEncryptedBalanceClaimerProver() }, ); // Fetch claimable UTXOs const { selfBurnable, received } = await scanUtxos(0, 0, 1000); // Claim each one if (selfBurnable.length > 0) { const result = await claimSelf(selfBurnable); console.log("Claimed self UTXOs:", result.signatures); } if (received.length > 0) { const result = await claimReceived(received); console.log("Claimed received UTXOs:", result.signatures); } ``` *** ## FetchUtxosError Thrown by `getClaimableUtxoScannerFunction`. Stage values: `"initialization"` | `"validation"` | `"key-derivation"` | `"indexer-fetch"` | `"proof-fetch"` See [Errors](./errors#fetchutxoserror) for full documentation. *** ## ClaimUtxoError Thrown by all claim functions. 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) for full documentation. Before retrying after `"transaction-send"`, verify on-chain - the nullifier may have been burned. # SDK Reference Source: https://sdk.umbraprivacy.com/reference/overview Complete SDK reference index: all factory functions from @umbra-privacy/sdk, import paths (/types, /interfaces, /utils, /constants, /errors), naming conventions. This reference covers the full public API surface of `@umbra-privacy/sdk`. Pages are organized by feature area - each section documents factory functions alongside their parameter types, result types, and associated errors. ## Import Paths * `@umbra-privacy/sdk` - `getUmbraClient`, all service factory functions, cryptographic primitives * `@umbra-privacy/sdk/errors` - All error classes, type guards, and stage union types * `@umbra-privacy/sdk/types` - Branded primitive types and assertion functions * `@umbra-privacy/sdk/interfaces` - TypeScript function type definitions * `@umbra-privacy/sdk/utils` - Converter utilities, PDA derivation helpers, address utilities * `@umbra-privacy/sdk/constants` - Network configs, program constants, domain separators *** ## Factory Function Pattern Every SDK operation follows the same two-step pattern: ```typescript theme={null} // Step 1 - build the function once (at setup time) const operation = getOperationFunction({ client }, deps?); // Step 2 - call it at runtime const result = await operation(/* runtime arguments */); ``` The factory (`getOperationFunction`) captures configuration and optional dependency overrides. The returned function is what you call per-operation. The optional `deps` argument accepts injectable overrides for RPC providers, key generators, and ZK provers - 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 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. * `RcEncryptionNonce` - 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. * `TransactionCallbacks` - `{ pre?: (tx: CompiledTransaction) => Promise; post?: (tx: CompiledTransaction, sig: TransactionSignature) => Promise }`. Hooks invoked before signing and after confirmation. * `IUmbraClient` - The client configuration object produced by `getUmbraClient`. Passed to every factory function as `args.client`. * `Network` - `"mainnet" | "devnet" | "localnet"`. *** ## Pages in This Reference * [Client](/reference/client) - `getUmbraClient`, `IUmbraClient`, `IUmbraSigner` * [Registration](/reference/registration) - User registration, key rotation, seed updates, staged fund recovery * [Deposit](/reference/deposit) - Direct deposit, self-claimable and receiver-claimable UTXO creation * [Withdrawal](/reference/withdraw) - Direct withdrawal from encrypted balance to public balance * [Query](/reference/query) - User account and encrypted balance queries * [Conversion](/reference/conversion) - Convert to shared encryption, rotate per-mint X25519 key * [Mixer](/reference/mixer) - Fetch claimable UTXOs, claim into encrypted or public balance * [Compliance](/reference/compliance) - Compliance grants, re-encryption operations * [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` ```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 for UTXO encryption). * `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"; 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` ```typescript theme={null} function getEncryptedBalanceQuerierFunction( args: GetEncryptedBalanceQuerierFunctionArgs, deps?: GetEncryptedBalanceQuerierFunctionDeps, ): EncryptedBalanceQuerierFunction ``` Returns a function that fetches and decrypts encrypted token balances for a set of mints. For shared-mode accounts, the balance is decrypted locally using the caller's X25519 private key. For MXE-mode accounts, the ciphertext cannot be decrypted client-side. *** ### GetEncryptedBalanceQuerierFunctionArgs * `client: IUmbraClient` ### GetEncryptedBalanceQuerierFunctionDeps * `accountInfoProvider?: AccountInfoProviderFunction` * `rcDecryptor?: RcDecryptorFunction` - Override the Rescue cipher decryptor used for shared-mode balance decryption. ### Returns `EncryptedBalanceQuerierFunction` ```typescript theme={null} type EncryptedBalanceQuerierFunction = ( mints: readonly Address[], options?: { commitment?: Commitment }, ) => Promise> ``` Returns a `Map` keyed by mint address. Each entry is one of: ### QueryEncryptedBalanceResult A discriminated union per mint: * `{ state: "shared"; balance: U64 }` - Shared-mode account. `balance` is the decrypted token amount in base units. * `{ state: "mxe" }` - MXE-mode account. Balance is encrypted for the Arcium network and cannot be read client-side. * `{ state: "uninitialized" }` - The encrypted token account exists but has not been initialised for this mint. * `{ state: "non_existent" }` - No encrypted token account exists for this mint. *** ### Errors Throws `QueryError`. See [Errors](./errors#queryerror). ### Example ```typescript theme={null} import { getEncryptedBalanceQuerierFunction } from "@umbra-privacy/sdk"; const queryBalances = getEncryptedBalanceQuerierFunction({ client }); const balances = await queryBalances([usdcMint, solMint]); for (const [mint, result] of balances) { if (result.state === "shared") { console.log(`${mint}: ${result.balance} base units`); } else if (result.state === "mxe") { console.log(`${mint}: MXE-encrypted, balance not readable client-side`); } else { console.log(`${mint}: ${result.state}`); } } ``` *** ## QueryError Thrown by both query functions. Stage values: `"initialization"` | `"pda-derivation"` | `"account-fetch"` | `"account-decode"` | `"key-derivation"` | `"decryption"` * `"key-derivation"` and `"decryption"` are only reachable from `getEncryptedBalanceQuerierFunction` (shared-mode decryption path). See [Errors](./errors#queryerror) for full documentation. # Registration Source: https://sdk.umbraprivacy.com/reference/registration API reference: getUserRegistrationFunction (3-step: account init, X25519 key, user commitment ZK proof), key rotators, entropy seed updaters, staged fund recovery. ## getUserRegistrationFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getUserRegistrationFunction( args: GetUserRegistrationFunctionArgs, deps?: GetUserRegistrationFunctionDeps, ): UserRegistrationFunction ``` Registers the caller on-chain. Registration is a multi-step idempotent flow that enables confidential token accounts (encrypted balances) and anonymous usage (mixer eligibility). Steps already completed in a previous session are skipped. *** ### GetUserRegistrationFunctionArgs * `client: IUmbraClient` - The configured client. *** ### GetUserRegistrationFunctionDeps All fields are optional overrides for SDK defaults. RPC overrides: * `accountInfoProvider?: AccountInfoProviderFunction` * `getLatestBlockhash?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` Key derivation overrides: * `userAccountX25519KeypairGenerator?: Curve25519KeypairGeneratorFunction` * `masterViewingKeyEncryptingX25519KeypairGenerator?: Curve25519KeypairGeneratorFunction` * `mintX25519KeypairGenerator?: MintX25519KeypairGeneratorFunction` * `masterViewingKeyGenerator?: MasterViewingKeyGeneratorFunction` * `masterViewingKeyBlindingFactorGenerator?: MasterViewingKeyBlindingFactorGeneratorFunction` * `poseidonPrivateKeyGenerator?: PoseidonPrivateKeyGeneratorFunction` * `poseidonBlindingFactorGenerator?: PoseidonBlindingFactorGeneratorFunction` * `rescueCommitmentBlindingFactorGenerator?: RescueEncryptionCommitmentBlindingFactorGeneratorFunction` * `randomFactorGenerator?: RandomFactorForPolynomialCommitmentGeneratorFunction` Rescue cipher overrides: * `getRcKeyGenerator?: (privateKey: X25519PrivateKey) => RcKeyGeneratorFunction` * `getRcEncryptor?: (privateKey: X25519PrivateKey) => RcEncryptorWithNonceFunction` * `rescueCommitmentGenerator?: RescueEncryptionCommitmentGeneratorFunction` Commitment overrides: * `userCommitmentGenerator?: UserCommitmentGeneratorFunction` Challenge / polynomial overrides: * `fiatShamirChallengeGenerator?: FiatShamirChallengeGeneratorFunction` * `challengePowersFunction?: ChallengePowersFunction` * `polynomialEvaluator?: PolynomialEvaluatorFunction` Poseidon / BN254 overrides: * `poseidonAggregator?: PoseidonAggregatorHashFunction` * `bn254ModInverter?: ModuleInvFunction` * `computeLimbwiseSumInverse?: (limbs: Base85LimbTuple) => Bn254FieldElement` ZK prover: * `zkProver?: IZkProverForUserRegistration` - ZK proof generator for the anonymous registration step. Required only when `options.anonymous` is `true`. Use `getUserRegistrationProver()` from `@umbra-privacy/web-zk-prover` — see [ZK Provers](/sdk/advanced/zk-provers). *** ### Returns `UserRegistrationFunction` ```typescript theme={null} type UserRegistrationFunction = ( options?: UserRegistrationOptions, ) => Promise ``` Returns the signatures of all transactions submitted during the registration flow. Already-completed steps produce no transactions. ### UserRegistrationOptions * `confidential?: boolean` - Register for confidential (encrypted) token account usage. Default: `true`. * `anonymous?: boolean` - Register for anonymous (mixer) usage. Requires a ZK proof. Default: `true`. * `callbacks?: UserRegistrationCallbacks` - Per-transaction lifecycle hooks. * `optionalData?: OptionalData32` - 32-byte caller metadata attached to on-chain instructions. *** ### Errors Throws `RegistrationError`. See [Errors](./errors#registrationerror) for all stages. ### Example ```typescript theme={null} import { getUserRegistrationFunction } from "@umbra-privacy/sdk"; import { getUserRegistrationProver } from "@umbra-privacy/web-zk-prover"; const zkProver = getUserRegistrationProver(); const register = getUserRegistrationFunction({ client }, { zkProver }); const signatures = await register({ confidential: true, anonymous: true }); ``` *** ## getRotateUserAccountX25519KeyFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getRotateUserAccountX25519KeyFunction( args: GetUserRegistrationFunctionArgs, deps?: GetUserRegistrationFunctionDeps, ): () => Promise ``` Rotates the X25519 public key stored in the on-chain user account. Use this when the existing key may have been compromised. Accepts the same `args` and `deps` as `getUserRegistrationFunction`. Returns a zero-argument function. Call it with no arguments to execute the rotation. *** ## getRotateMvkX25519KeyFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getRotateMvkX25519KeyFunction( args: GetUserRegistrationFunctionArgs, deps?: GetUserRegistrationFunctionDeps, ): () => Promise ``` Rotates the Master Viewing Key (MVK) X25519 encryption key. Accepts the same `args` and `deps` as `getUserRegistrationFunction`. Returns a zero-argument function. Call it with no arguments to execute the rotation. *** ## getUpdateRandomGenerationSeedFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getUpdateRandomGenerationSeedFunction( args: GetUpdateRandomGenerationSeedFunctionArgs, deps?: GetUpdateRandomGenerationSeedFunctionDeps, ): UpdateRandomGenerationSeedFunction ``` Updates the random generation seed stored in the on-chain user account. This seed contributes to commitment randomness in UTXO operations. ### GetUpdateRandomGenerationSeedFunctionArgs * `client: IUmbraClient` ### GetUpdateRandomGenerationSeedFunctionDeps * `blockhashProvider?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` ### Returns `UpdateRandomGenerationSeedFunction` ```typescript theme={null} type UpdateRandomGenerationSeedFunction = ( newSeed: Uint8Array, optionalData?: OptionalData32, callbacks?: TransactionCallbacks, ) => Promise ``` * `newSeed: Uint8Array` - The 32-byte replacement seed. *** ## getUpdateTokenAccountRandomGenerationSeedFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getUpdateTokenAccountRandomGenerationSeedFunction( args: GetUpdateTokenAccountRandomGenerationSeedFunctionArgs, deps?: GetUpdateTokenAccountRandomGenerationSeedFunctionDeps, ): UpdateTokenAccountRandomGenerationSeedFunction ``` Updates the random generation seed on a per-mint encrypted token account. ### GetUpdateTokenAccountRandomGenerationSeedFunctionArgs * `client: IUmbraClient` ### GetUpdateTokenAccountRandomGenerationSeedFunctionDeps * `blockhashProvider?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` ### Returns `UpdateTokenAccountRandomGenerationSeedFunction` ```typescript theme={null} type UpdateTokenAccountRandomGenerationSeedFunction = ( mint: Address, newSeed: Uint8Array, optionalData?: OptionalData32, callbacks?: TransactionCallbacks, ) => Promise ``` * `mint: Address` - The token mint whose account seed to update. * `newSeed: Uint8Array` - The 32-byte replacement seed. *** ## getClaimStagedSolFromPoolFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getClaimStagedSolFromPoolFunction( args: GetClaimStagedSolFromPoolFunctionArgs, deps?: GetClaimStagedSolFromPoolFunctionDeps, ): ClaimStagedSolFromPoolFunction ``` Recovers SOL that was staged in the protocol's unified fee pool during a failed MPC callback. Call this if a confidential operation's callback never landed and you need to reclaim the staged SOL. ### GetClaimStagedSolFromPoolFunctionArgs * `client: IUmbraClient` ### GetClaimStagedSolFromPoolFunctionDeps * `blockhashProvider?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` ### Returns `ClaimStagedSolFromPoolFunction` ```typescript theme={null} type ClaimStagedSolFromPoolFunction = ( mint: Address, amount: U64, destination: Address, optionalData?: OptionalData32, callbacks?: TransactionCallbacks, ) => Promise ``` * `mint: Address` - The mint of the failed operation. * `amount: U64` - The SOL amount (in lamports) to recover. * `destination: Address` - The SOL recipient account. *** ## getClaimStagedSplFromPoolFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getClaimStagedSplFromPoolFunction( args: GetClaimStagedSplFromPoolFunctionArgs, deps?: GetClaimStagedSplFromPoolFunctionDeps, ): ClaimStagedSplFromPoolFunction ``` Recovers SPL tokens that were staged in the protocol's unified fee pool during a failed MPC callback. ### GetClaimStagedSplFromPoolFunctionArgs * `client: IUmbraClient` ### GetClaimStagedSplFromPoolFunctionDeps * `blockhashProvider?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` ### Returns `ClaimStagedSplFromPoolFunction` ```typescript theme={null} type ClaimStagedSplFromPoolFunction = ( mint: Address, amount: U64, destinationAta: Address, optionalData?: OptionalData32, callbacks?: TransactionCallbacks, ) => Promise ``` * `mint: Address` - The token mint of the staged SPL tokens. * `amount: U64` - Token amount (in base units) to recover. * `destinationAta: Address` - The associated token account to receive the recovered tokens. *** ## RegistrationError Thrown by `getUserRegistrationFunction`. See [Errors](./errors#registrationerror). Stage values: `"initialization"` | `"master-seed-derivation"` | `"account-fetch"` | `"key-derivation"` | `"zk-proof-generation"` | `"pda-derivation"` | `"instruction-build"` | `"transaction-build"` | `"transaction-compile"` | `"transaction-sign"` | `"transaction-validate"` | `"transaction-send"` # Withdrawal Source: https://sdk.umbraprivacy.com/reference/withdraw API reference: getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction returns WithdrawResult with queueSignature and callbackSignature via Arcium MPC. ## getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction **Import:** `@umbra-privacy/sdk` ```typescript theme={null} function getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction( args: GetEncryptedBalanceToPublicBalanceDirectWithdrawerFunctionArgs, deps?: GetEncryptedBalanceToPublicBalanceDirectWithdrawerFunctionDeps, ): EncryptedBalanceToPublicBalanceDirectWithdrawerFunction ``` Withdraws tokens from the caller's encrypted balance to a public associated token account (ATA). This is the primary withdrawal path - no ZK proof required. The withdrawal queues an Arcium MPC computation; the Arcium network processes it and calls back on-chain to execute the transfer. Follows the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern). *** ### GetEncryptedBalanceToPublicBalanceDirectWithdrawerFunctionArgs * `client: IUmbraClient` *** ### GetEncryptedBalanceToPublicBalanceDirectWithdrawerFunctionDeps All fields are optional. * `accountInfoProvider?: AccountInfoProviderFunction` * `getLatestBlockhash?: GetLatestBlockhash` * `transactionForwarder?: TransactionForwarder` *** ### Returns `EncryptedBalanceToPublicBalanceDirectWithdrawerFunction` ```typescript theme={null} type EncryptedBalanceToPublicBalanceDirectWithdrawerFunction = ( destinationAddress: Address, mint: Address, withdrawalAmount: U64, options?: WithdrawOptions, ) => Promise ``` * `destinationAddress: Address` - The public ATA recipient. Must be an initialised token account for `mint`. * `mint: Address` - Token mint address. * `withdrawalAmount: U64` - Amount in base token units to withdraw from the encrypted balance. ### WithdrawOptions * `priorityFees?: U64` - Compute unit price in micro-lamports. Default: `0n`. * `purpose?: number` - Caller-defined purpose tag stored on-chain. Default: `0`. * `optionalData?: OptionalData32` - 32-byte caller metadata. Default: 32 zero bytes. * `awaitCallback?: boolean` - Whether to wait for the MPC callback. Default: `true`. * `skipPreflight?: boolean` - Skip Solana preflight simulation. Default: `false`. * `maxRetries?: number` - Max RPC retry attempts for transaction sending. * `accountInfoCommitment?: Commitment` - Per-call commitment for RPC account reads. Default: `"confirmed"`. * `epochInfoCommitment?: Commitment` - Per-call commitment for epoch info fetches. Default: `"confirmed"`. ### WithdrawResult ```typescript theme={null} interface WithdrawResult { queueSignature: TransactionSignature; callbackStatus?: "finalized" | "pruned" | "timed-out"; callbackSignature?: TransactionSignature; callbackElapsedMs?: number; rentClaimSignature?: TransactionSignature; rentClaimError?: string; } ``` * `queueSignature` - Signature of the handler (queue computation) transaction. * `callbackStatus` - The outcome of computation monitoring: `"finalized"`, `"pruned"`, or `"timed-out"`. Present when `awaitCallback` is `true`. * `callbackSignature` - Signature of the Arcium MPC callback. Present when `callbackStatus` is `"finalized"`. * `callbackElapsedMs` - Milliseconds spent waiting for the callback. * `rentClaimSignature` - Signature for reclaiming rent from the computation account. Attempted regardless of callback outcome. * `rentClaimError` - Error from rent reclaim attempt, if any. The withdrawal itself still succeeded. *** ### Errors Throws `EncryptedWithdrawalError`. See [Errors](./errors#encryptedwithdrawalerror). *** ### Example ```typescript theme={null} import { getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction } from "@umbra-privacy/sdk"; const withdraw = getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction({ client }); const result = await withdraw( destinationAta, // public ATA to receive tokens mintAddress, 500_000n, // amount in base units ); console.log("Queue signature:", result.queueSignature); ``` *** ## EncryptedWithdrawalError Thrown by `getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction`. Stage values: `"initialization"` | `"validation"` | `"mint-fetch"` | `"pda-derivation"` | `"instruction-build"` | `"transaction-build"` | `"transaction-compile"` | `"transaction-sign"` | `"transaction-send"` See [Errors](./errors#encryptedwithdrawalerror) for full documentation. # Claim Status Source: https://sdk.umbraprivacy.com/relayer/api-reference/claim-status openapi-relayer.yaml GET /v1/claims/{request_id} Poll the status of a submitted claim request. The SDK polls this endpoint automatically with a 3-second interval and a 120-second timeout. You only need to call it directly if building a custom integration. ## Polling Recommendations * Poll every **3 seconds** (the SDK default) * Stop polling when status reaches a terminal state: `completed`, `failed`, or `timed_out` * Maximum recommended polling duration: **120 seconds** * If status is `timed_out`, the MPC callback was not received within the expected block window — the callback may still arrive later ## Claim Lifecycle A claim request progresses through these statuses in order: * `received` — Accepted by the relayer API * `validating` — Preflight checks running (Merkle root, nullifiers, account state) * `offsets_reserved` — Nullifier offsets reserved for deduplication * `building_tx` — Transactions being constructed * `tx_built` — Transactions ready * `submitting` — Sending to Solana * `submitted` — Confirmed on-chain * `awaiting_callback` — Waiting for Arcium MPC callback * `callback_received` — MPC callback detected * `finalizing` — Running cleanup transactions * `completed` — Claim successful * `failed` — Error occurred (see `failure_reason` field) * `timed_out` — MPC callback deadline exceeded # Relayer Health Source: https://sdk.umbraprivacy.com/relayer/api-reference/health openapi-relayer.yaml GET /v1/health Check if the relayer service is operational. Used by load balancers and monitoring systems to verify the relayer is running. # Relayer Info Source: https://sdk.umbraprivacy.com/relayer/api-reference/info openapi-relayer.yaml GET /v1/relayer/info Get relayer identity, supported mints, and active stealth pool indices. Use this endpoint to verify the relayer is operational and supports the mint you want to claim. The SDK calls this internally via `getUmbraRelayer()` to cache the relayer's fee payer address. # Submit Claim Source: https://sdk.umbraprivacy.com/relayer/api-reference/submit-claim openapi-relayer.yaml POST /v1/claims Submit a UTXO claim request to the relayer for processing. This endpoint is designed to be called by the SDK's claim functions, not directly. The request body contains cryptographic proof data that must be generated correctly by the ZK prover. Use `getSelfClaimableUtxoToEncryptedBalanceClaimerFunction` or one of the other claim factory functions instead. The relayer validates the request, builds the on-chain transactions, and submits them asynchronously. Poll `GET /v1/claims/{request_id}` with the returned UUID to track progress. ## Claim Variants * `encrypted_balance` — Claim into an Encrypted Token Account (ETA). Requires additional Rescue-encrypted fee fields in `proof_account_data`. Each UTXO slot has 6 linker encryptions and 6 key commitments. * `public_balance` — Claim into a public Associated Token Account (ATA). Requires `fee_proof_data` with Merkle proofs for fee schedules. Each UTXO slot has 5 linker encryptions and 5 key commitments. ## Validation Rules * All base58 pubkeys must decode to exactly 32 bytes * All nullifiers within a request must be unique (duplicates return `409 DUPLICATE_OFFSET`) * `max_utxo_capacity` must be greater than 0 * `fee_proof_data` is required for `public_balance` variant only # Relayer Overview Source: https://sdk.umbraprivacy.com/relayer/overview Claim relay at relayer.api.umbraprivacy.com and relayer.api-devnet.umbraprivacy.com. Async POST /v1/claims, poll GET /v1/claims/{id}. Lifecycle: received to completed. SDK uses via getUmbraRelayer. ## What is the Relayer? The Umbra relayer is a transaction submission service that builds, signs, and submits claim transactions for users. When you claim a UTXO from the mixer, the relayer pays the Solana transaction fees and submits the claim on your behalf — ensuring your wallet address never appears as the fee payer in the transaction, preserving your privacy. The relayer is used internally by the SDK's claim functions. You do not need to interact with the relayer API directly unless you are building a custom integration. ## Base URL * **Mainnet:** `https://relayer.api.umbraprivacy.com` * **Devnet:** `https://relayer.api-devnet.umbraprivacy.com` ## How It Works 1. You call a claim function in the SDK (e.g., `getSelfClaimableUtxoToEncryptedBalanceClaimerFunction`) 2. The SDK constructs the claim proof data and UTXO slot data locally 3. The SDK submits a claim request to the relayer via `POST /v1/claims` 4. The relayer validates the request, builds the on-chain transactions, and submits them 5. The SDK polls `GET /v1/claims/{request_id}` until the claim reaches a terminal status 6. The claimed tokens appear in your encrypted balance or public ATA ## SDK Integration ```typescript theme={null} import { getUmbraRelayer } from "@umbra-privacy/sdk"; const relayer = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com", }); ``` The relayer object is passed as a dependency to claim factory functions: ```typescript theme={null} const claim = getSelfClaimableUtxoToEncryptedBalanceClaimerFunction( { client }, { zkProver, relayer }, ); ``` ## Claim Lifecycle A claim request progresses through these statuses: * `received` — Accepted by the relayer API * `validating` — Preflight checks running (Merkle root, nullifiers, account state) * `offsets_reserved` — Nullifier offsets reserved for deduplication * `building_tx` — Transactions being constructed * `tx_built` — Transactions ready * `submitting` — Sending to Solana * `submitted` — Confirmed on-chain * `awaiting_callback` — Waiting for Arcium MPC callback * `callback_received` — MPC callback detected * `finalizing` — Running cleanup transactions * `completed` — Claim successful * `failed` — Error occurred (see `failure_reason`) * `timed_out` — MPC callback deadline exceeded Terminal statuses: `completed`, `failed`, `timed_out` ## Response Format All relayer endpoints return JSON. ## Rate Limiting All endpoints are subject to rate limiting. Exceeded limits return `429 Too Many Requests`. # Account State Source: https://sdk.umbraprivacy.com/sdk/account-state getUserAccountQuerierFunction: read EncryptedUserAccount fields (X25519 key, user commitment, generation index, registration steps) for any address without submitting transactions. ## Checking Registration Status Use `getUserAccountQuerierFunction` to inspect an account's current registration state without triggering any transactions: ```typescript theme={null} import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk"; const query = getUserAccountQuerierFunction({ client }); const result = await query(client.signer.address); if (result.state === "non_existent") { console.log("Not registered - call register() first"); } else { const { data } = result; console.log("Account initialized:", data.isInitialised); console.log("X25519 key registered:", data.isUserAccountX25519KeyRegistered); console.log("Commitment registered:", data.isUserCommitmentRegistered); console.log("Anonymous usage active:", data.isActiveForAnonymousUsage); } ``` ## When to Register Register once, at account setup time. Check state before calling `register()` so you avoid unnecessary transaction prompts for users who are already fully set up: ```typescript theme={null} const query = getUserAccountQuerierFunction({ client }); const result = await query(client.signer.address); const isFullyRegistered = result.state === "exists" && result.data.isUserAccountX25519KeyRegistered && result.data.isUserCommitmentRegistered; if (!isFullyRegistered) { const register = getUserRegistrationFunction({ client }); await register({ confidential: true, anonymous: true }); } ``` ## Registration State Fields The `data` object returned when `result.state === "exists"`: * `isInitialised` - the base `EncryptedUserAccount` PDA has been created (Step 1 complete). * `isUserAccountX25519KeyRegistered` - the X25519 public key has been stored on-chain (Step 2 complete). Required for confidential deposits. * `isUserCommitmentRegistered` - the Poseidon user commitment has been stored (Step 3 complete). Required for the mixer. * `isActiveForAnonymousUsage` - the account is active and cleared for anonymous usage (both Step 2 and Step 3 complete and valid). * `x25519PublicKey` - the registered X25519 public key bytes, if Step 2 is complete. * `userCommitment` - the registered Poseidon commitment, if Step 3 is complete. * `generationIndex` - monotonic counter used for nonce derivation. * `randomGenerationSeed` - entropy bytes mixed into nonces. For the full type reference see [Query](/reference/query). # Compliance Source: https://sdk.umbraprivacy.com/sdk/compliance Two independent compliance mechanisms: hierarchical viewing keys (MVK + Poseidon-derived temporal keys) for mixer UTXOs, and X25519 compliance grants for encrypted balance re-encryption. ## Overview Umbra's compliance system is entirely voluntary and user-initiated. Two independent mechanisms cover different parts of the protocol: Derive scoped cryptographic keys from your master viewing key and share them with auditors. Viewing keys give read access to mixer pool (UTXO) activity within a specific time window or token scope - without exposing anything outside that scope. Create on-chain grants that authorize Arcium MPC to re-encrypt your encrypted token account balances under a grantee's X25519 key. The grantee can then decrypt and read the re-encrypted ciphertexts using their own private key. No party can access your data without your explicit action. Neither mechanism is reversible for data the grantee has already received - revoking a grant prevents future access but does not invalidate ciphertexts already re-encrypted. *** ## Mixer Pool Viewing Keys The **master viewing key** (MVK) is a BN254 field element derived from your master seed. It is the root of a key hierarchy you can share selectively: ``` Master Viewing Key ├── Mint Viewing Key { mint } │ ├── Yearly Viewing Key { year } │ │ ├── Monthly Viewing Key { year, month } │ │ │ └── Daily Viewing Key { year, month, day } ``` Each level of the hierarchy is derived from the level above using the Poseidon hash function. Sharing a key at any level grants read access to all UTXO activity within that scope - and nothing outside it. ```typescript theme={null} // Derive and share a monthly viewing key for February 2025 const monthlyVk = await client.monthlyViewingKey.generate(2025, 2); // Export as hex for out-of-band sharing const hex = Buffer.from(monthlyVk).toString("hex"); ``` See [Mixer Pool Viewing Keys](/sdk/compliance-viewing-keys) for full derivation details, export formats, and how auditors use these keys. *** ## X25519 Based Compliance Grants X25519 compliance grants use on-chain PDAs to authorize Arcium MPC to re-encrypt your encrypted token account ciphertexts under a grantee's X25519 key. You create the grant voluntarily, choose the receiver and a unique nonce, and can revoke it at any time by deleting the PDA. ```typescript theme={null} import { getComplianceGrantIssuerFunction } from "@umbra-privacy/sdk"; const createGrant = getComplianceGrantIssuerFunction({ client }); // Create a grant allowing `receiver` to re-encrypt your shared ciphertexts const signature = await createGrant( receiver, // receiver's wallet address (Address) granterX25519, // your MVK X25519 public key (X25519PublicKey) receiverX25519, // receiver's X25519 public key (X25519PublicKey) nonce, // u128 nonce (RcEncryptionNonce) ); ``` See [X25519 Compliance Grants](/sdk/compliance-x25519-grants) for the complete workflow including how to derive the required keys, generate nonces, re-encrypt ciphertexts, and query grant status. *** ## Privacy and Trust Model * Compliance grants are stored on-chain as marker PDAs - their existence is transparent and auditable * A viewing key at scope X gives access only to data within scope X - no access to anything outside that scope * Revoking a compliance grant (deleting the PDA) prevents future re-encryption requests, but does not invalidate ciphertexts the grantee has already obtained * The master viewing key and master seed are never shared directly through either mechanism # Mixer Pool Viewing Keys Source: https://sdk.umbraprivacy.com/sdk/compliance-viewing-keys MVK hierarchy: master viewing key (BN254) derived via KMAC256, Poseidon-derived temporal and per-mint scoped keys. Grant auditors read access to specific mixer UTXO time windows or tokens. ## What Are Viewing Keys? The **master viewing key** (MVK) is a 252-bit BN254 field element derived deterministically from your master seed. It sits at the root of a hierarchical key system that lets you grant scoped read access to your mixer pool activity without ever exposing the root key or any data outside the defined scope. Viewing keys are designed for compliance and audit use cases: * Provide an auditor or regulator access to transactions within a specific time window * Grant a compliance officer access to all activity for a specific token * Share full visibility at the master level with a tax advisor or legal counterparty * Prove to an exchange that specific UTXOs originated from you Viewing keys are read-only credentials. Possessing a viewing key does not allow spending UTXOs or withdrawing balances. They can only be used to decrypt ciphertext data that was encrypted under the associated key. ## The Viewing Key Hierarchy All keys in the hierarchy are BN254 field elements. Each level is derived from its parent using the Poseidon hash function. A key at any level grants access to all data within its scope - and nothing above or adjacent to it in the tree. ``` Master Viewing Key └── Mint Viewing Key { e.g. USDC · USDT · wSOL · ... } └── Yearly Viewing Key { e.g. 2024 · 2025 · ... } ├── Monthly Viewing Key { Jan } │ ├── Daily Viewing Key { Jan 01 } │ │ ├── Hourly Viewing Key { 00h } │ │ │ ├── Minute Viewing Key { 00m } │ │ │ │ └── Second Viewing Key { 00s } │ │ │ └── ... │ │ └── ... │ └── ... ├── Monthly Viewing Key { Feb } ├── Monthly Viewing Key { Mar } └── ... { Apr – Dec } ``` **Example scopes:** * Monthly key for `2025-02`: sees all USDC mixer activity in February 2025 * Yearly key for `2025`: sees all USDC mixer activity across all of 2025 * Mint key for USDC: sees all USDC mixer activity, all time * Master viewing key: sees everything across all tokens and all time ## Deriving Viewing Keys Each level of the hierarchy has a corresponding deriver factory function. Pass `{ client }` to get a deriver, then call it with the appropriate parameters. ### Master Viewing Key ```typescript theme={null} import { getMasterViewingKeyDeriver } from "@umbra-privacy/sdk"; // Derives the root of the hierarchy const deriveMvk = getMasterViewingKeyDeriver({ client }); const mvk = await deriveMvk(); // mvk is a bigint (BN254 field element) console.log("Master Viewing Key:", mvk.toString()); ``` The master viewing key gives unrestricted read access to all your mixer activity - past and future. Share it only with fully trusted parties. For audits with a limited scope, always use a scoped sub-key. ### Mint Viewing Key ```typescript theme={null} import { getMintViewingKeyDeriver } from "@umbra-privacy/sdk"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // Derives the mint-specific viewing key const deriveMintVk = getMintViewingKeyDeriver({ client }); const mintVk = await deriveMintVk(USDC); // Scope: all time, but only USDC activity console.log("USDC Viewing Key:", mintVk.toString()); ``` ### Yearly Viewing Key ```typescript theme={null} import { getYearlyViewingKeyDeriver } from "@umbra-privacy/sdk"; import type { Year } from "@umbra-privacy/sdk/types"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // Derives the yearly viewing key for 2025 under the USDC mint key const deriveYearlyVk = getYearlyViewingKeyDeriver({ client }); const yearlyVk = await deriveYearlyVk(USDC, 2025n as Year); // Scope: all USDC activity in 2025 only console.log("2025 USDC Viewing Key:", yearlyVk.toString()); ``` ### Monthly Viewing Key ```typescript theme={null} import { getMonthlyViewingKeyDeriver } from "@umbra-privacy/sdk"; import type { Year, Month } from "@umbra-privacy/sdk/types"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // Derives the monthly viewing key for February 2025 const deriveMonthlyVk = getMonthlyViewingKeyDeriver({ client }); const monthlyVk = await deriveMonthlyVk(USDC, 2025n as Year, 2n as Month); // Scope: USDC activity in February 2025 only console.log("Feb 2025 USDC Viewing Key:", monthlyVk.toString()); ``` ### Daily Viewing Key ```typescript theme={null} import { getDailyViewingKeyDeriver } from "@umbra-privacy/sdk"; import type { Year, Month, Day } from "@umbra-privacy/sdk/types"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // Derives the daily viewing key for February 14, 2025 const deriveDailyVk = getDailyViewingKeyDeriver({ client }); const dailyVk = await deriveDailyVk(USDC, 2025n as Year, 2n as Month, 14n as Day); // Scope: USDC activity on Feb 14, 2025 only console.log("Feb 14 2025 USDC Viewing Key:", dailyVk.toString()); ``` ## Exporting and Sharing a Viewing Key Viewing keys are `bigint` values (BN254 field elements). Export them as decimal or hex strings for out-of-band sharing with an auditor: ```typescript theme={null} // Export as decimal string (lossless, unambiguous) function exportViewingKey(key: bigint): string { return key.toString(); } // Export as hex string (compact) function exportViewingKeyHex(key: bigint): string { return "0x" + key.toString(16).padStart(64, "0"); } // Import back from decimal string function importViewingKey(decimal: string): bigint { return BigInt(decimal); } // Import back from hex string function importViewingKeyHex(hex: string): bigint { return BigInt(hex); } ``` ### Sharing Example ```typescript theme={null} import { getMonthlyViewingKeyDeriver } from "@umbra-privacy/sdk"; import type { Year, Month } from "@umbra-privacy/sdk/types"; // 1. Choose a scope - e.g., monthly key for an audit of Q1 2025 const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const deriveMonthlyVk = getMonthlyViewingKeyDeriver({ client }); const q1Keys = await Promise.all([ deriveMonthlyVk(USDC, 2025n as Year, 1n as Month), // January deriveMonthlyVk(USDC, 2025n as Year, 2n as Month), // February deriveMonthlyVk(USDC, 2025n as Year, 3n as Month), // March ]); // 2. Export as strings const q1Strings = q1Keys.map((key) => key.toString()); // 3. Share with auditor out-of-band (email, secure document, etc.) const auditPackage = { granterAddress: client.signer.address, token: USDC, period: "Q1 2025", viewingKeys: { "2025-01": q1Strings[0], "2025-02": q1Strings[1], "2025-03": q1Strings[2], }, }; console.log(JSON.stringify(auditPackage, null, 2)); ``` ## How Key Derivation Works Viewing keys are derived using the Poseidon hash function over BN254. Starting from the master viewing key: ``` MintViewingKey(mint) = Poseidon(MVK, mint_low_u128, mint_high_u128) YearlyViewingKey(mint, year) = Poseidon(MintViewingKey(mint), year) MonthlyViewingKey(mint, year, month) = Poseidon(YearlyViewingKey(mint, year), month) DailyViewingKey(mint, year, month, day) = Poseidon(MonthlyViewingKey(mint, year, month), day) ``` The mint address is split into two 128-bit halves before being passed to Poseidon, as BN254 field elements are 252-bit values and a 256-bit pubkey would exceed the field size. This structure means: * You cannot derive a parent key from a child key - the tree is one-directional * Child keys can always be re-derived from the parent at any time * Two separate mint keys cannot be linked without the master viewing key ## Security Properties * **One-directional** - a grantee who holds a monthly key cannot derive the yearly key, the master viewing key, or any key for another mint or time period * **Unlinkable** - viewing keys for different months or mints are computationally unrelated without the master key * **Deterministic** - the same wallet on the same network always produces the same viewing key hierarchy; keys can be re-derived at any time * **Non-transactional** - sharing a viewing key does not create any on-chain state; it is a purely off-chain credential ## What Auditors Can Do With a Viewing Key An auditor who holds a viewing key can: * Scan the mixer pool (Indexed Merkle Tree) for UTXOs that were encrypted under the corresponding key * Decrypt the UTXO payloads to learn the amount and recipient * Verify that specific transactions occurred within the defined scope An auditor cannot: * Access any data outside the scope of the key they hold * Claim UTXOs (viewing keys are read-only, not spending keys) * Link the activity to any adjacent scope without the parent key SDK utilities for auditors to scan the mixer pool and decrypt UTXO ciphertexts using viewing keys are on the roadmap. The key generation and export functionality described on this page is available today. # X25519 Compliance Grants Source: https://sdk.umbraprivacy.com/sdk/compliance-x25519-grants getComplianceGrantIssuerFunction and getComplianceGrantRevokerFunction: create/revoke on-chain PDA grants that authorize Arcium MPC to re-encrypt ETA ciphertexts under a grantee's X25519 key. ## How X25519 Compliance Grants Work Encrypted token account balances in Umbra are stored as ciphertexts on-chain. By default, only you can read your own balance - Arcium MPC will not re-encrypt your ciphertexts for a third party without explicit authorization. An **X25519 compliance grant** is an on-chain PDA that grants that authorization. Once the PDA exists, Arcium MPC is permitted to re-encrypt your ciphertexts under the grantee's X25519 key. The grantee can then decrypt the output locally using their own private key - without you needing to be online. ``` Without grant: Ciphertext (on-chain) ──► [grantee has no access - MPC will not re-encrypt] With X25519 compliance grant: Ciphertext (on-chain) ──► Arcium MPC re-encrypts ──► Re-encrypted under grantee's key └─► Grantee decrypts locally ``` The grant PDA is a **marker account** - its presence on-chain is the authorization. No data is stored in it beyond the discriminator. Once a re-encryption instruction is executed, the grantee receives a ciphertext encrypted under their own X25519 key. **They hold this permanently.** More critically: because Rescue is a stream cipher, possessing a re-encrypted ciphertext for a given nonce allows the grantee to derive the full keystream for that nonce. This means **all past and future encryptions produced under the same nonce are also permanently readable by the grantee** - not just the single ciphertext that was re-encrypted. Revoking the grant stops future re-encryption requests but cannot undo this. Treat each nonce as a scope of permanent disclosure once any re-encryption instruction for it has been executed. *** ## Prerequisites ### 1. Your X25519 Public Key (Granter Key) For user-granted compliance grants, the `granterX25519` parameter is **your MVK (master viewing key) X25519 public key** - the Curve25519 key derived from your master seed's MVK X25519 keypair. This is distinct from the user account X25519 key used for encrypting token balances. The MVK X25519 key is used to prove ownership of the master viewing key during grant creation. The SDK derives it internally via the `masterViewingKeyX25519KeypairGenerator` dependency. You can derive and read your MVK X25519 public key from the client: ```typescript theme={null} import { getMasterViewingKeyX25519KeypairGenerator } from "@umbra-privacy/sdk"; // The SDK derives this internally during grant creation. // You can also derive it explicitly if you need to share it with a grantee // so they can look up the grant PDA. const generateMvkKeypair = getMasterViewingKeyX25519KeypairGenerator({ client }); const mvkKeypairResult = await generateMvkKeypair(); const granterX25519 = mvkKeypairResult.x25519Keypair.publicKey; ``` ### 2. Receiver's X25519 Public Key The `receiverX25519` is the grantee's X25519 public key. This is the key that Arcium MPC will re-encrypt the ciphertext under. The receiver derives it from their own master seed. The receiver can share their X25519 public key with you out-of-band, or you can look it up from their registered user account: ```typescript theme={null} import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk"; const queryAccount = getUserAccountQuerierFunction({ client }); const receiverAccount = await queryAccount(receiverAddress); if (receiverAccount.state !== "exists" || !receiverAccount.data.isUserAccountX25519KeyRegistered) { throw new Error("Receiver has not registered an X25519 key"); } // receiverX25519 is a Uint8Array (32 bytes) const receiverX25519 = receiverAccount.data.x25519PublicKey; ``` ### 3. Nonce A compliance grant authorizes re-encryption of any Rescue cipher encryption scoped to a specific **X25519 public key + nonce** combination. Only ciphertexts encrypted under that exact pubkey and nonce are covered - anything encrypted under a different nonce is outside the scope of the grant. The nonce is also part of the on-chain PDA seed, so multiple independent grants (e.g. to different parties or different nonces) can coexist simultaneously. Generate a random nonce using the SDK's utility: ```typescript theme={null} import { generateRandomNonce } from "@umbra-privacy/sdk/utils"; const nonce = generateRandomNonce(); // Random u128 bigint ``` Store the nonce - you will need it to delete the grant later, to look up the PDA, and to pass as the `inputEncryptionNonce` when triggering re-encryption. Because Rescue is a stream cipher, a grantee who obtains a re-encrypted ciphertext for a given nonce can derive the keystream for that nonce and read all encryptions produced under it - not just the one that was re-encrypted. Use a fresh nonce for each grant and each disclosure scope. Never reuse a nonce across grants you intend to keep independent. *** ## Creating a User-Granted Compliance Grant ```typescript theme={null} import { getComplianceGrantIssuerFunction, getMasterViewingKeyX25519KeypairGenerator, } from "@umbra-privacy/sdk"; import { generateRandomNonce } from "@umbra-privacy/sdk/utils"; const createGrant = getComplianceGrantIssuerFunction({ client }); // Gather required keys const generateMvkKeypair = getMasterViewingKeyX25519KeypairGenerator({ client }); const mvkKeypairResult = await generateMvkKeypair(); const granterX25519 = mvkKeypairResult.x25519Keypair.publicKey; const nonce = generateRandomNonce(); // Create the on-chain grant PDA const signature = await createGrant( receiver, // Address - receiver's wallet address granterX25519, // X25519PublicKey - your MVK X25519 public key receiverX25519, // X25519PublicKey - receiver's X25519 public key nonce, // RcEncryptionNonce - 16-byte u128 ); console.log("Grant created:", signature); console.log("Save this nonce for later revocation:", nonce.toString()); ``` The transaction includes an Ed25519 signature over the grant parameters, produced using your MVK X25519 keypair's Ed25519 component. This proves to the on-chain program that the granter controls the master viewing key - without revealing the key itself. *** ## Querying a Grant Check whether a grant is active before attempting re-encryption: ```typescript theme={null} import { getUserComplianceGrantQuerierFunction } from "@umbra-privacy/sdk"; const queryGrant = getUserComplianceGrantQuerierFunction({ client }); const result = await queryGrant( granterX25519, // X25519PublicKey - granter's MVK X25519 key nonce, // RcEncryptionNonce - the same nonce used at creation receiverX25519, // X25519PublicKey - receiver's X25519 key ); if (result.state === "exists") { console.log("Grant is active - re-encryption is authorized"); } else { console.log("Grant not found - either never created or already revoked"); } ``` The grant PDA is derived deterministically from these three values, so the query is a pure account existence check with no network round-trip beyond the RPC call. *** ## Re-Encrypting Ciphertexts (Grantee Workflow) Once the grant is active, the **grantee** calls the re-encryption function. This triggers an Arcium MPC computation that decrypts the granter's Shared-mode ciphertexts and re-encrypts them under the receiver's X25519 key. ```typescript theme={null} import { getSharedCiphertextReencryptorForUserGrantFunction } from "@umbra-privacy/sdk"; // The grantee calls this (using their own client, not the granter's) const reencrypt = getSharedCiphertextReencryptorForUserGrantFunction({ client: granteeClient }); const signature = await reencrypt( granterX25519Key, // X25519PublicKey - granter's MVK X25519 key receiverX25519Key, // X25519PublicKey - receiver's (grantee's own) X25519 key nonce, // RcEncryptionNonce - the nonce used when creating the grant inputEncryptionNonce, // RcEncryptionNonce - the nonce of the ciphertext to re-encrypt ciphertexts, // readonly Uint8Array[] - 1 to 6 ciphertexts (32 bytes each) ); console.log("Re-encryption triggered:", signature); ``` ### Parameters The granter's MVK X25519 public key. Must match the key used when the grant was created. The receiver's (grantee's) X25519 public key. Arcium MPC will re-encrypt the output under this key. The 128-bit nonce from the grant creation. Used to locate the grant PDA on-chain. The nonce of the specific ciphertext you want re-encrypted. This is the nonce associated with the encrypted token account state at a specific point in time. An array of 1 to 6 ciphertext values (32 bytes each). Unused slots are padded with zero bytes. These are the raw encrypted balance ciphertexts fetched from the on-chain encrypted token account. Re-encryption follows the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern). The SDK submits the handler, waits for Arcium MPC to produce the re-encrypted output, then waits for the callback to confirm. The re-encrypted ciphertext is written on-chain in an MPC callback data PDA. *** ## Revoking a Grant Delete the grant PDA to prevent future re-encryption requests. The grant parameters must exactly match the original creation. ```typescript theme={null} import { getComplianceGrantRevokerFunction } from "@umbra-privacy/sdk"; const deleteGrant = getComplianceGrantRevokerFunction({ client }); const signature = await deleteGrant( receiver, // Address - receiver's wallet address granterX25519, // X25519PublicKey - your MVK X25519 key receiverX25519, // X25519PublicKey - receiver's X25519 key nonce, // RcEncryptionNonce - must be the original nonce ); console.log("Grant revoked:", signature); ``` Deleting the grant closes the PDA and returns the rent lamports to the fee payer. Revoking a grant stops future re-encryption requests but does not affect anything the grantee has already received. Any ciphertext they obtained before revocation remains permanently accessible to them - there is no mechanism to claw it back. Revocation is a forward-only control. *** ## Full End-to-End Workflow Example ```typescript theme={null} import { getComplianceGrantIssuerFunction, getComplianceGrantRevokerFunction, getUserComplianceGrantQuerierFunction, getUserAccountQuerierFunction, getMasterViewingKeyX25519KeypairGenerator, } from "@umbra-privacy/sdk"; import { generateRandomNonce } from "@umbra-privacy/sdk/utils"; // === GRANTER SIDE === // 1. Derive your MVK X25519 public key const generateMvkKeypair = getMasterViewingKeyX25519KeypairGenerator({ client: granterClient }); const mvkKeypairResult = await generateMvkKeypair(); const granterX25519 = mvkKeypairResult.x25519Keypair.publicKey; // 2. Look up the receiver's X25519 key const RECEIVER_ADDRESS = "GsbwXfJraMomNxBcpR3DBFyKCCmN9SKGzKFJBNKxRFkT"; const queryAccount = getUserAccountQuerierFunction({ client: granterClient }); const receiverAccount = await queryAccount(RECEIVER_ADDRESS); if (receiverAccount.state !== "exists") { throw new Error("Receiver is not registered"); } const receiverX25519 = receiverAccount.data.x25519PublicKey; // 3. Generate a nonce and save it const nonce = generateRandomNonce(); console.log("Nonce (save this):", nonce.toString()); // 4. Create the on-chain grant const createGrant = getComplianceGrantIssuerFunction({ client: granterClient }); const grantSig = await createGrant( RECEIVER_ADDRESS, granterX25519, receiverX25519, nonce, ); console.log("Grant created:", grantSig); // === GRANTEE SIDE === // (The grantee now calls re-encryption using their own client) // 5. (Optional) Grantee verifies the grant is active const queryGrant = getUserComplianceGrantQuerierFunction({ client: granteeClient }); const grantStatus = await queryGrant(granterX25519, nonce, receiverX25519); console.log("Grant active:", grantStatus.state === "exists"); // 6. Grantee triggers re-encryption of the granter's ciphertexts // (They provide: the ciphertexts from the on-chain encrypted balance account, the input nonce, etc.) // const reencrypt = getSharedCiphertextReencryptorForUserGrantFunction({ client: granteeClient }); // const reencryptSig = await reencrypt(granterX25519, receiverX25519, nonce, inputNonce, ciphertexts); // === GRANTER SIDE - Revoking === // 7. Later: revoke the grant const deleteGrant = getComplianceGrantRevokerFunction({ client: granterClient }); const deleteSig = await deleteGrant(RECEIVER_ADDRESS, granterX25519, receiverX25519, nonce); console.log("Grant revoked:", deleteSig); ``` *** ## PDA Structure For reference, the grant PDA is derived from these seeds (using Arcium-encoded byte representations): ``` seeds = [ SHA256("ArciumComplianceGrant"), SHA256("UserGrant"), granterX25519 (Arcium-encoded, 32 bytes), nonce (Arcium-encoded u128, 16 bytes), receiverX25519 (Arcium-encoded, 32 bytes), ] ``` The PDA's existence is the authorization - it contains no data beyond the account discriminator. # Conversion Source: https://sdk.umbraprivacy.com/sdk/conversion getNetworkEncryptionToSharedEncryptionConverterFunction: upgrade MXE-only ETAs to Shared mode. getMintEncryptionKeyRotatorFunction: rotate per-mint X25519 encryption keys. ## Overview When a user registers their X25519 key after already having encrypted token accounts in MXE-only mode, those existing balances remain in MXE-only mode. `getNetworkEncryptionToSharedEncryptionConverterFunction` upgrades them to **Shared mode** - re-encrypting the balance under both the Arcium MPC key and the user's X25519 key, enabling local balance queries in the future. This step is only necessary if you deposited tokens before registering your X25519 key (i.e., before calling `register({ confidential: true })`). Deposits made after X25519 registration automatically use Shared mode. ## Usage ```typescript theme={null} import { getNetworkEncryptionToSharedEncryptionConverterFunction } from "@umbra-privacy/sdk"; const convert = getNetworkEncryptionToSharedEncryptionConverterFunction({ client }); const result = await convert(mints, optionalData?, callbacks?); ``` ### Parameters An array of SPL or Token-2022 mint addresses to convert. Only accounts in MXE-only mode with an initialized balance will be converted. Others are automatically skipped and reported in `result.skipped`. 32 bytes of optional metadata stored with each conversion transaction. Defaults to all zeros. Optional lifecycle hooks fired before and after each per-mint conversion transaction. ### Return Value ```typescript theme={null} type ConvertToSharedEncryptionResult = { converted: Map; skipped: Map; }; ``` * `converted` - mints that were successfully upgraded, mapped to their transaction signature * `skipped` - mints that were not processed, mapped to the reason they were skipped Skip reasons: * `"non_existent"` - No encrypted token account exists for this mint * `"not_initialised"` - The token account exists but is not initialized * `"already_shared"` - The token account is already in Shared mode (no-op) * `"balance_not_initialised"` - The Arcium balance has not been initialized yet ## Example ```typescript theme={null} import { getNetworkEncryptionToSharedEncryptionConverterFunction } from "@umbra-privacy/sdk"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const USDT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"; const convert = getNetworkEncryptionToSharedEncryptionConverterFunction({ client }); const result = await convert([USDC, USDT]); for (const [mint, signature] of result.converted) { console.log(`Converted ${mint}: ${signature}`); } for (const [mint, reason] of result.skipped) { console.log(`Skipped ${mint}: ${reason}`); } ``` ## Rotate Mint Encryption Key If a per-mint X25519 key may have been compromised, use `getMintEncryptionKeyRotatorFunction` to rotate it: ```typescript theme={null} import { getMintEncryptionKeyRotatorFunction } from "@umbra-privacy/sdk"; const rotateMintKey = getMintEncryptionKeyRotatorFunction({ client }); const signature = await rotateMintKey(mint); ``` The account must already be in Shared mode. See [Key Rotation](/sdk/understanding-the-sdk/key-rotation) for details on when and how to rotate keys. ## When to Call This A typical onboarding flow checks whether conversion is needed after completing registration: ```typescript theme={null} import { getUserRegistrationFunction, getNetworkEncryptionToSharedEncryptionConverterFunction, getEncryptedBalanceQuerierFunction, } from "@umbra-privacy/sdk"; // 1. Register (ensures X25519 key is on-chain) const register = getUserRegistrationFunction({ client }); await register({ confidential: true, anonymous: true }); // 2. Check which mints need conversion const queryBalance = getEncryptedBalanceQuerierFunction({ client }); const MINTS_TO_CHECK = [USDC, USDT]; const balances = await queryBalance(MINTS_TO_CHECK); const mxeOnlyMints = [...balances.entries()] .filter(([, result]) => result.state === "exists" && result.data.mode === "mxe") .map(([mint]) => mint); if (mxeOnlyMints.length > 0) { // 3. Upgrade MXE-only balances to Shared mode const convert = getNetworkEncryptionToSharedEncryptionConverterFunction({ client }); const conversionResult = await convert(mxeOnlyMints); console.log(`Converted ${conversionResult.converted.size} balance(s) to Shared mode`); } ``` ## Error Handling Conversion processes each mint sequentially. If a transaction fails or is cancelled mid-way, any mints already converted will have their signatures in `result.converted` - they are not rolled back. ```typescript theme={null} import { isConversionError } from "@umbra-privacy/sdk/errors"; try { const result = await convert([USDC, USDT, WSOL]); console.log(`Converted ${result.converted.size} balance(s)`); console.log(`Skipped ${result.skipped.size} balance(s)`); } catch (err) { if (isConversionError(err)) { switch (err.stage) { case "transaction-sign": // User rejected a per-mint transaction in their wallet. // Any mints converted before this point are already confirmed on-chain. // Re-call convert() with the remaining mints to resume. showNotification("Conversion cancelled. Progress has been saved."); break; case "account-fetch": // RPC connectivity issue while fetching token account state. console.error("RPC error during conversion:", err.message); break; case "transaction-send": // Transaction submitted but confirmation timed out. // Any mints already converted in this batch are confirmed on-chain. console.warn("Confirmation timeout. Check on-chain before retrying."); break; default: // Other stages: initialization, pda-derivation, instruction-build, // transaction-build, transaction-compile, transaction-validate. console.error("Conversion failed at stage:", err.stage, err); } } else { throw err; } } ``` Conversion is idempotent for mints that are already in Shared mode - they are returned in `result.skipped` with reason `"already_shared"`. It is safe to call `convert()` repeatedly with the same mint list. See [Error Handling](/reference/errors) for a full reference of all error types. ## How It Works The conversion follows the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern). For each eligible token account: 1. The handler instruction queues a re-encryption computation on Arcium 2. Arcium MPC decrypts the MXE-only balance and re-encrypts it under both the MPC key and the user's X25519 key 3. The callback instruction updates the on-chain account to Shared mode After conversion, future balance queries will be able to decrypt locally using the user's X25519 private key. # Creating a Client Source: https://sdk.umbraprivacy.com/sdk/creating-a-client Call getUmbraClient(args: {signer, network, rpcUrl}) to create an IUmbraClient. Covers per-call commitment override model, deps injection, and client reuse across factory functions. ## Overview `IUmbraClient` is the entry point to the SDK. It is a plain configuration object (not a class) that holds your wallet, network settings, and all pre-constructed Solana infrastructure providers. You create one client at startup and pass it to every service factory function throughout your application. ## `getUmbraClient` ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; const client = await getUmbraClient(args, deps?); ``` `getUmbraClient` is async. It returns a `Promise`. By default the wallet is prompted to sign the master seed derivation message at construction time - the client resolves only after the seed is cached. Pass `deferMasterSeedSignature: true` to defer this prompt until the first operation that needs key material instead - see [Master Seed Derivation](#master-seed-derivation) below. ### Required Arguments Your wallet signer. Must implement `signTransaction`, `signTransactions`, `signMessage`, and expose an `address` property. See [Wallet Adapters](/sdk/wallet-adapters). The Solana network to connect to. Determines which program addresses and Arcium cluster endpoints are used. HTTP endpoint for your Solana JSON-RPC node. Used to fetch account data, submit transactions, and query blockhashes. ``` https://api.mainnet-beta.solana.com # Solana public node https://my-node.quiknode.pro/abc123/ # QuikNode https://rpc.helius.xyz/?api-key=abc123 # Helius ``` WebSocket endpoint for the same RPC node. Used by the transaction forwarder to subscribe to signature confirmations instead of polling. ``` wss://api.mainnet-beta.solana.com wss://my-node.quiknode.pro/abc123/ ``` Base URL for the Umbra indexer. Required for UTXO discovery and Merkle proof generation when using the mixer. Omit if you are only using encrypted balances (deposit/withdraw) and not the mixer. ``` https://utxo-indexer.api.umbraprivacy.com # mainnet ``` There is no client-level `commitment` parameter. Commitment is configured per-call via options such as `accountInfoCommitment` and `epochInfoCommitment` on each service function. All default to `"confirmed"` when not specified. Controls when the wallet is prompted to sign the master seed derivation message. * `false` (default) - eager derivation. `getUmbraClient` awaits the wallet signature before resolving. The seed is cached before the function returns. Use this to surface the prompt at a predictable point in your onboarding flow (e.g., immediately after the user clicks "Connect Wallet"). * `true` - lazy derivation. The client is constructed instantly with no wallet prompt. The prompt fires on the first operation that needs cryptographic key material (typically `register()`). ```typescript theme={null} // Default - wallet prompt fires at construction const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl, }); // Seed is already cached - no prompt during any subsequent operation await register({ confidential: true, anonymous: true }); ``` U512 key rotation offsets. All default to `0n`. Increment a specific offset to rotate that key without changing your wallet. See the [Key Derivation](/sdk/advanced/key-derivation) guide for details. Available keys: `masterViewingKey`, `poseidonPrivateKey`, `x25519UserAccountPrivateKey`, `x25519MasterViewingKeyEncryptingPrivateKey`, `mintX25519PrivateKey`, `rescueCommitmentBlindingFactor`, `randomCommitmentFactor`. ### Optional Dependency Overrides The second argument, `deps`, lets you override individual infrastructure providers. This is primarily used for testing - you can inject mocks without a live RPC node or wallet. Override the function used to fetch on-chain account data. Defaults to an RPC-based implementation constructed from `rpcUrl`. Override the function used to fetch the latest blockhash for transaction lifetime. Defaults to an RPC-based implementation. Override how transactions are broadcast and confirmed. Defaults to a WebSocket-based forwarder. Swap this out to use Jito bundles or a custom priority fee strategy. Override the epoch info provider used for Token-2022 transfer fee calculations. Defaults to an RPC-based implementation. Custom persistence for the master seed. Defaults to in-memory storage (lost on reload). Override `load`, `store`, and `generate` to persist the seed in secure storage. See [Wallet Adapters - Persisting the Master Seed](/sdk/wallet-adapters#persisting-the-master-seed). ## What the Client Stores Once constructed, the client exposes these properties (read-only): * `client.signer` - your wallet signer * `client.network` - `"mainnet"` | `"devnet"` | `"localnet"` * `client.networkConfig` - resolved program addresses and cluster config * `client.accountInfoProvider` - pre-built RPC account fetcher * `client.blockhashProvider` - pre-built blockhash fetcher * `client.transactionForwarder` - pre-built transaction broadcaster * `client.epochInfoProvider` - pre-built epoch info provider * `client.masterSeed.getMasterSeed()` - async function that derives (and caches) the master seed ## Master Seed Derivation The master seed is a 64-byte root secret derived from a deterministic wallet signature. It is the root of Umbra's key hierarchy - all encryption keys and commitments flow from it. The signer is critical to this process. The master seed is generated by having the signer sign a deterministic message; the resulting signature is passed through KMAC256 to produce the seed. This means the signer's identity directly determines every derived key - the viewing keys, nullifier keys, X25519 keypairs, and all cryptographic commitments for that wallet. This requirement can be bypassed entirely by overriding `deps.masterSeedStorage.generate` with a custom implementation. If you supply your own `generate` function, the signer's `signMessage` is never called for seed derivation - your function is responsible for returning the master seed. In that case the signer is still used only for transaction signing. See [Understanding the SDK](/sdk/understanding-the-sdk/overview) for details. ### Eager mode (default) With `deferMasterSeedSignature: false` (the default), the wallet prompt fires at `getUmbraClient` call time. The seed is cached before the function resolves. All subsequent operations use the cached seed with no further prompts. ```typescript theme={null} // Wallet prompt fires here const client = await getUmbraClient({ signer, network, rpcUrl, rpcSubscriptionsUrl }); // No prompt - seed is already cached const register = getUserRegistrationFunction({ client }); await register({ confidential: true, anonymous: true }); // All subsequent operations reuse the cached seed await deposit(signer.address, USDC_MINT, 1_000_000n); ``` ### Lazy mode With `deferMasterSeedSignature: true`, the client is constructed instantly with no wallet prompt. The seed is derived on demand - the first time any operation needs cryptographic key material. For most users, this happens during `register()`. ```typescript theme={null} // Instant - no wallet prompt const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl, deferMasterSeedSignature: true, }); // Wallet signs here for the first time await register({ confidential: true, anonymous: true }); ``` After the first derivation, the seed is cached in memory for the lifetime of the client object. The user is not prompted again unless the client is recreated. ## Example: Full Client Setup ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl: "https://rpc.helius.xyz/?api-key=YOUR_KEY", rpcSubscriptionsUrl: "wss://rpc.helius.xyz/?api-key=YOUR_KEY", indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com", }); ``` ## Example: Testing with Mocks ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; const client = await getUmbraClient( { signer: mockSigner, network: "localnet", rpcUrl: "http://127.0.0.1:8899", rpcSubscriptionsUrl: "ws://127.0.0.1:8900", }, { accountInfoProvider: mockAccountInfoProvider, transactionForwarder: mockTransactionForwarder, masterSeedStorage: { generate: async () => fixedTestSeed, // deterministic seed for tests }, } ); ``` # Deposit Source: https://sdk.umbraprivacy.com/sdk/deposit getPublicBalanceToEncryptedBalanceDirectDepositorFunction: transfer SPL/Token-2022 tokens from public ATA to encrypted balance. Returns DepositResult with net amount and tx signatures. ## Overview The `deposit` operation moves tokens from a user's public **Associated Token Account (ATA)** into their **encrypted balance**. After a successful deposit, the balance is hidden on-chain. Tokens are moved from the user's **Associated Token Account (ATA)** into the Shielded Pool - an on-chain SPL or Token-2022 custody account controlled by the Umbra program. Protocol fees and any Token-2022 transfer fees are subtracted from the incoming amount. The net amount is what gets credited to the encrypted balance. The net amount is added to the destination user's **encrypted balance**. The balance is hidden on-chain - it can only be decrypted by the Arcium MPC network and, in Shared mode, by the user's own X25519 key. The user must be [registered](/sdk/registration) before depositing. An `EncryptedUserAccount` PDA must exist for their wallet address. Deposit transactions are publicly visible on-chain. The depositor's wallet address, the destination address, and the gross transfer amount are all readable from the transaction. Only the resulting encrypted balance is hidden - the act of shielding itself is not private. ## Usage ```typescript theme={null} import { getPublicBalanceToEncryptedBalanceDirectDepositorFunction } from "@umbra-privacy/sdk"; const deposit = getPublicBalanceToEncryptedBalanceDirectDepositorFunction({ client }); const result = await deposit( destinationAddress, // whose encrypted balance to credit mint, // SPL or Token-2022 mint address transferAmount, // amount in native token units (U64) options?, // optional ); ``` ## Parameters The wallet address whose encrypted balance is credited. This is usually `client.signer.address` (depositing to yourself), but you can deposit into any registered user's account - the funds will be encrypted under *their* X25519 key. The SPL token mint address. Works with any standard SPL token and Token-2022 mints (see [Token-2022 Support](/advanced/token-2022) for transfer fee handling). The gross amount to transfer, in the token's native units (accounting for decimals). For example, `1_000_000n` for 1 USDC (6 decimals). Protocol fees and Token-2022 transfer fees are **subtracted** from this amount before the encrypted balance is credited. Additional compute unit price in microlamports. Increase this if transactions are timing out during periods of high network congestion. A purpose flag stored alongside the deposit. Reserved for protocol use; leave at `0` unless instructed otherwise. 32 bytes of arbitrary metadata stored with the deposit. Defaults to all zeros. Whether to wait for the Arcium MPC callback transaction to confirm before resolving. When `true`, the returned `DepositResult` includes `callbackSignature` and `callbackElapsedMs`. When `false`, returns immediately after the handler (queue) transaction confirms. Skip Solana transaction preflight simulation. Useful when preflight nodes are behind. Maximum number of times the RPC node should retry sending the transaction. Commitment level used for RPC account reads during deposit preparation. Overrides the default on a per-call basis. Commitment level used for fetching epoch info (Token-2022 transfer fee schedule). Overrides the default on a per-call basis. ## Return Value Returns a `Promise`. The `DepositResult` object contains: * `queueSignature` - The transaction signature of the handler (queue computation) transaction. * `callbackStatus` - The outcome of computation monitoring: `"finalized"`, `"pruned"`, or `"timed-out"`. Present when `awaitCallback` is `true` (the default). * `callbackSignature` - The transaction signature of the Arcium MPC callback transaction. Present when `callbackStatus` is `"finalized"`. * `callbackElapsedMs` - Wall-clock milliseconds spent waiting for the callback. Present when `awaitCallback` is `true`. * `rentClaimSignature` - Transaction signature for reclaiming rent from the computation account. Attempted regardless of callback outcome. * `rentClaimError` - Error encountered during rent reclaim, if any. The deposit itself still succeeded. Deposits follow the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern) - the SDK submits the handler transaction, waits for Arcium to complete the MPC computation, then waits for the callback transaction to confirm. ## Encryption Mode Selection The SDK automatically selects the correct instruction variant based on the state of the destination user's account: * **New MXE-only balance** - destination has no X25519 key registered; first deposit * **Existing MXE-only balance** - destination has no X25519 key; balance already exists * **New Shared balance** - destination has X25519 key registered; first deposit * **Existing Shared balance** - destination has X25519 key; balance already exists You do not need to specify the mode manually. ## Protocol Fees Fees are deducted from the `transferAmount` before crediting the encrypted balance. The amount credited is: ``` credited = transferAmount - baseFee - floor((transferAmount - baseFee) * commissionBps / 10000) ``` Protocol fee configuration is set on-chain by the pool admin and may vary per pool. The current default is a small fixed base fee plus a 0.3% commission. In most cases, direct deposits from your own ATA to your own encrypted balance carry **zero protocol fees** - the `baseFee` and `commissionBps` are both set to 0 for standard self-shielding. Fees may apply for deposits routed through relayers or for specific pool configurations. ## Example: Self-Deposit ```typescript theme={null} import { getPublicBalanceToEncryptedBalanceDirectDepositorFunction } from "@umbra-privacy/sdk"; const deposit = getPublicBalanceToEncryptedBalanceDirectDepositorFunction({ client }); // Deposit 100 USDC into your own encrypted balance const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const ONE_HUNDRED_USDC = 100_000_000n; // 6 decimals const result = await deposit(client.signer.address, USDC, ONE_HUNDRED_USDC); console.log("Queue signature:", result.queueSignature); console.log("Callback signature:", result.callbackSignature); ``` ## Example: Deposit to Another User You can shield tokens directly into another registered user's encrypted balance: ```typescript theme={null} const RECIPIENT = "GsbwXfJraMomNxBcpR3DBFyKCCmN9SKGzKFJBNKxRFkT"; const result = await deposit(RECIPIENT, USDC, ONE_HUNDRED_USDC); ``` The tokens are encrypted under the recipient's X25519 key. Only they can withdraw them. ## Error Handling Deposit errors include a `stage` field identifying where the failure occurred: ```typescript theme={null} import { isEncryptedDepositError } from "@umbra-privacy/sdk/errors"; try { await deposit(destinationAddress, mint, amount); } catch (err) { if (isEncryptedDepositError(err)) { switch (err.stage) { case "validation": // Invalid arguments - check destinationAddress, mint, amount break; case "mint-fetch": // Could not fetch mint account - check RPC connectivity and mint address break; case "fee-calculation": // Token-2022 transfer fee calculation failed break; case "account-fetch": // Could not fetch the destination user account or token account break; case "transaction-send": // Transaction submitted but confirmation failed - may still have landed break; default: // Other stages: pda-derivation, instruction-build, transaction-build, // transaction-compile, transaction-sign, transaction-validate console.error("Deposit failed at stage:", err.stage, err); } } } ``` See [Error Handling](/reference/errors) for a full reference. # Installation Source: https://sdk.umbraprivacy.com/sdk/installation Install @umbra-privacy/sdk. Sub-path imports: /types (branded primitives), /interfaces (function types), /utils (PDA helpers), /constants (network configs), /errors (error classes). ## Package ```bash pnpm theme={null} pnpm add @umbra-privacy/sdk ``` ```bash npm theme={null} npm install @umbra-privacy/sdk ``` ```bash yarn theme={null} yarn add @umbra-privacy/sdk ``` ## TypeScript The SDK is written in TypeScript and ships with full type declarations. No additional `@types` packages are needed. Minimum TypeScript version: **5.0**. Add the following to your `tsconfig.json` if you are targeting Node.js: ```json theme={null} { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true } } ``` ## Import Paths The SDK exposes several sub-path imports to keep bundles lean: * `@umbra-privacy/sdk` - All implementation modules: client, services, Solana providers, cryptography * `@umbra-privacy/sdk/types` - Branded type definitions and type assertions only * `@umbra-privacy/sdk/interfaces` - Function type signatures (useful for typing your own wrappers) * `@umbra-privacy/sdk/utils` - Unit converters and encoding utilities * `@umbra-privacy/sdk/constants` - Protocol constants (program ID, seeds, max tree depth, etc.) * `@umbra-privacy/sdk/errors` - Error classes and type guard functions Most applications only need the main `@umbra-privacy/sdk` import. ```typescript theme={null} // Everything you need for standard usage import { getUmbraClient, getUserRegistrationFunction, getPublicBalanceToEncryptedBalanceDirectDepositorFunction, getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction, } from "@umbra-privacy/sdk"; // Error handling import { isEncryptedDepositError } from "@umbra-privacy/sdk/errors"; ``` ## Runtime Requirements The SDK works in both Node.js (18+) and modern browser environments. It has no native dependencies - all cryptography is pure TypeScript/WebAssembly. If you are using a bundler (Vite, webpack, Next.js), no special configuration is needed. The SDK uses standard ES module syntax. # Claiming UTXOs Source: https://sdk.umbraprivacy.com/sdk/mixer/claiming-utxos 3 claim functions: getUtxoToEncryptedBalanceClaimerFunction, getUtxoToPublicBalanceClaimerFunction, getUtxoToPublicBalanceDirectClaimerFunction. Requires IZkProverSuite and relayer submission. ## Overview Claiming a UTXO presents a ZK proof on-chain that you know the secret inputs behind a Merkle tree commitment, burns the nullifier to prevent double-spending, and releases the tokens. You choose where the tokens go: * **Into an encrypted balance** - tokens remain private after claiming * **Into a public wallet** - tokens become visible in your ATA ## Claim Functions Three factory functions cover the main claim patterns: ### Claim Self-Claimable UTXO → Encrypted Balance ```typescript theme={null} import { getSelfClaimableUtxoToEncryptedBalanceClaimerFunction, getUmbraRelayer, } from "@umbra-privacy/sdk"; import { getSelfClaimableUtxoToEncryptedBalanceClaimerProver } from "@umbra-privacy/web-zk-prover"; const zkProver = getSelfClaimableUtxoToEncryptedBalanceClaimerProver(); const relayer = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com", }); const claim = getSelfClaimableUtxoToEncryptedBalanceClaimerFunction( { client }, { zkProver, relayer }, ); const result = await claim(utxos); // pass array of ClaimableUtxoData ``` ### Claim Self-Claimable UTXO → Public Balance ```typescript theme={null} import { getSelfClaimableUtxoToPublicBalanceClaimerFunction, getUmbraRelayer, } from "@umbra-privacy/sdk"; import { getSelfClaimableUtxoToPublicBalanceClaimerProver } from "@umbra-privacy/web-zk-prover"; const zkProver = getSelfClaimableUtxoToPublicBalanceClaimerProver(); const relayer = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com", }); const claim = getSelfClaimableUtxoToPublicBalanceClaimerFunction( { client }, { zkProver, relayer }, ); const result = await claim(utxos); // pass array of ClaimableUtxoData ``` ### Claim Receiver-Claimable UTXO → Encrypted Balance ```typescript theme={null} import { getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction, getUmbraRelayer, } from "@umbra-privacy/sdk"; import { getReceiverClaimableUtxoToEncryptedBalanceClaimerProver } from "@umbra-privacy/web-zk-prover"; const zkProver = getReceiverClaimableUtxoToEncryptedBalanceClaimerProver(); const relayer = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com", }); const claim = getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction( { client }, { zkProver, relayer }, ); const result = await claim(utxos); // pass array of ClaimableUtxoData ``` ## Parameters An array of claimable UTXO data objects returned by `getClaimableUtxoScannerFunction`. Each object contains all the data needed for the claim - UTXO values, Merkle proof, and metadata. Pass them directly from the fetch result without destructuring. 32 bytes of arbitrary metadata stored with the claim. Defaults to all zeros. ZK proof generation function. Use the claim-specific provers from `@umbra-privacy/web-zk-prover` — see [ZK Provers](/sdk/advanced/zk-provers) for details. Relayer instance created via `getUmbraRelayer({ apiEndpoint })`. The relayer pays transaction fees for claim operations so the user's wallet never appears on-chain as the fee payer. ## Return Value Returns a result object with `signatures: Record` - transaction signatures organized by batch index. Each batch can contain multiple UTXOs. ## Full Example: Fetch and Claim ```typescript theme={null} import { getClaimableUtxoScannerFunction, getSelfClaimableUtxoToEncryptedBalanceClaimerFunction, getUmbraRelayer, } from "@umbra-privacy/sdk"; import { getSelfClaimableUtxoToEncryptedBalanceClaimerProver } from "@umbra-privacy/web-zk-prover"; // Step 1: Fetch claimable UTXOs const scan = getClaimableUtxoScannerFunction({ client }); const result = await scan(0, 0); // scan tree 0 from the start if (result.selfBurnable.length === 0) { console.log("No claimable UTXOs found"); return; } // Step 2: Claim the first self-burnable UTXO into encrypted balance const zkProver = getSelfClaimableUtxoToEncryptedBalanceClaimerProver(); const relayer = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com", }); const claim = getSelfClaimableUtxoToEncryptedBalanceClaimerFunction( { client }, { zkProver, relayer }, ); const claimResult = await claim([result.selfBurnable[0]]); console.log("Claimed UTXO, signatures:", claimResult.signatures); ``` ## Example: Claim All UTXOs to Public Balance ```typescript theme={null} import { getClaimableUtxoScannerFunction, getSelfClaimableUtxoToPublicBalanceClaimerFunction, getUmbraRelayer, } from "@umbra-privacy/sdk"; import { getSelfClaimableUtxoToPublicBalanceClaimerProver } from "@umbra-privacy/web-zk-prover"; const zkProver = getSelfClaimableUtxoToPublicBalanceClaimerProver(); const relayer = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com", }); const scan = getClaimableUtxoScannerFunction({ client }); const claim = getSelfClaimableUtxoToPublicBalanceClaimerFunction( { client }, { zkProver, relayer }, ); const result = await scan(0, 0); if (result.selfBurnable.length > 0) { const claimResult = await claim(result.selfBurnable); console.log("Claimed all self-burnable UTXOs:", claimResult.signatures); } ``` ## ZK Proof Generation Claim operations generate a **[Groth16](https://eprint.iacr.org/2016/260.pdf) ZK proof** that proves: * You know the secret inputs to one of the commitments in the Merkle tree * The nullifier for that commitment has not been burned before The proof is verified on-chain by the Umbra program. Invalid proofs are rejected; valid proofs trigger the callback instruction that releases the tokens. Proof generation is CPU-intensive and may take 1–5 seconds. Consider showing a progress indicator while it runs. ## Stale Merkle Proofs Merkle proofs become stale if the tree root changes between when you fetch the proof and when you submit the claim. This can happen if other UTXOs are inserted into the tree. If your claim fails with a root mismatch error, re-fetch the proof using `getClaimableUtxoScannerFunction` and try again. Do not cache Merkle proofs for extended periods. Always fetch a fresh proof immediately before submitting a claim. The indexer's `/v1/trees/{tree_index}/proof/{insertion_index}` endpoint always returns the latest valid proof. ## Error Handling Claim operations share the same two ZK-specific failure modes as UTXO creation, plus an additional on-chain failure case: a stale Merkle proof. Use `isClaimUtxoError` from `@umbra-privacy/sdk/errors` and `switch` on `err.stage` to handle each one. ```typescript theme={null} import { isClaimUtxoError } from "@umbra-privacy/sdk/errors"; try { const result = await claim(utxos); } catch (err) { if (isClaimUtxoError(err)) { switch (err.stage) { case "zk-proof-generation": // ZK proof generation failed. // This may indicate an out-of-memory condition, or a zkProver mismatch. console.error("Proof generation failed:", err.message); showNotification("Failed to generate proof. Please try again."); break; case "transaction-sign": // User rejected the transaction in their wallet. showNotification("Claim cancelled."); break; case "transaction-validate": // Pre-flight simulation failed - often indicates a stale Merkle proof. // Re-fetch the proof using getClaimableUtxoScannerFunction and try again. console.warn("Claim pre-flight failed - Merkle proof may be stale:", err.message); break; case "transaction-send": // Transaction submitted but confirmation timed out. // Always verify on-chain before retrying - the nullifier may already be burned. console.warn("Confirmation timeout. Check on-chain before retrying."); break; default: // Other stages: initialization, validation, key-derivation, pda-derivation, // instruction-build, transaction-build, transaction-compile. console.error("Claim failed at stage:", err.stage, err); } } else { throw err; } } ``` If `err.stage === "transaction-send"`, always verify on-chain before retrying. A successful claim burns the nullifier - submitting a second claim for the same UTXO will be rejected on-chain and waste transaction fees. See [Error Handling](/reference/errors) for a full reference of all error types. ## Nullifier Reuse Once a UTXO is claimed, its nullifier is burned on-chain. Any attempt to claim the same UTXO again will be rejected by the program. You do not need to track this client-side - the on-chain check is authoritative. # Creating UTXOs Source: https://sdk.umbraprivacy.com/sdk/mixer/creating-utxos 4 UTXO creation functions: getEncryptedBalance[Self/Receiver]ClaimableUtxoCreatorFunction and getPublicBalance[Self/Receiver]ClaimableUtxoCreatorFunction. Merkle tree insertion and ciphertext publish. ## Overview Creating a UTXO inserts a cryptographic commitment into the on-chain Indexed Merkle Tree and locks the corresponding tokens in the shielded pool. The SDK also publishes an encrypted ciphertext on-chain so the recipient can discover the UTXO using their X25519 key. Choose the factory function that matches your source (encrypted balance or public ATA) and recipient (yourself or a third party). ## Factory Functions ### Self-Claimable from Encrypted Balance Fund the UTXO from your existing encrypted balance. You will claim it yourself. ```typescript theme={null} import { getEncryptedBalanceToSelfClaimableUtxoCreatorFunction } from "@umbra-privacy/sdk"; import { getEncryptedBalanceToSelfClaimableUtxoCreatorProver } from "@umbra-privacy/web-zk-prover"; const zkProver = getEncryptedBalanceToSelfClaimableUtxoCreatorProver(); const createUtxo = getEncryptedBalanceToSelfClaimableUtxoCreatorFunction( { client }, { zkProver }, ); const result = await createUtxo({ destinationAddress: client.signer.address, // recipient (yourself) mint, amount, }); ``` ### Self-Claimable from Public Balance Fund the UTXO directly from your public ATA. You will claim it yourself. ```typescript theme={null} import { getPublicBalanceToSelfClaimableUtxoCreatorFunction } from "@umbra-privacy/sdk"; import { getPublicBalanceToSelfClaimableUtxoCreatorProver } from "@umbra-privacy/web-zk-prover"; const zkProver = getPublicBalanceToSelfClaimableUtxoCreatorProver(); const createUtxo = getPublicBalanceToSelfClaimableUtxoCreatorFunction( { client }, { zkProver }, ); const result = await createUtxo({ destinationAddress: client.signer.address, mint, amount, }); ``` ### Receiver-Claimable from Public Balance Fund the UTXO from your public ATA. A specified recipient will claim it (anonymous payment). ```typescript theme={null} import { getPublicBalanceToReceiverClaimableUtxoCreatorFunction } from "@umbra-privacy/sdk"; import { getPublicBalanceToReceiverClaimableUtxoCreatorProver } from "@umbra-privacy/web-zk-prover"; const zkProver = getPublicBalanceToReceiverClaimableUtxoCreatorProver(); const createUtxo = getPublicBalanceToReceiverClaimableUtxoCreatorFunction( { client }, { zkProver }, ); const RECIPIENT = "GsbwXfJraMomNxBcpR3DBFyKCCmN9SKGzKFJBNKxRFkT"; const result = await createUtxo({ destinationAddress: RECIPIENT, // recipient's wallet address mint, amount, }); ``` ### Receiver-Claimable from Encrypted Balance Fund the UTXO from your encrypted balance. A specified recipient will claim it. ```typescript theme={null} import { getEncryptedBalanceToReceiverClaimableUtxoCreatorFunction } from "@umbra-privacy/sdk"; import { getEncryptedBalanceToReceiverClaimableUtxoCreatorProver } from "@umbra-privacy/web-zk-prover"; const zkProver = getEncryptedBalanceToReceiverClaimableUtxoCreatorProver(); const createUtxo = getEncryptedBalanceToReceiverClaimableUtxoCreatorFunction( { client }, { zkProver }, ); const result = await createUtxo({ destinationAddress: RECIPIENT, mint, amount, }); ``` ## Parameters All create UTXO functions accept a `CreateUtxoArgs` object: The wallet address that will claim this UTXO. For self-claimable UTXOs, use `client.signer.address`. For receiver-claimable, use the recipient's address. The recipient must be registered (X25519 key on-chain) so their key can be used to encrypt the ciphertext. SPL or Token-2022 mint address. Amount in native token units. Protocol fees and Token-2022 transfer fees are subtracted from this amount. Additional compute unit price in microlamports. A ZK proof generation function. Required for all UTXO creation operations. This is the circuit-specific prover that generates the Groth16 proof for the commitment. Use `@umbra-privacy/web-zk-prover` for the recommended browser-based prover — see [ZK Provers](/sdk/advanced/zk-provers) for details. ## Return Value The return type depends on the source: * **From encrypted balance:** `Promise` - a multi-transaction result with MPC callback. Contains the transaction signatures for both the queue computation and callback transactions. * **From public balance:** `Promise` - a single-transaction result with no MPC involvement. Contains the transaction signature for the UTXO creation transaction. ## The ZK Prover Dependency UTXO creation requires a ZK prover function (`zkProver` in `deps`). This function generates a Groth16 proof that the commitment was constructed correctly. The prover is a CPU-intensive operation - generating a proof can take 1–5 seconds on a modern device. For browser applications, consider running the prover in a Web Worker to avoid blocking the main thread. The `zkProver` dependency is **required** and cannot be omitted. Attempting to create a UTXO without it will throw at factory construction time. ## Example: Anonymous Payment Send 50 USDC to a recipient without revealing you as the sender: ```typescript theme={null} import { getPublicBalanceToReceiverClaimableUtxoCreatorFunction } from "@umbra-privacy/sdk"; import { getPublicBalanceToReceiverClaimableUtxoCreatorProver } from "@umbra-privacy/web-zk-prover"; const zkProver = getPublicBalanceToReceiverClaimableUtxoCreatorProver(); const createUtxo = getPublicBalanceToReceiverClaimableUtxoCreatorFunction( { client }, { zkProver }, ); const RECIPIENT = "RecipientWalletAddressHere..."; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const result = await createUtxo({ destinationAddress: RECIPIENT, mint: USDC, amount: 50_000_000n, }); console.log("UTXO created:", result); // The recipient can now fetch and claim this UTXO using getClaimableUtxoScannerFunction // and getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction ``` ## Error Handling UTXO creation has two failure modes that are specific to this operation: ZK proof generation and stale on-chain state. Use `isCreateUtxoError` from `@umbra-privacy/sdk/errors` and `switch` on `err.stage` to handle each one. ```typescript theme={null} import { isCreateUtxoError } from "@umbra-privacy/sdk/errors"; try { const signatures = await createUtxo({ destinationAddress: recipient, mint, amount }); } catch (err) { if (isCreateUtxoError(err)) { switch (err.stage) { case "zk-proof-generation": // ZK proof generation failed - the most common failure mode for UTXO creation. // This may indicate an out-of-memory condition in the browser, // or a mismatch between the prover and the circuit parameters. console.error("Proof generation failed:", err.message); showNotification("Failed to generate proof. Please try again."); break; case "transaction-sign": // User rejected the transaction in their wallet. showNotification("UTXO creation cancelled."); break; case "account-fetch": // Could not fetch the recipient's on-chain account to look up their X25519 key. console.error("RPC error:", err.message); break; case "transaction-send": // Transaction submitted but confirmation failed - may still have landed. // Check whether the commitment was inserted before retrying. console.warn("Confirmation timeout. Check on-chain before retrying."); break; default: // Other stages: initialization, validation, mint-fetch, fee-calculation, // key-derivation, pda-derivation, instruction-build, transaction-build, // transaction-compile, transaction-validate. console.error("UTXO creation failed at stage:", err.stage, err); } } else { throw err; } } ``` If `err.stage === "transaction-send"`, do not immediately retry - the transaction may have landed. Fetch the recipient's UTXO list with `getClaimableUtxoScannerFunction` first to confirm whether the commitment was inserted into the tree. See [Error Handling](/reference/errors) for a full reference of all error types. ## Protocol Fees Fees are deducted from `amount` before the commitment is created. The net amount committed is `amount - fees`. This is the amount the recipient will receive when they claim. # Fetching UTXOs Source: https://sdk.umbraprivacy.com/sdk/mixer/fetching-utxos getClaimableUtxoScannerFunction: scan indexer for X25519-encrypted ciphertexts, decrypt matching UTXOs, return ScannedUtxoResult[] with Merkle proofs. Accepts U32 pagination params. ## Overview After tokens are deposited into the mixer, the depositor publishes an encrypted ciphertext on-chain addressed to the recipient's [X25519](https://www.rfc-editor.org/rfc/rfc7748) key. To claim, the recipient must: 1. Fetch all ciphertexts from the indexer 2. Attempt to decrypt each one using their X25519 private key 3. Successfully decrypted ciphertexts are their claimable UTXOs 4. Fetch the Merkle proof for each claimable UTXO `getClaimableUtxoScannerFunction` handles all of these steps automatically. This function requires the indexer. Ensure `indexerApiEndpoint` is set when creating the client with `getUmbraClient`, or the function will throw. ## Usage ```typescript theme={null} import { getClaimableUtxoScannerFunction } from "@umbra-privacy/sdk"; const scan = getClaimableUtxoScannerFunction({ client }); const result = await scan( treeIndex, // which Merkle tree to scan (U32) startInsertionIndex, // start scanning from this leaf position (U32) endInsertionIndex?, // optional upper bound (U32) ); ``` ## Parameters The zero-based index of the Merkle tree to scan. Start with `0` for the first tree. If the current tree is filling up, increment to check the next one. This is a branded `U32` type, not a plain `number`. The leaf position to start scanning from (inclusive). Pass `0` to scan from the beginning. To resume from where you left off, pass the insertion index of the last UTXO you have already seen. This is a branded `U32` type, not a plain `number`. The leaf position to stop at (inclusive). If omitted, scans to the end of the current tree. Use this to limit the scan range when you know the approximate insertion window of your UTXOs. This is a branded `U32` type, not a plain `number`. ## Return Value ```typescript theme={null} type ScannedUtxoResult = { selfBurnable: ClaimableUtxoData[]; // UTXOs you created yourself (from encrypted balance) received: ClaimableUtxoData[]; // UTXOs sent to you by others (from encrypted balance) publicSelfBurnable: ClaimableUtxoData[]; // UTXOs you created yourself (from public balance) publicReceived: ClaimableUtxoData[]; // UTXOs sent to you by others (from public balance) }; ``` Each `ClaimableUtxoData` is a flat structure containing both the UTXO data and its Merkle proof. These objects are passed directly to the claim functions — no destructuring needed. ## Example: Fetch All UTXOs for Tree 0 ```typescript theme={null} import { getClaimableUtxoScannerFunction } from "@umbra-privacy/sdk"; const scan = getClaimableUtxoScannerFunction({ client }); const result = await scan(0, 0); // scan all of tree 0 console.log("Self-burnable UTXOs:", result.selfBurnable.length); console.log("Received UTXOs:", result.received.length); for (const utxo of result.selfBurnable) { console.log( `UTXO: amount=${utxo.amount}` ); } ``` ## Example: Paginated Scan For large trees, scan in chunks to avoid timeouts: ```typescript theme={null} const CHUNK_SIZE = 10_000; async function fetchAllUtxos(treeIndex: U32) { const scan = getClaimableUtxoScannerFunction({ client }); const allSelfBurnable = []; const allReceived = []; let cursor = 0; while (true) { const result = await scan(treeIndex, cursor, cursor + CHUNK_SIZE - 1); allSelfBurnable.push(...result.selfBurnable); allReceived.push(...result.received); // Stop if we've reached the end of the tree if (result.selfBurnable.length + result.received.length === 0 && cursor + CHUNK_SIZE >= 1_048_576) { break; } cursor += CHUNK_SIZE; } return { selfBurnable: allSelfBurnable, received: allReceived }; } ``` ## How Decryption Works The SDK derives your X25519 private key from the master seed, then for each UTXO ciphertext: 1. Extracts the depositor's ephemeral X25519 public key from the ciphertext header 2. Computes an [X25519 ECDH](https://www.rfc-editor.org/rfc/rfc7748) shared secret 3. Derives an [AES-GCM](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf) key from the shared secret 4. Attempts to decrypt the 68-byte payload 5. If decryption succeeds, checks the 12-byte domain separator to categorize the UTXO type Your private key never leaves your device. The decryption happens entirely in the SDK. ## Error Handling Fetching UTXOs involves two distinct infrastructure dependencies: the Umbra indexer (for ciphertext discovery) and the RPC node (for Merkle proof data). Use `isFetchUtxosError` from `@umbra-privacy/sdk/errors` and `switch` on `err.stage` to handle each failure point. ```typescript theme={null} import { isFetchUtxosError } from "@umbra-privacy/sdk/errors"; try { const result = await fetch(treeIndex, startInsertionIndex); } catch (err) { if (isFetchUtxosError(err)) { switch (err.stage) { case "initialization": // Factory construction failed - indexerApiEndpoint was not configured. // Ensure indexerApiEndpoint is set when calling getUmbraClient. console.error("Indexer not configured:", err.message); break; case "validation": // Invalid treeIndex or insertion index parameters. console.error("Invalid fetch parameters:", err.message); break; case "key-derivation": // X25519 private key derivation from master seed failed. console.error("Key derivation failed:", err.message); break; case "indexer-fetch": // Indexer HTTP call failed - unreachable, rate-limited, or returned an error. console.error("Indexer fetch failed:", err.message); showNotification("Could not reach the network. Please check your connection."); break; case "proof-fetch": // Merkle proof HTTP call failed. console.error("Proof fetch failed:", err.message); break; } } else { throw err; } } ``` An empty result (`selfBurnable: [], received: []`) is not an error - it means no UTXOs addressed to you were found in that scan range. Errors are only thrown for infrastructure failures. See [Error Handling](/reference/errors) for a full reference of all error types. ## Storing UTXO State The SDK does not persist UTXO state between calls. If your application needs to track which UTXOs have been claimed, maintain your own list (keyed by `insertionIndex` + `treeIndex`) and exclude already-claimed entries from future claims. A claimed UTXO's nullifier is burned on-chain - attempting to claim it again will fail at the on-chain program level. # Mixer Overview Source: https://sdk.umbraprivacy.com/sdk/mixer/overview Mixer overview: 4 UTXO creation variants (encrypted/public x self/receiver), 3 claim variants (to encrypted, to public, to public direct). Self-claimable vs receiver-claimable flows. ## The Most Powerful Shielded Pool on Solana Umbra's mixer is the most powerful shielded pool on Solana today - and it is built to fit into any flow. Tokens can enter from a public wallet or an encrypted balance, exit to a fresh address or stay private inside another encrypted balance, and be sent to yourself or to any registered recipient. Every combination is a first-class path, not an afterthought. The mixer requires full [registration](/sdk/registration) with `anonymous: true`. Both your X25519 key and user commitment must be registered before creating UTXOs. ## UTXO Structure The mixer operates on UTXOs - commitments inserted into an on-chain Indexed Merkle Tree that represent locked tokens in the shielded pool. Every UTXO encodes three distinct roles: * **Sender** - the address that funded the UTXO, locked the tokens into the shielded pool, and fixed the recipient at creation time * **Unlocker** - the address authorized to burn the UTXO's nullifier on-chain and release the tokens, choosing whether they exit to a public ATA or into an encrypted balance * **Recipient** - the final destination address, set by the sender and not modifiable by the unlocker The sender controls who receives. The unlocker controls when and how the tokens exit. Separating these two roles is what makes deep mixing possible - the sender's involvement ends at creation time. ## UTXO Variants You are both the sender and the unlocker. Burn the UTXO on your own schedule and choose whether tokens exit to your public ATA or stay in an encrypted balance. The recipient is the unlocker. They burn the UTXO themselves and decide the exit - public or shielded. The exit happens entirely on their timeline, with no further action from the sender. Receiver-claimable UTXOs produce stronger anonymity sets. Because the sender's actions and the exit are fully decoupled, timing correlation between deposit and withdrawal becomes significantly harder. ## Source Options Each variant can be funded from two sources: * **From encrypted balance** - tokens are drawn from your existing encrypted balance before entering the shielded pool * **From public balance** - tokens are transferred directly from your public ATA This gives four factory functions in total: * `getEncryptedBalanceToSelfClaimableUtxoCreatorFunction` - source: encrypted balance, unlocker: you * `getEncryptedBalanceToReceiverClaimableUtxoCreatorFunction` - source: encrypted balance, unlocker: recipient * `getPublicBalanceToSelfClaimableUtxoCreatorFunction` - source: ATA, unlocker: you * `getPublicBalanceToReceiverClaimableUtxoCreatorFunction` - source: ATA, unlocker: recipient See [Creating UTXOs](/sdk/mixer/creating-utxos) for full API details. ## The Mixer Flow Choose a factory function, specify the source, recipient, and amount, and submit. The SDK locks tokens in the shielded pool, inserts a commitment into the Merkle tree, and publishes an encrypted ciphertext on-chain so the recipient can discover it. See [Creating UTXOs](/sdk/mixer/creating-utxos). The more other users deposit into the same tree, the stronger your privacy guarantee. There is no enforced waiting period - this is a trade-off you manage in your application. The SDK queries the indexer for all ciphertexts addressed to the caller's X25519 key, decrypts them locally, and fetches the current Merkle inclusion proofs. See [Fetching UTXOs](/sdk/mixer/fetching-utxos). Submit a Groth16 ZK proof on-chain proving knowledge of the secret inputs. The nullifier is burned on-chain to prevent double-spending, and the tokens are released to the chosen destination - a public ATA or an encrypted balance. See [Claiming UTXOs](/sdk/mixer/claiming-utxos). ## Indexer Requirement UTXO discovery requires the Umbra indexer. Pass `indexerApiEndpoint` when constructing the client: ```typescript theme={null} const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl: "...", rpcSubscriptionsUrl: "...", indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com", }); ``` Without it, `getClaimableUtxoScannerFunction` will fail. The indexer is not required for creating UTXOs - only for discovering and claiming them. ## Privacy Considerations **Claim into an encrypted balance by default.** When a UTXO is burned into an encrypted balance, the claimed amount is hidden, the destination is not revealed, and no observable exit event is produced on-chain. At burn time, the sender is completely unlinkable - there is no visible amount, no destination address, and no signal that can tie the burn back to any specific deposit. Public ATA claims should be the opt-out, not the default. **Use round, pool-common amounts when claiming publicly.** If a claim must exit to a public ATA, the amount becomes visible on-chain. Unusual or fragmented amounts let an observer eliminate most of the tree and narrow in on the matching commitment. Standardise on amounts that are common in the pool (e.g. exactly 100 USDC) to stay indistinguishable within the largest possible set. **Claim to a fresh address when going public.** Claiming into an address with existing on-chain history re-establishes a link. Use an address that has never appeared on-chain before. For a complete breakdown of all eight source and destination combinations - including what is visible, what is hidden, timing risk, and recommended use cases - see [Privacy Analysis](/sdk/mixer/privacy-analysis). # Privacy Analysis Source: https://sdk.umbraprivacy.com/sdk/mixer/privacy-analysis 3-tier privacy model: EncryptedBalance-to-EncryptedBalance (strongest), mixed flows (partial), ATA-to-ATA (weakest). Covers timing analysis, amount correlation, and anonymity set sizing. ## Summary The mixer's privacy strength is determined by three independent choices: where tokens come from, where they land, and who controls the burn. * Shielding both ends (Encrypted Balance → Encrypted Balance) eliminates amount correlation entirely and makes the sender completely unlinkable at burn time - the strongest possible configuration. * Shielding one end (mixed flows) partially limits what an observer can correlate, depending on which side is visible. * Public flows (ATA → ATA) expose amounts at both ends; the only remaining privacy property is the absence of a direct on-chain link between the two addresses. * Receiver-claimable UTXOs are cryptographically equivalent to self-claimable UTXOs with the same source and destination, but are stronger in practice - recipients naturally claim later and independently, widening the anonymity set without any deliberate effort. For a complete breakdown of all eight combinations see the [Privacy Ranking](#privacy-ranking) below. *** ## How to Read This Page Every UTXO flow through the mixer has two independent axes that determine its privacy profile: * **Source** - where the tokens come from: a public ATA or an encrypted balance (Encrypted Balance) * **Destination** - where the tokens land after the UTXO is burned: a public ATA or an encrypted balance (Encrypted Balance) * **Claimant type** - whether the sender or the recipient is the unlocker Creating a UTXO from an Encrypted Balance hides the deposited amount - no observer can see how much entered the shielded pool. Burning a UTXO into an Encrypted Balance hides the claimed amount - no observer can see how much left the shielded pool. When both ends are shielded, amounts cannot be correlated at all, and the sender is completely unlinkable at burn time. ### Self-claimable vs receiver-claimable Cryptographically, self-claimable and receiver-claimable UTXOs with the same source and destination are equivalent - the same information is visible or hidden on-chain in both cases. The practical difference is behavioural. A sender burning their own UTXO tends to do so relatively promptly. A recipient, acting independently, will typically claim at a time of their own choosing - often much later, and without any coordination with the sender. This natural timing gap widens the anonymity set and makes temporal correlation significantly harder in practice. Receiver-claimable is therefore the stronger choice, not because of what is on the chain, but because of how humans use it. Each of the eight combinations below is assessed on: * **Visible on-chain** - what any observer can read from the chain * **Hidden** - what an observer cannot determine * **Timing risk** - how susceptible the flow is to correlation by time * **Best for** - the use case this combination suits *** ## Privacy Ranking Pairs share the same cryptographic strength. Within each pair, receiver-claimable is stronger in practice due to natural timing behaviour. * **Tier 1** - Encrypted Balance → Encrypted Balance (both ends shielded, no amounts observable) * Encrypted Balance → Receiver-claimable → Encrypted Balance * Encrypted Balance → Self-claimable → Encrypted Balance * **Tier 2** - Mixed (one end shielded, amounts partially observable) * Encrypted Balance → Receiver-claimable → ATA * Encrypted Balance → Self-claimable → ATA * ATA → Receiver-claimable → Encrypted Balance * ATA → Self-claimable → Encrypted Balance * **Tier 3** - ATA → ATA (both ends public, amounts fully observable) * ATA → Receiver-claimable → ATA * ATA → Self-claimable → ATA *** ## Tier 1 - Encrypted Balance → Encrypted Balance Both the deposit and the claim are shielded. No amounts are visible at either end. This is the strongest category regardless of claimant type. ### Encrypted Balance → Receiver-claimable → Encrypted Balance The strongest possible flow. No amounts are visible at either end, and the exit happens entirely on the recipient's timeline with no involvement from the sender. * **Visible on-chain** - a UTXO commitment was inserted into the tree; a nullifier was burned at some later time * **Hidden** - the deposited amount, the claimed amount, the depositor's identity, the recipient's identity, and any temporal relationship between the two events * **Timing risk** - minimal. No amounts are observable at either end, making correlation by value impossible. The recipient burns the UTXO at a time of their own choosing with no signal from the sender. * **Best for** - anonymous payments to a recipient where maximum privacy is required for both parties *** ### Encrypted Balance → Self-claimable → Encrypted Balance Cryptographically identical to the receiver-claimable variant above. The only difference is that the same party controls both the deposit and the burn, so the timing gap between the two events is down to the sender's own discipline. * **Visible on-chain** - a UTXO commitment was inserted; a nullifier was burned * **Hidden** - the deposited amount, the claimed amount, and any link between the deposit and burn events * **Timing risk** - low. Amounts cannot be correlated. The sender controls burn timing and can delay it to widen the anonymity set, though this requires deliberate effort rather than happening naturally. * **Best for** - moving your own funds through the mixer with no on-chain amount trail at either end *** ## Tier 2 - Mixed (One End Shielded) One side is visible, one is hidden. Amounts can be partially observed but not fully correlated across the pool boundary. ### Encrypted Balance → Receiver-claimable → ATA The deposit is hidden but the claim exits publicly. No amount is visible at entry, so the visible exit cannot be tied back to any specific deposit with certainty. * **Visible on-chain** - the claimed amount and the destination ATA at burn time * **Hidden** - the deposited amount, the depositor's identity, and any link from the visible claim back to the original deposit * **Timing risk** - low-medium. No inflow amount is observable to match against the claim. The recipient controls burn timing independently of the sender, naturally introducing a gap. * **Best for** - sending to a recipient who needs tokens in a public wallet, where protecting the sender's identity and deposit amount is the priority *** ### Encrypted Balance → Self-claimable → ATA Cryptographically identical to the receiver-claimable variant above. The deposit is hidden and the exit is public, but the same party controls both events, so the timing gap is self-imposed. * **Visible on-chain** - the claimed amount and destination ATA at burn time * **Hidden** - the deposited amount and any quantitative link from the burn back to the deposit * **Timing risk** - medium. The claim amount is visible but no matching deposit amount can be observed. Timing correlation is possible but requires the sender to burn promptly after depositing. * **Best for** - surfacing funds into a public wallet where hiding the deposit amount matters more than hiding the exit *** ### ATA → Receiver-claimable → Encrypted Balance The deposit is public but the claim is hidden. An observer can see who deposited and how much, but the burn produces no visible exit - no amount, no destination. The recipient acts independently, naturally introducing timing separation. * **Visible on-chain** - the deposited amount and the depositing ATA at UTXO creation time * **Hidden** - the claimed amount, the claim destination, and any link from the visible deposit to the eventual claim * **Timing risk** - low-medium. The exit is invisible, so even though the deposit is known, there is no observable claim event to correlate it against. The recipient controls when to burn. * **Best for** - scenarios where the sender's identity is not sensitive but the recipient's privacy must be protected *** ### ATA → Self-claimable → Encrypted Balance Cryptographically identical to the receiver-claimable variant above. The deposit is public and the exit is hidden, but the same party controls both events. * **Visible on-chain** - the deposited amount and depositing ATA at UTXO creation time * **Hidden** - the claimed amount and the destination * **Timing risk** - medium. The deposit is fully visible. The exit leaves no trace, but the sender burning their own UTXO may do so in a way that is temporally close to the deposit, narrowing the window of uncertainty. * **Best for** - moving funds into a private encrypted balance from a known public wallet, where the source is not sensitive but the destination must be hidden *** ## Tier 3 - ATA → ATA Both ends are public. Amounts are fully observable at deposit and claim. The only privacy property is the absence of a direct on-chain link between the two addresses. ### ATA → Receiver-claimable → ATA Both ends are public. The sole privacy property is that the sender and recipient are different parties with no observable on-chain link connecting them. The recipient's natural timing behaviour provides some practical separation. * **Visible on-chain** - the deposited amount, the depositing ATA, the claimed amount, and the destination ATA * **Hidden** - the relationship between the depositor and the recipient; no on-chain record connects the two addresses * **Timing risk** - medium-high. Both amounts are visible and can be matched if equal or closely related. The recipient controlling burn timing reduces but does not eliminate amount-based correlation. * **Best for** - anonymous payments where neither party's balance needs to stay shielded and the sole goal is unlinking sender from recipient *** ### ATA → Self-claimable → ATA The weakest flow. Both the deposit and claim are fully visible and the same party controls both, making timing and amount correlation straightforward. * **Visible on-chain** - the deposited amount, the depositing ATA, the claimed amount, and the destination ATA * **Hidden** - the direct on-chain link between the two addresses; a ZK proof still prevents anyone from proving which deposit maps to which claim without the secret inputs * **Timing risk** - high. Amounts are visible at both ends. Matching equal amounts deposited and claimed within a short window is easy when the anonymity set is small. * **Best for** - the minimum viable case where address unlinking is the only goal and privacy of amounts or timing is not a concern; use round, pool-common amounts to maximise the anonymity set # Query State Source: https://sdk.umbraprivacy.com/sdk/query getEncryptedBalanceQuerierFunction and getUserAccountQuerierFunction: read-only queries for encrypted balance metadata (per-mint) and user account registration state. ## Overview Two query functions let you inspect on-chain state without modifying it: * **`getUserAccountQuerierFunction`** - reads registration status and account metadata for any address * **`getEncryptedBalanceQuerierFunction`** - reads encrypted balance metadata for the calling user across multiple mints Both are read-only and do not require a wallet signing prompt. ## Query User Account ```typescript theme={null} import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk"; const query = getUserAccountQuerierFunction({ client }); const result = await query(userAddress, options?); ``` ### Parameters The wallet address to query registration status for. Commitment level used for the RPC account read. Overrides the default on a per-call basis. ### Return Value The result is a discriminated union: ```typescript theme={null} type QueryUserAccountResult = | { state: "non_existent" } | { state: "exists"; data: EncryptedUserAccount }; ``` When `state === "exists"`, `data` contains: Whether the base account has been created on-chain. This is `true` after the first registration step. Whether the X25519 public key has been registered. Required for Shared-mode encrypted balances and for receiving mixer UTXOs. Whether the user commitment (Poseidon hash) has been registered. Required for mixer / anonymous transfers. Whether both X25519 and commitment registration are complete and the account is ready for all features. The user's registered X25519 public key (32 bytes). Used by other users to encrypt UTXOs addressed to this account. Monotonic counter incremented on each deposit/withdrawal. Used internally for nonce derivation. ### Example: Check Registration Status ```typescript theme={null} const result = await query(client.signer.address); if (result.state === "non_existent") { // User is not registered - prompt them to register console.log("Not registered"); return; } const { data } = result; if (!data.isUserAccountX25519KeyRegistered) { console.log("X25519 key not registered - re-run registration"); } if (!data.isUserCommitmentRegistered) { console.log("Commitment not registered - re-run registration with anonymous: true"); } if (data.isActiveForAnonymousUsage) { console.log("Fully registered - all features available"); } ``` *** ## Query Encrypted Balance ```typescript theme={null} import { getEncryptedBalanceQuerierFunction } from "@umbra-privacy/sdk"; const query = getEncryptedBalanceQuerierFunction({ client }); // Query your encrypted balances for one or more mints const balances = await query([USDC_MINT, USDT_MINT], options?); // Returns Map ``` The function queries the calling user's own encrypted balances. Pass an array of mint addresses to check multiple mints in a single call. ### Parameters An array of SPL or Token-2022 mint addresses to query. The SDK fetches the encrypted token account PDA for each mint and returns a result for every mint in the array. Commitment level used for the RPC account reads. Overrides the default on a per-call basis. ### Return Value ```typescript theme={null} type QueryEncryptedBalanceResult = | { readonly state: "non_existent" } | { readonly state: "uninitialized" } | { readonly state: "mxe" } | { readonly state: "shared"; readonly balance: MathU64 }; ``` The four states represent: * `"non_existent"` - No encrypted token account exists for this mint. The user needs to deposit first. * `"uninitialized"` - The account PDA exists on-chain but the Arcium balance has not been initialized yet. * `"mxe"` - The account is in MXE-only mode. The balance is encrypted under the network key and cannot be decrypted client-side. * `"shared"` - The account is in Shared mode. The SDK automatically decrypts the balance using the user's X25519 private key and returns the plaintext `balance` as a `MathU64`. ### Example ```typescript theme={null} import { getEncryptedBalanceQuerierFunction } from "@umbra-privacy/sdk"; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const USDT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"; const query = getEncryptedBalanceQuerierFunction({ client }); const balances = await query([USDC, USDT]); for (const [mint, result] of balances) { switch (result.state) { case "shared": console.log(`Mint ${mint}: balance=${result.balance}`); break; case "mxe": console.log(`Mint ${mint}: MXE mode (cannot decrypt client-side)`); break; case "uninitialized": console.log(`Mint ${mint}: account exists but balance not initialized`); break; case "non_existent": console.log(`No encrypted balance for mint ${mint} - deposit first`); break; } } ``` Shared-mode accounts return the decrypted balance automatically. MXE-mode accounts cannot be decrypted client-side - convert to Shared mode first using [Conversion](/sdk/conversion). *** ## Common Patterns ### Check Before Depositing Before depositing to an external address, verify the recipient is registered: ```typescript theme={null} const recipientQuery = getUserAccountQuerierFunction({ client }); const recipientResult = await recipientQuery(recipientAddress); if ( recipientResult.state === "non_existent" || !recipientResult.data.isUserAccountX25519KeyRegistered ) { throw new Error("Recipient is not registered for encrypted deposits"); } // Safe to deposit await deposit(recipientAddress, mint, amount); ``` ### Poll Until Registered For UI flows where you start registration and want to confirm it landed: ```typescript theme={null} async function waitForRegistration(address: Address, maxAttempts = 10) { const query = getUserAccountQuerierFunction({ client }); for (let attempt = 0; attempt < maxAttempts; attempt++) { const result = await query(address); if (result.state === "exists" && result.data.isActiveForAnonymousUsage) { return result.data; } await new Promise((r) => setTimeout(r, 2000)); // wait 2s between polls } throw new Error("Registration not confirmed after polling"); } ``` ### Query Multiple Balances at Once Use the array-based query to check several token balances in a single round-trip: ```typescript theme={null} const MINTS = [ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", // USDT "So11111111111111111111111111111111111111112", // wSOL ]; const query = getEncryptedBalanceQuerierFunction({ client }); const results = await query(MINTS); const activeBalances = [...results.entries()] .filter(([, result]) => result.state === "shared" || result.state === "mxe") .map(([mint]) => mint); console.log("Mints with active encrypted balances:", activeBalances); ``` ## Error Handling Both query functions are read-only - they never submit transactions. Use `isQueryError` from `@umbra-privacy/sdk/errors` and `switch` on `err.stage` to handle each failure point. ```typescript theme={null} import { isQueryError } from "@umbra-privacy/sdk/errors"; try { const result = await query(userAddress); // or: const balances = await queryBalance([USDC, USDT]); } catch (err) { if (isQueryError(err)) { switch (err.stage) { case "pda-derivation": // Could not derive the account's program address - unexpected on-chain state. console.error("PDA derivation failed:", err.message); break; case "account-fetch": // RPC node unreachable, returned an HTTP error, or returned a JSON-RPC error. console.error("RPC error fetching account:", err.message); showNotification("Could not reach the network. Please check your connection."); break; case "account-decode": // Account data exists on-chain but could not be decoded. // This indicates unexpected on-chain state - not a transient error. console.error("Account data could not be decoded:", err.message); break; case "key-derivation": // X25519 key derivation failed (encrypted balance query only). console.error("Key derivation failed:", err.message); break; case "decryption": // Rescue cipher decryption failed (encrypted balance query only). console.error("Balance decryption failed:", err.message); break; default: // Other stages: initialization. console.error("Query failed at stage:", err.stage, err); } } else { throw err; } } ``` Query functions do not throw when an account does not exist - they return `{ state: "non_existent" }` instead. Errors are reserved for infrastructure failures (RPC unreachable, malformed data). See [Error Handling](/reference/errors) for a full reference of all error types. # Registration Source: https://sdk.umbraprivacy.com/sdk/registration getUserRegistrationFunction: 3-step idempotent flow that creates on-chain user account, registers X25519 encryption key (confidential), and stores user commitment (anonymous). ## Overview Before a user can deposit tokens or interact with the mixer, they need an on-chain **Umbra user account**. Registration creates this account and optionally registers the cryptographic keys needed for encrypted balances and anonymous transfers. Registration is idempotent - it checks on-chain state before each step and skips any steps already completed, including handling key rotation when keys have changed. That said, each call may submit on-chain transactions with SOL costs, so in practice you should [check whether the account is already registered](./account-state) before calling it. ## Usage ```typescript theme={null} import { getUserRegistrationFunction } from "@umbra-privacy/sdk"; const register = getUserRegistrationFunction({ client }); const signatures = await register({ confidential: true, // enable encrypted balances (Shared mode) anonymous: true, // enable mixer / anonymous transfers }); console.log(`Completed ${signatures.length} registration transaction(s)`); // 0 if already fully registered // 1–3 depending on which steps were needed ``` ## Options Register the X25519 key for Shared encryption mode. Without this, deposits use MXE-only mode and you cannot query your balance locally. Register the user commitment for mixer / anonymous transfer support. Can be used independently of `confidential`. Override the commitment level used for on-chain account reads during registration. Defaults to `"confirmed"`. Override the commitment level used for epoch info reads (relevant for Token-2022 transfer fee calculations). Defaults to `"confirmed"`. Optional lifecycle hooks called before and after each step. `pre` receives the signed transaction before it is sent; `post` receives the transaction and its confirmed signature. Skipped steps do not invoke callbacks. ```typescript theme={null} await register({ confidential: true, anonymous: true, callbacks: { userAccountInitialisation: { pre: async (tx) => { console.log("Creating account..."); }, post: async (tx, sig) => { console.log("Account created:", sig); }, }, registerX25519PublicKey: { pre: async (tx) => { console.log("Registering encryption key..."); }, post: async (tx, sig) => { console.log("Key registered:", sig); }, }, registerUserForAnonymousUsage: { pre: async (tx) => { console.log("Registering commitment..."); }, post: async (tx, sig) => { console.log("Commitment registered:", sig); }, }, }, }); ``` ## The Three Registration Steps Registration performs up to three on-chain transactions, in order: Creates the `EncryptedUserAccount` PDA - a [program-derived address](https://solana.com/docs/core/pda) that serves as the root of your Umbra identity. This is always the first step. Derives your [X25519](https://www.rfc-editor.org/rfc/rfc7748) public key from your master seed and stores it on-chain. This key enables **Shared encryption mode** - deposits will be encrypted under both the Arcium MPC key and your key, allowing you to decrypt your own balance locally. Required if `confidential: true`. Registers your [Poseidon](https://eprint.iacr.org/2019/458.pdf) user commitment and encrypts your master viewing key on-chain via a [Groth16](https://eprint.iacr.org/2016/260.pdf) ZK proof. This is the step that enables the **mixer / anonymous transfer** flow. Required if `anonymous: true`. All three steps require the master seed to be derived (overridable via [dependency injection](/sdk/understanding-the-sdk/dependency-injection#master-seed-storage)). The wallet signing prompt will appear on step 1 if the seed has not been derived yet in this session. ## On-Chain Account After registration, the user's `EncryptedUserAccount` PDA stores: * X25519 public key (for Shared mode deposits addressed to this user) * Poseidon user commitment (for ZK proof generation) * Generation index (monotonic counter used for nonce derivation) * Random generation seed (entropy mixed into nonces) * Status flags for each registration step This account is the persistent on-chain identity for the wallet address. It does not hold any tokens. ## Error Handling Registration can fail at several distinct points. Use `isRegistrationError` from `@umbra-privacy/sdk/errors` and `switch` on `err.stage` to handle each one. ```typescript theme={null} import { isRegistrationError } from "@umbra-privacy/sdk/errors"; try { await register({ confidential: true, anonymous: true }); } catch (err) { if (isRegistrationError(err)) { switch (err.stage) { case "master-seed-derivation": // User declined to sign the master seed derivation message. // Without the seed, no keys can be derived and registration cannot proceed. showNotification("Please sign the master seed message to set up your account."); break; case "transaction-sign": // User rejected one of the registration transactions in their wallet. showNotification("Registration cancelled."); break; case "zk-proof-generation": // ZK proof generation failed. Only applies to the commitment step (anonymous: true). console.error("Proof generation failed:", err.message); showNotification("Failed to generate proof. Please try again."); break; case "account-fetch": // RPC connectivity issue while fetching on-chain account state. console.error("RPC error during registration:", err.message); break; case "transaction-send": // Transaction submitted but confirmation timed out. // The transaction may have landed - check on-chain before retrying. console.warn("Confirmation timeout. Check on-chain before retrying."); break; default: console.error("Registration failed at stage:", err.stage, err); } } else { throw err; } } ``` **Partial completion**: Registration submits up to three separate transactions. If the second or third transaction fails or is cancelled, the first is already confirmed on-chain. Call `register()` again - it checks on-chain state and skips the steps that already completed. See [Error Handling](/reference/errors) for a full reference of all error types. # Transfers Source: https://sdk.umbraprivacy.com/sdk/transfers An overview of the two transfer types in Umbra - unlinkable transfers via the mixer and confidential-only transfers via encrypted balances. ## Overview Umbra supports two distinct models for transferring tokens privately. They differ in what they hide, what infrastructure they require, and how strong their privacy guarantees are. ## Unlinkable Transfers (via the Mixer) The strongest privacy model. The on-chain link between the sender's deposit and the recipient's withdrawal is broken entirely - an observer cannot connect the two events by examining the chain. Tokens enter the shielded pool as a UTXO commitment inserted into an on-chain Indexed Merkle Tree. The claim is later submitted with a Groth16 ZK proof that proves ownership of the UTXO without revealing which commitment is being spent. The deposit and the withdrawal are indistinguishable from every other deposit and withdrawal in the same tree. Both entry and exit can be from any account type: * **From an encrypted balance** - tokens are drawn from an encrypted balance before entering the shielded pool * **From a public wallet (ATA)** - tokens enter the shielded pool directly from a public Associated Token Account On exit, the recipient can claim tokens into either a public wallet (ATA) or an encrypted balance. This gives four source configurations in total, each available in both self-claimable and receiver-claimable variants. See the [Mixer](/sdk/mixer/overview) section for the full API. Unlinkable transfers require full registration with `anonymous: true`. The indexer must also be configured on the client for UTXO discovery. ## Confidential-Only Transfers (via Encrypted Balances) A lighter-weight privacy model that hides token amounts and balances on-chain, but does not break the link between sender and recipient. The transfer is confidential - amounts are hidden under MPC encryption - but the participating addresses remain observable. This covers scenarios where amount privacy is the goal and linkability is an acceptable trade-off: for example, moving tokens between accounts you control without exposing the amounts, or settling a payment where both parties know each other but want the value hidden. SDK support for confidential-only transfers is a work in progress and will be available shortly. The on-chain program already supports confidential transfers - the SDK factory functions are being finalised. # Branded Types Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/branded-types Branded types with phantom symbol branding: createU64, createU128, createBn254FieldElement, createX25519PublicKey. Prevents silent bigint/Uint8Array misuse at compile time, zero runtime cost. ## Why Branded Types Exist TypeScript uses **structural typing** — two types with the same underlying representation are treated as identical by the compiler. This means a raw `bigint` that represents an encrypted balance ciphertext is structurally the same as one that represents a Poseidon encryption key, or a U64 token amount. Without extra guardrails, you can pass the wrong value and TypeScript will not warn you. The SDK uses **branded types** (also called nominal types) to prevent this class of silent bug. Every cryptographic and numeric value in the SDK carries a phantom brand that makes it distinct from every other value with the same base type — at compile time, with zero runtime overhead. ```typescript theme={null} // Without branding — TypeScript accepts both, one is silently wrong: function encryptBalance(key: bigint, plaintext: bigint) { ... } encryptBalance(myBalance, myKey); // Oops — args are swapped, no error // With branding — the compiler catches the swap: function encryptBalance(key: RcKey, plaintext: RcPlaintext) { ... } encryptBalance(myBalance, myKey); // Error: RcPlaintext is not assignable to RcKey ``` *** ## The Type Hierarchy The SDK's branded types form a hierarchy. A more specific type is always assignable to its parent, but a parent is never assignable to a more specific child without an explicit helper call. **Integer types** (base: `bigint`): * `U64` — 64-bit unsigned integer (`0` to `2^64 - 1`). Used for token amounts and instruction parameters. * `U128` — 128-bit unsigned integer. Used for instruction seeds and generation indices. * `U256` — 256-bit unsigned integer. Used for UTXO commitment preimages and random generation seeds. **Cryptographic field elements** (base: `bigint`, extend `U256`): * `Bn254FieldElement` — BN254 scalar field element. Strict upper bound: the BN254 field prime. * `Curve25519FieldElement` — Curve25519 field element. Strict upper bound: `2^255 - 19`. **Poseidon types** (extend `Bn254FieldElement`): * `PoseidonKey` — Encryption key for the Poseidon cipher scheme. Derived from the user's master seed. **Rescue Cipher types** (extend `Curve25519FieldElement`): * `RcPlaintext` — An unencrypted balance value. * `RcCiphertext` — An encrypted balance value stored on-chain. * `RcKey` — A session encryption key derived from the Poseidon key and account nonce. * `RcEncryptionNonce` — The monotonically increasing counter (derived from `generationIndex`) that ensures each encryption produces a distinct ciphertext. **Protocol types** (base: `Uint8Array` or `bigint`): * `OptionalData32` — A 32-byte opaque payload stored alongside deposits and claims. * `MicroLamportsPerAcu` — Priority fee in micro-lamports per Arcium Computation Unit. **Byte array types** (base: `Uint8Array`): * `X25519PublicKey`, `X25519PrivateKey`, `SharedSecret` — 32-byte X25519 values. * Sized little-endian/big-endian variants (`U64LeBytes`, `U256BeBytes`, etc.) for serialisation. *** ## The `create*` Helper Functions Raw `bigint` or `Uint8Array` values you read from on-chain accounts, compute locally, or receive from external sources are unbranded. Before passing them to any SDK function that expects a branded type, run them through the corresponding `create*` helper. Each helper: 1. **Validates** the value at runtime (range check, type check, length check). 2. **Brands** the value — returns it typed as the specific branded type. 3. **Throws** a descriptive error (never silently returns `undefined`) if validation fails. Import helpers from `@umbra-privacy/sdk`: ```typescript theme={null} import { createU64, createU128, createU256, createBn254FieldElement, createCurve25519FieldElement, createPoseidonKey, createRcPlaintext, createRcCiphertext, createRcKey, createRcEncryptionNonce, createBase85Limb, createOptionalData32, createMicroLamportsPerAcu, } from "@umbra-privacy/sdk"; ``` ### Mathematics Helpers These are the helpers you will use most often. Token amounts, offsets, and protocol-level integers all go through here. ```typescript theme={null} // createU64 — validate and brand a bigint as a 64-bit unsigned integer. // Use for token amounts, fee values, and any U64 parameter. const amount: U64 = createU64(1_000_000n, "amount"); // Throws MathematicsAssertionError for negative or oversized values: createU64(-1n); // MathematicsAssertionError createU64(2n ** 64n); // MathematicsAssertionError ``` ```typescript theme={null} // createU128 — validate and brand a bigint as a 128-bit unsigned integer. // Use for instruction seeds and generation-index-derived values. const seed: U128 = createU128(340282366920938463463374607431768211455n, "seed"); ``` ```typescript theme={null} // createU256 — validate and brand a bigint as a 256-bit unsigned integer. // Use for UTXO commitment preimages, random generation seeds, and full-width scalars. const commitment: U256 = createU256(rawCommitmentBigint, "utxoCommitment"); ``` ### Field Element Helpers ```typescript theme={null} // createBn254FieldElement — validates that the value is in the BN254 scalar field. // Required for Groth16 ZK proof public inputs and Poseidon hash inputs. const publicInput: Bn254FieldElement = createBn254FieldElement(leafHash, "leafHash"); ``` ```typescript theme={null} // createCurve25519FieldElement — validates that the value is in the Curve25519 field. // Required for X25519 key derivation. const privKey: Curve25519FieldElement = createCurve25519FieldElement(rawKey, "x25519PrivateKey"); ``` ```typescript theme={null} // createPoseidonKey — validates a BN254 field element as a Poseidon cipher key. // Keys are derived from the user's master seed; call this after derivation. const encKey: PoseidonKey = createPoseidonKey(derivedKeyBigint); ``` ### Rescue Cipher Helpers The Rescue Cipher encrypts on-chain encrypted balances. Keeping plaintexts, ciphertexts, keys, and nonces as distinct types prevents the most dangerous class of encryption bug — passing a ciphertext where the decryptor expects a key. ```typescript theme={null} // createRcPlaintext — the unencrypted balance value before encryption. const balance: RcPlaintext = createRcPlaintext(rawBalance, "mxeBalance"); // createRcCiphertext — the encrypted balance stored on-chain. const ct: RcCiphertext = createRcCiphertext(rawCiphertext, "encryptedBalance"); // createRcKey — the session key passed to the Rescue Cipher. const sessionKey: RcKey = createRcKey(derivedKey, "rcKey"); // createRcEncryptionNonce — the per-account counter derived from generationIndex. const nonce: RcEncryptionNonce = createRcEncryptionNonce(generationIndex, "encryptionNonce"); ``` ### Protocol Helpers ```typescript theme={null} // createOptionalData32 — validates a Uint8Array as a 32-byte optional metadata payload. // Used as the optionalData field in deposit, withdraw, and UTXO operations. const noData: OptionalData32 = createOptionalData32(new Uint8Array(32)); // createMicroLamportsPerAcu — validates a bigint as a priority fee (>= 0). // Pass to priorityFees in operation options to speed up processing under load. const fee: MicroLamportsPerAcu = createMicroLamportsPerAcu(1000n); ``` ### Limb Helper ```typescript theme={null} // createBase85Limb — validates a bigint as a 85-bit limb (0 to 2^85 - 1). // Used internally when decomposing 256-bit values into three limbs for ZK circuit inputs. const limb0: Base85Limb = createBase85Limb(value & ((1n << 85n) - 1n), "limb0"); const limb1: Base85Limb = createBase85Limb((value >> 85n) & ((1n << 85n) - 1n), "limb1"); const limb2: Base85Limb = createBase85Limb(value >> 170n, "limb2"); ``` *** ## Common Patterns ### Converting a user-supplied number to U64 User-facing forms and external APIs often produce plain `number` or `string`. Always validate before passing to the SDK: ```typescript theme={null} import { createU64 } from "@umbra-privacy/sdk"; // User input from a form field (string) const rawInput = "1000000"; // 1 USDC (6 decimals) const amount: U64 = createU64(BigInt(rawInput), "depositAmount"); // Call the SDK function — amount is now type-safe const signature = await deposit("DestinationAddress", mint, amount); ``` ### Reading on-chain data and re-branding Account data fetched via RPC arrives as raw `bigint`. Validate before any cryptographic operation: ```typescript theme={null} import { createRcCiphertext } from "@umbra-privacy/sdk"; // rawAccount.ciphertext is bigint from on-chain deserialization const ciphertext: RcCiphertext = createRcCiphertext( rawAccount.ciphertext, "onChainCiphertext", ); ``` ### The `assert*` vs `create*` distinction The SDK exports both families. Use `create*` (not `assert*`) in expression contexts — `assert*` returns `void` and forces a two-step pattern: ```typescript theme={null} // ❌ Verbose — requires a temporary variable and an unsafe cast assertU64(rawAmount, "amount"); const amount = rawAmount as unknown as U64; // ✓ Concise — validates and brands in a single expression const amount = createU64(rawAmount, "amount"); ``` ### Error handling All helpers throw a typed error on failure. Catch at the boundary where you receive external input: ```typescript theme={null} import { createU64, MathematicsAssertionError } from "@umbra-privacy/sdk"; try { const amount = createU64(BigInt(userInput), "amount"); await deposit(destination, mint, amount); } catch (err) { if (err instanceof MathematicsAssertionError) { // Show a validation message to the user setError(`Invalid amount: ${err.message}`); } else { throw err; } } ``` ### Passing branded values through the SDK Once a value is branded, you can pass it directly to any SDK function that accepts that type — no additional wrapping needed: ```typescript theme={null} import { createU64, createU256 } from "@umbra-privacy/sdk"; const amount: U64 = createU64(depositAmountBigint, "amount"); const seed: U256 = createU256(randomSeedBigint, "randomSeed"); // Both are now type-safe for use in SDK operations const signature = await createSelfClaimableUtxo(amount, seed); ``` *** ## What the SDK Does Not Accept You cannot pass a plain `bigint` where a branded type is required — the TypeScript compiler will reject the call: ```typescript theme={null} const raw: bigint = 1_000_000n; // Error: Argument of type 'bigint' is not assignable to parameter of type 'U64' await deposit(destination, mint, raw); // Correct: await deposit(destination, mint, createU64(raw, "amount")); ``` This is intentional. The validation the `create*` helpers perform is not optional — it is the mechanism that guarantees protocol invariants hold before values reach the on-chain program. # Transaction Callbacks Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/callbacks TransactionCallbacks interface: PreTransactionCallback and PostTransactionCallback hooks fired around sign/send/confirm lifecycle. Used for progress UI, analytics, and transaction logging. ## Overview Every function that submits a transaction accepts an optional `callbacks` object on its options or options-like argument. Callbacks give you visibility into each transaction as it moves through the sign → send → confirm lifecycle. ```typescript theme={null} import type { TransactionCallbacks, PreTransactionCallback, PostTransactionCallback, } from "@umbra-privacy/sdk/interfaces"; ``` *** ## Callback Types ```typescript theme={null} // Called with the signed transaction immediately before it is sent type PreTransactionCallback = (transaction: SignedTransaction) => Promise; // Called with the transaction and its confirmed signature after it lands type PostTransactionCallback = ( transaction: SignedTransaction, signature: TransactionSignature, ) => Promise; interface TransactionCallbacks { readonly pre?: PreTransactionCallback; readonly post?: PostTransactionCallback; } ``` **Skipped steps do not invoke callbacks.** If a registration step is already complete on-chain, neither `pre` nor `post` fires for it. *** ## Usage - Registration Each of the three registration steps has its own `TransactionCallbacks` slot: ```typescript theme={null} const register = getUserRegistrationFunction({ client }); await register({ confidential: true, anonymous: true, callbacks: { userAccountInitialisation: { pre: async (tx) => setStatus("Creating account..."), post: async (tx, sig) => setProgress(33), }, registerX25519PublicKey: { pre: async (tx) => setStatus("Registering encryption key..."), post: async (tx, sig) => setProgress(66), }, registerUserForAnonymousUsage: { pre: async (tx) => setStatus("Enabling anonymous mode..."), post: async (tx, sig) => { setProgress(100); setStatus("Done!"); }, }, }, }); ``` *** ## Usage - UTXO Creation UTXO creation involves up to three transactions. Each has its own callbacks slot inside the `options` argument: ```typescript theme={null} // See /sdk/advanced/zk-provers for zkProver setup const createUtxo = getPublicBalanceToSelfClaimableUtxoCreatorFunction( { client }, { zkProver }, // e.g. getCreateSelfClaimableUtxoFromPublicBalanceProver(assetProvider) ); await createUtxo(recipient, mint, amount, { createUtxo: { pre: async (tx) => console.log("Submitting UTXO creation..."), post: async (tx, sig) => console.log("UTXO created:", sig), }, createProofAccount: { pre: async (tx) => console.log("Creating proof account..."), post: async (tx, sig) => console.log("Proof account:", sig), }, closeProofAccount: { // Only fires if a stale proof account was found and closed pre: async (tx) => console.log("Closing stale proof account..."), post: async (tx, sig) => console.log("Closed:", sig), }, }); ``` *** ## Usage - Single-Transaction Operations Deposits, withdrawals, compliance operations, and most other functions take a single top-level `callbacks` argument: ```typescript theme={null} const deposit = getPublicBalanceToEncryptedBalanceDirectDepositorFunction({ client }); await deposit(destinationAddress, mint, amount, { callbacks: { pre: async (tx) => console.log("Sending deposit..."), post: async (tx, sig) => console.log("Deposited:", sig), }, }); ``` # Dependency Injection Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/dependency-injection Dependency injection: args (required params) vs deps (injectable RPC, blockhash, tx forwarding, master seed). Client-level defaults with per-call commitment overrides on any factory function. ## Client-Level Defaults `getUmbraClient` accepts its own `deps` object that sets the **default** for every downstream function. Any function-level `deps` override takes precedence. ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; const client = await getUmbraClient( { signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl, }, { accountInfoProvider, // override RPC account fetching blockhashProvider, // override blockhash source transactionForwarder, // override broadcast + confirmation epochInfoProvider, // override epoch info masterSeedStorage: { // control seed persistence load, store, generate, }, }, ); ``` ```typescript theme={null} interface GetUmbraClientFromSignerDeps { readonly accountInfoProvider?: AccountInfoProviderFunction; readonly blockhashProvider?: GetLatestBlockhash; readonly transactionForwarder?: TransactionForwarder; readonly epochInfoProvider?: GetEpochInfo; readonly masterSeedStorage?: { readonly load?: MasterSeedLoaderFunction; readonly store?: MasterSeedStorerFunction; readonly generate?: MasterSeedGeneratorFunction; }; } ``` *** ## Infrastructure Providers These appear as optional overrides across virtually every function's `deps` type. ### `accountInfoProvider` Fetches raw on-chain account data. Used by every function that reads on-chain state. ```typescript theme={null} type AccountInfoProviderFunction = ( addresses: readonly Address[], ) => Promise>; ``` Default: RPC batch fetch via `rpcUrl`. Override for: unit tests (no live node), custom RPC middleware, account caching, or simulated state. ### `blockhashProvider` / `getLatestBlockhash` Returns the latest confirmed [blockhash](https://solana.com/docs/terminology#blockhash) and last valid block height for transaction construction. ```typescript theme={null} type GetLatestBlockhash = () => Promise<{ blockhash: string; lastValidBlockHeight: number; }>; ``` Default: RPC call via `rpcUrl`. Override for: deterministic transaction snapshots in tests, or to fetch from an alternative source. ### `transactionForwarder` Handles transaction broadcast and confirmation. Used by every function that submits on-chain transactions. ```typescript theme={null} interface TransactionForwarder { forwardSequentially(txs: readonly SignedTransaction[]): Promise; forwardInParallel(txs: readonly SignedTransaction[]): Promise; } ``` Default: [WebSocket](https://solana.com/docs/rpc/websocket/signaturesubscribe)-based forwarder using `signatureSubscribe` for confirmation. Override for [Jito](https://www.jito.network/) bundle submission, priority fee management, dry-run recording in tests, or custom retry logic: ```typescript theme={null} const jitoForwarder: TransactionForwarder = { forwardSequentially: async (txs) => submitToJitoBundleEndpoint(txs), forwardInParallel: async (txs) => submitToJitoBundleEndpoint(txs), }; ``` ### `epochInfoProvider` Returns current Solana epoch information. Used for [Token-2022 transfer fee](/advanced/token-2022) schedule selection. ```typescript theme={null} type GetEpochInfo = () => Promise; ``` Default: RPC call via `rpcUrl`. *** ## Master Seed Storage Controls how the master seed is persisted and derived between sessions. Provided at the client level only. ```typescript theme={null} interface MasterSeedStorage { load: () => Promise<{ exists: true; seed: MasterSeed } | { exists: false }>; store: (seed: MasterSeed) => Promise; generate: () => Promise; // called when load returns exists: false } ``` Default: in-memory (no persistence across page loads). ```typescript theme={null} // CI - fixed deterministic seed, no wallet prompt const client = await getUmbraClient(args, { masterSeedStorage: { generate: async () => FIXED_TEST_SEED, }, }); // Production - persist across refreshes in encrypted sessionStorage const client = await getUmbraClient(args, { masterSeedStorage: { load: async () => { const raw = sessionStorage.getItem("umbra_seed"); return raw ? { exists: true, seed: hexToBytes(raw) } : { exists: false }; }, store: async (seed) => { sessionStorage.setItem("umbra_seed", bytesToHex(seed)); }, }, }); ``` See [Key Derivation](/sdk/advanced/key-derivation) for how the master seed is structured and derived from the wallet signature. # Key Generators & Crypto Operations Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/key-generators Injectable key generators: KMAC256-based MVK and blinding factor derivers, Poseidon user commitment generators, X25519 keypair generators. Override any derivation step via deps injection. ## Key Derivation Generator Overrides All cryptographic keys are derived from the master seed via [KMAC256](/sdk/advanced/cryptography/kmac256) or [Poseidon](/sdk/advanced/cryptography/poseidon). Every derivation step is injectable. The following generator types appear as optional `deps` across the functions that use them. ### Master Viewing Key Generators ```typescript theme={null} type MasterViewingKeyGeneratorFunction = () => Promise; type MasterViewingKeyBlindingFactorGeneratorFunction = () => Promise; ``` Used by: registration, UTXO creation (self-claimable and receiver-claimable), UTXO claiming. ### Poseidon Private Key Generators ```typescript theme={null} type PoseidonPrivateKeyGeneratorFunction = () => Promise; type PoseidonBlindingFactorGeneratorFunction = () => Promise; ``` Used by: registration, UTXO creation, UTXO claiming. ### X25519 / Curve25519 Keypair Generators ```typescript theme={null} // User account X25519 key - used for token account encryption type Curve25519KeypairGeneratorFunction = () => Promise; // Per-mint X25519 key - used for UTXO ciphertext addressing type MintX25519KeypairGeneratorFunction = (mint: Address) => Promise; ``` Used by: registration, UTXO creation. Also exposed via `getMintEncryptionKeyRotatorFunction` deps. ### Viewing Key Generators (Poseidon sub-hierarchy) ```typescript theme={null} type MintViewingKeyGeneratorFunction = ( mint: Address, options?: ViewingKeyGeneratorOptions, ) => Promise; type YearlyViewingKeyGeneratorFunction = ( mint: Address, year: Year, options?: ViewingKeyGeneratorOptions, ) => Promise; type MonthlyViewingKeyGeneratorFunction = ( mint: Address, year: Year, month: Month, options?: ViewingKeyGeneratorOptions, ) => Promise; type DailyViewingKeyGeneratorFunction = ( mint: Address, year: Year, month: Month, day: Day, options?: ViewingKeyGeneratorOptions, ) => Promise; type SecondViewingKeyGeneratorFunction = ( mint: Address, year: Year, month: Month, day: Day, hour: Hour, minute: Minute, second: Second, options?: ViewingKeyGeneratorOptions, ) => Promise; ``` Used by: UTXO creation (the second viewing key scopes the UTXO ciphertext). ### Commitment Factor Generators ```typescript theme={null} type RescueEncryptionCommitmentBlindingFactorGeneratorFunction = () => Promise; type RandomFactorForPolynomialCommitmentGeneratorFunction = () => Promise; type PoseidonKeystreamBlindingFactorGeneratorFunction = () => Promise; ``` ### Ephemeral UTXO Key Generators Each UTXO operation uses per-UTXO ephemeral keys parameterized by the on-chain generation index. All are injectable: ```typescript theme={null} type EphemeralUtxoMasterViewingKeyGeneratorFunction = (offset: U256) => Promise; type EphemeralUtxoMasterViewingKeyBlindingFactorGeneratorFunction = (offset: U256) => Promise; type EphemeralUtxoPoseidonPrivateKeyGeneratorFunction = (offset: U256) => Promise; type EphemeralUtxoPoseidonPrivateKeyBlindingFactorGeneratorFunction = (offset: U256) => Promise; type EphemeralUtxoNullifierGeneratorFunction = (offset: U256) => Promise; type EphemeralUtxoH2RandomSecretGeneratorFunction = (offset: U256) => Promise; ``` Used by: all UTXO creation and UTXO claiming functions. *** ## Cryptographic Operation Overrides Beyond key generation, the low-level cryptographic operations themselves are injectable. ### Rescue Cipher (RC) - Encryption and Decryption The [Rescue cipher](/sdk/advanced/cryptography/rescue-cipher) is used to encrypt UTXO payloads and encrypted token account balances. ```typescript theme={null} type RcEncryptorFunction = ( plaintext: readonly RcPlaintext[], ) => Promise<{ ciphertexts: RcCiphertext[]; nonce: RcEncryptionNonce }>; type RcEncryptorWithNonceFunction = ( plaintext: readonly RcPlaintext[], nonce: RcEncryptionNonce, ) => Promise; type RcDecryptorFunction = ( ciphertext: readonly RcCiphertext[], nonce: RcEncryptionNonce, ) => Promise; type RcKeyGeneratorFunction = ( counters: readonly RcCounter[], nonce: RcEncryptionNonce, ) => Promise; ``` * `getRcEncryptor` and `getRcKeyGenerator` appear in: registration, UTXO creation deps * `rcDecryptor` appears in: `getEncryptedBalanceQuerierFunction` deps - override to provide a custom decryption backend ### AES Encryption [AES-GCM](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf) is used for symmetric encryption of UTXO commitment payloads within the mixer. ```typescript theme={null} type AesEncryptorFunction = ( key: AesKey, plaintext: AesPlaintext, ) => Promise; type AesDecryptorFunction = ( key: AesKey, ciphertext: AesCiphertextWithMetadata, ) => Promise; ``` Used by: UTXO creation functions. ### Poseidon Hash, Encryption, and Keystream See [Poseidon](/sdk/advanced/cryptography/poseidon) for the hash construction and security properties. ```typescript theme={null} type PoseidonHashFunction = ( dataPoints: readonly Bn254FieldElement[], ) => Promise; type PoseidonAggregatorHashFunction = ( dataPoints: readonly Bn254FieldElement[], ) => Promise; type PoseidonEncryptorFunction = ( plaintext: readonly PoseidonPlaintext[], key: PoseidonKey, ) => Promise; type PoseidonKeystreamGeneratorFunction = ( counters: readonly PoseidonCounter[], key: PoseidonKey, ) => Promise>; type KeystreamCommitmentFunction = ( keystream: Bn254FieldElement, blindingFactor: Bn254FieldElement, ) => Promise; ``` Used by: registration, UTXO creation, UTXO claiming. ### Fiat-Shamir Challenge and Polynomial Evaluation ```typescript theme={null} type FiatShamirChallengeGeneratorFunction = ( input: Uint8Array, ) => Curve25519FieldElement; type ChallengePowersFunction = ( challenge: Curve25519FieldElement, maxPower: number, ) => Curve25519FieldElement[]; type PolynomialEvaluatorFunction = ( coefficients: Curve25519FieldElement[], point: Curve25519FieldElement, ) => Curve25519FieldElement; ``` Used by: registration, UTXO creation. Override to run evaluation in a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) or swap in a WASM implementation. ### User Commitment Generator ```typescript theme={null} type UserCommitmentGeneratorFunction = ( masterViewingKey: Bn254FieldElement, masterViewingKeyBlindingFactor: Bn254FieldElement, poseidonPrivateKey: Bn254FieldElement, poseidonBlindingFactor: Bn254FieldElement, ) => Promise; ``` Used by: registration, UTXO creation. # Key Rotation & Deps Reference Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/key-rotation Offset-based key rotation: U512 offsets on getUmbraClient shift KMAC256 derivation to produce new X25519, MVK, and commitment keys for the same wallet. Complete deps map per factory function. ## Key Rotation via Offsets Key rotation is a distinct mechanism from dependency injection. Rather than replacing the generator function, you increment a numeric offset at client construction time. This causes the [KMAC256](/sdk/advanced/cryptography/kmac256) derivation to produce a completely different key for the same wallet, without changing any code. ```typescript theme={null} const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl, offsets: { x25519UserAccountPrivateKey: 1n, // rotate the token encryption key poseidonPrivateKey: 0n, masterViewingKey: 0n, x25519MasterViewingKeyEncryptingPrivateKey: 0n, mintX25519PrivateKey: 0n, rescueCommitmentBlindingFactor: 0n, randomCommitmentFactor: 0n, }, }); ``` Available offset keys: * `masterViewingKey` * `poseidonPrivateKey` * `x25519UserAccountPrivateKey` * `x25519MasterViewingKeyEncryptingPrivateKey` * `mintX25519PrivateKey` * `rescueCommitmentBlindingFactor` * `randomCommitmentFactor` Rotating an offset produces a different derived key. Any on-chain state - registered X25519 keys, encrypted balances, compliance grants - was created under the old key and cannot be accessed with the rotated key. Always re-register after rotating the user account key, and update any grants that referenced the old MVK X25519 key. *** ## Per-Function Deps Reference A complete map of which injectable deps each factory function accepts. ### Registration **`getUserRegistrationFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` * `masterViewingKeyGenerator`, `masterViewingKeyBlindingFactorGenerator` * `poseidonPrivateKeyGenerator`, `poseidonBlindingFactorGenerator` * `userAccountX25519KeypairGenerator`, `masterViewingKeyEncryptingX25519KeypairGenerator` * `mintX25519KeypairGenerator` * `rescueCommitmentBlindingFactorGenerator`, `randomFactorGenerator` * `getRcKeyGenerator`, `getRcEncryptor`, `rescueCommitmentGenerator` * `userCommitmentGenerator` * `fiatShamirChallengeGenerator`, `challengePowersFunction`, `polynomialEvaluator` * `poseidonAggregator` * `bn254ModInverter`, `computeLimbwiseSumInverse` * `zkProver` (`IZkProverForUserRegistration`) - **required** ### Deposit **`getPublicBalanceToEncryptedBalanceDirectDepositorFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder`, `getEpochInfo` ### Withdrawal **`getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` ### Query **`getUserAccountQuerierFunction`** * `accountInfoProvider` **`getEncryptedBalanceQuerierFunction`** * `accountInfoProvider` * `rcDecryptor` - override to provide a custom Rescue cipher decryption backend **`getClaimableUtxoScannerFunction`** * `accountInfoProvider` * `merkleProofFetcher` - override to fetch Merkle proofs from a custom indexer * `utxoDataFetcher` - override to fetch UTXO data from a custom indexer ### UTXO Creation - from Encrypted Balance **`getEncryptedBalanceToSelfClaimableUtxoCreatorFunction`** * `accountInfoProvider`, `blockhashProvider`, `transactionForwarder` * `masterViewingKeyGenerator`, `masterViewingKeyBlindingFactorGenerator` * `poseidonPrivateKeyGenerator`, `poseidonBlindingFactorGenerator` * `userAccountX25519KeypairGenerator` * `secondViewingKeyGenerator` * `ephemeralUtxoMasterViewingKeyGenerator`, `ephemeralUtxoMasterViewingKeyBlindingFactorGenerator` * `ephemeralUtxoPoseidonKeyGenerator`, `ephemeralUtxoPoseidonKeyBlindingFactorGenerator` * `ephemeralUtxoNullifierGenerator`, `ephemeralUtxoH2RandomSecretGenerator` * `poseidonKeystreamBlindingFactorGenerator` * `poseidonHasher`, `poseidonEncryptor`, `poseidonKeystreamGenerator`, `keystreamCommitmentGenerator` * `rescueEncryptor`, `aesEncryptor` * `fiatShamirChallengeGenerator`, `userCommitmentGenerator`, `h2Generator` * `getUtcNow`, `generateRandomU256`, `getRandomComputationOffset` * `zkProver` (`IZkProverForSelfClaimableUtxo`) - **required** **`getEncryptedBalanceToReceiverClaimableUtxoCreatorFunction`** * Same set as self-claimable from encrypted balance * `zkProver` (`IZkProverForReceiverClaimableUtxo`) - **required** ### UTXO Creation - from Public Balance **`getPublicBalanceToSelfClaimableUtxoCreatorFunction`** * `accountInfoProvider`, `blockhashProvider`, `transactionForwarder` * `masterViewingKeyGenerator`, `poseidonPrivateKeyGenerator` * Ephemeral UTXO key generators (same as above) * Cryptographic operation functions (Poseidon, Rescue, AES, Fiat-Shamir) * `zkProver` (`IZkProverForSelfClaimableUtxo`) - **required** **`getPublicBalanceToReceiverClaimableUtxoCreatorFunction`** * Same set as self-claimable from public balance * `zkProver` (`IZkProverForReceiverClaimableUtxo`) - **required** ### UTXO Claiming **`getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` * Key derivation generators (master viewing key, Poseidon, X25519) * Ephemeral UTXO key generators * Cryptographic operation functions * `zkProver` (`IZkProverForClaimReceiverClaimableUtxoIntoEncryptedBalance`) - **required** **`getSelfClaimableUtxoToEncryptedBalanceClaimerFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` * Key derivation generators * Cryptographic operation functions * `zkProver` (`IZkProverForClaimSelfClaimableUtxoIntoEncryptedBalance`) - **required** **`getSelfClaimableUtxoToPublicBalanceClaimerFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` * Key derivation generators * Cryptographic operation functions * `zkProver` (`IZkProverForClaimSelfClaimableUtxoIntoPublicBalance`) - **required** ### Conversion **`getNetworkEncryptionToSharedEncryptionConverterFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` **`getMintEncryptionKeyRotatorFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` * `mintX25519KeypairGenerator` - override to supply the new keypair directly ### Compliance Grants **`getComplianceGrantIssuerFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` **`getComplianceGrantRevokerFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` **`getSharedCiphertextReencryptorForUserGrantFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` **`getUserComplianceGrantQuerierFunction`** * `accountInfoProvider` ### Miscellaneous **`getClaimStagedSolFromPoolFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` **`getClaimStagedSplFromPoolFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` **`getUpdateRandomGenerationSeedFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` **`getUpdateTokenAccountRandomGenerationSeedFunction`** * `accountInfoProvider`, `getLatestBlockhash`, `transactionForwarder` # The SDK Pattern Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/overview SDK architecture: factory pattern with get[Source]To[Target][Verb]Function naming, args (required params) vs deps (injectable providers), and per-call commitment overrides. ## Design Philosophy The Umbra SDK is written in the same **functional, dependency-injected style** as [Anza's `@solana/kit`](https://github.com/anza-xyz/solana-web3.js). Every capability is either a pure function or a factory that returns one. There are no classes with internal state, no singletons, and no implicit globals. Dependencies are explicit, injectable, and testable. This mirrors the architecture of `@solana/kit` itself - functions like `getTransferSolInstruction` and `createSolanaRpc` follow the same closure-based factory pattern you will find throughout the Umbra SDK. ## The Factory Pattern Every SDK operation follows the same two-step pattern: ```typescript theme={null} // Step 1 - build the function once, at setup time const deposit = getPublicBalanceToEncryptedBalanceDirectDepositorFunction({ client }); // Step 2 - call it at runtime const signature = await deposit(destinationAddress, mint, amount); ``` The factory call (`get*Function`) is cheap. It binds configuration and resolves defaults. The returned function is what does the actual async work - sending transactions, generating proofs, querying the indexer. This is the same pattern `@solana/kit` uses for its instruction builders: ```typescript theme={null} // @solana/kit instruction builder const ix = getTransferSolInstruction({ source, destination, amount }); // Umbra SDK factory function - same pattern, describing the data flow const withdraw = getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction({ client }); ``` ## Naming Conventions All factory functions use a consistent `get[Source]To[Target][Verb]Function` scheme. The name describes the data flow and operation: * `getPublicBalanceToEncryptedBalance...` - credits tokens from a public account to an encrypted account * `getEncryptedBalanceToPublicBalance...` - debits tokens from an encrypted account to a public account * `getEncryptedBalanceTo...UtxoCreator...` - creates a UTXO from an encrypted balance * `getPublicBalanceTo...UtxoCreator...` - creates a UTXO from a public balance * `get...UtxoToEncryptedBalanceClaimer...` - claims a UTXO into an encrypted balance * `get...UtxoToPublicBalanceClaimer...` - claims a UTXO into a public balance * `getClaimableUtxoScanner...` - scans for claimable UTXOs via the indexer * `get...Querier...` - queries on-chain state (RPC) * `get...Rotator...` - replaces a registered key * `get...Converter...` - migrates an account between protocol modes * `get...Reencryptor...` - re-encrypts ciphertexts under a new key * `getComplianceGrantIssuer...` - creates a compliance grant * `getComplianceGrantRevoker...` - deletes a compliance grant * `getUserRegistration...` - the full registration flow ## args and deps Every factory accepts two arguments: ```typescript theme={null} const fn = getOperationFunction(args, deps?); ``` * `args` - required. Always contains `client: IUmbraClient` and any fixed configuration for this operation (e.g. a specific mint). * `deps` - optional. Contains every infrastructure provider, key generator, and ZK prover used internally. All `deps` fields have sensible defaults derived from the `client`. The only exceptions are ZK provers, which have no default and must always be supplied explicitly. ```typescript theme={null} // Using all defaults const deposit = getPublicBalanceToEncryptedBalanceDirectDepositorFunction({ client }); // Overriding the transaction forwarder (e.g. Jito bundles) const deposit = getPublicBalanceToEncryptedBalanceDirectDepositorFunction( { client }, { transactionForwarder: jitoForwarder }, ); ``` Function-level `deps` override client-level `deps`. Client-level `deps` override built-in defaults. The **Advanced** section covers dependency injection internals, key generators, ZK provers, transaction callbacks, and key rotation in depth. Most applications do not need those details to get started. # Types & Interfaces Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/types SDK type system: branded types U64/U128/U256, Bn254FieldElement, X25519PublicKey. Integration with @solana/kit Address and TransactionSignature. Function types via /interfaces subpath. ## Anza Solana Kit Types The SDK is built on top of `@solana/kit` and shares its core types directly. You do not need to import `@solana/kit` yourself - these types flow through the SDK's own types naturally - but it helps to know where they come from: * [`Address`](https://github.com/anza-xyz/solana-web3.js/blob/main/packages/addresses/src/address.ts) - a base58-encoded public key string. Every `mint`, `destinationAddress`, and `userAddress` parameter in the SDK is an `Address`. * [`TransactionSignature`](https://github.com/anza-xyz/solana-web3.js/blob/main/packages/transaction-messages/src/transaction-message.ts) - a base58-encoded signature string. Returned by every operation that submits a transaction. * `SignableTransaction` / `SignedTransaction` - the versioned transaction types passed to `IUmbraSigner.signTransaction`. * `GetLatestBlockhash`, `GetEpochInfo` - RPC function types used in the `deps` of most factory functions. If you are writing unit tests or building a custom RPC layer, the [`@solana/kit` documentation](https://github.com/anza-xyz/solana-web3.js) is the primary reference for these types. ## Function Types Every returned function has a named type exported from `@umbra-privacy/sdk/interfaces`. Store functions in state, type React context values, or annotate variables without re-running the factory: ```typescript theme={null} import type { PublicBalanceToEncryptedBalanceDirectDepositorFunction, UserRegistrationFunction, ClaimableUtxoScannerFunction, SelfClaimableUtxoToEncryptedBalanceClaimerFunction, } from "@umbra-privacy/sdk/interfaces"; ``` This is useful when you build a factory function once and store it in React state or a context: ```typescript theme={null} const [deposit, setDeposit] = useState(null); useEffect(() => { if (client) { setDeposit(() => getPublicBalanceToEncryptedBalanceDirectDepositorFunction({ client })); } }, [client]); ``` The full list of exported function types mirrors the factory functions - one type per returned function. See the [Reference](/reference/overview) for the complete inventory. # ZK Prover Interfaces Source: https://sdk.umbraprivacy.com/sdk/understanding-the-sdk/zk-provers IZkProverSuite: six injectable Groth16 prover interfaces (one per Circom circuit). Each implements prove(inputs) returning {proofA, proofB, proofC}. Used by @umbra-privacy/web-zk-prover. ## Overview Every operation that generates a [Groth16](/sdk/advanced/cryptography/groth16) ZK proof requires a prover passed through `deps`. There is no hard-coded default - you must always supply one. All provers follow the same `prove(inputs) → { proofA, proofB, proofC }` contract. For instantiation, Web Worker setup, and remote proving backends, see [ZK Provers](/sdk/advanced/zk-provers). *** ## `IZkProverForUserRegistration` ```typescript theme={null} interface IZkProverForUserRegistration { prove(inputs: UserRegistrationCircuitInputs): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` Required by: `getUserRegistrationFunction` deps (`zkProver`). *** ## `IZkProverForSelfClaimableUtxo` ```typescript theme={null} interface IZkProverForSelfClaimableUtxo { prove(inputs: SelfClaimableUtxoCircuitInputs): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` Required by: `getEncryptedBalanceToSelfClaimableUtxoCreatorFunction` deps (`zkProver`). For public-balance UTXO creation, see `ZkProverForSelfClaimableUtxoFromPublicBalance` below. *** ## `IZkProverForReceiverClaimableUtxo` ```typescript theme={null} interface IZkProverForReceiverClaimableUtxo { prove(inputs: ReceiverClaimableUtxoCircuitInputs): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` Required by: `getEncryptedBalanceToReceiverClaimableUtxoCreatorFunction` deps (`zkProver`). For public-balance UTXO creation, see `ZkProverForReceiverClaimableUtxoFromPublicBalance` below. *** ## `IZkProverForClaimSelfClaimableUtxoIntoEncryptedBalance` ```typescript theme={null} interface IZkProverForClaimSelfClaimableUtxoIntoEncryptedBalance { readonly maxUtxoCapacity: 1; prove(inputs: ClaimSelfClaimableUtxoIntoEncryptedBalanceCircuitInputs): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` Required by: `getSelfClaimableUtxoToEncryptedBalanceClaimerFunction` deps (`zkProver`). *** ## `IZkProverForClaimReceiverClaimableUtxoIntoEncryptedBalance` ```typescript theme={null} interface IZkProverForClaimReceiverClaimableUtxoIntoEncryptedBalance { prove( inputs: ClaimReceiverClaimableUtxoIntoEncryptedBalanceCircuitInputs, nLeaves: ClaimBatchSize, ): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` Required by: `getReceiverClaimableUtxoToEncryptedBalanceClaimerFunction` deps (`zkProver`). This prover accepts a batch size `nLeaves` (1–16) because there are 16 distinct circuits, one per batch size. *** ## `IZkProverForClaimSelfClaimableUtxoIntoPublicBalance` ```typescript theme={null} interface IZkProverForClaimSelfClaimableUtxoIntoPublicBalance { readonly maxUtxoCapacity: 1; prove(inputs: ClaimSelfClaimableUtxoIntoPublicBalanceCircuitInputs): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` Required by: `getSelfClaimableUtxoToPublicBalanceClaimerFunction` deps (`zkProver`). *** ## `ZkProverForSelfClaimableUtxoFromPublicBalance` ```typescript theme={null} interface ZkProverForSelfClaimableUtxoFromPublicBalance { prove(inputs: SelfClaimableUtxoFromPublicBalanceCircuitInputs): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` Required by: `getPublicBalanceToSelfClaimableUtxoCreatorFunction` deps (`zkProver`). *** ## `ZkProverForReceiverClaimableUtxoFromPublicBalance` ```typescript theme={null} interface ZkProverForReceiverClaimableUtxoFromPublicBalance { prove(inputs: ReceiverClaimableUtxoFromPublicBalanceCircuitInputs): Promise<{ proofA: Groth16ProofA; proofB: Groth16ProofB; proofC: Groth16ProofC; }>; } ``` Required by: `getPublicBalanceToReceiverClaimableUtxoCreatorFunction` deps (`zkProver`). *** ## Unified Prover Suite All six provers can be combined into a single object and passed wherever individual provers are expected: ```typescript theme={null} interface IZkProverSuite { readonly registration: IZkProverForUserRegistration; readonly utxoSelfClaimable: IZkProverForSelfClaimableUtxo; readonly utxoReceiverClaimable: IZkProverForReceiverClaimableUtxo; readonly claimSelfClaimableIntoEncryptedBalance: IZkProverForClaimSelfClaimableUtxoIntoEncryptedBalance; readonly claimReceiverClaimableIntoEncryptedBalance: IZkProverForClaimReceiverClaimableUtxoIntoEncryptedBalance; readonly claimSelfClaimableIntoPublicBalance: IZkProverForClaimSelfClaimableUtxoIntoPublicBalance; } ``` ## Quick Setup with `@umbra-privacy/web-zk-prover` The `@umbra-privacy/web-zk-prover` package provides ready-made factory functions for each prover interface. It uses [snarkjs](https://github.com/iden3/snarkjs) Groth16 under the hood and fetches proving keys from a CDN. ```typescript theme={null} import { getUserRegistrationProver, getCreateSelfClaimableUtxoFromEncryptedBalanceProver, getCreateReceiverClaimableUtxoFromEncryptedBalanceProver, getCreateSelfClaimableUtxoFromPublicBalanceProver, getCreateReceiverClaimableUtxoFromPublicBalanceProver, getClaimSelfClaimableUtxoIntoEncryptedBalanceProver, getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver, getClaimSelfClaimableUtxoIntoPublicBalanceProver, } from "@umbra-privacy/web-zk-prover"; const registrationProver = getUserRegistrationProver(); const utxoSelfClaimableProver = getCreateSelfClaimableUtxoFromEncryptedBalanceProver(); const utxoReceiverClaimableProver = getCreateReceiverClaimableUtxoFromEncryptedBalanceProver(); const utxoSelfClaimableFromPublicProver = getCreateSelfClaimableUtxoFromPublicBalanceProver(); const utxoReceiverClaimableFromPublicProver = getCreateReceiverClaimableUtxoFromPublicBalanceProver(); const claimSelfClaimableEncryptedBalanceProver = getClaimSelfClaimableUtxoIntoEncryptedBalanceProver(); const claimReceiverClaimableEncryptedBalanceProver = getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(); const claimSelfClaimablePublicBalanceProver = getClaimSelfClaimableUtxoIntoPublicBalanceProver(); ``` Each factory accepts an optional `IZkAssetProvider` parameter - a simple interface with one method: `getAssetUrls(type, variant?)`. When no provider is passed, the factories default to the CDN provider (equivalent to calling `getCdnZkAssetProvider()` from `@umbra-privacy/web-zk-prover/cdn`), which fetches proving keys from Umbra's CDN. You can supply your own provider to customise asset resolution. See [ZK Provers](/sdk/advanced/zk-provers) for Web Worker setup, custom asset providers, and remote proving backends. # Wallet Adapters Source: https://sdk.umbraprivacy.com/sdk/wallet-adapters IUmbraSigner interface and adapters: createInMemorySigner (keypair), createSignerFromWalletAccount (Wallet Standard). Covers UMBRA_MESSAGE_TO_SIGN consent and signMessage requirements. ## The IUmbraSigner Interface The Umbra SDK requires a signer that implements `IUmbraSigner`: ```typescript theme={null} interface IUmbraSigner { readonly address: Address; signTransaction(tx: SignableTransaction): Promise; signTransactions(txs: readonly SignableTransaction[]): Promise; signMessage(message: Uint8Array): Promise; } ``` `signMessage` is critical - it is used to derive the master seed from a wallet signature. The other methods sign the Solana transactions that interact with the Umbra program. The SDK exports ready-made helper functions for every common signer source. You do not need to implement `IUmbraSigner` yourself. ## Option 1: In-Memory Keypair (Testing) Use the SDK's built-in helpers to generate or load an in-memory keypair. Best for scripts, CI, and local development. ```typescript theme={null} import { createInMemorySigner, createSignerFromPrivateKeyBytes, createSignerFromKeyPair, } from "@umbra-privacy/sdk"; // Generate a new random keypair const signer = await createInMemorySigner(); console.log("Address:", signer.address); // Load from raw bytes (64-byte keypair or 32-byte seed - @solana/kit accepts both) import { readFileSync } from "fs"; const keyFile = JSON.parse(readFileSync("/path/to/keypair.json", "utf8")); const signer = await createSignerFromPrivateKeyBytes(new Uint8Array(keyFile)); // Adapt an existing @solana/kit KeyPairSigner you already hold import { generateKeyPairSigner } from "@solana/kit"; const kps = await generateKeyPairSigner(); const signer = createSignerFromKeyPair(kps); ``` In-memory keypairs are ephemeral. If your process restarts, the keypair is gone. Only use this for testing or scripts where you control the key lifecycle. ## Option 2: Wallet Standard Browser Wallet (Production) Modern Solana wallets — Phantom, Backpack, Solflare, and others — implement the [Wallet Standard](https://github.com/wallet-standard/wallet-standard). Use `createSignerFromWalletAccount` to adapt them to `IUmbraSigner`. You obtain the `wallet` and `account` objects by discovering registered wallets with `getWallets()` from `@wallet-standard/app`, then calling the wallet's `standard:connect` feature. ```bash theme={null} pnpm add @wallet-standard/app @wallet-standard/base @wallet-standard/features ``` ```typescript theme={null} import { getWallets } from "@wallet-standard/app"; import type { Wallet, WalletAccount } from "@wallet-standard/base"; import { StandardConnect, StandardDisconnect } from "@wallet-standard/features"; import { createSignerFromWalletAccount, getUmbraClient, } from "@umbra-privacy/sdk"; // 1. Discover wallets that support Solana signing const { get } = getWallets(); const solanaWallets = get().filter((w) => { const features = Object.keys(w.features); return ( features.includes("solana:signTransaction") && features.includes("solana:signMessage") ); }); // 2. Connect to a wallet (prompts the user) const wallet = solanaWallets[0]; // e.g. Phantom const connectFeature = wallet.features[StandardConnect]; const { accounts } = await connectFeature.connect(); const account = accounts[0]; // 3. Create the Umbra signer const signer = createSignerFromWalletAccount(wallet, account); // 4. Create the Umbra client const client = await getUmbraClient({ signer, network: "mainnet", rpcUrl: "https://api.mainnet-beta.solana.com", rpcSubscriptionsUrl: "wss://api.mainnet-beta.solana.com", indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com", deferMasterSeedSignature: true, // prompt fires on first operation, not at connect time }); ``` For React applications, you can also use the `useWallets()` hook from `@wallet-standard/react` and the `useConnect()` / `useDisconnect()` hooks for a more ergonomic integration: ```typescript theme={null} import { useWallets, useConnect } from "@wallet-standard/react"; import { createSignerFromWalletAccount } from "@umbra-privacy/sdk"; ``` Re-create the client whenever the wallet connection changes. Since the client holds a reference to the signer, stale signers will cause transactions to fail. The wallet must support both `"solana:signTransaction"` and `"solana:signMessage"` features — an error is thrown immediately if either is missing. ### How signing works 1. The `@solana/kit` transaction is serialized to wire-format bytes via `getTransactionEncoder()` 2. The bytes are passed directly to the wallet's `solana:signTransaction` feature 3. The wallet returns signed wire bytes 4. `getTransactionDecoder()` reconstructs the `@solana/kit` `Transaction`, and signatures are merged back in This path is compatible with all Wallet Standard wallets and does not require `@solana/web3.js`. ## Master Seed Derivation The first time any cryptographic operation runs (typically during `register()`), the SDK calls `signer.signMessage` with a deterministic message to derive the **master seed** (overridable via [dependency injection](/sdk/understanding-the-sdk/dependency-injection#master-seed-storage)). This produces a one-time wallet signing prompt. The message that the user signs is a multi-paragraph legal consent and acknowledgement. It is exported as `UMBRA_MESSAGE_TO_SIGN` from `@umbra-privacy/sdk`: ```typescript theme={null} import { UMBRA_MESSAGE_TO_SIGN } from "@umbra-privacy/sdk"; ``` The signature is then hashed with KMAC256 (dkLen=64) to produce the 512-bit master seed. The master seed is derived deterministically from this signature. If the message changed, a completely different master seed would be produced - all previously registered keys would become inaccessible. Never modify the derivation message. After derivation, the seed is cached in memory for the lifetime of the client object. The prompt does not appear again unless the client is recreated. To control exactly when the prompt fires, use the `deferMasterSeedSignature` option in `getUmbraClient`. See [Creating a Client - Master Seed Derivation](/sdk/creating-a-client#master-seed-derivation). ## Persisting the Master Seed By default, the master seed lives only in memory. If you want to avoid re-deriving it on every page load, you can persist it using the `masterSeedStorage` dependency override: ```typescript theme={null} import { getUmbraClient } from "@umbra-privacy/sdk"; const client = await getUmbraClient( { signer, network: "mainnet", rpcUrl, rpcSubscriptionsUrl }, { masterSeedStorage: { load: async () => { const stored = sessionStorage.getItem("umbra:masterSeed"); if (!stored) return { exists: false }; return { exists: true, seed: new Uint8Array(JSON.parse(stored)) }; }, store: async (seed) => { sessionStorage.setItem("umbra:masterSeed", JSON.stringify(Array.from(seed))); }, }, } ); ``` The master seed is a 64-byte root secret equivalent in sensitivity to a private key. Only persist it in secure storage - never in `localStorage` in plaintext. `sessionStorage` is slightly better (cleared on tab close) but still not suitable for long-term storage. For production applications, consider deriving it on every session or using a secure enclave. # Withdraw Source: https://sdk.umbraprivacy.com/sdk/withdraw getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction: move tokens from encrypted balance to public ATA. Returns WithdrawResult with Arcium MPC callback confirmation. ## Overview The `withdraw` operation is the reverse of a deposit. It moves tokens from a user's **encrypted balance** back to their public **Associated Token Account (ATA)**. ``` Encrypted Balance (on-chain) | v Umbra Vault (on-chain SPL) --withdraw--> Your Public ATA ``` Arcium MPC verifies the withdrawal authorization. The operation follows the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern) - the SDK waits for both the handler and the callback transactions to confirm before returning. ## Usage ```typescript theme={null} import { getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction } from "@umbra-privacy/sdk"; const withdraw = getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction({ client }); const result = await withdraw( destinationAddress, // where tokens go (typically client.signer.address) mint, // SPL or Token-2022 mint address amount, // amount in native token units (U64) options?, // optional ); ``` ## Parameters The Solana address to receive the withdrawn tokens. This is typically `client.signer.address`. The SPL or Token-2022 mint address. Must match a mint for which the user has an existing encrypted balance. The amount to withdraw, in the token's native units. Must not exceed the current encrypted balance. Additional compute unit price in microlamports. Recommended during high network load. 32 bytes of arbitrary metadata stored with the withdrawal. Defaults to all zeros. Whether to wait for the Arcium MPC callback transaction to confirm before resolving. When `true`, the returned `WithdrawResult` includes `callbackSignature` and `callbackElapsedMs`. When `false`, returns immediately after the handler (queue) transaction confirms. Skip Solana transaction preflight simulation. Maximum number of times the RPC node should retry sending the transaction. Commitment level used for RPC account reads during withdrawal preparation. Overrides the default on a per-call basis. Commitment level used for fetching epoch info (Token-2022 transfer fee schedule). Overrides the default on a per-call basis. ## Return Value Returns a `Promise`. The `WithdrawResult` object contains: * `queueSignature` - The transaction signature of the handler (queue computation) transaction. * `callbackStatus` - The outcome of computation monitoring: `"finalized"`, `"pruned"`, or `"timed-out"`. Present when `awaitCallback` is `true` (the default). * `callbackSignature` - The transaction signature of the Arcium MPC callback transaction. Present when `callbackStatus` is `"finalized"`. * `callbackElapsedMs` - Wall-clock milliseconds spent waiting for the callback. Present when `awaitCallback` is `true`. * `rentClaimSignature` - Transaction signature for reclaiming rent from the computation account. Attempted regardless of callback outcome. * `rentClaimError` - Error encountered during rent reclaim, if any. The withdrawal itself still succeeded. ## Example ```typescript theme={null} import { getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction } from "@umbra-privacy/sdk"; const withdraw = getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction({ client }); const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const result = await withdraw(client.signer.address, USDC, 50_000_000n); // 50 USDC console.log("Queue signature:", result.queueSignature); console.log("Callback signature:", result.callbackSignature); ``` ## Withdrawal Destination The tokens are sent to the ATA associated with the `destinationAddress` for the given mint. If that ATA does not exist, the transaction will fail with an account-not-found error. Create the ATA first using standard token tooling if needed. ## Error Handling Withdrawal errors are typed with a `stage` field identifying exactly where in the pipeline the failure occurred: ```typescript theme={null} import { isEncryptedWithdrawalError } from "@umbra-privacy/sdk/errors"; try { const result = await withdraw(client.signer.address, mint, amount); } catch (err) { if (isEncryptedWithdrawalError(err)) { switch (err.stage) { case "validation": // Zero amount, or no encrypted balance exists for this mint. console.error("Withdrawal validation failed:", err.message); break; case "mint-fetch": // Could not fetch the mint account - check RPC connectivity and mint address. break; case "pda-derivation": // Could not derive required program addresses - unexpected on-chain state. break; case "instruction-build": // Could not construct the instruction - protocol state mismatch. break; case "transaction-sign": // User rejected the transaction in their wallet. showNotification("Withdrawal cancelled."); break; case "transaction-send": // Transaction submitted but confirmation timed out. // The transaction may have landed - check on-chain before retrying. console.warn("Confirmation timeout. Check signature on-chain:", err.cause); break; default: // Other stages: initialization, transaction-build, transaction-compile. console.error("Withdrawal failed at stage:", err.stage, err); } } else { throw err; } } ``` See [Error Handling](/reference/errors) for retry guidance and a full stage reference. ## Timing Withdrawals follow the [dual-instruction pattern](/concepts/how-umbra-works#the-dual-instruction-pattern). The SDK submits the handler transaction, waits for Arcium to complete the MPC computation, then waits for the callback transaction to confirm. Total wall-clock time is typically a few seconds on a well-connected network. # Supported Tokens Source: https://sdk.umbraprivacy.com/supported-tokens List of SPL and Token-2022 tokens with active shielded pools per network: USDC, USDT, wSOL, UMBRA. Includes mint addresses, decimals, and Token-2022 transfer fee handling. ## Overview Each shielded pool is deployed per token mint. A pool supports both the **confidential balance** feature (encrypted deposits and withdrawals) and the **mixer** feature (anonymous transfers via the Indexed Merkle Tree). Token-2022 mints with a transfer fee extension are fully supported - the SDK automatically accounts for the transfer fee before calculating protocol fees. See [Token-2022 Support](/advanced/token-2022). *** ## Mainnet * **USDC** * Mint: `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` * Token program: SPL * Confidentiality: enabled * Mixer: enabled * **USDT** * Mint: `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` * Token program: SPL * Confidentiality: enabled * Mixer: enabled * **wSOL** (Wrapped SOL) * Mint: `So11111111111111111111111111111111111111112` * Token program: SPL * Confidentiality: enabled * Mixer: enabled * **UMBRA** * Mint: `PRVT6TB7uss3FrUd2D9xs2zqDBsa3GbMJMwCQsgmeta` * Token program: SPL * Confidentiality: enabled * Mixer: enabled *** ## Using a Supported Mint Pass the mint address directly to any SDK function that accepts a `mint` parameter: ```typescript theme={null} const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // Deposit into encrypted balance const deposit = getPublicBalanceToEncryptedBalanceDirectDepositorFunction({ client }); const depositResult = await deposit(client.signer.address, USDC, 1_000_000n); // Withdraw from encrypted balance const withdraw = getEncryptedBalanceToPublicBalanceDirectWithdrawerFunction({ client }); const withdrawResult = await withdraw(client.signer.address, USDC, 1_000_000n); ``` The SDK will fetch the pool configuration on-chain and select the correct instruction variant automatically. Attempting to deposit into a mint that has no active pool will fail at the account-fetch stage with an account-not-found error.