Discovering Notes

Discovery scans your channels and decrypts the notes addressed to you. It is a query, not a transaction - no proof, no fee, no submission. Use it to show the user's private balance and to feed .inputs(...).

Snippets assume transfers, account and provider from Getting Started.

const { notes, timestamp } = await transfers.discoverNotes({
  tokens: [BigInt(tokenAddress)], // token filter entries MUST be bigint
})

// notes: AddressMap<Note[]> keyed by BIGINT token address
const tokenNotes = notes.get(BigInt(tokenAddress)) ?? []
const balance = tokenNotes.reduce((sum, note) => sum + note.amount, 0n)

AddressMap

The result map normalizes address keys - but as bigints, not strings:

notes.get(tokenAddress) // undefined - string key never matches
notes.get(tokenAddress.toLowerCase()) // still undefined
notes.get(BigInt(tokenAddress)) // works

// Iterating across all tokens is always safe:
for (const tokenNotes of notes.values()) {
  // ...
}

Incremental scans with a cursor

Discovery without a cursor scans from the beginning. Persist the cursor from the underlying provider (or better: reuse the registry) so repeat scans only cover new blocks:

const { notes } = await transfers.discoverNotes({
  tokens: [BigInt(tokenAddress)],
  cursor: previousCursor, // resume where the last scan stopped
})

The higher-level version of the same idea is the registry: every execute() returns an updated PrivateRegistry (channels + notes + cursor). Pass it back into the next build({ registry }) and combine with autoDiscover: { notes: "missing" } to fetch only what the registry lacks - "refresh" re-scans, "all" rebuilds from scratch.

Discovering channels

const { channels, total } = await transfers.discoverChannels("all", {
  cursor: previousChannelCursor,
})
// channels: AddressMap<Channel> keyed by recipient address

Pass "all" or a list of recipient addresses to filter. Channels hold the shared key and per-token nonces; you mostly treat them as opaque values that the registry manages for you.

Things to notice

  • A note becomes visible to discovery once its transaction is accepted, but it is only spendable 10 blocks after creation. Check note.created against the current block before offering it as an input.
  • Discovery cost scales with unscanned history - a fresh full scan on a long-lived pool is slow on RPC-based providers. Cursors and the registry exist precisely to avoid it.

Next: Discovery Providers - choosing what backs these scans.