Skip to main content

Overview

After tokens are deposited into the mixer, the depositor publishes an encrypted ciphertext on-chain addressed to the recipient’s X25519 key. To claim, the recipient must:
  1. Fetch all ciphertexts from the indexer
  2. Attempt to decrypt each one using their X25519 private key
  3. Successfully decrypted ciphertexts are their claimable UTXOs
  4. Fetch the Merkle proof for each claimable UTXO
getClaimableUtxoScannerFunction handles all of these steps automatically.
This function requires the indexer. Ensure indexerApiEndpoint is set when creating the client with getUmbraClient, or the function will throw.

Usage

import { getClaimableUtxoScannerFunction } from "@umbra-privacy/sdk";

const scan = getClaimableUtxoScannerFunction({ client });

const result = await scan(
  treeIndex,           // which Merkle tree to scan (U32)
  startInsertionIndex, // start scanning from this leaf position (U32)
  endInsertionIndex?,  // optional upper bound (U32)
);

Parameters

treeIndex
U32
required
The zero-based index of the Merkle tree to scan. Start with 0 for the first tree. If the current tree is filling up, increment to check the next one. This is a branded U32 type, not a plain number.
startInsertionIndex
U32
required
The leaf position to start scanning from (inclusive). Pass 0 to scan from the beginning. To resume from where you left off, pass the insertion index of the last UTXO you have already seen. This is a branded U32 type, not a plain number.
endInsertionIndex
U32
The leaf position to stop at (inclusive). If omitted, scans to the end of the current tree. Use this to limit the scan range when you know the approximate insertion window of your UTXOs. This is a branded U32 type, not a plain number.

Return Value

type ScannedUtxoResult = {
  selfBurnable: ClaimableUtxoData[];       // UTXOs you created yourself (from encrypted balance)
  received: ClaimableUtxoData[];           // UTXOs sent to you by others (from encrypted balance)
  publicSelfBurnable: ClaimableUtxoData[]; // UTXOs you created yourself (from public balance)
  publicReceived: ClaimableUtxoData[];     // UTXOs sent to you by others (from public balance)
};
Each ClaimableUtxoData is a flat structure containing both the UTXO data and its Merkle proof. These objects are passed directly to the claim functions — no destructuring needed.

Example: Fetch All UTXOs for Tree 0

import { getClaimableUtxoScannerFunction } from "@umbra-privacy/sdk";

const scan = getClaimableUtxoScannerFunction({ client });

const result = await scan(0, 0); // scan all of tree 0

console.log("Self-burnable UTXOs:", result.selfBurnable.length);
console.log("Received UTXOs:", result.received.length);

for (const utxo of result.selfBurnable) {
  console.log(
    `UTXO: amount=${utxo.amount}`
  );
}

Example: Paginated Scan

For large trees, scan in chunks to avoid timeouts:
const CHUNK_SIZE = 10_000;

async function fetchAllUtxos(treeIndex: U32) {
  const scan = getClaimableUtxoScannerFunction({ client });
  const allSelfBurnable = [];
  const allReceived = [];

  let cursor = 0;
  while (true) {
    const result = await scan(treeIndex, cursor, cursor + CHUNK_SIZE - 1);
    allSelfBurnable.push(...result.selfBurnable);
    allReceived.push(...result.received);

    // Stop if we've reached the end of the tree
    if (result.selfBurnable.length + result.received.length === 0
        && cursor + CHUNK_SIZE >= 1_048_576) {
      break;
    }
    cursor += CHUNK_SIZE;
  }

  return { selfBurnable: allSelfBurnable, received: allReceived };
}

How Decryption Works

The SDK derives your X25519 private key from the master seed, then for each UTXO ciphertext:
  1. Extracts the depositor’s ephemeral X25519 public key from the ciphertext header
  2. Computes an X25519 ECDH shared secret
  3. Derives an AES-GCM key from the shared secret
  4. Attempts to decrypt the 68-byte payload
  5. If decryption succeeds, checks the 12-byte domain separator to categorize the UTXO type
Your private key never leaves your device. The decryption happens entirely in the SDK.

Error Handling

Fetching UTXOs involves two distinct infrastructure dependencies: the Umbra indexer (for ciphertext discovery) and the RPC node (for Merkle proof data). Use isFetchUtxosError from @umbra-privacy/sdk/errors and switch on err.stage to handle each failure point.
import { isFetchUtxosError } from "@umbra-privacy/sdk/errors";

try {
  const result = await fetch(treeIndex, startInsertionIndex);
} catch (err) {
  if (isFetchUtxosError(err)) {
    switch (err.stage) {
      case "initialization":
        // Factory construction failed - indexerApiEndpoint was not configured.
        // Ensure indexerApiEndpoint is set when calling getUmbraClient.
        console.error("Indexer not configured:", err.message);
        break;

      case "validation":
        // Invalid treeIndex or insertion index parameters.
        console.error("Invalid fetch parameters:", err.message);
        break;

      case "key-derivation":
        // X25519 private key derivation from master seed failed.
        console.error("Key derivation failed:", err.message);
        break;

      case "indexer-fetch":
        // Indexer HTTP call failed - unreachable, rate-limited, or returned an error.
        console.error("Indexer fetch failed:", err.message);
        showNotification("Could not reach the network. Please check your connection.");
        break;

      case "proof-fetch":
        // Merkle proof HTTP call failed.
        console.error("Proof fetch failed:", err.message);
        break;
    }
  } else {
    throw err;
  }
}
An empty result (selfBurnable: [], received: []) is not an error - it means no UTXOs addressed to you were found in that scan range. Errors are only thrown for infrastructure failures.
See Error Handling for a full reference of all error types.

Storing UTXO State

The SDK does not persist UTXO state between calls. If your application needs to track which UTXOs have been claimed, maintain your own list (keyed by insertionIndex + treeIndex) and exclude already-claimed entries from future claims. A claimed UTXO’s nullifier is burned on-chain - attempting to claim it again will fail at the on-chain program level.