@orbinum/sdk 0.25.0 → 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 +355 -23
- package/dist/adapters/indexeddb/index.d.mts +152 -0
- package/dist/adapters/indexeddb/index.d.ts +152 -0
- package/dist/adapters/indexeddb/index.js +381 -0
- package/dist/adapters/indexeddb/index.mjs +326 -0
- package/dist/chunk-JMYU5QAK.mjs +40 -0
- package/dist/chunk-Y6LNYJAJ.mjs +1242 -0
- package/dist/index-JYVjYJtf.d.mts +353 -0
- package/dist/index-JYVjYJtf.d.ts +353 -0
- package/dist/index.d.mts +4745 -2971
- package/dist/index.d.ts +4745 -2971
- package/dist/index.js +6319 -3431
- package/dist/index.mjs +4884 -3259
- package/dist/secretStore-CF6Nse__.d.mts +292 -0
- package/dist/secretStore-CF6Nse__.d.ts +292 -0
- package/dist/wallet/worker/index.d.mts +1 -0
- package/dist/wallet/worker/index.d.ts +1 -0
- package/dist/wallet/worker/index.js +859 -0
- package/dist/wallet/worker/index.mjs +28 -0
- package/package.json +24 -7
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
|
|
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
|
-
|
|
9
|
-
# or
|
|
10
|
-
pnpm add @orbinum/sdk
|
|
41
|
+
pnpm add @orbinum/sdk polkadot-api @polkadot/util-crypto
|
|
11
42
|
```
|
|
12
43
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
29
|
-
const root
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
|
43
|
-
|
|
|
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
|
-
|
|
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 };
|