Notes & Balances
Understanding how balances work in the privacy pool.
What is a Note?
A note is a private UTXO (Unspent Transaction Output). It contains:
| Field | Description |
|---|---|
npk | Nullifier Public Key - derives who can spend |
amount | The value stored in this note |
random | Random blinding factor for privacy |
tokenId | Which token (0 = ETH) |
Notes are encrypted so only the owner can read them.
Note Lifecycle
Shield (create) Transfer (spend + create) Unshield (spend)
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Note A │ ──spend──▶ │ Note A │ ──spend──▶ │ Note C │
│ 100 USDC │ │ (nullified) │ (nullified)
└───────────┘ └───────────┘ └───────────┘
│
▼
┌───────────┐
│ Note B │ (change)
│ 60 USDC │
└───────────┘
┌───────────┐
│ Note C │ (to recipient)
│ 40 USDC │
└───────────┘Checking Balance
Your balance is the sum of all unspent notes:
typescript
await client.sync();
// Total ETH balance
const balance = client.getBalance();
// Specific token
const usdcBalance = client.getBalanceByTokenId(BigInt(tokenAddress));
// All tokens
const allBalances = client.getAllBalances();
for (const [tokenId, amount] of allBalances) {
console.log(`Token ${tokenId}: ${amount}`);
}Viewing Notes
typescript
// All unspent notes
const notes = client.getUnspentNotes();
for (const note of notes) {
console.log({
amount: note.note.amount,
tokenId: note.tokenId,
leafIndex: note.leafIndex,
});
}
// Notes for specific token
const usdcNotes = client.getUnspentNotes(BigInt(tokenAddress));Note Selection
When transferring, the SDK selects notes automatically:
- Filter by token ID
- Sort by amount (largest first)
- Select minimum notes to cover amount + fee
Manual Selection
typescript
const notes = client.getUnspentNotes();
// Pick specific notes
await client.transfer({
recipient: 'zks1...',
amount: parseEther('50'),
inputNotes: [notes[0], notes[1]], // Use these specific notes
});Note Consolidation
If you have many small notes, consolidate them:
typescript
const notes = client.getUnspentNotes();
if (notes.length > 10) {
// Merge 2 notes at a time
await client.consolidate({
notes: notes.slice(0, 2),
});
}Benefits:
- Faster future transactions
- Lower proof complexity
- Cleaner state
Syncing
Notes are discovered by syncing with the blockchain:
typescript
// Initial sync (slower)
await client.sync();
// With progress callback
await client.sync((progress) => {
console.log(`${progress.phase}: ${progress.current}/${progress.total}`);
});Sync Phases
- Commitments: Fetch all note commitments from pool
- Notes: Decrypt commitments to find your notes
- Nullifiers: Check which notes are spent
QuickSync
With QuickSync configured, initial sync is much faster:
typescript
const client = new PrivacyClient({
quickSyncUrl: 'https://sync.zkprivacy.dev',
// ...
});QuickSync provides:
- Pre-indexed commitment tree
- WebSocket for real-time updates
- Cached note data
Caching
The SDK caches sync state in IndexedDB:
typescript
// Clear cache (force full resync)
await client.clearCache();Cache stores:
- Merkle tree state
- Your decrypted notes
- Nullifier set
Note Privacy
What's private:
- ✅ Note contents (encrypted)
- ✅ Who owns notes
- ✅ Links between transactions
What's public:
- ❌ Commitment hashes (in Merkle tree)
- ❌ Nullifiers (when spent)
- ❌ Total pool size