Storage Adapters
The SDK uses a StorageAdapter interface for all persistence. You can use a built-in adapter or implement your own.
Built-in Adapters
MemoryStore
In-memory storage. Default when no storage is provided. Data is lost on page reload / process exit.
import { MemoryStore } from '@ocash/sdk';
const sdk = createSdk({
chains: [...],
storage: new MemoryStore({ maxOperations: 100 }),
});IndexedDbStore (Browser)
Persistent browser storage using IndexedDB. Import from @ocash/sdk/browser:
import { createSdk, IndexedDbStore } from '@ocash/sdk/browser';
const sdk = createSdk({
chains: [...],
storage: new IndexedDbStore({
dbName: 'myapp', // Default: 'ocash_sdk'
storeName: 'ocash_store', // Default: 'ocash_store'
maxOperations: 200,
}),
});FileStore (Node.js)
JSON file-based storage. Import from @ocash/sdk/node:
import { createSdk, FileStore } from '@ocash/sdk/node';
const sdk = createSdk({
chains: [...],
storage: new FileStore({
baseDir: './data',
maxOperations: 500,
}),
});Files are stored as {baseDir}/{walletId}.store.json.
KeyValueStore
Wraps any key-value client (Redis, SQLite, custom):
import { KeyValueStore, RedisStore, SqliteStore } from '@ocash/sdk';
// Redis
const redisStore = new RedisStore({ url: 'redis://localhost:6379' });
// SQLite
const sqliteStore = new SqliteStore({ filename: './data.db' });
// Generic KV
const kvStore = new KeyValueStore({
client: myKvClient, // implements get/set/del
});StorageAdapter Interface
Required methods:
interface StorageAdapter {
// UTXO management
upsertUtxos(utxos: UtxoRecord[]): Promise<void>;
listUtxos(query?: ListUtxosQuery): Promise<{ total: number; rows: UtxoRecord[] }>;
markSpent(input: { chainId: number; nullifiers: Hex[] }): Promise<number>;
// Sync cursors
getSyncCursor(chainId: number): Promise<SyncCursor | undefined>;
setSyncCursor(chainId: number, cursor: SyncCursor): Promise<void>;
// Lifecycle (optional)
init?(options?: { walletId?: string }): Promise<void> | void;
close?(): Promise<void> | void;
}Optional methods for enhanced functionality:
interface StorageAdapter {
// ... required methods above
// Operations history
createOperation?<T extends OperationType>(input: ...): StoredOperation;
updateOperation?(id: string, patch: Partial<StoredOperation>): void;
listOperations?(input?: number | ListOperationsQuery): StoredOperation[];
deleteOperation?(id: string): boolean;
clearOperations?(): void;
pruneOperations?(options?: { max?: number }): number;
// Merkle tree state
getMerkleTree?(chainId: number): Promise<MerkleTreeState | undefined>;
setMerkleTree?(chainId: number, tree: MerkleTreeState): Promise<void>;
getMerkleLeaves?(chainId: number): Promise<Array<{ cid: number; commitment: Hex }>>;
appendMerkleLeaves?(chainId: number, leaves: ...): Promise<void>;
getMerkleNode?(chainId: number, id: string): Promise<MerkleNodeRecord | undefined>;
upsertMerkleNodes?(chainId: number, nodes: MerkleNodeRecord[]): Promise<void>;
// Entry data cache
upsertEntryMemos?(memos: EntryMemoRecord[]): Promise<number>;
listEntryMemos?(query: ListEntryMemosQuery): Promise<{ total; rows }>;
upsertEntryNullifiers?(nullifiers: EntryNullifierRecord[]): Promise<number>;
listEntryNullifiers?(query: ...): Promise<{ total; rows }>;
}Wallet Scoping
When wallet.open({ seed }) is called, the storage is initialized with a walletId derived from the seed. This ensures different wallets use separate namespaces.
Switching wallets (calling close() then open() with a different seed) clears in-memory state and switches the storage namespace.
Operations Governance
For long-running applications, use maxOperations to cap history size:
const store = new FileStore({
baseDir: './data',
maxOperations: 200, // Auto-prune oldest records
});
// Manual cleanup
store.pruneOperations({ max: 100 });
store.deleteOperation('op-123');
store.clearOperations();Custom Adapter
Implement the StorageAdapter interface for any backend:
class MyCustomStore implements StorageAdapter {
async upsertUtxos(utxos: UtxoRecord[]) { /* ... */ }
async listUtxos(query?: ListUtxosQuery) { /* ... */ }
async markSpent(input: { chainId: number; nullifiers: Hex[] }) { /* ... */ }
async getSyncCursor(chainId: number) { /* ... */ }
async setSyncCursor(chainId: number, cursor: SyncCursor) { /* ... */ }
}Only the five required methods need to be implemented. Optional methods enable additional features (operation history, Merkle caching, entry data caching) but are not required for basic functionality.