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

# Scanning for Notes

> getBurnableStealthPoolNoteScannerFunction (V18, zero-arg): walks every active tree, decrypts X25519-AES ciphertexts addressable by your viewing keys, persists cursor in client.utxoDataStore. Returns ScannedStealthPoolNoteResult grouped by (kind, source).

## Overview

After a Stealth Pool Note is written, the SDK publishes an X25519-AES ciphertext on-chain addressed either to the sender (self-burnable) or to the recipient (receiver-burnable). To find your burnable notes, the scanner:

1. Discovers every active stealth-pool tree.
2. Loads scan progress from `client.utxoDataStore` and computes the unscanned ranges.
3. Fetches the encrypted note data for those ranges from the indexer.
4. Tries to decrypt each ciphertext with your X25519 + viewing keys.
5. Persists progress.
6. Returns the successful decryptions, grouped by `(kind, source)`.

The V18 scanner is **zero-arg** — the cursor is fully SDK-managed.

<Warning>
  The scanner requires the indexer. Ensure `indexerApiEndpoint` is set when constructing the client. The browser scanner is several orders of magnitude faster when `utxoDataStore` and `nullifierStore` are wired — without them, every call re-scans every active tree from genesis.
</Warning>

## Usage

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

const scan = getBurnableStealthPoolNoteScannerFunction({ client });

const result = await scan(); // zero-arg in V18
```

## Return Value

```typescript theme={null}
interface ScannedStealthPoolNoteResult {
  readonly etaToStealthPoolSelfBurnable: readonly DecryptedStealthPoolNoteData[];
  readonly etaToStealthPoolReceiverBurnable: readonly DecryptedStealthPoolNoteData[];
  readonly ataToStealthPoolSelfBurnable: readonly DecryptedStealthPoolNoteData[];
  readonly ataToStealthPoolReceiverBurnable: readonly DecryptedStealthPoolNoteData[];
  readonly scannedTrees: readonly ScannedTreeProgress[];
}

interface ScannedTreeProgress {
  readonly treeIndex: U128;
  readonly scannedRange: { start: U32; end: U32 } | null;
  readonly totalLeaves: U32;
  readonly fullyScanned: boolean;
}
```

The four note buckets are exhaustive — every burnable note maps onto exactly one. Within each bucket, the notes are **not** Merkle-proof-bundled: proofs are fetched per batch at burn time so a single proof set is shared across an entire burn batch (much cheaper than fetching one proof per note).

## Example: Run the scanner

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

const scan   = getBurnableStealthPoolNoteScannerFunction({ client });
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);

for (const tree of result.scannedTrees) {
  console.log(
    `Tree ${tree.treeIndex}: scanned ${tree.scannedRange?.start}–${tree.scannedRange?.end} of ${tree.totalLeaves}${tree.fullyScanned ? " (full)" : ""}`,
  );
}
```

## Idempotent — call as often as you like

The scanner is idempotent: subsequent calls only fetch and decrypt new leaves since the last call. The cursor lives in `client.utxoDataStore.getScanProgress(treeIndex)` / `client.utxoDataStore.addScannedRange(treeIndex, start, end)`. Re-mounting the scanner factory does not reset state.

A typical browser app calls the scanner on every page load and on a 30s timer while the page is open.

## How decryption works

The SDK derives your X25519 private key from the master seed, then for each note ciphertext:

1. Extracts the writer's ephemeral X25519 public key from the ciphertext header.
2. Computes an [X25519 ECDH](https://www.rfc-editor.org/rfc/rfc7748) shared secret.
3. Derives an [AES-GCM](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf) key.
4. Attempts to decrypt the payload.
5. If decryption succeeds, reads the 12-byte domain separator and routes the note into the matching `(kind, source)` bucket.

Your private key never leaves your device.

## Custom indexer fetchers

If you are running a private indexer mirror, override the data fetchers via `deps`:

```typescript theme={null}
const scan = getBurnableStealthPoolNoteScannerFunction(
  { client },
  {
    fetchStealthPoolNoteData: async (treeIndex, range)      => mirror.fetchNotes(treeIndex, range),
    fetchMerkleProof:         async (treeIndex, leafIndex)  => mirror.fetchProof(treeIndex, leafIndex),
    // Multi-key wallets can scan with extra viewing keys:
    additionalX25519PrivateKeys: [otherPrivKey],
  },
);
```

(The burner factory's per-batch `fetchBatchMerkleProof` is a separate dep on the burner — see [Burning](/sdk/mixer/claiming-utxos).)

## Error Handling

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

try {
  const result = await scan();
} catch (err) {
  if (isFetchUtxosError(err)) {
    switch (err.stage) {
      case "initialization":
        // The scanner was constructed without an indexer endpoint.
        console.error("Indexer not configured:", err.message);
        break;
      case "key-derivation":
        console.error("Viewing-key derivation failed:", err.message);
        break;
      case "indexer-fetch":
        // The indexer was unreachable or returned an error.
        console.error("Indexer fetch failed:", err.message);
        break;
      case "proof-fetch":
        // The burner's per-batch Merkle proof fetch failed.
        console.error("Merkle proof fetch failed:", err.message);
        break;
      case "proof-enrichment":
        // enrichWithMerkleProof failed to attach a proof entry to a scanned note.
        console.error("Proof enrichment failed:", err.message);
        break;
    }
  } else {
    throw err;
  }
}
```

<Note>
  An empty result (all buckets empty) is **not** an error — it means no notes addressable by your viewing keys exist in the trees the scanner walked. Errors are reserved for infrastructure failures.
</Note>

## Burn the result

Pass the scanned buckets directly to the matching burner factory — the burner fetches per-batch proofs internally:

```typescript theme={null}
import {
  getReceiverBurnableStealthPoolNoteIntoETABurnerFunction,
  getSelfBurnableStealthPoolNoteIntoETABurnerFunction,
  getSelfBurnableStealthPoolNoteIntoATABurnerFunction,
} from "@umbra-privacy/sdk/burn";

// Burner factory setup omitted — see /sdk/mixer/claiming-utxos.

// Burn all receiver-burnable notes from ATA-source writes into your ETA.
await burnReceiverIntoEta(result.ataToStealthPoolReceiverBurnable);
```

See [Burning](/sdk/mixer/claiming-utxos) for full burner factory setup, batching behaviour, and dropped-callback recovery.
