Quickstart
Go from an empty Vite or Next.js app to Alice depositing, paying Bob privately, and Bob withdrawing.
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
pnpm add @umbra/nimbus-solana-web @solana/kitYou 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:
{
"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.
const = [
{ : "Cross-Origin-Opener-Policy", : "same-origin" },
{ : "Cross-Origin-Embedder-Policy", : "require-corp" },
];
export default { : async () => [{ : "/:path*", : }] };Install and set up 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.
import { } from "@umbra/nimbus-solana-web";
const = await (
{ : "mainnet" },
{ : "/nimbus/" },
);assetsBaseUrl is the URL of the folder you copied the files into. In React, load it from an
effect; Use it with React shows how.
Connect a wallet
listWallets returns the installed wallets that can sign on mainnet. Show them, and connect the
one Alice picks.
import { , } from "@umbra/nimbus-solana-web";
const [] = ();
if ( === ) throw new ("Install a Solana wallet first.");
const = await (, { });
switch (.) {
case "declined":
throw new ("Alice declined to connect.");
case "no-account":
throw new ("The wallet has no account on Solana mainnet.");
case "connected":
break;
}
const { , } = .;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.
import { } from "@umbra/nimbus-solana-web";
const = await (
,
{ : },
{ },
);
if (. === "declined") throw new ("Alice declined to sign.");
const { } = ;The gateway token lets your app use Nimbus's services on mainnet. To get one, email dev@umbraprivacy.com or reach out to any Umbra team member. 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.
import { , } from "@umbra/nimbus-solana-web";
if (.. === "unregistered") {
const = await (
await (),
{},
{ },
);
switch (.) {
case "complete":
break;
case "submitted":
if (... !== "landed") {
throw new ("The registration did not land. Try again.");
}
break;
case "declined":
case "store-refused":
throw new ("Nothing was sent. Try again.");
}
}Her wallet approves one transaction and pays its network fee. 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.
import { } from "@solana/kit";
import { , } from "@umbra/nimbus-solana-web";
const = ("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const = await (
await (),
{ : , : 25_000_000n, : . },
{ },
);
if (
. !== "submitted" ||
... !== "landed"
) {
throw new ("The deposit did not land.");
}The protocol fee comes out of the 25 USDC. 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.
import type { Balance } from "@umbra/nimbus-solana-web";
import { , } from "@umbra/nimbus-solana-web";
await (, {});
const = await (, {});
const = (: Balance["state"]): bigint =>
.(() => . === && . === )?. ??
0n;
.("Available:", ("available"));
.("Reserved:", ("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 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.
import { , } from "@umbra/nimbus-solana-web";
import type { } from "@umbra/nimbus-solana-web";
const = await (await (), {
: ,
: 10_000_000n,
: ,
});
function (: ): string {
switch (.) {
case "relayed":
switch (..) {
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: ${..}.`;
case "relay-refused":
case "unsent":
case "merge-failed":
return "The payment was not sent. Try again.";
}
}
.(());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 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.
import { , } from "@umbra/nimbus-solana-web";
const = await (await (), {
: ,
: 5_000_000n,
: .,
});
if (. === "relayed" && .. === "landed") {
.("Bob withdrew 5 USDC.");
}The withdrawal syncs Bob's device first, so it sees Alice's payment. A relayer submits it too. 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 explains the pattern.
The whole file
import { } from "@solana/kit";
import {
,
,
,
,
,
,
,
,
,
,
,
,
,
,
} from "@umbra/nimbus-solana-web";
const = ("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
// Load the SDK.
const = await (
{ : "mainnet" },
{ : "/nimbus/" },
);
// Connect a wallet.
const [] = ();
if ( === ) throw new ("Install a Solana wallet first.");
const = await (, { });
if (. !== "connected") throw new ("Not connected.");
const { , } = .;
// Open Alice's private account.
const = await (
,
{ : },
{ },
);
if (. === "declined") throw new ("Alice declined to sign.");
const { } = ;
// Register her, once.
if (.. === "unregistered") {
const = await (
await (),
{},
{ },
);
const =
. === "complete" ||
(. === "submitted" &&
... === "landed");
if (!) throw new ("The registration did not land.");
}
// Deposit 25 USDC.
const = await (
await (),
{ : , : 25_000_000n, : . },
{ },
);
if (
. !== "submitted" ||
... !== "landed"
) {
throw new ("The deposit did not land.");
}
// Read her balance.
await (, {});
const = await (, {});
for (const of ) {
if (. === ) .(., .);
}
// Pay Bob 10 USDC privately.
const = await (await (), {
: ,
: 10_000_000n,
: ,
});
if (. === "relayed" && .. === "landed") {
.("Bob has been paid.");
}
// Bob withdraws 5 USDC, from his own session.
const = await (await (), {
: ,
: 5_000_000n,
: .,
});
if (. === "relayed" && .. === "landed") {
.("Bob withdrew 5 USDC.");
}Next
Last updated