> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.umbraprivacy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Build directly on @umbra-privacy/sdk: register, deposit, create a Stealth Pool Note, scan, burn.

## Prerequisites

* Node.js 18+ or a modern browser
* A Solana wallet (or a generated keypair for testing)
* A Solana JSON-RPC URL (HTTP **and** WebSocket)

## 1. Install

The SDK is one package — the ZK prover, indexer client, relayer client, and Solana
primitives are all subpath modules of `@umbra-privacy/sdk`. **`snarkjs` is a required
peer dependency** (the SDK imports it for Groth16 proving) — install it alongside the SDK
or the first import throws `ERR_MODULE_NOT_FOUND`.

<CodeGroup>
  ```bash pnpm theme={null}
  pnpm add @umbra-privacy/sdk@5.0.0-rc.6 snarkjs
  ```

  ```bash npm theme={null}
  npm install @umbra-privacy/sdk@5.0.0-rc.6 snarkjs
  ```

  ```bash yarn theme={null}
  yarn add @umbra-privacy/sdk@5.0.0-rc.6 snarkjs
  ```
</CodeGroup>

<Note>
  If you connect a browser wallet (Phantom/Solflare/…) you also need the
  `@wallet-standard/core` peer dependency. It is not required for the
  `createInMemorySigner` / `createSignerFromPrivateKeyBytes` paths used below.
</Note>

<Warning>
  Pin the SDK to an exact version (no `^`, no `~`). The ZK assets are versioned in
  lockstep with the package; floating versions can resolve a manifest that points at
  the wrong circuit hash.
</Warning>

## 2. Create a Signer

For quick testing, generate an in-memory keypair. For anything reusable, load a
persisted keypair with `createSignerFromPrivateKeyBytes` (it accepts a 64-byte
keypair or a 32-byte seed). For production, use a wallet adapter — see
[Wallet Adapters](/sdk/wallet-adapters).

```typescript theme={null}
import {
  createInMemorySigner,
  createSignerFromPrivateKeyBytes,
} from "@umbra-privacy/sdk";

// Ephemeral — disappears when the process exits. Test/dev only.
const signer = await createInMemorySigner();

// Reusable — pass your own secret bytes (e.g. from a Solana CLI keypair file).
// const secret = Uint8Array.from(JSON.parse(readFileSync("keypair.json", "utf8")));
// const signer = await createSignerFromPrivateKeyBytes(secret);

console.log("Wallet address:", signer.address);
```

<Warning>
  Both factories are **async** — you must `await` them, or `signer.address` is `undefined`.
</Warning>

## 3. Create the Umbra Client

The client owns your signer, network configuration, and indexer endpoint. The relayer
is **not** a client argument — you build it separately with `getUmbraRelayer` (step 9).

```typescript theme={null}
import { getUmbraClient } from "@umbra-privacy/sdk";

const client = await getUmbraClient({
  signer,
  network: "mainnet",
  rpcUrl: "https://api.mainnet-beta.solana.com",
  rpcSubscriptionsUrl: "wss://api.mainnet-beta.solana.com",
  indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com",
  // NOTE: there is NO relayerApiEndpoint here — see step 9.
});
```

<Note>
  * **Testing on devnet?** Use `network: "devnet"`, `rpcUrl: "https://api.devnet.solana.com"` (+ `wss://api.devnet.solana.com`), `indexerApiEndpoint: "https://utxo-indexer.api-devnet.umbraprivacy.com"`, the devnet relayer `https://relayer.api-devnet.umbraprivacy.com`, and the devnet **dUSDC** mint `4oG4sjmopf5MzvTHLE8rpVJ2uyczxfsw2K84SUTpNDx7`. See [Supported Tokens](/supported-tokens) for the full per-network mint list.
  * The Umbra program address differs between devnet and mainnet. The SDK resolves the correct address automatically based on the `network` parameter.
  * The first operation that needs the master seed (typically `register()`) prompts the wallet to sign a deterministic consent message. Subsequent operations reuse the cached seed without re-prompting.
  * **Browser: persist scan state.** On Node you can omit persistent stores and the scanner falls back to in-memory state (fine for scripts). In the browser, wire `createShardedUtxoDataStore` + `createShardedNullifierStore` (`@umbra-privacy/sdk/store-adapters`) — otherwise every `scan()` walks every tree from genesis. Because those stores derive their keys from the client's master seed, it's a **two-phase build**: construct a bootstrap client, build the stores from it, then rebuild the client with the stores in `deps`. See the example app's [`lib/umbra-client.ts`](https://github.com/umbra-defi/examples/blob/main/private-payments-nextjs/lib/umbra-client.ts) for the full pattern.
</Note>

## 4. Register Your Account

Registration sets up your on-chain Umbra identity. It is idempotent: skipped sub-steps cost zero SOL, so you can call it safely on every app start.

