Custom storage
The wallet persists three kinds of state, each through its own interface:
| State | Interface | connect option |
|---|---|---|
| decrypted notes and sync cursor | NoteStore | storage.notes |
| Merkle tree | TreePersistence | storage.tree |
| spent-nullifier set | NullifierPersistence | storage.nullifiers |
All three default to memory. A wallet without persistent storage re-downloads and re-scans the note feed on every start. The interfaces are in @lelantos-org/sdk/advanced.
This page covers NoteStore. For the tree and nullifier set, see Persisting the tree and spent set.
Implementing NoteStore
NoteStore has two methods: load() returns the full notes file, and save() replaces it.
import type { NoteStore, NotesFile } from "@lelantos-org/sdk/advanced";
export class IndexedDbNoteStore implements NoteStore {
async load(): Promise<NotesFile> {
const json = (await idbGet("lelantos-notes")) ?? '{"version":1,"notes":[]}';
return JSON.parse(json) as NotesFile;
}
async save(file: NotesFile): Promise<void> {
await idbSet("lelantos-notes", JSON.stringify(file));
}
}StoredNote encodes bigint fields as decimal strings, so NotesFile serializes with plain JSON.stringify.
Persist cursor
NotesFile.cursor is the sync resume position. A store that drops it — for example by saving only notes — causes every sync to re-scan from the beginning, with no error. Save the whole object passed to save().
Pass the store when connecting:
import { connect } from "@lelantos-org/sdk";
const wallet = await connect({
network: "base",
rpcUrl,
privateKey,
storage: { notes: new IndexedDbNoteStore() },
});createWallet takes the same store as noteStore, and connectWatch as storage.notes. InMemoryNoteStore is the default implementation.
Implementation requirements
| Requirement | Reason |
|---|---|
Save the entire NotesFile, including cursor and version | a missing cursor forces a full re-scan; a missing version makes the wallet refuse to open |
Make save() atomic (write to a temporary key, then rename; or use a transactional store) | a partially written file that fails to parse forces a full re-scan |
Call wallet.sync({ reload: true }) after modifying the store outside the SDK, such as from another tab | the wallet does not detect external changes |
| Encrypt the stored file | see Contents |
The SDK serializes its own reads and writes to the store, so save() is never called concurrently by one wallet.
Schema version
The current NotesFile version is 3. Each stored note records d, the diversifier of the address it was received at, and is spendable only under that address's pk. The SDK does not upgrade files: opening a wallet whose store holds another version rejects WALLET_CONFIG. Clear the store (or have load() return an empty version-3 file) and the next sync re-scans the feed.
Contents
The notes file contains the commitments the wallet owns, their values, the memos received with them as plain text, and the sync cursor. It does not contain nsk or nullifiers; nullifiers are computed in memory and never written.
The notes file is sensitive
The file cannot be used to spend, but it links its holder to every commitment the wallet owns on chain. Encrypt it at rest.
Reducing file size
wallet.compact() removes notes already marked spent and returns the number removed. Balances are unchanged.