@orbinum/sdk 0.25.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,21 +1,52 @@
1
1
  # @orbinum/sdk
2
2
 
3
- Official TypeScript SDK for Orbinum — a privacy-focused blockchain built on Substrate with an EVM compatibility layer.
3
+ Official TypeScript SDK for Orbinum — a privacy-focused blockchain built on
4
+ Substrate with an EVM compatibility layer.
5
+
6
+ **This package is the whole wallet.** Storing notes encrypted, finding your own
7
+ without telling anyone which they are, and spending them all live here — not
8
+ just the cryptography under them. What a host supplies is UI, transport, and
9
+ platform adapters.
10
+
11
+ The package is environment-agnostic: it uses WebCrypto through the global
12
+ `crypto`, and touches no DOM and no Node built-in. It runs in a browser, in an
13
+ extension service worker, in React Native, in Node 18+, in Deno, in Bun, and
14
+ inside a Web Worker.
15
+
16
+ ## How it is organised
17
+
18
+ Deep dives live in [`docs/`](./docs): [the SDK's architecture as a
19
+ package](./docs/sdk-architecture.md), and the [note model
20
+ series](./docs/notes/README.md) — cryptography, identity, vault, discovery,
21
+ spending.
22
+
23
+ Four layers, each depending only downward. Where a symbol sits tells you what it
24
+ needs:
25
+
26
+ | Layer | Contents | Needs |
27
+ | ------------ | ------------------------------------------ | ------------ |
28
+ | `foundation` | encoding, crypto primitives, formatting | nothing |
29
+ | `protocol` | what a note IS — build, seal, find, select | no chain |
30
+ | `chain` | clients, RPC, the pallets | a connection |
31
+ | `wallet` | vault, scanner, spend ops, identity | both |
32
+
33
+ Everything is re-exported from the package root, so this matters for reading the
34
+ source rather than for importing. The two exceptions are the subpaths under
35
+ [Entry points](#entry-points), which exist because each needs something only a
36
+ platform can give.
4
37
 
5
38
  ## Installation
6
39
 
7
40
  ```bash
8
- npm install @orbinum/sdk
9
- # or
10
- pnpm add @orbinum/sdk
41
+ pnpm add @orbinum/sdk polkadot-api @polkadot/util-crypto
11
42
  ```
12
43
 
13
- ## Requirements
14
-
15
- - Node.js 18 or later
16
- - A running Orbinum node (Substrate WebSocket endpoint)
44
+ `polkadot-api` and `@polkadot/util-crypto` are **peer dependencies**. They are
45
+ singletons in practice — a second copy of `polkadot-api` means a second
46
+ connection and a second view of chain state — so the host application owns the
47
+ version.
17
48
 
18
- ## Quick Start
49
+ ## Quick start
19
50
 
20
51
  ```ts
21
52
  import { OrbinumClient } from '@orbinum/sdk';
@@ -25,24 +56,325 @@ const client = await OrbinumClient.connect({
25
56
  evmRpc: 'http://localhost:9933', // optional
26
57
  });
27
58
 
28
- const info = await client.substrate.getChainInfo();
29
- const root = await client.rpcV2.privacy.getMerkleRoot();
30
- const block = await client.substrate.getBlock('best');
31
- const isValidator = await client.rpcV2.chain.isValidator('5GrwvaEF...');
59
+ const info = await client.substrate.getChainInfo();
60
+ const root = await client.privacy.getMerkleRoot();
61
+ ```
62
+
63
+ `OrbinumClient` exposes:
64
+
65
+ | Property | Description |
66
+ | ---------------------- | -------------------------------------------------------- |
67
+ | `client.substrate` | Substrate RPC — blocks, events, transactions |
68
+ | `client.evm` | EVM JSON-RPC client (`null` when `evmRpc` is unset) |
69
+ | `client.evmExplorer` | Enriched EVM queries (`null` when `evmRpc` is unset) |
70
+ | `client.privacy` | `privacy_*` RPC — Merkle roots, proofs, nullifier status |
71
+ | `client.chain` | `chain_*` RPC — validator queries |
72
+ | `client.shieldedPool` | Shielded-pool extrinsics |
73
+ | `client.zkVerifier` | Circuit versions and verification keys |
74
+ | `client.relayerStatus` | Relayer availability |
75
+ | `client.precompiles` | EVM precompile wrappers (`null` when `evmRpc` is unset) |
76
+
77
+ Every module also takes a `SubstrateClient` by constructor, so it can be used
78
+ without the `OrbinumClient` facade.
79
+
80
+ ## Note cryptography
81
+
82
+ The shielded-pool primitives are pure and usable on their own — no client, no
83
+ transport:
84
+
85
+ ```ts
86
+ import {
87
+ NoteBuilder,
88
+ tryDecryptNote,
89
+ deriveViewingSecretKey,
90
+ deriveViewingPublicKey,
91
+ } from '@orbinum/sdk';
92
+
93
+ const ivsk = deriveViewingSecretKey(spendingKey);
94
+ const ivk = deriveViewingPublicKey(ivsk);
95
+
96
+ // A wallet scans by trying every pool hint against its own keys.
97
+ const note = tryDecryptNote(hint, ivsk, spendingKey, ownerPk);
98
+ ```
99
+
100
+ Two mechanisms make that scan cheaper by letting the receiver _predict_ the
101
+ ephemeral key a sender will publish, turning a per-note elliptic-curve
102
+ multiplication into a hash lookup:
103
+
104
+ - `deriveSelfEphSk` / `selfEphWindow` — the wallet's own notes (shields, change)
105
+ - `derivePairwiseSharedSecret` / `pairwiseEphWindow` — notes from a counterparty
106
+ whose privacy address you already hold
107
+
108
+ Both publish a PRF-derived curve point in the field the protocol already
109
+ carries, so the wire format is unchanged and a wallet that knows nothing about
110
+ them still recovers the note the slow way.
111
+
112
+ > Reusing an ephemeral index republishes the same point and publicly links the
113
+ > two notes. The window functions are pure functions of `(secret, range)` so the
114
+ > caller owns that counter — persist it, and never reuse an index.
115
+
116
+ ## Building a wallet
117
+
118
+ The pieces above are enough to build one by hand. `OrbinumWallet` wires them the
119
+ way they have to be wired — one session driving both the vault and the scan, one
120
+ storage holding both the notes and the ephemeral counters.
121
+
122
+ ```ts
123
+ import { OrbinumWallet, MemoryVaultStorage } from '@orbinum/sdk';
124
+ import { createDecryptPool } from '@orbinum/sdk/worker';
125
+
126
+ const wallet = new OrbinumWallet({
127
+ storage: new MemoryVaultStorage(), // IndexedDbVaultStorage in a browser
128
+ hints: myScanFeed, // ScanHintSource — 1 method, + optional sealed chunks
129
+ nullifiers: mySpentFeed, // NullifierSource — 3 methods
130
+ pool: createDecryptPool({ factory: null }), // main thread; workers in a browser
131
+ });
132
+
133
+ await wallet.unlock(masterBytes); // from a signed SpendingKeyRequest
134
+ wallet.onNotesChanged((notes) => render(notes));
135
+
136
+ const { found } = await wallet.scan({ onProgress: (p) => console.log(p.scanned, p.total) });
137
+ const balance = wallet.getNotes().reduce((sum, n) => sum + n.value, 0n);
138
+ ```
139
+
140
+ [`examples/node-wallet`](./examples/node-wallet) is that program, complete and
141
+ runnable. It runs in CI against a packed tarball, so the snippet above cannot
142
+ drift from something that works.
143
+
144
+ ### Spending
145
+
146
+ The SDK owns the protocol half of a spend; the host owns transport. Each op takes
147
+ its dependencies by injection, and `OrbinumWallet` produces all the ones that
148
+ involve key material:
149
+
150
+ ```ts
151
+ import { transferNotes, planTransfer } from '@orbinum/sdk';
152
+
153
+ // What a UI needs before enabling a button: inputs, change, ceiling.
154
+ const plan = planTransfer({ notes: wallet.getNotes(), amount, fee });
155
+ if (!plan.ok) return showProblem(plan.problem); // 'needs-consolidation', …
156
+
157
+ await transferNotes(
158
+ {
159
+ privacy: client.privacy, // nullifier status + merkle proofs
160
+ resolver: client.circuitVersionResolver, // fail-closed version pinning
161
+ buildNote: wallet.buildOutputNote,
162
+ vault: wallet.vault,
163
+ recoverStealth: wallet.recoverStealth,
164
+ submit: (request) => mySubmit(request), // yours: substrate or EVM
165
+ selfOwnerPk: wallet.spendKeys().ownerPk,
166
+ },
167
+ { inputNotes: plan.inputs!, transferAmount: amount, recipientPk, fee }
168
+ );
169
+ ```
170
+
171
+ `unshieldNote` and `claimFees` follow the same shape. What lives inside them is
172
+ the part that is easy to get wrong and expensive when wrong:
173
+
174
+ - **Merkle root reconciliation** — the circuit proves both inputs under ONE root,
175
+ but each RPC fetch resolves under its own best block. The ops refetch until
176
+ the roots agree, and rule out cross-tree pairs first, since those can never
177
+ agree.
178
+ - **Stealth change** — the change note's key derivation and the memo share one
179
+ ephemeral, so the memo must be submitted verbatim; a regenerated one makes the
180
+ change unspendable.
181
+ - **Pre-flight guards** — a drifted note or mixed circuit versions fail here with
182
+ a readable reason instead of as an opaque assert seconds into proving.
183
+
184
+ `buildShieldParams(note)` marshals a shield: the commitment goes on chain
185
+ **little-endian**, and a big-endian one is accepted by the chain while producing
186
+ a note nobody can ever find.
187
+
188
+ [`examples/node-wallet/spend.ts`](./examples/node-wallet/spend.ts) runs this in
189
+ CI against the packed tarball.
190
+
191
+ ### Chain rules a wallet cannot derive
192
+
193
+ Consensus values, not preferences — the chain rejects an extrinsic that
194
+ disagrees with them:
195
+
196
+ ```ts
197
+ import { MIN_GASLESS_FEE, NATIVE_ASSET_ID, isNativeAsset } from '@orbinum/sdk';
198
+
199
+ const plan = planTransfer({ notes, amount, fee: MIN_GASLESS_FEE });
32
200
  ```
33
201
 
34
- `OrbinumClient` exposes the following modules:
202
+ `planTransfer` and `planUnshield` take a `fee`, and `MIN_GASLESS_FEE` is what
203
+ fills it. Below it the pallet rejects with `FeeTooLow`; without the constant the
204
+ value has to be recovered from a failed submit.
205
+
206
+ An unshield's fee is fixed at exactly this minimum. A private transfer's is the
207
+ sender's choice at or above it — which is why a reconstructed transfer fee is
208
+ unknown rather than assumed.
209
+
210
+ ### When the chain rejects a spend
211
+
212
+ What a wallet should DO about a failure is protocol knowledge; the words shown
213
+ to a person are not:
214
+
215
+ ```ts
216
+ import { classifyChainError } from '@orbinum/sdk';
217
+
218
+ switch (classifyChainError(rawError)) {
219
+ case 'already-spent': // the vault is behind — resync
220
+ case 'stale-proof': // the tree moved — rescan, then retry
221
+ case 'ghost-note': // the commitment is not on chain — purge the note
222
+ case 'amount': // nothing to retry until the user changes it
223
+ }
224
+ ```
225
+
226
+ `stale-proof` and `ghost-note` are the pair worth reading twice. Both are
227
+ proof-versus-tree failures and they look alike in a stack trace, but a ghost note
228
+ is gone for good while a stale root means the note IS on chain and only the proof
229
+ is old. **Purging on a stale root deletes a live, spendable note.**
230
+
231
+ `palletErrorKind(name)` classifies a name directly, and `KNOWN_PALLET_ERRORS`
232
+ lists every one this version knows — useful for building a copy table. An
233
+ unrecognised name returns `unknown`: reacting to it by guessing is how a wallet
234
+ deletes notes it should not.
235
+
236
+ ### Outgoing history
237
+
238
+ The chain never records what a private transfer sent or to whom — that is saved
239
+ locally at submission time. `reconstructOutgoingTxRecords` rebuilds it after a
240
+ restore from each transfer's shape (`amount = Σ(inputs) − change − fee`), reading
241
+ through a `TransferFactsSource` the host implements.
242
+
243
+ That source is separate from the scan feeds on purpose: unlike `NullifierSource`,
244
+ its queries DO send the wallet's own identifiers to the server. It is the
245
+ documented linkage trade-off history reconstruction makes, and a host that
246
+ prefers not to make it simply does not implement it.
247
+
248
+ ### Storage
249
+
250
+ `VaultStorage` is an interface, so the vault can live anywhere. Two
251
+ implementations ship:
252
+
253
+ - `MemoryVaultStorage` (root entry) — servers, tests
254
+ - `IndexedDbVaultStorage` (`@orbinum/sdk/storage/indexeddb`) — browsers
255
+
256
+ Both pass the same conformance suite. One requirement is easy to miss when
257
+ writing a third: **`updateConfig` must be atomic.** Two callers that read the
258
+ same `selfEphCounter` derive the same ephemeral index and publish the same
259
+ ephemeral point, which publicly links the two notes. That is a privacy leak, not
260
+ a lost update.
261
+
262
+ ### Moving notes between clients
263
+
264
+ `orbinum://notes/v1/` is the format for handing notes from one Orbinum client to
265
+ another — desktop to mobile, as scannable pages. Both halves ship here so the
266
+ encoder and decoder cannot drift apart:
267
+
268
+ ```ts
269
+ import {
270
+ encodeNoteTransferPages,
271
+ decodeNoteTransferPage,
272
+ assembleNoteTransfer,
273
+ } from '@orbinum/sdk';
274
+
275
+ const pages = encodeNoteTransferPages(notes); // render each as a QR
276
+ const entries = assembleNoteTransfer(scanned.map(decodeNoteTransferPage));
277
+ ```
278
+
279
+ `assembleNoteTransfer` refuses an incomplete or mixed batch rather than importing
280
+ what it has — a partial import looks like a successful one, and the user would
281
+ never learn which notes never arrived.
282
+
283
+ The payload carries **spending keys**: anyone who scans these codes can spend the
284
+ notes. It is for an in-person transfer between two devices the same person owns,
285
+ and a host must say so before showing one.
35
286
 
36
- | Property | Description |
37
- |---|---|
38
- | `client.substrate` | Substrate RPC blocks, events, transactions |
39
- | `client.evm` | EVM JSON-RPC client (`null` if `evmRpc` not set) |
40
- | `client.rpcV2` | Orbinum `rpc-v2` namespaces (`chain_*`, `privacy_*`) |
41
- | `client.rpcV2.chain` | Chain-level queries (`chain_isValidator`, etc.) |
42
- | `client.shieldedPool` | Shielded pool extrinsics |
43
- | `client.precompiles` | EVM precompile wrappers (`null` if `evmRpc` not set) |
287
+ ### Porting to another platform
288
+
289
+ Three adapters are all that differ between a web page, a browser extension and a
290
+ mobile app. Everything above them the encrypted vault, the scan, the identity
291
+ cache is shared.
292
+
293
+ | Interface | Browser | Extension | Mobile |
294
+ | ---------------- | ------------------------------- | ---------------------- | ------------------ |
295
+ | `VaultStorage` | `IndexedDbVaultStorage` | same | SQLite, MMKV |
296
+ | `SecretStore` | `createWebStorageSecretStore` | `chrome.storage.local` | Keychain, Keystore |
297
+ | `DeviceKeyStore` | `createIndexedDbDeviceKeyStore` | same | secure enclave |
298
+
299
+ The identity cache is what keeps a user from re-signing on every launch. On
300
+ Substrate that is more than convenience: sr25519 signatures are randomised, so a
301
+ second signature over the same message derives a DIFFERENT key and a different
302
+ vault.
303
+
304
+ ```ts
305
+ import { cacheSession, restoreSession, createDeviceKeyProvider } from '@orbinum/sdk';
306
+
307
+ const deviceKey = await createDeviceKeyProvider(myKeyStore)();
308
+ await cacheSession({ store: mySecretStore, deviceKey }, address, chainId, manager.exportHex());
309
+
310
+ // Next launch — no signature needed:
311
+ const identity = await restoreSession({ store: mySecretStore, deviceKey }, address, chainId);
312
+ ```
313
+
314
+ The cached value is encrypted at rest under a device key that never leaves the
315
+ device, and it is scoped per `(chainId, account)` — the chain is part of the key
316
+ derivation, so a cache shared across networks would restore one network's
317
+ identity into another.
318
+
319
+ Use `vaultStorageName(address, chainFingerprint)` for the vault's name rather
320
+ than composing one. It canonicalises the account exactly as the key derivation
321
+ does; keying off the raw address means a wallet that re-lists an account under a
322
+ different SS58 prefix derives the same key but opens a different vault, and the
323
+ notes are orphaned with no error to explain it.
324
+
325
+ [`examples/node-wallet/portability.ts`](./examples/node-wallet/portability.ts)
326
+ runs the whole flow with none of the browser adapters, in CI.
327
+
328
+ #### What each host has to provide
329
+
330
+ The SDK itself uses no browser API outside the `storage/indexeddb` subpath. Two
331
+ globals it does assume are missing from React Native and need a polyfill
332
+ imported **before** the SDK:
333
+
334
+ | Global | Needed by | React Native |
335
+ | ----------------------------------------- | ---------------------------------------------------------- | ---------------------------------------- |
336
+ | `crypto.subtle`, `crypto.getRandomValues` | vault encryption, blinding | `react-native-quick-crypto` |
337
+ | `atob` | `poseidon-lite` decodes its round constants at import time | `react-native-quick-base64` or `base-64` |
338
+
339
+ The `atob` one fails at **module load**, not on first use, so the symptom is an
340
+ import that throws rather than a hash that misbehaves. Everything else — Node,
341
+ Deno, Bun, Cloudflare Workers, extension service workers — already has both.
342
+
343
+ Browser wallet extensions are the one capability that cannot be polyfilled: they
344
+ need a page. Call `hasInjectedExtensions()` before offering them, and sign with
345
+ `getSubstrateSigner` (a keypair) where it returns false.
346
+
347
+ ### Scanning without leaking what you own
348
+
349
+ `NullifierSource` has no "is this nullifier spent?" method, and that omission is
350
+ the design. The wallet downloads the spent set and intersects it locally, so
351
+ every request the server sees is identical regardless of which notes the caller
352
+ holds. A per-nullifier lookup would be simpler and would tell the server exactly
353
+ what you own.
354
+
355
+ ## Entry points
356
+
357
+ | Import | Contents |
358
+ | -------------------------------- | -------------------------------------------------------------------- |
359
+ | `@orbinum/sdk` | Protocol, keys, vault, scanner, wallet facade — environment-agnostic |
360
+ | `@orbinum/sdk/storage/indexeddb` | `IndexedDbVaultStorage`, the browser vault backend |
361
+ | `@orbinum/sdk/worker` | Trial-decryption kernel and pool, for a Web Worker |
362
+
363
+ The worker entry carries no chain client and no transport, so a worker bundle
364
+ built from it stays small.
365
+
366
+ Web Workers are spawned by the HOST, not by the SDK: `new Worker(new URL(...,
367
+ import.meta.url))` is a build-time rewrite that a published package cannot
368
+ perform. Pass a `factory` to `createDecryptPool`, or `null` to decrypt on the
369
+ calling thread.
370
+
371
+ ## Requirements
44
372
 
45
- Each module can also be instantiated independently without `OrbinumClient`.
373
+ - Any runtime with WebCrypto and `fetch` Node 18+, Deno, Bun, a browser, an
374
+ extension service worker, a Cloudflare Worker
375
+ - React Native additionally needs two polyfills imported before the SDK; see
376
+ [What each host has to provide](#what-each-host-has-to-provide)
377
+ - An Orbinum node's Substrate WebSocket endpoint
46
378
 
47
379
  ## License
48
380
 
@@ -0,0 +1,152 @@
1
+ import { V as VaultStorage, a as VaultConfigRecord, E as EncryptedNoteRecord, b as EncryptedTxRecord, C as CachedNullifier, N as NullifierSyncMeta, S as SpendDetails, D as DeviceKeyStore, c as SecretStore } from '../../secretStore-CF6Nse__.mjs';
2
+
3
+ /**
4
+ * `VaultStorage` over IndexedDB — the browser's copy of a wallet's notes.
5
+ *
6
+ * The DOM lib reference above is scoped to this file: the package's tsconfig
7
+ * ships `lib: ["esnext"]`, so nothing outside this entry point can reach for a
8
+ * browser API by accident.
9
+ *
10
+ * Ships in its own subpath so a consumer on Node, React Native or a worker
11
+ * implements their own backend and never loads this module — the interface it
12
+ * satisfies lives in the root entry.
13
+ *
14
+ * ## Object stores
15
+ *
16
+ * vault_config one record, id "main": schema version, scan cursor, the
17
+ * ephemeral-index counters
18
+ * vault_notes one record per note, keyed by its BLINDED commitment tag.
19
+ * The note itself is encrypted and its identifiers are
20
+ * blinded, so a database dump reveals nothing linkable to
21
+ * chain activity while equality lookups still work.
22
+ * vault_tx_history encrypted outgoing-transfer records
23
+ * nullifier_set the spent-nullifier mirror. Public chain data, stored in
24
+ * the clear on purpose: it is the same set every wallet
25
+ * downloads, so encrypting it would protect nothing and
26
+ * make membership checks cost a decrypt each.
27
+ * nullifier_sync sync progress for the above
28
+ *
29
+ * The database NAME is supplied by the caller rather than fixed here. One vault
30
+ * per (chain, account) is what keeps a wallet from reading notes that belong to
31
+ * a different chain or a different key, and only the host knows those.
32
+ */
33
+
34
+ interface IndexedDbVaultStorageOptions {
35
+ /**
36
+ * Database name. Use one per (chain, account): a vault opened against the
37
+ * wrong chain holds notes whose commitments no longer exist, and one opened
38
+ * under another account cannot decrypt anything it finds.
39
+ */
40
+ name: string;
41
+ /**
42
+ * IndexedDB factory. Defaults to the global one; pass a fake to test without
43
+ * a browser.
44
+ */
45
+ indexedDB?: IDBFactory | undefined;
46
+ }
47
+ /** Browser-backed `VaultStorage`. One instance per database. */
48
+ declare class IndexedDbVaultStorage implements VaultStorage {
49
+ private readonly name;
50
+ private readonly idb;
51
+ private db;
52
+ constructor(options: IndexedDbVaultStorageOptions);
53
+ private openDB;
54
+ /**
55
+ * Runs one transaction, reopening once if the cached connection was already
56
+ * dead.
57
+ *
58
+ * `onclose` does not fire in every closing path — notably a connection
59
+ * killed between `openDB()` and the transaction call — so this retry is what
60
+ * actually makes the adapter self-healing. `InvalidStateError` means "this
61
+ * handle is finished", and a fresh one is the only fix. Once is enough: a
62
+ * second failure is a real problem, not a stale handle.
63
+ *
64
+ * `run` must build its requests synchronously (no await before the last
65
+ * one), or the transaction auto-commits underneath it.
66
+ */
67
+ private withDB;
68
+ getConfig(): Promise<VaultConfigRecord | null>;
69
+ putConfig(config: VaultConfigRecord): Promise<void>;
70
+ /**
71
+ * Read-modify-write in ONE transaction.
72
+ *
73
+ * Doing this as getConfig() then putConfig() spans two transactions, so two
74
+ * concurrent callers both read the old record and the second write wins. For
75
+ * `selfEphCounter` that lost increment means two notes derive the SAME
76
+ * ephemeral index and publish one ephPk twice, linking them as
77
+ * same-creator — a privacy leak, not a lost UI update. A single readwrite
78
+ * transaction serialises them, since IndexedDB scopes those per store.
79
+ */
80
+ updateConfig(mutate: (config: VaultConfigRecord) => VaultConfigRecord): Promise<VaultConfigRecord | null>;
81
+ hasVault(): Promise<boolean>;
82
+ getAllNoteRecords(): Promise<EncryptedNoteRecord[]>;
83
+ putNote(record: EncryptedNoteRecord): Promise<void>;
84
+ putNotes(records: EncryptedNoteRecord[]): Promise<void>;
85
+ deleteNote(commitmentTag: string): Promise<void>;
86
+ deleteNotes(commitmentTags: string[]): Promise<void>;
87
+ clearNotes(): Promise<void>;
88
+ addTxRecord(record: EncryptedTxRecord): Promise<void>;
89
+ getAllTxRecords(): Promise<EncryptedTxRecord[]>;
90
+ /**
91
+ * Persists one sealed chunk AND the sync progress it produced in a single
92
+ * transaction. Both must land together: progress ahead of the data would
93
+ * make the next sync resume past chunks that were never stored, leaving
94
+ * spent notes looking unspent.
95
+ */
96
+ putNullifierChunk(entries: CachedNullifier[], meta: NullifierSyncMeta): Promise<void>;
97
+ getNullifierSyncMeta(): Promise<NullifierSyncMeta | null>;
98
+ /**
99
+ * Which of `hexes` the cache holds, as batch point-gets.
100
+ *
101
+ * Local lookups are the whole point: asking a server whether one specific
102
+ * nullifier is spent would tell it which notes this wallet owns.
103
+ */
104
+ getSpentNullifiers(hexes: string[]): Promise<Map<string, SpendDetails>>;
105
+ countNullifiers(): Promise<number>;
106
+ clearNullifierCache(): Promise<void>;
107
+ /** Closes the cached connection. The next call reopens it. */
108
+ close(): void;
109
+ }
110
+
111
+ /**
112
+ * A `DeviceKeyStore` backed by a tiny dedicated IndexedDB.
113
+ *
114
+ * IndexedDB rather than localStorage because it stores a `CryptoKey` HANDLE via
115
+ * structured clone. The key is generated non-extractable, so its material never
116
+ * becomes visible to JavaScript — a storage dump yields an opaque handle, not
117
+ * bytes. localStorage can only hold strings, which would mean exporting the key.
118
+ *
119
+ * Its own database, separate from the vault: the device key outlives any single
120
+ * vault and must survive one being dropped.
121
+ */
122
+ declare function createIndexedDbDeviceKeyStore(indexedDBFactory?: IDBFactory): DeviceKeyStore;
123
+ /**
124
+ * The browser device key, generated and persisted on first use.
125
+ *
126
+ * The store is built on the FIRST CALL, not at import time: an extension's
127
+ * service worker and a test environment can both import this module before
128
+ * IndexedDB is reachable, and failing there would take down everything that
129
+ * merely imports the entry point.
130
+ */
131
+ declare const getOrCreateIndexedDbDeviceKey: () => Promise<CryptoKey>;
132
+
133
+ /**
134
+ * A `SecretStore` over Web Storage.
135
+ *
136
+ * No IndexedDB involved — it ships from this entry point because a consumer
137
+ * reaching for browser persistence wants both adapters together, and splitting
138
+ * them across two subpaths would buy nothing.
139
+ */
140
+
141
+ /**
142
+ * A `SecretStore` over Web Storage.
143
+ *
144
+ * Reads fall back to `sessionStorage` so a value written by an older build, or
145
+ * by a deliberately session-scoped flow, is still found. Writes always go to the
146
+ * durable store and clear the session copy, so one key never lives in both.
147
+ *
148
+ * Values are encrypted before they arrive here — see `sessionCache`.
149
+ */
150
+ declare function createWebStorageSecretStore(storage?: Storage, sessionStorageArea?: Storage | null): SecretStore;
151
+
152
+ export { IndexedDbVaultStorage, type IndexedDbVaultStorageOptions, createIndexedDbDeviceKeyStore, createWebStorageSecretStore, getOrCreateIndexedDbDeviceKey };