Anonymous registration runs a Groth16 proof, so it **requires a `zkProver`** in the
factory's deps — omitting it throws `ZK prover is required for anonymous-mode registration`.

```typescript theme={null}
import { getUserRegistrationFunction } from "@umbra-privacy/sdk/registration";
import { getUserRegistrationProver } from "@umbra-privacy/sdk/zk-prover";

const register = getUserRegistrationFunction(
  { client },
  { zkProver: getUserRegistrationProver() },
);

const signatures = await register({
  confidential: true, // register the X25519 key (Shared-mode balances + receiver-burnable notes)
  anonymous: true,    // register the user commitment (anonymous mixer transfers) — needs the zkProver above
});

console.log(`Registered in ${signatures.length} transaction(s)`);
```

Internally registration is two independent on-chain instructions:

* **`register_user_for_confidential_usage`** — synchronous; initialises the `EncryptedUserAccount` and stores the X25519 token-encryption pubkey.
* **`register_user_for_anonymous_usage_v18`** — MPC + ZK; stages master-viewing-key material, runs Fiat-Shamir + Groth16, queues the Arcium MPC computation, and stores the on-chain user commitment.

Both `init_if_needed` the user account, so either may run first. The orchestrator picks the right combination based on requested flags and on-chain state.

## 5. Deposit Tokens (ATA → ETA)

Shield an SPL or Token-2022 balance by moving it from your public Associated Token Account into your Encrypted Token Account.

Amounts are branded `U64` and mints are branded `Address` — wrap raw values with
`createU64` (`@umbra-privacy/sdk/types`) and `address` (`@solana/kit`) so the calls
typecheck.

```typescript theme={null}
import { getATAIntoETADirectDepositorFunction } from "@umbra-privacy/sdk/deposit";
import { createU64 } from "@umbra-privacy/sdk/types";
import { address } from "@solana/kit";

const deposit = getATAIntoETADirectDepositorFunction({ client });

const MINT = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); // USDC (mainnet)
const amount = createU64({ value: 1_000_000n }); // 1 USDC (6 decimals)

const result = await deposit(
  signer.address, // credit your own ETA
  MINT,
  amount,
);

console.log("Deposit queued:", result.queueSignature);
if (result.callback?.status === "finalized") {
  console.log("Callback confirmed:", result.callback.signature);
}
```

## 6. Withdraw Tokens (ETA → ATA)

Move tokens back from your Encrypted Token Account to your public ATA. (`MINT` and
`amount` are the branded values from step 5.)

```typescript theme={null}
import { getETAIntoATAWithdrawerFunction } from "@umbra-privacy/sdk/withdrawal";

const withdraw = getETAIntoATAWithdrawerFunction({ client });

const result = await withdraw(
  signer.address, // destination ATA owner
  MINT,
  amount,
);

console.log("Withdraw queued:", result.queueSignature);
if (result.callback?.status === "finalized") {
  console.log("Callback confirmed:", result.callback.signature);
}
```

## 7. Create a Receiver-Burnable Stealth Pool Note

Send tokens privately to a recipient by writing a Stealth Pool Note into the mixer. The recipient can later scan and burn it with no on-chain link to you as the sender.

<Note>
  Creating a note requires a `zkProver` dependency for Groth16 proof generation. The
  prover ships inside the SDK at `@umbra-privacy/sdk/zk-prover` — there is no separate
  `@umbra-privacy/web-zk-prover` package any more.
</Note>

