@umbra-privacy/nimbus-solana-web
v0.4.0
Published
The Solana SDK for browsers with every dependency filled: load it for a cluster from its bundled preset, open a session for the connected wallet, and run the flows, with crypto in WASM, IndexedDB storage, the prover's WASM, the service clients and @solana
Readme
@umbra-privacy/nimbus-solana-web
Private balances and payments for Solana web apps. 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, all in the browser. Spending keys stay on the user's device.
Pre-release (0.x): every API, type and behaviour is subject to change at any time, as and when the Nimbus Privacy team (built by Umbra Privacy) decides or requires.
Install
pnpm add @umbra-privacy/nimbus-solana-web @solana/kit
# or
npm install @umbra-privacy/nimbus-solana-web @solana/kit
# or
yarn add @umbra-privacy/nimbus-solana-web @solana/kitEvery SDK function takes mints and wallets as a kit Address. Make one with address from
@solana/kit.
The SDK runs in the browser only. Load it from code that runs in the browser, not in a server render.
Setup
1. Serve the SDK's files
The package ships WebAssembly and a worker script. Your app serves them from its own site. Copy them into your static folder after every install:
{
"scripts": {
"postinstall": "nimbus-solana-web copy-assets public/nimbus"
}
}Keep that folder out of git. Pass its URL to loadNimbusSdk as assetsBaseUrl, ending with /.
The SDK checks every file's SHA-256 before it runs it.
2. Turn on cross-origin isolation
By default, proofs use shared memory. Browsers allow it only on pages served with these two headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corpSend both with every page that loads the SDK.
Install and set up shows the config for
Next.js, Vite, Vercel and Cloudflare. If a wallet breaks under them, load the SDK on that page with
proving: { threading: "workers" }, which proves more slowly and needs neither header.
3. Get a mainnet gateway token
Nimbus's services on mainnet sit behind a gateway. It accepts only requests that carry a token issued for your app. To get one, email [email protected] or reach out to any Umbra team member.
Pass it to openNimbusSession as gatewayToken. Without it, a session still opens and reads
balances. Registration and every payment fail with reason unauthorized.
Example
Alice connects her wallet, registers, deposits 25 USDC and reads her private balance. This runs on mainnet and moves real USDC.
import { address } from "@solana/kit";
import {
createDepositContext,
createRegistrationContext,
deposit,
listWallets,
loadNimbusSdk,
openNimbusSession,
openWalletConnection,
readBalances,
register,
syncNoteStore,
} from "@umbra-privacy/nimbus-solana-web";
const usdc = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
// Load the SDK once per page.
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. Her wallet signs one message; no money moves.
const opened = await openNimbusSession(
sdk,
{ connectedWallet: signer },
{ gatewayToken: "your-gateway-token" },
);
if (opened.kind === "declined") throw new Error("Alice declined to sign.");
const { session } = opened;
// Register her, once, before her first deposit.
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. USDC has 6 decimals. The protocol fee comes out of it.
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.");
}
// Sync with Solana, then read her private balance.
await syncNoteStore(session, {});
const balances = await readBalances(session, {});
for (const row of balances) {
if (row.mint === usdc) console.log(row.state, row.total);
}A new deposit can read as pending for a moment, until Solana confirms it. Then it reads as
available.
Private Send (transfer) and withdraw follow the same pattern. A relayer submits them, so the
wallet signs nothing and pays no network fee. The
quickstart walks through both and
handles every result.
What's public and what's private
- 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.
Read the privacy boundaries before you launch.
Links
- Documentation: https://sdk.nimbusprivacy.xyz
- Protocol documentation: https://docs.nimbusprivacy.xyz
License
Apache-2.0
Nimbus Privacy (built by Umbra Privacy)
