# The function template (/api-design/function-template) The SDK has no client class. It exports plain functions, and each one takes an object it acts on as its first argument. There are three such objects, and each lives for a different time. ## Three objects | Object | Made by | Lives for | Holds secrets | | --------- | --------------------------------------------------------------- | -------------------------------------------------- | ------------------- | | The SDK | `loadNimbusSdk` | The page. Load it once. | No | | A session | `openNimbusSession` | One user on one device, until `closeNimbusSession` | Yes | | A context | `createDepositContext` and the other `create…Context` functions | One payment, or until a pending deposit resolves | Through its session | Each object is made from the one before it. The functions that take each one are grouped below. ```mermaid flowchart LR sdk(["SDK"]) --> session(["Session"]) session --> context(["Context"]) sdk -.-> wallets["Wallets and tokens: listWallets, followWallets, openWalletConnection, findMintByIdentifier"] session -.-> reads["The private account: syncNoteStore, readBalances, readHistory, readRegistration, listReservedSpends, releaseReservedNotes"] context -.-> flows["Payments: register, deposit, transfer, withdraw, resolvePendingDeposit, refundStrandedDeposit"] ``` *The SDK makes a session, and a session makes a context. Each function takes the object it needs.* Wallet functions need only the SDK. Reading and syncing the private account needs a session. A payment needs a context, which adds the pool and the services that payment uses. Create a fresh context right before each payment. It is cheap, and it reads the current pool. ## The call shape Most functions take three arguments, always in this order: 1. **The object** it acts on: the SDK, a session or a context. 2. **`args`**: what the call is about. These are required. 3. **`options`**: how it behaves. Every option has a default, so you can leave any of them out. Here is a withdrawal, with each part marked: ```ts import { withdraw } from "@umbra/nimbus-solana-web"; const result = await withdraw( // 1. The object: a withdrawal context, made for this one payment. context, // 2. args: which token, how much and to whom. All required. { mint: usdc, amount: 10_000_000n, recipient: bobsWallet }, // 3. options: each one changes a default. { fees: "added", commitment: "finalized" }, ); ``` Where a call needs nothing in `args`, it takes `{}`, and you may leave it out. A few functions differ. `loadNimbusSdk` takes `{ cluster }` first, since there is no SDK yet. The `create…Context` functions take a session and options. The sign-in store functions take options at most, and `closeNimbusSession` takes only a session. ## Names The verb at the start of a name tells you what the call does: | Verb | What happens | Functions | | --------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `load`, `open`, `close` | Starts or ends one of the three objects, or a wallet connection. | `loadNimbusSdk`, `openNimbusSession`, `openWalletConnection`, `closeNimbusSession` | | `create` | Makes a context or a sign-in store. No transaction is sent. | `createDepositContext`, `createTabSignatureSecretStore` and the rest | | `register`, `deposit`, `transfer`, `withdraw` | Runs a payment. | One function each | | `resolve`, `refund` | Settles a deposit that did not finish. | `resolvePendingDeposit`, `refundStrandedDeposit` | | `read` | Returns state and changes nothing. | `readBalances`, `readHistory`, `readRegistration` | | `list` | Returns what is there now. | `listWallets`, `listReservedSpends` | | `sync` | Brings this device up to date. | `syncNoteStore` | | `release` | Frees funds a payment holds. | `releaseReservedNotes` | | `follow` | Calls you back on every change, until you stop it. | `followWallets` | | `find` | Looks something up in the SDK's own lists. | `findMintByIdentifier` | | `is` | Tests what was thrown. | `isNimbusSdkError`, `isNimbusSolanaFlowError` | `readBalances` and `readHistory` read only this device and fetch nothing. `readRegistration` is the exception: it asks Nimbus's context service, since registration happens on Solana. ## Values - **Amounts** are `bigint`, in the token's smallest unit. 25 USDC is `25_000_000n`. - **Addresses** are the `Address` type from `@solana/kit`. - **Lamports** are always named as such, as in `priorityFeeLamports`. - **Results** that can end more than one way have a `kind` to `switch` on. ## How a function's entry reads Each function is documented once, in the [API reference](https://sdk.nimbusprivacy.xyz/reference). Every entry has the same parts, in this order: 1. **Summary.** One or two sentences on what it does. 2. **Signature.** The function's TypeScript declaration. 3. **Example.** A short call, type-checked against the SDK when the site is built. 4. **Parameters.** Every argument and option, with its type and meaning. 5. **Returns.** What it resolves to, and what each `kind` means. 6. **Throws.** The errors it throws, and why. 7. **Details.** The finer points, collapsed until you open them. See [readBalances](https://sdk.nimbusprivacy.xyz/reference/reads#readbalances) for a short entry. - [Results and errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors): The three ways a call ends badly. - [Progress](https://sdk.nimbusprivacy.xyz/api-design/progress): Every stage a payment reports. Last updated 21 October 2026 # Progress (/api-design/progress) A payment takes a few seconds, and most of that is the proof. Pass `onProgress` to show your user what is happening meanwhile. ## How onProgress works Every payment function takes an `onProgress` option. The SDK calls it once per stage, with an event whose `stage` names it. The event also carries what that stage produced, such as the fees or a transaction signature. If your callback throws, the SDK drops the error and carries on. If it returns a promise, the SDK does not wait for it. Your callback cannot stop a payment. ## Every stage The messages are the ones the [example app](https://sdk.nimbusprivacy.xyz/resources/example-app) shows. | Stage | Reported by | Message to show | | ------------------- | ------------------------------------------------ | --------------------------------------------------------------------------- | | `synced` | deposit, Private Send, withdraw | Brought this device up to date with Solana. | | `prepared` | every payment | Prepared on this device. | | `endorsed` | register, deposit | The committee approved the transaction. | | `proved` | register, deposit, Private Send, withdraw | The privacy work on this device finished. | | `funding-signed` | deposit, from a wallet that signs only version 0 | Your wallet signed the transfer that funds the deposit. | | `funding-sent` | deposit, from a wallet that signs only version 0 | Sent the funding transfer to Solana. | | `funding-confirmed` | deposit, from a wallet that signs only version 0 | The funding transfer landed. | | `signed` | register, deposit, refund | Your wallet signed the transaction. | | `sent` | register, deposit, refund | Sent the transaction to Solana. | | `confirmed` | register, deposit, refund | The transaction landed on Solana. | | `merge-prepared` | Private Send, withdraw | Combining part of your private balance so one payment can cover the amount. | | `merge-relayed` | Private Send, withdraw | The relayer accepted the combining step. | | `merge-settled` | Private Send, withdraw | The combining step finished. | | `relayed` | Private Send, withdraw | The relayer accepted the transaction and will send it. | | `settled` | Private Send, withdraw | Followed the payment until Solana settled it. | "Refund" here is `refundStrandedDeposit`. A few stages do not always come, or come in a different order: - `endorsed` and `proved` run at the same time, so they can arrive in either order. - A registration with nothing left to send stops after `prepared`. - A transaction that fails, expires or is still pending stops at `sent`. The result says where it ended up. - The merge stages come before the payment's own, once per merge. Each carries a `merge` number, from 1. - `settled` comes only when the SDK follows the payment, which it does unless you pass `follow: false`. ## A message for every stage Each payment's stages form a union on `stage`. A `switch` over all of them is checked by TypeScript. If the SDK adds a stage, this stops compiling until you give it a message. ```ts type PaymentStage = | RegisterStage | DepositStage | TransferStage | WithdrawStage | RefundStage | SyncedStage | SettledStage; function describeStage(event: PaymentStage): string { switch (event.stage) { case "synced": return "Brought this device up to date with Solana."; case "prepared": return "Prepared on this device."; case "endorsed": return "The committee approved the transaction."; case "proved": return "The privacy work on this device finished."; case "funding-signed": return "Your wallet signed the transfer that funds the deposit."; case "funding-sent": return "Sent the funding transfer to Solana."; case "funding-confirmed": return "The funding transfer landed."; case "signed": return "Your wallet signed the transaction."; case "sent": return "Sent the transaction to Solana."; case "confirmed": return "The transaction landed on Solana."; case "merge-prepared": return `Combining part of your private balance, step ${event.merge}.`; case "merge-relayed": return `The relayer accepted combining step ${event.merge}.`; case "merge-settled": return `Combining step ${event.merge} finished.`; case "relayed": return "The relayer accepted the transaction and will send it."; case "settled": return "Followed the payment until Solana settled it."; } } ``` ## A progress list in React This component runs a deposit and lists each stage as it arrives: ```tsx import { useState } from "react"; import type { ReactElement } from "react"; import { deposit } from "@umbra/nimbus-solana-web"; export function DepositWithProgress({ context, usdc, }: { context: DepositContext; usdc: Address; }): ReactElement { const [lines, setLines] = useState([]); const [running, setRunning] = useState(false); async function run(): Promise { setLines([]); setRunning(true); try { await deposit( context, { mint: usdc, amount: 25_000_000n, recipient: context.session.walletAddress, }, { onProgress: (event) => { setLines((current) => [...current, describeStage(event)]); }, }, ); } finally { setRunning(false); } } return (
{running &&

Keep this tab open until the deposit finishes.

}
    {lines.map((line, index) => (
  1. {line}
  2. ))}
); } ``` The button stays disabled while the deposit runs, so your user cannot start a second one. The result's `kind` still says how it ended. Show it as [the deposit guide](https://sdk.nimbusprivacy.xyz/deposit) does. - [How a payment works](https://sdk.nimbusprivacy.xyz/explainers/payments): What each stage stands for. - [Results and errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors): What to show when a payment ends. Last updated 21 October 2026 # Results and errors (/api-design/results-and-errors) A call that does not succeed tells you why in one of three ways. Each needs different handling, so it helps to know which to expect. ## The three ways | Way | How you see it | What causes it | | ----------------------- | ----------------------- | ------------------------------------------------------------ | | A result | Returned, with a `kind` | Your user, their balance, or Solana's answer | | `NimbusSdkError` | Thrown | The browser, your setup, or a service that cannot be reached | | `NimbusSolanaFlowError` | Thrown | The protocol's rules refuse what was asked | The rule is simple: - **If your user caused it, or trying again could fix it, it is returned.** A declined signature, a balance that is too small and a transaction that expired are all results. Handle them where you make the call, with a `switch` on `kind`. - **If the SDK cannot go on, it throws a `NimbusSdkError`.** Its `reason` says whether to fix your setup, try again later or ask for another wallet. - **If the protocol refuses, it throws a `NimbusSolanaFlowError`.** The token is not supported, or the amount cannot cover its fees. Change what you ask for. Compare the `reason`, never the message. Test with `isNimbusSdkError` and `isNimbusSolanaFlowError` rather than `instanceof`, so the test still works across bundles. ## One function for every error Wrap each call in `try`, and pass whatever it throws to one function. TypeScript checks that every reason has a message, so a new reason in the SDK stops this from compiling. ```ts import { isNimbusSdkError, isNimbusSolanaFlowError, } from "@umbra/nimbus-solana-web"; function describeSdkReason(reason: NimbusSdkErrorReason): string { switch (reason) { case "misconfigured": return "This app is set up wrongly. Please tell us."; case "unreadable": return "A service sent something unexpected. Try again."; case "unsupported-browser": return "This browser cannot make private payments. Try another one, outside private mode."; case "wallet-unsupported": return "This wallet cannot be used here. Connect another wallet."; case "service-unavailable": return "The network cannot be reached right now. Try again shortly."; case "unauthorized": return "Your session was not accepted. Reload the page and sign in again."; case "dependency-nonconforming": return "A downloaded file failed its check, so it was not used. Reload the page."; case "recipient-unregistered": return "The recipient has not set up a private account yet."; case "account-unregistered": return "Set up your private account before you pay."; } } function describeFlowReason(reason: NimbusSolanaFlowErrorReason): string { switch (reason) { case "amount-unfundable": return "That amount cannot cover its fees. Try a larger amount."; case "mint-unsupported": return "This token cannot be used. Choose another."; case "fee-changing": return "This token's fee is about to change. Try again in a few minutes."; case "context-mismatched": return "The payment could not be prepared. Try again."; case "payout-unpayable": return "That address cannot receive this token. Choose another address."; case "relay-unavailable": return "The relayer cannot carry this payment. Try another token or amount."; case "binding-mismatched": return "This wallet is linked to a different private account."; case "signature-invalid": case "transaction-altered": return "Your wallet changed what it was asked to sign. Try again, or use another wallet."; case "transaction-unsendable": return "Solana would not accept this transaction. Try again."; case "fee-payer-unfunded": return "Your wallet needs a little SOL to pay the network fee. Add some and try again."; case "committee-mismatched": case "standing-mismatched": case "absence-unproven": return "A Nimbus service gave an answer that did not check out. Try again."; case "relay-mismatched": return "The relayer sent something unexpected. The payment may still land."; case "zone-unauthorised": return "This payment was not authorised. Contact the app's operator."; case "message-altered": case "deployment-invalid": case "proof-mismatched": return "This app is set up wrongly. Please tell us."; } } export function describeNimbusError(caught: unknown): string { if (isNimbusSdkError(caught)) { return describeSdkReason(caught.reason); } if (isNimbusSolanaFlowError(caught)) { return describeFlowReason(caught.reason); } return "Something went wrong. Try again."; } ``` Log the error's own `message` beside the message you show. It names what failed, for you to debug. ## A party who has not registered Paying a recipient who has not registered throws a `NimbusSdkError` with reason `recipient-unregistered`. Paying from an account that has not registered throws one with reason `account-unregistered`. Either is thrown before the wallet is asked to sign, and nothing is sent. A registration that has only just landed counts once Nimbus's indexer reaches it. Ask the user to wait a moment and try again. ## NimbusSdkError reasons The SDK throws a `NimbusSdkError` when it cannot go on. Each reason comes from the SDK's own description: | Reason | What it means | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `misconfigured` | the cluster's preset leaves an endpoint or the proving keys unconfigured and the options did not supply them, or a configured value or an argument is unusable: an `assetsBaseUrl` that is not a directory URL, no prover worker, no endpoint for the committee node a context names, an address that is not base58, an option outside its range, a mint identifier shorter than 32 bytes, a deposit submission whose outcome is settled already, a gateway token that is not a bearer token, or no open pool. The message names each one; pass it in `loadNimbusSdk`'s options or fix the argument. Not recoverable by retrying. | | `unreadable` | a party answered, or the browser's storage held, something that cannot be read: a malformed service answer, a note store another deployment or a newer schema wrote, or a note store another tab replaced or kept writing to while it was in use. Retrying the same party is unlikely to help, though a call that met a note store being replaced may succeed when made again, since each call opens the store afresh. | | `unsupported-browser` | the browser lacks a global the SDK needs (`fetch`, Web Crypto, `navigator.locks`, `navigator.storage`, IndexedDB, `IDBKeyRange`, WebAssembly), or refused to open its storage, as a private window may; or the page is not cross-origin isolated, which the server fixes by sending `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`. Not recoverable in this browser or on this page. | | `wallet-unsupported` | the wallet cannot connect, sign messages and sign transactions on the cluster's chain, its account's address is not its public key's, or it failed while connecting or signing, signed other bytes than it was given, or answered a signature that does not verify. A user who declines is not this error: the call answers `declined`. Ask the user to connect another wallet. | | `service-unavailable` | a service, the RPC or the artifact host could not be reached or answered a failing status. Retry later. | | `unauthorized` | a gateway service answered 401 or 403, because the session was opened without a gateway token or the gateway refused it, or the session's token issuer failed. The message names the service and never the token. Open a session with a token the gateway accepts, or have the issuer return a fresh one. | | `dependency-nonconforming` | a dependency answered against its contract: a proving key or witness calculator that is not the one the manifest and the deployed program pin, a prover that refused the statement, a file of this package's `assets/` whose SHA-256 is not the one the package pins, or primitives that fail their known-answer checks. The value must not be used; retrying will not help, and serving the `assets/` packed with this copy of the package fixes a mismatched file. | | `recipient-unregistered` | the context API answered that the wallet a deposit or a transfer pays has no registration in the pool, so no note can be sealed to it. The message carries the API's detail. Nothing was signed or sent. Ask the recipient to register, or pay another wallet; a registration that has only just landed is seen once the indexer reaches it. | | `account-unregistered` | the context API answered that the session's own identity, or for a withdrawal the connected wallet, has no registration in the pool. The message carries the API's detail. Nothing was signed or sent. Run `register` first; a registration that has only just landed is seen once the indexer reaches it. | And here is what to tell your user for each: | `reason` | Tell your user | What you do | | -------------------------- | --------------------------------------------------- | --------------------------------------------------------------------- | | `misconfigured` | Something is wrong on our side. | Fix the option or argument the message names. Retrying will not help. | | `unreadable` | Something unexpected happened. Try again. | Retry once. A replaced browser store can succeed on the next call. | | `unsupported-browser` | Try another browser, outside private mode. | Check the page is cross-origin isolated. | | `wallet-unsupported` | Connect another wallet. | Nothing. A user who declines gets a `declined` result instead. | | `service-unavailable` | Try again shortly. | Retry later. | | `unauthorized` | Sign in again. | Open the session with a gateway token the gateway accepts. | | `dependency-nonconforming` | Reload the page. | Serve the `assets/` that came with this version of the package. | | `recipient-unregistered` | The recipient has not set up a private account yet. | Ask them to register, or pay someone else. | | `account-unregistered` | Set up your private account first. | Run `register`, then try again. | ## NimbusSolanaFlowError reasons A payment throws a `NimbusSolanaFlowError` when the protocol refuses it. Most mean the payment cannot succeed as asked. A few mean a service's answer did not check out, and a retry may help. | `reason` | Cause | What to do | | ------------------------ | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | | `amount-unfundable` | The amount cannot settle: nothing reaches the pool, or the fees take it all. | Choose another amount or fee mode. | | `mint-unsupported` | The address is not an SPL Token or Token-2022 mint. | Choose another token. | | `fee-changing` | The token's Token-2022 transfer fee changes at the next epoch. | Try again once the epoch turns. | | `context-mismatched` | The context service's answer does not fit the payment. | Try again. If it persists, the service is wrong. | | `payout-unpayable` | The withdrawal's destination cannot receive this token. | Name another destination. | | `relay-unavailable` | The relayer does not carry this payment: its token, its fee or its destination. | Change the payment, or use another relayer. | | `binding-mismatched` | The wallet is already linked to another private account, such as one at another `accountIndex`. | Open the session with the matching `accountIndex`. | | `standing-mismatched` | The context service's answer does not fit the registration. | Try again later. | | `absence-unproven` | A proof the context service served does not check out. | Try again. If it persists, the service is wrong. | | `committee-mismatched` | A committee service refused, or signed the wrong thing. Nothing was sent. | Try again. If it persists, report it. | | `transaction-altered` | The wallet changed the transaction it was asked to sign. Nothing was sent. | Ask again, or use another wallet. | | `transaction-unsendable` | The transaction is too large, or its simulation failed. Nothing was sent. | Retry. A failed simulation may pass, a size will not. | | `fee-payer-unfunded` | The wallet holds too little SOL to pay for the transaction it signs. Nothing was proved or sent. | Ask your user to add SOL. `detail.requiredLamports` says how much it needs. | | `relay-mismatched` | The relayer accepted something other than what was built. | Sync to learn whether it landed. Its funds stay reserved until then. | | `zone-unauthorised` | The signer for a non-default zone refused. | Ask that zone's operator, or use the default zone. | | `proof-mismatched` | The prover proved something other than what was prepared. | Retrying will not help. Report it. | | `deployment-invalid` | The deployment settings contradict themselves. | Fix the configuration. | | `message-altered` | The sign-in message in this build is not the pinned one. | The build is wrong. Reinstall the package. | | `signature-invalid` | The wallet's sign-in signature does not verify. | Ask again. A wallet that wraps the message is unsupported. | `openNimbusSession` reports `message-altered` and `signature-invalid` as a `NimbusSdkError` with reason `wallet-unsupported`. So you seldom see those two directly. ## Refusals inside results Some results report that this browser's own storage refused a step. Nothing was sent when that happens: - `deposit` can return `store-refused`, `intent-refused` or `attempt-refused`. - `withdraw` and `transfer` can return `refused` with `store-refused`, `lease-refused`, `attempt-refused`, `stale-handle` or `store-unavailable`. They can also return `unsent`. - `register` and `resolvePendingDeposit` can return `store-refused`. These are rare. They happen when another tab of your app took the same funds a moment earlier, or replaced the browser's store, or when the browser blocks its storage. Ask your user to reload and try again. - [Errors reference](https://sdk.nimbusprivacy.xyz/reference/errors): NimbusSdkError and the two guards. - [Tabs and reserved funds](https://sdk.nimbusprivacy.xyz/explainers/reserved-funds): Why another tab can take funds first. Last updated 21 October 2026 # Deposit (/deposit) A deposit moves tokens from your user's wallet into a private balance. It is the first payment every user makes, and the only one their wallet signs. In this guide, Alice deposits 25 USDC into her own private balance. Then she deposits 5 USDC straight into Bob's. ## What's public A deposit looks like an ordinary transfer into the pool. Because Alice registered her wallet, it can be tied to her. Privacy starts once the tokens are inside. ## Deposit into your user's balance Create a deposit context from the session, then call `deposit`. ```ts import { address } from "@solana/kit"; import { createDepositContext, deposit } from "@umbra/nimbus-solana-web"; const usdc = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); const context = await createDepositContext(session); const result = await deposit( context, { mint: usdc, amount: 25_000_000n, recipient: session.walletAddress }, { transactionVersion }, ); ``` - **`amount`** is in the token's smallest unit. USDC has 6 decimals, so 25 USDC is `25_000_000n`. [Tokens](https://sdk.nimbusprivacy.xyz/helpers/tokens) shows how to read any token's decimals. - **`recipient`** is the wallet whose private balance receives the deposit. Here it is Alice's own. - **`transactionVersion`** is what the wallet can sign. `openWalletConnection` tells you; see [Connect a wallet](https://sdk.nimbusprivacy.xyz/helpers/wallets). Alice's wallet asks her to approve one transaction. She pays its network fee from her wallet. ### The protocol fee The protocol fee comes out of the amount. When Alice deposits 25 USDC, her private balance grows by 25 USDC less the fee. To charge the fee on top instead, pass `fees: "added"`. The fee is then paid from the private balance Alice already holds in that token. If she holds too little, the deposit throws before anything is signed. ## Deposit into another user's balance Alice can pay Bob directly from her wallet. Pass Bob's wallet address as `recipient`. ```ts import { deposit } from "@umbra/nimbus-solana-web"; const result = await deposit(context, { mint: usdc, amount: 5_000_000n, recipient: bob, }); ``` The 5 USDC arrive in Bob's private balance, not Alice's. Bob sees them the next time his device syncs. Bob must have [registered](https://sdk.nimbusprivacy.xyz/registration) first. If he has not, `deposit` throws a `NimbusSdkError` with reason `recipient-unregistered`, and Alice's wallet is never asked to sign. ## Handle the result `deposit` returns a result instead of throwing for anything your user can cause. Check its `kind`: | `kind` | What happened | What to do | | ---------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------- | | `submitted` | The deposit was sent to Solana. | Read where it ended up, below. | | `declined` | Alice rejected it in her wallet. Nothing was sent. | Let her try again. | | `store-refused`, `intent-refused`, `attempt-refused` | This browser could not record the deposit. Nothing was sent. | Ask her to reload the page and try again. | A `submitted` result carries the transaction's fate in `result.submission.submission.kind`: | `kind` | What it means | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `landed` | Done. The private balance holds the deposit. | | `failed` | It reached Solana and failed. Only the network fee was charged. | | `expired` or `refused` | It never reached Solana. Nothing moved, and it is safe to try again. | | `unknown` | The SDK stopped waiting before Solana decided. See [If the deposit is still pending](#if-the-deposit-is-still-pending). | Here is the whole check in one function. TypeScript checks that it covers every case. ```ts function describeDeposit(result: DepositResult): string { switch (result.kind) { case "submitted": switch (result.submission.submission.kind) { case "landed": return "Deposited."; case "failed": return "The deposit failed on Solana. Only the network fee was charged."; case "expired": case "refused": return "The deposit never reached Solana. Try again."; case "unknown": return "The deposit is still pending."; } case "declined": return "You declined the deposit in your wallet."; case "store-refused": case "intent-refused": case "attempt-refused": return "This browser could not record the deposit. Reload and try again."; case "funding-failed": case "funding-unknown": case "deposit-unsendable": return "See Wallets that sign only version 0."; } } ``` The last three kinds happen only with older wallets. They are covered in [Wallets that sign only version 0](#wallets-that-sign-only-version-0). ## Show progress A deposit takes a few seconds, most of it building the proof. Pass `onProgress` to tell your user what is happening: ```ts import { deposit } from "@umbra/nimbus-solana-web"; await deposit( context, { mint: usdc, amount: 25_000_000n, recipient }, { onProgress: (event) => showStatus(event.stage) }, ); ``` [Show progress](https://sdk.nimbusprivacy.xyz/api-design/progress) lists every stage with a message you can show as it is. ## If the deposit is still pending Sometimes the SDK stops waiting before Solana settles a deposit. The result is `submitted`, and its submission's kind is `unknown`. Do not deposit the same funds again yet: call `resolvePendingDeposit` to learn how it ended. [Pending and stranded deposits](https://sdk.nimbusprivacy.xyz/deposit/pending) shows how, including across a page reload. ## Wallets that sign only version 0 A wallet that signs only version 0 needs two transactions, and Alice approves both. The first moves the funds to her platform wallet, a second address the SDK derives for her, and the second deposits them. When they do not both go through, the result is `funding-failed`, `funding-unknown` or `deposit-unsendable`. [Pending and stranded deposits](https://sdk.nimbusprivacy.xyz/deposit/pending#wallets-that-sign-only-version-0) says when to try again and when to refund. ## Common errors These are thrown, not returned, because your app usually cannot fix them by asking the user. | Error | Reason | Cause | | ----------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | `NimbusSdkError` | `recipient-unregistered` | The recipient has not registered. | | `NimbusSdkError` | `account-unregistered` | Alice has not registered. Run `register` first. | | `NimbusSolanaFlowError` | `context-mismatched` | A Nimbus service answered with data that does not fit the deposit. Try again. | | `NimbusSolanaFlowError` | `mint-unsupported` | The address is not an SPL or Token-2022 mint. | | `NimbusSolanaFlowError` | `amount-unfundable` | The amount is too small to cover the fee, or the private balance cannot pay an added fee. | | `NimbusSolanaFlowError` | `fee-changing` | The token's transfer fee changes at the next epoch. Try again after it turns. | | `NimbusSolanaFlowError` | `fee-payer-unfunded` | Alice's wallet holds too little SOL for the fee, the SOL it deposits, or a new token account's rent. Nothing was proved or sent. | | `NimbusSdkError` | `service-unavailable` | Solana or a Nimbus service could not be reached. Try again. | [Handle errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors) shows one function that covers every error the SDK throws. ## Options | Option | Default | What it does | | --------------------- | ------------- | -------------------------------------------------------------------- | | `transactionVersion` | `1` | The transaction version the wallet signs. | | `fees` | `"deducted"` | `"added"` charges the protocol fee on top of `amount`. | | `onProgress` | none | Called at each stage of the deposit. | | `sync` | `true` | Brings the device up to date first. Pass `false` if you just synced. | | `commitment` | `"confirmed"` | `"finalized"` waits until the deposit cannot be rolled back. | | `priorityFeeLamports` | `0n` | A priority fee, in lamports, to land faster when Solana is busy. | The [API reference](https://sdk.nimbusprivacy.xyz/reference/deposit#deposit) lists every option. ## Next - [Private Send](https://sdk.nimbusprivacy.xyz/transfer): Pay another user from the private balance. - [Show balances](https://sdk.nimbusprivacy.xyz/helpers/balances): Read what the private balance holds. - [Handle errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors): Cover every result and error in one place. Last updated 21 October 2026 # Pending and stranded deposits (/deposit/pending) Most deposits settle before `deposit` returns. This page covers the ones that do not: a deposit Solana had not decided on yet, and funds an older wallet left waiting halfway. In this guide, Alice deposits 25 USDC and the SDK stops waiting before Solana settles it. ## When a deposit is pending `deposit` sends the transaction and waits for Solana to settle it. Sometimes the wait ends first. The result is then `submitted`, and its submission's kind is `unknown`. A pending deposit may still land. Its transaction stays valid until its blockhash expires. Until you know how it ended, do not deposit the same funds again. If both landed, Alice would deposit twice. A deposit also combines up to three earlier amounts of the same token in Alice's private balance. Those stay reserved while the deposit is pending, so her available balance can look lower. ## Resolve it Call `resolvePendingDeposit` with the deposit's context and its submission. It waits for Solana to decide, then tells the device how the deposit ended. ```ts import { resolvePendingDeposit } from "@umbra/nimbus-solana-web"; if ( result.kind === "submitted" && result.submission.submission.kind === "unknown" ) { const resolution = await resolvePendingDeposit(context, { submission: result.submission, }); } ``` - **`context`** is the deposit context the deposit ran in, or a new one for the same wallet. - **`submission`** is the `submitted` result's `submission`, whose kind is `unknown`. It accepts only a pending submission. Pass one that already settled, and it throws a `NimbusSdkError` with reason `misconfigured` before it fetches anything. | `kind` | What happened | What to do | | --------------- | -------------------------------------------------------------------------------------- | ----------------------------------------- | | `landed` | The deposit went through. The balance shows it after the next sync. | Nothing more. | | `failed` | It reached Solana and failed. Nothing was deposited, and the reserved funds are freed. | Let Alice deposit again. | | `expired` | It can never land. Nothing was deposited, and the reserved funds are freed. | Let Alice deposit again. | | `unknown` | Solana still has not decided. | Call it again in a few seconds. | | `store-refused` | This browser could not open Alice's private account data. Nothing was recorded. | Ask her to reload the page and try again. | TypeScript checks that this switch covers every case: ```ts function describeResolution(resolution: PendingDepositResolutionResult): string { switch (resolution.kind) { case "landed": return "Deposited."; case "failed": case "expired": return "The deposit did not go through. You can deposit again."; case "unknown": return "The deposit is still pending."; case "store-refused": return "This browser could not record the deposit. Reload and try again."; } } ``` ## Survive a page reload The session and the context live in memory, so a reload loses them. To finish the deposit after one, store its submission as soon as `deposit` returns. Store the mint, amount and recipient too, so your app can show what is pending. A `DepositSubmission` holds `Uint8Array` and `bigint` values. `JSON.stringify` cannot write either, so encode them first. These two functions tag each value and restore it: ```ts function encodeForStorage(value: unknown): string { return JSON.stringify(value, (_key, item: unknown) => { if (typeof item === "bigint") return { bigint: item.toString() }; if (item instanceof Uint8Array) return { bytes: Array.from(item) }; return item; }); } function decodeFromStorage(text: string): unknown { return JSON.parse(text, (_key, item: unknown) => { if (typeof item !== "object" || item === null) return item; if ("bigint" in item && typeof item.bigint === "string") { return BigInt(item.bigint); } if ("bytes" in item && Array.isArray(item.bytes)) { return Uint8Array.from(item.bytes); } return item; }); } ``` Save the pending deposit when `deposit` returns: ```ts if ( result.kind === "submitted" && result.submission.submission.kind === "unknown" ) { localStorage.setItem( "pending-deposit", encodeForStorage({ submission: result.submission, mint: usdc, amount: 25_000_000n, recipient, }), ); } ``` After the reload, open a session for the same wallet, then rebuild the context with `createDepositContext`: ```ts import { createDepositContext, resolvePendingDeposit, } from "@umbra/nimbus-solana-web"; const stored = localStorage.getItem("pending-deposit"); if (stored !== null) { const { submission } = decodeFromStorage(stored) as { submission: DepositSubmission; }; const context = await createDepositContext(session); const resolution = await resolvePendingDeposit(context, { submission }); if (resolution.kind !== "unknown" && resolution.kind !== "store-refused") { localStorage.removeItem("pending-deposit"); } } ``` The session must belong to the wallet that made the deposit. The device's record of the deposit is kept per wallet, and the submission points into it. ## Wallets that sign only version 0 Most wallets sign version 1 transactions, and a deposit is then one transaction. A wallet that signs only version 0 needs two, and Alice approves both: ### Funding Moves the tokens, and the SOL for the second transaction's fee, from Alice's wallet to her platform wallet. Her platform wallet is a second Solana address the SDK derives for her from her signature. ### Deposit Moves them from her platform wallet into the pool. Her platform wallet signs and pays for it. When the two do not both go through, the result says how far it got: | `kind` | What happened | What to do | | -------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------ | | `funding-failed` | The funding transaction did not land. Nothing moved. | Try again. | | `funding-unknown` | The funding transaction may still land. Nothing was deposited. | Wait a minute, then try again. | | `deposit-unsendable` | The funding landed, but the deposit failed its check before sending. The funds wait in the platform wallet. | Try again, or refund. | A `submitted` deposit whose transaction then failed or expired also leaves its funding in the platform wallet. So does an `attempt-refused` result after a funding that landed. To try again, call `deposit` with the same mint. It counts what the platform wallet already holds and funds only the rest. Once a funding has settled, a retry never funds it twice. To give the funds back instead, refund them. ## Refund a stranded deposit `refundStrandedDeposit` returns everything the platform wallet holds of one token to Alice's wallet. It also returns the SOL left there for fees. Alice's wallet signs and pays the network fee. ```ts import { createDepositContext, refundStrandedDeposit, } from "@umbra/nimbus-solana-web"; const refund = await refundStrandedDeposit( await createDepositContext(session), { mint: usdc }, { transactionVersion, closeTokenAccount: true }, ); ``` `closeTokenAccount: true` also closes the platform wallet's token account. That returns the rent Solana held for it. A wrapped SOL account is always closed. > **Refund only after every deposit settles** > > The refund takes whatever the platform wallet holds at that moment. If a deposit of the same token > is still in flight, the refund can take the funds it was about to move. | `kind` | What happened | What to do | | -------------- | ------------------------------------------------------------------------- | --------------------------------------------------------- | | `nothing-held` | The platform wallet holds none of the token and no SOL. Nothing was sent. | Nothing to refund. | | `declined` | Alice rejected it in her wallet. Nothing was sent. | Let her try again. | | `submitted` | The refund was sent. Only a `landed` submission returned the funds. | If it `failed`, `expired` or was `refused`, run it again. | ```ts function describeRefund(refund: StrandedDepositRefundResult): string { switch (refund.kind) { case "nothing-held": return "There is nothing to refund."; case "declined": return "You declined the refund in your wallet."; case "submitted": switch (refund.submission.kind) { case "landed": return "Refunded to your wallet."; case "failed": case "expired": case "refused": return "The refund did not go through. Try again."; case "unknown": return "The refund is still pending. Check again in a minute."; } } } ``` ## A stopped deposit that reserved funds A deposit that combines earlier amounts reserves them while it runs. If it stops before it settles, they stay reserved. That happens when the result is pending, or when the tab closes mid-deposit. `listReservedSpends` shows such a deposit with kind `"deposit"`. If you still have its submission, resolve it as above. If not, [Reserved funds](https://sdk.nimbusprivacy.xyz/helpers/reserved-funds) shows how to release it. A deposit into a private balance with nothing of that token reserves nothing. ## Next - [Deposit](https://sdk.nimbusprivacy.xyz/deposit): The deposit guide this page continues. - [Reserved funds](https://sdk.nimbusprivacy.xyz/helpers/reserved-funds): List and release funds a stopped payment holds. - [API reference](https://sdk.nimbusprivacy.xyz/reference/deposit): Every option of resolvePendingDeposit and refundStrandedDeposit. Last updated 21 October 2026 # Fees (/explainers/fees) Every Solana transaction pays a network fee in SOL. A Nimbus payment can carry two more: a protocol fee, and a tip for the relayer that sends it. ## The three fees - **The protocol fee** is a share of the amount, paid in the token being moved. Governance sets the rates for each kind of payment and token. The SDK reads them when it prepares the payment. - **The Solana network fee** is paid in SOL by whoever sends the transaction. - **The relayer's tip** pays the relayer that sends a Private Send or a withdrawal. It is paid in the token being moved, and anyone can see it. Change that comes back to your user pays no protocol fee. Neither does a merge, which only moves value between your user's own notes. ## Who pays what | Payment | Protocol fee | Network fee | Relayer's tip | | ------------ | -------------------------- | ------------------ | --------------------- | | Register | None | Your user's wallet | None | | Deposit | From the amount, or on top | Your user's wallet | None by default | | Private Send | From the amount, or on top | The relayer | The relayer's minimum | | Withdraw | From the amount, or on top | The relayer | The relayer's minimum | The relayer's minimum is zero while it sponsors payments. Otherwise it is the minimum its published policy asks for. A withdrawal to a token account that does not exist yet also pays the relayer's creation fee, unless the relayer creates it for free. You can offer more with the `submitterFee` option. ## Deducted or added By default, fees are **deducted**: they come out of `amount`. Pass `fees: "added"` to pay them on top instead. Here is how that plays out for Alice, in USDC: | Alice asks to | With `fees: "deducted"` | With `fees: "added"` | | ---------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | Deposit 25 USDC | 25 USDC leaves her wallet. Her private balance grows by 25 USDC less the protocol fee. | 25 USDC leaves her wallet, and her new note holds all 25. The fee comes from USDC already in her private balance. | | Send Bob 10 USDC | Her balance drops by 10 USDC. Bob receives 10 USDC less the fees. | Bob receives exactly 10 USDC. Her balance drops by 10 USDC plus the fees. | | Withdraw 10 USDC | Her balance drops by 10 USDC. The address receives 10 USDC less the fees. | The address receives exactly 10 USDC. Her balance drops by 10 USDC plus the fees. | With `added`, the private balance must cover the fees. If it cannot, a deposit throws `amount-unfundable`. A Private Send or withdrawal returns `refused` with `insufficient`. Nothing is sent either way. The `prepared` progress stage carries the exact fees. You can show them while the proof runs. A Token-2022 token may also keep a transfer fee of its own on the way into the pool. The protocol fee applies to what arrives. ## Priority fees When Solana is busy, a priority fee helps a transaction land sooner. Pass `priorityFeeLamports` to `register`, `deposit` or `refundStrandedDeposit`. It is in lamports for the whole transaction, `0n` by default, and your user's wallet pays it. A Private Send or withdrawal takes no priority fee. The relayer sends it, so the relayer decides. ## Rent Your user's first deposit of a token creates a token account for their platform wallet. Their wallet pays that account's rent. If a deposit ever needs a refund, `refundStrandedDeposit` can return that rent too. Pass `closeTokenAccount: true` to close the emptied account. ## What this means for your app - **Show the fee before your user commits,** or as soon as the `prepared` stage reports it. - **Pick one fee mode** and say which in your UI. "You send 10, Bob gets 10" reads differently from "You send 10, Bob gets a little less". - **Expect the tip to change.** It follows the relayer's policy, which can change. * [The fee model](https://docs.nimbusprivacy.xyz/fees/fee-model): How the protocol computes its fee. * [Show progress](https://sdk.nimbusprivacy.xyz/api-design/progress): Read the fees from the prepared stage. Last updated 21 October 2026 # How a payment works (/explainers/payments) A Solana transfer is built, signed, sent and confirmed. A Nimbus payment adds two steps: a group of services approves it, and the browser proves it follows the pool's rules. ## The five steps This chart follows a Private Send, which a relayer sends for your user. ```mermaid sequenceDiagram participant App as Your app participant SDK participant Services as Nimbus services participant Solana App->>SDK: transfer SDK->>SDK: prepare and reserve the notes SDK->>SDK: prove, in the browser SDK->>Services: approve and relay Services->>Solana: send the transaction SDK->>Services: follow until it lands SDK-->>App: relayed, with its settlement ``` *A Private Send: the SDK prepares and proves it on the device, Nimbus's services approve and relay it, and the SDK follows it until Solana settles it.* Your app makes one call and gets one result. Each step in between works like this. ### Prepare The SDK syncs, picks the notes to spend and reserves them. It asks Nimbus's context service for what the proof needs. It stops here if the balance is too small or cannot cover the fees. ### Approve A committee of Nimbus services checks the payment and co-signs it. Solana accepts the payment only with their combined signature. If one refuses, the payment stops before anything is sent. ### Prove The browser builds a zero-knowledge proof that the payment follows the pool's rules. This takes most of a payment's time, and closing the tab stops it. ### Send Your user's wallet signs and the SDK sends it, or a relayer sends it. Your user can decline in the wallet, and a relayer can refuse. ### Settle The SDK waits for Solana to confirm the transaction. It checks every 1,000 milliseconds. It returns once the transaction lands or fails, or when it stops waiting. For a deposit or registration, approval and proof run at the same time. For a Private Send or withdrawal, the committee approves as the relayer accepts the payment. ## Who sends it | Payment | Who signs | Who sends it to Solana | | ------------ | ---------------------------------------------------------- | ------------------------- | | Register | Your user's wallet, then the SDK for their platform wallet | The SDK, from the browser | | Deposit | Your user's wallet, then the SDK for their platform wallet | The SDK, from the browser | | Private Send | Nobody on the device | A relayer | | Withdraw | Nobody on the device | A relayer | A relayer pays the Solana fee and appears as the signer. That keeps your user's wallet off their Private Sends and withdrawals. ## How it settles A relayed payment returns `relayed` with a `settlement`. The SDK checks on it up to 180 times. | `settlement.kind` | What it means | | ----------------- | ----------------------------------------------------------------------------- | | `landed` | It landed, and the SDK synced the private balance after. | | `failed` | The relayer rejected it, gave it up, or it failed on Solana. | | `pending` | The SDK stopped waiting first. It may still land, and a later sync will tell. | | `not-followed` | You passed `follow: false`, so the SDK returned when the relayer accepted it. | A transaction your user's wallet sent returns a `TransactionSubmission`. The SDK checks it up to 90 times. | `kind` | What it means | | --------- | ------------------------------------------------------- | | `landed` | It landed without error. | | `failed` | It landed and failed. Only the network fee was charged. | | `expired` | Its blockhash expired first. It will never land. | | `refused` | The RPC would not forward it. It cannot land. | | `unknown` | The SDK stopped waiting first. It may still land. | ## Confirmed or finalized Solana reports a transaction as `confirmed` within seconds. It becomes `finalized` once it can no longer be rolled back, some seconds later. Payments wait for `confirmed` by default. Pass `commitment: "finalized"` when your app needs certainty more than speed. ## Merging One payment spends at most three notes. The SDK always takes the three largest, so the rest of the balance gathers in the change. When the three largest cannot cover a Private Send or withdrawal, the SDK merges first. Each merge sends three notes back to your user as one, then waits for it to land. You see each merge as `merge-prepared`, `merge-relayed` and `merge-settled` stages. A merge pays the relayer's tip, but no protocol fee. If one fails, the result is `merge-failed`. Merges that finished stay merged, so a retry needs fewer. A deposit also merges up to three of your user's notes of that token into its change. ## What this means for your app - **Show progress.** A payment takes seconds, and most of it is the proof. - **Ask your user to keep the tab open** until the payment returns. - **Read the settlement, not just the kind.** `relayed` means accepted, and `landed` means done. * [Show progress](https://sdk.nimbusprivacy.xyz/api-design/progress): Every stage, with a message to show. * [Results and errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors): How a payment reports what went wrong. Last updated 21 October 2026 # The private account (/explainers/private-account) A Solana wallet is a keypair. Whoever holds the secret key controls the wallet. A private account has no keypair of its own to back up. It comes from the wallet your user already has. ## One signature opens it When your app calls `openNimbusSession`, the wallet asks your user to sign a fixed message. The SDK derives everything the account needs from that signature, on the device. Signing moves no money and costs nothing. No transaction is sent, and nothing is written to Solana. The same wallet always opens the same private account, on any device. There is nothing extra to back up. ## What the account holds - **Keys.** They read your user's notes and spend them. They stay in the browser. - **A platform wallet.** This is a second Solana address the SDK derives for your user. Deposits pass through it on their way into the pool. - **An identity.** This is the public value Solana knows the account by. Registering publishes it and ties it to both wallets. It reveals nothing about the keys. Each `accountIndex` you pass to `openNimbusSession` opens a separate private account from the same wallet. Most apps use the default, 0. ## Remembering the sign-in So that a reload does not ask the wallet again, the session keeps the signature in a **signature secret store**. You choose one when you open the session: | Store | Remembers it for | On reload, your user sees | Kept in | Readable by | | ----------------------------------------- | ---------------- | --------------------------------- | ----------------- | ------------------------------------------------------------ | | `createMemorySignatureSecretStore` | Never | A signature request, every time | The page's memory | Nothing outside the page | | `createTabSignatureSecretStore` (default) | This tab | No request. A new tab asks again. | `sessionStorage` | Any script on your site, while the tab is open | | `createPersistentSignatureSecretStore` | This browser | No request, until you wipe it | `localStorage` | Any script on your site, and anyone with the browser profile | The longer a store remembers, the smoother sign-in feels and the wider the exposure. > **A stored signature is the whole account** > > The tab and browser stores keep the signature unencrypted. Any script that runs on your site can > read it, including an injected one or a compromised dependency. Whoever reads it can spend the > private balance. ## Keys never leave the browser The SDK sends services only what a payment needs. These are the most sensitive parts: - **To Nimbus's context service:** the account's identity, the token, the recipient and the notes a payment spends. It answers with what the proof needs. - **To each guardian node:** one share of a viewing key, encrypted to that node. One share alone reveals nothing, and no share can spend. - **To the fee enclave:** the payment's fee details, sealed to the enclave. - **To the relayer:** the finished, proved transaction. - **To Nimbus's gateway:** the gateway token, sent to the gateway's addresses and nowhere else. It never sends the signature, the spending keys or the device's copy of the notes. [Nimbus's services](https://docs.nimbusprivacy.xyz/architecture/services) describes what each service receives. ## What this means for your app - **Ask once.** Your user signs one message. The store you choose decides how long it lasts. - **Pick a store for your product.** A wallet-style app may choose the browser store. A shared or kiosk device should choose memory. - **Sign out properly.** `closeNimbusSession` leaves the store as it is. Wipe the store too. * [Sessions](https://sdk.nimbusprivacy.xyz/helpers/sessions): Open and close a session, and choose a store. * [Registration](https://sdk.nimbusprivacy.xyz/registration): Publish the account before its first deposit. Last updated 21 October 2026 # Private balances (/explainers/private-balance) On Solana, a token balance lives in a token account. Anyone can read its owner, its mint and its amount. A private balance holds the same three facts, but only its owner can read them. ## A balance is a set of notes A private balance is made of **notes**. Each note is like a small token account: it has an owner, a token and an amount. Notes are stored on Solana, sealed so that only the owner's keys can open them. A user's balance in a token is the sum of their notes in that token. Alice holds three USDC notes: ```mermaid flowchart LR a["Note: 10 USDC"] --> b(["Private balance: 25 USDC"]) c["Note: 10 USDC"] --> b d["Note: 5 USDC"] --> b ``` *Alice's private balance in USDC is the sum of her three USDC notes.* Nobody else can see these notes as Alice's. To an observer they are three sealed records among everyone else's. ## Spending makes new notes A payment never edits a note. It consumes some of the payer's notes and creates new ones. Say Alice pays Bob 10 USDC. The SDK spends up to three of her largest notes in one payment, so here it spends all three, 25 USDC in total. It creates two new notes: 10 USDC for Bob, and 15 USDC back to Alice as change. Fees are left out to keep the sums simple. Alice's balance is now one 15 USDC note. Bob's balance has gained a 10 USDC note. ## Why the device keeps a copy Finding a user's notes takes their keys, and the keys stay in their browser. So the SDK finds the notes on the device. It reads new pool activity, tries the user's keys on each new note, and keeps the ones that open. It stores what it finds in IndexedDB, one database per account. Every tab of your app shares it. `readBalances` reads this copy and fetches nothing. Bringing the copy up to date is a [sync](https://sdk.nimbusprivacy.xyz/explainers/sync). ## The states of a balance `readBalances` returns one entry per token and state: | State | What it holds | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `available` | Notes your user can spend now. | | `pending` | Notes not yet confirmed on Solana, or not yet past the slot they become spendable from. | | `reserved` | Notes a payment is spending. The payment may be running, stopped or sent and not yet settled. A deposit that merges notes reserves them too. | | `expired` | Notes past the last slot they could be spent in. | A payment reserves every note it spends, change included. While Alice pays Bob 10 USDC, all 25 USDC show as reserved. [Tabs and reserved funds](https://sdk.nimbusprivacy.xyz/explainers/reserved-funds) explains why. ## What this means for your app - **Sync before you show a balance.** The copy is only as fresh as the last sync. - **A new device starts empty.** It shows nothing until its first sync has read the user's notes back. - **Show the states apart.** Show `available` as the balance, and `pending` and `reserved` beside it. * [Keeping the device in sync](https://sdk.nimbusprivacy.xyz/explainers/sync): When to sync, and what a sync does. * [Show balances](https://sdk.nimbusprivacy.xyz/helpers/balances): Read the private balance with readBalances. Last updated 21 October 2026 # Tabs and reserved funds (/explainers/reserved-funds) On Solana, if two transactions need the same lamports, only one succeeds. A private balance has the same rule. A payment that loses that race wastes a proof and your user's time, so the SDK stops it before it starts. ## Two tabs, one balance Every tab of your app shares one copy of the private balance. Two tabs could pick the same notes for two payments at once. To prevent that, a payment **reserves** its notes before it builds anything. Another payment, in this tab or another, chooses among the notes left. `readBalances` counts reserved notes as `reserved`. A payment reserves every note it spends, change included. Paying 10 USDC from three notes worth 25 USDC reserves all 25. Each running payment also holds a Web Lock named for it. The browser drops the lock when the tab closes or crashes. That is how the SDK tells a running payment from a stopped one. A running payment keeps its notes until it finishes. A stopped one keeps them until something frees them, as below. ## Where a reserved payment stands `listReservedSpends` lists every payment holding notes. Each has a `state`, and a `running` flag that says whether some tab still holds its lock. | `state` | What it means | What releasing it does | | ----------- | --------------------------------------------- | ----------------------------------------------------------------------- | | `unsent` | Nothing has left the device. | Frees its notes at once. | | `sent` | It may have reached the relayer or Solana. | Stops new attempts. The notes free once it lands or can no longer land. | | `releasing` | Released while a sent attempt may still land. | Nothing more. It is already waiting. | A sent payment keeps its notes until it settles. Spending them again meanwhile could race it. A deposit can reserve notes too. A deposit merges up to three of your user's notes of that token into its change, and reserves them until it settles. It is listed with `kind: "deposit"`. ## What frees reserved funds - **Finishing.** A payment that lands spends its notes. Once a sync sees that, they leave the balance. - **The next sync,** if the tab closed before sending anything. The payment is `unsent` and no tab holds its lock. - **Your app's release button.** `releaseReservedNotes` releases what is not running. With `force: true` it releases a running payment too, which then fails at its next step. - **A relayer's refusal.** A payment that comes back `relay-refused` never reached Solana, so its notes free at once. - **Time,** for a sent payment that can no longer land. After a relay comes back `failed`, its notes stay reserved until its proof's window passes. ## What this means for your app - **Show reserved funds apart from the available balance,** so the numbers add up. - **Offer a release button** for payments that are not running. - **Expect a quick retry to fail after `failed`.** Its notes may still be reserved. A retry after `relay-refused` can spend them at once. * [Reserved funds](https://sdk.nimbusprivacy.xyz/helpers/reserved-funds): List and release what a payment holds. * [Private balances](https://sdk.nimbusprivacy.xyz/explainers/private-balance): Every state a balance can be in. Last updated 21 October 2026 # Keeping the device in sync (/explainers/sync) A wallet app reads a token balance straight from an RPC node. A private balance cannot be read that way. Only your user's keys can find their notes, so the device finds them and keeps a copy. A sync brings that copy up to date. ## What a sync does `syncNoteStore` runs in rounds until it has read everything new: ```mermaid sequenceDiagram participant App as Your app participant SDK participant Nimbus as Nimbus indexer and Solana participant Store as This device App->>SDK: syncNoteStore loop until caught up SDK->>Nimbus: the next page of pool activity SDK->>SDK: try your user's keys on each new note SDK->>Store: save their notes and spends end SDK->>Store: release payments a closed tab left SDK-->>App: ingested ``` *Each round reads one page of new pool activity and saves what belongs to your user.* In each round, the SDK reads one page of new notes, spends and records for each pool. It keeps the notes your user's keys open. It marks their notes as spent once Solana shows them spent. Last, it frees the notes of any payment whose tab closed before sending it. A sync downloads every new note, not just your user's. So it never tells the indexer which notes are theirs. ## When the SDK syncs for you Every deposit, Private Send and withdrawal syncs first, by default. A followed Private Send or withdrawal syncs again once it lands. Pass `sync: false` to a payment only if you synced just before. ## When your app should sync - **On load,** once the session is open, before you show a balance. - **When the tab regains focus,** since your user may have been paid meanwhile. - **On an interval** while the page is visible, to pick up incoming payments. Every tab shares one copy. A sync in one tab updates the balance for all of them. ## Long syncs A device that has been away a while, or a new device, has a lot to read. Three options keep a sync in hand: - **`maxRounds`** stops after that many rounds. The result's `caughtUp` says whether it finished. - **`signal`** stops it between rounds when an `AbortSignal` fires. - **`onProgress`** hears about each round, with the slot reached. The next sync picks up where the last one stopped. ## What this means for your app - **Show "updating" while a sync runs.** Show the last balance meanwhile, not a spinner in its place. - **Never block a payment on your own sync.** The payment syncs for itself. - **Expect `unregistered` before registration.** Until your user registers, there is nothing to read. * [syncNoteStore](https://sdk.nimbusprivacy.xyz/reference/reads#syncnotestore): Every option and result of a sync. * [Private balances](https://sdk.nimbusprivacy.xyz/explainers/private-balance): What the device keeps, and why. Last updated 21 October 2026 # What's public and what's private (/explainers/visibility) A normal SPL transfer shows everything: the sender, the recipient, the token and the amount. With Nimbus, the way in and the way out still look like that. Everything in between is private. ## What each payment shows These cards show what anyone reading Solana can see, and what stays private. A zone is a group of notes within the pool. Most apps use the default zone and never set one. ## How privacy narrows The proofs reveal nothing about the notes they spend. Privacy can still narrow in these ways: - **Registration ties the wallets together.** Anyone can see that your user's wallet and platform wallet belong to one account. So a deposit can be traced to your user. - **Amounts and timing can match.** A withdrawal of the amount just deposited, soon after, suggests the same person. The chain records no link, but the pattern is there. - **A known destination gives it away.** If the withdrawal address is already tied to your user, so is the withdrawal. - **The relayer's tip is public.** The SDK pays it in the token being moved, so a tip shows which token moved. - **The recipient learns the sender.** A Private Send records the sender's identity in the new notes. Bob can read it and follow it to Alice's wallets. - **Rare tokens and schedules stand out.** A token few people use, or payments at the same hour every week, are easier to group. ## What this means for your app - **Don't suggest an instant round trip.** Value that stays in the pool for a while, with other activity in between, is harder to pair. - **Don't default to withdrawing what was deposited.** Different amounts remove the easiest match. - **Let users choose where to withdraw.** Withdrawing to the wallet that deposited links the two ends. Say so beside the address field. - **Tell senders that recipients will know them.** A Private Send is private from everyone except the person paid. - **Keep the sign-in on the device.** Whoever has the stored signature has the account. See [The private account](https://sdk.nimbusprivacy.xyz/explainers/private-account). The protocol documentation covers each point in depth: - [What is private and what is visible](https://docs.nimbusprivacy.xyz/privacy/visibility): Every operation, field by field. - [How privacy is lost](https://docs.nimbusprivacy.xyz/privacy/leaks): Every known way privacy narrows. - [The anonymity set](https://docs.nimbusprivacy.xyz/privacy/anonymity-set): The notes your user's cannot be told apart from. - [Disclosure](https://docs.nimbusprivacy.xyz/disclosure): Who can read which parts of a payment. Last updated 21 October 2026 # Quickstart (/getting-started/quickstart) ## What you'll build Alice deposits 25 USDC into her private balance, then pays Bob 10 USDC from it. Bob takes his USDC back out to his own wallet. Everything runs on mainnet, so it moves real USDC. Each step below is a few lines of browser code. ## Install ```bash pnpm add @umbra/nimbus-solana-web @solana/kit ``` You use `@solana/kit` for its `address` function, which turns a base58 string into an `Address`. ## Serve the SDK's files The SDK ships WebAssembly and a worker script that your app serves itself. Copy them into your static folder after every install, and keep that folder out of git: ```json title="package.json" { "scripts": { "postinstall": "nimbus-solana-web copy-assets public/nimbus" } } ``` The page that loads the SDK also needs two headers. They let the proofs use shared memory. **Next.js** ```ts const isolation = [ { key: "Cross-Origin-Opener-Policy", value: "same-origin" }, { key: "Cross-Origin-Embedder-Policy", value: "require-corp" }, ]; export default { headers: async () => [{ source: "/:path*", headers: isolation }] }; ``` **Vite** ```ts const headers = { "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", }; export default { server: { headers }, preview: { headers } }; ``` [Install and set up](https://sdk.nimbusprivacy.xyz/getting-started/setup) explains both, and covers Vercel and Cloudflare. ## Load the SDK Load it once per page, in code that runs in the browser. It needs no wallet yet. ```ts import { loadNimbusSdk } from "@umbra/nimbus-solana-web"; const sdk = await loadNimbusSdk( { cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/" }, ); ``` `assetsBaseUrl` is the URL of the folder you copied the files into. In React, load it from an effect; [Use it with React](https://sdk.nimbusprivacy.xyz/getting-started/react) shows how. ## Connect a wallet `listWallets` returns the installed wallets that can sign on mainnet. Show them, and connect the one Alice picks. ```ts import { listWallets, openWalletConnection } from "@umbra/nimbus-solana-web"; const [wallet] = listWallets(sdk); if (wallet === undefined) throw new Error("Install a Solana wallet first."); const connection = await openWalletConnection(sdk, { wallet }); switch (connection.kind) { case "declined": throw new Error("Alice declined to connect."); case "no-account": throw new Error("The wallet has no account on Solana mainnet."); case "connected": break; } const { signer, transactionVersion } = connection.connected; ``` Keep `transactionVersion`. It says which transactions the wallet can sign, and every payment her wallet signs takes it. ## Open Alice's private account Opening a session asks Alice's wallet to sign one message. That signature unlocks her private account on this device. No money moves, and it costs nothing. ```ts import { openNimbusSession } from "@umbra/nimbus-solana-web"; const opened = await openNimbusSession( sdk, { connectedWallet: signer }, { gatewayToken }, ); if (opened.kind === "declined") throw new Error("Alice declined to sign."); const { session } = opened; ``` The gateway token lets your app use Nimbus's services on mainnet. To get one, email [dev@umbraprivacy.com](mailto:dev@umbraprivacy.com) or reach out to any Umbra team member. [The mainnet gateway token](https://sdk.nimbusprivacy.xyz/getting-started/setup#the-mainnet-gateway-token) shows how to pass it. ## Register her Each wallet registers once, before its first deposit. `session.registration` says whether Alice already has. ```ts import { createRegistrationContext, register } from "@umbra/nimbus-solana-web"; if (session.registration.kind === "unregistered") { const result = await register( await createRegistrationContext(session), {}, { transactionVersion }, ); switch (result.kind) { case "complete": break; case "submitted": if (result.submission.submission.kind !== "landed") { throw new Error("The registration did not land. Try again."); } break; case "declined": case "store-refused": throw new Error("Nothing was sent. Try again."); } } ``` Her wallet approves one transaction and pays its network fee. [Registration](https://sdk.nimbusprivacy.xyz/registration) covers every result. ## Deposit 25 USDC USDC has 6 decimals, so 25 USDC is `25_000_000n`. Alice deposits into her own private balance. ```ts import { address } from "@solana/kit"; import { createDepositContext, deposit } from "@umbra/nimbus-solana-web"; const usdc = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); const result = await deposit( await createDepositContext(session), { mint: usdc, amount: 25_000_000n, recipient: session.walletAddress }, { transactionVersion }, ); if ( result.kind !== "submitted" || result.submission.submission.kind !== "landed" ) { throw new Error("The deposit did not land."); } ``` The protocol fee comes out of the 25 USDC. [Deposit](https://sdk.nimbusprivacy.xyz/deposit) handles every result, and the `unknown` case where Solana has not decided yet. ## Read her balance The private balance lives on Alice's device. Sync it with Solana first, then read it. ```ts import type { Balance } from "@umbra/nimbus-solana-web"; import { readBalances, syncNoteStore } from "@umbra/nimbus-solana-web"; await syncNoteStore(session, {}); const balances = await readBalances(session, {}); const usdcIn = (state: Balance["state"]): bigint => balances.find((row) => row.mint === usdc && row.state === state)?.total ?? 0n; console.log("Available:", usdcIn("available")); console.log("Reserved:", usdcIn("reserved")); ``` Each row is one token in one state. A new deposit reads as `pending` until Solana finalizes it, then as `available`. `reserved` is what a payment still in progress holds. [Balances](https://sdk.nimbusprivacy.xyz/helpers/balances) explains every state. ## Pay Bob 10 USDC privately A Private Send moves value from Alice's private balance to Bob's. Bob must have registered too. If he has not, `transfer` throws a `NimbusSdkError` with reason `recipient-unregistered`. ```ts import { createTransferContext, transfer } from "@umbra/nimbus-solana-web"; import type { TransferResult } from "@umbra/nimbus-solana-web"; const result = await transfer(await createTransferContext(session), { mint: usdc, amount: 10_000_000n, recipient: bob, }); function describeTransfer(result: TransferResult): string { switch (result.kind) { case "relayed": switch (result.settlement.kind) { case "landed": return "Bob has been paid."; case "failed": return "The payment failed on Solana. Nothing moved."; case "pending": case "not-followed": return "The payment is on its way."; } case "refused": return `Nothing was sent: ${result.refusal.kind}.`; case "relay-refused": case "unsent": case "merge-failed": return "The payment was not sent. Try again."; } } console.log(describeTransfer(result)); ``` A relayer submits the payment, so Alice's wallet signs nothing and pays no network fee. The call then follows it until it settles on Solana. [Private Send](https://sdk.nimbusprivacy.xyz/transfer) covers each result. ## Bob withdraws On his own device, Bob has opened his session and registered, as Alice did. He withdraws 5 USDC to his wallet. ```ts import { createWithdrawalContext, withdraw } from "@umbra/nimbus-solana-web"; const result = await withdraw(await createWithdrawalContext(bobSession), { mint: usdc, amount: 5_000_000n, recipient: bobSession.walletAddress, }); if (result.kind === "relayed" && result.settlement.kind === "landed") { console.log("Bob withdrew 5 USDC."); } ``` The withdrawal syncs Bob's device first, so it sees Alice's payment. A relayer submits it too. [Withdraw](https://sdk.nimbusprivacy.xyz/withdraw) covers every result. > **The objects you hold** > > You load one **SDK** per page. You open one **session** per user, when they connect. You create > one **context** per payment, from the session. Every function takes one of these first. > [The function template](https://sdk.nimbusprivacy.xyz/api-design/function-template) explains the pattern. ## The whole file **The whole file** ```ts import { address } from "@solana/kit"; import { createDepositContext, createRegistrationContext, createTransferContext, createWithdrawalContext, deposit, listWallets, loadNimbusSdk, openNimbusSession, openWalletConnection, readBalances, register, syncNoteStore, transfer, withdraw, } from "@umbra/nimbus-solana-web"; const usdc = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); // Load the SDK. const sdk = await loadNimbusSdk( { cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/" }, ); // Connect a wallet. const [wallet] = listWallets(sdk); if (wallet === undefined) throw new Error("Install a Solana wallet first."); const connection = await openWalletConnection(sdk, { wallet }); if (connection.kind !== "connected") throw new Error("Not connected."); const { signer, transactionVersion } = connection.connected; // Open Alice's private account. const opened = await openNimbusSession( sdk, { connectedWallet: signer }, { gatewayToken }, ); if (opened.kind === "declined") throw new Error("Alice declined to sign."); const { session } = opened; // Register her, once. if (session.registration.kind === "unregistered") { const registration = await register( await createRegistrationContext(session), {}, { transactionVersion }, ); const registered = registration.kind === "complete" || (registration.kind === "submitted" && registration.submission.submission.kind === "landed"); if (!registered) throw new Error("The registration did not land."); } // Deposit 25 USDC. const deposited = await deposit( await createDepositContext(session), { mint: usdc, amount: 25_000_000n, recipient: session.walletAddress }, { transactionVersion }, ); if ( deposited.kind !== "submitted" || deposited.submission.submission.kind !== "landed" ) { throw new Error("The deposit did not land."); } // Read her balance. await syncNoteStore(session, {}); const balances = await readBalances(session, {}); for (const row of balances) { if (row.mint === usdc) console.log(row.state, row.total); } // Pay Bob 10 USDC privately. const paid = await transfer(await createTransferContext(session), { mint: usdc, amount: 10_000_000n, recipient: bob, }); if (paid.kind === "relayed" && paid.settlement.kind === "landed") { console.log("Bob has been paid."); } // Bob withdraws 5 USDC, from his own session. const withdrawn = await withdraw(await createWithdrawalContext(bobSession), { mint: usdc, amount: 5_000_000n, recipient: bobSession.walletAddress, }); if (withdrawn.kind === "relayed" && withdrawn.settlement.kind === "landed") { console.log("Bob withdrew 5 USDC."); } ``` ## Next - [Install and set up](https://sdk.nimbusprivacy.xyz/getting-started/setup): Every header, file and token your app needs, in detail. - [Use it with React](https://sdk.nimbusprivacy.xyz/getting-started/react): Load once, follow wallets, keep the session in context. - [Private balances](https://sdk.nimbusprivacy.xyz/explainers/private-balance): Where the balance lives, and why the device syncs. - [Production checklist](https://sdk.nimbusprivacy.xyz/production/checklist): What to check before real users arrive. Last updated 21 October 2026 # Use it with React (/getting-started/react) These patterns keep the SDK out of server rendering and tie a session to the component tree. They come from the [example app](https://sdk.nimbusprivacy.xyz/resources/example-app), a Next.js App Router app. They work the same in a Vite app. ## Load once, in the browser The SDK needs IndexedDB, workers and WebAssembly threads. None of these exist while a server renders a page. So import the SDK inside an effect, and keep one load for the whole page: ```tsx "use client"; import type { NimbusSdk } from "@umbra/nimbus-solana-web"; import { useEffect, useState } from "react"; export type SdkState = | { kind: "loading" } | { kind: "loaded"; sdk: NimbusSdk } | { kind: "failed"; message: string }; let loading: Promise | undefined; /** Loads the SDK the first time it is called, and returns that same load after. */ async function loadOnce(): Promise { loading ??= import("@umbra/nimbus-solana-web").then(({ loadNimbusSdk }) => loadNimbusSdk({ cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/" }), ); return loading; } /** Returns the SDK once it has loaded in the browser. */ export function useNimbusSdk(): SdkState { const [state, setState] = useState({ kind: "loading" }); useEffect(() => { let current = true; loadOnce().then( (sdk) => current && setState({ kind: "loaded", sdk }), (thrown: unknown) => current && setState({ kind: "failed", message: String(thrown) }), ); return () => { current = false; }; }, []); return state; } ``` - **`"use client"`** marks the file for the browser in the Next.js App Router. Without it, the hook cannot run. - **The dynamic `import`** runs only inside the effect, so the server never evaluates the SDK. - **`loadOnce`** shares one load between every component, and survives React's Strict Mode running effects twice. - **`current`** stops a component that has unmounted from setting state. A component that uses the SDK on every line can instead be loaded with Next.js's `next/dynamic` and `ssr: false`, from a client component. ## Pick a wallet Wallet extensions can register after the page loads. `followWallets` calls you back each time the list changes, so start it in an effect: ```tsx "use client"; import { followWallets, listWallets } from "@umbra/nimbus-solana-web"; import type { NimbusSdk, Wallet } from "@umbra/nimbus-solana-web"; import { useEffect, useState } from "react"; /** Returns the wallets that can sign on the SDK's cluster, kept current. */ export function useWallets(sdk: NimbusSdk): readonly Wallet[] { const [wallets, setWallets] = useState(() => listWallets(sdk)); useEffect(() => { setWallets(listWallets(sdk)); return followWallets(sdk, { onChange: setWallets }); }, [sdk]); return wallets; } /** Renders one button per wallet. */ export function WalletPicker(props: { sdk: NimbusSdk; onPick: (wallet: Wallet) => void; }) { const wallets = useWallets(props.sdk); if (wallets.length === 0) { return

Install a Solana wallet to continue.

; } return (
    {wallets.map((wallet) => (
  • ))}
); } ``` `followWallets` returns the function that stops following. Returning it from the effect stops it when the component unmounts. ## Keep the session in context A session holds the user's keys in memory. Open it when they sign in, and close it when they sign out, disconnect, or leave. ```tsx "use client"; import { closeNimbusSession, openNimbusSession, openWalletConnection, } from "@umbra/nimbus-solana-web"; import type { NimbusSdk, NimbusSession, TransactionVersion, Wallet, } from "@umbra/nimbus-solana-web"; import { createContext, useContext, useEffect, useState } from "react"; import type { ReactNode } from "react"; interface Account { session: NimbusSession; wallet: Wallet; transactionVersion: TransactionVersion; } interface AccountState { account: Account | undefined; signIn: (wallet: Wallet) => Promise; signOut: () => void; } /** The Wallet Standard's `standard:events` feature, as far as this file uses it. */ interface WalletEvents { on: ( event: "change", listener: (properties: { accounts?: Wallet["accounts"] }) => void, ) => () => void; } const AccountContext = createContext(undefined); /** Holds the signed-in account, and closes its session when it goes. */ export function AccountProvider(props: { sdk: NimbusSdk; gatewayToken: string; children: ReactNode; }) { const { sdk, gatewayToken } = props; const [account, setAccount] = useState(); async function signIn(wallet: Wallet): Promise { const connection = await openWalletConnection(sdk, { wallet }); if (connection.kind !== "connected") return connection.kind; const { signer, transactionVersion } = connection.connected; const opened = await openNimbusSession( sdk, { connectedWallet: signer }, { gatewayToken }, ); if (opened.kind === "opened") { setAccount({ session: opened.session, wallet, transactionVersion }); } return opened.kind; } useEffect(() => { if (account === undefined) return; return () => closeNimbusSession(account.session); }, [account]); useEffect(() => { if (account === undefined) return; const events = account.wallet.features["standard:events"] as | WalletEvents | undefined; return events?.on("change", ({ accounts }) => { const stillThere = accounts?.some( (entry) => entry.address === account.session.walletAddress, ); if (stillThere === false) setAccount(undefined); }); }, [account]); const state = { account, signIn, signOut: () => setAccount(undefined) }; return ( {props.children} ); } /** Returns the account state; call it below `AccountProvider`. */ export function useAccount(): AccountState { const state = useContext(AccountContext); if (state === undefined) throw new Error("useAccount is outside AccountProvider"); return state; } ``` - **The first effect** closes a session whenever the account changes or the provider unmounts. Closing zeroes the keys in memory. Closing twice does nothing more. - **The second effect** listens for the wallet's `change` event. When the user disconnects or switches account, the session closes. - **`signIn`** returns the `kind` it stopped at, such as `declined`, so your UI can say why. By default, the user's signature is kept for the tab's life, so a reload does not ask again. To forget it on sign-out, call the store's `wipeSignatureSecret` as well. [Sessions](https://sdk.nimbusprivacy.xyz/helpers/sessions) compares the three places it can be kept. ## Show progress Every payment takes an `onProgress` callback. Put the stage into state, and render it: ```tsx "use client"; import { createWithdrawalContext, withdraw } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; import type { Address } from "@solana/kit"; import { useState } from "react"; /** Withdraws 5 USDC to the user's own wallet, showing each stage. */ export function WithdrawButton(props: { session: NimbusSession; usdc: Address }) { const { session, usdc } = props; const [stage, setStage] = useState(); async function run(): Promise { const result = await withdraw( await createWithdrawalContext(session), { mint: usdc, amount: 5_000_000n, recipient: session.walletAddress }, { onProgress: (event) => setStage(event.stage) }, ); setStage(result.kind); } return ( <> {stage === undefined ? null :

{stage}

} ); } ``` Proofs run in the tab. Ask the user to keep it open while a payment runs, and disable the buttons that would start another. [Show progress](https://sdk.nimbusprivacy.xyz/api-design/progress) has a message for every stage. ## Sync in the background The private balance changes only when the device syncs. Payments sync on their own. To show payments Alice receives, sync on an interval: ```tsx "use client"; import { syncNoteStore } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; import { useEffect } from "react"; /** Syncs every 30 seconds while the session is open, then calls `onSynced`. */ export function useBackgroundSync( session: NimbusSession | undefined, onSynced: () => void, ): void { useEffect(() => { if (session === undefined) return; const controller = new AbortController(); let running = false; async function syncOnce(): Promise { if (running || session === undefined) return; running = true; try { const result = await syncNoteStore( session, {}, { signal: controller.signal }, ); if (result.kind === "ingested") onSynced(); } catch (thrown) { if (!controller.signal.aborted) console.error(thrown); } finally { running = false; } } void syncOnce(); const timer = setInterval(() => void syncOnce(), 30_000); return () => { clearInterval(timer); controller.abort(); }; }, [session, onSynced]); } ``` - **`running`** skips a round while the previous one is still going. - **The `AbortController`** stops a sync between rounds when the session changes or the component unmounts. - **`onSynced`** is where you read the balance again with `readBalances`. Wrap it in `useCallback`, or the effect restarts on every render. [Sync](https://sdk.nimbusprivacy.xyz/helpers/sync) covers what a sync reads and what each result means. ## Next - [The example app](https://sdk.nimbusprivacy.xyz/resources/example-app): A whole Next.js app built from these patterns. - [Sessions](https://sdk.nimbusprivacy.xyz/helpers/sessions): Open, close and remember a user's private account. - [Production checklist](https://sdk.nimbusprivacy.xyz/production/checklist): What to check before real users arrive. Last updated 21 October 2026 # Install and set up (/getting-started/setup) Your app needs four things before the SDK runs: the package, its files served from your site, two response headers, and a gateway token on mainnet. This page sets up each one, then checks them. ## Install the package ```bash pnpm add @umbra/nimbus-solana-web @solana/kit ``` Install `@solana/kit` beside it. Every SDK function takes mints and wallets as a kit `Address`, which its `address` function makes from a base58 string. The SDK runs in the browser only. It needs IndexedDB, Web Workers, Web Locks and WebAssembly threads, which a server render does not have. Load it from code that runs in the browser. ## Serve the SDK's files The package ships the browser files the SDK runs in its `assets/` folder: - **`nimbus_primitives_wasm_bg.wasm`**, the WebAssembly the SDK's cryptography runs in. - **`prover-worker.js`**, the worker script each proof runs in. - **`prover/`**, the WebAssembly prover the worker builds proofs with. - **`witness-graph/`**, the WebAssembly module that prepares each proof's inputs. Your app serves these from its own site. A browser starts a module worker only from a script on the page's own origin. No preset hosts them for you. ### Copy them on every install `nimbus-solana-web copy-assets ` copies every file into a folder you serve. Run it after every install, so an upgraded SDK never meets old files: ```json title="package.json" { "scripts": { "postinstall": "nimbus-solana-web copy-assets public/nimbus" } } ``` You can run it before `dev` and `build` instead, as `nimbus-solana-web copy-assets public/nimbus && next build`. The command creates the folder and overwrites files of the same names. It leaves other files alone. The copied files are build output. Add the folder to `.gitignore`: ```text title=".gitignore" /public/nimbus/ ``` ### Point the SDK at them Pass the folder's URL as `assetsBaseUrl`. It must end with `/`, and it may be relative to the page. ```ts import { loadNimbusSdk } from "@umbra/nimbus-solana-web"; const sdk = await loadNimbusSdk( { cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/" }, ); ``` The SDK fetches the WebAssembly from that folder while it loads. Each proof starts a worker from `prover-worker.js`, and the worker fetches `prover/` and `witness-graph/` beside it. ### How the SDK checks them The package records the SHA-256 of every file it ships. The SDK hashes each file before it runs it, and refuses one that does not match: - The main WebAssembly is hashed before it starts. Its functions are then tested against known answers. - The worker script is fetched and hashed before a worker starts from it. - The worker hashes each file it loads in turn. An altered file throws a `NimbusSdkError` with reason `dependency-nonconforming`. The message names the file and both hashes. The usual cause is files left over from an older SDK, so run `copy-assets` again. `sdk.settings.assetPinning` says whether this copy of the SDK checks at all. A packed package reads `"pinned"`. It reads `"unpinned"` only when the SDK runs from its source, and then nothing is checked. ```ts if (sdk.settings.assetPinning !== "pinned") { throw new Error("This SDK build does not check its files."); } ``` ## Turn on cross-origin isolation The prover shares memory between threads, which a browser allows only on a page served with these two headers: ```text Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` Send both with every page that loads the SDK. Sending them with every response is simplest. **Next.js** ```ts import type { NextConfig } from "next"; const crossOriginIsolation = [ { key: "Cross-Origin-Opener-Policy", value: "same-origin" }, { key: "Cross-Origin-Embedder-Policy", value: "require-corp" }, ]; const config: NextConfig = { headers: async () => [{ source: "/:path*", headers: crossOriginIsolation }], }; export default config; ``` This covers `next dev`, `next start` and a deployment on Vercel. **Vite** ```ts const crossOriginIsolation = { "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", }; export default { server: { headers: crossOriginIsolation }, preview: { headers: crossOriginIsolation }, }; ``` This covers `vite` and `vite preview`. The host that serves your build needs the headers too. **Vercel** ```json title="vercel.json" { "headers": [ { "source": "/(.*)", "headers": [ { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }, { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" } ] } ] } ``` Use this for a static build on Vercel, such as Vite's. **Cloudflare** ```text title="public/_headers" /* Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` Cloudflare Pages reads `_headers` from the root of the folder it deploys. To check, open your app and type this into the browser's console: ```ts console.log(crossOriginIsolated); // true on an isolated page ``` If it prints `false`, check the headers on the page's own response in the network panel. Headers on the SDK's files alone are not enough. Isolation changes what the page may embed. An image or script from another origin loads only if that origin allows it, with `Cross-Origin-Resource-Policy` or CORS. Serve token logos from your own site. Requests the SDK makes with `fetch` work as before. ## The mainnet gateway token On mainnet, Nimbus's services sit behind a gateway. It accepts a request only when it carries a token issued for your app. Without one, it answers 401. To get one, email [dev@umbraprivacy.com](mailto:dev@umbraprivacy.com) or reach out to any Umbra team member. The RPC, the indexer and the context service are not behind the gateway. So without a token, a session still opens, syncs and reads balances. Registration and every payment fail with reason `unauthorized`. ### Pass it to the session `openNimbusSession` takes the token in its `gatewayToken` option. Pass a string, or a function that returns one. **A fixed token** ```ts import { openNimbusSession } from "@umbra/nimbus-solana-web"; const opened = await openNimbusSession( sdk, { connectedWallet: signer }, { gatewayToken: "your-gateway-token" }, ); ``` The string must be an RFC 6750 bearer token. Anything else throws `misconfigured`, and the message never repeats it. **A function** ```ts import { openNimbusSession } from "@umbra/nimbus-solana-web"; let cached: string | undefined; async function fetchGatewayToken(): Promise { if (cached === undefined) { const response = await fetch("/api/gateway-token"); cached = await response.text(); } return cached; } const opened = await openNimbusSession( sdk, { connectedWallet: signer }, { gatewayToken: fetchGatewayToken }, ); ``` The SDK calls it for every request to the gateway and keeps nothing. Cache the token yourself, as above. If the function throws, that request fails `unauthorized`. Use a function when your app loads the token at run time, for example from your own backend. Either way, a visitor can read any token their browser holds. ### Where it is sent The SDK sends the token as `Authorization: Bearer` to the gateway's origins only. `sdk.settings.gatewayEndpoints` lists them, each with the name an error uses for it: ```ts for (const { service, endpoint } of sdk.settings.gatewayEndpoints) { console.log(`${service}: ${endpoint}`); } ``` The RPC, the indexer, the context service, your own site and the proving key host never receive it. The SDK never stores it, logs it, or puts it in an error. `closeNimbusSession` drops it. ## Check it works Load the SDK and print what it resolved. This is a quick test that the files and headers are right: ```ts import { isNimbusSdkError, loadNimbusSdk } from "@umbra/nimbus-solana-web"; try { const sdk = await loadNimbusSdk( { cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/" }, ); console.log(sdk.settings.assetPinning, sdk.settings.endpoints); } catch (thrown) { if (isNimbusSdkError(thrown)) console.error(thrown.reason, thrown.message); } ``` It can throw these: | Reason | Cause | | -------------------------- | ---------------------------------------------------------------------------------------------------------- | | `unsupported-browser` | The page is not cross-origin isolated, or the browser lacks a feature the SDK needs. The message names it. | | `misconfigured` | `assetsBaseUrl` does not end with `/`, or an endpoint is missing. The message names it. | | `service-unavailable` | The WebAssembly could not be fetched. Check that `copy-assets` ran. | | `dependency-nonconforming` | A served file is not the one this SDK ships. Run `copy-assets` again. | [Troubleshooting](https://sdk.nimbusprivacy.xyz/production/troubleshooting) covers each one in more depth. ## Next - [Quickstart](https://sdk.nimbusprivacy.xyz/getting-started/quickstart): Deposit, pay and withdraw in one walkthrough. - [Use it with React](https://sdk.nimbusprivacy.xyz/getting-started/react): Load once, follow wallets, keep the session in context. - [Endpoints](https://sdk.nimbusprivacy.xyz/production/endpoints): The mainnet endpoints, your own RPC and your own services. Last updated 21 October 2026 # Balances (/helpers/balances) `readBalances` reads the user's private balance from their device. It fetches nothing, so it returns at once. It shows what the last [sync](https://sdk.nimbusprivacy.xyz/helpers/sync) left, so sync first when you need the latest. In this guide, Alice's app shows her USDC balance, with what is still settling beside it. ## Read the balance ```ts import { readBalances, syncNoteStore } from "@umbra/nimbus-solana-web"; await syncNoteStore(session, {}); const balances = await readBalances(session, {}); ``` Each `Balance` is what Alice holds of one token in one state: - **`mint`** is the token's address. It is absent for a token the SDK's preset does not list. - **`state`** says whether she can spend it, below. - **`total`** is the amount, in the token's smallest unit. - **`count`** is how many separate pieces make up the total. - **`zone`** is `0`, unless you read with a `zone` option. Most apps never pass one. A user who has not registered yet reads as an empty list. ## Each state | `state` | What it means | What to show | | ----------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | `available` | Alice can spend it now. | Her balance. | | `pending` | She received it, but it is not final on Solana yet, or not yet past the time it unlocks. | "Settling". It becomes available on its own. | | `reserved` | A payment that has not finished is holding it. | "In use". See [Reserved funds](https://sdk.nimbusprivacy.xyz/helpers/reserved-funds). | | `expired` | It passed the time it could be spent by. | "Expired". It can no longer be spent. | Show `available` as the balance. A payment can only spend that part. ## One row per token `readBalances` returns one entry per token and state. Most apps show one row per token, with the spendable amount first. The example app groups them like this: ```ts interface TokenBalance { mint: string; available: bigint; pending: bigint; reserved: bigint; expired: bigint; } function groupBalances(balances: readonly Balance[]): TokenBalance[] { const rows = new Map(); for (const balance of balances) { if (balance.mint === undefined) continue; const row = rows.get(balance.mint) ?? { mint: balance.mint, available: 0n, pending: 0n, reserved: 0n, expired: 0n, }; row[balance.state] += balance.total; rows.set(balance.mint, row); } return [...rows.values()]; } ``` ## Show token names and logos Amounts are integers in the token's smallest unit. Format them with the token's decimals, which `sdk.mints` lists: ```ts const usdc = sdk.mints.find( (entry) => entry.mint === "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", ); console.log(formatAmount(25_000_000n, usdc?.decimals ?? 0)); // "25" ``` [Tokens](https://sdk.nimbusprivacy.xyz/helpers/tokens) has `formatAmount` in full. The SDK carries no token names or logos. The example app reads them from a token list it writes ahead of time. [Tokens](https://sdk.nimbusprivacy.xyz/helpers/tokens#names-and-logos) shows how. ## Refresh after a sync Read the balance again after each of these: - **A sync you run.** See [Sync](https://sdk.nimbusprivacy.xyz/helpers/sync) for one that runs in the background. - **A payment that returns.** Each payment syncs before it starts. A Private Send or withdrawal the SDK followed until it landed has synced again since. - **A release** of [reserved funds](https://sdk.nimbusprivacy.xyz/helpers/reserved-funds). A deposit lands in the private balance with the next sync, not at once. Sync before you read it back. ## Next - [Sync](https://sdk.nimbusprivacy.xyz/helpers/sync): Keep the device up to date with Solana. - [History](https://sdk.nimbusprivacy.xyz/helpers/history): Show the user's past payments. - [Private balances](https://sdk.nimbusprivacy.xyz/explainers/private-balance): Where the balance lives, and why the device syncs. - [API reference](https://sdk.nimbusprivacy.xyz/reference/reads#readbalances): readBalances: its options and what it returns. Last updated 21 October 2026 # Errors (/helpers/errors) Most of what your user can cause comes back as a result, such as `declined`. The SDK throws only when your app usually cannot fix things by asking the user. It throws one of two errors: | Error | What it means | Guard | | ----------------------- | ------------------------------------------------------------------------------- | ------------------------- | | `NimbusSdkError` | The SDK could not go on: a setting, the browser, the wallet or a service. | `isNimbusSdkError` | | `NimbusSolanaFlowError` | The protocol refused the payment, such as an amount too small to cover the fee. | `isNimbusSolanaFlowError` | Each carries a `reason`. Compare the reason, never the message. The message is for your logs. ## Catch them Use the guards, not `instanceof`. A guard still works when your bundle holds two copies of the SDK. ```ts import { deposit, isNimbusSdkError, isNimbusSolanaFlowError, } from "@umbra/nimbus-solana-web"; try { await deposit(context, { mint: usdc, amount: 5_000_000n, recipient: bob }); } catch (thrown) { if (isNimbusSolanaFlowError(thrown)) { console.error(thrown.reason, thrown.detail); showError("The payment was refused. Check the amount and the recipient."); } else if (isNimbusSdkError(thrown)) { console.error(thrown.reason, thrown.message); showError("Something went wrong. Try again in a moment."); } else { throw thrown; } } ``` Rethrow anything neither guard knows. It is a bug in your app or in the SDK, not a refusal. A `NimbusSolanaFlowError` also carries `detail`, the names and counts that locate the refusal. It never holds keys, so it is safe to log. [Results and errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors) lists every reason of both errors, with what to do about each. ## Next - [Results and errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors): Every reason, and what to do about it. - [Troubleshooting](https://sdk.nimbusprivacy.xyz/production/troubleshooting): Fixes for the errors developers meet most. - [API reference](https://sdk.nimbusprivacy.xyz/reference/errors): NimbusSdkError and the two guards. Last updated 21 October 2026 # History (/helpers/history) `readHistory` reads one page of the user's past payments, newest first. Like the balance, it reads the device, not the network. It shows what the last [sync](https://sdk.nimbusprivacy.xyz/helpers/sync) took in. In this guide, Alice's app lists her payments ten at a time. Bob paid her 5 USDC with a Private Send, and the app shows it as received. ## Read a page ```ts import { readHistory } from "@umbra/nimbus-solana-web"; const page = await readHistory(session, {}, { limit: 10 }); for (const item of page.items) { console.log(item.slot, item.received.length, item.spent.length); } ``` Each item is one Solana transaction that touched Alice's private balance: - **`slot`** and **`signature`** say where it landed. - **`received`** lists what it paid into her private balance. Each entry's `note.amount` is the amount, and `note.mintIdentifier` names the token. - **`spent`** lists what it took out of her private balance. - **`record`** is `kept` for Alice's own transactions. It holds their public side, such as the wallet a deposit came from. - **`others`** holds the public side of someone else's transaction that paid her, when there is one. ## Page through it `limit` is the page size, from 1 to 100. It is 20 when you leave it out. Any other value throws a `NimbusSdkError` with reason `misconfigured`. When `page.next.kind` is `more`, older items remain. Pass `page.next.position` back as `after` to read them: ```ts import { readHistory } from "@umbra/nimbus-solana-web"; switch (page.next.kind) { case "more": { const older = await readHistory( session, {}, { limit: 10, after: page.next.position }, ); console.log(older.items.length); break; } case "done": console.log("That was the oldest page."); break; } ``` Pass the position back unchanged. Each page continues where the last one ended, without repeats. ## Deposit, withdrawal or Private Send The SDK does not label items. A transaction's public side decides what it was. Money in from a public wallet is a deposit. Money out to one is a withdrawal. A transaction with neither is a Private Send. The example app labels items this way, in `apps/nimbus-example/lib/activity.ts`: ```ts type HistoryItem = HistoryPage["items"][number]; type Activity = | "deposit" | "withdrawal" | "deposit-and-withdrawal" | "private-send-sent" | "private-send-received" | "deposit-received"; function describeActivity(item: HistoryItem): Activity { if (item.record.kind === "kept") { const legs = item.record.judgment.row.legs; if (legs.kind === "known") { const paidIn = legs.inflows.length > 0; const paidOut = legs.outflows.length > 0; if (paidIn && !paidOut) return "deposit"; if (paidOut && !paidIn) return "withdrawal"; if (paidOut) return "deposit-and-withdrawal"; } return item.spent.length === 0 ? "deposit" : "private-send-sent"; } if (item.spent.length > 0) return "private-send-sent"; if (item.others.kind === "legs" && item.others.legs.inflows.length > 0) { return "deposit-received"; } return "private-send-received"; } ``` Alice's own transactions carry their `record`. Its `legs` are the public money in (`inflows`) and out (`outflows`), each with an address, a token and an amount. When the network has not served them yet, `legs.kind` is `missing`. Bob's Private Send to Alice has no record of hers, so it reads as `private-send-received`. When Bob deposits straight into Alice's private balance instead, `others` shows his wallet, and it reads as `deposit-received`. ## Name the token A received entry names its token by `note.mintIdentifier`, not by address. [`findMintByIdentifier`](https://sdk.nimbusprivacy.xyz/reference/tokens#findmintbyidentifier) turns it back into the token's address and decimals: ```ts import { findMintByIdentifier } from "@umbra/nimbus-solana-web"; for (const item of page.items) { for (const { note } of item.received) { const token = findMintByIdentifier(sdk, { mintIdentifier: note.mintIdentifier, }); console.log(note.amount, token?.mint, token?.decimals); } } ``` It returns nothing for a token the SDK's preset does not list. ## What history can and cannot show - **Only what this device has synced.** A payment appears after the next sync takes it in. - **Not who sent a Private Send.** Bob's payment shows its amount and token. Nothing in it names Bob. - **Not every slot yet.** `page.coverage` gives, for each pool, the slot the history is complete through. Do not show anything after that slot as "nothing happened". - **Payments still on their way.** `page.pendingOwn` lists Alice's own payments that were sent but not yet taken in by a sync. It is the same on every page. ## Next - [Tokens](https://sdk.nimbusprivacy.xyz/helpers/tokens): Name a token and format its amounts. - [Sync](https://sdk.nimbusprivacy.xyz/helpers/sync): Bring the device up to date before you read. - [What's public](https://sdk.nimbusprivacy.xyz/explainers/visibility): What each payment shows, and to whom. - [API reference](https://sdk.nimbusprivacy.xyz/reference/reads#readhistory): readHistory: its options and what it returns. Last updated 21 October 2026 # Load the SDK (/helpers/load) `loadNimbusSdk` is the first call your app makes. It reads the settings the SDK ships for mainnet, checks the browser, and loads the SDK's WebAssembly. Every other function takes the `NimbusSdk` it returns, directly or through a session. Load it once per page. One SDK serves any number of sessions, and it holds no keys. ## Load it ```ts import { loadNimbusSdk } from "@umbra/nimbus-solana-web"; const sdk = await loadNimbusSdk( { cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/" }, ); ``` `assetsBaseUrl` is the directory your app serves the SDK's files from, ending with `/`. [Serve the SDK's files](https://sdk.nimbusprivacy.xyz/getting-started/setup#serve-the-sdks-files) shows how to put them there. Loading asks no wallet for anything. It fetches only the SDK's WebAssembly. Nimbus's services are first reached when a session opens or a payment runs. ## The mainnet preset The SDK ships a preset for mainnet. It holds the programs, the tokens, every service endpoint and the proving keys, the large files each proof is built from. So your app passes only `assetsBaseUrl`. These are the mainnet preset's endpoints, read from the SDK when this site was built: | Option | mainnet preset | Gateway | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `endpoints.rpc` | [https://api.mainnet-beta.solana.com](https://api.mainnet-beta.solana.com) | No | | `endpoints.indexer` | [https://data.api-v2.umbraprivacy.com](https://data.api-v2.umbraprivacy.com) | No | | `endpoints.context` | [https://context.api-v2.umbraprivacy.com](https://context.api-v2.umbraprivacy.com) | No | | `endpoints.guardians` | 1: [https://gateway.api-v2.umbraprivacy.com/guardian-share-node-relay-1](https://gateway.api-v2.umbraprivacy.com/guardian-share-node-relay-1), 2: [https://gateway.api-v2.umbraprivacy.com/guardian-share-node-relay-2](https://gateway.api-v2.umbraprivacy.com/guardian-share-node-relay-2), 3: [https://gateway.api-v2.umbraprivacy.com/guardian-share-node-relay-3](https://gateway.api-v2.umbraprivacy.com/guardian-share-node-relay-3) | Yes | | `endpoints.screeners` | 0: [https://gateway.api-v2.umbraprivacy.com/screener](https://gateway.api-v2.umbraprivacy.com/screener) | Yes | | `endpoints.dataEndorsers` | 0: [https://gateway.api-v2.umbraprivacy.com/data-endorser](https://gateway.api-v2.umbraprivacy.com/data-endorser) | Yes | | `endpoints.feeEnclave` | [https://gateway.api-v2.umbraprivacy.com/fee-enclave](https://gateway.api-v2.umbraprivacy.com/fee-enclave) | Yes | | `endpoints.relayer` | [https://gateway.api-v2.umbraprivacy.com/relayer](https://gateway.api-v2.umbraprivacy.com/relayer) | Yes | | `endpoints.coordinator` | [https://gateway.api-v2.umbraprivacy.com/process-coordinator](https://gateway.api-v2.umbraprivacy.com/process-coordinator) | Yes | | `endpoints.primitivesWasm` | Your app supplies it | No | | `provingKeys` | [https://assets.api-v2.umbraprivacy.com/zk/](https://assets.api-v2.umbraprivacy.com/zk/) | No | `primitivesWasm` comes from `assetsBaseUrl`, so you rarely pass it yourself. ## Replace an endpoint Pass `endpoints` to replace any of the preset's, one at a time. The rest keep their preset values. The most common case is your own Solana RPC: ```ts import { loadNimbusSdk } from "@umbra/nimbus-solana-web"; const sdk = await loadNimbusSdk( { cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/", endpoints: { rpc: "https://rpc.example.com" }, }, ); ``` The example app does the same when its settings name an RPC. [Endpoints](https://sdk.nimbusprivacy.xyz/production/endpoints) shows how to point the SDK at services you run yourself. If you replace an endpoint the gateway fronts, your replacement receives the session's gateway token. Point it only at a host you trust. ## What your app reads from it Treat every field of the SDK as read-only. These are the ones an app usually reads: | Field | What it holds | | --------------------------- | ------------------------------------------------------------------------------------------------------------- | | `cluster` | The cluster you loaded, `"mainnet"`. | | `deployment.chain` | The chain wallets sign for, such as `solana:mainnet`. | | `mints` | The tokens the preset lists, with their decimals. See [Tokens](https://sdk.nimbusprivacy.xyz/helpers/tokens). | | `settings.endpoints` | Every endpoint, after your replacements. | | `settings.gatewayEndpoints` | The services the gateway fronts, each with its URL. The gateway token goes only to these. | | `settings.assetPinning` | `"pinned"` when the SDK checks its own files before running them, `"unpinned"` when it does not. | ### Refuse an unpinned build A packed copy of the package, the kind a registry serves, is always `pinned`. A copy built from source can be `unpinned`, and then runs its files unchecked. Refuse to go on in production if you see it: ```ts if (sdk.settings.assetPinning !== "pinned") { throw new Error("This build runs the SDK's files unchecked."); } ``` ## What it checks Before it returns, `loadNimbusSdk` makes sure the SDK can run safely on this page. Each failed check throws a `NimbusSdkError`: | What it checks | Reason when it fails | How to fix it | | ---------------------------------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------- | | Every endpoint and the proving keys are set, and `assetsBaseUrl` ends with `/` | `misconfigured` | Pass what the message names. | | The browser has `fetch`, Web Crypto, IndexedDB, Web Locks, storage and WebAssembly | `unsupported-browser` | Ask the user for a current browser. | | The page is cross-origin isolated | `unsupported-browser` | [Send the two headers](https://sdk.nimbusprivacy.xyz/getting-started/setup#turn-on-cross-origin-isolation). | | The WebAssembly file can be fetched | `service-unavailable` | Check `assetsBaseUrl` and that the files are served. | | The file's SHA-256 is the one the package pins | `dependency-nonconforming` | Serve the files that shipped with this copy of the package. | | The WebAssembly gives the right answers to its known-answer self-tests | `dependency-nonconforming` | The same. | | The file is WebAssembly at all | `unreadable` | Check that `assetsBaseUrl` names the right directory. | | The browser opens the SDK's cache in IndexedDB | `unsupported-browser` | Some private windows refuse it. | The prover worker is checked the same way when the first proof starts. Its script, and each file it loads, must match the SHA-256 the package pins. Turn each reason into a message your user can act on: ```ts import { isNimbusSdkError } from "@umbra/nimbus-solana-web"; function describeLoadFailure(thrown: unknown): string { if (!isNimbusSdkError(thrown)) return "The private balance could not load."; switch (thrown.reason) { case "unsupported-browser": return "This browser cannot run private payments. Try a current Chrome, Firefox or Safari."; case "service-unavailable": return "The app's files could not be fetched. Try again."; case "misconfigured": case "dependency-nonconforming": case "unreadable": return "This app is set up wrongly. Please tell its developers."; case "wallet-unsupported": case "unauthorized": case "recipient-unregistered": case "account-unregistered": return "The private balance could not load."; } } ``` Log `thrown.message` as well. It names the endpoint, file or browser feature at fault. ## Next - [Wallets](https://sdk.nimbusprivacy.xyz/helpers/wallets): List the user's wallets and connect one. - [Set up your app](https://sdk.nimbusprivacy.xyz/getting-started/setup): Serve the SDK's files and turn on cross-origin isolation. - [Endpoints](https://sdk.nimbusprivacy.xyz/production/endpoints): The mainnet endpoints, and services of your own. - [API reference](https://sdk.nimbusprivacy.xyz/reference/load): loadNimbusSdk: its signature, options and errors. Last updated 21 October 2026 # Prover worker and CLI (/helpers/prover-worker) The SDK builds each proof in a Web Worker, from WebAssembly it ships. Your app serves those files one of two ways: | Way | When to use it | | ---------------------------------------------------- | --------------------------------------------------------------------------- | | Copy the shipped files with `copy-assets` | Almost always. The SDK starts its own worker and checks every file it runs. | | Bundle the prover yourself and start your own worker | Only when your build must produce every script and WebAssembly file itself. | ## Copy the shipped files The package installs a command, `nimbus-solana-web`, with one subcommand: ```bash nimbus-solana-web copy-assets ``` It copies the browser files the SDK runs into ``, which your app serves as static files. It takes exactly one argument and no options. Run it from a package script, as the example app does: ```json title="package.json" { "scripts": { "assets": "nimbus-solana-web copy-assets public/nimbus" } } ``` Then pass the URL that directory is served at to `loadNimbusSdk`: ```ts import { loadNimbusSdk } from "@umbra/nimbus-solana-web"; const sdk = await loadNimbusSdk( { cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/" }, ); ``` ### What it copies - **`nimbus_primitives_wasm_bg.wasm`**, the WebAssembly `loadNimbusSdk` loads. - **`prover-worker.js`**, the worker each proof runs in. - **`prover/` and `witness-graph/`**, the WebAssembly the worker proves with. It creates the directory when it does not exist. It overwrites files of the same names and touches no other file. Run it again each time you update the package, so the files match the version you installed. [`copy-assets`](https://sdk.nimbusprivacy.xyz/reference/prover-worker#copy-assets) lists its exit codes. ## Start your own worker Skip this section unless your build must bundle the prover's WebAssembly itself. Your worker module answers each message with `proveWorkerRequest`, from the package's `./prover-worker` entry. Its whole body is: ```ts import initGraph, * as graph from "./umbra_witness_graph.js"; import initProver, * as wasm from "./umbra_prover.js"; import { proveWorkerRequest } from "@umbra/nimbus-solana-web/prover-worker"; self.addEventListener("message", (event) => { void Promise.all([initGraph(), initProver()]) .then(async () => proveWorkerRequest(event.data, { graph, wasm })) .then((answer) => self.postMessage(answer)); }); ``` This block is not type-checked here. It imports two files that your own build writes: the witness-graph and prover glue code. Then pass a function that starts your worker as `platform.startProverWorker`: ```ts import { loadNimbusSdk } from "@umbra/nimbus-solana-web"; const sdk = await loadNimbusSdk( { cluster: "mainnet" }, { endpoints: { primitivesWasm: "/wasm/nimbus_primitives_wasm_bg.wasm" }, platform: { startProverWorker: () => new Worker(proverWorkerUrl, { type: "module" }), }, }, ); ``` Without `assetsBaseUrl`, pass `endpoints.primitivesWasm` too. It is the URL your app serves the SDK's main WebAssembly file at. The SDK starts a fresh worker for each proof and terminates it afterwards. Each request carries the user's private inputs, so a worker is never reused. > **Your worker is not checked** > > The shipped worker checks every file it loads against the SHA-256 the package pins. A worker of > your own skips those checks, unless you add them. ## Next - [Set up your app](https://sdk.nimbusprivacy.xyz/getting-started/setup): Serve the SDK's files and turn on cross-origin isolation. - [Load the SDK](https://sdk.nimbusprivacy.xyz/helpers/load): Point the SDK at the files you serve. - [Browser support](https://sdk.nimbusprivacy.xyz/production/browser-support): What the prover needs from the browser. - [API reference](https://sdk.nimbusprivacy.xyz/reference/prover-worker): proveWorkerRequest and the copy-assets command. Last updated 21 October 2026 # Reserved funds (/helpers/reserved-funds) While a payment runs, it holds the part of the private balance it spends. `readBalances` shows that part as `reserved`. Most payments let go on their own when they finish. [Reserved funds](https://sdk.nimbusprivacy.xyz/explainers/reserved-funds) explains why they hold it. A payment that stopped midway can keep holding funds. In this guide, Alice closed her tab during a Private Send to Bob. Her app lists what is still held and offers her a Release button. ## List what is held ```ts import { listReservedSpends } from "@umbra/nimbus-solana-web"; const held = await listReservedSpends(session, {}); for (const spend of held) { console.log(spend.kind, spend.amount, spend.state, spend.running); } ``` It reads only the device, so it is cheap to call after every sync. Each `ReservedSpend` has: - **`id`**, which `releaseReservedNotes` takes. - **`kind`**, what the payment is, below. - **`mint`** and **`amount`**, what it moves, in the token's smallest unit. `mint` is absent for a token the SDK's preset does not list. - **`state`**, where it stands, below. - **`running`**, `true` while a tab of your app is still running it. - **`attempts`**, how many times it has been tried. | `kind` | What the payment is | | --------- | ------------------------------------------------------------------------------------------------------------------- | | `spend` | A Private Send, a withdrawal, or a step that combines amounts before one. The SDK does not tell these apart. | | `escape` | A withdrawal through the pool's escape hatch. | | `deposit` | A deposit that also combines amounts already in the private balance. Its `amount` is what it takes from the wallet. | ## Each state | `state` | What it means | What Release does | | ----------- | ------------------------------------------------------ | ---------------------------------------------------------------------------- | | `unsent` | Nothing left the device. | Frees the funds at once. | | `sent` | It may have reached the relayer or Solana. | Stops new attempts. The funds come back once it lands or can no longer land. | | `releasing` | Already released, while a sent attempt may still land. | Nothing more. Wait for it to settle. | `running` is separate from `state`. A running payment is still in progress in some tab. Leave it alone. It frees on its own when that tab finishes or closes. If a `sent` payment lands, it completes. Bob receives his USDC, and Alice's funds are spent, not returned. Say so beside the button. ## Release them ```ts import { releaseReservedNotes } from "@umbra/nimbus-solana-web"; const one = await releaseReservedNotes(session, {}, { spendId }); const everything = await releaseReservedNotes(session, {}); console.log(everything.released.length, everything.settling.length); ``` Leave out `spendId` to release every held payment. The result sorts them by what happened: | Field | What happened | | ---------- | ----------------------------------------------------------------------------------- | | `released` | The funds are available again now. | | `settling` | An attempt may still land. The funds come back once it lands or can no longer land. | | `running` | A tab is still running them, so they were left alone. | Releasing what is already released, or an id that names nothing, changes nothing. A release reads the latest slot from your RPC. When it cannot be reached, it throws a `NimbusSdkError` with reason `service-unavailable`. ## A Release button This React component follows the example app's balances panel. It shows one held payment, hides the button while the payment runs, and reports what the release did: ```tsx import { releaseReservedNotes } from "@umbra/nimbus-solana-web"; import type { ReservedSpend, ReservedSpendState, } from "@umbra/nimbus-solana-web"; import { useState } from "react"; const STATE_TEXT: Record = { unsent: "Stopped before it was sent. Release returns it now.", sent: "May have been sent. Releasing stops new attempts.", releasing: "Released. It returns once the sent attempt settles.", }; export function HeldPayment({ spend }: { spend: ReservedSpend }) { const [message, setMessage] = useState(""); async function release(): Promise { const result = await releaseReservedNotes( session, {}, { spendId: spend.id }, ); if (result.released.length > 0) { setMessage("Released. The funds are available again."); } else if (result.settling.length > 0) { setMessage("It may still land. The funds return once it settles."); } else if (result.running.length > 0) { setMessage("Another tab is still running it."); } await refreshBalances(); } if (spend.running) { return

Still running in another tab. It frees when that tab finishes.

; } return (

{STATE_TEXT[spend.state]}

{spend.state === "releasing" ? null : ( )}

{message}

); } ``` ## Force a release Pass `force: true` to release a payment a tab is still running. That payment then fails at its next step. Do not offer it as a normal button. Use it only when Alice says the other tab is gone, but the browser still reports it running. A payment released mid-flight may already have reached the relayer, so it can still land. ## Next - [Why funds are reserved](https://sdk.nimbusprivacy.xyz/explainers/reserved-funds): What holds them, and when they come back. - [Balances](https://sdk.nimbusprivacy.xyz/helpers/balances): Show reserved funds beside the rest. - [Sync](https://sdk.nimbusprivacy.xyz/helpers/sync): A sync frees payments that stopped before sending. - [API reference](https://sdk.nimbusprivacy.xyz/reference/reserved-funds): listReservedSpends and releaseReservedNotes. Last updated 21 October 2026 # Sessions (/helpers/sessions) A session is the user's private account, opened on this device. Every payment and every read takes one. Open it after the wallet connects, keep it while the user is signed in, and close it when they leave. In this guide, Alice opens her session, switches to another wallet, and signs out. ## Open a session Pass the signer from [`openWalletConnection`](https://sdk.nimbusprivacy.xyz/reference/wallets#openwalletconnection): ```ts import { openNimbusSession } from "@umbra/nimbus-solana-web"; const opened = await openNimbusSession( sdk, { connectedWallet: signer }, { gatewayToken }, ); switch (opened.kind) { case "opened": console.log(opened.session.registration.kind); break; case "declined": console.log("You declined to sign, so your private account stays locked."); break; } ``` The first time, Alice's wallet asks her to sign one message. Signing moves no money and costs nothing. Her signature unlocks her private account, and the same wallet always unlocks the same account. | `kind` | What happened | What to do | | ---------- | ------------------------------------------ | ------------------------------------------------- | | `opened` | The session is open. | Check `session.registration`, then let Alice pay. | | `declined` | Alice refused to sign. Nothing was stored. | Let her try again. | `session.registration.kind` is `registered` or `unregistered`. Alice must [register](https://sdk.nimbusprivacy.xyz/registration) before her first deposit. ## Options | Option | Default | What it does | | ---------------------- | ----------- | -------------------------------------------------------------------------------------------------- | | `gatewayToken` | none | The token your app's requests to Nimbus's gateway carry. A string, or a function that returns one. | | `signatureSecretStore` | a tab store | Where the unlock signature is kept, below. | | `accountIndex` | `0` | Which of the wallet's private accounts to open. Each index is a separate balance. | A session opens without a gateway token. But every payment then fails with reason `unauthorized`. To get one, email [dev@umbraprivacy.com](mailto:dev@umbraprivacy.com) or reach out to any Umbra team member. ## Choose where the signature is kept What Alice's signature unlocks is kept in a signature secret store. A reload reads it from there instead of asking her wallet again. The SDK has three stores: | Store | Where it keeps the secret | When Alice signs again | | ---------------------------------------------- | ---------------------------- | ----------------------------------------- | | `createMemorySignatureSecretStore()` | This page's memory only | After every reload | | `createTabSignatureSecretStore()`, the default | The tab's `sessionStorage` | When she opens a new tab | | `createPersistentSignatureSecretStore()` | The browser's `localStorage` | When she signs out, or clears the browser | The tab and persistent stores keep the secret unencrypted. Any script running on your site can read it, a compromised dependency included. The memory store keeps it off disk, at the cost of a signature after each reload. [Private accounts](https://sdk.nimbusprivacy.xyz/explainers/private-account) explains the trade-off in full. Pass the store you choose when you open the session: ```ts import { createMemorySignatureSecretStore, openNimbusSession, } from "@umbra/nimbus-solana-web"; const signatureSecretStore = createMemorySignatureSecretStore(); const opened = await openNimbusSession( sdk, { connectedWallet: signer }, { signatureSecretStore }, ); ``` Keep the store, too. You need the same one to sign the user out. ## Switch wallets A session belongs to one wallet. When Alice switches, close her old session before you open the new one. Closing clears its keys from memory. ```ts import { closeNimbusSession, openNimbusSession, } from "@umbra/nimbus-solana-web"; closeNimbusSession(session); const reopened = await openNimbusSession(sdk, { connectedWallet: nextSigner }); if (reopened.kind === "opened") { session = reopened.session; } ``` ## Close it when the wallet disconnects Close the session as soon as the wallet disconnects. A Wallet Standard wallet reports this through its `standard:events` feature. Its `change` event lists the accounts it still offers: ```ts import { closeNimbusSession } from "@umbra/nimbus-solana-web"; interface StandardEvents { readonly on: ( event: "change", listener: (change: { readonly accounts?: readonly { readonly address: string }[]; }) => void, ) => () => void; } const events = wallet.features["standard:events"] as StandardEvents | undefined; const stopListening: (() => void) | undefined = events?.on("change", ({ accounts }) => { if (accounts === undefined) return; const stillConnected = accounts.some( (account) => account.address === session.walletAddress, ); if (!stillConnected) { closeNimbusSession(session); stopListening?.(); } }); ``` The same check catches Alice switching accounts inside her wallet. Her old session closes, and you open a new one for the new account. ## Sign out `closeNimbusSession` clears the session's keys and gateway token from memory. It leaves the signature secret store alone, so the next session opens without a signature. To sign Alice out completely, wipe the store too. Use the store the session was opened with: ```ts import { closeNimbusSession } from "@umbra/nimbus-solana-web"; closeNimbusSession(session); await signatureSecretStore.wipeSignatureSecret({ walletAddress: session.walletAddress, cluster: session.sdk.cluster, chain: session.sdk.deployment.chain, }); ``` Alice's private balance stays on the device. Her next session finds it there. ## Next - [Private accounts](https://sdk.nimbusprivacy.xyz/explainers/private-account): What the signature unlocks, and how to choose a store. - [Registration](https://sdk.nimbusprivacy.xyz/registration): Register before the first deposit. - [Balances](https://sdk.nimbusprivacy.xyz/helpers/balances): Read what the private balance holds. - [API reference](https://sdk.nimbusprivacy.xyz/reference/sessions): openNimbusSession, closeNimbusSession and the three stores. Last updated 21 October 2026 # Sync (/helpers/sync) The private balance lives on the user's device. `syncNoteStore` brings it up to date with Solana and Nimbus's indexer. [Why the device syncs](https://sdk.nimbusprivacy.xyz/explainers/sync) explains the reason. Every payment syncs before it starts. Between payments, syncing is up to you. In this guide, Alice's app syncs when she opens it, then every 15 seconds while the tab is visible. ## Sync once ```ts import { syncNoteStore } from "@umbra/nimbus-solana-web"; const result = await syncNoteStore(session, {}); switch (result.kind) { case "ingested": console.log(`Synced through slot ${result.slot}.`, result.caughtUp); break; case "unregistered": console.log("Register first. There is nothing to sync yet."); break; case "unavailable": console.log(`Stopped at ${result.step}. The next sync carries on.`); break; } ``` ## The result | `kind` | What happened | What to do | | -------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------- | | `ingested` | The device took in everything through `slot`, in `rounds` rounds. | Read the balance again. | | `unregistered` | Alice has not registered, so there is nothing to read. | [Register](https://sdk.nimbusprivacy.xyz/registration) first. | | `unavailable` | The device's storage refused a step, or another tab kept writing to it. | Sync again later. Nothing taken in is lost. | `unavailable` also carries `step`, the step that stopped, and `detail`. Both are for your logs. A sync throws a `NimbusSdkError` when it cannot go on. The reason is `service-unavailable` when the indexer or the RPC cannot be reached. It is `unreadable` when either answers something that cannot be read. ## What caughtUp means A sync runs in rounds. Each round reads one page of everything new, up to `pageRows` rows. `caughtUp` is `true` when the last round reached the end of everything new. It is `false` only when `maxRounds` ran out first. Sync again to read the rest. ## Options | Option | Default | What it does | | ------------ | -------- | ---------------------------------------------------------------- | | `maxRounds` | no limit | The most rounds to run. A whole number of at least 1. | | `pageRows` | `1000` | The rows asked for in each page, from 1 to 1000. | | `signal` | none | An `AbortSignal` that stops the sync between rounds. | | `onProgress` | none | Called after each round with its `round`, `slot` and `caughtUp`. | A `maxRounds` or `pageRows` outside its range throws a `NimbusSdkError` with reason `misconfigured`, before anything is fetched. When the signal aborts, the sync stops between rounds. It rejects with the signal's reason, and what earlier rounds took in stays. ## Sync in the background This recipe syncs every 15 seconds while the tab is visible. It reads a few rounds at a time, never runs two syncs at once, and returns a function that stops it: ```ts import { syncNoteStore } from "@umbra/nimbus-solana-web"; function startBackgroundSync(session: NimbusSession): () => void { const controller = new AbortController(); let syncing = false; const tick = async (): Promise => { if (syncing || document.visibilityState !== "visible") return; syncing = true; try { const result = await syncNoteStore( session, {}, { maxRounds: 3, signal: controller.signal }, ); if (result.kind === "ingested") await refreshBalances(); } catch (thrown) { if (!controller.signal.aborted) console.error(thrown); } finally { syncing = false; } }; void tick(); const timer = setInterval(() => void tick(), 15_000); return () => { clearInterval(timer); controller.abort(); }; } ``` Call the stop function before you [close the session](https://sdk.nimbusprivacy.xyz/reference/sessions#closenimbussession). A sync that is still running then stops at the end of its round. ## What a sync also does - **It frees stopped payments.** A payment whose tab closed before it sent anything holds part of the balance. A sync gives it back. See [Reserved funds](https://sdk.nimbusprivacy.xyz/helpers/reserved-funds). - **It serves every tab.** Each tab of your app on the same site reads the same device storage. A sync in one tab updates the balance for all of them. Two tabs syncing at once is safe, but wasted work. A step that keeps losing to the other tab returns `unavailable`, and the next sync finishes it. ## Next - [Balances](https://sdk.nimbusprivacy.xyz/helpers/balances): Read what the sync took in. - [Why the device syncs](https://sdk.nimbusprivacy.xyz/explainers/sync): Where the private balance lives. - [Reserved funds](https://sdk.nimbusprivacy.xyz/helpers/reserved-funds): Free what a stopped payment holds. - [API reference](https://sdk.nimbusprivacy.xyz/reference/reads#syncnotestore): syncNoteStore: its options and what it returns. Last updated 21 October 2026 # Tokens (/helpers/tokens) Every amount the SDK takes or returns is a `bigint` in the token's smallest unit. This page shows where a token's decimals come from, how to format and parse amounts, and how to give tokens names and logos. The running example is USDC, which has 6 decimals. 25 USDC is `25_000_000n`. ## The tokens a cluster lists `sdk.mints` lists the tokens the mainnet preset names, 57 of them. Each `NimbusMint` has: - **`mint`**, the token's mint address. - **`tokenProgram`**, the program that owns the mint: SPL Token or Token-2022. - **`decimals`**, how many decimal places its smallest unit has. ```ts for (const { mint, decimals, tokenProgram } of sdk.mints) { console.log(mint, decimals, tokenProgram); } ``` Offer only these tokens in your token picker. The SDK names a token in a balance, a held payment or a history item only when it is in this list. ## Format an amount Divide by the token's decimals to show an amount. This formatter keeps the full precision of a `bigint`, drops trailing zeros and groups thousands: ```ts function formatAmount(value: bigint, decimals: number): string { const negative = value < 0n; const digits = (negative ? -value : value) .toString() .padStart(decimals + 1, "0"); const whole = digits.slice(0, digits.length - decimals); const fraction = digits.slice(digits.length - decimals).replace(/0+$/u, ""); const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/gu, ","); const sign = negative ? "-" : ""; return fraction === "" ? `${sign}${grouped}` : `${sign}${grouped}.${fraction}`; } formatAmount(25_000_000n, 6); // "25" formatAmount(1_234_567_890n, 6); // "1,234.56789" ``` Avoid `Number` for amounts. A large balance in a token with 9 decimals loses precision as a `Number`. ## Parse what the user types Turn what Alice types into the smallest unit the same way, in reverse: ```ts function parseAmount(text: string, decimals: number): bigint | undefined { const match = /^(\d+)(?:\.(\d*))?$/u.exec(text.trim().replace(/,/gu, "")); if (match?.[1] === undefined) return undefined; const fraction = match[2] ?? ""; if (fraction.length > decimals) return undefined; return BigInt(match[1] + fraction.padEnd(decimals, "0")); } parseAmount("25", 6); // 25_000_000n parseAmount("12.5", 6); // 12_500_000n ``` It refuses more decimal places than the token has. Tell the user why, as the example app does. ## Names and logos The SDK carries no token names, symbols or logos. Take them from a token list you trust. The [example app](https://sdk.nimbusprivacy.xyz/resources/example-app) writes its list ahead of time with a script, `apps/nimbus-example/scripts/fetch-tokens.ts`, which it runs as `pnpm tokens`: ### Ask Jupiter for each token It sends every mainnet preset address to Jupiter's token search, and reads back each token's symbol, name and logo URL. It warns when Jupiter's decimals differ from the preset's. ### Save each logo on its own site It resizes each logo to a 96-pixel WebP through an image proxy. It saves the result under `public/tokens/`, and skips any logo over 64 KB. ### Write the list It writes `lib/token-list.json`, one entry per token with its address, symbol, name and logo path. The app commits this file and reads it at runtime. Serving logos from your own site matters. A cross-origin isolated page shows an image from another site only when that site allows it, and many logo hosts do not. For a token the list lacks, show its shortened address, and draw its initials in place of a logo. ## Unsupported tokens A deposit of an address that is not a token mint throws a `NimbusSolanaFlowError` with reason `mint-unsupported`. That covers an address with no account, an account another program owns, and data that is not a mint. A Token-2022 token whose transfer fee changes at the next epoch throws `fee-changing` instead. Try again once the epoch turns. ## Next - [Balances](https://sdk.nimbusprivacy.xyz/helpers/balances): Show each token's balance with its name and logo. - [History](https://sdk.nimbusprivacy.xyz/helpers/history): Label past payments and name their tokens. - [Deposit](https://sdk.nimbusprivacy.xyz/deposit): Deposit USDC into a private balance. - [API reference](https://sdk.nimbusprivacy.xyz/reference/tokens): findMintByIdentifier: its signature and what it returns. Last updated 21 October 2026 # Wallets (/helpers/wallets) The SDK finds wallets through the [Wallet Standard](https://github.com/wallet-standard/wallet-standard), which every major Solana browser wallet supports. You need no wallet adapter library. In this guide, Alice picks her wallet from a list, connects it, and gets the signer her session needs. ## List the wallets `listWallets` returns the installed wallets that can do everything the SDK asks. Each one must connect, sign messages and sign transactions on the SDK's cluster. Any other wallet is left out. ```ts import { listWallets } from "@umbra/nimbus-solana-web"; for (const wallet of listWallets(sdk)) { console.log(wallet.name, wallet.icon); } ``` Each entry is a Wallet Standard `Wallet`. Show its `name` and `icon` in your wallet picker. ## Follow wallets that load late A browser extension can register its wallet after your page loads. `followWallets` calls you with the new list each time a wallet registers or unregisters. It returns a function that stops following. In React, a hook keeps the list current: ```tsx import { followWallets, listWallets } from "@umbra/nimbus-solana-web"; import { useEffect, useState } from "react"; export function useWallets(sdk: NimbusSdk): readonly Wallet[] { const [wallets, setWallets] = useState(() => listWallets(sdk)); useEffect(() => followWallets(sdk, { onChange: setWallets }), [sdk]); return wallets; } ``` The stop function is the effect's cleanup. The hook stops following when the component unmounts. ## Connect one When Alice picks a wallet, call `openWalletConnection`. Her wallet shows its connection prompt, unless your page is already connected. ```ts import { openWalletConnection } from "@umbra/nimbus-solana-web"; const connection = await openWalletConnection(sdk, { wallet }); switch (connection.kind) { case "connected": { const { signer, transactionVersion } = connection.connected; console.log(signer.account.address, transactionVersion); break; } case "no-account": console.log(`${wallet.name} has no account on this network.`); break; case "declined": console.log("You declined to connect."); break; } ``` | `kind` | What happened | What to do | | ------------ | ----------------------------------------------------------------- | ----------------------------------------------------------------- | | `connected` | The wallet connected. `connected.signer` is what a session takes. | [Open a session](https://sdk.nimbusprivacy.xyz/helpers/sessions). | | `no-account` | The wallet connected but has no account on this cluster. | Ask Alice to add one, or pick another wallet. | | `declined` | Alice refused to connect. | Let her try again. | A wallet that fails while connecting throws a `NimbusSdkError` with reason `wallet-unsupported`. Ask Alice to pick another wallet. ## Version 0 and version 1 wallets `connected.transactionVersion` is the newest transaction version the wallet signs. It is `1` for most wallets, and `0` for older ones. It matters only for the payments Alice's own wallet signs: [registration](https://sdk.nimbusprivacy.xyz/registration), [deposit](https://sdk.nimbusprivacy.xyz/deposit) and a deposit refund. Pass it to each as the `transactionVersion` option. The default is `1`, which a version 0 wallet cannot sign. At version 0, a deposit takes two transactions, so Alice approves twice. [Wallets that sign only version 0](https://sdk.nimbusprivacy.xyz/deposit#wallets-that-sign-only-version-0) explains why. A [Private Send](https://sdk.nimbusprivacy.xyz/transfer) or a [withdrawal](https://sdk.nimbusprivacy.xyz/withdraw) is sent by a relayer. Alice's wallet never signs it, so those take no version. ## Bring your own signer A session takes any `WalletStandardSigner`. That is an account and two functions, shaped like the Wallet Standard's `solana:signMessage` and `solana:signTransaction`. Build one when your app already connects wallets through another library, or uses an embedded wallet. ```ts import type { WalletStandardSigner } from "@umbra/nimbus-solana-web"; const signer: WalletStandardSigner = { account: { address: accountAddress, publicKey: accountPublicKey, chains: ["solana:mainnet"], features: ["solana:signMessage", "solana:signTransaction"], }, signMessage: async (...inputs) => Promise.all( inputs.map(async ({ message }) => ({ signedMessage: message, signature: await signBytes(message), })), ), signTransaction: async (...inputs) => Promise.all( inputs.map(async ({ transaction }) => ({ signedTransaction: await signWireTransaction(transaction), })), ), }; ``` The SDK holds your signer to the same rules as a wallet: - **The account** lists the cluster's chain, `solana:mainnet`. Its address is the base58 form of its 32-byte public key. - **`signMessage`** signs the bytes exactly as given, and returns them as `signedMessage`. A signer that wraps or prefixes the message is refused as `wallet-unsupported`. - **`signTransaction`** returns the whole signed transaction. If it changes the transaction, the payment throws a `NimbusSolanaFlowError` with reason `transaction-altered`. Nothing is sent. - **When the user refuses**, reject with an error whose `code` is `4001`. A message that says rejected, declined, denied or cancelled works too. The SDK then answers `declined` instead of throwing. ## Next - [Sessions](https://sdk.nimbusprivacy.xyz/helpers/sessions): Open the user's private account with the signer. - [Registration](https://sdk.nimbusprivacy.xyz/registration): Register the wallet before its first deposit. - [Deposit](https://sdk.nimbusprivacy.xyz/deposit): Move tokens from the wallet into a private balance. - [API reference](https://sdk.nimbusprivacy.xyz/reference/wallets): listWallets, followWallets and openWalletConnection. Last updated 21 October 2026 # Nimbus SDK (/) Nimbus lets you add private balances and payments to a Solana web app. Users deposit from their wallet, pay other users and withdraw to a Solana address. The SDK builds proofs, stores and syncs private balances, and submits payments in the browser. Spending keys stay on the user's device. Your app connects the wallet and shows each payment's progress and result. New to Nimbus? Start with [private balances](https://sdk.nimbusprivacy.xyz/explainers/private-balance) and [Umbra 101](https://docs.nimbusprivacy.xyz/introduction/umbra-101). ## Choose your path - [Quickstart](https://sdk.nimbusprivacy.xyz/getting-started/quickstart): Follow Alice and Bob through setup, a deposit, Private Send and a withdrawal. - [API reference](https://sdk.nimbusprivacy.xyz/reference): Find the functions, types, options and results your integration needs. ## Use cases Private balances and payments are building blocks for financial products. Private Send settles between registered Nimbus users. - [Fintech & neobanks](https://sdk.nimbusprivacy.xyz/helpers/balances): Offer private balances and payments in consumer finance apps, without publishing individual account balances or the amounts customers send. - [Cross-border payments](https://sdk.nimbusprivacy.xyz/transfer): Build stablecoin remittance flows that keep senders, recipients and payment amounts off the public transaction record. - [Payroll & payouts](https://sdk.nimbusprivacy.xyz/transfer#options): Pay employees and contractors individually with Private Send, keeping recipients and payment amounts out of the public transaction record. - [Merchant payments](https://sdk.nimbusprivacy.xyz/transfer#follow-it-to-the-end): Accept customer payments without publishing the buyer, merchant or payment amount on the public transaction record. - [B2B payments](https://sdk.nimbusprivacy.xyz/transfer): Settle supplier invoices and partner payments while keeping counterparties and individual payment amounts private on the public chain. - [Corporate treasury](https://sdk.nimbusprivacy.xyz/helpers/balances): Manage private operating balances and move funds between registered accounts without publishing each account's holdings or internal payment amounts. Your app provides business workflows and any fiat or payment-service integrations. Add [payment history](https://sdk.nimbusprivacy.xyz/helpers/history) and [progress feedback](https://sdk.nimbusprivacy.xyz/api-design/progress) as you build out the experience. ## How it works Deposit into a private balance, then send to a registered user or withdraw. Deposits and withdrawals have public records. Private Sends and withdrawals use a relayer. Spending keys stay on the device; the pool program controls deposited tokens. - [Alice’s wallet](https://sdk.nimbusprivacy.xyz/deposit) - [Alice’s private balance](https://sdk.nimbusprivacy.xyz/explainers/private-balance) - [Bob’s private balance](https://sdk.nimbusprivacy.xyz/transfer) - [Solana address](https://sdk.nimbusprivacy.xyz/withdraw) **Open an account.** Signing the wallet's unlock message opens the private account on this device. It moves no money and costs nothing. **Deposit.** Deposits increase the private balance by the deposited amount, less the protocol fee. **Pay or withdraw.** A relayer submits Private Sends and withdrawals. The connected wallet does not sign those transactions or pay their Solana network fee. **Keep control.** The pool program controls deposited tokens; spending a private balance requires its owner's keys. ## What your app needs - **A web app.** Next.js, Vite or any bundler works. The SDK runs in the browser, not on a server. - **Two response headers.** The proofs use shared memory, which browsers allow only on [cross-origin isolated](https://sdk.nimbusprivacy.xyz/getting-started/setup#turn-on-cross-origin-isolation) pages. - **The SDK's files.** It ships WebAssembly and a worker script that your app [serves from its own site](https://sdk.nimbusprivacy.xyz/getting-started/setup#serve-the-sdks-files). - **A gateway token for mainnet.** It lets your app use Nimbus's services. To get one, email [dev@umbraprivacy.com](mailto:dev@umbraprivacy.com) or reach out to any Umbra team member. [The mainnet gateway token](https://sdk.nimbusprivacy.xyz/getting-started/setup#the-mainnet-gateway-token) shows how to pass it. Check [browser support](https://sdk.nimbusprivacy.xyz/production/browser-support) before choosing the browsers your app supports. The [quickstart](https://sdk.nimbusprivacy.xyz/getting-started/quickstart) uses mainnet and moves real USDC. Its code examples are checked against the SDK when the site builds. ## Before you launch Deposits publish the funding wallet, token and amount, but not whose private balance receives them. Withdrawals publish the destination, token and amount, but not which private balance paid. Private Send does not publish the sender, recipient, payment token or amount. The relayer transaction and any paid tip remain public. A tip paid in the payment token makes that token visible. Matching amounts and nearby transaction times can still narrow privacy. Review [privacy boundaries](https://docs.nimbusprivacy.xyz/privacy/visibility), [privacy risks](https://docs.nimbusprivacy.xyz/privacy/leaks) and the [production checklist](https://sdk.nimbusprivacy.xyz/production/checklist). Use [troubleshooting](https://sdk.nimbusprivacy.xyz/production/troubleshooting) when an integration fails, and check the [changelog](https://sdk.nimbusprivacy.xyz/resources/changelog) before upgrading. Last updated 21 October 2026 # Browser support (/production/browser-support) **Only Chrome is tested.** The SDK's browser tests run in headless Chrome, on a cross-origin isolated page. No other browser runs them, so no other browser is known to work. If your app supports another browser, test it there yourself. Phones are not tested either. Proving uses every core and a lot of memory. A phone may also pause a tab in the background, which stops a proof that is running. ## What a browser must offer `loadNimbusSdk` checks for most of these before it loads. If one is missing, it throws a `NimbusSdkError` with reason `unsupported-browser` that names each one. | Feature | What the SDK uses it for | Checked when it loads | | ---------------------------------- | ----------------------------------------------------------------------------- | --------------------- | | Cross-origin isolation | Shared memory, so the prover runs on every core | Yes | | `WebAssembly` | The SDK's cryptography and the prover | Yes | | `fetch` | Every request to Solana, the services and your site | Yes | | `crypto.getRandomValues` | Fresh randomness for each payment | Yes | | `crypto.subtle` | Checking the SHA-256 of the SDK's files | Yes | | `indexedDB` and `IDBKeyRange` | The private balance on the device, and the cached circuit files | Yes | | `navigator.locks` | Stopping two tabs registering at once, and marking which payments are running | Yes | | `navigator.storage` | Asking the browser to keep the private balance, and reading its quota | Yes | | Module Web Workers | Running proofs off the page's main thread | No | | `sessionStorage` or `localStorage` | Remembering the sign-in, if you keep it for the tab or the browser | No | | `navigator.locks.query` | Telling a stopped payment from a running one | No | ## When a feature is missing Without module workers, the SDK loads, but the first proof fails. Without Web Storage, opening a session throws `unsupported-browser`. This applies only to the tab and browser stores. The memory store needs no storage. Without `navigator.locks.query`, every payment still runs. A sync then cannot tell which stopped payments to release, so their funds stay reserved until you release them. Some private windows refuse IndexedDB or Web Storage. The SDK then throws `unsupported-browser`, and its message names what the browser refused. ## Next - [Production checklist](https://sdk.nimbusprivacy.xyz/production/checklist): What to check before real users arrive. - [Troubleshooting](https://sdk.nimbusprivacy.xyz/production/troubleshooting): Symptoms, causes and fixes. Last updated 21 October 2026 # Production checklist (/production/checklist) Work through these before you ship. Each one says what to set up and how to check it in the deployed app. ## Every page is cross-origin isolated Both headers are on every route that loads the SDK, in production as well as in development. [Turn on cross-origin isolation](https://sdk.nimbusprivacy.xyz/getting-started/setup#turn-on-cross-origin-isolation) has the config for each host. **How to check.** Open the deployed app and run `crossOriginIsolated` in the console. It must print `true`. A CDN or proxy in front of your app can drop the headers, so check the live site. ## The SDK's files are pinned and current Your install runs `nimbus-solana-web copy-assets`, so the served files match the installed SDK. Your app refuses to run a build that does not check its files: ```ts if (sdk.settings.assetPinning !== "pinned") { throw new Error("Refusing to run unchecked WebAssembly."); } ``` **How to check.** `sdk.settings.assetPinning` reads `"pinned"` in the deployed app. A payment then runs without a `dependency-nonconforming` error. ## The Content Security Policy allows the SDK A strict policy is your best defence for the user's signature, which the default store keeps readable by any script on your site. On top of your own rules, the SDK needs these: ```text script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self' ; ``` - **`'wasm-unsafe-eval'`** lets the page and the workers compile WebAssembly. It does not allow JavaScript `eval`. - **`worker-src 'self'`** lets the SDK start its workers. The prover worker and the threads it starts are scripts on your own site, so `blob:` is not needed. - **`connect-src`** lists every origin the SDK fetches from. Build it from the loaded SDK, so it follows the preset and your own endpoints: ```ts const { endpoints, provingKeys } = sdk.settings; const urls = [ endpoints.rpc, endpoints.indexer, endpoints.context, endpoints.feeEnclave, endpoints.relayer, endpoints.coordinator, ...endpoints.guardians.map((node) => node.endpoint), ...endpoints.screeners.map((node) => node.endpoint), ...endpoints.dataEndorsers.map((node) => node.endpoint), provingKeys.baseUrl, ].filter((url) => url !== ""); const origins = new Set(urls.map((url) => new URL(url, location.href).origin)); console.log([...origins].join(" ")); ``` Send the policy with the SDK's files too. A worker follows the policy its own script was served with, not the page's. **How to check.** Run a deposit and a withdrawal with the console open. No policy violation is reported. ## The files are cached sensibly The SDK's files keep the same names from one version to the next. Serve them with a short cache lifetime. Or copy them into a folder named for the version, such as `/nimbus-0.1.0/`, and cache that for as long as you like. The SDK caches the larger circuit files itself, in IndexedDB. They need nothing from you. **How to check.** Upgrade the SDK, deploy, and load the app in a browser that visited before. It loads without a `dependency-nonconforming` error. ## The gateway token stays out of sight Your app needs a gateway token for mainnet. To get one, email [dev@umbraprivacy.com](mailto:dev@umbraprivacy.com) or reach out to any Umbra team member. The SDK never logs the token or puts it in an error. Keep your own code the same: - Keep the token out of your repository. Load it from your configuration at build or run time. [The mainnet gateway token](https://sdk.nimbusprivacy.xyz/getting-started/setup#the-mainnet-gateway-token) shows how to pass it. - Never log request headers, or the options you pass to `openNimbusSession`. **How to check.** Search your logs and your error reports for the token. It appears nowhere. ## You chose where the sign-in is kept The signature that unlocks a private account can be kept for the tab, for the browser, or not at all. The first two are readable by any script on your site. Pick one on purpose, and tell your users. [Sessions](https://sdk.nimbusprivacy.xyz/helpers/sessions) compares them. **How to check.** Find the `signatureSecretStore` you pass to `openNimbusSession`. Leaving it out means the tab store. ## Errors reach your reports Report what helps you fix a problem, and nothing secret: - For a `NimbusSdkError`, report its `reason` and `message`. The message never holds the token. - For a protocol refusal, `isNimbusSolanaFlowError` tells you it is one. Report its `reason`. - For a result that is not a success, report its `kind`. - Never report the session object. It holds the user's keys. **How to check.** Load the app on a page without the isolation headers. Your reports show `unsupported-browser` and the message. ## Balances update while the app is open Payments sync the device on their own. Payments others make to your user arrive only with a sync. Run one on an interval while a session is open, as [Use it with React](https://sdk.nimbusprivacy.xyz/getting-started/react#sync-in-the-background) shows. **How to check.** Leave the app open, and pay its user from another account. The balance updates without a reload. ## Users can release stuck funds A payment that stops halfway, because its tab closed or a step failed, keeps its funds reserved. A sync frees one that never sent anything. Give users a button that calls `releaseReservedNotes` for the rest. [Reserved funds](https://sdk.nimbusprivacy.xyz/helpers/reserved-funds) shows the list and the button. **How to check.** While `listReservedSpends` lists a payment, press the button. Its amount moves from `reserved` back to `available` once nothing of it can still land. ## You tested the browsers you support The SDK's own tests run in headless Chrome only. Test your app in every browser you support, including on phones if you support them. [Browser support](https://sdk.nimbusprivacy.xyz/production/browser-support) lists what each browser must offer. **How to check.** Run a deposit, a Private Send and a withdrawal in each browser. ## Next - [Endpoints](https://sdk.nimbusprivacy.xyz/production/endpoints): The mainnet endpoints, your own RPC and your own services. - [Troubleshooting](https://sdk.nimbusprivacy.xyz/production/troubleshooting): Symptoms, causes and fixes. Last updated 21 October 2026 # Endpoints (/production/endpoints) `loadNimbusSdk` starts from the mainnet preset. The preset holds the programs, the supported tokens, every service's endpoint and the proving keys. So your app passes only where it serves the SDK's files. Any endpoint you pass replaces the preset's, one at a time. `sdk.settings.endpoints` shows every endpoint after your options are applied. ## The mainnet endpoints The mainnet preset supports 57 tokens. These are its endpoints, read from the SDK when this site was built: | Option | mainnet preset | Gateway | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `endpoints.rpc` | [https://api.mainnet-beta.solana.com](https://api.mainnet-beta.solana.com) | No | | `endpoints.indexer` | [https://data.api-v2.umbraprivacy.com](https://data.api-v2.umbraprivacy.com) | No | | `endpoints.context` | [https://context.api-v2.umbraprivacy.com](https://context.api-v2.umbraprivacy.com) | No | | `endpoints.guardians` | 1: [https://gateway.api-v2.umbraprivacy.com/guardian-share-node-relay-1](https://gateway.api-v2.umbraprivacy.com/guardian-share-node-relay-1), 2: [https://gateway.api-v2.umbraprivacy.com/guardian-share-node-relay-2](https://gateway.api-v2.umbraprivacy.com/guardian-share-node-relay-2), 3: [https://gateway.api-v2.umbraprivacy.com/guardian-share-node-relay-3](https://gateway.api-v2.umbraprivacy.com/guardian-share-node-relay-3) | Yes | | `endpoints.screeners` | 0: [https://gateway.api-v2.umbraprivacy.com/screener](https://gateway.api-v2.umbraprivacy.com/screener) | Yes | | `endpoints.dataEndorsers` | 0: [https://gateway.api-v2.umbraprivacy.com/data-endorser](https://gateway.api-v2.umbraprivacy.com/data-endorser) | Yes | | `endpoints.feeEnclave` | [https://gateway.api-v2.umbraprivacy.com/fee-enclave](https://gateway.api-v2.umbraprivacy.com/fee-enclave) | Yes | | `endpoints.relayer` | [https://gateway.api-v2.umbraprivacy.com/relayer](https://gateway.api-v2.umbraprivacy.com/relayer) | Yes | | `endpoints.coordinator` | [https://gateway.api-v2.umbraprivacy.com/process-coordinator](https://gateway.api-v2.umbraprivacy.com/process-coordinator) | Yes | | `endpoints.primitivesWasm` | Your app supplies it | No | | `provingKeys` | [https://assets.api-v2.umbraprivacy.com/zk/](https://assets.api-v2.umbraprivacy.com/zk/) | No | "Gateway" marks the services that need the [gateway token](https://sdk.nimbusprivacy.xyz/getting-started/setup#the-mainnet-gateway-token). ## Use your own RPC The preset's RPC is https\://api.mainnet-beta.solana.com, Solana's public endpoint. It limits how often one client may call it, and syncing calls it often. Pass your provider's endpoint instead: ```ts import { loadNimbusSdk } from "@umbra/nimbus-solana-web"; const sdk = await loadNimbusSdk( { cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/", endpoints: { rpc: "https://mainnet.your-rpc-provider.example" }, }, ); ``` ## Use your own services The same `endpoints` option replaces any service, such as a relayer you run yourself: ```ts import { loadNimbusSdk } from "@umbra/nimbus-solana-web"; const sdk = await loadNimbusSdk( { cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/", endpoints: { relayer: "https://relayer.your-app.example" }, }, ); ``` The guardians, screeners and data endorsers are lists of nodes, each with its `nodeIndex`. Guardian nodes are numbered from 1. Screener and data endorser nodes are numbered from 0. A service the gateway fronts still receives the gateway token at its new address. The one exception is an address that shares its origin with a service reached directly, such as the RPC. That service gets no token, and its requests fail `unauthorized`. ## Route through the coordinator By default, the browser asks each service itself. With `route: "coordinator"`, one service makes those requests for it, beside the other services. The requests a payment makes at one step go to it together, in one exchange. ```ts import { loadNimbusSdk } from "@umbra/nimbus-solana-web"; const sdk = await loadNimbusSdk( { cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/", route: "coordinator" }, ); ``` A busy service that turns a request away is asked again, up to three times in all. Registration, the relayer's policy, following a payment, the RPC, the indexer and the prover stay direct on both routes. The mainnet preset configures the coordinator. ## Next - [Install and set up](https://sdk.nimbusprivacy.xyz/getting-started/setup): The files, headers and token your app needs. - [Load the SDK](https://sdk.nimbusprivacy.xyz/helpers/load): Load it, and handle what it checks. Last updated 21 October 2026 # Troubleshooting (/production/troubleshooting) Find the symptom, then read its section below. | Symptom | Likely cause | Fix | | ------------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `unsupported-browser` saying the page is not cross-origin isolated | The page lacks the two headers | [Send both headers](#not-cross-origin-isolated) | | `dependency-nonconforming` naming a file and two hashes | Files from another SDK version | [Copy the files again](#a-file-digest-mismatch) | | Progress stops at `prepared` | The first proof is downloading circuit files | [Wait, and keep the tab open](#stuck-at-prepared) | | `listWallets` returns nothing | The wallet registered late, or cannot sign on this cluster | [Follow the wallets](#a-wallet-is-not-listed) | | The wallet fails to sign a deposit | It signs only version 0 transactions | [Pass `transactionVersion`](#version-0-and-version-1) | | `misconfigured` from a call | An argument is out of range | [Fix the argument](#misconfigured-from-a-call) | | `recipient-unregistered` from a payment | The recipient has not registered | [Ask them to register](#the-recipient-has-not-registered) | | `fee-payer-unfunded` from a registration or deposit | The wallet holds too little SOL | [Add SOL to the wallet](#the-wallet-cannot-pay-the-fee) | | Every payment fails `unauthorized` | No gateway token, or the gateway refused it | [Pass a token](https://sdk.nimbusprivacy.xyz/getting-started/setup#the-mainnet-gateway-token). To get one, email [dev@umbraprivacy.com](mailto:dev@umbraprivacy.com) or reach out to any Umbra team member. | | A balance does not change | The device has not synced | [Sync, then read](#a-balance-is-not-updating) | ## Not cross-origin isolated `loadNimbusSdk` threw a `NimbusSdkError` with reason `unsupported-browser`. Its message says the page is not cross-origin isolated. **Cause.** The page was served without `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`. Without them, the browser will not share memory with the prover. **Fix.** Send both headers with the page itself, as [Turn on cross-origin isolation](https://sdk.nimbusprivacy.xyz/getting-started/setup#turn-on-cross-origin-isolation) shows. Then run `crossOriginIsolated` in the console. If it still prints `false`, look for a proxy or CDN that drops the headers. ## A file digest mismatch `loadNimbusSdk`, or the first payment, threw `dependency-nonconforming`. The message names a file, its SHA-256, and the SHA-256 this SDK expects. **Cause.** Your app serves a different build of that file than the installed SDK ships. Usually it is left over from an older version, or a cache serves an old copy. **Fix.** Run `nimbus-solana-web copy-assets` again, and run it after every install from now on. Clear any cache in front of the files. [Serve the SDK's files](https://sdk.nimbusprivacy.xyz/getting-started/setup#serve-the-sdks-files) shows the script. ## Stuck at prepared The progress log shows `prepared`, then nothing for a long time. **Cause.** After `prepared`, the payment is proved on the device. The first proof also downloads the circuit files, which can take a while on a slow connection. Later proofs read them from the browser's cache. A browser may also slow a tab that is in the background. **Fix.** Keep the tab open and in front, and tell your user to. If it never moves, open the developer tools. Look for a failed request to the proving key host, or an error from the worker. ## A wallet is not listed `listWallets` returns an empty list, or misses the wallet your user has. **Cause.** `listWallets` reads the wallets installed at the moment you call it. An extension that registers later is missing. A wallet is also left out when it cannot connect, sign messages and sign transactions on the SDK's cluster. **Fix.** Use `followWallets` to keep the list current, as [Use it with React](https://sdk.nimbusprivacy.xyz/getting-started/react#pick-a-wallet) shows. Check that the wallet supports the cluster you loaded, such as `solana:mainnet`. A wallet that does not implement the Wallet Standard cannot be listed. ## Version 0 and version 1 A registration or deposit fails when the wallet is asked to sign it. **Cause.** Payments send version 1 transactions by default. Some wallets sign only version 0. **Fix.** Pass the `transactionVersion` that `openWalletConnection` returned to every call the wallet signs. At version 0, a deposit takes two transactions. The first moves the funds to a second address the SDK derives for your user. The second deposits them from there. [Wallets that sign only version 0](https://sdk.nimbusprivacy.xyz/deposit#wallets-that-sign-only-version-0) covers what to do when only the first lands. ## Misconfigured from a call A call threw `misconfigured` before it fetched anything. The message names the argument. **Cause.** An argument or option is outside what the function takes. For example: - `readHistory`'s `limit` is outside 1 to 100. - `syncNoteStore`'s `pageRows` is outside 1 to 1000, or `maxRounds` is below 1. - A context's `poolIndex` or node index is outside 0 to 65535. - An address is not valid base58. - `resolvePendingDeposit` was given a deposit whose outcome was not `unknown`. - The gateway token is not a valid bearer token. **Fix.** Pass a value in range. Resolve a deposit only when its submission's `kind` is `unknown`. Any other outcome is settled already. ## The recipient has not registered A deposit to another user, or a Private Send, threw a `NimbusSdkError` with reason `recipient-unregistered`. The same reason's partner, `account-unregistered`, means the payer itself has not registered. **Cause.** The recipient's wallet has never registered, so there is no private balance to pay into. Nothing was signed or sent. **Fix.** Ask the recipient to open your app and register. Then try the payment again. Retrying before they register gives the same error. ## The wallet cannot pay the fee A registration, a deposit or a refund threw a `NimbusSolanaFlowError` with reason `fee-payer-unfunded`. **Cause.** The connected wallet pays for these transactions, and it holds too little SOL. A wallet that has never held SOL has no account on Solana at all. The SDK reads its balance before it builds any proof or asks Nimbus to approve. So it throws before the slow steps, and nothing is signed or sent. The amount counts the network fee and any priority fee. A deposit also counts the SOL it moves and the rent of a token account it opens. The wallet must keep Solana's rent-exempt minimum on top. **Fix.** Ask your user to add SOL to the wallet, then try again. The error's `detail.requiredLamports` and `detail.heldLamports` say what it needs and what it holds. A Private Send or a withdrawal never throws this, because the relayer pays for them. ## A balance is not updating The balance does not show a deposit, or a payment someone made to your user. **Cause.** The balance is read from the device, and only a sync brings it up to date. A new deposit also reads as `pending` until Solana finalizes it. **Fix.** Call `syncNoteStore` before `readBalances`. To show payments as they arrive, sync on an interval, as [Use it with React](https://sdk.nimbusprivacy.xyz/getting-started/react#sync-in-the-background) shows. A deposit whose result was `unknown` may not have landed yet. [If the deposit is still pending](https://sdk.nimbusprivacy.xyz/deposit#if-the-deposit-is-still-pending) shows how to settle it. ## Next - [Handle errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors): Cover every result and error in one place. - [Browser support](https://sdk.nimbusprivacy.xyz/production/browser-support): What each browser must offer. Last updated 21 October 2026 # Deposit (/reference/deposit) A deposit runs in a context made from the session. [Deposit](https://sdk.nimbusprivacy.xyz/deposit) walks through the flow, and [Pending and stranded deposits](https://sdk.nimbusprivacy.xyz/deposit/pending) covers what can be left over. ## createDepositContext Create one from the session before you deposit. `resolvePendingDeposit` and `refundStrandedDeposit` take the same kind of context. ```ts function createDepositContext( session: NimbusSession, options?: NimbusContextOptions, ): Promise; ``` Creates the context a session deposits, refunds a stranded deposit and resolves a pending deposit in. Without `options.poolIndex` the pools' accounts are read from the RPC once, and the highest open pool is chosen; the nodes and guardian cluster default to the cluster's preset. **Parameters** | Parameter | Type | Description | | ---------- | ---------------------- | ------------------------------------------------------------------------- | | `session` | `NimbusSession` | The opened session. | | `options?` | `NimbusContextOptions` | The pool, the screener and data endorser nodes, and the guardian cluster. | **Returns** `Promise` The context. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"misconfigured"` when an option is out of range or no pool is open; `"service-unavailable"` or `"unreadable"` when the RPC cannot be read. **Example** ```ts import { createDepositContext } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; declare const session: NimbusSession; const context = await createDepositContext(session, { screenerNodeIndex: 1 }); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/contexts.ts`. ## deposit Use it to move tokens from the connected wallet into a private balance, the user's own or another registered user's. ```ts function deposit( context: DepositContext, args: { readonly mint: Address; readonly amount: bigint; readonly recipient: Address; }, options?: DepositOptions, ): Promise; ``` Deposits `amount` of `mint` from the connected wallet into the context's pool, as a note for `recipient`, a registered identity's wallet address: the session's own, or another's. First syncs the note store until it is caught up, unless `sync: false`. The context API resolves the recipient to its identity and the key the note is sealed to. The committee endorses, the deposit is proved, the wallet signs once (twice at version 0) and the transaction is sent and followed to `commitment`. **Parameters** | Parameter | Type | Description | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `context` | `DepositContext` | The deposit context. | | `args` | ``{ /** The mint deposited, which the connected wallet holds. */ readonly mint: Address; /** * What leaves the connected wallet, in base units of the mint, before any transfer fee the mint * withholds; at least 1 and below 2^64. Under `fees: "deducted"` the protocol fee comes out of * it; under `added` the note holds it and the fee is paid from the session's notes. */ readonly amount: bigint; /** * A registered identity's wallet address, whose identity the note is sealed to: the session's * own, or another's. */ readonly recipient: Address; }`` | The mint, the amount in its base units, and the recipient's wallet address. | | `options?` | `DepositOptions` | The progress hook, which also sees `synced`; whether to sync first; who pays the fee, the submitter's fee and its mint, the zone, the priority fee, the transaction version and the commitment. | **Returns** `Promise` `submitted` with how the deposit was funded and where its transaction ended up, which [`resolvePendingDeposit`](https://sdk.nimbusprivacy.xyz/reference/deposit#resolvependingdeposit) follows when that is `unknown`; `declined` when the wallet declined to sign; `store-refused`, `intent-refused` or `attempt-refused` when the note store would not take the deposit; and, at version 0 only, `funding-failed`, `funding-unknown` or `deposit-unsendable` when the funding transaction did not land or the deposit could not follow it. Nothing was deposited unless a `submitted` result's transaction `landed`. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"misconfigured"` when an address is not base58 or no prover worker or committee endpoint was configured; `"service-unavailable"` or `"unreadable"` when a service or the RPC cannot answer, and `"unreadable"` too when the note store cannot be synced before it starts; `"unauthorized"` when a gateway service refuses the session's gateway token or the session has none; `"dependency-nonconforming"` when a proving key or the prover breaks its contract; `"wallet-unsupported"` when the wallet fails; `"recipient-unregistered"` when the context API answers that `recipient` has no registration in the pool, and `"account-unregistered"` when it answers that the session's identity has none, each before the wallet is asked to sign; [`NimbusSolanaFlowError`](https://sdk.nimbusprivacy.xyz/reference/types#nimbussolanaflowerror) when the protocol refuses the deposit, with reason `"fee-payer-unfunded"` when the connected wallet has no account or too few lamports for the transaction it pays for (at version 0, the funding): its fee, the SOL it moves, the rent of the token account it creates and its own rent-exempt minimum, raised before any proving key is downloaded or the committee is asked. **Example** ```ts import { createDepositContext, deposit } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; import type { Address } from "@solana/kit"; declare const session: NimbusSession; declare const mint: Address; const result = await deposit(await createDepositContext(session), { amount: 1_000_000n, mint, recipient: session.walletAddress, }); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## resolvePendingDeposit Use it when a deposit's submission came back `unknown`, to learn whether it landed. ```ts function resolvePendingDeposit( context: DepositContext, args: { readonly submission: DepositSubmission }, _options?: Readonly>, ): Promise; ``` Waits for a deposit whose outcome was `unknown` to land, fail or expire at the note store's finality, `confirmed`, and tells the note store, which releases a failed or expired deposit's notes. **Parameters** | Parameter | Type | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | `context` | `DepositContext` | The deposit context the deposit ran in. | | `args` | ``{ /** The `submitted` deposit result's `submission`, whose transaction's outcome was `unknown`. */ readonly submission: DepositSubmission; }`` | The deposit's submission, from a `submitted` deposit result whose transaction's outcome was `unknown`. | | `_options?` | `Readonly>` | Unused; kept so every function takes the same arguments. | **Returns** `Promise` `landed` with its slot; `failed` or `expired`, nothing deposited, with the store's answers to the failure and to abandoning the deposit's intent; `unknown` when the wait ended first, its notes still held; or `store-refused` when the note store would not open. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"misconfigured"` when the submission's outcome was not `unknown`, before anything is fetched: one that landed, failed, expired or was refused is settled already; `"service-unavailable"` or `"unreadable"` when the RPC or the store cannot answer. **Example** ```ts import { resolvePendingDeposit } from "@umbra/nimbus-solana-web"; import type { DepositContext, DepositResult } from "@umbra/nimbus-solana-web"; declare const context: DepositContext; declare const result: DepositResult; if (result.kind === "submitted" && result.submission.submission.kind === "unknown") { await resolvePendingDeposit(context, { submission: result.submission }); } ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## refundStrandedDeposit Use it to return funds a version 0 deposit left halfway, once every deposit of that token has settled. ```ts function refundStrandedDeposit( context: DepositContext, args: { readonly mint: Address }, options?: StrandedDepositRefundOptions, ): Promise; ``` Returns what the session's platform wallet holds of `mint` to the connected wallet: value a version 0 deposit's funding left there when the deposit did not land. Run it only once every deposit of the mint has settled: it returns what the platform wallet holds when it is prepared. The connected wallet signs and pays; the platform wallet signs as the owner of what moves. **Parameters** | Parameter | Type | Description | | ---------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `context` | `DepositContext` | The deposit context. | | `args` | `{ /** The mint whose stranded value is returned. */ readonly mint: Address; }` | The mint. | | `options?` | `StrandedDepositRefundOptions` | The progress hook, whether to close the emptied token account, the priority fee, the transaction version and the commitment. | **Returns** `Promise` `nothing-held` when the platform wallet holds nothing of the mint and no lamports; `declined` when the wallet declined to sign; or `submitted` with where the transaction ended up, which returned the value only when it `landed`. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"misconfigured"` when the mint is not base58; `"service-unavailable"` or `"unreadable"` when the RPC cannot answer; `"wallet-unsupported"` when the wallet fails; [`NimbusSolanaFlowError`](https://sdk.nimbusprivacy.xyz/reference/types#nimbussolanaflowerror) with reason `"mint-unsupported"` when the mint is not one, `"fee-payer-unfunded"` when the connected wallet has no account or too few lamports to pay the refund's fee, before the wallet is asked to sign, or `"transaction-unsendable"` or `"transaction-altered"` when the refund cannot be sent or the wallet alters it. **Example** ```ts import { createDepositContext, refundStrandedDeposit } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; import type { Address } from "@solana/kit"; declare const session: NimbusSession; declare const mint: Address; const result = await refundStrandedDeposit(await createDepositContext(session), { mint }); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. Last updated 21 October 2026 # Errors (/reference/errors) The SDK throws two errors. [Errors](https://sdk.nimbusprivacy.xyz/helpers/errors) shows a `catch` block that tells them apart, and [Results and errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors) lists every reason. ## NimbusSdkError The error the SDK throws when it cannot go on. Read its `reason` to decide what to tell the user. The error every function of this package throws when it cannot go on. Compare `reason`, not the message, and test for the error with [`isNimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#isnimbussdkerror), which holds across bundles where `instanceof` does not. ```ts class NimbusSdkError extends Error { readonly name = "NimbusSdkError"; readonly reason: NimbusSdkErrorReason; constructor( reason: NimbusSdkErrorReason, message: string, cause: | { readonly kind: "none" } | { readonly kind: "cause"; readonly cause: unknown }, ); } ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/errors.ts`. ## isNimbusSdkError Use it in a `catch` block to recognise a `NimbusSdkError` and read its `reason`. ```ts function isNimbusSdkError(thrown: unknown): thrown is NimbusSdkError; ``` Returns whether a thrown value is a [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror), from this bundle or another copy of the package. **Parameters** | Parameter | Type | Description | | --------- | --------- | ---------------------- | | `thrown` | `unknown` | Whatever a call threw. | **Returns** `thrown is NimbusSdkError` Whether it is named `NimbusSdkError` and carries one of its reasons. **Example** ```ts import { isNimbusSdkError, loadNimbusSdk } from "@umbra/nimbus-solana-web"; try { await loadNimbusSdk({ cluster: "mainnet" }); } catch (thrown) { if (isNimbusSdkError(thrown) && thrown.reason === "misconfigured") { console.error(thrown.message); } } ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/errors.ts`. ## isNimbusSolanaFlowError Use it in a `catch` block around a payment, to recognise the protocol's refusals. ```ts function isNimbusSolanaFlowError( value: unknown, ): value is NimbusSolanaFlowError; ``` Returns whether a value is a [`NimbusSolanaFlowError`](https://sdk.nimbusprivacy.xyz/reference/types#nimbussolanaflowerror), by its name and reason. **Parameters** | Parameter | Type | Description | | --------- | --------- | ---------------- | | `value` | `unknown` | Anything caught. | **Returns** `value is NimbusSolanaFlowError` Whether it is one. Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/context/errors.ts`. Last updated 21 October 2026 # API reference (/reference) This section documents everything `@umbra/nimbus-solana-web` exports. Each function has one entry, with its signature, an example, its parameters and what it returns. Each entry is generated from the SDK's declarations and TSDoc when this site is built, so it matches the package. The guides show how the functions fit together. This section is where you look one up. Every mention of an export elsewhere on the site links to its entry here. ## Functions | Export | What it does | | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | [`loadNimbusSdk`](https://sdk.nimbusprivacy.xyz/reference/load#loadnimbussdk) | Loads the SDK for a cluster. Every other call starts from what it returns. | | [`listWallets`](https://sdk.nimbusprivacy.xyz/reference/wallets#listwallets) | Lists the installed wallets that can sign on the cluster. | | [`followWallets`](https://sdk.nimbusprivacy.xyz/reference/wallets#followwallets) | Calls you with the new list each time a wallet registers or unregisters. | | [`openWalletConnection`](https://sdk.nimbusprivacy.xyz/reference/wallets#openwalletconnection) | Connects a wallet and returns the signer a session takes. | | [`openNimbusSession`](https://sdk.nimbusprivacy.xyz/reference/sessions#opennimbussession) | Opens the user's private account on this device. | | [`closeNimbusSession`](https://sdk.nimbusprivacy.xyz/reference/sessions#closenimbussession) | Clears a session's keys and gateway token from memory. | | [`createMemorySignatureSecretStore`](https://sdk.nimbusprivacy.xyz/reference/sessions#creatememorysignaturesecretstore) | Keeps the unlock signature in the page's memory only. | | [`createTabSignatureSecretStore`](https://sdk.nimbusprivacy.xyz/reference/sessions#createtabsignaturesecretstore) | Keeps the unlock signature for the tab. This is the default. | | [`createPersistentSignatureSecretStore`](https://sdk.nimbusprivacy.xyz/reference/sessions#createpersistentsignaturesecretstore) | Keeps the unlock signature in the browser until it is wiped. | | [`readRegistration`](https://sdk.nimbusprivacy.xyz/reference/registration#readregistration) | Reads whether the user is registered now. | | [`createRegistrationContext`](https://sdk.nimbusprivacy.xyz/reference/registration#createregistrationcontext) | Creates the context a registration runs in. | | [`register`](https://sdk.nimbusprivacy.xyz/reference/registration#register) | Registers the user, once, before their first deposit. | | [`createDepositContext`](https://sdk.nimbusprivacy.xyz/reference/deposit#createdepositcontext) | Creates the context a deposit runs in. | | [`deposit`](https://sdk.nimbusprivacy.xyz/reference/deposit#deposit) | Moves tokens from the wallet into a private balance. | | [`resolvePendingDeposit`](https://sdk.nimbusprivacy.xyz/reference/deposit#resolvependingdeposit) | Learns how a deposit whose outcome was unknown ended. | | [`refundStrandedDeposit`](https://sdk.nimbusprivacy.xyz/reference/deposit#refundstrandeddeposit) | Returns funds a version 0 deposit left halfway. | | [`createTransferContext`](https://sdk.nimbusprivacy.xyz/reference/transfer#createtransfercontext) | Creates the context a Private Send runs in. | | [`transfer`](https://sdk.nimbusprivacy.xyz/reference/transfer#transfer) | Pays another user from the private balance. | | [`createWithdrawalContext`](https://sdk.nimbusprivacy.xyz/reference/withdraw#createwithdrawalcontext) | Creates the context a withdrawal runs in. | | [`withdraw`](https://sdk.nimbusprivacy.xyz/reference/withdraw#withdraw) | Moves tokens out of the private balance to any address. | | [`readBalances`](https://sdk.nimbusprivacy.xyz/reference/reads#readbalances) | Reads the private balance from the device. | | [`readHistory`](https://sdk.nimbusprivacy.xyz/reference/reads#readhistory) | Reads past payments, one page at a time. | | [`syncNoteStore`](https://sdk.nimbusprivacy.xyz/reference/reads#syncnotestore) | Brings the device up to date with Solana. | | [`listReservedSpends`](https://sdk.nimbusprivacy.xyz/reference/reserved-funds#listreservedspends) | Lists the payments that hold part of the balance. | | [`releaseReservedNotes`](https://sdk.nimbusprivacy.xyz/reference/reserved-funds#releasereservednotes) | Gives back what stopped payments hold. | | [`findMintByIdentifier`](https://sdk.nimbusprivacy.xyz/reference/tokens#findmintbyidentifier) | Names the token of a history item. | | [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) | The error the SDK throws when it cannot go on. | | [`isNimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#isnimbussdkerror) | Recognises a `NimbusSdkError`. | | [`isNimbusSolanaFlowError`](https://sdk.nimbusprivacy.xyz/reference/errors#isnimbussolanaflowerror) | Recognises the protocol's refusals. | | [`proveWorkerRequest`](https://sdk.nimbusprivacy.xyz/reference/prover-worker#proveworkerrequest) | Answers a proof request inside a prover worker of your own. | | [`copy-assets`](https://sdk.nimbusprivacy.xyz/reference/prover-worker#copy-assets) | The command that copies the SDK's browser files into your app. | ## Types [Types](https://sdk.nimbusprivacy.xyz/reference/types) lists every exported type in alphabetical order. Here they are grouped by what they describe. | Group | Types | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Loading and settings | `LoadNimbusSdkOptions`, `NimbusSdk`, `NimbusCluster`, `NimbusRoute`, `NimbusRoutingDefaults`, `NimbusSettings`, `NimbusEndpoints`, `NimbusGatewayEndpoint`, `NimbusNodeEndpoint`, `NimbusGatewayToken`, `NimbusMint`, `NimbusPlatform`, `NimbusBrowserPlatform`, `NimbusProving`, `NimbusProvingKeys`, `NimbusProvingKeyStatement`, `NimbusTiming` | | Wallets | `ConnectedWallet`, `WalletConnectionResult`, `WalletDiscoveryOptions`, `WalletStandardAccount`, `WalletStandardSigner` | | Sessions | `NimbusSession`, `OpenNimbusSessionOptions`, `OpenNimbusSessionResult`, `NimbusRegistrationStanding`, `SignatureSecretKey`, `SignatureSecretReading`, `SignatureSecretStore`, `SignatureSecretStoreOptions` | | Contexts | `RegistrationContext`, `DepositContext`, `TransferContext`, `WithdrawalContext`, `RegistrationContextOptions`, `NimbusContextOptions`, `NimbusFlowRouting`, `NimbusZone` | | Options | `RegisterOptions`, `DepositOptions`, `TransferOptions`, `WithdrawOptions`, `SpendOptions`, `SentTransactionOptions`, `SyncFirstOptions`, `StrandedDepositRefundOptions`, `TransactionVersion`, `ConfirmationCommitment` | | Results | `RegistrationResult`, `DepositResult`, `TransferResult`, `WithdrawalResult`, `PendingDepositResolutionResult`, `StrandedDepositRefundResult`, `DepositSubmission`, `TransactionSubmission`, `SettledSpendOutcome`, `SpendSettlement` | | Progress | `RegisterStage`, `DepositStage`, `TransferStage`, `WithdrawStage`, `RefundStage`, `SyncedStage`, `SettledStage` | | Refusals | `SpendRefusal`, `WalletStoreRefusal`, `BeginAttemptRefusal`, `LeaseRefusal`, `MarkAttemptSentRefusal`, `MergeFailureCause` | | Errors | `NimbusSdkErrorReason`, `NimbusSolanaFlowError`, `NimbusSolanaFlowErrorReason` | | Balances, history and sync | `Balance`, `HistoryPage`, `HistoryPosition`, `NoteStoreSyncResult`, `NoteStoreSyncRound`, `SyncNoteStoreOptions` | | Reserved funds | `ReservedSpend`, `ReservedSpendState`, `ReleasedNotes` | | Prover worker | `ProverWorkerRequest`, `ProverWorkerAnswer`, `ProverWorkerDeps`, `WitnessGraphHandle` | The SDK also exports `Wallet`, the Wallet Standard's own type. ## Next - [The function template](https://sdk.nimbusprivacy.xyz/api-design/function-template): How every function is shaped. - [Results and errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors): Every result kind and error reason. - [Types](https://sdk.nimbusprivacy.xyz/reference/types): Every exported type, in alphabetical order. Last updated 21 October 2026 # Load (/reference/load) Your app loads the SDK once per page. [Load the SDK](https://sdk.nimbusprivacy.xyz/helpers/load) walks through the options and what it checks. ## loadNimbusSdk Call it once when your app starts, before you list wallets or open a session. ```ts function loadNimbusSdk( context: { readonly cluster: NimbusCluster }, options?: LoadNimbusSdkOptions, ): Promise; ``` Loads the SDK for a cluster: its bundled preset completed by the options, the browser's functions, the primitives' WebAssembly module and the circuit artifact cache. Nothing is fetched besides the primitives' module until a session is opened or a flow runs, and no wallet is asked for anything. The primitives' module is checked twice before the SDK is returned: its bytes against the SHA-256 this package pins for the file it ships, before it is instantiated, and the primitives it fills against `@umbra/nimbus-primitives/conformance`'s known answers. The shipped prover worker's script, and each WebAssembly module and script it loads, are checked against their pins in the same way before they run; `settings.assetPinning` says whether this copy of the package carries pins at all, which a packed copy always does. The preset carries the cluster's programs, mints, lookup table, genesis hash and routing defaults. Mainnet's also carries every service endpoint and the proving keys, so a mainnet app passes only `assetsBaseUrl`. No preset hosts the browser files the SDK runs: this package ships them in its `assets/` directory, the primitives' module, the prover worker and the two WebAssembly modules it proves with, which the application copies into the directory it serves statically with `nimbus-solana-web copy-assets ` and names in `assetsBaseUrl`. Where a preset publishes no endpoint or proving keys, each must come from `options`. Every gateway service answers 401 without a gateway token, which `openNimbusSession` takes per session. The page must be cross-origin isolated, served with `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`: the prover's WebAssembly module declares shared memory, which a browser creates on no other page. **Parameters** | Parameter | Type | Description | | ---------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `context` | ``{ /** The cluster whose bundled preset to load, such as `mainnet`. */ readonly cluster: NimbusCluster; }`` | The cluster to load. | | `options?` | `LoadNimbusSdkOptions` | Where the assets are served, endpoints, proving keys, route and browser functions; each falls back to the preset, the served assets or the browser's global. | **Returns** `Promise` The loaded SDK. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"misconfigured"` naming every endpoint, and the proving keys, that neither the preset nor the options configure, when `cluster` has no preset, or when `assetsBaseUrl` does not end with `/` or is not a URL; `"unsupported-browser"` naming every global the browser lacks (`fetch`, `crypto.getRandomValues`, `crypto.subtle`, `indexedDB`, `IDBKeyRange`, `navigator.locks`, `navigator.storage`, `WebAssembly`), when the page is not cross-origin isolated, or when the browser refuses to open the artifact cache; `"service-unavailable"` when the primitives' module cannot be fetched; `"dependency-nonconforming"` naming the file and both digests when the primitives' module is not the one this package pins, or naming the primitive and its check when one fails its known answer; `"unreadable"` when what that URL serves is not the module. **Example** ```ts import { loadNimbusSdk } from "@umbra/nimbus-solana-web"; // After `nimbus-solana-web copy-assets public/nimbus`, on a page served with // Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp: const mainnet = await loadNimbusSdk( { cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/" }, ); // The same preset, with the app's own RPC in place of the preset's: const ownRpc = await loadNimbusSdk( { cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/", endpoints: { rpc: "https://rpc.example" } }, ); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/load.ts`. Last updated 21 October 2026 # Prover worker and CLI (/reference/prover-worker) Most apps copy the SDK's files with `copy-assets` and never write a worker. [Prover worker and CLI](https://sdk.nimbusprivacy.xyz/helpers/prover-worker) explains both ways. ## proveWorkerRequest Use it inside your own prover worker, to answer each message the SDK sends it. It comes from the package's `./prover-worker` entry. ```ts function proveWorkerRequest( request: ProverWorkerRequest, deps: ProverWorkerDeps, ): Promise; ``` Computes a request's witness and proves it, inside the prover worker. The graph is freed and the witness zeroed before the answer, on every path. **Parameters** | Parameter | Type | Description | | --------- | --------------------- | ---------------------------------------------------------------------- | | `request` | `ProverWorkerRequest` | The circuit, its checked proving key and witness graph, and the input. | | `deps` | `ProverWorkerDeps` | The initialised witness-graph and prover WASM modules. | **Returns** `Promise` `proved` with the proof and public signals, or `failed` with the prover's reason and message: `backend` when the graph cannot be parsed or refuses the inputs, including inputs that do not satisfy the circuit. It never rejects, so the worker always answers. **Example** ```ts import { proveWorkerRequest } from "@umbra/nimbus-solana-web/prover-worker"; import type { ProverWorkerDeps, ProverWorkerRequest } from "@umbra/nimbus-solana-web/prover-worker"; declare const request: ProverWorkerRequest; declare const deps: ProverWorkerDeps; const answer = await proveWorkerRequest(request, deps); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/prover-worker.ts`. ## copy-assets The package installs a command, `nimbus-solana-web`, with this one subcommand. It copies the browser files the SDK runs into a directory your app serves. ```bash nimbus-solana-web copy-assets ``` It takes exactly one argument, the directory, and no options. It creates the directory when it does not exist. It overwrites files of the same names and touches no other file. | Exit code | What it means | | --------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `0` | It copied the files, and printed how many. | | `1` | The copy failed. The directory could not be written, or this copy of the package was built without its files. The message says which. | | `2` | The arguments were wrong: no subcommand, an unknown one, or other than one directory. It prints its usage. | Last updated 21 October 2026 # Balances, history and sync (/reference/reads) The private balance and its history live on the user's device. A sync brings them up to date, and the reads return what it left. [Balances](https://sdk.nimbusprivacy.xyz/helpers/balances), [History](https://sdk.nimbusprivacy.xyz/helpers/history) and [Sync](https://sdk.nimbusprivacy.xyz/helpers/sync) walk through each. ## readBalances Use it whenever you show the private balance. It is cheap enough to call after every sync. ```ts function readBalances( session: NimbusSession, _args?: Readonly>, options?: { readonly zone?: number }, ): Promise; ``` Reads the session's balances in one zone from its note store: one entry per mint and state, in the order of the mints' identifiers. Reads only what [`syncNoteStore`](https://sdk.nimbusprivacy.xyz/reference/reads#syncnotestore) has synced; nothing is fetched. A session whose store holds nothing yet reads as no balances. **Parameters** | Parameter | Type | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | | `session` | `NimbusSession` | The opened session. | | `_args?` | `Readonly>` | Unused; kept so every function takes the same arguments. | | `options?` | `{ /** The zone, below 2^20; 0 when omitted. A zone no note carries reads as no balances. */ readonly zone?: number; }` | The zone, below 2^20; 0 when omitted. | **Returns** `Promise` The balances. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"unreadable"` when the store was replaced while it was read or cannot be read back; `"unsupported-browser"` when the browser refuses it. **Example** ```ts import { readBalances } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; declare const session: NimbusSession; const balances = await readBalances(session, {}); const available = balances.filter((balance) => balance.state === "available"); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reads.ts`. ## readHistory Use it to show the user's past payments, one page at a time. ```ts function readHistory( session: NimbusSession, _args?: Readonly>, options?: { readonly limit?: number; readonly after?: HistoryPosition }, ): Promise; ``` Reads one page of the session's history from its note store, newest first: each transaction with the identity's notes it created and spent, and its record when it is the session's. Reads only what [`syncNoteStore`](https://sdk.nimbusprivacy.xyz/reference/reads#syncnotestore) has synced; nothing is fetched. A page's `coverage` names, per pool, the slot its history is complete through: nothing above it is shown as nothing having happened. A session whose store holds nothing yet reads as an empty page. **Parameters** | Parameter | Type | Description | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `session` | `NimbusSession` | The opened session. | | `_args?` | `Readonly>` | Unused; kept so every function takes the same arguments. | | `options?` | ``{ /** The most transactions the page holds, 1 to 100; 20 when omitted. */ readonly limit?: number; /** The previous page's `next.position`; from the newest when omitted. */ readonly after?: HistoryPosition; }`` | The page size, 1 to 100, 20 when omitted; and `after`, the position the previous page's `next` named, from the newest when omitted. | **Returns** `Promise` The page. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"misconfigured"` when `limit` is not a whole number from 1 to 100, before the store is opened; `"unreadable"` when the store was replaced while it was read or cannot be read back; `"unsupported-browser"` when the browser refuses it. **Example** ```ts import { readHistory } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; declare const session: NimbusSession; const first = await readHistory(session, {}, { limit: 10 }); if (first.next.kind === "more") { await readHistory(session, {}, { after: first.next.position, limit: 10 }); } ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reads.ts`. ## syncNoteStore Use it before you read the balance or history, and on a timer while your app is open. ```ts function syncNoteStore( session: NimbusSession, _args?: Readonly>, options?: SyncNoteStoreOptions, ): Promise; ``` Syncs the session's note store: the pools' accounts and counters at the confirmed slot, then each followed pool's notes, spends, records and registrations from the indexer, a page of each per round, until every stream is caught up, or `maxRounds` rounds when given. Notes sealed to the identity become its balance; spends retire its notes; records become its history; its registration row in a pool lets it spend from that pool. Every row is checked against its stream before it is taken. A store with no birthday, which an identity that has not registered through it has, is left untouched. Last, a spend whose tab stopped before sending anything, its lock no longer held by any tab of this origin, is released, so its notes are available again; a browser that cannot list its locks skips that step. **Parameters** | Parameter | Type | Description | | ---------- | --------------------------------- | -------------------------------------------------------------------------------- | | `session` | `NimbusSession` | The opened session. | | `_args?` | `Readonly>` | Unused; kept so every function takes the same arguments. | | `options?` | `SyncNoteStoreOptions` | The most rounds, a signal that stops it, the rows per page, and a progress hook. | **Returns** `Promise` `ingested`, `unregistered`, or `unavailable` with the step the store could not finish. `ingested` is `caughtUp` unless `maxRounds` ran out first. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"misconfigured"` when `maxRounds` is not a whole number of at least 1 or `pageRows` is not a whole number from 1 to 1000, before anything is fetched or read; `"service-unavailable"` when the indexer or the RPC cannot be reached; `"unreadable"` when either answers what cannot be read, or the store was replaced while it was read; `"unsupported-browser"` when the browser refuses the store. `signal`'s reason once it aborts, between rounds. **Example** ```ts import { syncNoteStore } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; declare const session: NimbusSession; const result = await syncNoteStore(session, {}, { onProgress: (round) => { console.log(round.round, round.caughtUp); }, }); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reads.ts`. Last updated 21 October 2026 # Registration (/reference/registration) A user registers once before their first deposit. [Register a user](https://sdk.nimbusprivacy.xyz/registration) walks through the flow. ## readRegistration Use it to learn whether the user is registered now. The session's own `registration` is fixed when the session opens. ```ts function readRegistration( session: NimbusSession, _args?: Readonly>, ): Promise; ``` Reads the session's registration standing again, from the context API: what `register` changed, which the session's own `registration`, fixed when it opened, does not show. Call it after [`register`](https://sdk.nimbusprivacy.xyz/reference/registration#register) lands to learn the identity is registered without reopening the session. The standing is for pool 0, as the session's is. **Parameters** | Parameter | Type | Description | | --------- | --------------------------------- | -------------------------------------------------------- | | `session` | `NimbusSession` | The opened session. | | `_args?` | `Readonly>` | Unused; kept so every function takes the same arguments. | **Returns** `Promise` `registered` or `unregistered`, with the pool the standing is for. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"service-unavailable"` when the context API cannot be reached, or `"unreadable"` when it answers what cannot be read. **Example** ```ts import { readRegistration } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; declare const session: NimbusSession; const standing = await readRegistration(session, {}); if (standing.kind === "registered") { console.log(`registered in pool ${String(standing.poolIndex)}`); } ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/session.ts`. ## createRegistrationContext Create one before you call `register`. It picks the pool the user registers in. ```ts function createRegistrationContext( session: NimbusSession, options?: RegistrationContextOptions, ): Promise; ``` Creates the context a session registers in. Without `options.poolIndex` the pools' accounts are read from the RPC once, and the highest open pool is chosen; the data endorser node and guardian cluster default to the cluster's preset. **Parameters** | Parameter | Type | Description | | ---------- | ---------------------------- | ---------------------------------------------------------- | | `session` | `NimbusSession` | The opened session. | | `options?` | `RegistrationContextOptions` | The pool, the data endorser node and the guardian cluster. | **Returns** `Promise` The context. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"misconfigured"` when an option is out of range or no pool is open; `"service-unavailable"` or `"unreadable"` when the RPC cannot be read. **Example** ```ts import { createRegistrationContext, register } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; declare const session: NimbusSession; const result = await register(await createRegistrationContext(session), {}); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/contexts.ts`. ## register Use it once per user, while `readRegistration` says `unregistered`. ```ts function register( context: RegistrationContext, _args?: Readonly>, options?: RegisterOptions, ): Promise; ``` Registers the session's identity in the context's pool and binds the connected wallet to its platform wallet, in one transaction the wallet signs, sending only what is still missing. Reads the identity's standing from the context API, asks the guardians and the data endorser to endorse it, proves it, has the wallet sign once and sends it. An identity already registered and bound is `complete` with nothing sent. **Parameters** | Parameter | Type | Description | | ---------- | --------------------------------- | -------------------------------------------------------------------------------- | | `context` | `RegistrationContext` | The registration context. | | `_args?` | `Readonly>` | Unused; kept so every function takes the same arguments. | | `options?` | `RegisterOptions` | The progress hook, the priority fee, the transaction version and the commitment. | **Returns** `Promise` `complete` when the identity was registered and bound already, with nothing sent; `submitted` with the instructions sent and where the transaction ended up, which registered only when its `submission.kind` is `landed`; `declined` when the wallet declined to sign; or `store-refused` when the note store would not open for the identity. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"service-unavailable"` or `"unreadable"` when a service or the RPC cannot answer, `"unauthorized"` when a gateway service refuses the session's gateway token or the session has none, `"dependency-nonconforming"` when a proving key or the prover breaks its contract, `"wallet-unsupported"` when the wallet fails, and `"misconfigured"` when no prover worker or committee endpoint was configured; [`NimbusSolanaFlowError`](https://sdk.nimbusprivacy.xyz/reference/types#nimbussolanaflowerror), which [`isNimbusSolanaFlowError`](https://sdk.nimbusprivacy.xyz/reference/errors#isnimbussolanaflowerror) recognises, when the protocol refuses the registration, with reason `"fee-payer-unfunded"` when the connected wallet has no account or too few lamports to pay the transaction's fee and keep its rent-exempt minimum, raised before any proving key is downloaded or the committee is asked. **Example** ```ts import { createRegistrationContext, register } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; declare const session: NimbusSession; const result = await register(await createRegistrationContext(session), {}, { onProgress: (event) => { console.log(event.stage); }, }); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. Last updated 21 October 2026 # Reserved funds (/reference/reserved-funds) A payment that stops halfway can keep part of the balance reserved. [Reserved funds](https://sdk.nimbusprivacy.xyz/helpers/reserved-funds) shows a list of those payments and a Release button. ## listReservedSpends Use it to show what part of the balance is held, and by which payments. ```ts function listReservedSpends( session: NimbusSession, _args?: Readonly>, ): Promise; ``` Lists the spends holding the session's notes, in id order: what `readBalances` counts as `reserved`. Reads only the session's note store; nothing is fetched. A spend that stopped without finishing, because its tab closed or a step failed, holds its notes until it is released, here or by the store once its holder's lock is gone. A deposit is listed, as `deposit`, only while it holds the notes it merges. **Parameters** | Parameter | Type | Description | | --------- | --------------------------------- | -------------------------------------------------------- | | `session` | `NimbusSession` | The opened session. | | `_args?` | `Readonly>` | Unused; kept so every function takes the same arguments. | **Returns** `Promise` The spends; none for a session whose store holds nothing yet. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"unreadable"` when the store was replaced while it was read or cannot be read back; `"unsupported-browser"` when the browser refuses it. **Example** ```ts import { listReservedSpends } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; declare const session: NimbusSession; const stuck = (await listReservedSpends(session, {})).filter((spend) => spend.state === "unsent"); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reserved.ts`. ## releaseReservedNotes Use it behind a Release button, for payments that are not running. ```ts function releaseReservedNotes( session: NimbusSession, _args?: Readonly>, options?: { readonly spendId?: string; readonly force?: boolean }, ): Promise; ``` Releases the spends holding the session's notes, or one of them: no attempt of a released spend begins or is sent again. An `unsent` spend's notes are free at once. A spend that may have been sent keeps its notes until that attempt lands, which completes the payment, or can no longer land, since spending them again meanwhile could race it. A deposit whose transaction may still land is released the same way; [`resolvePendingDeposit`](https://sdk.nimbusprivacy.xyz/reference/deposit#resolvependingdeposit) still settles it afterwards, and a `failed` or `expired` resolution then answers its intent as `settled`, since the release already stopped it. A spend a tab of this origin is still running, which holds its lock, is left alone and returned as `running` unless `force` is set: released, it would fail at its next step. A browser that cannot list its locks reports no spend as running. Releasing what is already released, or an id that names no reserved spend, changes nothing. **Parameters** | Parameter | Type | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `session` | `NimbusSession` | The opened session. | | `_args?` | `Readonly>` | Unused; kept so every function takes the same arguments. | | `options?` | ``{ /** One spend's id, from {@link listReservedSpends}; every reserved spend when omitted. */ readonly spendId?: string; /** Whether to release a spend a tab is still running too; `false` when omitted. */ readonly force?: boolean; }`` | `spendId`, one spend's id from [`listReservedSpends`](https://sdk.nimbusprivacy.xyz/reference/reserved-funds#listreservedspends), every reserved spend when omitted; and `force`, to release a spend a tab is still running too, false when omitted. | **Returns** `Promise` The spends released at once, those settling, each as it stands after the release, and those left running. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"service-unavailable"` when the RPC cannot be reached for the confirmed slot; `"unreadable"` when the store was replaced while it was written, cannot be read back, or kept losing to other writers; `"unsupported-browser"` when the browser refuses it. **Example** ```ts import { releaseReservedNotes } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; declare const session: NimbusSession; const { released, settling, running } = await releaseReservedNotes(session, {}); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reserved.ts`. Last updated 21 October 2026 # Sessions (/reference/sessions) A session is the user's private account, opened on this device. [Sessions](https://sdk.nimbusprivacy.xyz/helpers/sessions) walks through opening one, switching wallets and signing out. ## openNimbusSession Use it after the wallet connects, to unlock the user's private account. ```ts function openNimbusSession( sdk: NimbusSdk, args: { readonly connectedWallet: WalletStandardSigner }, options?: OpenNimbusSessionOptions, ): Promise; ``` Opens a session for the connected wallet on the SDK's cluster: reads the signature secret from the signature secret store or asks the wallet to sign the unlock message once, derives the seed, the platform wallet, the identity and its keys, reads its registration standing, and opens its note store in IndexedDB. The wallet signs only when the store holds no secret for this wallet, cluster and chain; what its signature yields is written to the store before anything else, so a reload opens without asking again. The same wallet always opens the same identity on the same cluster, on any device. A secret the store returns is trusted: a store an attacker can write to can open another identity under this wallet's name. The note store is named by `buildNoteStoreBackendName`, one database per deployment and identity. A registered identity's store keeps the birthday it has, or starts at slot 0 on a device that has none, so every note is read; an unregistered identity's store is left without one, for registering to set. Opening reaches only the context API, which takes no gateway token, so a session opens whether or not it has one; the token is first sent when a flow reaches a gateway service. **Parameters** | Parameter | Type | Description | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `sdk` | `NimbusSdk` | The loaded SDK. | | `args` | ``{ /** * The connected wallet's signer, as {@link openWalletConnection} builds it: its account must * offer `solana:signMessage` on the cluster's chain. */ readonly connectedWallet: WalletStandardSigner; }`` | The connected wallet. | | `options?` | `OpenNimbusSessionOptions` | The signature secret store, the platform wallet's account index and the gateway token. | **Returns** `Promise` `opened` with the session, or `declined` when the user refused to sign. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"wallet-unsupported"` when the wallet lacks `solana:signMessage` or the cluster's chain, its account's address is not its public key's, or it fails while signing, signs other bytes than the unlock message, or answers a signature that does not verify; `"misconfigured"` when `accountIndex` is not an integer in `[0, 2^31)` or `gatewayToken` is a string that is not an RFC 6750 bearer token; `"unsupported-browser"` when the browser refuses the signature secret store's storage or the note store's database; `"unreadable"` when the note store holds another deployment's rows or a newer schema, or the context API answers what cannot be read; `"service-unavailable"` when the context API cannot be reached or refuses. **Example** ```ts import { openNimbusSession } from "@umbra/nimbus-solana-web"; import type { NimbusSdk, WalletStandardSigner } from "@umbra/nimbus-solana-web"; declare const sdk: NimbusSdk; declare const connectedWallet: WalletStandardSigner; declare const fetchGatewayToken: (wallet: string) => Promise; const opened = await openNimbusSession( sdk, { connectedWallet }, { gatewayToken: async () => fetchGatewayToken(connectedWallet.account.address) }, ); if (opened.kind === "opened" && opened.session.registration.kind === "unregistered") { // register before the first deposit } ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/session.ts`. ## closeNimbusSession Use it when the user signs out, switches wallets, or the wallet disconnects. ```ts function closeNimbusSession(session: NimbusSession): void; ``` Closes a session: zeroes its wallet seed and the keys a sync holds, drops a held gateway token, and closes its note store. The signature secret store is left as it is, so the next open needs no signature; call its `wipeSignatureSecret` as well to sign the user out. Every later use of the session fails. Closing twice does nothing more. **Parameters** | Parameter | Type | Description | | --------- | --------------- | --------------------- | | `session` | `NimbusSession` | The session to close. | **Example** ```ts import { closeNimbusSession } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; declare const session: NimbusSession; closeNimbusSession(session); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/session.ts`. ## createMemorySignatureSecretStore Use it when no secret may touch the browser's storage, and a signature after every reload is acceptable. ```ts function createMemorySignatureSecretStore(): SignatureSecretStore; ``` Creates a signature secret store that keeps secrets in this page's memory only, so the wallet signs again after every reload. Nothing reaches Web Storage. A wiped secret's bytes are zeroed. **Returns** `SignatureSecretStore` The store's read, write and wipe functions. **Example** ```ts import { createMemorySignatureSecretStore } from "@umbra/nimbus-solana-web"; const signatureSecretStore = createMemorySignatureSecretStore(); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/signature-secret-store.ts`. ## createTabSignatureSecretStore Use it for the default: one signature per tab, kept until the tab closes. ```ts function createTabSignatureSecretStore( options?: SignatureSecretStoreOptions, ): SignatureSecretStore; ``` Creates the default signature secret store, over the browser's `sessionStorage`: the secret lasts as long as the tab, survives a reload, and is gone when the tab closes. The secret is kept base64-encoded and unencrypted; any script running on the page's origin can read it while the tab is open. The store touches `sessionStorage` only when one of its functions is called. **Parameters** | Parameter | Type | Description | | ---------- | ----------------------------- | ----------------------------------------- | | `options?` | `SignatureSecretStoreOptions` | The prefix every entry's key starts with. | **Returns** `SignatureSecretStore` The store's read, write and wipe functions. **Throws** Nothing itself; its functions throw [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"unsupported-browser"` when the browser has no `sessionStorage`, refuses access to it or refuses a write, as a full quota does. **Example** ```ts import { createTabSignatureSecretStore, openNimbusSession, } from "@umbra/nimbus-solana-web"; import type { NimbusSdk, WalletStandardSigner } from "@umbra/nimbus-solana-web"; declare const sdk: NimbusSdk; declare const connectedWallet: WalletStandardSigner; const opened = await openNimbusSession( sdk, { connectedWallet }, { signatureSecretStore: createTabSignatureSecretStore({ keyPrefix: "my-app/" }) }, ); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/signature-secret-store.ts`. ## createPersistentSignatureSecretStore Use it when users should sign once per device, and you accept the secret staying in `localStorage`. ```ts function createPersistentSignatureSecretStore( options?: SignatureSecretStoreOptions, ): SignatureSecretStore; ``` Creates a signature secret store over the browser's `localStorage`: the secret survives closing the browser, so the wallet signs once per device until the secret is wiped. The secret is kept base64-encoded and unencrypted, indefinitely: any script that ever runs on the page's origin can read it, and anyone with access to the browser profile can copy it from disk. The store touches `localStorage` only when one of its functions is called. **Parameters** | Parameter | Type | Description | | ---------- | ----------------------------- | ----------------------------------------- | | `options?` | `SignatureSecretStoreOptions` | The prefix every entry's key starts with. | **Returns** `SignatureSecretStore` The store's read, write and wipe functions. **Throws** Nothing itself; its functions throw [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"unsupported-browser"` when the browser has no `localStorage`, refuses access to it or refuses a write, as a full quota does. **Example** ```ts import { createPersistentSignatureSecretStore } from "@umbra/nimbus-solana-web"; const signatureSecretStore = createPersistentSignatureSecretStore(); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/signature-secret-store.ts`. Last updated 21 October 2026 # Tokens (/reference/tokens) [Tokens](https://sdk.nimbusprivacy.xyz/helpers/tokens) shows where a token's decimals come from, and how to format amounts and show names and logos. ## findMintByIdentifier Use it to name the token of a history item. A history item names its token by a 32-byte value derived from the mint address, not by the address itself. ```ts function findMintByIdentifier( sdk: NimbusSdk, args: { readonly mintIdentifier: Uint8Array }, ): NimbusMint | undefined; ``` Finds the preset mint a note or an intent names by its mint identifier, such as a `HistoryPage` entry's `note.mintIdentifier`. **Parameters** | Parameter | Type | Description | | --------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | `sdk` | `NimbusSdk` | The loaded SDK, whose preset lists the mints. | | `args` | `{ /** The mint identifier, a 32-byte little-endian field element, as a note carries it. */ readonly mintIdentifier: Uint8Array; }` | The mint identifier, its 32-byte little-endian field encoding as the note store keeps it. | **Returns** `NimbusMint | undefined` The mint, with its address, token program and decimals; none when the preset lists no mint with that identifier. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"misconfigured"` when `mintIdentifier` is shorter than 32 bytes. **Example** ```ts import { findMintByIdentifier } from "@umbra/nimbus-solana-web"; import type { HistoryPage, NimbusSdk } from "@umbra/nimbus-solana-web"; declare const sdk: NimbusSdk; declare const page: HistoryPage; for (const item of page.items) { for (const received of item.received) { const mint = findMintByIdentifier(sdk, { mintIdentifier: received.note.mintIdentifier }); console.log(received.note.amount, mint?.decimals); } } ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/mints.ts`. Last updated 21 October 2026 # Transfer (/reference/transfer) A Private Send runs in a context made from the session. [Private Send](https://sdk.nimbusprivacy.xyz/transfer) walks through the flow. ## createTransferContext Create one from the session before a Private Send. It picks the pool the payment uses. ```ts function createTransferContext( session: NimbusSession, options?: NimbusContextOptions, ): Promise; ``` Creates the context a session transfers in. Without `options.poolIndex` the pools' accounts are read from the RPC once, and the highest open pool is chosen; the nodes and guardian cluster default to the cluster's preset. **Parameters** | Parameter | Type | Description | | ---------- | ---------------------- | ------------------------------------------------------------------------- | | `session` | `NimbusSession` | The opened session. | | `options?` | `NimbusContextOptions` | The pool, the screener and data endorser nodes, and the guardian cluster. | **Returns** `Promise` The context. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"misconfigured"` when an option is out of range or no pool is open; `"service-unavailable"` or `"unreadable"` when the RPC cannot be read. **Example** ```ts import { createTransferContext } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; declare const session: NimbusSession; const context = await createTransferContext(session); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/contexts.ts`. ## transfer Use it to pay another registered user from the private balance, so the tokens arrive in theirs. ```ts function transfer( context: TransferContext, args: { readonly mint: Address; readonly amount: bigint; readonly recipient: Address; }, options?: TransferOptions, ): Promise; ``` Transfers `amount` of `mint` from the session's notes in the context's pool to `recipient`, a registered identity's wallet address, as a note sealed to its identity, through the relayer. The context API resolves the recipient to its identity; a wallet that has not registered is refused with reason `"recipient-unregistered"` before any note is merged or anything relayed. Syncs first, spends, and follows the relay as [`withdraw`](https://sdk.nimbusprivacy.xyz/reference/withdraw#withdraw) does, merging notes first when it must. **Parameters** | Parameter | Type | Description | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `context` | `TransferContext` | The transfer context. | | `args` | ``{ /** The mint transferred, of the session's notes. */ readonly mint: Address; /** * In base units of the mint: under `fees: "deducted"` what leaves the session's notes, every * fee taken from it; under `added`, what the recipient's note holds. */ readonly amount: bigint; /** A registered identity's wallet address, whose identity the note is sealed to. */ readonly recipient: Address; }`` | The mint, the amount in its base units, and the recipient's wallet address. | | `options?` | `TransferOptions` | As [`withdraw`](https://sdk.nimbusprivacy.xyz/reference/withdraw#withdraw) takes them, less the token account paid. | **Returns** `Promise` As [`withdraw`](https://sdk.nimbusprivacy.xyz/reference/withdraw#withdraw) returns, the payee's note known made once the settlement is `landed`. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) as [`withdraw`](https://sdk.nimbusprivacy.xyz/reference/withdraw#withdraw) throws, and with reason `"recipient-unregistered"` when the context API answers that `recipient` has no registration in the pool; [`NimbusSolanaFlowError`](https://sdk.nimbusprivacy.xyz/reference/types#nimbussolanaflowerror) when the protocol refuses the transfer. **Example** ```ts import { createTransferContext, transfer } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; import type { Address } from "@solana/kit"; declare const session: NimbusSession; declare const mint: Address; declare const friend: Address; const result = await transfer(await createTransferContext(session), { amount: 250_000n, mint, recipient: friend, }); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. Last updated 21 October 2026 # Types (/reference/types) Every type `@umbra/nimbus-solana-web` exports is listed here once, in alphabetical order. Each entry is generated from the SDK's own declarations and TSDoc when this site is built. Hover over a type in any code block on this site to see the same information. The SDK also exports `Wallet`, which is the [Wallet Standard's own type](https://github.com/wallet-standard/wallet-standard) and is not listed here. `NimbusSdkError` is a class, listed with the [errors](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror). The four types `ProverWorkerAnswer`, `ProverWorkerDeps`, `ProverWorkerRequest` and `WitnessGraphHandle`, come from the `./prover-worker` entry. ## Balance One balance a session's identity holds: what its notes of one mint in one zone come to, in one state. | Property | Type | Description | | -------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mint?` | `Address \| undefined` | The mint; absent when the preset lists no mint with the notes' mint identifier. | | `zone` | `number` | The zone the notes are in; 0 for the zone every deposit goes to by default. | | `state` | `"available" \| "expired" \| "pending" \| "reserved"` | `available` to spend; `pending` until their notes are final and past their valid-after slot; `reserved` by a spend or a deposit that leased them and has not settled; or `expired`, past their valid-before slot. | | `total` | `bigint` | The notes' amounts summed, in the mint's base units. | | `count` | `bigint` | How many notes. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reads.ts`. ## BeginAttemptRefusal Why an attempt was refused. ```ts type BeginAttemptRefusal = | { readonly kind: "unknown-intent" } | { readonly kind: "intent-settled"; readonly intent: Intent } | { readonly kind: "not-held"; readonly intent: Intent } | { readonly kind: "pool-frozen"; readonly intent: Intent } | { readonly kind: "append-pool-full"; readonly pool: number } | { readonly kind: "append-pool-not-open"; readonly pool: number; readonly status: Exclude; } | { readonly kind: "input-invalid"; readonly leaf: LeafId } | { readonly kind: "would-pay-twice"; readonly attempt: Attempt } | { readonly kind: "padding-of-another-intent"; readonly nullifier: Uint8Array; readonly intentId: Uint8Array; }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-storage-solana/src/notes/attempts.ts`. ## ConfirmationCommitment The commitment a flow waits for before it reports a transaction landed: `confirmed`, by a supermajority, or `finalized`, which no fork reverts. Not `processed`: a version 0 deposit is simulated at `confirmed` once its funding has landed, so a funding reported at `processed` could leave the deposit simulating against a platform wallet that does not hold it yet. ```ts type ConfirmationCommitment = Extract< SolanaCommitment, "confirmed" | "finalized" >; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/flows/transaction.ts`. ## ConnectedWallet A connected wallet: the signer the SDK takes, and the transaction version the wallet signs. | Property | Type | Description | | -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `wallet` | `Wallet` | The wallet connected, as the registry lists it. | | `signer` | `WalletStandardSigner` | The wallet's first account on the SDK's cluster, for `openNimbusSession`. | | `transactionVersion` | `TransactionVersion` | `1` when the wallet signs version 1 transactions, which the flows send by default; else `0`, which a flow's `transactionVersion` option must then name. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/wallets.ts`. ## DepositContext The context `deposit`, `refundStrandedDeposit` and `resolvePendingDeposit` run in. | Property | Type | Description | | --------- | ------------------- | ---------------------------------------------------------------- | | `flow` | `"deposit"` | | | `session` | `NimbusSession` | The opened session the flow runs for. | | `routing` | `NimbusFlowRouting` | The pool, committee nodes and guardian cluster the deposit uses. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/contexts.ts`. ## DepositOptions How `deposit` runs. | Property | Type | Description | | ---------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `onProgress?` | `((event: DepositStage \| SyncedStage) => void) \| undefined` | Called once per stage with what it produced; its throw is dropped. | | `fees?` | `"added" \| "deducted" \| undefined` | `deducted`, the default: the protocol fee comes out of `amount`; `added`: it is paid on top, from the session's notes of the mint the deposit merges, and refused with `amount-unfundable` when they cannot cover it. | | `submitterFee?` | `bigint \| undefined` | What the payload pays the submitter, in base units; `0n` when omitted. | | `submitterFeeMint?` | `Address \| undefined` | The mint the submitter's fee is paid in; the deposited mint when omitted. | | `zone?` | `NimbusZone \| undefined` | The zone the deposit's notes go to; zone 0 when omitted. | | `sync?` | `boolean \| undefined` | `true`, the default: sync the note store until it is caught up before starting, so the flow sees every note and spend the indexer has; `false` when the caller has just synced. | | `priorityFeeLamports?` | `bigint \| undefined` | The priority fee, in lamports for the whole transaction; `0n` when omitted. | | `transactionVersion?` | `TransactionVersion \| undefined` | `1`, one version 1 transaction, when omitted; `0` for a wallet that cannot sign version 1, which needs the deployment's lookup table. | | `commitment?` | `ConfirmationCommitment \| undefined` | What the flow waits for before it reports success; `confirmed` when omitted. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## DepositResult What depositing came to: - `submitted`: the deposit was sent; `submission.submission` says where it ended up. Only `landed` deposited; `unknown` may still land, which [`resolvePendingDeposit`](https://sdk.nimbusprivacy.xyz/reference/deposit#resolvependingdeposit) settles; `failed`, `expired` or `refused` deposited nothing. - `declined`: the wallet declined to sign; nothing was sent. - `store-refused`, `intent-refused`, `attempt-refused`: the note store would not open, begin the deposit or record its attempt; nothing was deposited. Read `detail` or `refusal` for why. - Version 0 only: `funding-failed`, the funding transaction did not land, nothing moved; `funding-unknown`, it may still land, so wait before retrying, which then funds only what is missing; `deposit-unsendable`, the funding landed but the deposit could not be sent, its value now in the platform wallet, which a retried deposit counts or [`refundStrandedDeposit`](https://sdk.nimbusprivacy.xyz/reference/deposit#refundstrandeddeposit) returns. ```ts type DepositResult = DepositOutcome; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## DepositStage One stage of a one-call deposit, as its `observe` hook is handed it: what the stage produced since the hook was last called, public values only. - `prepared`: the wallet store has begun the deposit's intent and leased any notes it merges. `intentId` is the store's 16-byte id for it, `executionSlot` the slot the proofs bind, `proofWindow` the slots the proofs stay valid for after it, and `funding` what the connected wallet moves to the platform wallet, in lamports and in base units of the mint. - `endorsed`: the committee's summed signature, 32 bytes; `proved`: the transaction proof and the absence proof, 128 bytes each, compressed. Each arrives as it completes, so they come in either order unless `serialise` proves first. - Version 0 only, when the platform wallet lacks what the deposit moves and pays: `funding-signed`, `funding-sent` and `funding-confirmed`, the funding transaction's own stages, before the deposit is signed. - `signed`: the store has begun the attempt and every signer has signed; nothing is sent. - `sent`: the store has marked the attempt sent and the RPC took the transaction, or answered without settling whether it did. - `confirmed`: it landed without error at `opts.commitment`, which the stage reports, as `funding-confirmed` does for the funding. A transaction that failed, expired or is still unknown stops at its `sent` stage, and the outcome says where it ended up. ```ts type DepositStage = | { readonly stage: "prepared"; readonly intentId: Uint8Array; readonly mint: Uint8Array; readonly recipient: Uint8Array; readonly amounts: DepositAmounts; readonly contextDigest: Uint8Array; readonly executionSlot: bigint; readonly proofWindow: bigint; readonly transactionVersion: TransactionVersion; readonly funding: { readonly lamports: bigint; readonly tokens: bigint }; } | { readonly stage: "endorsed"; readonly committeeSignature: Uint8Array } | { readonly stage: "proved"; readonly proof: Uint8Array; readonly nullifierProof: Uint8Array; } | SignedTransactionStage<"funding-signed"> | SentTransactionStage<"funding-sent"> | ConfirmedTransactionStage<"funding-confirmed"> | (SignedTransactionStage<"signed"> & { readonly attempt: number }) | SentTransactionStage<"sent"> | ConfirmedTransactionStage<"confirmed">; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/flows/deposit/types.ts`. ## DepositSubmission Where a deposit's transaction ended up. | Property | Type | Description | | --------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `intentId` | `Uint8Array` | The wallet store's 16-byte id for the deposit's intent. | | `attempt` | `number` | The attempt's number within the intent, from 0. | | `submission` | `TransactionSubmission` | Where the deposit's transaction ended up. | | `priorityFeeLamports` | `bigint` | The priority fee the transaction pays, in lamports: the one asked for, or at version 0 none when the deposit fits its packet only without its `SetComputeUnitPrice`. | Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/flows/deposit/types.ts`. ## HistoryPage One page of the wallet's history. | Property | Type | Description | | ------------ | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kind` | `"page"` | | | `pendingOwn` | `readonly ExpectedRecord[]` | What the wallet's own attempts built that the store has not yet kept from the stream, an attempt held dead among them, since it may still land; `listAttempts` gives each one's phase. | | `items` | `readonly HistoryItem[]` | The transactions, newest first. | | `next` | `{ readonly kind: "done"; } \| { readonly kind: "more"; readonly position: ListPosition; }` | `done` when no older transaction remains, or `more` with the position the next page starts from. | | `coverage` | `readonly HistoryCoverage[]` | One per records stream asked about, in the order asked. | Generated from the TSDoc in `sdks/typescript/nimbus-storage-solana/src/records/history.ts`. ## HistoryPosition Where a page of [`readHistory`](https://sdk.nimbusprivacy.xyz/reference/reads#readhistory) starts, as the note store lists its rows: `from-start`, the newest, as when `options.after` is omitted; or `after` a row's key, as a page's `next.position` names it when another page follows, to be passed back unchanged. ```ts type HistoryPosition = ListPosition; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reads.ts`. ## LeaseRefusal Why a lease was refused. ```ts type LeaseRefusal = | { readonly kind: "unknown-intent" } | { readonly kind: "intent-settled"; readonly intent: Intent } | { readonly kind: "held"; readonly intent: Intent } | { readonly kind: "inputs-frozen"; readonly intent: Intent } | { readonly kind: "not-registered"; readonly registration: Registration } | { readonly kind: "two-owner-addresses"; readonly leaf: LeafId } | { readonly kind: "two-zones"; readonly leaf: LeafId } | { readonly kind: "note-unavailable"; readonly leaf: LeafId; readonly reason: | "absent" | "other-mint" | "nullifier-unknown" | "not-selectable" | "invalid-at-execution-slot" | "leased"; }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-storage-solana/src/notes/leases.ts`. ## LoadNimbusSdkOptions What a caller may add to, or override in, the cluster's preset. | Property | Type | Description | | ---------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `assetsBaseUrl?` | `string \| undefined` | The URL of the directory the application serves this package's `assets/` in, ending with `/`, absolute or relative to the page, such as `/nimbus/`; `nimbus-solana-web copy-assets ` copies them there. The primitives' module is then fetched from it unless `endpoints.primitivesWasm` is given, and each proof runs in a module worker started from its `prover-worker.js` unless `platform.startProverWorker` is given. | | `endpoints?` | `Partial \| undefined` | Endpoints that replace the preset's, one by one. Each endpoint the preset leaves unconfigured must be given here, `primitivesWasm` unless `assetsBaseUrl` is; `coordinator` only on the `coordinator` route. A replaced gateway-routed endpoint is sent the session's gateway token at its own origin. | | `provingKeys?` | `NimbusProvingKeys \| undefined` | The proving keys, replacing the preset's; required while the preset leaves them unconfigured. | | `route?` | `NimbusRoute \| undefined` | How transactions reach the committee and the relayer; `direct` when omitted. | | `platform?` | `Partial \| undefined` | Browser functions that replace the browser's own globals, one by one. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/load.ts`. ## MarkAttemptSentRefusal Why marking an attempt sent was refused. ```ts type MarkAttemptSentRefusal = | { readonly kind: "unknown-attempt" } | { readonly kind: "not-begun"; readonly attempt: Attempt } | { readonly kind: "intent-settled"; readonly intent: Intent } | { readonly kind: "not-held"; readonly intent: Intent } | { readonly kind: "signature-required"; readonly attempt: Attempt } | { readonly kind: "volatile-backend" }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-storage-solana/src/notes/attempts.ts`. ## MergeFailureCause Why merging a wallet's notes stopped before the payment could be made. - `refused`, `relay-refused`, `unsent`: the failing merge's own outcome, as a spend reports it. - `failed`: the relayer gave the merge up or it failed on chain, as `followRelay` reports it. - `timeout`: the merge did not land, or its note did not become spendable in the wallet store, within `mergeTimeout` slots; `relayId` names the relay, which may still land, and `finalizedSlot` is the last finalized slot read. - `stale-handle`: the wallet's store was destroyed, replaced or migrated while the merge was followed; open it again. ```ts type MergeFailureCause = | Extract, { readonly kind: "refused" }> | Extract | Extract | { readonly kind: "timeout"; readonly relayId: string; readonly finalizedSlot: bigint; } | { readonly kind: "stale-handle" }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/flows/spend/types.ts`. ## NimbusBrowserPlatform Every browser function the SDK calls: the platform, and the globals that are always the browser's own. | Property | Type | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `keyRange` | `Pick<{ new (): IDBKeyRange; prototype: IDBKeyRange; bound(lower: any, upper: any, lowerOpen?: boolean, upperOpen?: boolean): IDBKeyRange; lowerBound(lower: any, open?: boolean): IDBKeyRange; only(value: any): IDBKeyRange; upperBound(upper: any, open?: boolean): IDBKeyRange; }, "bound">` | `IDBKeyRange`, which the note store's backend bounds its scans with. | | `storageManager` | `Pick` | `navigator.storage`, which the note store's backend asks for persistence and quota. | | `waitFor` | `(args: { readonly milliseconds: number; }) => Promise` | Resolves after `milliseconds`. | | `fetch` | `{ (input: RequestInfo \| URL, init?: RequestInit): Promise; (input: string \| URL \| Request, init?: RequestInit): Promise; }` | `fetch`, bound to its global: `(input, init) => fetch(input, init)`. | | `getRandomValues` | `(bytes: Uint8Array) => Uint8Array` | Web Crypto's `getRandomValues`, bound: `(bytes) => crypto.getRandomValues(bytes)`. | | `computeSha256Digest` | `(bytes: Uint8Array) => Promise` | Web Crypto's SHA-256, bound: `(bytes) => crypto.subtle.digest("SHA-256", bytes)`, resolving to the 32-byte digest. It checks a shipped file against the digest this package pins before the primitives, which could otherwise hash it, are loaded. | | `locks` | `Pick & Partial>` | `navigator.locks`, which holds a flow's exclusive lock while it runs: one lock per wallet and account for a registration, so two tabs never register at once, and a lock of its own, freshly named, for each deposit and spend, so a sync can tell one whose tab has gone. `query`, when the platform gives it, tells a sync which stopped spends to release. | | `indexedDB` | `IDBFactory` | The `IDBFactory` the note store and the artifact cache open their databases with. | | `startProverWorker` | `() => Worker \| Promise` | Starts a worker running a prover worker module, which answers each message with `@umbra/nimbus-solana-web/prover-worker`'s `proveWorkerRequest`, or resolves to one. It defaults to a module worker started from the `prover-worker.js` under `loadNimbusSdk`'s `assetsBaseUrl`, once that script's bytes are found to be the ones this package pins; without either, a flow that proves fails with `misconfigured` before it signs or sends anything. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## NimbusCluster A cluster the SDK ships a preset for. ```ts type NimbusCluster = "mainnet" | "devnet"; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/presets.ts`. ## NimbusContextOptions What a context may route differently from the defaults. | Property | Type | Description | | ------------------------ | --------------------- | ------------------------------------------------------------- | | `poolIndex?` | `number \| undefined` | The pool; the highest pool the chain shows open when omitted. | | `screenerNodeIndex?` | `number \| undefined` | The screener node; the preset's when omitted. | | `dataEndorserNodeIndex?` | `number \| undefined` | The data endorser node; the preset's when omitted. | | `guardianCluster?` | `bigint \| undefined` | The guardian cluster; the preset's when omitted. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/contexts.ts`. ## NimbusEndpoints Where each party the SDK reaches answers. | Property | Type | Description | | ---------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `rpc` | `string` | The Solana JSON-RPC endpoint. | | `indexer` | `string` | The indexer's data API, which serves the notes, spends and records feeds. | | `context` | `string` | The context API, which answers what a registration or a transaction proves against. | | `guardians` | `readonly NimbusNodeEndpoint[]` | Every guardian node of the cluster's guardian committee. | | `screeners` | `readonly NimbusNodeEndpoint[]` | The deposit screener nodes a deposit may ask for clearance. | | `dataEndorsers` | `readonly NimbusNodeEndpoint[]` | The data endorser nodes a registration or a transaction may ask to endorse it. | | `feeEnclave` | `string` | The fee enclave, which endorses a transaction's fee claim. | | `relayer` | `string` | The relayer, which submits withdrawals and transfers. | | `coordinator` | `string` | The process coordinator, which a `coordinator` route sends a transaction's reads, committee requests and relay through; read only on that route. | | `primitivesWasm` | `string` | The URL of `nimbus_primitives_wasm_bg.wasm`, the primitives' WebAssembly module this package ships in its `assets/` directory and the application serves. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## NimbusFlowRouting Where a flow's transaction is routed, every value chosen. | Property | Type | Description | | ----------------------- | -------- | ---------------------------------------------------------------------------------- | | `poolIndex` | `number` | The pool the transaction appends to and spends from, `0` to `65535`. | | `screenerNodeIndex` | `number` | The deposit screener node whose clearance the transaction carries, `0` to `65535`. | | `dataEndorserNodeIndex` | `number` | The data endorser node whose endorsement the transaction carries, `0` to `65535`. | | `guardianCluster` | `bigint` | The guardian cluster whose quorum endorses, a u64. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/contexts.ts`. ## NimbusGatewayEndpoint An endpoint the deployment's gateway fronts, which answers 401 to a request without a gateway token. | Property | Type | Description | | ---------- | -------- | ----------------------------------------------------------------------------------- | | `service` | `string` | The service, as an `unauthorized` error names it: `the relayer`, `guardian node 2`. | | `endpoint` | `string` | The service's base URL. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## NimbusGatewayToken The gateway token a session sends, as `openNimbusSession`'s `gatewayToken` option set it. ```ts type NimbusGatewayToken = | { readonly kind: "none" } | { readonly kind: "held"; readonly token: Secret } | { readonly kind: "issued"; readonly issue: () => Promise }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/gateway.ts`. ## NimbusMint A mint the cluster's deployment supports, as `@umbra/solana-deployments`' `mints.json` lists it. | Property | Type | Description | | -------------- | --------- | ------------------------------------------------------------------------------------------- | | `mint` | `Address` | The mint's address. | | `tokenProgram` | `Address` | The token program that owns the mint. | | `decimals` | `number` | How many decimal places the mint's base units carry: one token is `10 ** decimals` of them. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## NimbusNodeEndpoint One node of a committee and where it answers. | Property | Type | Description | | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------- | | `nodeIndex` | `number` | The node's index, as its registry numbers it: from 1 for a guardian node, from 0 for a screener or a data endorser. | | `endpoint` | `string` | The node's base URL. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## NimbusPlatform The browser's functions the SDK calls. Each defaults to the browser's own global. | Property | Type | Description | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `fetch` | `{ (input: RequestInfo \| URL, init?: RequestInit): Promise; (input: string \| URL \| Request, init?: RequestInit): Promise; }` | `fetch`, bound to its global: `(input, init) => fetch(input, init)`. | | `getRandomValues` | `(bytes: Uint8Array) => Uint8Array` | Web Crypto's `getRandomValues`, bound: `(bytes) => crypto.getRandomValues(bytes)`. | | `computeSha256Digest` | `(bytes: Uint8Array) => Promise` | Web Crypto's SHA-256, bound: `(bytes) => crypto.subtle.digest("SHA-256", bytes)`, resolving to the 32-byte digest. It checks a shipped file against the digest this package pins before the primitives, which could otherwise hash it, are loaded. | | `locks` | `Pick & Partial>` | `navigator.locks`, which holds a flow's exclusive lock while it runs: one lock per wallet and account for a registration, so two tabs never register at once, and a lock of its own, freshly named, for each deposit and spend, so a sync can tell one whose tab has gone. `query`, when the platform gives it, tells a sync which stopped spends to release. | | `indexedDB` | `IDBFactory` | The `IDBFactory` the note store and the artifact cache open their databases with. | | `startProverWorker` | `() => Worker \| Promise` | Starts a worker running a prover worker module, which answers each message with `@umbra/nimbus-solana-web/prover-worker`'s `proveWorkerRequest`, or resolves to one. It defaults to a module worker started from the `prover-worker.js` under `loadNimbusSdk`'s `assetsBaseUrl`, once that script's bytes are found to be the ones this package pins; without either, a flow that proves fails with `misconfigured` before it signs or sends anything. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## NimbusProving How the prover runs in its worker. | Property | Type | Description | | --------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `threads` | `number` | The threads the prover runs on: one per logical core the browser reports. | | `allowSingleThreaded` | `boolean` | Whether to prove on one thread when the page is not cross-origin isolated, rather than refuse; `false`, since `loadNimbusSdk` loads only on a page that is. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## NimbusProvingKeys Where the circuits' proving keys and witness calculators come from, and what each key proves. The manifest is configuration, not fetched: it pins each artifact's SHA-256 and each proving key's verifying key, so the host that serves the bytes cannot also choose what they must hash to. Every key is further checked against the verifying keys compiled into the deployed pool program before it is used. | Property | Type | Description | | ------------ | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `baseUrl` | `string` | The origin every manifest path is relative to, ending with `/`. | | `manifest` | `CircuitManifest` | The circuits' artifacts, each by its path under `baseUrl` and its SHA-256, and the verifying key each proving key belongs to. | | `statements` | `readonly NimbusProvingKeyStatement[]` | The statement each of the manifest's proving keys proves, at most one per circuit: a proving key is handed over only for a selection that equals one of these. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## NimbusProvingKeyStatement The statement one of the manifest's proving keys proves, named as the pool program selects its verifying key: the circuit, and the parameters its circuit fixes. ```ts type NimbusProvingKeyStatement = | { readonly circuit: "registration"; readonly quorumThreshold: number; readonly quorumNodes: number; } | { readonly circuit: "transaction"; readonly quorumThreshold: number; readonly quorumNodes: number; readonly shape: CircuitShape; } | { readonly circuit: "nullifier_non_membership"; readonly inputs: number }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## NimbusRegistrationStanding Where a session's identity stands in the identity tree of the pool the context API serves. ```ts type NimbusRegistrationStanding = | { readonly kind: "registered"; readonly poolIndex: number } | { readonly kind: "unregistered"; readonly poolIndex: number }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/session.ts`. ## NimbusRoute How a transaction reaches the committee and the relayer. - `direct`: the browser asks each committee member, the context API and the relayer itself. - `coordinator`: a transaction's reads, its committee requests and its relay go through the process coordinator, which makes them from beside the services, so a relayed spend's four committee requests and its relay become one exchange. Registrations, the relay's progress, the RPC, the indexer and the prover are reached directly on both routes. ```ts type NimbusRoute = "direct" | "coordinator"; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/load.ts`. ## NimbusRoutingDefaults The committee nodes and guardian cluster a context asks when its options name none. | Property | Type | Description | | ----------------------- | -------- | -------------------------------------------------------------------------------- | | `screenerNodeIndex` | `number` | The deposit screener node a deposit asks for clearance. | | `dataEndorserNodeIndex` | `number` | The data endorser node a registration or a transaction asks to endorse it. | | `guardianCluster` | `bigint` | The guardian cluster whose quorum endorses, as the guardian registry numbers it. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/presets.ts`. ## NimbusSdk A loaded SDK: the cluster's deployment, its mints and routing defaults, the resolved settings, and the handles every function of this package reads, the primitives' module and the artifact cache among them. Pass it to `openNimbusSession`; treat every field as read-only. One SDK serves any number of sessions, and holds no secret. | Property | Type | Description | | ------------ | ---------------------------- | ----------------------------------------------------------------------------------------- | | `cluster` | `NimbusCluster` | The cluster the SDK was loaded for. | | `route` | `NimbusRoute` | How transactions reach the committee and the relayer. | | `deployment` | `NimbusSolanaFlowDeployment` | The deployment, decoded: what every flow and the note store act on. | | `mints` | `readonly NimbusMint[]` | The mints the deployment supports. | | `routing` | `NimbusRoutingDefaults` | The committee nodes and guardian cluster a context asks when its options name none. | | `settings` | `NimbusSettings` | Every endpoint, the proving keys and the other values the preset and options resolved to. | | `platform` | `NimbusBrowserPlatform` | The browser functions every call makes, the options' in place of the browser's own. | | `primitives` | `PrimitiveDeps` | Every primitive, filled from the primitives' WebAssembly module. | | `cache` | `IndexedDbBlobContext` | The opened IndexedDB blob store circuit artifacts are cached in. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/load.ts`. ## NimbusSdkErrorReason Why the SDK could not go on. - `misconfigured`: the cluster's preset leaves an endpoint or the proving keys unconfigured and the options did not supply them, or a configured value or an argument is unusable: an `assetsBaseUrl` that is not a directory URL, no prover worker, no endpoint for the committee node a context names, an address that is not base58, an option outside its range, a mint identifier shorter than 32 bytes, a deposit submission whose outcome is settled already, a gateway token that is not a bearer token, or no open pool. The message names each one; pass it in `loadNimbusSdk`'s options or fix the argument. Not recoverable by retrying. - `unreadable`: a party answered, or the browser's storage held, something that cannot be read: a malformed service answer, a note store another deployment or a newer schema wrote, or a note store another tab replaced or kept writing to while it was in use. Retrying the same party is unlikely to help, though a call that met a note store being replaced may succeed when made again, since each call opens the store afresh. - `unsupported-browser`: the browser lacks a global the SDK needs (`fetch`, Web Crypto, `navigator.locks`, `navigator.storage`, IndexedDB, `IDBKeyRange`, WebAssembly), or refused to open its storage, as a private window may; or the page is not cross-origin isolated, which the server fixes by sending `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`. Not recoverable in this browser or on this page. - `wallet-unsupported`: the wallet cannot connect, sign messages and sign transactions on the cluster's chain, its account's address is not its public key's, or it failed while connecting or signing, signed other bytes than it was given, or answered a signature that does not verify. A user who declines is not this error: the call answers `declined`. Ask the user to connect another wallet. - `service-unavailable`: a service, the RPC or the artifact host could not be reached or answered a failing status. Retry later. - `unauthorized`: a gateway service answered 401 or 403, because the session was opened without a gateway token or the gateway refused it, or the session's token issuer failed. The message names the service and never the token. Open a session with a token the gateway accepts, or have the issuer return a fresh one. - `dependency-nonconforming`: a dependency answered against its contract: a proving key or witness calculator that is not the one the manifest and the deployed program pin, a prover that refused the statement, a file of this package's `assets/` whose SHA-256 is not the one the package pins, or primitives that fail their known-answer checks. The value must not be used; retrying will not help, and serving the `assets/` packed with this copy of the package fixes a mismatched file. - `recipient-unregistered`: the context API answered that the wallet a deposit or a transfer pays has no registration in the pool, so no note can be sealed to it. The message carries the API's detail. Nothing was signed or sent. Ask the recipient to register, or pay another wallet; a registration that has only just landed is seen once the indexer reaches it. - `account-unregistered`: the context API answered that the session's own identity, or for a withdrawal the connected wallet, has no registration in the pool. The message carries the API's detail. Nothing was signed or sent. Run `register` first; a registration that has only just landed is seen once the indexer reaches it. ```ts type NimbusSdkErrorReason = | "misconfigured" | "unreadable" | "unsupported-browser" | "wallet-unsupported" | "service-unavailable" | "unauthorized" | "dependency-nonconforming" | "recipient-unregistered" | "account-unregistered"; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/errors.ts`. ## NimbusSession An opened session: the connected wallet, the seed and keys derived from its signature, its identity, its note store, and where it stood when it was opened. It holds secrets in memory: the wallet seed every key derives from, the keys a sync recognises notes with, and the gateway token. Close it with [`closeNimbusSession`](https://sdk.nimbusprivacy.xyz/reference/sessions#closenimbussession) when the user is done; never serialise it. | Property | Type | Description | | ---------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sdk` | `NimbusSdk` | The loaded SDK, its platform's `fetch` sending NimbusSession.gatewayToken to the gateway's origins: what every function taking the session reaches each party through. | | `gatewayToken` | `NimbusGatewayToken` | The gateway token the session's gateway-bound requests carry; never persisted. | | `wallet` | `WalletStandardSigner` | The connected wallet's signer, which signs registrations, deposits and refunds. | | `walletAddress` | `Address` | The connected wallet's address: it pays, and the user registry names it the owner. | | `accountIndex` | `number` | The platform wallet's account index, SLIP-0010's `m/44'/501'/accountIndex'/0'`. | | `platformWallet` | `Address` | The platform wallet the identity binds, derived from the seed at `accountIndex`. | | `walletSession` | `WalletSession` | The connected wallet's 32-byte public key and the 64-byte wallet seed, which is secret. | | `identity` | `WalletIdentity` | The identity's public values: its commitment and the key root a registration publishes. | | `keys` | `WalletKeys` | The keys a sync recognises notes and records with; secret. | | `store` | `{ readonly backend: IndexedDbBackend; readonly choices: WalletStorageChoices; }` | The note store's opened IndexedDB backend and the choices it was created with. Until the identity registers, the store has no birthday and holds nothing. | | `registration` | `NimbusRegistrationStanding` | Where the identity stood when the session was opened. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/session.ts`. ## NimbusSettings Everything the fillers are bound to besides the deployment and the platform, every value resolved from the preset and the options. | Property | Type | Description | | ------------------ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `endpoints` | `NimbusEndpoints` | Where each party answers: the preset's endpoints, each replaced by the options' when given. | | `assetPinning` | `"pinned" \| "unpinned"` | Whether this copy of the package checks the files it ships in `assets/` against their SHA-256 before running them. A packed package is `pinned`: `prepack` writes the digests of the assets packed with it. `unpinned` means the package runs from source, or from a `dist/` built after its assets, and runs those files unchecked; an application can refuse to go on when it reads `unpinned` in production. | | `gatewayEndpoints` | `readonly NimbusGatewayEndpoint[]` | Every configured endpoint the preset marks gateway-routed, the coordinator only on the `coordinator` route: the origins a session's gateway token is sent to. | | `provingKeys` | `NimbusProvingKeys` | The proving keys: the options' when given, else the preset's. | | `cacheDatabase` | `string` | The IndexedDB database the downloaded circuit artifacts are cached in. | | `network` | `string` | The network name the context API reports, such as `mainnet`. | | `proving` | `NimbusProving` | How the prover runs in its worker. | | `timing` | `NimbusTiming` | How long the SDK waits on the chain and the relayer, from the preset. | | `observedPools` | `number` | How many pools, from pool 0, each sync reads the accounts of. Under the pool order the first pool read that is not open proves no later pool exists. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## NimbusSolanaFlowError A refusal a caller can act on. Its message is fixed text and its `detail` holds names, counts and indices, never key material. | Property | Type | Description | | --------- | ----------------------------------- | --------------------------------------------------------------------------------- | | `name` | `"NimbusSolanaFlowError"` | | | `reason` | `NimbusSolanaFlowErrorReason` | Why the flow refused, which decides what the caller does next. | | `detail` | `Readonly>` | Names, counts and indices that locate the refusal, for a log; never key material. | | `message` | `string` | | | `stack?` | `string \| undefined` | | | `cause?` | `unknown` | | Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/context/errors.ts`. ## NimbusSolanaFlowErrorReason Why the context derivation or a flow refused to go on. - `message-altered`: the unlock message in this build does not hash to its pinned digest. Not recoverable at run time: the build is wrong, and nothing may be signed from it. - `signature-invalid`: the wallet's signature is not 64 bytes or does not verify over the unlock message for the connected wallet. Ask again; a wallet that wraps the message is unsupported. - `deployment-invalid`: the deployment in `ctx` contradicts itself, such as a pool program that is not `programs.pool`. Fix the configuration. - `binding-mismatched`: the user registry binds the connected wallet to another platform wallet than this account derives. Repair is a new bind both wallets sign, never automatic. - `standing-mismatched`: the context service's answer does not fit the registration: another pool, another cluster, a shape the circuit does not fold, a stale root or an inactive cluster. Fetch again later. - `context-mismatched`: the context service's answer does not fit the deposit or the spend: a root the chain has retired, another sender, a payee or recipient of the other kind, witnesses that do not pair with what was asked or are of another shape, a rate absence proven against another root, or a quorum the circuit does not fold. Fetch again; a persistent refusal means the service is wrong. - `absence-unproven`: the gap the service served does not prove the identity, or an input's nullifier, absent from the root it names. Fetch again; a persistent refusal means the service is wrong. - `mint-unsupported`: no mint account exists at the address, a program other than SPL Token or Token-2022 owns it, or its bytes are not a mint's. Nothing can be deposited of it; choose another mint. - `fee-changing`: the mint's Token-2022 transfer fee changes at the next epoch, too soon for a deposit prepared now to be sure which applies. Retry once the epoch has turned. - `amount-unfundable`: the amount cannot settle: the mint's transfer fee takes the whole amount, so nothing reaches the pool token account; the fee leaves the payee nothing; the merged or selected notes cannot pay the amount and its fees; or the policy charges the change. Nothing was asked of the committee; choose another amount or fee mode. - `committee-mismatched`: the guardians', the screener's, the fee enclave's or the data endorser's answers cannot be summed into a signature the chain accepts: a node refused, signed another digest, or holds a key the cluster does not publish. Nothing was sent. Retry; a persistent refusal means a node or the cluster's published keys are wrong. - `proof-mismatched`: the prover proved another statement than the one prepared. The prover or its key is wrong, and retrying with them will not help. - `transaction-altered`: the connected wallet returned a transaction whose message differs from the one built, or whose signature does not verify. Nothing was sent. Ask again; a wallet that rewrites what it is asked to sign is unsupported. - `transaction-unsendable`: the transaction is larger than its version carries, or its simulation failed; nothing was signed or sent. A failed simulation may pass on a retry; a size will not. - `fee-payer-unfunded`: the connected wallet pays for the flow's transaction and has no account on the cluster, or holds fewer lamports than paying for it requires: the base fee for each signature, the priority fee, the lamports the transaction moves from it, the rent of an account it creates, and the rent-exempt minimum it keeps. Raised while the flow is prepared, so nothing was proved, asked of the committee, signed or sent. `detail` holds `requiredLamports` and `heldLamports` as decimal strings; add SOL to the wallet and retry. - `relay-unavailable`: the relayer's policy does not carry the spend: another program, another pool, a mint it does not relay, a fee it refuses, no tip accrual for the fee's mint, or a destination it will not create. Nothing was asked of the committee; choose another relayer. - `payout-unpayable`: the withdrawal's destination cannot be paid: no account and no owner to create it for, an account that is not a token account of the mint, an owner whose associated token account it is not, or an owner named for a native SOL wallet other than the wallet. It is raised too when no account exists at the mint or a program other than SPL Token or Token-2022 owns it, and when a native SOL payout, after every fee, would leave the destination below the rent-exempt minimum. Nothing was asked of the committee; name another destination, or for native SOL a larger amount. - `relay-mismatched`: what was admitted is not the relay built: another context digest, or a posted payload whose committee signature is not the committee's sum. The relay may still land, so its notes stay held until the wallet store sees it land or die; sync to learn which. - `zone-unauthorised`: a zone signer refused, answered for another zone or side, answered with a value outside the field, or with a key the zone's authorised set does not hold at the root the statement proves against. Nothing was asked of the committee and the notes are released; ask the zone's operator, or use another zone. ```ts type NimbusSolanaFlowErrorReason = | "message-altered" | "signature-invalid" | "deployment-invalid" | "binding-mismatched" | "standing-mismatched" | "context-mismatched" | "absence-unproven" | "mint-unsupported" | "fee-changing" | "amount-unfundable" | "committee-mismatched" | "proof-mismatched" | "transaction-altered" | "transaction-unsendable" | "fee-payer-unfunded" | "relay-unavailable" | "payout-unpayable" | "relay-mismatched" | "zone-unauthorised"; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/context/errors.ts`. ## NimbusTiming How long the SDK waits on the chain and the relayer. | Property | Type | Description | | -------------------------- | -------- | ------------------------------------------------------------------------------------- | | `confirmationPolls` | `number` | How many times a sent transaction's status is read before the wait ends as `unknown`. | | `relayPolls` | `number` | How many times a relay's status is read before the wait ends as `pending`. | | `pollIntervalMilliseconds` | `number` | The pause between two status reads, in milliseconds. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## NimbusZone A zone other than zone 0 and the signer its authorised set holds. | Property | Type | Description | | -------- | ---------------- | -------------------------------------------------------------- | | `id` | `number` | The zone, from 1 to 2^20 - 1. | | `signer` | `ZoneSignerPort` | Signs the zone's authorisation for each side whose gate fires. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## NoteStoreSyncResult What syncing a session's note store came to. ```ts type NoteStoreSyncResult = | { readonly kind: "unregistered" } | { readonly kind: "ingested"; readonly slot: bigint; readonly rounds: number; readonly caughtUp: boolean; } | { readonly kind: "unavailable"; readonly step: string; readonly detail: Readonly>; }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reads.ts`. ## NoteStoreSyncRound One round of [`syncNoteStore`](https://sdk.nimbusprivacy.xyz/reference/reads#syncnotestore), as `onProgress` is handed it. | Property | Type | Description | | ---------- | --------- | ---------------------------------------------------------------------------------------- | | `round` | `number` | The round, from 1. | | `slot` | `bigint` | The slot the round read the chain at: the latest confirmed one, as the store decides on. | | `caughtUp` | `boolean` | Whether every stream the round read has been taken in to the feed's end. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reads.ts`. ## OpenNimbusSessionOptions How a session is opened. | Property | Type | Description | | ----------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `signatureSecretStore?` | `SignatureSecretStore \| undefined` | Where the secret the unlock signature yields is read from and written to; a new `createTabSignatureSecretStore()` when omitted. | | `accountIndex?` | `number \| undefined` | The platform wallet's account index, an integer in `[0, 2^31)`; `0`, the one a registration binds by default, when omitted. Each index is a separate identity. | | `gatewayToken?` | `string \| (() => Promise) \| undefined` | The bearer token every request to the deployment's gateway carries, issued for this session: an RFC 6750 token, held in a `Secret` until the session closes, or an issuer called once for every gateway-bound request, retries included, which should return a cached token until it nears expiry. It is sent only to the origins of the SDK's `settings.gatewayEndpoints`, never stored, logged or put in an error or a progress event. Omitted, the session opens and reads, and every step a gateway serves fails `unauthorized`. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/session.ts`. ## OpenNimbusSessionResult What opening a session came to. ```ts type OpenNimbusSessionResult = | { readonly kind: "opened"; readonly session: NimbusSession } | { readonly kind: "declined" }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/session.ts`. ## PendingDepositResolutionResult What resolving a pending deposit came to: - `landed`: the deposit landed, at `slot`; its note arrives with the next sync. - `failed` or `expired`: nothing was deposited, and the note store was told; `report` and `intent` are its answers, `abandoned` meaning the notes the deposit merged are free again, and `settled` that [`releaseReservedNotes`](https://sdk.nimbusprivacy.xyz/reference/reserved-funds#releasereservednotes) had stopped it, the report freeing them. At version 0 a landed funding's value stays in the platform wallet: deposit again, or [`refundStrandedDeposit`](https://sdk.nimbusprivacy.xyz/reference/deposit#refundstrandeddeposit). - `unknown`: the wait ended first; it may still land until its last valid height, so call again. - `store-refused`: the note store would not open for the identity; nothing was reported. ```ts type PendingDepositResolutionResult = DepositResolution; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## ProverWorkerAnswer The prover worker's answer: the proof as snarkjs writes it, or why it failed. ```ts type ProverWorkerAnswer = | { readonly kind: "proved"; readonly proof: { readonly pi_a: readonly string[]; readonly pi_b: readonly (readonly string[])[]; readonly pi_c: readonly string[]; }; readonly publicSignals: readonly string[]; } | { readonly kind: "failed"; readonly reason: string; readonly message: string; }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/prover-worker.ts`. ## ProverWorkerDeps The witness-graph and prover WASM modules, each initialised by the application before the call. | Property | Type | Description | | -------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------ | | `graph` | `{ readonly load_graph: (bytes: Uint8Array) => WitnessGraphHandle; }` | The module namespace of the glue `just build-witness-graph-wasm` writes. | | `wasm` | `UmbraProverWasm` | The module namespace of the glue `just build-umbra-prover-wasm` writes. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/prover-worker.ts`. ## ProverWorkerRequest What the main thread asks the prover worker for. | Property | Type | Description | | --------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------- | | `circuit` | `string` | The circuit's name, as `@umbra/circuit-artifacts` names it, for the answer's messages. | | `provingKey` | `Uint8Array` | The circuit's proving key, already checked by the main thread. | | `witnessGraph` | `Uint8Array` | The circuit's witness graph, its `.wcd`, already checked by the main thread. | | `input` | `Readonly>` | Every signal the circuit declares, by its own name; secret. | | `threads` | `number` | The threads the prover runs on. | | `allowSingleThreaded` | `boolean` | Whether to prove on one thread when the page is not cross-origin isolated, rather than fail. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/prover-worker.ts`. ## RefundStage One stage of a one-call refund, as its `observe` hook is handed it: what the stage produced since the hook was last called, public values only. - `prepared`: the platform wallet, the mint as read, what the platform wallet held, and how many instructions return it. A platform wallet holding nothing stops before this stage. - `signed`: both wallets signed the transaction; nothing is sent. - `sent`: the RPC took it, or answered without settling whether it did. - `confirmed`: it landed without error at `opts.commitment`, which the stage reports. One that failed, expired or is still unknown stops at `sent`, and the outcome says where it ended up. ```ts type RefundStage = | { readonly stage: "prepared"; readonly platformWallet: Uint8Array; readonly mint: PreparedDepositRefund["mint"]; readonly holdings: PlatformWalletHoldings; readonly instructionCount: number; } | SignedTransactionStage<"signed"> | SentTransactionStage<"sent"> | ConfirmedTransactionStage<"confirmed">; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/flows/deposit/refund.ts`. ## RegisterOptions How `register` runs. | Property | Type | Description | | ---------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `onProgress?` | `((event: RegisterStage) => void) \| undefined` | Called once per stage with what it produced; its throw is dropped. | | `priorityFeeLamports?` | `bigint \| undefined` | The priority fee, in lamports for the whole transaction; `0n` when omitted. | | `transactionVersion?` | `TransactionVersion \| undefined` | `1`, one version 1 transaction, when omitted; `0` for a wallet that cannot sign version 1, which needs the deployment's lookup table. | | `commitment?` | `ConfirmationCommitment \| undefined` | What the flow waits for before it reports success; `confirmed` when omitted. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## RegisterStage One stage of a one-call registration, as its `observe` hook is handed it: what the stage produced since the hook was last called, public values only. - `prepared`: the identity, its platform wallet and pool, whether the bind is still needed, and whether the identity is registered already or pending, with the context digest the committee signs and the slot the wallet's store was opened at. A registration with nothing left to send stops here. - `endorsed`: the committee's summed signature, 32 bytes; `proved`: the 128-byte compressed proof. A pending identity has both, each as it completes, so they arrive in either order unless `serialise` proves first; a registered one has neither. - `signed`: the transaction both wallets signed, before anything is sent, and the instructions it carries. - `sent`: the RPC took it, or answered without settling whether it did. - `confirmed`: it landed without error at `opts.commitment`, which the stage reports. One that failed, expired or is still unknown stops at `sent`, and the outcome says where it ended up. ```ts type RegisterStage = | { readonly stage: "prepared"; readonly identityCommitment: bigint; readonly platformWallet: Uint8Array; readonly poolIndex: number; readonly bind: PlatformWalletBindingPlan; readonly identity: | { readonly kind: "registered" } | { readonly kind: "pending"; readonly contextDigest: Uint8Array; readonly birthday: bigint; }; } | { readonly stage: "endorsed"; readonly committeeSignature: Uint8Array } | { readonly stage: "proved"; readonly proof: Uint8Array } | (SignedTransactionStage<"signed"> & { readonly wrote: readonly RegistrationWrite[]; }) | SentTransactionStage<"sent"> | ConfirmedTransactionStage<"confirmed">; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/flows/register/types.ts`. ## RegistrationContext The context `register` runs in. | Property | Type | Description | | --------- | ---------------------------------------------- | ------------------------------------------------------------------------ | | `flow` | `"registration"` | | | `session` | `NimbusSession` | The opened session the flow runs for. | | `routing` | `Omit` | The pool, data endorser node and guardian cluster the registration uses. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/contexts.ts`. ## RegistrationContextOptions What a registration context may route differently: a registration asks no screener. ```ts type RegistrationContextOptions = Omit< NimbusContextOptions, "screenerNodeIndex" >; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/contexts.ts`. ## RegistrationResult What registering came to: - `complete`: the identity was registered and bound already; nothing was sent. - `submitted`: the transaction was sent; `submission.submission` says where it ended up. Only `landed` registered it: read [`readRegistration`](https://sdk.nimbusprivacy.xyz/reference/registration#readregistration) then. `failed`, `expired` or `refused` registered nothing, and `register` may be called again; `unknown` may still land, so read the standing again before retrying. - `declined`: a wallet declined to sign; nothing was sent. - `store-refused`: the note store would not open for this identity, as `refusal` says; nothing was asked of any service. ```ts type RegistrationResult = RegistrationOutcome; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## ReleasedNotes What releasing came to: the spends whose notes are free now, those whose notes free once an attempt that may have been sent lands or can no longer land, and those left alone because a tab is still running them. | Property | Type | Description | | ---------- | -------------------------- | ----------------------------------------------------------------------------------------- | | `released` | `readonly ReservedSpend[]` | The spends whose notes are free now. | | `settling` | `readonly ReservedSpend[]` | The spends now `releasing`, whose notes free once their sent attempt settles. | | `running` | `readonly ReservedSpend[]` | The spends a tab of this origin is still running, left alone because `force` was not set. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reserved.ts`. ## ReservedSpend One spend holding a session's notes: a withdrawal, a transfer or a merge, which the store does not tell apart; an escape; or a deposit that merges some of the session's notes, spending them into one new note beside the one it deposits. A deposit that merges none holds no notes and is not one. | Property | Type | Description | | ---------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | `string` | The spend's id, 32 lowercase hex characters; releaseReservedNotes takes it. | | `kind` | `"deposit" \| "escape" \| "spend"` | `spend` for a withdrawal, a transfer or a merge (a step that combines several pieces of the private balance into one, so one payment can cover the amount); `escape` for a withdrawal through the pool's escape hatch, which needs no committee signature, so it works while a service is down, but publishes the note's contents on chain; `deposit` for a deposit that merges the notes it holds. | | `mint?` | `Address \| undefined` | The mint it moves; absent when the preset lists no mint with its identifier. | | `amount` | `bigint` | The amount it moves, in the mint's base units: for a deposit, what it takes from the connected wallet, not what the notes it merges hold. | | `state` | `ReservedSpendState` | Where it stands, which decides what releasing it does. | | `attempts` | `number` | How many attempts of it have begun. | | `running` | `boolean` | Whether a tab of this origin holds the spend's lock now, so the spend is still running there; false when none does, or when the browser cannot list its locks. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reserved.ts`. ## ReservedSpendState Where a spend holding notes stands, which decides what releasing it does: - `unsent`: nothing of it has left the device; releasing it frees its notes at once. - `sent`: an attempt of it may have reached the relayer or the chain; releasing it stops any new attempt, and its notes free once that attempt lands or can no longer land. - `releasing`: released while an attempt may still land; its notes free once it lands or can no longer land. ```ts type ReservedSpendState = "unsent" | "sent" | "releasing"; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reserved.ts`. ## SentTransactionOptions What every flow the connected wallet signs and sends may vary. | Property | Type | Description | | ---------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `priorityFeeLamports?` | `bigint \| undefined` | The priority fee, in lamports for the whole transaction; `0n` when omitted. | | `transactionVersion?` | `TransactionVersion \| undefined` | `1`, one version 1 transaction, when omitted; `0` for a wallet that cannot sign version 1, which needs the deployment's lookup table. | | `commitment?` | `ConfirmationCommitment \| undefined` | What the flow waits for before it reports success; `confirmed` when omitted. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## SettledSpendOutcome What a withdrawal or a transfer came to: - `relayed`: the relayer admitted it; `settlement` says how following it went, and the payment is known made once that is `landed`. A `pending` settlement may still land; a later sync settles it, and its notes stay reserved until then. - `refused`: nothing was asked of the committee; `refusal`, a [`SpendRefusal`](https://sdk.nimbusprivacy.xyz/reference/types#spendrefusal), says why, `insufficient` with what the session may spend. - `relay-refused`: the relayer refused it, with its `code`, creating no relay; its notes are released at once, so a retry may spend them. - `unsent`: the note store would not mark it sent, so nothing was posted. - `merge-failed`: the notes had to be merged first and a merge did not settle; `mergesCompleted` stay merged, so a retry needs fewer, and `cause` says why. ```ts type SettledSpendOutcome = | Exclude | (Extract & { readonly settlement: SpendSettlement; }); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## SettledStage The stage a withdrawal or a transfer reports once following its relay settled, with how. | Property | Type | Description | | ------------ | ----------------- | -------------------------------- | | `stage` | `"settled"` | | | `settlement` | `SpendSettlement` | How following the relay settled. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## SignatureSecretKey Which wallet, cluster and chain a stored secret belongs to. | Property | Type | Description | | --------------- | --------------------------- | --------------------------------------------------------------- | | `walletAddress` | `Address` | The connected wallet's address. | | `cluster` | `NimbusCluster` | The cluster the session is opened on. | | `chain` | `` `${string}:${string}` `` | The CAIP-2 chain the session acts on, such as `solana:mainnet`. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/signature-secret-store.ts`. ## SignatureSecretReading What reading a stored secret came to. ```ts type SignatureSecretReading = | { readonly kind: "found"; readonly signatureSecret: Secret } | { readonly kind: "absent" }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/signature-secret-store.ts`. ## SignatureSecretStore Where the signature secret is kept between page loads: three functions over one place. `openNimbusSession` reads the secret first, asks the wallet to sign only when it is absent, and writes what the signature yields. Call `wipeSignatureSecret` when the user signs out; it does not close a session already open. | Property | Type | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | `readSignatureSecret` | `(key: SignatureSecretKey) => Promise` | Reads the secret stored for a key, if any. | | `writeSignatureSecret` | `(key: SignatureSecretKey, args: { readonly signatureSecret: Secret; }) => Promise` | Stores a key's 64-byte secret, replacing any before it; the caller keeps its carrier. | | `wipeSignatureSecret` | `(key: SignatureSecretKey) => Promise` | Deletes the secret stored for a key; deleting what is absent does nothing. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/signature-secret-store.ts`. ## SignatureSecretStoreOptions How a Web Storage store names its entries. | Property | Type | Description | | ------------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `keyPrefix?` | `string \| undefined` | What every entry's key starts with, before the cluster, chain and wallet address; `umbra/nimbus-solana-web/account-secret/` when omitted. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/signature-secret-store.ts`. ## SpendOptions How `withdraw` and `transfer` run. The relayer sends their transaction, so they take no priority fee, version or commitment. | Property | Type | Description | | ------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `follow?` | `boolean \| undefined` | `true`, the default: once the relayer admits the spend, follow it until it lands at `commitment`, then sync the note store, so the result says it landed and the spent notes are no longer reserved; `false`: return at the relayer's admission. | | `commitment?` | `"confirmed" \| "finalized" \| undefined` | What following the relay waits for: `confirmed` when omitted, or `finalized`. | | `fees?` | `"added" \| "deducted" \| undefined` | `deducted`, the default: the fees come out of `amount`; `added`: they are paid on top. | | `submitterFee?` | `bigint \| undefined` | The submitter's fee: the relayer's floor when omitted, or an amount offered in base units. The floor includes the relayer's published fee for creating a withdrawal's missing token account when it does not sponsor creating it, and shows in the `prepared` stage's `relayerFee`; an offered amount below what creating it costs is refused with `relay-unavailable`. | | `submitterFeeMint?` | `Address \| undefined` | The mint the submitter's fee is paid in; the spent mint when omitted. | | `zone?` | `NimbusZone \| undefined` | The zone the spent notes come from and the outputs go to; zone 0 when omitted. | | `mergeTimeout?` | `bigint \| undefined` | How many slots each merge may take to land and for its note to become spendable in the note store; 300 when omitted. | | `sync?` | `boolean \| undefined` | `true`, the default: sync the note store until it is caught up before starting, so the flow sees every note and spend the indexer has; `false` when the caller has just synced. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## SpendRefusal Why a spend's preparation stopped short, writing nothing to any service. ```ts type SpendRefusal = | { readonly kind: "insufficient"; readonly available: bigint } | { readonly kind: "store-refused"; readonly refusal: WalletStoreRefusal } | { readonly kind: "not-registered"; readonly registration: Registration } | { readonly kind: "lease-refused"; readonly refusal: LeaseRefusal } | { readonly kind: "attempt-refused"; readonly refusal: BeginAttemptRefusal } | { readonly kind: "stale-handle" } | { readonly kind: "store-unavailable"; readonly detail: Readonly>; }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/flows/spend/types.ts`. ## SpendSettlement What following a relayed spend came to: - `landed`: it reached the commitment followed, and the note store was synced after, `sync` saying how that went; - `failed`: the relayer reported it rejected, expired or failed, with its failure code; - `pending`: the follow's status reads ran out first, with the last state read; a later sync still settles it; - `not-followed`: `follow: false` was passed. ```ts type SpendSettlement = | { readonly kind: "landed"; readonly signature: Uint8Array; readonly sync: NoteStoreSyncResult; } | { readonly kind: "failed"; readonly state: "rejected" | "expired" | "failed"; readonly failureCode: string; } | { readonly kind: "pending"; readonly state: string } | { readonly kind: "not-followed" }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## StrandedDepositRefundOptions How `refundStrandedDeposit` runs. | Property | Type | Description | | ---------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `onProgress?` | `((event: RefundStage) => void) \| undefined` | Called once per stage with what it produced; its throw is dropped. | | `closeTokenAccount?` | `boolean \| undefined` | Whether to close the platform wallet's emptied token account; `false` when omitted. | | `priorityFeeLamports?` | `bigint \| undefined` | The priority fee, in lamports for the whole transaction; `0n` when omitted. | | `transactionVersion?` | `TransactionVersion \| undefined` | `1`, one version 1 transaction, when omitted; `0` for a wallet that cannot sign version 1, which needs the deployment's lookup table. | | `commitment?` | `ConfirmationCommitment \| undefined` | What the flow waits for before it reports success; `confirmed` when omitted. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## StrandedDepositRefundResult What refunding a stranded deposit came to: - `nothing-held`: the platform wallet holds nothing of the mint and no lamports; `holdings` says what was read. Nothing was sent. - `declined`: the wallet declined to sign; nothing was sent. - `submitted`: the refund was sent; `submission` says where it ended up, and only `landed` returned the value. A `failed`, `expired` or `refused` refund may be run again. ```ts type StrandedDepositRefundResult = RefundOutcome; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## SyncedStage The stage a flow reports once it has synced the note store before it starts, with how the sync went. | Property | Type | Description | | -------- | --------------------- | ------------------------------------------------------------------------------------------- | | `stage` | `"synced"` | | | `sync` | `NoteStoreSyncResult` | How the sync went: `ingested` or `unregistered`, since an `unavailable` one stops the flow. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## SyncFirstOptions Whether a flow first syncs the note store, until it is caught up. | Property | Type | Description | | -------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sync?` | `boolean \| undefined` | `true`, the default: sync the note store until it is caught up before starting, so the flow sees every note and spend the indexer has; `false` when the caller has just synced. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## SyncNoteStoreOptions How much [`syncNoteStore`](https://sdk.nimbusprivacy.xyz/reference/reads#syncnotestore) reads, and what it reports as it goes. | Property | Type | Description | | ------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `maxRounds?` | `number \| undefined` | The most rounds it runs, each reading at most one page of every stream; no limit when omitted, so it runs until every stream is caught up. | | `signal?` | `AbortSignal \| undefined` | Stops the sync between rounds once aborted, rejecting with the signal's reason. | | `pageRows?` | `number \| undefined` | The rows asked for in each page, a whole number from 1 to 1000; 1000 when omitted. | | `onProgress?` | `((round: NoteStoreSyncRound) => void) \| undefined` | Called after each round; a throw is ignored. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/reads.ts`. ## TransactionSubmission Where a sent transaction ended up. Only `landed` applied it; `failed` charged its fee and applied nothing else; `expired` and `refused` never land from this send; `unknown` may still land until its last valid block height, so a caller waits it out before sending the same value again. ```ts type TransactionSubmission = | { readonly kind: "landed"; readonly signature: Uint8Array; readonly slot: bigint; } | { readonly kind: "failed"; readonly signature: Uint8Array; readonly slot: bigint; readonly error: string; } | { readonly kind: "expired"; readonly signature: Uint8Array } | { readonly kind: "refused"; readonly signature: Uint8Array; readonly detail: string; } | { readonly kind: "unknown"; readonly signature: Uint8Array; readonly lastValidBlockHeight: bigint; readonly detail: string; }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/flows/transaction.ts`. ## TransactionVersion The transaction format a flow compiles: version 1 (SIMD-0385), the default, or version 0, for a wallet that cannot sign version 1. ```ts type TransactionVersion = 0 | 1; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/flows/transaction.ts`. ## TransferContext The context `transfer` runs in. | Property | Type | Description | | --------- | ------------------- | ----------------------------------------------------------------- | | `flow` | `"transfer"` | | | `session` | `NimbusSession` | The opened session the flow runs for. | | `routing` | `NimbusFlowRouting` | The pool, committee nodes and guardian cluster the transfer uses. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/contexts.ts`. ## TransferOptions How `transfer` runs. | Property | Type | Description | | ------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `onProgress?` | `((event: TransferStage \| SyncedStage \| SettledStage) => void) \| undefined` | Called once per stage with what it produced; its throw is dropped. | | `sync?` | `boolean \| undefined` | `true`, the default: sync the note store until it is caught up before starting, so the flow sees every note and spend the indexer has; `false` when the caller has just synced. | | `follow?` | `boolean \| undefined` | `true`, the default: once the relayer admits the spend, follow it until it lands at `commitment`, then sync the note store, so the result says it landed and the spent notes are no longer reserved; `false`: return at the relayer's admission. | | `commitment?` | `"confirmed" \| "finalized" \| undefined` | What following the relay waits for: `confirmed` when omitted, or `finalized`. | | `fees?` | `"added" \| "deducted" \| undefined` | `deducted`, the default: the fees come out of `amount`; `added`: they are paid on top. | | `submitterFee?` | `bigint \| undefined` | The submitter's fee: the relayer's floor when omitted, or an amount offered in base units. The floor includes the relayer's published fee for creating a withdrawal's missing token account when it does not sponsor creating it, and shows in the `prepared` stage's `relayerFee`; an offered amount below what creating it costs is refused with `relay-unavailable`. | | `submitterFeeMint?` | `Address \| undefined` | The mint the submitter's fee is paid in; the spent mint when omitted. | | `zone?` | `NimbusZone \| undefined` | The zone the spent notes come from and the outputs go to; zone 0 when omitted. | | `mergeTimeout?` | `bigint \| undefined` | How many slots each merge may take to land and for its note to become spendable in the note store; 300 when omitted. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## TransferResult What transferring came to, each variant as [`SettledSpendOutcome`](https://sdk.nimbusprivacy.xyz/reference/types#settledspendoutcome) lists it; a `relayed` transfer's note is the recipient's once its settlement is `landed`. ```ts type TransferResult = SettledSpendOutcome; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## TransferStage One stage of a one-call transfer, as its `observe` hook is handed it; see `SpendStage`. Its `prepared` stage's `payee` is the recipient's wallet address. ```ts type TransferStage = SpendStage>; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/flows/transfer/types.ts`. ## WalletConnectionResult What asking a wallet to connect came to. - `connected`: the wallet connected, with an account on the SDK's cluster. - `no-account`: the wallet connected but offered no account on the SDK's cluster. - `declined`: the user refused to connect. ```ts type WalletConnectionResult = | { readonly kind: "connected"; readonly connected: ConnectedWallet } | { readonly kind: "no-account" } | { readonly kind: "declined" }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/wallets.ts`. ## WalletDiscoveryOptions Where the installed wallets are read from. | Property | Type | Description | | ---------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `wallets?` | `Pick \| undefined` | The Wallet Standard registry, as `@wallet-standard/app`'s `getWallets()` returns it, which it defaults to: every wallet an extension registers on the page. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/wallets.ts`. ## WalletStandardAccount The connected wallet's Solana account, as the Wallet Standard's `WalletAccount` describes it. | Property | Type | Description | | ----------- | ------------------------------------ | -------------------------------------------------------------------------------- | | `address` | `string` | The account's base58 address. | | `publicKey` | `Uint8Array` | The account's 32-byte Ed25519 public key. | | `chains` | ``readonly `${string}:${string}`[]`` | The CAIP-2 chains the account signs for, such as `solana:mainnet`. | | `features` | ``readonly `${string}:${string}`[]`` | The Wallet Standard features the account supports, such as `solana:signMessage`. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## WalletStandardSigner A Wallet Standard wallet's connected account and the two features the SDK asks it for, each the feature's own method: `solana:signMessage`'s `signMessage` and `solana:signTransaction`'s `signTransaction`. Opening a session asks only for `signMessage`, and only when the signature secret store holds no secret; registering and depositing ask for `signTransaction`. A wallet that cannot sign a version 1 transaction can still sign messages; the flows' version 0 route is for it. | Property | Type | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `account` | `WalletStandardAccount` | The connected account, which signs and pays. | | `signMessage` | `(...inputs: readonly { readonly account: WalletStandardAccount; readonly message: Uint8Array; }[]) => Promise` | Signs each message with the account, answering one output per input, in order. When the user declines it rejects with code `4001`, or a message saying the user rejected, denied or cancelled, which the SDK reads as declining rather than failing. | | `signTransaction` | ``(...inputs: readonly { readonly account: WalletStandardAccount; readonly transaction: Uint8Array; readonly chain: `${string}:${string}`; }[]) => Promise`` | Signs each serialized transaction with the account, answering one output per input, in order. Declining rejects as for `signMessage`. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/config.ts`. ## WalletStoreRefusal Why a flow's wallet store would not open for the account's identity: the store's own refusal, a handle gone stale while its header was read, `unbound` when no identity has been bound to it (registering binds one), or `other-identity` when another identity has. ```ts type WalletStoreRefusal = | Exclude, { readonly kind: "opened" }> | StaleHandle | { readonly kind: "unbound" } | { readonly kind: "other-identity" }; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/flows/wallet-store.ts`. ## WithdrawalContext The context `withdraw` runs in. | Property | Type | Description | | --------- | ------------------- | ------------------------------------------------------------------- | | `flow` | `"withdrawal"` | | | `session` | `NimbusSession` | The opened session the flow runs for. | | `routing` | `NimbusFlowRouting` | The pool, committee nodes and guardian cluster the withdrawal uses. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/contexts.ts`. ## WithdrawalResult What withdrawing came to, each variant as [`SettledSpendOutcome`](https://sdk.nimbusprivacy.xyz/reference/types#settledspendoutcome) lists it: `relayed` with how following it settled, `refused` before anything was asked of the committee, `relay-refused`, `unsent`, or `merge-failed` when the notes had to be merged first and a merge did not settle. ```ts type WithdrawalResult = SettledSpendOutcome; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## WithdrawOptions How `withdraw` runs. | Property | Type | Description | | ------------------------ | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `onProgress?` | `((event: WithdrawStage \| SyncedStage \| SettledStage) => void) \| undefined` | Called once per stage with what it produced; its throw is dropped. | | `recipientTokenAccount?` | `Address \| undefined` | The token account paid; the recipient's associated token account for the mint when omitted, which the relayer creates if missing. | | `sync?` | `boolean \| undefined` | `true`, the default: sync the note store until it is caught up before starting, so the flow sees every note and spend the indexer has; `false` when the caller has just synced. | | `follow?` | `boolean \| undefined` | `true`, the default: once the relayer admits the spend, follow it until it lands at `commitment`, then sync the note store, so the result says it landed and the spent notes are no longer reserved; `false`: return at the relayer's admission. | | `commitment?` | `"confirmed" \| "finalized" \| undefined` | What following the relay waits for: `confirmed` when omitted, or `finalized`. | | `fees?` | `"added" \| "deducted" \| undefined` | `deducted`, the default: the fees come out of `amount`; `added`: they are paid on top. | | `submitterFee?` | `bigint \| undefined` | The submitter's fee: the relayer's floor when omitted, or an amount offered in base units. The floor includes the relayer's published fee for creating a withdrawal's missing token account when it does not sponsor creating it, and shows in the `prepared` stage's `relayerFee`; an offered amount below what creating it costs is refused with `relay-unavailable`. | | `submitterFeeMint?` | `Address \| undefined` | The mint the submitter's fee is paid in; the spent mint when omitted. | | `zone?` | `NimbusZone \| undefined` | The zone the spent notes come from and the outputs go to; zone 0 when omitted. | | `mergeTimeout?` | `bigint \| undefined` | How many slots each merge may take to land and for its note to become spendable in the note store; 300 when omitted. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. ## WithdrawStage One stage of a one-call withdrawal, as its `observe` hook is handed it; see `SpendStage`. Its `prepared` stage's `payee` is the token account paid and, when the relayer creates it, its owner. ```ts type WithdrawStage = SpendStage< Extract >; ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana/src/flows/withdraw/types.ts`. ## WitnessGraphHandle One parsed witness graph, held by the witness-graph WASM until `free` releases it. | Property | Type | Description | | --------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `witness` | `(inputsJson: string) => Uint8Array` | Computes the witness, in `.wtns` binary form, for the circuit's inputs written as JSON with each field element a decimal string. Throws when an input is missing, extra or misshapen, or when the inputs do not satisfy the circuit. | | `free` | `() => void` | Releases the graph's WASM memory. | Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/prover-worker.ts`. Last updated 21 October 2026 # Wallets (/reference/wallets) These functions find Wallet Standard wallets and connect one. [Wallets](https://sdk.nimbusprivacy.xyz/helpers/wallets) walks through a wallet picker and a signer of your own. ## listWallets Use it to fill a wallet picker when your page loads. ```ts function listWallets( sdk: NimbusSdk, _args?: Readonly>, options?: WalletDiscoveryOptions, ): readonly Wallet[]; ``` Lists the installed wallets that can connect, sign messages and sign transactions on the SDK's cluster, the ones [`openWalletConnection`](https://sdk.nimbusprivacy.xyz/reference/wallets#openwalletconnection) accepts. **Parameters** | Parameter | Type | Description | | ---------- | --------------------------------- | --------------------------------------------------------------------------- | | `sdk` | `NimbusSdk` | The loaded SDK, whose deployment names the chain, such as `solana:mainnet`. | | `_args?` | `Readonly>` | Unused; kept so every function takes the same arguments. | | `options?` | `WalletDiscoveryOptions` | The registry to read; the page's Wallet Standard registry by default. | **Returns** `readonly Wallet[]` The wallets, in the order they registered; empty when none is installed. **Example** ```ts import { listWallets } from "@umbra/nimbus-solana-web"; import type { NimbusSdk } from "@umbra/nimbus-solana-web"; declare const sdk: NimbusSdk; for (const wallet of listWallets(sdk)) { console.log(wallet.name); } ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/wallets.ts`. ## followWallets Use it to keep the picker current while the page is open, as extensions load. ```ts function followWallets( sdk: NimbusSdk, args: { readonly onChange: (wallets: readonly Wallet[]) => void }, options?: WalletDiscoveryOptions, ): () => void; ``` Follows the installed wallets, calling `onChange` with [`listWallets`](https://sdk.nimbusprivacy.xyz/reference/wallets#listwallets)' answer whenever a wallet registers or unregisters, as an extension may after the page has loaded. **Parameters** | Parameter | Type | Description | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `sdk` | `NimbusSdk` | The loaded SDK, whose deployment names the chain. | | `args` | `{ /** Called with the wallets that can sign on the SDK's cluster, after each change only. */ readonly onChange: (wallets: readonly Wallet[]) => void; }` | `onChange`, called with the wallets that can sign on the SDK's cluster. | | `options?` | `WalletDiscoveryOptions` | The registry to follow; the page's Wallet Standard registry by default. | **Returns** `() => void` A function that stops following; calling it again does nothing. **Example** ```ts import { followWallets } from "@umbra/nimbus-solana-web"; import type { NimbusSdk } from "@umbra/nimbus-solana-web"; declare const sdk: NimbusSdk; const stop = followWallets(sdk, { onChange: (wallets) => { console.log(wallets.map((wallet) => wallet.name)); }, }); stop(); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/wallets.ts`. ## openWalletConnection Use it when the user picks a wallet. It gives you the signer a session takes. ```ts function openWalletConnection( sdk: NimbusSdk, args: { readonly wallet: Wallet }, ): Promise; ``` Asks a wallet to connect, and builds the signer for its first account on the SDK's cluster. The wallet shows the user its connection prompt, unless the page is already connected. The signer hands the wallet its own account object with every request, and signs for the chain the flows name, which is the deployment's. **Parameters** | Parameter | Type | Description | | --------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | `sdk` | `NimbusSdk` | The loaded SDK, whose deployment names the chain the account must sign on. | | `args` | `{ /** The wallet to connect, one {@link listWallets} listed. */ readonly wallet: Wallet; }` | The wallet to connect, one [`listWallets`](https://sdk.nimbusprivacy.xyz/reference/wallets#listwallets) listed. | **Returns** `Promise` `connected` with the signer `openNimbusSession` takes and the transaction version the wallet signs, `1` when it supports it, else `0`; `no-account` when the wallet offered no account on the SDK's cluster; `declined` when the user refused. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"wallet-unsupported"` when the wallet cannot connect, sign messages and sign transactions on the SDK's cluster, or fails while connecting. **Example** ```ts import { listWallets, openNimbusSession, openWalletConnection } from "@umbra/nimbus-solana-web"; import type { NimbusSdk } from "@umbra/nimbus-solana-web"; declare const sdk: NimbusSdk; const [wallet] = listWallets(sdk); if (wallet !== undefined) { const result = await openWalletConnection(sdk, { wallet }); if (result.kind === "connected") { await openNimbusSession(sdk, { connectedWallet: result.connected.signer }); } } ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/wallets.ts`. Last updated 21 October 2026 # Withdraw (/reference/withdraw) A withdrawal runs in a context made from the session. [Withdraw](https://sdk.nimbusprivacy.xyz/withdraw) walks through the flow. ## createWithdrawalContext Create one from the session before a withdrawal. It picks the pool the withdrawal spends from. ```ts function createWithdrawalContext( session: NimbusSession, options?: NimbusContextOptions, ): Promise; ``` Creates the context a session withdraws in. Without `options.poolIndex` the pools' accounts are read from the RPC once, and the highest open pool is chosen; the nodes and guardian cluster default to the cluster's preset. **Parameters** | Parameter | Type | Description | | ---------- | ---------------------- | ------------------------------------------------------------------------- | | `session` | `NimbusSession` | The opened session. | | `options?` | `NimbusContextOptions` | The pool, the screener and data endorser nodes, and the guardian cluster. | **Returns** `Promise` The context. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"misconfigured"` when an option is out of range or no pool is open; `"service-unavailable"` or `"unreadable"` when the RPC cannot be read. **Example** ```ts import { createWithdrawalContext } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; declare const session: NimbusSession; const context = await createWithdrawalContext(session); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/contexts.ts`. ## withdraw Use it to move tokens out of the private balance to any Solana address. ```ts function withdraw( context: WithdrawalContext, args: { readonly mint: Address; readonly amount: bigint; readonly recipient: Address; }, options?: WithdrawOptions, ): Promise; ``` Withdraws `amount` of `mint` from the session's notes in the context's pool to `recipient`'s token account, through the relayer. First syncs the note store until it is caught up, unless `sync: false`, and spends the largest notes the session may spend; the committee endorses, the spend is proved, and the relayer submits it. Unless `follow: false`, it then follows the relay until it lands at `commitment` and syncs again, so a `relayed` result's `settlement` says it landed and the spent notes are no longer reserved; with `follow: false` they stay reserved until a later sync sees them spent. When the largest three notes cannot cover the withdrawal but all of them can, less the relayer's fee for each merge, it first merges them, each merge a transfer to the session's own identity that it waits on, syncing the note store, until the merged note is spendable; `onProgress` sees each merge's `merge-prepared`, `merge-relayed` and `merge-settled` before the withdrawal's stages. **Parameters** | Parameter | Type | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `context` | `WithdrawalContext` | The withdrawal context. | | `args` | ``{ /** The mint withdrawn, of the session's notes. */ readonly mint: Address; /** * In base units of the mint: under `fees: "deducted"` what leaves the session's notes, every * fee taken from it; under `added`, what the token account receives. */ readonly amount: bigint; /** * The wallet address paid: its associated token account, unless `recipientTokenAccount` names * one. */ readonly recipient: Address; }`` | The mint, the amount in its base units, and the wallet address paid. | | `options?` | `WithdrawOptions` | The progress hook, which also sees `synced` and `settled`; whether to sync first and to follow the relay, and the commitment followed to; who pays the fees, the submitter's fee and its mint, the zone, the token account paid, and each merge's timeout in slots. | **Returns** `Promise` `relayed` with the relayer's admission and how following it settled, the payment known made once the settlement is `landed`; `refused` with a [`SpendRefusal`](https://sdk.nimbusprivacy.xyz/reference/types#spendrefusal) when nothing was asked of the committee, whose `insufficient` names the total of every note the session may spend in the pool, zone and mint; `relay-refused` when the relayer refused it; `unsent` when the note store would not mark it sent; or `merge-failed` with the merges completed and the [`MergeFailureCause`](https://sdk.nimbusprivacy.xyz/reference/types#mergefailurecause) of the next. **Throws** [`NimbusSdkError`](https://sdk.nimbusprivacy.xyz/reference/errors#nimbussdkerror) with reason `"misconfigured"` when an address is not base58, the mint does not exist, or no prover worker or committee endpoint was configured; `"service-unavailable"` or `"unreadable"` when a service or the RPC cannot answer, and `"unreadable"` too when the note store cannot be synced before it starts; `"unauthorized"` when a gateway service refuses the session's gateway token or the session has none; `"dependency-nonconforming"` when a proving key or the prover breaks its contract; `"account-unregistered"` when the context API answers that the session's identity or the connected wallet has no registration in the pool, before anything is relayed; [`NimbusSolanaFlowError`](https://sdk.nimbusprivacy.xyz/reference/types#nimbussolanaflowerror) when the protocol refuses the withdrawal. **Example** ```ts import { createWithdrawalContext, withdraw } from "@umbra/nimbus-solana-web"; import type { NimbusSession } from "@umbra/nimbus-solana-web"; import type { Address } from "@solana/kit"; declare const session: NimbusSession; declare const mint: Address; const result = await withdraw(await createWithdrawalContext(session), { amount: 500_000n, mint, recipient: session.walletAddress, }); ``` Generated from the TSDoc in `sdks/typescript/nimbus-solana-web/src/flows.ts`. Last updated 21 October 2026 # Register a user (/registration) Registering is a one-time Solana transaction. It creates Alice's identity, the public record of her private account, and links her wallet to it. Nobody can pay Alice privately until she registers. That includes deposits into her own private balance, from her wallet or anyone else's. In this guide, Alice checks whether she is registered, then registers from the wallet she connected. ## What's public Her platform wallet is a second Solana address the SDK derives from her signature. It signs her deposits alongside her wallet. Registering links the two wallets in public, so anyone can tell they belong to one private account. Registering reveals nothing about what Alice later holds or pays. [What each operation shows](https://docs.nimbusprivacy.xyz/privacy/visibility) explains each line. ## Check first `session.registration` says where Alice stood when the session opened. It never changes while the session is open. To read where she stands now, call `readRegistration`: ```ts import { readRegistration } from "@umbra/nimbus-solana-web"; if (session.registration.kind === "unregistered") { const standing = await readRegistration(session, {}); if (standing.kind === "unregistered") showRegisterButton(); } ``` - **`session.registration`** is current when the session opens. Use it to decide what to show first. - **`readRegistration`** asks Nimbus again. Use it after `register` lands, or when another tab may have registered. ## Register Create a registration context from the session, then call `register`. ```ts import { createRegistrationContext, register, } from "@umbra/nimbus-solana-web"; const context = await createRegistrationContext(session); const result = await register(context, {}, { transactionVersion }); ``` - **`{}`** is the arguments, and there are none. The session says who registers. - **`transactionVersion`** is what the wallet can sign. A registration fits one transaction at either version. See [Connect a wallet](https://sdk.nimbusprivacy.xyz/helpers/wallets). - **`createRegistrationContext`** picks the newest open pool. Pass `poolIndex` to choose another. First the SDK builds a proof on the device and collects Nimbus's approval. That takes a few seconds. Then Alice's wallet asks her to approve one transaction. That transaction registers her and links her wallet in one step. She pays its network fee from her wallet. Registering opens no new account on Solana, so there is no rent to pay. Her wallet needs a little SOL for that fee. The SDK checks her balance before it builds the proof. A wallet without enough throws at once, so she never waits for a proof that cannot be sent. ## Handle the result `register` returns a result instead of throwing for anything your user can cause. Check its `kind`: | `kind` | What happened | What to do | | --------------- | ----------------------------------------------------------------------- | ----------------------------------------- | | `complete` | Alice was registered already. Nothing was sent. | Carry on to her first deposit. | | `submitted` | The registration was sent to Solana. | Read where it ended up, below. | | `declined` | Alice rejected it in her wallet. Nothing was sent. | Let her try again. | | `store-refused` | This browser could not open her private account data. Nothing was sent. | Ask her to reload the page and try again. | A `submitted` result carries the transaction's fate in `result.submission.submission.kind`: | `kind` | What it means | | ---------------------- | ------------------------------------------------------------------------------------------------------------------- | | `landed` | Done. Alice is registered. | | `failed` | It reached Solana and failed. Only the network fee was charged. | | `expired` or `refused` | It never reached Solana. Nothing changed, and it is safe to try again. | | `unknown` | The SDK stopped waiting before Solana decided. See [A registration still settling](#a-registration-still-settling). | Here is the whole check in one function. TypeScript checks that it covers every case. ```ts function describeRegistration(result: RegistrationResult): string { switch (result.kind) { case "complete": return "You are registered."; case "submitted": switch (result.submission.submission.kind) { case "landed": return "You are registered."; case "failed": return "Registration failed on Solana. Only the network fee was charged."; case "expired": case "refused": return "Registration never reached Solana. Try again."; case "unknown": return "Registration is still pending."; } case "declined": return "You declined to register in your wallet."; case "store-refused": return "This browser could not open your account data. Reload and try again."; } } ``` Alice is registered when the result is `complete`, or `submitted` with a `landed` submission. She can deposit straight away. ## Show progress Pass `onProgress` to tell Alice what is happening: ```ts import { register } from "@umbra/nimbus-solana-web"; await register(context, {}, { onProgress: (event) => showStatus(event.stage) }); ``` The stages are `prepared`, `endorsed`, `proved`, `signed`, `sent` and `confirmed`. `endorsed` and `proved` can arrive in either order. A registration with nothing left to send stops after `prepared`. [Show progress](https://sdk.nimbusprivacy.xyz/api-design/progress) has a message for each stage. ## Edge cases ### Already registered `register` checks first and sends only what is missing. If Alice is registered and her wallet is linked, it returns `complete`. Her wallet never asks her to sign. ### A registration still settling A submission of kind `unknown` may still land until its blockhash expires. Do not call `register` again straight away. Wait a minute, then call `readRegistration`. If it says `unregistered`, try again. A landed registration lets Alice deposit at once. Spending takes a little longer. Her private balance can pay only once the registration is final on Solana and her device has synced. Until then, a Private Send or a withdrawal is refused with `not-registered`. ### Two tabs registering at once `register` holds a lock for Alice's wallet for the whole call. The lock is shared by every tab of your site in one browser. A second tab waits for the first to finish, then checks again. If the first tab's registration landed and Nimbus has seen it, the second returns `complete`. ## Common errors These are thrown, not returned, because your app usually cannot fix them by asking the user. | Error | Reason | Cause | | ----------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------- | | `NimbusSolanaFlowError` | `binding-mismatched` | Alice's wallet is already linked to a different platform wallet, such as one from another `accountIndex`. | | `NimbusSolanaFlowError` | `standing-mismatched` | A Nimbus service answered for another pool or a stale state. Try again later. | | `NimbusSolanaFlowError` | `fee-payer-unfunded` | Alice's wallet holds too little SOL for the network fee. Nothing was proved or sent. Ask her to add some. | | `NimbusSolanaFlowError` | `transaction-unsendable` | The transaction failed its check before sending. Nothing was signed. Try again. | | `NimbusSolanaFlowError` | `transaction-altered` | The wallet changed the transaction it was asked to sign. Nothing was sent. | | `NimbusSdkError` | `service-unavailable` | Solana or a Nimbus service could not be reached. Try again. | | `NimbusSdkError` | `unauthorized` | The session has no gateway token, or the gateway refused it. | | `NimbusSdkError` | `wallet-unsupported` | The wallet failed while signing. | [Handle errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors) shows one function that covers every error the SDK throws. ## Options | Option | Default | What it does | | --------------------- | ------------- | ----------------------------------------------------------------- | | `transactionVersion` | `1` | The transaction version the wallet signs. | | `onProgress` | none | Called at each stage of the registration. | | `commitment` | `"confirmed"` | `"finalized"` waits until the registration cannot be rolled back. | | `priorityFeeLamports` | `0n` | A priority fee, in lamports, to land faster when Solana is busy. | The [API reference](https://sdk.nimbusprivacy.xyz/reference/registration#register) lists every option. ## Next - [Deposit](https://sdk.nimbusprivacy.xyz/deposit): Alice's first payment into her private balance. - [Open a session](https://sdk.nimbusprivacy.xyz/helpers/sessions): Where session.registration comes from. - [Handle errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors): Cover every result and error in one place. Last updated 21 October 2026 # Use these docs with AI tools (/resources/ai-tools) Every page of this site is also served as Markdown, so an AI assistant can read it as plain text. Point your assistant at the page you are working from, or at the whole site. ## One page Each page has two buttons under its title: - **Copy Markdown** copies the page as Markdown, ready to paste into a chat. - **Open** shows the Markdown, or opens the page in ChatGPT, Claude, Cursor or Scira AI. Any page's Markdown is also at its URL with `.md` added. For example, the [Quickstart](https://sdk.nimbusprivacy.xyz/getting-started/quickstart) is at `https://sdk.nimbusprivacy.xyz/getting-started/quickstart.md`. The home page is at `/index.md`. A request that asks for `text/markdown` in its `Accept` header gets the Markdown at the page's usual URL. Every page also names its Markdown copy in a `Link` header. ## The whole site Two files describe the whole site: | File | What it holds | | --------------------------------------------------------------- | ------------------------------------------------------------------------- | | [`/llms.txt`](https://sdk.nimbusprivacy.xyz/llms.txt) | An index: every page, with a link to its Markdown and a one-line summary. | | [`/llms-full.txt`](https://sdk.nimbusprivacy.xyz/llms-full.txt) | The full text of every page, in one Markdown file. | Give your assistant `llms.txt` when it can fetch pages itself, and it reads only what it needs. Give it `llms-full.txt` when it cannot fetch, or when you want everything in one place. ## Point an assistant at the docs Add a line like this to your project's instructions for your assistant, such as `AGENTS.md` or `CLAUDE.md`: ```text For @umbra/nimbus-solana-web, read https://sdk.nimbusprivacy.xyz/llms.txt first. Fetch a page's Markdown by adding .md to its URL. ``` The code on every page compiles against the SDK when the site is built. So an assistant that copies an example gets code that matched the SDK on that day. ## Next - [Quickstart](https://sdk.nimbusprivacy.xyz/getting-started/quickstart): Deposit, pay and withdraw in one walkthrough. - [Types](https://sdk.nimbusprivacy.xyz/reference/types): Every type the SDK exports. Last updated 21 October 2026 # Changelog (/resources/changelog) The SDK has no released version yet, so it has no `CHANGELOG.md`. This page lists every change waiting for the next version, newest first. It is built from the repository's changesets when the site is built. Each entry carries a label: | Label | What it means for your app | | ------------ | -------------------------------------------------- | | **Breaking** | You may need to change your code when you upgrade. | | **Added** | Something new, and nothing you use changes. | | **Fixed** | A bug fix. | Older entries name functions as they were then. A later entry may have renamed them. ### minor: nimbus-solana-fee-payer-unfunded (21 October 2026) A flow the connected wallet pays for refuses a wallet that cannot pay before any work: a new `NimbusSolanaFlowErrorReason`, `fee-payer-unfunded`, is thrown while `register`, `deposit` (both transaction versions) and `refundStrandedDeposit` are prepared, before any proving key is downloaded, any proof made or the committee and data endorser asked. It is raised when the wallet has no account or holds fewer lamports than the transaction takes from it plus the rent-exempt minimum it keeps; its `detail` carries `requiredLamports` and `heldLamports` as decimal strings. The wallet is read with the accounts each flow reads already. `prepareRegistration` and `prepareDepositRefund` take the priority fee in their options, and `RegisterDeps` gains `fetchSolanaAccounts`. Relayed withdrawals and transfers are unchanged: the relayer pays for them. ### patch: nimbus-solana-relay-refused (21 October 2026) A withdrawal, transfer or merge the relayer refuses (`relay-refused`) gives its notes back at once. The relayer's definitive refusal, and a committee that withheld its signatures so nothing was posted, are reported to the note store as `relay-refused`, which ends the attempt, so the abandon that follows frees the notes and restores the balance instead of holding them until the proof window closes. A lost answer, or a withheld outcome whose answers do sum, still holds them. ### patch: nimbus-solana-web-coordinator-batches (21 October 2026) The reads and committee requests a flow makes together through the process coordinator now share one `prepare` and one `attest`, as the earlier SDK's did, instead of one call per item: a deposit makes 3 coordinator calls where it made 11, and a relayed spend 3 where it made 7. Busy items are still re-sent alone, an item's refusal reaches only its caller, and two flows never share a batch. ### patch: nimbus-solana-web-coordinator-busy-items (21 October 2026) On the coordinator route, a read or committee request an upstream answered busy (a status the service clients retry, or no answer at all) is sent again on its own, up to three sends in all, pausing 500 ms and then 1000 ms through the platform's `waitFor`. One 429 or 503 from a guardian, the screener, the data endorser, the fee enclave, the context API or the relayer no longer fails a deposit or spend before anything is sent; an item still busy after the third send fails as before. ### patch: nimbus-solana-web-direct-relay-policy (21 October 2026) On the coordinator route the relayer's policy is read from the relayer itself, as the earlier SDK read it, so a relayed spend makes one coordinator prepare again instead of two. ### patch: nimbus-solana-web-mainnet-lookup-table (21 October 2026) The mainnet preset names DE3Y2HM8CrwQfb7nTpuuMFeYeXboBnKUDDrMS4RmJLgY as the deposit lookup table, the one mainnet-v2 deposits compile against, which holds every address of the table it named before and 49 more, uUSDv's and kVUSDC's among them. ### patch: nimbus-solana-default-creation-fee (21 October 2026) A withdrawal to an address whose token account does not exist yet, through a relayer that accepts a zero fee but does not sponsor creating the account, pays the relayer's published floor and destination-creation fee in the fee's mint by default, instead of refusing with `relay-unavailable`. The fee is the prepared stage's `relayerFee` and is settled as every relayer fee is: out of the amount under `deduct`, on top of it under `add`. Only the default floor changes: an offered fee below what creating the account costs is still refused, and a relayer that publishes no payment or creation fee in the mint, or no tip accrual for it, still refuses. `@umbra/nimbus-solana-web`'s `submitterFee` option documents the same. ### minor: nimbus-web-unregistered-party (21 October 2026) A deposit or a transfer to a wallet that has not registered throws `NimbusSdkError` with the new reason `recipient-unregistered`, its message carrying the context API's detail, before the wallet is asked to sign or anything is sent. It was `service-unavailable`, which an application could tell from an unreachable service only by its message. When the context API holds no registration of the session's own identity, or for a withdrawal of the connected wallet, the reason is the new `account-unregistered`. Both route the same way directly and through the coordinator. `@umbra/service-client-core`'s `refuseStatus` keeps an RFC 9457 problem body on the `ClientError` it throws, as `problem`: the answer's HTTP status, the problem's `type`, and its `code` and `detail` when present. The new `ServiceProblem` type describes it. The reason and message are unchanged. ### patch: nimbus-solana-web-tsdoc-corrections (21 October 2026) Correct two TSDoc entries. `unsupported-browser` also names `navigator.storage` and `IDBKeyRange`, which `loadNimbusSdk` checks for. `ReservedSpend.kind` says what a merge and an escape are. Docs only: no behaviour changes. ### patch: nimbus-solana-web-misconfigured-arguments (21 October 2026) A bad argument is refused as `NimbusSdkError` with reason `misconfigured`, before anything is fetched or read, so `isNimbusSdkError` recognises every caller's mistake: - `resolvePendingDeposit` refuses a submission whose outcome is not `unknown`. It threw `@umbra/nimbus-primitives`' `ProtocolError`, which neither `isNimbusSdkError` nor `isNimbusSolanaFlowError` recognised. - `findMintByIdentifier` refuses an identifier shorter than 32 bytes. It threw a `RangeError`. - `readHistory` refuses a `limit` that is not a whole number from 1 to 100. It threw `unreadable`, and for a session whose store held nothing yet it returned an empty page instead. - `syncNoteStore` refuses a `pageRows` that is not a whole number from 1 to 1000. It threw the note store's `RangeError`, and only for a registered identity; an unregistered one returned `unregistered`. ### minor: nimbus-solana-web-reserved-deposits (21 October 2026) `listReservedSpends` lists a deposit that holds the notes it merges, as `kind: "deposit"`, and `releaseReservedNotes` and the release that ends every `syncNoteStore` release it as they release a spend. Before, `readBalances` counted such a deposit's notes as `reserved` while neither listed nor released them, so a stopped deposit's merged notes stayed held and the application had no call that freed them. A deposit that merges no notes holds none and is still not listed. `ReservedSpend.kind` gains the variant `"deposit"`. An application whose `switch` over it is exhaustive, or which types a table as `Record`, stops compiling until it handles a deposit. A deposit released while its transaction may still land keeps its notes until that transaction lands or can no longer land. `resolvePendingDeposit` still settles it, and a `failed` or `expired` resolution then answers `intent: "settled"`, the release having stopped it already. ### minor: nimbus-solana-web-transaction-types (21 October 2026) The package exports the transaction types its public types name, so an application no longer derives them or imports them from `@umbra/nimbus-solana`: - `TransactionSubmission`, where a transaction the wallet sent ended up, which a `submitted` registration, deposit or refund result carries. - `TransactionVersion`, the `transactionVersion` the flows' options and `ConnectedWallet` name. - `ConfirmationCommitment`, the `commitment` the flows' options name. All three are type-only exports. ### patch: nimbus-solana-web-tsdoc (21 October 2026) Document every public function and type in full: each field of the configuration, contexts, options, results and stages, each inline argument, each `NimbusSdkErrorReason` with what an application does about it, and each function's return values and failures, including the `misconfigured` refusals of `syncNoteStore`'s `maxRounds` and the `RangeError` its `pageRows` throws. Comments only; no behaviour changes. ### minor: nimbus-solana-web-app-types (21 October 2026) The package exports the types an application names, so it no longer imports them from the packages below it. - The flows' progress stages: `RegisterStage`, `DepositStage`, `WithdrawStage`, `TransferStage` and `RefundStage`. - `DepositSubmission`, which `resolvePendingDeposit` takes. - `SpendRefusal` and the refusals a spend result names: `MergeFailureCause`, `WalletStoreRefusal`, `LeaseRefusal`, `BeginAttemptRefusal` and `MarkAttemptSentRefusal`. - `HistoryPosition`, the type of `readHistory`'s `after`. - The wallet types: `ConnectedWallet`, `WalletConnectionResult`, `WalletDiscoveryOptions` and the Wallet Standard's `Wallet`. - `isNimbusSolanaFlowError`, with `NimbusSolanaFlowError` and `NimbusSolanaFlowErrorReason`, to recognise the protocol's own refusals a flow throws. ### minor: nimbus-solana-web-asset-integrity (21 October 2026) The SDK checks the WebAssembly and scripts it ships before it runs them, and the primitives before it uses them. - `build:assets`, which `prepack` runs after `build`, pins every file it writes to `assets/` by its SHA-256: in the built `dist/asset-digests.js` for the main thread, and in `prover-worker.js` for the files the worker loads. A packed package carries the digests of exactly the assets packed with it. - `loadNimbusSdk` hashes the primitives' module before instantiating it, and the default prover worker's script before starting it, refusing a file that is not the pinned one as `dependency-nonconforming`, naming the file and both digests. The shipped worker checks each `.wasm` on the bytes it instantiates and each glue or helper script before importing it, answering `failed` with reason `dependency-nonconforming`. Scripts are imported by URL after their check, so a server that answers the check and the import with different bytes is not caught; a mismatched build or a corrupted file is. - `loadNimbusSdk` runs `@umbra/nimbus-primitives/conformance`'s known-answer checks on the loaded primitives, refusing a failure as `dependency-nonconforming` naming the primitive and its check. - `settings.assetPinning` is `pinned` in a packed package, and `unpinned` when it runs from source, where no file is checked. - `NimbusPlatform.computeSha256Digest` is Web Crypto's SHA-256 by default, and `platform.startProverWorker` may resolve to its worker. ### minor: nimbus-solana-web-wallets (21 October 2026) The SDK finds and connects Wallet Standard wallets itself, so an application no longer writes its own discovery and signer. - `listWallets(sdk)` lists the installed wallets that can connect, sign messages and sign transactions on the SDK's cluster chain (`solana:mainnet` or `solana:devnet`). - `followWallets(sdk, { onChange })` calls `onChange` with that list whenever a wallet registers or unregisters, and returns the function that stops following. - `openWalletConnection(sdk, { wallet })` asks the wallet to connect and answers `connected` with the `WalletStandardSigner` `openNimbusSession` takes and the transaction version the wallet signs (`1` when it supports it, else `0`), `no-account` when it offers no account on the cluster, or `declined` when the user refuses; a wallet that cannot sign on the cluster, or fails to connect, is refused as `wallet-unsupported`. - Each takes `options.wallets`, the registry to read, which defaults to `@wallet-standard/app`'s `getWallets()`. ### major: nimbus-solana-web-shipped-assets (21 October 2026) The package ships every browser file it runs, so an application copies one folder, serves the page cross-origin isolated, and loads with `loadNimbusSdk({ cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/" })`. - `assets/` holds the primitives' module, the umbra prover's and the witness graph's WASM packages, and `prover-worker.js`, a self-contained module worker that loads both WASM modules beside itself and answers with `proveWorkerRequest`. `nimbus-solana-web copy-assets ` copies it into the directory the application serves. - `LoadNimbusSdkOptions.assetsBaseUrl` names that directory, ending with `/`. The primitives' module is fetched from it unless `endpoints.primitivesWasm` is given, and each proof runs in a module worker started from its `prover-worker.js` unless `platform.startProverWorker` is given. A value without the trailing slash, or one that is not a URL, is refused as `misconfigured`. - `loadNimbusSdk` refuses a page that is not cross-origin isolated as `unsupported-browser`, naming `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`: the prover's WebAssembly declares shared memory, which a browser creates on no other page. `settings.proving.allowSingleThreaded` is `false`, and `threads` the core count. ### major: nimbus-solana-web-mainnet-out-of-the-box (20 October 2026) Mainnet works out of the box: `loadNimbusSdk({ cluster: "mainnet" }, …)` needs only `endpoints.primitivesWasm`, which the application serves, and a prover worker. - The mainnet preset configures every endpoint the production web app reaches: the Solana RPC, the context and data APIs on their own hosts, and the relayer, process coordinator, screener node 0, data endorser node 0, fee enclave and guardian nodes 1 to 3 behind `gateway.api-v2.umbraprivacy.com`. Its proving keys are the mainnet-v2 asset host's `zk/` files, pinned by the web app's manifest, with a 2-of-3 statement for registration and the transaction circuit and a three-input statement for the absence circuit. `primitivesWasm` stays unconfigured on every preset, and the refusal says the application serves it. - `openNimbusSession` takes a `gatewayToken` option, a bearer token string or an issuer `() => Promise` asked once per gateway request. The session holds it as `session.gatewayToken` (`NimbusGatewayToken`, a string in a `Secret`), and `session.sdk` is the loaded SDK with its `fetch` sending `Authorization: Bearer` to the origins of `settings.gatewayEndpoints` and nowhere else. `closeNimbusSession` drops a held token. A string that is not an RFC 6750 bearer token is refused as `misconfigured`. - `NimbusSdkErrorReason` gains `unauthorized`: a gateway service answered 401 or 403, or the issuer gave no usable token. The message names the service, never the token. Either status now fails at once: a 403 is no longer retried, and a 401 no longer read as the service's refusal. - `NimbusSettings` gains `gatewayEndpoints` (`NimbusGatewayEndpoint[]`): every configured endpoint the preset marks gateway-routed, the coordinator only on the `coordinator` route. Mainnet marks all six gateway services, and devnet the guardians and the screener, as each stack's gateway routes them. - Cached circuit artifacts are keyed by the digest the manifest pins, so a key rotated under the same constraint system is downloaded afresh, where the cached old key would fail every proof. Artifacts cached before this release are downloaded once more. ### major: nimbus-solana-web-merge-notes (20 October 2026) `withdraw` and `transfer` merge the session's notes first when the largest three cannot cover the payment, as often as the payment needs, as `@umbra/nimbus-solana`'s one-call flows now do. They take `mergeTimeout?` (slots for each merge, 300 when omitted), hand `onProgress` the merge stages (`merge-prepared`, `merge-relayed`, `merge-settled`) before the payment's, and `WithdrawalResult` and `TransferResult` gain `merge-failed`. A refusal `insufficient` names the total of every note the session may spend in the pool, zone and mint. The RPC filler gains `fetchFinalizedSlotAfter`, polling at the preset's interval. ### major: nimbus-solana-web-session-names (20 October 2026) Renames the public API to sessions and signature secrets. Behaviour, stored bytes and key derivation are unchanged, and the default `sessionStorage` key prefix stays `umbra/nimbus-solana-web/account-secret/`, so secrets already stored still open without a signature. - `openNimbusAccount(sdk, { wallet }, { secretStore?, accountIndex? })` → `openNimbusSession(sdk, { connectedWallet }, { signatureSecretStore?, accountIndex? })` - `OpenNimbusAccountOptions` → `OpenNimbusSessionOptions` - `OpenNimbusAccountResult` (`{ kind: "opened", account }`) → `OpenNimbusSessionResult` (`{ kind: "opened", session }`) - `closeNimbusAccount(account)` → `closeNimbusSession(session)` - `NimbusAccount` → `NimbusSession`, whose `session` field (the `WalletSession`) is now `walletSession` - `createSessionStorageAccountSecretStore` → `createTabSignatureSecretStore` - `createLocalStorageAccountSecretStore` → `createPersistentSignatureSecretStore` - `createMemoryAccountSecretStore` → `createMemorySignatureSecretStore` - `AccountSecretStore` (`readAccountSecret`, `writeAccountSecret`, `wipeAccountSecret`) → `SignatureSecretStore` (`readSignatureSecret`, `writeSignatureSecret`, `wipeSignatureSecret`) - `AccountSecretKey` → `SignatureSecretKey`, `AccountSecretReading` → `SignatureSecretReading`, `AccountSecretStoreOptions` → `SignatureSecretStoreOptions` - the stores' `accountSecret` field, in a `found` reading and in `writeSignatureSecret`'s arguments → `signatureSecret` - `ingestAccountActivity(account, …)` → `syncNoteStore(session, …)` - `AccountActivityResult` → `NoteStoreSyncResult`, `IngestAccountActivityOptions` → `SyncNoteStoreOptions`, `AccountActivityRound` → `NoteStoreSyncRound` - `readAccountBalances(account, …)` → `readBalances(session, …)`, `AccountBalance` → `Balance` - `readAccountHistory(account, …)` → `readHistory(session, …)`, `AccountHistoryPage` → `HistoryPage`, re-exported from `@umbra/nimbus-storage-solana/record-store` - `refund(ctx, { mint }, options?)` → `refundStrandedDeposit(ctx, { mint }, options?)`, `RefundOptions` → `StrandedDepositRefundOptions`, `RefundResult` → `StrandedDepositRefundResult` - `resolveDeposit(ctx, { submission })` → `resolvePendingDeposit(ctx, { submission })`, `DepositResolutionResult` → `PendingDepositResolutionResult` - the contexts' `account` field → `session`, and `createRegistrationContext`, `createDepositContext`, `createWithdrawalContext` and `createTransferContext` take the session ### patch: nimbus-solana-web-mainnet-deployment (20 October 2026) Mainnet's preset derives keys under the deployment `mainnet`, as the web app does, not the `mainnet-v2` stack name: under `mainnet-v2` every existing mainnet account would have opened as a new, empty one. A test pins the web app's own derivation vectors for both clusters. ### major: nimbus-solana-web-flows (20 October 2026) Adds the contexts (`createRegistrationContext`, `createDepositContext`, `createWithdrawalContext`, `createTransferContext`), the flows with every dependency filled (`register`, `deposit`, `withdraw`, `transfer`, `refund`, `resolveDeposit`) and the account reads (`ingestAccountActivity`, which also takes in each pool's registrations a spend needs, `readAccountBalances`, `readAccountHistory`). Addresses and mints are base58 `Address`es; a zone travels as `zone: { id, signer }`. ### patch: nimbus-solana-routing-context (20 October 2026) Every flow's `ctx` is `{ deployment, session, store, routing }`, and `args` holds only the payment. - `routing` (`FlowRouting`) carries `account`, `poolIndex`, `screenerNodeIndex`, `dataEndorserNodeIndex` and `clusterIdentifier`; a registration takes `RegisterRouting`, the same without the screener. `clusterIdentifier` leaves the deposit's and spends' `ctx` top level and the registration's `args` for it. - `store` is `{ backend, choices }` (`FlowWalletStore`) everywhere. Withdrawals and transfers no longer take an opened `wallet`: they open the store bound to the account's identity, as a deposit does, refuse with `store-refused` (`WalletStoreRefusal`) when it will not open, and carry the opened store in `PendingSpend`, so `relaySpend`, `relayEndorsedSpend` and `abandonSpend` no longer read the store from `ctx`. - `args`: deposit `{ mint, amount, recipient }`, withdraw `{ mint, amount, recipient }` (the recipient is the token account and owner `WithdrawDestination` was, now `WithdrawRecipient`), transfer `{ mint, amount, recipient }`, register `{}`, refund `{ mint }`, and `resolveDeposit` `{ submission }`. `destination` is `recipient` throughout, the deposit stage and the context port's `DepositContextRequest` included; `SpendSource` is `SpendPayment`. - The zone is a flow option: `opts.zone` is `{ kind: "zone-0" }` or `{ kind: "zone"; id; signer }`, `id` from 1 to 2^20 - 1 and `signer` a `ZoneSignerPort`, in the one-call deposit, withdrawal and transfer and in `prepareDeposit`, `prepareWithdrawal` and `prepareTransfer`. `signZoneAuthorisation` leaves `NimbusSolanaDeps`; the defaults spend in zone 0. - A deposit pays a registered identity only: `DepositDestination` and its `program` kind are gone, and a recipient the context service resolves with no registration path is refused as `context-mismatched`. The wallet's balances and history show the notes the identity owns. ### major: nimbus-solana-web-unregistered-store (19 October 2026) `openNimbusAccount` no longer gives an unregistered identity's note store a birthday, which made the store refuse the one `register` sets, and keeps the birthday a registered identity's store already has. `NimbusAccount.noteStore` is removed; the store is opened where it is read. ### major: nimbus-solana-web-load-and-accounts (19 October 2026) Replaces `loadNimbusSolanaWeb`, its configuration and platform types, `NimbusSolanaWebError`, `openNimbusSolanaWebBackend`, `KIT_SOLANA_FORMATS` and the `./coordinated` entry with `loadNimbusSdk({ cluster }, options?)`, which reads a bundled per-cluster preset completed by its options (`route: "coordinator"` replaces `./coordinated`), `openNimbusAccount` and `closeNimbusAccount`, three account secret stores (session storage by default, local storage and memory), and `NimbusSdkError` with `isNimbusSdkError`. The wallet is no longer part of loading. ### minor: no-sdk-reaches-for-global-fetch (19 October 2026) No SDK package reaches for the global `fetch`. `@umbra/circuit-artifacts`: `FetchDeps.fetch` is required, and `fetchManifest`, the encodings' `download` and the resolver no longer fall back to the global. `deps` stays after `opts` and is required: `fetchManifest(args, opts, { fetch })`, `download(source, args, opts, { fetch })`. `ResolveDeps` extends `FetchDeps`, and the resolver hands `download` that `fetch`, so an encoding module still passes as `EncodingDeps` whole: `{ ...plain, fetch, ...store }`. `@umbra/nimbus-robinhood`: `readArtifactManifest` takes required deps with `fetch`, and `createArtifactResolver`'s deps carry the `fetch` its downloads go through. `@umbra/nimbus-solana-web`: Kit's Solana RPC runs on a transport over the platform's `fetch` (Kit's own HTTP transport calls the global), so every RPC request, like every service request, goes through the platform's `fetch`. The transport keeps Kit's bigint-preserving JSON and its HTTP error; it does not coalesce identical requests in flight together, as Kit's default does. `@umbra/solana-indexer-client`: the injected `fetch` is called with no receiver, so a browser's own `fetch` can be passed as `{ fetch }`. ### patch: inject-fetch-service-clients (19 October 2026) Every service client takes `fetch` as an injected dependency, with no default: nothing reaches for the global `fetch`. `@umbra/service-client-core` exports `ServiceDeps` (`{ fetch }`), and its `request`, `requestJson`, `requestBytes` and `isLive` take `deps` before their optional `opts`: `(ctx, args, deps, opts?)`. A call that was `requestJson(ctx, args, opts)` becomes `requestJson(ctx, args, { fetch }, opts)`. The injected `fetch` is called with no receiver, so a browser's own `fetch` can be passed as `{ fetch }`. In each client, every function that reaches its service takes the same deps, in the same position: - A function whose deps slot was empty now takes `deps: Pick` before its opts, and an optional `_args` becomes a required `{}`. For example, `readIdentity(ctx, {}, { fetch })`. - `GuardianDeps` and `FeeEnclaveDeps` gain `fetch`, so the endorse calls, the committee helpers and `provideFeeClaim` keep their positions and take it with their seal primitives. The functions that only seal (`buildProcessEndorsement`, `sealFeeClaim`) take the primitives and the random source without it. - Factory-made clients (`createRelayClient`, `createDepositDataClient`, `createSpendDataClient`) take it in their closures' existing deps slot, which was typed `unknown` and ignored. The directories whose closure takes one args object (`createRegistrationDirectory`, `createOwnerDirectory`) take it as the closure's second parameter. - note-sync's `DownloadDeps` gains `fetch`, and its readers (`fetchManifest`, `fetchChunk`, `fetchChunkHeader`, `fetchChunkRange`, the page and record reads) take it before their opts. `nimbus-robinhood`'s `FlowDeps` gains `fetch`, and `createFlowDeps` takes it beside the signer, the prover and the storage; `createRpc`, `createAuthorisers`, `createCoordinatedAuthorisers`, the JSON-RPC, indexer and relayer readers take it in their deps. `nimbus-solana-web` passes the platform's `fetch` to every client, direct or through the coordinator, and `robinhood-pool-cli` passes the platform's own at each call, as an entry point does. ### minor: nimbus-solana-web-zone-signer (19 October 2026) The deps fill `nimbus-solana`'s new `signZoneAuthorisation` with a signer that refuses every zone, naming it and the side: no zone signer service exists, zone 0 never asks one, and a caller using another zone replaces the member. ### patch: nimbus-solana-web-context-leaf (19 October 2026) The context filler takes a registered standing's leaf from `@umbra/solana-context-client`'s `leaf`, which is `unassigned` exactly when the context API serves no index, instead of inferring it from `provable`. Deposit and spend contexts the API serves now fill the port: the client parses each path's leaf index and shape and each gap's value and shape, which the filler refused as unreadable while the client cast them. ### patch: nimbus-solana-web-wallet-entry (19 October 2026) Imports the wallet's types from `@umbra/nimbus-solana/wallet`, the entry `./wallet-store` became. ### minor: nimbus-solana-web (19 October 2026) Add the browser SDK: `loadNimbusSolanaWeb(config, platform)` loads the primitives' WASM module and returns the decoded deployment and one deps object that fills every member `@umbra/nimbus-solana`'s flows and wallet call. Crypto comes from `@umbra/nimbus-primitives-wasm`; Anza's formats and the instruction builders from `@solana/kit` and `@solana-program/*` (`KIT_SOLANA_FORMATS`); the RPC port, the platform wallet binding and the sync's pool reading from Kit's RPC; the wallet signer from the Wallet Standard's `solana:signMessage` and `solana:signTransaction`; the lock from Web Locks; storage from the IndexedDB backend `openNimbusSolanaWebBackend` opens under `buildNoteStoreBackendName`; the indexer feeds from `@umbra/solana-indexer-client`; every service from its client; proving keys from `@umbra/circuit-artifacts`, cached through `@umbra/store-web` and checked against the one verifying key the selection names: the configured statement, the manifest's verifying key, and its body in the deployed program's ELF; and proofs from `@umbra/prover`'s umbra backend in a worker that runs `./prover-worker`'s `proveWorkerRequest`. `./coordinated`'s `loadCoordinatedNimbusSolanaWeb` reaches a transaction's reads, committee and relay through the process coordinator. Configuration is data; nothing is bundled. Last updated 21 October 2026 # Don't confuse (/resources/dont-confuse) ## Private balance and wallet balance The **wallet balance** is what Alice's Solana wallet holds. Anyone can read it on chain. The **private balance** is what she holds inside Nimbus. Only her device can read it, after a sync. A deposit moves tokens from the first to the second, and a withdrawal moves them back. ## The account signature and a transaction signature The **account signature** is the one message Alice's wallet signs to open her private account. It never goes on chain, and it moves no money. Every key of her private account comes from it, so treat it as a secret. A **transaction signature** approves one Solana transaction, such as a deposit. It is public once the transaction lands. ## Reserved and pending **Reserved** funds are held by a payment Alice started that has not settled yet. They come back once the payment can no longer land. Releasing a payment that never sent anything frees them at once. **Pending** funds were paid to Alice, and are not spendable yet. A new deposit reads as pending until Solana finalizes it. Both are part of her private balance, and neither can be spent right now. ## Confirmed and finalized **Confirmed** means a supermajority of Solana's stake has voted for the block. It is fast, and very rarely rolled back. The SDK waits for it by default. **Finalized** means the block can no longer be rolled back. It takes several seconds longer. Pass `commitment: "finalized"` to a payment to wait for it. ## Registering and opening a session **Registering** is a Solana transaction. Each wallet does it once, ever, before its first deposit. The wallet pays its network fee. **Opening a session** signs a message on this device. It happens each time the user signs in, unless the signature is remembered. Nothing goes on chain, and it costs nothing. ## Private Send and a deposit to someone else A **Private Send** pays Bob from Alice's private balance. A relayer submits it. The chain shows neither Alice, nor Bob, nor the amount. A **deposit to someone else** pays Bob from Alice's wallet. The chain shows Alice's wallet, the token and the amount. Only who receives it stays private. ## Next - [Glossary](https://sdk.nimbusprivacy.xyz/resources/glossary): Every term these docs use, in plain words. - [What's public](https://sdk.nimbusprivacy.xyz/explainers/visibility): What each payment shows on chain. Last updated 21 October 2026 # The example app (/resources/example-app) `apps/nimbus-example` is a small Next.js App Router app built on the SDK. It runs on mainnet with real funds. It covers what every app needs: connect, register, deposit, Private Send, withdraw, balances, history and recovery. Each file is short and maps onto a page of these docs. Read them side by side. ## Run it The app lives in the Nimbus monorepo and uses the SDK from source. Build the SDK's WebAssembly and files first, then start the app. ### Build the SDK and its files From the repository root, install, then build the three WebAssembly modules the SDK ships: ```bash pnpm install just build-nimbus-primitives-wasm just build-umbra-prover-wasm just build-witness-graph-wasm ``` Then build the SDK, and gather its files into its `assets/` folder: ```bash pnpm --filter "@umbra/nimbus-example^..." run build pnpm --filter @umbra/nimbus-solana-web run build:assets ``` ### Set the environment Copy `.env.example` to `.env.local` in `apps/nimbus-example`. Both settings are optional: | Variable | What it does | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `NEXT_PUBLIC_NIMBUS_RPC` | A Solana RPC to use in place of the preset's public one. | | `NEXT_PUBLIC_NIMBUS_GATEWAY_TOKEN` | The gateway token every session sends. Without it, accounts open and read, and every payment fails `unauthorized`. | Next.js writes both into the page, so every visitor can read the token. To get a token, email [dev@umbraprivacy.com](mailto:dev@umbraprivacy.com) or reach out to any Umbra team member. ### Copy the files and start ```bash cd apps/nimbus-example pnpm assets pnpm dev ``` `pnpm assets` runs `nimbus-solana-web copy-assets public/nimbus`. `next.config.mjs` sends the two cross-origin isolation headers with every route. Its other scripts are `pnpm build` and `pnpm start` for a production build, and `pnpm typecheck`. `pnpm tokens` rewrites the token list, as the table below explains. ## Where each feature lives | Feature | Files | Docs | | ------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | Loading the SDK once, in the browser | `components/app-provider.tsx`, `lib/nimbus/load.ts` | [Use it with React](https://sdk.nimbusprivacy.xyz/getting-started/react) | | Listing wallets and connecting one | `components/connect-card.tsx` | [Wallets](https://sdk.nimbusprivacy.xyz/helpers/wallets) | | Opening a session, and where its sign-in is kept | `lib/nimbus/session.ts`, `components/connect-card.tsx` | [Sessions](https://sdk.nimbusprivacy.xyz/helpers/sessions) | | Registering before the first deposit | `components/account-gate.tsx`, `components/register-card.tsx`, `lib/nimbus/register.ts` | [Registration](https://sdk.nimbusprivacy.xyz/registration) | | Deposit, withdraw and Private Send on one card | `components/action-card.tsx`, `components/flow-form.tsx` | [Deposit](https://sdk.nimbusprivacy.xyz/deposit), [Withdraw](https://sdk.nimbusprivacy.xyz/withdraw), [Transfer](https://sdk.nimbusprivacy.xyz/transfer) | | Each payment, in one small function | `lib/nimbus/deposit.ts`, `lib/nimbus/withdraw.ts`, `lib/nimbus/private-send.ts` | [The function template](https://sdk.nimbusprivacy.xyz/api-design/function-template) | | One description of a withdrawal or Private Send result | `lib/nimbus/spend.ts` | [Results and errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors) | | A message for each progress stage | `lib/progress.ts`, `components/keep-tab-open.tsx` | [Show progress](https://sdk.nimbusprivacy.xyz/api-design/progress) | | Syncing, and balances grouped per token | `components/use-account.ts`, `lib/nimbus/account.ts` | [Balances](https://sdk.nimbusprivacy.xyz/helpers/balances), [Sync](https://sdk.nimbusprivacy.xyz/helpers/sync) | | Reserved funds with a Release button | `components/balances-panel.tsx`, `lib/nimbus/account.ts` | [Reserved funds](https://sdk.nimbusprivacy.xyz/helpers/reserved-funds) | | History, ten entries a page | `components/activity-list.tsx`, `lib/nimbus/history.ts`, `lib/activity.ts` | [History](https://sdk.nimbusprivacy.xyz/helpers/history) | | A pending deposit, and a refund of a stuck one | `components/recovery-card.tsx`, `lib/nimbus/recovery.ts` | [If the deposit is still pending](https://sdk.nimbusprivacy.xyz/deposit/pending) | | Token names and logos | `lib/tokens.ts`, `scripts/fetch-tokens.ts` | [Tokens](https://sdk.nimbusprivacy.xyz/helpers/tokens) | The token logos are saved under `public/tokens/`, because a cross-origin isolated page shows images from another site only when that site allows it. `pnpm tokens` fetches them again when the preset's tokens change. ## What it leaves out The example keeps to the basics. It runs only on mainnet, and asks each service directly rather than through the coordinator. It does not sync in the background. [Use it with React](https://sdk.nimbusprivacy.xyz/getting-started/react#sync-in-the-background) shows how to add that. ## Next - [Use it with React](https://sdk.nimbusprivacy.xyz/getting-started/react): The patterns the example is built from. - [Quickstart](https://sdk.nimbusprivacy.xyz/getting-started/quickstart): The same journey, in one file. Last updated 21 October 2026 # Glossary (/resources/glossary) Terms are in alphabetical order. Each has its own link, so you can point to it from your own docs. The [protocol glossary](https://docs.nimbusprivacy.xyz/resources/glossary) goes deeper. ### Account signature The one message a user's wallet signs to open their private account. Every key of the account is derived from it, so the SDK treats it as a secret. ### Commitment A value that fixes some data without revealing it. Each new note's commitment is published on chain, while the note itself stays encrypted. The `commitment` option is something else: Solana's confirmation level. ### Committee The services that check and sign every payment before the pool accepts it: the guardian nodes, the screener, the fee enclave and the data endorser. Their signatures are combined into one. ### Context The object a payment runs in, made from a session by `createDepositContext` and its siblings. It picks the pool and the committee nodes the payment uses. ### Coordinator A service that can make a payment's requests to the committee and the relayer on the browser's behalf. The SDK uses it only with `route: "coordinator"`. ### Deposit Moving tokens from a user's wallet into a private balance, theirs or another user's. ### Gateway token The token that lets your app use Nimbus's services on mainnet. A session sends it to the gateway's services and nowhere else. To get one, email [dev@umbraprivacy.com](mailto:dev@umbraprivacy.com) or reach out to any Umbra team member. ### Identity A user's registered entry in the pool: one value that commits to their keys and their platform wallet. Registering publishes it. ### Indexer The service a sync reads new notes and spends from. ### Intent The device's record of a payment it has started. These docs call it a payment in progress. It stays until the payment lands or can no longer land. ### Lease The hold a payment in progress puts on the notes it spends, so no other payment spends them too. Leased notes count as `reserved`. ### Merge A Private Send to the user's own account that combines several notes into one. The SDK runs merges first when the three largest notes cannot cover a payment. ### Note The unit of a private balance: an amount of one token, owned by one private account. A private balance is the sum of its unspent notes. ### Nullifier A value a payment publishes to mark a note spent. Nobody can tell which note it marks without the owner's keys. ### Pending Funds paid to a user that cannot be spent yet. A new deposit is pending until Solana finalizes it. ### Platform wallet A second Solana address the SDK derives from the account signature. Registration binds it to the user's wallet, and deposits pass through it. ### Pool The Solana program that holds every deposited token, under the rules it enforces. Pools are numbered, and the SDK uses the highest open one by default. ### Private account Everything a user holds in Nimbus. The account signature unlocks it on a device. ### Private balance What a user holds in Nimbus, kept on their device. The SDK reads it with `readBalances`. ### Private Send A payment from one private balance to another. The chain shows neither the sender, the recipient, nor the amount. ### Proof A zero-knowledge proof the device builds for each registration and payment. It shows the payment follows the pool's rules without revealing its private details. ### Proving keys The large circuit files the device needs to build proofs. The SDK downloads them once, checks each one's SHA-256, and caches them. ### Registration The one Solana transaction a wallet sends before its first deposit. It publishes the user's identity in the pool. ### Relayer A service that submits withdrawals and Private Sends to Solana and pays their network fee. The user's wallet signs neither. ### Reserved Funds held by a payment in progress. They come back once the payment can no longer land, or when `releaseReservedNotes` frees them. ### Seed The 64-byte secret the SDK derives from the account signature. Every key of the private account comes from it, and closing the session zeroes it. ### Session A private account opened on this device by `openNimbusSession`. It holds the user's keys in memory until `closeNimbusSession`. ### Settlement Where a relayed payment ended up: `landed`, `failed`, `pending` or `not-followed`. By default the SDK follows each payment until it lands. ### Sync Reading the chain and the indexer into the device, so its private balance is current. Only a sync changes what `readBalances` returns. ### Transaction version The Solana transaction format a wallet signs. Payments use version 1 by default, and version 0 for wallets that cannot sign version 1. ### Withdraw Moving tokens from a private balance to any Solana address. ### Zone A numbered group of notes whose own signer must approve payments into or out of it. Zone 0 has no signer, and every payment uses it by default. ## Next - [Don't confuse](https://sdk.nimbusprivacy.xyz/resources/dont-confuse): Six pairs that sound alike and behave differently. - [Types](https://sdk.nimbusprivacy.xyz/reference/types): Every type the SDK exports. Last updated 21 October 2026 # Private Send (/transfer) A Private Send pays another user from the private balance. The tokens never leave the pool. They move from the sender's private balance to the recipient's. In this guide, Alice pays Bob 10 USDC. Anyone watching the chain sees that a payment happened, but not who paid, who was paid or how much. ## What's public A relayer sends the transaction, so neither wallet appears on chain. Bob himself can tell the payment came from Alice. Each payment records who funded it, and Bob's keys can read that. [How privacy is lost](https://docs.nimbusprivacy.xyz/privacy/leaks) explains why. ## Send Create a transfer context from the session, then call `transfer` with Bob's wallet address. ```ts import { address } from "@solana/kit"; import { createTransferContext, transfer } from "@umbra/nimbus-solana-web"; const usdc = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); const context = await createTransferContext(session); const result = await transfer(context, { mint: usdc, amount: 10_000_000n, recipient: bob, }); ``` - **`amount`** is in the token's smallest unit, so 10 USDC is `10_000_000n`. The fees come out of it, so Bob receives a little less. Pass `fees: "added"` to pay them on top. - **`recipient`** is Bob's wallet address. Bob must have [registered](https://sdk.nimbusprivacy.xyz/registration). - **`mint`** is the token. Alice pays from her private balance in it. Alice's wallet signs nothing. The SDK builds the proof on her device, and a relayer sends the transaction and pays its network fee. ## Follow it to the end By default, `transfer` follows the payment until it lands, then syncs the device. Two options change that: - **`follow: false`** returns as soon as the relayer accepts the payment. - **`commitment: "finalized"`** waits until the payment cannot be rolled back. The default is `"confirmed"`. A `relayed` result carries how following it went, in `result.settlement.kind`: | `kind` | What happened | What to do | | -------------- | ------------------------------------------------------------------------------------ | ---------------------------------------- | | `landed` | The payment landed, and the device synced. `settlement.sync` says how the sync went. | Done. Bob sees it after his next sync. | | `failed` | The relayer reported it `rejected`, `expired` or `failed`. Nothing moved. | Alice can try again in a few minutes. | | `pending` | The SDK stopped following before it settled. It may still land. | Sync again in a minute to see it settle. | | `not-followed` | You passed `follow: false`. | Sync later to see it settle. | After `failed`, the funds the payment used stay reserved for a few minutes. After `pending` or `not-followed`, they stay reserved until a sync sees the payment settle. [Reserved funds](https://sdk.nimbusprivacy.xyz/explainers/reserved-funds) explains why. ## Handle the result `transfer` returns a result instead of throwing for anything your user can cause. Check its `kind`: | `kind` | What happened | What to do | | --------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | | `relayed` | The relayer accepted the payment. | Read its settlement, above. | | `refused` | It stopped before anything was sent. `refusal` says why. | See the refusals below. | | `relay-refused` | The relayer turned it down, with a `code` and a `detail`. Nothing reached Solana, and its funds are free again. | Check the `code`, then try again. | | `unsent` | This browser would not record the payment as sent, so nothing was sent. | Ask Alice to reload the page and try again. | | `merge-failed` | The balance had to be combined first, and that stopped. | Try again. It needs fewer steps now. | A `refused` result carries a `refusal`, whose `kind` says what stopped it: | `refusal.kind` | What happened | What to do | | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------- | | `insufficient` | Alice cannot cover the amount and its fees. `available` is what she can spend of the token. | Show `available`, and ask for less. | | `not-registered` | Alice's own registration is not final on Solana yet. | Try again in a minute. | | `lease-refused` | Another payment, perhaps in another tab, took some of the same funds. | Try again once it finishes. | | `store-refused`, `attempt-refused`, `stale-handle`, `store-unavailable` | This browser's account data could not be used. | Ask Alice to reload the page and try again. | Here is the whole check. TypeScript checks that it covers every case. ```ts function describeSettlement(settlement: SpendSettlement): string { switch (settlement.kind) { case "landed": return "Sent."; case "failed": return `The payment did not go through (${settlement.state}).`; case "pending": return "Sent, and not settled yet. Check again in a minute."; case "not-followed": return "The relayer accepted the payment."; } } function describeRefusal(refusal: SpendRefusal): string { switch (refusal.kind) { case "insufficient": return `You can send at most ${refusal.available} base units, fees included.`; case "not-registered": return "Your registration is not final yet. Try again in a minute."; case "lease-refused": return "Another payment is using these funds. Try again soon."; case "store-refused": case "attempt-refused": case "stale-handle": case "store-unavailable": return "This browser could not use your account data. Reload and try again."; } } function describeTransfer(result: TransferResult): string { switch (result.kind) { case "relayed": return describeSettlement(result.settlement); case "refused": return describeRefusal(result.refusal); case "relay-refused": return `The relayer refused the payment: ${result.detail}`; case "unsent": return "Nothing was sent. Reload and try again."; case "merge-failed": return `Combining your balance stopped after ${result.mergesCompleted} step(s).`; } } ``` A withdrawal returns the same type, so `describeTransfer` works for `withdraw` too. ## When the balance is spread out Alice's private balance in a token is made of separate amounts, one for each payment she received. A single Private Send can draw on at most three of them. If her three largest amounts cannot cover 10 USDC, but all of them together can, the SDK combines them first. Each combining step is a Private Send to herself, sent by the relayer. The SDK waits for each step to land and reach her device before the next. That takes longer, and each step pays the relayer's fee. Each step may take up to `mergeTimeout` slots, 300 by default. `onProgress` reports each step as `merge-prepared`, `merge-relayed` and `merge-settled`, numbered from 1 in `merge`. If a step does not finish, the result is `merge-failed`. The steps that finished stay combined. `cause` says why the next one stopped. ## Show progress Pass `onProgress` to tell Alice what is happening: ```ts import { transfer } from "@umbra/nimbus-solana-web"; await transfer( context, { mint: usdc, amount: 10_000_000n, recipient: bob }, { onProgress: (event) => showStatus(event.stage) }, ); ``` The stages are `synced`, then any combining steps, then `prepared`, `proved`, `relayed` and `settled`. `settled` comes only while following. [Show progress](https://sdk.nimbusprivacy.xyz/api-design/progress) has a message for each stage. ## Edge cases ### Not enough available balance The result is `refused` with `insufficient`. Its `available` counts only what Alice can spend right now. Funds held by an unfinished payment are left out. ### Funds reserved by another tab A payment running in another tab holds the funds it uses. This payment cannot use them, and `available` leaves them out. If both tabs reach for the same funds at once, one gets `lease-refused`. [Reserved funds](https://sdk.nimbusprivacy.xyz/helpers/reserved-funds) lists what is held and by what. ### Bob is not registered `transfer` throws a `NimbusSdkError` with reason `recipient-unregistered`, and nothing is sent. Ask Bob to open your app and register first. ## Common errors These are thrown, not returned, because your app usually cannot fix them by asking the user. | Error | Reason | Cause | | ----------------------- | ------------------------ | ---------------------------------------------------------------------------------------- | | `NimbusSdkError` | `recipient-unregistered` | Bob has not registered. | | `NimbusSdkError` | `account-unregistered` | Alice has not registered. Run `register` first. | | `NimbusSdkError` | `service-unavailable` | Solana or a Nimbus service could not be reached. Try again. | | `NimbusSolanaFlowError` | `relay-unavailable` | The relayer will not carry this payment: a token it does not relay, or a fee it refuses. | | `NimbusSolanaFlowError` | `committee-mismatched` | Nimbus's approval could not be put together. Nothing was sent. Try again. | | `NimbusSolanaFlowError` | `context-mismatched` | A Nimbus service answered with data that does not fit the payment. Try again. | | `NimbusSolanaFlowError` | `amount-unfundable` | The fees would leave Bob nothing, or the amount is out of range. | | `NimbusSolanaFlowError` | `zone-unauthorised` | You passed a `zone` option, and its signer refused. | | `NimbusSdkError` | `unauthorized` | The session has no gateway token, or the gateway refused it. | [Handle errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors) shows one function that covers every error the SDK throws. ## Options | Option | Default | What it does | | -------------- | --------------------- | -------------------------------------------------------------------- | | `onProgress` | none | Called at each stage of the payment. | | `follow` | `true` | `false` returns once the relayer accepts the payment. | | `commitment` | `"confirmed"` | `"finalized"` waits until the payment cannot be rolled back. | | `fees` | `"deducted"` | `"added"` pays the fees on top, so Bob receives `amount`. | | `submitterFee` | the relayer's minimum | Offer the relayer more, in base units. | | `mergeTimeout` | `300n` | How many slots each combining step may take. | | `sync` | `true` | Brings the device up to date first. Pass `false` if you just synced. | The [API reference](https://sdk.nimbusprivacy.xyz/reference/transfer#transfer) lists every option. ## Next - [Withdraw](https://sdk.nimbusprivacy.xyz/withdraw): Take tokens out of the private balance to any address. - [Show history](https://sdk.nimbusprivacy.xyz/helpers/history): How Bob sees the payment arrive. - [Reserved funds](https://sdk.nimbusprivacy.xyz/explainers/reserved-funds): Why a payment can hold funds for a few minutes. Last updated 21 October 2026 # Withdraw (/withdraw) A withdrawal moves tokens out of the private balance to a public Solana address. It can be the user's own wallet or anyone else's. In this guide, Bob withdraws 10 USDC to his wallet. Then he sends SOL straight to another address. ## What's public A relayer sends the withdrawal, so no wallet of Bob's signs it. The address that receives the tokens is public, like any Solana transfer. > **Keep the two ends apart** > > Avoid withdrawing the exact amount you just deposited, since matching amounts and times suggest > the same person. A withdrawal to a wallet already tied to you, such as the one you registered, > shows who withdrew. [Privacy practices](https://docs.nimbusprivacy.xyz/privacy/practices) has more. ## Withdraw Create a withdrawal context from the session, then call `withdraw`. ```ts import { address } from "@solana/kit"; import { createWithdrawalContext, withdraw } from "@umbra/nimbus-solana-web"; const usdc = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); const context = await createWithdrawalContext(session); const result = await withdraw(context, { mint: usdc, amount: 10_000_000n, recipient: session.walletAddress, }); ``` - **`amount`** is what leaves the private balance, in the token's smallest unit. The fees come out of it. Pass `fees: "added"` so the recipient receives exactly `amount`. - **`recipient`** is the wallet paid. Here it is Bob's own, but any Solana address works. - **`mint`** is the token. Bob pays from his private balance in it. Bob's wallet signs nothing. The SDK builds the proof on his device, and a relayer sends the transaction and pays its network fee. ## Where the tokens go ### A wallet By default the tokens go to the recipient wallet's associated token account for the mint. If that account does not exist, the relayer creates it and pays its rent. The relayer charges a creation fee for that, which comes out of the withdrawal, unless it covers the cost itself. ### A token account To pay a different token account, pass it as `recipientTokenAccount`. It must already exist and hold the same token. ### SOL itself To send SOL rather than wrapped SOL, withdraw the wrapped SOL mint. Pass the receiving wallet's own address as `recipientTokenAccount` too. The SOL then lands in the wallet itself. ```ts import { address } from "@solana/kit"; import { withdraw } from "@umbra/nimbus-solana-web"; const wrappedSol = address("So11111111111111111111111111111111111111112"); const result = await withdraw( context, { mint: wrappedSol, amount: 100_000_000n, recipient: destination }, { recipientTokenAccount: destination }, ); ``` A wallet that holds no SOL must receive at least 890,880 lamports, Solana's rent-exempt minimum. Without `recipientTokenAccount`, the SDK pays the wallet's wrapped SOL token account instead. If that account does not exist, `withdraw` throws. ### When the destination cannot be paid `withdraw` reads the destination from Solana before it asks Nimbus for anything. When it cannot be paid, `withdraw` throws a `NimbusSolanaFlowError` with reason `payout-unpayable`. Some causes: - The token account holds a different token. - The account belongs to neither token program. - The account does not exist, and it is not the recipient's associated token account. - A SOL payment would leave the wallet below the rent-exempt minimum. Nothing is sent. Choose another destination, or a larger amount. ## Follow it to the end A withdrawal is followed the way a Private Send is. By default `withdraw` waits until it lands, then syncs the device. Pass `follow: false` to return once the relayer accepts it. A `relayed` result's `settlement.kind` is `landed`, `failed`, `pending` or `not-followed`. [Follow it to the end](https://sdk.nimbusprivacy.xyz/transfer#follow-it-to-the-end) says what each means and what to do. ## Handle the result `withdraw` returns a result instead of throwing for anything your user can cause. Check its `kind`: | `kind` | What happened | What to do | | --------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | `relayed` | The relayer accepted the withdrawal. | Read its settlement, above. | | `refused` | It stopped before anything was sent. `refusal` says why. | See [the refusals](https://sdk.nimbusprivacy.xyz/transfer#handle-the-result) on Private Send. | | `relay-refused` | The relayer turned it down, with a `code` and a `detail`. Nothing reached Solana, and its funds are free again. | Check the `code`, then try again. | | `unsent` | This browser would not record the withdrawal as sent, so nothing was sent. | Ask Bob to reload the page and try again. | | `merge-failed` | The balance had to be combined first, and that stopped. | Try again. It needs fewer steps now. | Here is the whole check. TypeScript checks that it covers every case. ```ts function describeWithdrawal(result: WithdrawalResult): string { switch (result.kind) { case "relayed": switch (result.settlement.kind) { case "landed": return "Withdrawn."; case "failed": return `The withdrawal did not go through (${result.settlement.state}).`; case "pending": return "Sent, and not settled yet. Check again in a minute."; case "not-followed": return "The relayer accepted the withdrawal."; } case "refused": switch (result.refusal.kind) { case "insufficient": return `You can withdraw at most ${result.refusal.available} base units.`; case "not-registered": return "Your registration is not final yet. Try again in a minute."; case "lease-refused": return "Another payment is using these funds. Try again soon."; case "store-refused": case "attempt-refused": case "stale-handle": case "store-unavailable": return "This browser could not use your account data. Reload and try again."; } case "relay-refused": return `The relayer refused the withdrawal: ${result.detail}`; case "unsent": return "Nothing was sent. Reload and try again."; case "merge-failed": return `Combining your balance stopped after ${result.mergesCompleted} step(s).`; } } ``` ## Show progress Pass `onProgress` to tell Bob what is happening: ```ts import { withdraw } from "@umbra/nimbus-solana-web"; await withdraw( context, { mint: usdc, amount: 10_000_000n, recipient }, { onProgress: (event) => showStatus(event.stage) }, ); ``` The stages are `synced`, then any combining steps, then `prepared`, `proved`, `relayed` and `settled`. [Show progress](https://sdk.nimbusprivacy.xyz/api-design/progress) has a message for each stage. ## Edge cases ### The balance is spread out One withdrawal draws on at most three separate amounts in the private balance. When those cannot cover it, the SDK combines amounts first, as a [Private Send does](https://sdk.nimbusprivacy.xyz/transfer#when-the-balance-is-spread-out). That takes longer and costs a relayer fee per step. ### A token with a transfer fee Some Token-2022 tokens keep a fee on every transfer. The recipient then receives less again, even with `fees: "added"`. ### Trying again after a refusal After `relay-refused`, the funds it used are free again at once, so a retry can spend them. After a `failed` settlement they stay reserved for a few minutes. A retry straight away may then come back `insufficient`. [Reserved funds](https://sdk.nimbusprivacy.xyz/helpers/reserved-funds) shows what is held. ## Common errors These are thrown, not returned, because your app usually cannot fix them by asking the user. | Error | Reason | Cause | | ----------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `NimbusSolanaFlowError` | `payout-unpayable` | The destination cannot be paid. See above. | | `NimbusSolanaFlowError` | `relay-unavailable` | The relayer will not carry this withdrawal, publishes no creation fee for a missing token account, or the fee you offered is too low. | | `NimbusSolanaFlowError` | `amount-unfundable` | The fees would leave the recipient nothing, or the amount is out of range. | | `NimbusSolanaFlowError` | `committee-mismatched` | Nimbus's approval could not be put together. Nothing was sent. Try again. | | `NimbusSolanaFlowError` | `context-mismatched` | A Nimbus service answered with data that does not fit the withdrawal. Try again. | | `NimbusSdkError` | `misconfigured` | An address is not valid base58, or the mint does not exist on this cluster. | | `NimbusSdkError` | `account-unregistered` | Bob's account has not registered. Run `register` first. | | `NimbusSdkError` | `service-unavailable` | Solana or a Nimbus service could not be reached. Try again. | | `NimbusSdkError` | `unauthorized` | The session has no gateway token, or the gateway refused it. | [Handle errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors) shows one function that covers every error the SDK throws. ## Options | Option | Default | What it does | | ----------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------- | | `onProgress` | none | Called at each stage of the withdrawal. | | `recipientTokenAccount` | the associated token account | The token account paid. | | `follow` | `true` | `false` returns once the relayer accepts the withdrawal. | | `commitment` | `"confirmed"` | `"finalized"` waits until the withdrawal cannot be rolled back. | | `fees` | `"deducted"` | `"added"` pays the fees on top, so the recipient receives `amount`. | | `submitterFee` | the relayer's minimum, plus its creation fee for a missing token account | Offer the relayer more, in base units. | | `mergeTimeout` | `300n` | How many slots each combining step may take. | | `sync` | `true` | Brings the device up to date first. Pass `false` if you just synced. | The [API reference](https://sdk.nimbusprivacy.xyz/reference/withdraw#withdraw) lists every option. ## Next - [Load the SDK](https://sdk.nimbusprivacy.xyz/helpers/load): Every option loadNimbusSdk takes. - [Show balances](https://sdk.nimbusprivacy.xyz/helpers/balances): Read what the private balance holds after the withdrawal. - [Private Send](https://sdk.nimbusprivacy.xyz/transfer): Pay another user without leaving the pool. Last updated 21 October 2026