> ## 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.

# Writing Stealth Pool Notes

> 4 V18 note-creator factory functions: get{ATA|ETA}IntoSelfBurnableStealthPoolNoteCreatorFunction and get{ATA|ETA}IntoReceiverBurnableStealthPoolNoteCreatorFunction. Merkle insertion + on-chain ciphertext.

## Overview

Writing a Stealth Pool Note inserts a Poseidon commitment into the on-chain Indexed Merkle Tree and locks the corresponding tokens in the pool. The SDK also publishes an X25519 + AES-GCM ciphertext on-chain so the unlocker can later discover the note with their viewing keys.

Choose the factory function that matches your source (`ATA` or `ETA`) and unlocker (self-burnable or receiver-burnable). All four factories live under `@umbra-privacy/sdk/deposit`.

## Registration prerequisites differ by variant

This is the most common integration footgun.

* **Self-burnable creates** encrypt the unlocker against the **sender's** master-seed-derived key. Sender needs `isUserAccountX25519KeyRegistered`. Recipient needs nothing.
* **Receiver-burnable creates** encrypt against the **recipient's** `userCommitment`. The recipient must have completed **all three** registration sub-step flags (`isInitialised`, `isUserAccountX25519KeyRegistered`, `isUserCommitmentRegistered`) on-chain.

**Always pre-check the recipient before a receiver-burnable create:**

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

const queryAccount = getUserAccountQuerierFunction({ client });
const r = await queryAccount(recipient);
const ready = r.state === "exists"
  && r.data?.isInitialised
  && r.data?.isUserAccountX25519KeyRegistered
  && r.data?.isUserCommitmentRegistered;

if (!ready) {
  // Either abort with a clear "ask the recipient to register" error,
  // or fall back to a self-burnable create — see "Anonymous payment fallback" below.
}
```

## Factory Functions

### Self-burnable from ATA

Single-tx, no MPC. Funds locked directly from your ATA. You unlock.

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

const zkProver = getATAIntoStealthPoolNoteCreatorProver();

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

const signatures = await createNote({
  destinationAddress: client.signer.address, // you unlock
  mint,
  amount,
});
```

### Receiver-burnable from ATA

Single-tx, no MPC. Funds locked from your ATA. **Recipient must be fully registered.**

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

const zkProver = getATAIntoStealthPoolNoteCreatorProver();

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

const RECIPIENT = "GsbwXfJraMomNxBcpR3DBFyKCCmN9SKGzKFJBNKxRFkT";

const signatures = await createNote({
  destinationAddress: RECIPIENT,
  mint,
  amount,
});
```

### Self-burnable from ETA

Two-tx pipeline (`createProofAccount` → `createUtxo`), MPC. Funds drawn from your ETA. You unlock.

```typescript theme={null}
import { getETAIntoSelfBurnableStealthPoolNoteCreatorFunction } from "@umbra-privacy/sdk/deposit";
import { getETAIntoStealthPoolNoteCreatorProver } from "@umbra-privacy/sdk/zk-prover";

const zkProver = getETAIntoStealthPoolNoteCreatorProver();

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

const signatures = await createNote({
  destinationAddress: client.signer.address,
  mint,
  amount,
});
```

### Receiver-burnable from ETA

Two-tx pipeline, MPC. Funds drawn from your ETA. **Recipient must be fully registered.**

```typescript theme={null}
import { getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction } from "@umbra-privacy/sdk/deposit";
import { getETAIntoStealthPoolNoteCreatorProver } from "@umbra-privacy/sdk/zk-prover";

const zkProver = getETAIntoStealthPoolNoteCreatorProver();

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