<Warning>
  Receiver-burnable creates encrypt the unlocker against the recipient's `userCommitment`,
  so the recipient **must** have completed all three registration sub-steps (account init,
  X25519 key, user commitment). If they haven't, fall back to a **self-burnable** create
  with `getATAIntoSelfBurnableStealthPoolNoteCreatorFunction` — the *sender* stays the
  unlocker and the recipient needs zero on-chain state. Note this is "park then hand off",
  not a direct credit: the funds remain spendable by you until you share the note's
  unlocking material out-of-band, so it suits one-shot transfers to someone you can reach
  off-chain. See [Stealth Pool Notes — Registration prerequisites](/concepts/utxos-and-mixer#registration-prerequisites-differ-by-variant)
  for the full pre-check rubric.
</Warning>

```typescript theme={null}
import { getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction } from "@umbra-privacy/sdk/deposit";
import { getATAIntoStealthPoolNoteCreatorProver } from "@umbra-privacy/sdk/zk-prover";
import { address } from "@solana/kit";

const zkProver = getATAIntoStealthPoolNoteCreatorProver();

const createNote = getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction(
  { client },
  { zkProver },
);

const RECIPIENT = address("RecipientWalletAddressHere");

const signatures = await createNote({
  destinationAddress: RECIPIENT,
  mint: MINT,
  amount,
});
console.log("Stealth Pool Note created:", signatures[0]);
```

<Note>
  **Spending from your shielded balance.** The factory above sources tokens from your
  public ATA. To create a note from your *existing* Encrypted Token Account balance
  (the common case once you've deposited), use the ETA-sourced creators —
  `getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction` /
  `getETAIntoSelfBurnableStealthPoolNoteCreatorFunction` — same call shape, paired with
  `getETAIntoStealthPoolNoteCreatorProver`.
</Note>

## 8. Scan for Burnable Notes

As the recipient, run the scanner. It walks every active stealth-pool tree, decrypts every commitment it can with your viewing keys, and groups the results by `(burn kind, source)`. Cursor state lives in `client.utxoDataStore` — the scanner picks up exactly where the last call left off.

```typescript theme={null}
import { getBurnableStealthPoolNoteScannerFunction } from "@umbra-privacy/sdk/burn";

const scan = getBurnableStealthPoolNoteScannerFunction({ client });

// Zero-arg. No (treeIndex, start, end) — the cursor is managed by the SDK.
const result = await scan();

console.log("Self-burnable from ETA: ", result.etaToStealthPoolSelfBurnable.length);
console.log("Receiver-burnable from ETA: ", result.etaToStealthPoolReceiverBurnable.length);
console.log("Self-burnable from ATA: ", result.ataToStealthPoolSelfBurnable.length);
console.log("Receiver-burnable from ATA: ", result.ataToStealthPoolReceiverBurnable.length);
console.log("Scanned trees:", result.scannedTrees.length);
```

<Warning>
  The scanner does **not** attach Merkle proofs to the returned notes. Proofs are fetched per batch at burn time so a single proof set is shared across an entire batch — far cheaper than fetching one proof per note. Pass the scanned notes (with a `kind` tag — see step 9) directly to a burner factory; do not call `enrichWithMerkleProof` yourself unless you are building a custom burn pipeline.
</Warning>

## 9. Burn the Note

Burning consumes a note, reveals its nullifier on-chain to prevent double-spend, and credits the destination balance. We'll burn the receiver-burnable note we just created, into the recipient's Encrypted Token Account.

The burner factory needs three dependencies: a `fetchBatchMerkleProof` (already exposed on `client`), a `zkProver` for the burn circuit, and a `relayer` adapter whose three methods (`submitBurn`, `pollBurnStatus`, `getRelayerAddress`) forward to the relayer service.

The scanner returns notes **without** a `kind` discriminator, but the burner input is
typed `DecryptedStealthPoolNoteData & { kind: "receiver-burnable" }` — tag each note
with `kind` before passing it in.

```typescript theme={null}
import {
  getReceiverBurnableStealthPoolNoteIntoETABurnerFunction,
} from "@umbra-privacy/sdk/burn";
import { getUmbraRelayer } from "@umbra-privacy/sdk";
import { getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver } from "@umbra-privacy/sdk/zk-prover";

const zkProver = getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver();
const r        = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com" });

const burn = getReceiverBurnableStealthPoolNoteIntoETABurnerFunction(
  { client },
  {
    fetchBatchMerkleProof: client.fetchBatchMerkleProof!,
    zkProver,
    relayer: {
      submitBurn:        r.submitClaim,        // alias: BurnSubmitter ≡ ClaimSubmitter
      pollBurnStatus:    r.pollClaimStatus,    // alias: BurnStatusPoller ≡ ClaimStatusPoller
      getRelayerAddress: r.getRelayerAddress,
    },
  },
);

// Tag the scanned notes with their burn kind (the scanner does not).
const notes = result.etaToStealthPoolReceiverBurnable.map(
  (n) => ({ ...n, kind: "receiver-burnable" as const }),
);
if (notes.length > 0) {
  const out = await burn([notes[0]]);
  for (const [batchIndex, batch] of out.batches) {
    console.log(
      "Batch",   batchIndex,
      "status:", batch.status,
      "id:",     batch.requestId,
      "sig:",    batch.callbackSignature ?? batch.txSignature,
    );
  }
}
```

<Note>
  For **self-burnable** notes use the matching burners + provers:
  `getSelfBurnableStealthPoolNoteIntoETABurnerFunction` /
  `getSelfBurnableStealthPoolNoteIntoATABurnerFunction`, with
  `getClaimSelfClaimableUtxoIntoEncryptedBalanceProver` /
  `getClaimSelfClaimableUtxoIntoPublicBalanceProver`. Tag those notes with
  `kind: "self-burnable"`.
</Note>

<Note>
  The `submitBurn` / `pollBurnStatus` property names map onto the relayer client's existing `submitClaim` / `pollClaimStatus` via plain TypeScript aliases (`BurnSubmitterFunction = ClaimSubmitterFunction`). The wire protocol still calls the endpoint `/v1/claims` — nothing has changed there.
</Note>

<Note>
  Batching: pass an array of notes. The receiver-into-ETA burner groups them by `destinationAddress`, chunks to ≤5 per proof, and runs each chunk through the relayer. Single-note burners (`self-into-ETA`, `self-into-ATA`) have `MAX_NOTES_PER_PROOF = 1` and loop internally — caller still passes an array.
</Note>

## Full Example

A runnable Node script (uses an ephemeral signer, so fund + reuse a real keypair to
take it past `register`). Stores are omitted — on Node the scanner falls back to
in-memory state; for browser persistence see the two-phase note in step 3.

```typescript theme={null}
import {
  createInMemorySigner,
  getUmbraClient,
  getUmbraRelayer,
} from "@umbra-privacy/sdk";
import { createU64 } from "@umbra-privacy/sdk/types";
import { address } from "@solana/kit";
import { getUserRegistrationFunction } from "@umbra-privacy/sdk/registration";
import {
  getATAIntoETADirectDepositorFunction,
  getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction,
} from "@umbra-privacy/sdk/deposit";
import { getETAIntoATAWithdrawerFunction } from "@umbra-privacy/sdk/withdrawal";
import {
  getBurnableStealthPoolNoteScannerFunction,
  getReceiverBurnableStealthPoolNoteIntoETABurnerFunction,
} from "@umbra-privacy/sdk/burn";
import {
  getUserRegistrationProver,
  getATAIntoStealthPoolNoteCreatorProver,
  getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver,
} from "@umbra-privacy/sdk/zk-prover";

async function main() {
  // 1. Signer (use your wallet adapter / a persisted keypair in production).
  const signer = await createInMemorySigner();

  // 2. Client (relayer is built separately, below).
  const client = await getUmbraClient({
    signer,
    network: "mainnet",
    rpcUrl: "https://api.mainnet-beta.solana.com",
    rpcSubscriptionsUrl: "wss://api.mainnet-beta.solana.com",
    indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com",
  });

  const MINT = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); // USDC

  // 3. Register (idempotent; anonymous mode needs the registration prover).
  const register = getUserRegistrationFunction(
    { client },
    { zkProver: getUserRegistrationProver() },
  );
  await register({ confidential: true, anonymous: true });

  // 4. Deposit (ATA → ETA).
  const deposit = getATAIntoETADirectDepositorFunction({ client });
  await deposit(signer.address, MINT, createU64({ value: 1_000_000n }));

  // 5. Withdraw (ETA → ATA).
  const withdraw = getETAIntoATAWithdrawerFunction({ client });
  await withdraw(signer.address, MINT, createU64({ value: 1_000_000n }));

  // 6. ZK provers (Groth16).
  const createProver = getATAIntoStealthPoolNoteCreatorProver();
  const burnProver   = getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver();

  // 7. Create a receiver-burnable Stealth Pool Note.
  const createNote = getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction(
    { client },
    { zkProver: createProver },
  );
  const RECIPIENT = address("RecipientWalletAddressHere");
  await createNote({ destinationAddress: RECIPIENT, mint: MINT, amount: createU64({ value: 500_000n }) });

  // 8. Scan (zero-arg, cursor in client.utxoDataStore).
  const scan = getBurnableStealthPoolNoteScannerFunction({ client });
  const result = await scan();

  // 9. Burn the first receiver-burnable note into the recipient's ETA.
  const r = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com" });
  const burn = getReceiverBurnableStealthPoolNoteIntoETABurnerFunction(
    { client },
    {
      fetchBatchMerkleProof: client.fetchBatchMerkleProof!,
      zkProver: burnProver,
      relayer: {
        submitBurn:        r.submitClaim,
        pollBurnStatus:    r.pollClaimStatus,
        getRelayerAddress: r.getRelayerAddress,
      },
    },
  );

  // Tag scanned notes with their burn kind before passing to the burner.
  const candidates = result.etaToStealthPoolReceiverBurnable.map(
    (n) => ({ ...n, kind: "receiver-burnable" as const }),
  );
  if (candidates.length > 0) {
    const out = await burn([candidates[0]]);
    for (const [, b] of out.batches) {
      console.log("Burn status:", b.status, "sig:", b.callbackSignature ?? b.txSignature);
    }
  }
}

main().catch(console.error);
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Wallet Adapters" icon="wallet" href="/sdk/wallet-adapters">
    Connect Phantom, Solflare, or any Solana wallet.
  </Card>

  <Card title="Stealth Pool" icon="shuffle" href="/sdk/mixer/overview">
    Anonymous transfers via Stealth Pool Notes.
  </Card>

  <Card title="Query State" icon="magnifying-glass" href="/sdk/query">
    Read encrypted balances and account state on-chain.
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation" href="/reference/errors">
    Recover from dropped relayer callbacks and partial burns.
  </Card>
</CardGroup>
