UPP — Universal Private PoolSDK Reference

Note Indexer

makeRpcIndexer — scanning the blockchain for encrypted notes belonging to your account.

Note Indexer

The note indexer scans the blockchain for NoteCreated events, attempts to decrypt each one using your viewing key, and returns notes that belong to you.

makeRpcIndexer

import { makeRpcIndexer } from '@permissionless-technologies/upp-sdk/indexer'

const indexer = makeRpcIndexer({
  publicClient,             // viem PublicClient
  contractAddress: Address, // UPP contract address
  fromBlock?: bigint,       // start block (default: contract deployment block)
})

Usage

// Scan for notes owned by this account
const { notes, lastScannedBlock } = await indexer.scan({
  viewingKey: myViewingKey,   // derived from wallet signature
  fromBlock?: bigint,         // optional incremental scan
})

Incremental Scanning

Store the last scanned block to avoid rescanning from genesis:

const lastBlock = localStorage.getItem('lastScannedBlock')

const { notes, lastScannedBlock } = await indexer.scan({
  viewingKey,
  fromBlock: lastBlock ? BigInt(lastBlock) : undefined,
})

localStorage.setItem('lastScannedBlock', lastScannedBlock.toString())

How It Works

  1. Fetch all NoteCreated(commitment, encryptedNote) events from the contract
  2. For each event, extract the ephemeral public key R from the encrypted data
  3. Compute the ECDH shared secret: shared = viewingKey * R
  4. Check the 64-bit search tag (filter ~99% of non-matching notes without decrypting)
  5. For tag matches, attempt AES-GCM decryption
  6. Verify the decrypted note against the on-chain commitment
  7. Return verified notes

Nullifier Verification

After collecting notes, the indexer checks which ones are spent:

const spentNullifiers = await indexer.getSpentNullifiers(notes)

const unspentNotes = notes.filter(
  note => !spentNullifiers.has(note.nullifier)
)

Custom Indexer

You can implement your own indexer by implementing the IIndexer interface:

interface IIndexer {
  scan(params: ScanParams): Promise<{ notes: Note[]; lastScannedBlock: bigint }>
  getSpentNullifiers(notes: Note[]): Promise<Set<Hex>>
}

This allows using a centralized indexer service or a subgraph for faster scanning, while keeping the scanning logic privacy-preserving (your viewing key never leaves your device).

On this page