Skip to content

Custom storage ​

The wallet persists three kinds of state, each through its own interface:

StateInterfaceconnect option
decrypted notes and sync cursorNoteStorestorage.notes
Merkle treeTreePersistencestorage.tree
spent-nullifier setNullifierPersistencestorage.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.

ts
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:

ts
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 ​

RequirementReason
Save the entire NotesFile, including cursor and versiona 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 tabthe wallet does not detect external changes
Encrypt the stored filesee 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.

Next ​