@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
|
@@ -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__.js';
|
|
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 };
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/adapters/indexeddb/index.ts
|
|
21
|
+
var indexeddb_exports = {};
|
|
22
|
+
__export(indexeddb_exports, {
|
|
23
|
+
IndexedDbVaultStorage: () => IndexedDbVaultStorage,
|
|
24
|
+
createIndexedDbDeviceKeyStore: () => createIndexedDbDeviceKeyStore,
|
|
25
|
+
createWebStorageSecretStore: () => createWebStorageSecretStore,
|
|
26
|
+
getOrCreateIndexedDbDeviceKey: () => getOrCreateIndexedDbDeviceKey
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(indexeddb_exports);
|
|
29
|
+
|
|
30
|
+
// src/adapters/indexeddb/idb.ts
|
|
31
|
+
function idbRequest(req) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
req.onsuccess = () => resolve(req.result);
|
|
34
|
+
req.onerror = () => reject(req.error);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/adapters/indexeddb/VaultStorage.ts
|
|
39
|
+
var DB_VERSION = 1;
|
|
40
|
+
var STORE_CONFIG = "vault_config";
|
|
41
|
+
var STORE_NOTES = "vault_notes";
|
|
42
|
+
var STORE_TX_HISTORY = "vault_tx_history";
|
|
43
|
+
var STORE_NULLIFIERS = "nullifier_set";
|
|
44
|
+
var STORE_NULLIFIER_SYNC = "nullifier_sync";
|
|
45
|
+
var IndexedDbVaultStorage = class {
|
|
46
|
+
name;
|
|
47
|
+
idb;
|
|
48
|
+
db = null;
|
|
49
|
+
constructor(options) {
|
|
50
|
+
this.name = options.name;
|
|
51
|
+
const factory = options.indexedDB ?? globalThis.indexedDB;
|
|
52
|
+
if (!factory) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"IndexedDB is unavailable. Pass `indexedDB`, or use a different VaultStorage."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
this.idb = factory;
|
|
58
|
+
}
|
|
59
|
+
openDB() {
|
|
60
|
+
if (this.db) return Promise.resolve(this.db);
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const req = this.idb.open(this.name, DB_VERSION);
|
|
63
|
+
req.onupgradeneeded = (e) => {
|
|
64
|
+
const db = e.target.result;
|
|
65
|
+
for (const [store, keyPath] of [
|
|
66
|
+
[STORE_CONFIG, "id"],
|
|
67
|
+
[STORE_NOTES, "commitmentTag"],
|
|
68
|
+
[STORE_TX_HISTORY, "id"],
|
|
69
|
+
[STORE_NULLIFIERS, "h"],
|
|
70
|
+
[STORE_NULLIFIER_SYNC, "id"]
|
|
71
|
+
]) {
|
|
72
|
+
if (!db.objectStoreNames.contains(store)) {
|
|
73
|
+
db.createObjectStore(store, { keyPath });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
req.onsuccess = (e) => {
|
|
78
|
+
const db = e.target.result;
|
|
79
|
+
db.onclose = () => {
|
|
80
|
+
if (this.db === db) this.db = null;
|
|
81
|
+
};
|
|
82
|
+
db.onversionchange = () => {
|
|
83
|
+
db.close();
|
|
84
|
+
if (this.db === db) this.db = null;
|
|
85
|
+
};
|
|
86
|
+
this.db = db;
|
|
87
|
+
resolve(db);
|
|
88
|
+
};
|
|
89
|
+
req.onerror = () => reject(new Error(`Failed to open IndexedDB: ${String(req.error?.message)}`));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Runs one transaction, reopening once if the cached connection was already
|
|
94
|
+
* dead.
|
|
95
|
+
*
|
|
96
|
+
* `onclose` does not fire in every closing path — notably a connection
|
|
97
|
+
* killed between `openDB()` and the transaction call — so this retry is what
|
|
98
|
+
* actually makes the adapter self-healing. `InvalidStateError` means "this
|
|
99
|
+
* handle is finished", and a fresh one is the only fix. Once is enough: a
|
|
100
|
+
* second failure is a real problem, not a stale handle.
|
|
101
|
+
*
|
|
102
|
+
* `run` must build its requests synchronously (no await before the last
|
|
103
|
+
* one), or the transaction auto-commits underneath it.
|
|
104
|
+
*/
|
|
105
|
+
async withDB(run) {
|
|
106
|
+
try {
|
|
107
|
+
return await run(await this.openDB());
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err?.name !== "InvalidStateError") throw err;
|
|
110
|
+
this.db = null;
|
|
111
|
+
return run(await this.openDB());
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ── Config ───────────────────────────────────────────────────────────────
|
|
115
|
+
async getConfig() {
|
|
116
|
+
return this.withDB(async (db) => {
|
|
117
|
+
const tx = db.transaction(STORE_CONFIG, "readonly");
|
|
118
|
+
const result = await idbRequest(
|
|
119
|
+
tx.objectStore(STORE_CONFIG).get("main")
|
|
120
|
+
);
|
|
121
|
+
return result ?? null;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async putConfig(config) {
|
|
125
|
+
await this.withDB(async (db) => {
|
|
126
|
+
const tx = db.transaction(STORE_CONFIG, "readwrite");
|
|
127
|
+
await idbRequest(tx.objectStore(STORE_CONFIG).put(config));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Read-modify-write in ONE transaction.
|
|
132
|
+
*
|
|
133
|
+
* Doing this as getConfig() then putConfig() spans two transactions, so two
|
|
134
|
+
* concurrent callers both read the old record and the second write wins. For
|
|
135
|
+
* `selfEphCounter` that lost increment means two notes derive the SAME
|
|
136
|
+
* ephemeral index and publish one ephPk twice, linking them as
|
|
137
|
+
* same-creator — a privacy leak, not a lost UI update. A single readwrite
|
|
138
|
+
* transaction serialises them, since IndexedDB scopes those per store.
|
|
139
|
+
*/
|
|
140
|
+
async updateConfig(mutate) {
|
|
141
|
+
return this.withDB(async (db) => {
|
|
142
|
+
const tx = db.transaction(STORE_CONFIG, "readwrite");
|
|
143
|
+
const store = tx.objectStore(STORE_CONFIG);
|
|
144
|
+
const current = await idbRequest(store.get("main"));
|
|
145
|
+
if (!current) return null;
|
|
146
|
+
const updated = mutate(current);
|
|
147
|
+
await idbRequest(store.put(updated));
|
|
148
|
+
return updated;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async hasVault() {
|
|
152
|
+
return await this.getConfig() !== null;
|
|
153
|
+
}
|
|
154
|
+
// ── Notes ────────────────────────────────────────────────────────────────
|
|
155
|
+
async getAllNoteRecords() {
|
|
156
|
+
return this.withDB((db) => {
|
|
157
|
+
const tx = db.transaction(STORE_NOTES, "readonly");
|
|
158
|
+
return idbRequest(tx.objectStore(STORE_NOTES).getAll());
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async putNote(record) {
|
|
162
|
+
await this.putNotes([record]);
|
|
163
|
+
}
|
|
164
|
+
async putNotes(records) {
|
|
165
|
+
if (records.length === 0) return;
|
|
166
|
+
await this.withDB(async (db) => {
|
|
167
|
+
const tx = db.transaction(STORE_NOTES, "readwrite");
|
|
168
|
+
const store = tx.objectStore(STORE_NOTES);
|
|
169
|
+
await Promise.all(records.map((r) => idbRequest(store.put(r))));
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async deleteNote(commitmentTag) {
|
|
173
|
+
await this.deleteNotes([commitmentTag]);
|
|
174
|
+
}
|
|
175
|
+
async deleteNotes(commitmentTags) {
|
|
176
|
+
if (commitmentTags.length === 0) return;
|
|
177
|
+
await this.withDB(async (db) => {
|
|
178
|
+
const tx = db.transaction(STORE_NOTES, "readwrite");
|
|
179
|
+
const store = tx.objectStore(STORE_NOTES);
|
|
180
|
+
await Promise.all(commitmentTags.map((tag) => idbRequest(store.delete(tag))));
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async clearNotes() {
|
|
184
|
+
await this.withDB(async (db) => {
|
|
185
|
+
const tx = db.transaction(STORE_NOTES, "readwrite");
|
|
186
|
+
await idbRequest(tx.objectStore(STORE_NOTES).clear());
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// ── Transaction history ──────────────────────────────────────────────────
|
|
190
|
+
// Storage-dumb: rows are written and read as-is. Encryption lives in
|
|
191
|
+
// VaultStore, which is what a caller should use.
|
|
192
|
+
async addTxRecord(record) {
|
|
193
|
+
await this.withDB(async (db) => {
|
|
194
|
+
const tx = db.transaction(STORE_TX_HISTORY, "readwrite");
|
|
195
|
+
await idbRequest(tx.objectStore(STORE_TX_HISTORY).put(record));
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async getAllTxRecords() {
|
|
199
|
+
return this.withDB((db) => {
|
|
200
|
+
const tx = db.transaction(STORE_TX_HISTORY, "readonly");
|
|
201
|
+
return idbRequest(tx.objectStore(STORE_TX_HISTORY).getAll());
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// ── Nullifier cache ──────────────────────────────────────────────────────
|
|
205
|
+
/**
|
|
206
|
+
* Persists one sealed chunk AND the sync progress it produced in a single
|
|
207
|
+
* transaction. Both must land together: progress ahead of the data would
|
|
208
|
+
* make the next sync resume past chunks that were never stored, leaving
|
|
209
|
+
* spent notes looking unspent.
|
|
210
|
+
*/
|
|
211
|
+
async putNullifierChunk(entries, meta) {
|
|
212
|
+
await this.withDB(async (db) => {
|
|
213
|
+
const tx = db.transaction([STORE_NULLIFIERS, STORE_NULLIFIER_SYNC], "readwrite");
|
|
214
|
+
const store = tx.objectStore(STORE_NULLIFIERS);
|
|
215
|
+
await Promise.all([
|
|
216
|
+
...entries.map((e) => idbRequest(store.put(e))),
|
|
217
|
+
idbRequest(tx.objectStore(STORE_NULLIFIER_SYNC).put(meta))
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async getNullifierSyncMeta() {
|
|
222
|
+
return this.withDB(async (db) => {
|
|
223
|
+
const tx = db.transaction(STORE_NULLIFIER_SYNC, "readonly");
|
|
224
|
+
const result = await idbRequest(
|
|
225
|
+
tx.objectStore(STORE_NULLIFIER_SYNC).get("main")
|
|
226
|
+
);
|
|
227
|
+
return result ?? null;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Which of `hexes` the cache holds, as batch point-gets.
|
|
232
|
+
*
|
|
233
|
+
* Local lookups are the whole point: asking a server whether one specific
|
|
234
|
+
* nullifier is spent would tell it which notes this wallet owns.
|
|
235
|
+
*/
|
|
236
|
+
async getSpentNullifiers(hexes) {
|
|
237
|
+
if (hexes.length === 0) return /* @__PURE__ */ new Map();
|
|
238
|
+
return this.withDB(async (db) => {
|
|
239
|
+
const tx = db.transaction(STORE_NULLIFIERS, "readonly");
|
|
240
|
+
const store = tx.objectStore(STORE_NULLIFIERS);
|
|
241
|
+
const results = await Promise.all(
|
|
242
|
+
hexes.map((h) => idbRequest(store.get(h)))
|
|
243
|
+
);
|
|
244
|
+
return new Map(
|
|
245
|
+
results.filter((r) => r !== void 0).map((r) => [r.h, { spentAt: r.ts ?? null, txHash: r.tx ?? null }])
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
async countNullifiers() {
|
|
250
|
+
return this.withDB((db) => {
|
|
251
|
+
const tx = db.transaction(STORE_NULLIFIERS, "readonly");
|
|
252
|
+
return idbRequest(tx.objectStore(STORE_NULLIFIERS).count());
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async clearNullifierCache() {
|
|
256
|
+
await this.withDB(async (db) => {
|
|
257
|
+
const tx = db.transaction([STORE_NULLIFIERS, STORE_NULLIFIER_SYNC], "readwrite");
|
|
258
|
+
await Promise.all([
|
|
259
|
+
idbRequest(tx.objectStore(STORE_NULLIFIERS).clear()),
|
|
260
|
+
idbRequest(tx.objectStore(STORE_NULLIFIER_SYNC).clear())
|
|
261
|
+
]);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/** Closes the cached connection. The next call reopens it. */
|
|
265
|
+
close() {
|
|
266
|
+
this.db?.close();
|
|
267
|
+
this.db = null;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/wallet/identity/deviceKey.ts
|
|
272
|
+
async function generateDeviceKey(extractable = false) {
|
|
273
|
+
return crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, extractable, [
|
|
274
|
+
"encrypt",
|
|
275
|
+
"decrypt"
|
|
276
|
+
]);
|
|
277
|
+
}
|
|
278
|
+
function createDeviceKeyProvider(store) {
|
|
279
|
+
let cached = null;
|
|
280
|
+
let inFlight = null;
|
|
281
|
+
return async function getOrCreateDeviceKey() {
|
|
282
|
+
if (cached) return cached;
|
|
283
|
+
if (inFlight) return inFlight;
|
|
284
|
+
inFlight = (async () => {
|
|
285
|
+
const existing = await store.load();
|
|
286
|
+
if (existing) return existing;
|
|
287
|
+
const key = await generateDeviceKey();
|
|
288
|
+
await store.save(key);
|
|
289
|
+
return key;
|
|
290
|
+
})();
|
|
291
|
+
try {
|
|
292
|
+
cached = await inFlight;
|
|
293
|
+
return cached;
|
|
294
|
+
} finally {
|
|
295
|
+
inFlight = null;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/adapters/indexeddb/deviceKeyStore.ts
|
|
301
|
+
var KEYSTORE_DB = "orbinum-keystore";
|
|
302
|
+
var KEYSTORE_STORE = "keys";
|
|
303
|
+
var DEVICE_KEY_ID = "device";
|
|
304
|
+
function createIndexedDbDeviceKeyStore(indexedDBFactory) {
|
|
305
|
+
const idb = indexedDBFactory ?? globalThis.indexedDB;
|
|
306
|
+
if (!idb) throw new Error("IndexedDB is unavailable; supply a different DeviceKeyStore.");
|
|
307
|
+
const open = () => new Promise((resolve, reject) => {
|
|
308
|
+
const req = idb.open(KEYSTORE_DB, 1);
|
|
309
|
+
req.onupgradeneeded = () => {
|
|
310
|
+
const db = req.result;
|
|
311
|
+
if (!db.objectStoreNames.contains(KEYSTORE_STORE)) {
|
|
312
|
+
db.createObjectStore(KEYSTORE_STORE, { keyPath: "id" });
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
req.onsuccess = () => resolve(req.result);
|
|
316
|
+
req.onerror = () => reject(new Error(`Failed to open keystore: ${String(req.error?.message)}`));
|
|
317
|
+
});
|
|
318
|
+
return {
|
|
319
|
+
async load() {
|
|
320
|
+
const db = await open();
|
|
321
|
+
try {
|
|
322
|
+
const row = await idbRequest(
|
|
323
|
+
db.transaction(KEYSTORE_STORE, "readonly").objectStore(KEYSTORE_STORE).get(DEVICE_KEY_ID)
|
|
324
|
+
);
|
|
325
|
+
return row?.key ?? null;
|
|
326
|
+
} finally {
|
|
327
|
+
db.close();
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
async save(key) {
|
|
331
|
+
const db = await open();
|
|
332
|
+
try {
|
|
333
|
+
await idbRequest(
|
|
334
|
+
db.transaction(KEYSTORE_STORE, "readwrite").objectStore(KEYSTORE_STORE).put({ id: DEVICE_KEY_ID, key })
|
|
335
|
+
);
|
|
336
|
+
} finally {
|
|
337
|
+
db.close();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
var getOrCreateIndexedDbDeviceKey = createDeviceKeyProvider({
|
|
343
|
+
load: () => createIndexedDbDeviceKeyStore().load(),
|
|
344
|
+
save: (key) => createIndexedDbDeviceKeyStore().save(key)
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// src/adapters/indexeddb/secretStore.ts
|
|
348
|
+
function createWebStorageSecretStore(storage, sessionStorageArea) {
|
|
349
|
+
const durable = () => {
|
|
350
|
+
const area = storage ?? globalThis.localStorage;
|
|
351
|
+
if (!area) throw new Error("Web Storage is unavailable; supply a different SecretStore.");
|
|
352
|
+
return area;
|
|
353
|
+
};
|
|
354
|
+
const session = () => sessionStorageArea === void 0 ? globalThis.sessionStorage ?? null : sessionStorageArea;
|
|
355
|
+
return {
|
|
356
|
+
async get(key) {
|
|
357
|
+
return durable().getItem(key) ?? session()?.getItem(key) ?? null;
|
|
358
|
+
},
|
|
359
|
+
async set(key, value) {
|
|
360
|
+
durable().setItem(key, value);
|
|
361
|
+
session()?.removeItem(key);
|
|
362
|
+
},
|
|
363
|
+
async remove(key) {
|
|
364
|
+
durable().removeItem(key);
|
|
365
|
+
session()?.removeItem(key);
|
|
366
|
+
},
|
|
367
|
+
async keys() {
|
|
368
|
+
const all = new Set(Object.keys(durable()));
|
|
369
|
+
const area = session();
|
|
370
|
+
if (area) for (const k of Object.keys(area)) all.add(k);
|
|
371
|
+
return [...all];
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
376
|
+
0 && (module.exports = {
|
|
377
|
+
IndexedDbVaultStorage,
|
|
378
|
+
createIndexedDbDeviceKeyStore,
|
|
379
|
+
createWebStorageSecretStore,
|
|
380
|
+
getOrCreateIndexedDbDeviceKey
|
|
381
|
+
});
|