Skip to content

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:

FieldDescription
npkNullifier Public Key - derives who can spend
amountThe value stored in this note
randomRandom blinding factor for privacy
tokenIdWhich 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:

  1. Filter by token ID
  2. Sort by amount (largest first)
  3. 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

  1. Commitments: Fetch all note commitments from pool
  2. Notes: Decrypt commitments to find your notes
  3. 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

Released under the MIT License.