const signatures = await createNote({
  destinationAddress: RECIPIENT,
  mint,
  amount,
});
```

## Parameters

All four creator factories accept a `CreateUtxoArgs` object as the first argument:

<ParamField path="args.destinationAddress" type="Address" required>
  The wallet address that can unlock this note. For self-burnable, use `client.signer.address`. For receiver-burnable, use the recipient's address.
</ParamField>

<ParamField path="args.mint" type="Address" required>
  SPL or Token-2022 mint address. Must be a supported mint.
</ParamField>

<ParamField path="args.amount" type="bigint" required>
  Amount in native token units. Protocol fees and Token-2022 transfer fees are subtracted from this amount before the commitment is written.
</ParamField>

<ParamField path="options.generationIndex" type="U256">
  Optional deterministic generation index. Defaults to a random `U256` (CSPRNG). Pass the same `generationIndex` on retry to allow the V18 pipeline's `closeProofAccount` step to reclaim a proof-account orphan from a prior failed attempt.
</ParamField>

<ParamField path="options.optionalData" type="OptionalData32">
  32 bytes of opaque metadata stored alongside the note. **Must be pre-hashed or pre-encrypted** — never store plaintext identifiers.
</ParamField>

<ParamField path="options.accountInfoCommitment" type="Commitment" default="&#x22;confirmed&#x22;">
  Commitment level for RPC account reads during note construction.
</ParamField>

<ParamField path="options.epochInfoCommitment" type="Commitment" default="&#x22;confirmed&#x22;">
  (ATA-source only) Commitment level for epoch-info fetches (Token-2022 transfer-fee schedule).
</ParamField>

<ParamField path="options.hooks" type="ETAIntoStealthPoolNoteCreatorHooks | ATAIntoStealthPoolNoteCreatorHooks">
  Per-phase + per-step lifecycle hooks. Per-step slots use `{ onPreSend, onPostSend, onSkipped }`. Step slot names: `closeProofAccount` (fires only when a stale proof account from a prior failed attempt is reclaimed), `populateProofAccount`, `createStealthPoolNote` (ATA-source pipeline only; ETA-source uses `queueComputation` instead).
</ParamField>

<ParamField path="deps.zkProver" type="ZkProver" required>
  Circuit-specific Groth16 prover. **Required** for every variant — there is no built-in default. Provers ship at `@umbra-privacy/sdk/zk-prover`.
</ParamField>

<Note>
  V4 fields `priorityFees`, `purpose`, `awaitCallback`, `skipPreflight`, `maxRetries` do not exist on V5 note creators. The V4 `callbacks: { createProofAccount, createUtxo, closeProofAccount }` shape has been replaced by `options.hooks` (ATA-source) or `deps.hooks` (ETA-source) with `onPreSend`/`onPostSend`/`onSkipped` per-step events.
</Note>

## Return Value

The return type depends on the source:

* **From ATA (ZK-only):** `Promise<TransactionSignature[]>` of length 1 — `[noteWriteSig]`.
* **From ETA (MPC pipeline):** `Promise<TransactionSignature[]>` of length 2 — `[proofAccountSig, noteWriteSig]`.

## The ZK prover dependency

Note creation requires a ZK prover function (`zkProver` in `deps`). This function generates a Groth16 proof that the commitment was constructed correctly.

The prover is CPU-intensive — generating a proof takes 2–8 seconds in the browser and 1–3 seconds in Node.js. For browser applications, run the prover in a Web Worker to avoid blocking the main thread. See [ZK Provers](/sdk/advanced/zk-provers) for the canonical comlink pattern.

<Warning>
  The `zkProver` dependency is **required** and cannot be omitted. There is no built-in default. Use `@umbra-privacy/sdk/zk-prover` for the CDN-backed implementation.
</Warning>

## Anonymous payment fallback (receiver-burnable → self-burnable)

For one-shot payments to a recipient who may not be registered, prefer the **self-burnable ATA** variant + share a recovery secret out of band (or use the relayer's escrow flow). This avoids the "recipient must register first" UX trap:

```typescript theme={null}
import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk/query";
import {
  getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction,
  getATAIntoSelfBurnableStealthPoolNoteCreatorFunction,
} from "@umbra-privacy/sdk/deposit";
import {
  getATAIntoStealthPoolNoteCreatorProver,
} from "@umbra-privacy/sdk/zk-prover";

const queryAccount = getUserAccountQuerierFunction({ client });
const r = await queryAccount(RECIPIENT);
const recipientReady = r.state === "exists"
  && r.data?.isInitialised
  && r.data?.isUserAccountX25519KeyRegistered
  && r.data?.isUserCommitmentRegistered;

if (recipientReady) {
  const create = getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction(
    { client },
    { zkProver: getATAIntoStealthPoolNoteCreatorProver() },
  );
  await create({ destinationAddress: RECIPIENT, mint, amount });
} else {
  // Fallback: sender stays the unlocker, hand over the secret out of band.
  const create = getATAIntoSelfBurnableStealthPoolNoteCreatorFunction(
    { client },
    { zkProver: getATAIntoStealthPoolNoteCreatorProver() },
  );
  await create({ destinationAddress: client.signer.address, mint, amount });
}
```

## Error Handling

Create-side failures have two distinguishing modes — ZK proof generation and stale on-chain state. Use `isCreateUtxoError` from `@umbra-privacy/sdk/errors`:

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

try {
  const signatures = await createNote({ destinationAddress: recipient, mint, amount });
} catch (err) {
  if (isCreateUtxoError(err)) {
    switch (err.stage) {
      case "zk-proof-generation":
        console.error("Proof generation failed:", err.message);
        break;
      case "transaction-sign":
        console.log("Cancelled.");
        break;
      case "account-fetch":
        console.error("RPC error:", err.message);
        break;
      case "transaction-send":
        // May have landed. Re-run the scanner to check whether the commitment was inserted.
        console.warn("Confirmation timeout. Check on-chain before retrying.");
        break;
      default:
        console.error("Create failed at:", err.stage, err);
    }
  } else {
    throw err;
  }
}
```

<Warning>
  On `transaction-send`, do **not** immediately retry. Run the scanner first — the commitment may already be in the tree. If retrying an ETA-source create, you must pass the same `generationIndex` via `options.generationIndex` so the SDK's `closeProofAccount` step can reclaim the orphan from your prior attempt (see [Recovery](/sdk/advanced/recovery)).
</Warning>

See [Error Handling](/reference/errors) for a full reference.

## Concurrency rule

Never run note creates concurrently from the same client. The SDK auto-derives `generationIndex` from on-chain account state during each call; two parallel `Promise.all`-style creates read the same `generationIndex` before either has incremented it, derive identical ephemeral keys, and collide silently (fund loss / scan failure). Serialise creates per `(signer, network)`.

## Protocol Fees

Fees are deducted from `amount` before the commitment is written. The net amount committed (= what the unlocker receives at burn time) is `amount − fees`. See [Pricing](/pricing) for current rates.
