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