@ghostlytools/cli 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.
Files changed (57) hide show
  1. package/README.md +405 -0
  2. package/SKILL.md +282 -0
  3. package/dist/assets/argon2id-088uQfnH.js +645 -0
  4. package/dist/assets/argon2id-088uQfnH.js.map +1 -0
  5. package/dist/assets/bark_ffi_wasm_bg-Bxru_BVW.wasm +0 -0
  6. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js +6 -0
  7. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -0
  8. package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js +6 -0
  9. package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -0
  10. package/dist/assets/bitcoindevkit_bg-CRkZTSbO.wasm +0 -0
  11. package/dist/assets/breez_sdk_spark_wasm_bg.wasm +0 -0
  12. package/dist/assets/endpoint-h3cqRyRz.js +185 -0
  13. package/dist/assets/endpoint-h3cqRyRz.js.map +1 -0
  14. package/dist/assets/fedimintWorker.mjs +95 -0
  15. package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js +6 -0
  16. package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -0
  17. package/dist/assets/fedimint_client_wasm_bg-DeFtkIv6.wasm +0 -0
  18. package/dist/assets/fileBytes-ByS16quP.js +154 -0
  19. package/dist/assets/fileBytes-ByS16quP.js.map +1 -0
  20. package/dist/assets/fileBytesIdb-COzoilo9.js +230 -0
  21. package/dist/assets/fileBytesIdb-COzoilo9.js.map +1 -0
  22. package/dist/assets/fileBytesOpfs-B27wAMUh.js +145 -0
  23. package/dist/assets/fileBytesOpfs-B27wAMUh.js.map +1 -0
  24. package/dist/assets/ghostly_iroh_web-CPa_YEfb.js +1154 -0
  25. package/dist/assets/ghostly_iroh_web-CPa_YEfb.js.map +1 -0
  26. package/dist/assets/ghostly_iroh_web_bg.wasm +0 -0
  27. package/dist/assets/hyperdhtRelay-BKsFpPjr.js +342 -0
  28. package/dist/assets/hyperdhtRelay-BKsFpPjr.js.map +1 -0
  29. package/dist/assets/legacy_ciphers-JJOu0JXL.js +5185 -0
  30. package/dist/assets/legacy_ciphers-JJOu0JXL.js.map +1 -0
  31. package/dist/assets/mainline-DOncVbJN.js +176 -0
  32. package/dist/assets/mainline-DOncVbJN.js.map +1 -0
  33. package/dist/assets/nacl-fast-CDhNCYHs.js +1512 -0
  34. package/dist/assets/nacl-fast-CDhNCYHs.js.map +1 -0
  35. package/dist/assets/noble_curves-DDWn5AH0.js +2978 -0
  36. package/dist/assets/noble_curves-DDWn5AH0.js.map +1 -0
  37. package/dist/assets/noble_hashes-B7AZyFyS.js +427 -0
  38. package/dist/assets/noble_hashes-B7AZyFyS.js.map +1 -0
  39. package/dist/assets/openpgp-BBFr-zHQ.js +16573 -0
  40. package/dist/assets/openpgp-BBFr-zHQ.js.map +1 -0
  41. package/dist/assets/server-5T5l8hfy.js +28293 -0
  42. package/dist/assets/server-5T5l8hfy.js.map +1 -0
  43. package/dist/assets/sha512-BYId-G-T.js +1034 -0
  44. package/dist/assets/sha512-BYId-G-T.js.map +1 -0
  45. package/dist/assets/src-DptDr2et.js +20971 -0
  46. package/dist/assets/src-DptDr2et.js.map +1 -0
  47. package/dist/assets/storage-Bya5b2Xt.js +1648 -0
  48. package/dist/assets/storage-Bya5b2Xt.js.map +1 -0
  49. package/dist/assets/tree-store-N362nuzZ.js +1031 -0
  50. package/dist/assets/tree-store-N362nuzZ.js.map +1 -0
  51. package/dist/assets/unbzip2-stream-B1C-JX9L.js +681 -0
  52. package/dist/assets/unbzip2-stream-B1C-JX9L.js.map +1 -0
  53. package/dist/assets/web-OUQl4Q7L.js +3802 -0
  54. package/dist/assets/web-OUQl4Q7L.js.map +1 -0
  55. package/dist/ghostly.mjs +17087 -0
  56. package/dist/ghostly.mjs.map +1 -0
  57. package/package.json +76 -0
@@ -0,0 +1,1031 @@
1
+ //#region ../../node_modules/@breeztech/breez-sdk-spark/web/tree-store/index.js
2
+ /**
3
+ * ES module implementation of the durable tree store for browsers, backed by
4
+ * IndexedDB. Ports the two-table (leaves + ancestors) model and reservation /
5
+ * refresh / spent-guard semantics of the PostgreSQL tree store to a single-user
6
+ * IndexedDB database: no user_id scoping and no `brz_` table prefix, since each
7
+ * browser origin has its own isolated database.
8
+ *
9
+ * IndexedDB transaction lifetime: a transaction auto-commits once its request
10
+ * queue drains and control returns to the event loop, so awaiting any non-IDB
11
+ * promise mid-transaction closes it. Every logical operation therefore runs in
12
+ * ONE transaction: all reads are issued up front, and the compute + writes run
13
+ * synchronously inside the last read's success handler (never awaiting anything
14
+ * that isn't part of the transaction). Because IndexedDB serializes readwrite
15
+ * transactions with overlapping store scope, this also gives the mutating ops
16
+ * the same serialization the Postgres backend gets from its per-tenant lock.
17
+ */
18
+ /** Reservations older than this are stale and get released on the next refresh. */
19
+ var RESERVATION_TIMEOUT_MS = 3e5;
20
+ /** Spent-leaf markers older than this (relative to a refresh) are pruned. */
21
+ var SPENT_MARKER_CLEANUP_THRESHOLD_MS = 3e5;
22
+ var DB_VERSION = 5;
23
+ var STORE_LEAVES = "leaves";
24
+ var STORE_ANCESTORS = "ancestors";
25
+ var STORE_RESERVATIONS = "reservations";
26
+ var STORE_SPENT = "spent";
27
+ var STORE_SWAP_STATUS = "swapStatus";
28
+ /** Singleton key of the swap-status row. */
29
+ var SWAP_STATUS_ID = 1;
30
+ var TreeStoreError = class TreeStoreError extends Error {
31
+ constructor(message, cause = null) {
32
+ super(message);
33
+ this.name = "TreeStoreError";
34
+ this.cause = cause;
35
+ if (Error.captureStackTrace) Error.captureStackTrace(this, TreeStoreError);
36
+ }
37
+ };
38
+ function _resolveIndexedDB() {
39
+ if (typeof indexedDB !== "undefined") return indexedDB;
40
+ if (typeof globalThis !== "undefined" && globalThis.indexedDB) return globalThis.indexedDB;
41
+ if (typeof self !== "undefined" && self.indexedDB) return self.indexedDB;
42
+ if (typeof window !== "undefined" && window.indexedDB) return window.indexedDB;
43
+ return null;
44
+ }
45
+ /**
46
+ * Pair a leaf with its ancestors (nearest first) by walking `parent_node_id`
47
+ * through `nodes`. Returns null if the leaf itself is absent; stops at a gap or
48
+ * cycle, returning a partial chain.
49
+ * @param {Map<string, object>} nodes
50
+ * @param {string} leafId
51
+ * @returns {{leaf: object, ancestors: Array<object>}|null}
52
+ */
53
+ function assembleExitChain(nodes, leafId) {
54
+ const leaf = nodes.get(leafId);
55
+ if (!leaf) return null;
56
+ const ancestors = [];
57
+ const visited = /* @__PURE__ */ new Set([leafId]);
58
+ let current = leaf.parent_node_id;
59
+ while (current != null && !visited.has(current)) {
60
+ visited.add(current);
61
+ const node = nodes.get(current);
62
+ if (!node) break;
63
+ ancestors.push(node);
64
+ current = node.parent_node_id;
65
+ }
66
+ return {
67
+ leaf,
68
+ ancestors
69
+ };
70
+ }
71
+ var WebTreeStore = class {
72
+ constructor(dbName = "BreezSdkSparkTree", logger = null) {
73
+ this.dbName = dbName;
74
+ this.db = null;
75
+ this.logger = logger;
76
+ }
77
+ async initialize() {
78
+ if (this.db) return this;
79
+ const idbFactory = _resolveIndexedDB();
80
+ if (!idbFactory) throw new TreeStoreError("IndexedDB is not available in this environment");
81
+ this.db = await new Promise((resolve, reject) => {
82
+ const request = idbFactory.open(this.dbName, DB_VERSION);
83
+ request.onupgradeneeded = (event) => {
84
+ const db = event.target.result;
85
+ if (!db.objectStoreNames.contains(STORE_LEAVES)) {
86
+ const leaves = db.createObjectStore(STORE_LEAVES, { keyPath: "id" });
87
+ leaves.createIndex("reservation_id", "reservation_id", { unique: false });
88
+ leaves.createIndex("chain_complete", "chain_complete", { unique: false });
89
+ }
90
+ if (event.oldVersion < 2 && db.objectStoreNames.contains(STORE_ANCESTORS)) db.deleteObjectStore(STORE_ANCESTORS);
91
+ if (!db.objectStoreNames.contains(STORE_ANCESTORS)) db.createObjectStore(STORE_ANCESTORS, { keyPath: ["leaf_id", "id"] }).createIndex("leaf_id", "leaf_id", { unique: false });
92
+ if (event.oldVersion > 0 && event.oldVersion < DB_VERSION) {
93
+ const tx = event.target.transaction;
94
+ const leaves = tx.objectStore(STORE_LEAVES);
95
+ if (!leaves.indexNames.contains("chain_complete")) leaves.createIndex("chain_complete", "chain_complete", { unique: false });
96
+ const ancestors = tx.objectStore(STORE_ANCESTORS);
97
+ const linked = /* @__PURE__ */ new Set();
98
+ ancestors.openCursor().onsuccess = (cursorEvent) => {
99
+ const cursor = cursorEvent.target.result;
100
+ if (cursor) {
101
+ linked.add(`${cursor.value.leaf_id}\u0000${cursor.value.id}`);
102
+ cursor.continue();
103
+ return;
104
+ }
105
+ leaves.openCursor().onsuccess = (leafEvent) => {
106
+ const leafCursor = leafEvent.target.result;
107
+ if (!leafCursor) return;
108
+ const row = leafCursor.value;
109
+ row.chain_complete = row.parent_node_id == null || linked.has(`${row.id}\u0000${row.parent_node_id}`) ? 1 : 0;
110
+ leafCursor.update(row);
111
+ leafCursor.continue();
112
+ };
113
+ };
114
+ }
115
+ if (!db.objectStoreNames.contains(STORE_RESERVATIONS)) db.createObjectStore(STORE_RESERVATIONS, { keyPath: "id" });
116
+ if (!db.objectStoreNames.contains(STORE_SPENT)) db.createObjectStore(STORE_SPENT, { keyPath: "id" });
117
+ if (!db.objectStoreNames.contains(STORE_SWAP_STATUS)) db.createObjectStore(STORE_SWAP_STATUS, { keyPath: "id" });
118
+ };
119
+ request.onsuccess = () => {
120
+ const db = request.result;
121
+ db.onversionchange = () => {
122
+ db.close();
123
+ this.db = null;
124
+ };
125
+ resolve(db);
126
+ };
127
+ request.onerror = () => reject(new TreeStoreError(`Failed to open IndexedDB: ${request.error?.message || "Unknown error"}`, request.error));
128
+ request.onblocked = () => {};
129
+ });
130
+ return this;
131
+ }
132
+ close() {
133
+ if (this.db) {
134
+ this.db.close();
135
+ this.db = null;
136
+ }
137
+ }
138
+ /**
139
+ * Runs one transaction. `reads` is a list of `{ name, store, key?, index?,
140
+ * all? }`: with a key it is a `get`, without it a `getAll`, and `all` with a
141
+ * key reads every record under that key; `index` reads through that
142
+ * index on `store` instead of the store directly. All reads are issued up
143
+ * front; once the last completes, `compute(results, tx)` runs synchronously
144
+ * in that read's success handler and may issue writes on `tx`. Its return
145
+ * value resolves the promise on `oncomplete`, so writes are durable before
146
+ * resolve. `compute` must never await a non-IDB promise (it would close `tx`).
147
+ */
148
+ _txRun(storeNames, mode, reads, compute) {
149
+ return new Promise((resolve, reject) => {
150
+ if (!this.db) {
151
+ reject(new TreeStoreError("Database not initialized"));
152
+ return;
153
+ }
154
+ let tx;
155
+ try {
156
+ tx = this.db.transaction(storeNames, mode);
157
+ } catch (e) {
158
+ reject(e);
159
+ return;
160
+ }
161
+ let outcome;
162
+ let settled = false;
163
+ let computeError = null;
164
+ const done = (fn) => {
165
+ if (!settled) {
166
+ settled = true;
167
+ fn();
168
+ }
169
+ };
170
+ tx.oncomplete = () => done(() => resolve(outcome));
171
+ tx.onabort = () => done(() => reject(computeError || new TreeStoreError(`transaction aborted: ${tx.error?.message || "unknown"}`, tx.error)));
172
+ tx.onerror = () => done(() => reject(computeError || new TreeStoreError(`transaction error: ${tx.error?.message || "unknown"}`, tx.error)));
173
+ const runCompute = (results) => {
174
+ try {
175
+ outcome = compute(results, tx);
176
+ } catch (e) {
177
+ computeError = e;
178
+ try {
179
+ tx.abort();
180
+ } catch (_) {
181
+ done(() => reject(e));
182
+ }
183
+ }
184
+ };
185
+ if (reads.length === 0) {
186
+ runCompute({});
187
+ return;
188
+ }
189
+ const results = {};
190
+ let pending = reads.length;
191
+ for (const r of reads) {
192
+ let req;
193
+ try {
194
+ const store = tx.objectStore(r.store);
195
+ const source = r.index ? store.index(r.index) : store;
196
+ if (r.keysOnly) req = source.getAllKeys(r.key);
197
+ else if (r.all) req = source.getAll(r.key);
198
+ else req = "key" in r ? source.get(r.key) : source.getAll();
199
+ } catch (e) {
200
+ done(() => reject(e));
201
+ return;
202
+ }
203
+ req.onsuccess = () => {
204
+ results[r.name] = req.result;
205
+ pending -= 1;
206
+ if (pending === 0) runCompute(results);
207
+ };
208
+ }
209
+ });
210
+ }
211
+ /**
212
+ * Reconstruct the exit chains for many leaves in one transaction, each as
213
+ * { leaf, ancestors } with ancestors nearest first. A leaf absent from the store
214
+ * is skipped; a chain that hits a gap comes back partial.
215
+ * @param {Array<string>} leafIds
216
+ * @returns {Promise<Array<{leaf: object, ancestors: Array<object>}>>}
217
+ */
218
+ async getExitChains(leafIds) {
219
+ try {
220
+ if (!leafIds || leafIds.length === 0) return [];
221
+ return await new Promise((resolve, reject) => {
222
+ if (!this.db) {
223
+ reject(new TreeStoreError("Database not initialized"));
224
+ return;
225
+ }
226
+ const tx = this.db.transaction([STORE_LEAVES, STORE_ANCESTORS], "readonly");
227
+ const leavesStore = tx.objectStore(STORE_LEAVES);
228
+ const ancestorsIndex = tx.objectStore(STORE_ANCESTORS).index("leaf_id");
229
+ const leafById = /* @__PURE__ */ new Map();
230
+ const ancestorsByLeaf = /* @__PURE__ */ new Map();
231
+ let settled = false;
232
+ const fail = (msg) => {
233
+ if (!settled) {
234
+ settled = true;
235
+ reject(new TreeStoreError(msg));
236
+ }
237
+ };
238
+ tx.onabort = () => fail(`Failed to get exit chains: ${tx.error?.message || "aborted"}`);
239
+ tx.onerror = () => fail(`Failed to get exit chains: ${tx.error?.message || "error"}`);
240
+ tx.oncomplete = () => {
241
+ if (settled) return;
242
+ settled = true;
243
+ const result = [];
244
+ for (const id of leafIds) {
245
+ const leafRow = leafById.get(id);
246
+ if (!leafRow) continue;
247
+ const nodes = ancestorsByLeaf.get(id) || /* @__PURE__ */ new Map();
248
+ nodes.set(leafRow.id, leafRow.data);
249
+ const pedigree = assembleExitChain(nodes, id);
250
+ if (pedigree) result.push(pedigree);
251
+ }
252
+ resolve(result);
253
+ };
254
+ for (const id of leafIds) {
255
+ const leafReq = leavesStore.get(id);
256
+ leafReq.onsuccess = () => {
257
+ if (leafReq.result) leafById.set(id, leafReq.result);
258
+ };
259
+ const ancestorsReq = ancestorsIndex.getAll(id);
260
+ ancestorsReq.onsuccess = () => {
261
+ const nodes = /* @__PURE__ */ new Map();
262
+ for (const row of ancestorsReq.result || []) nodes.set(row.id, row.data);
263
+ ancestorsByLeaf.set(id, nodes);
264
+ };
265
+ }
266
+ });
267
+ } catch (error) {
268
+ if (error instanceof TreeStoreError) throw error;
269
+ throw new TreeStoreError(`Failed to get exit chains: ${error.message}`, error);
270
+ }
271
+ }
272
+ /**
273
+ * Ids of the stored leaves whose chain cannot back an exit: the leaf has a
274
+ * parent, and no ancestor row of its own holds that parent.
275
+ * @returns {Promise<Array<string>>}
276
+ */
277
+ async leavesMissingExitChains() {
278
+ try {
279
+ return await this._txRun([STORE_LEAVES], "readonly", [{
280
+ name: "ids",
281
+ store: STORE_LEAVES,
282
+ index: "chain_complete",
283
+ key: 0,
284
+ keysOnly: true
285
+ }], (res) => res.ids);
286
+ } catch (error) {
287
+ if (error instanceof TreeStoreError) throw error;
288
+ throw new TreeStoreError(`Failed to get leaves missing exit chains: ${error.message}`, error);
289
+ }
290
+ }
291
+ async getLeaves() {
292
+ try {
293
+ return await this._txRun([STORE_LEAVES, STORE_RESERVATIONS], "readonly", [{
294
+ name: "leaves",
295
+ store: STORE_LEAVES
296
+ }, {
297
+ name: "reservations",
298
+ store: STORE_RESERVATIONS
299
+ }], (res) => {
300
+ const resMap = new Map(res.reservations.map((r) => [r.id, r]));
301
+ const available = [];
302
+ const notAvailable = [];
303
+ const availableMissingFromOperators = [];
304
+ const reservedForPayment = [];
305
+ const reservedForSwap = [];
306
+ for (const row of res.leaves) {
307
+ const node = row.data;
308
+ const purpose = row.reservation_id != null ? resMap.get(row.reservation_id)?.purpose : void 0;
309
+ const spendable = node.status === "Available";
310
+ if (purpose) {
311
+ if (purpose === "Payment") reservedForPayment.push(node);
312
+ else if (purpose === "Swap") reservedForSwap.push(node);
313
+ } else if (!spendable) notAvailable.push(node);
314
+ else if (row.is_missing_from_operators) availableMissingFromOperators.push(node);
315
+ else available.push(node);
316
+ }
317
+ return {
318
+ available,
319
+ notAvailable,
320
+ availableMissingFromOperators,
321
+ reservedForPayment,
322
+ reservedForSwap
323
+ };
324
+ });
325
+ } catch (error) {
326
+ if (error instanceof TreeStoreError) throw error;
327
+ throw new TreeStoreError(`Failed to get leaves: ${error.message}`, error);
328
+ }
329
+ }
330
+ async getAvailableBalance() {
331
+ try {
332
+ return await this._txRun([STORE_LEAVES, STORE_RESERVATIONS], "readonly", [{
333
+ name: "leaves",
334
+ store: STORE_LEAVES
335
+ }, {
336
+ name: "reservations",
337
+ store: STORE_RESERVATIONS
338
+ }], (res) => {
339
+ const resMap = new Map(res.reservations.map((r) => [r.id, r]));
340
+ let balance = 0n;
341
+ for (const row of res.leaves) {
342
+ const purpose = row.reservation_id != null ? resMap.get(row.reservation_id)?.purpose : void 0;
343
+ if (row.reservation_id == null && row.status === "Available" || purpose === "Swap") balance += BigInt(row.value);
344
+ }
345
+ return balance;
346
+ });
347
+ } catch (error) {
348
+ if (error instanceof TreeStoreError) throw error;
349
+ throw new TreeStoreError(`Failed to get available balance: ${error.message}`, error);
350
+ }
351
+ }
352
+ async getVerifiedLeafKeys() {
353
+ try {
354
+ return await this._txRun([STORE_LEAVES], "readonly", [{
355
+ name: "leaves",
356
+ store: STORE_LEAVES
357
+ }], (res) => res.leaves.map((row) => [
358
+ row.id,
359
+ row.verifying_public_key,
360
+ row.signing_public_key
361
+ ]));
362
+ } catch (error) {
363
+ if (error instanceof TreeStoreError) throw error;
364
+ throw new TreeStoreError(`Failed to get verified leaf keys: ${error.message}`, error);
365
+ }
366
+ }
367
+ async now() {
368
+ return this._nowMs();
369
+ }
370
+ async addLeaves(leaves) {
371
+ try {
372
+ if (!leaves || leaves.length === 0) return;
373
+ const leafIds = leaves.map((l) => l.id);
374
+ await this._txRun([STORE_LEAVES, STORE_SPENT], "readwrite", [{
375
+ name: "leaves",
376
+ store: STORE_LEAVES
377
+ }], (res, tx) => {
378
+ const leavesStore = tx.objectStore(STORE_LEAVES);
379
+ const spentStore = tx.objectStore(STORE_SPENT);
380
+ const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
381
+ for (const id of leafIds) spentStore.delete(id);
382
+ for (const leaf of leaves) leavesStore.put(this._leafRow(leaf, false, leafMap.get(leaf.id)));
383
+ });
384
+ } catch (error) {
385
+ if (error instanceof TreeStoreError) throw error;
386
+ throw new TreeStoreError(`Failed to add leaves: ${error.message}`, error);
387
+ }
388
+ }
389
+ async storeAncestors(pedigrees) {
390
+ try {
391
+ if (!pedigrees || pedigrees.length === 0) return;
392
+ const reads = [];
393
+ for (const p of pedigrees) {
394
+ reads.push({
395
+ name: `leaf:${p.leaf.id}`,
396
+ store: STORE_LEAVES,
397
+ key: p.leaf.id
398
+ });
399
+ reads.push({
400
+ name: `ancestors:${p.leaf.id}`,
401
+ store: STORE_ANCESTORS,
402
+ index: "leaf_id",
403
+ key: p.leaf.id,
404
+ all: true
405
+ });
406
+ }
407
+ await this._txRun([STORE_LEAVES, STORE_ANCESTORS], "readwrite", reads, (res, tx) => {
408
+ const ancestorsStore = tx.objectStore(STORE_ANCESTORS);
409
+ const leavesStore = tx.objectStore(STORE_LEAVES);
410
+ const ancestorRowsByLeaf = this._ancestorRowsByLeaf(pedigrees.flatMap((p) => res[`ancestors:${p.leaf.id}`] || []));
411
+ const leafRowsById = /* @__PURE__ */ new Map();
412
+ for (const p of pedigrees) {
413
+ const row = res[`leaf:${p.leaf.id}`];
414
+ if (row) leafRowsById.set(row.id, row);
415
+ }
416
+ for (const p of pedigrees) {
417
+ const leafRow = leafRowsById.get(p.leaf.id);
418
+ if (!leafRow) continue;
419
+ this._replaceAncestors(ancestorsStore, ancestorRowsByLeaf, p.leaf.id, p.ancestors);
420
+ const chainComplete = this._chainComplete(leafRow.data, p.ancestors, leafRow);
421
+ if (chainComplete !== !!leafRow.chain_complete) leavesStore.put({
422
+ ...leafRow,
423
+ chain_complete: chainComplete ? 1 : 0
424
+ });
425
+ }
426
+ });
427
+ } catch (error) {
428
+ if (error instanceof TreeStoreError) throw error;
429
+ throw new TreeStoreError(`Failed to store ancestors: ${error.message}`, error);
430
+ }
431
+ }
432
+ async setLeaves(leaves, missingLeaves, refreshStartedAtMs) {
433
+ try {
434
+ const reported = leaves || [];
435
+ const missing = missingLeaves || [];
436
+ const refreshMs = refreshStartedAtMs;
437
+ await this._txRun([
438
+ STORE_LEAVES,
439
+ STORE_ANCESTORS,
440
+ STORE_RESERVATIONS,
441
+ STORE_SPENT,
442
+ STORE_SWAP_STATUS
443
+ ], "readwrite", [
444
+ {
445
+ name: "leaves",
446
+ store: STORE_LEAVES
447
+ },
448
+ {
449
+ name: "ancestors",
450
+ store: STORE_ANCESTORS
451
+ },
452
+ {
453
+ name: "reservations",
454
+ store: STORE_RESERVATIONS
455
+ },
456
+ {
457
+ name: "spent",
458
+ store: STORE_SPENT
459
+ },
460
+ {
461
+ name: "swap",
462
+ store: STORE_SWAP_STATUS,
463
+ key: SWAP_STATUS_ID
464
+ }
465
+ ], (res, tx) => {
466
+ const leavesStore = tx.objectStore(STORE_LEAVES);
467
+ const ancestorsStore = tx.objectStore(STORE_ANCESTORS);
468
+ const reservationsStore = tx.objectStore(STORE_RESERVATIONS);
469
+ const spentStore = tx.objectStore(STORE_SPENT);
470
+ const nowMs = this._nowMs();
471
+ const leafMap = new Map(res.leaves.map((r) => [r.id, { ...r }]));
472
+ const ancestorRowsByLeaf = this._ancestorRowsByLeaf(res.ancestors);
473
+ const staleCutoff = nowMs - RESERVATION_TIMEOUT_MS;
474
+ const staleIds = new Set(res.reservations.filter((r) => r.created_at < staleCutoff).map((r) => r.id));
475
+ for (const id of staleIds) reservationsStore.delete(id);
476
+ for (const row of leafMap.values()) if (row.reservation_id != null && staleIds.has(row.reservation_id)) {
477
+ row.reservation_id = null;
478
+ leavesStore.put(row);
479
+ }
480
+ const hasActiveSwap = res.reservations.filter((r) => !staleIds.has(r.id)).some((r) => r.purpose === "Swap");
481
+ const swapCompletedDuringRefresh = !!res.swap && res.swap.last_completed_at >= refreshMs;
482
+ if (hasActiveSwap || swapCompletedDuringRefresh) return;
483
+ const spentCleanupCutoff = refreshMs - SPENT_MARKER_CLEANUP_THRESHOLD_MS;
484
+ for (const s of res.spent) if (s.spent_at < spentCleanupCutoff) spentStore.delete(s.id);
485
+ const spentIds = new Set(res.spent.filter((s) => s.spent_at >= refreshMs).map((s) => s.id));
486
+ const priorRows = new Map(leafMap);
487
+ const deletedIds = [];
488
+ for (const row of Array.from(leafMap.values())) if (row.reservation_id == null && row.added_at < refreshMs) {
489
+ leavesStore.delete(row.id);
490
+ leafMap.delete(row.id);
491
+ deletedIds.push(row.id);
492
+ }
493
+ const liveLeaves = reported.filter((p) => !spentIds.has(p.id));
494
+ const liveMissing = missing.filter((p) => !spentIds.has(p.id));
495
+ for (const p of liveLeaves) {
496
+ const row = this._leafRow(p, false, priorRows.get(p.id));
497
+ leavesStore.put(row);
498
+ leafMap.set(row.id, row);
499
+ }
500
+ for (const p of liveMissing) {
501
+ const row = this._leafRow(p, true, priorRows.get(p.id));
502
+ leavesStore.put(row);
503
+ leafMap.set(row.id, row);
504
+ }
505
+ const survivingIds = /* @__PURE__ */ new Set();
506
+ for (const p of reported.concat(missing)) if (!spentIds.has(p.id)) survivingIds.add(p.id);
507
+ for (const id of deletedIds) {
508
+ if (survivingIds.has(id)) continue;
509
+ for (const existingId of (ancestorRowsByLeaf.get(id) || /* @__PURE__ */ new Map()).keys()) ancestorsStore.delete([id, existingId]);
510
+ }
511
+ });
512
+ } catch (error) {
513
+ if (error instanceof TreeStoreError) throw error;
514
+ throw new TreeStoreError(`Failed to set leaves: ${error.message}`, error);
515
+ }
516
+ }
517
+ async cancelReservation(id, leavesToKeep) {
518
+ try {
519
+ const keep = leavesToKeep || [];
520
+ await this._txRun([
521
+ STORE_LEAVES,
522
+ STORE_ANCESTORS,
523
+ STORE_RESERVATIONS
524
+ ], "readwrite", [
525
+ {
526
+ name: "leaves",
527
+ store: STORE_LEAVES
528
+ },
529
+ {
530
+ name: "ancestors",
531
+ store: STORE_ANCESTORS
532
+ },
533
+ {
534
+ name: "res",
535
+ store: STORE_RESERVATIONS,
536
+ key: id
537
+ }
538
+ ], (res, tx) => {
539
+ const leavesStore = tx.objectStore(STORE_LEAVES);
540
+ const ancestorsStore = tx.objectStore(STORE_ANCESTORS);
541
+ const reservationsStore = tx.objectStore(STORE_RESERVATIONS);
542
+ const keepIds = new Set(keep.map((l) => l.id));
543
+ const ancestorRowsByLeaf = this._ancestorRowsByLeaf(res.ancestors);
544
+ const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
545
+ const priorRows = new Map(leafMap);
546
+ for (const l of res.leaves) {
547
+ if (l.reservation_id !== id) continue;
548
+ leavesStore.delete(l.id);
549
+ leafMap.delete(l.id);
550
+ if (!keepIds.has(l.id)) for (const existingId of (ancestorRowsByLeaf.get(l.id) || /* @__PURE__ */ new Map()).keys()) ancestorsStore.delete([l.id, existingId]);
551
+ }
552
+ reservationsStore.delete(id);
553
+ if (keep.length > 0) for (const leaf of keep) {
554
+ const prior = priorRows.get(leaf.id);
555
+ const held = prior?.reservation_id === id ? void 0 : prior;
556
+ const chainComplete = this._chainComplete(leaf, void 0, prior);
557
+ leavesStore.put(this._leafRow(leaf, false, held, chainComplete));
558
+ }
559
+ });
560
+ } catch (error) {
561
+ if (error instanceof TreeStoreError) throw error;
562
+ throw new TreeStoreError(`Failed to cancel reservation '${id}': ${error.message}`, error);
563
+ }
564
+ }
565
+ async finalizeReservation(id, newLeaves) {
566
+ try {
567
+ const added = newLeaves || null;
568
+ await this._txRun([
569
+ STORE_LEAVES,
570
+ STORE_ANCESTORS,
571
+ STORE_RESERVATIONS,
572
+ STORE_SPENT,
573
+ STORE_SWAP_STATUS
574
+ ], "readwrite", [
575
+ {
576
+ name: "leaves",
577
+ store: STORE_LEAVES
578
+ },
579
+ {
580
+ name: "ancestors",
581
+ store: STORE_ANCESTORS
582
+ },
583
+ {
584
+ name: "res",
585
+ store: STORE_RESERVATIONS,
586
+ key: id
587
+ }
588
+ ], (res, tx) => {
589
+ const leavesStore = tx.objectStore(STORE_LEAVES);
590
+ const ancestorsStore = tx.objectStore(STORE_ANCESTORS);
591
+ const reservationsStore = tx.objectStore(STORE_RESERVATIONS);
592
+ const spentStore = tx.objectStore(STORE_SPENT);
593
+ const swapStore = tx.objectStore(STORE_SWAP_STATUS);
594
+ const nowMs = this._nowMs();
595
+ const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
596
+ const ancestorRowsByLeaf = this._ancestorRowsByLeaf(res.ancestors);
597
+ let isSwap = false;
598
+ if (res.res) {
599
+ isSwap = res.res.purpose === "Swap";
600
+ for (const l of res.leaves) if (l.reservation_id === id) {
601
+ spentStore.put({
602
+ id: l.id,
603
+ spent_at: nowMs
604
+ });
605
+ leavesStore.delete(l.id);
606
+ leafMap.delete(l.id);
607
+ for (const existingId of (ancestorRowsByLeaf.get(l.id) || /* @__PURE__ */ new Map()).keys()) ancestorsStore.delete([l.id, existingId]);
608
+ }
609
+ reservationsStore.delete(id);
610
+ }
611
+ if (added && added.length > 0) for (const p of added) {
612
+ const row = this._leafRow(p, false, leafMap.get(p.id));
613
+ leavesStore.put(row);
614
+ leafMap.set(row.id, row);
615
+ }
616
+ if (isSwap && added && added.length > 0) swapStore.put({
617
+ id: SWAP_STATUS_ID,
618
+ last_completed_at: nowMs
619
+ });
620
+ });
621
+ } catch (error) {
622
+ if (error instanceof TreeStoreError) throw error;
623
+ throw new TreeStoreError(`Failed to finalize reservation '${id}': ${error.message}`, error);
624
+ }
625
+ }
626
+ async updateReservation(reservationId, reservedLeaves, changeLeaves) {
627
+ try {
628
+ const reserved = reservedLeaves || [];
629
+ const change = changeLeaves || [];
630
+ return await this._txRun([
631
+ STORE_LEAVES,
632
+ STORE_ANCESTORS,
633
+ STORE_RESERVATIONS,
634
+ STORE_SPENT
635
+ ], "readwrite", [
636
+ {
637
+ name: "leaves",
638
+ store: STORE_LEAVES
639
+ },
640
+ {
641
+ name: "ancestors",
642
+ store: STORE_ANCESTORS
643
+ },
644
+ {
645
+ name: "res",
646
+ store: STORE_RESERVATIONS,
647
+ key: reservationId
648
+ }
649
+ ], (res, tx) => {
650
+ if (!res.res) throw new TreeStoreError(`Reservation ${reservationId} not found`);
651
+ const leavesStore = tx.objectStore(STORE_LEAVES);
652
+ const ancestorsStore = tx.objectStore(STORE_ANCESTORS);
653
+ const reservationsStore = tx.objectStore(STORE_RESERVATIONS);
654
+ const spentStore = tx.objectStore(STORE_SPENT);
655
+ const nowMs = this._nowMs();
656
+ const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
657
+ const ancestorRowsByLeaf = this._ancestorRowsByLeaf(res.ancestors);
658
+ for (const l of res.leaves) if (l.reservation_id === reservationId) {
659
+ spentStore.put({
660
+ id: l.id,
661
+ spent_at: nowMs
662
+ });
663
+ leavesStore.delete(l.id);
664
+ leafMap.delete(l.id);
665
+ for (const existingId of (ancestorRowsByLeaf.get(l.id) || /* @__PURE__ */ new Map()).keys()) ancestorsStore.delete([l.id, existingId]);
666
+ }
667
+ change.concat(reserved);
668
+ for (const p of change) {
669
+ const row = this._leafRow(p, false, leafMap.get(p.id));
670
+ leavesStore.put(row);
671
+ leafMap.set(row.id, row);
672
+ }
673
+ for (const p of reserved) {
674
+ const row = this._leafRow(p, false, leafMap.get(p.id));
675
+ row.reservation_id = reservationId;
676
+ leavesStore.put(row);
677
+ leafMap.set(row.id, row);
678
+ }
679
+ reservationsStore.put({
680
+ ...res.res,
681
+ pending_change_amount: 0
682
+ });
683
+ return {
684
+ id: reservationId,
685
+ leaves: reserved
686
+ };
687
+ });
688
+ } catch (error) {
689
+ if (error instanceof TreeStoreError) throw error;
690
+ throw new TreeStoreError(`Failed to update reservation '${reservationId}': ${error.message}`, error);
691
+ }
692
+ }
693
+ async tryReserveLeaves(targetAmounts, exactOnly, purpose) {
694
+ try {
695
+ return await this._txRun([STORE_LEAVES, STORE_RESERVATIONS], "readwrite", [{
696
+ name: "leaves",
697
+ store: STORE_LEAVES
698
+ }, {
699
+ name: "reservations",
700
+ store: STORE_RESERVATIONS
701
+ }], (res, tx) => {
702
+ const leavesStore = tx.objectStore(STORE_LEAVES);
703
+ const reservationsStore = tx.objectStore(STORE_RESERVATIONS);
704
+ const targetAmount = targetAmounts ? this._totalSats(targetAmounts) : 0;
705
+ const maxTarget = this._maxTargetForPrefilter(targetAmounts);
706
+ const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
707
+ const eligible = this._eligibleSlim(res.leaves);
708
+ const available = eligible.reduce((s, l) => s + l.value, 0);
709
+ const slim = this._slimCandidates(eligible, maxTarget);
710
+ const pending = res.reservations.reduce((s, r) => s + (r.pending_change_amount || 0), 0);
711
+ const selected = this._selectLeavesByTargetAmounts(slim, targetAmounts);
712
+ if (selected !== null) {
713
+ if (selected.length === 0) throw new TreeStoreError("NonReservableLeaves");
714
+ const ids = selected.map((s) => s.id);
715
+ const fullLeaves = ids.map((leafId) => leafMap.get(leafId).data);
716
+ const reservationId = this._generateId();
717
+ this._createReservation(reservationsStore, leavesStore, leafMap, reservationId, ids, purpose, 0);
718
+ return {
719
+ type: "success",
720
+ reservation: {
721
+ id: reservationId,
722
+ leaves: fullLeaves
723
+ }
724
+ };
725
+ }
726
+ if (!exactOnly) {
727
+ const minSelected = this._selectLeavesByMinimumAmount(slim, targetAmount);
728
+ if (minSelected !== null) {
729
+ const ids = minSelected.map((s) => s.id);
730
+ const fullLeaves = ids.map((leafId) => leafMap.get(leafId).data);
731
+ const reservedAmount = fullLeaves.reduce((s, l) => s + l.value, 0);
732
+ const pendingChange = reservedAmount > targetAmount && targetAmount > 0 ? reservedAmount - targetAmount : 0;
733
+ const reservationId = this._generateId();
734
+ this._createReservation(reservationsStore, leavesStore, leafMap, reservationId, ids, purpose, pendingChange);
735
+ return {
736
+ type: "success",
737
+ reservation: {
738
+ id: reservationId,
739
+ leaves: fullLeaves
740
+ }
741
+ };
742
+ }
743
+ }
744
+ if (available + pending >= targetAmount) return {
745
+ type: "waitForPending",
746
+ needed: targetAmount,
747
+ available,
748
+ pending
749
+ };
750
+ return { type: "insufficientFunds" };
751
+ });
752
+ } catch (error) {
753
+ if (error instanceof TreeStoreError) throw error;
754
+ throw new TreeStoreError(`Failed to try reserve leaves: ${error.message}`, error);
755
+ }
756
+ }
757
+ async tryReserveLeavesByIds(leafIds, purpose) {
758
+ try {
759
+ if (!leafIds || leafIds.length === 0) throw new TreeStoreError("NonReservableLeaves");
760
+ return await this._txRun([STORE_LEAVES, STORE_RESERVATIONS], "readwrite", [{
761
+ name: "leaves",
762
+ store: STORE_LEAVES
763
+ }], (res, tx) => {
764
+ const leavesStore = tx.objectStore(STORE_LEAVES);
765
+ const reservationsStore = tx.objectStore(STORE_RESERVATIONS);
766
+ const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
767
+ const eligibleIds = new Set(res.leaves.filter((r) => r.status === "Available" && !r.is_missing_from_operators && r.reservation_id == null).map((r) => r.id));
768
+ if (new Set(leafIds.filter((id) => eligibleIds.has(id))).size !== leafIds.length) throw new TreeStoreError("NonReservableLeaves");
769
+ const fullLeaves = leafIds.map((id) => leafMap.get(id).data);
770
+ const reservationId = this._generateId();
771
+ this._createReservation(reservationsStore, leavesStore, leafMap, reservationId, leafIds, purpose, 0);
772
+ return {
773
+ id: reservationId,
774
+ leaves: fullLeaves
775
+ };
776
+ });
777
+ } catch (error) {
778
+ if (error instanceof TreeStoreError) throw error;
779
+ throw new TreeStoreError(`Failed to try reserve leaves by ids: ${error.message}`, error);
780
+ }
781
+ }
782
+ async trySelectLeaves(targetAmounts) {
783
+ try {
784
+ const targetAmount = targetAmounts ? this._totalSats(targetAmounts) : 0;
785
+ const maxTarget = this._maxTargetForPrefilter(targetAmounts);
786
+ return await this._txRun([STORE_LEAVES], "readonly", [{
787
+ name: "leaves",
788
+ store: STORE_LEAVES
789
+ }], (res) => {
790
+ const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
791
+ const slim = this._slimCandidates(this._eligibleSlim(res.leaves), maxTarget);
792
+ const selected = this._selectLeavesByTargetAmounts(slim, targetAmounts);
793
+ if (selected !== null && selected.length > 0) return {
794
+ type: "exact",
795
+ leaves: selected.map((s) => leafMap.get(s.id).data)
796
+ };
797
+ const minSelected = this._selectLeavesByMinimumAmount(slim, targetAmount);
798
+ if (minSelected !== null) return {
799
+ type: "swapNeeded",
800
+ leaves: minSelected.map((s) => leafMap.get(s.id).data)
801
+ };
802
+ return { type: "insufficientFunds" };
803
+ });
804
+ } catch (error) {
805
+ if (error instanceof TreeStoreError) throw error;
806
+ throw new TreeStoreError(`Failed to try select leaves: ${error.message}`, error);
807
+ }
808
+ }
809
+ _nowMs() {
810
+ return Date.now();
811
+ }
812
+ _generateId() {
813
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
814
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
815
+ const r = Math.random() * 16 | 0;
816
+ return (c === "x" ? r : r & 3 | 8).toString(16);
817
+ });
818
+ }
819
+ /** Row shape for the leaf pool. Preserves `reservation_id` from an existing
820
+ * row; every other field comes from the operators' latest copy. */
821
+ _leafRow(node, isMissing, existingRow, chainComplete) {
822
+ return {
823
+ id: node.id,
824
+ parent_node_id: node.parent_node_id ?? null,
825
+ status: node.status,
826
+ value: node.value,
827
+ verifying_public_key: node.verifying_public_key,
828
+ signing_public_key: node.signing_keyshare.public_key,
829
+ is_missing_from_operators: !!isMissing,
830
+ reservation_id: existingRow ? existingRow.reservation_id ?? null : null,
831
+ added_at: this._nowMs(),
832
+ chain_complete: (chainComplete !== void 0 ? chainComplete : this._chainComplete(node, void 0, existingRow)) ? 1 : 0,
833
+ data: node
834
+ };
835
+ }
836
+ /**
837
+ * Whether `node` has a stored chain reaching its current parent, which is what
838
+ * makes it exitable without the operators. A stored chain runs from some parent
839
+ * to a root, so holding the parent the leaf has now means it spans the whole
840
+ * path. A leaf that is itself a root needs no ancestors. An empty incoming
841
+ * chain means "unknown", not "none", so it keeps whatever the row already
842
+ * claimed, unless the leaf has been reparented since: the chain then describes
843
+ * the parent it had before.
844
+ */
845
+ _chainComplete(node, ancestors, existingRow) {
846
+ if (node.parent_node_id == null) return true;
847
+ if (ancestors && ancestors.length > 0) return ancestors.some((a) => a.id === node.parent_node_id);
848
+ if (!existingRow) return false;
849
+ return existingRow.parent_node_id === node.parent_node_id && !!existingRow.chain_complete;
850
+ }
851
+ /** Row shape for an ancestor: no pool metadata (reservation / missing / added_at).
852
+ * `leaf_id` plus `id` is the key, so the same node stores one row per leaf
853
+ * that descends from it rather than one row shared by all of them. */
854
+ _ancestorRow(leafId, node) {
855
+ return {
856
+ leaf_id: leafId,
857
+ id: node.id,
858
+ parent_node_id: node.parent_node_id ?? null,
859
+ status: node.status,
860
+ value: node.value,
861
+ verifying_public_key: node.verifying_public_key,
862
+ signing_public_key: node.signing_keyshare.public_key,
863
+ data: node
864
+ };
865
+ }
866
+ /** Groups ancestor rows (from a full-store read) by the leaf that owns them,
867
+ * as `Map<leaf_id, Map<id, row>>`, so a leaf's existing rows can be found
868
+ * and deleted by key without a range scan on the compound `[leaf_id, id]`
869
+ * keyPath. */
870
+ _ancestorRowsByLeaf(ancestorRows) {
871
+ const map = /* @__PURE__ */ new Map();
872
+ for (const row of ancestorRows || []) {
873
+ let byId = map.get(row.leaf_id);
874
+ if (!byId) {
875
+ byId = /* @__PURE__ */ new Map();
876
+ map.set(row.leaf_id, byId);
877
+ }
878
+ byId.set(row.id, row);
879
+ }
880
+ return map;
881
+ }
882
+ /**
883
+ * Replaces `leafId`'s ancestor rows wholesale (delete then insert), keeping
884
+ * `ancestorRowsByLeaf` current so a later step in the same transaction sees
885
+ * this leaf's live rows rather than a stale snapshot. An empty `nodes` list
886
+ * is a no-op: it means the chain is unknown, not that the leaf has none.
887
+ */
888
+ _replaceAncestors(ancestorsStore, ancestorRowsByLeaf, leafId, nodes) {
889
+ if (!nodes || nodes.length === 0) return;
890
+ const ownRows = ancestorRowsByLeaf.get(leafId);
891
+ if (ownRows) for (const existingId of ownRows.keys()) ancestorsStore.delete([leafId, existingId]);
892
+ const newRows = /* @__PURE__ */ new Map();
893
+ for (const node of nodes) {
894
+ const row = this._ancestorRow(leafId, node);
895
+ ancestorsStore.put(row);
896
+ newRows.set(node.id, row);
897
+ }
898
+ ancestorRowsByLeaf.set(leafId, newRows);
899
+ }
900
+ _createReservation(reservationsStore, leavesStore, leafMap, reservationId, leafIds, purpose, pendingChange) {
901
+ reservationsStore.put({
902
+ id: reservationId,
903
+ purpose,
904
+ pending_change_amount: pendingChange,
905
+ created_at: this._nowMs()
906
+ });
907
+ for (const id of leafIds) {
908
+ const row = leafMap.get(id);
909
+ if (row) {
910
+ row.reservation_id = reservationId;
911
+ leavesStore.put(row);
912
+ }
913
+ }
914
+ }
915
+ /** Slim `{id, value}` projection of the leaves eligible for selection. */
916
+ _eligibleSlim(leafRows) {
917
+ return leafRows.filter((r) => r.status === "Available" && !r.is_missing_from_operators && r.reservation_id == null).map((r) => ({
918
+ id: r.id,
919
+ value: r.value
920
+ }));
921
+ }
922
+ /**
923
+ * Prefilter mirroring the SQL slim candidate set: every eligible leaf with
924
+ * value <= maxTarget, plus the single smallest leaf with value > maxTarget
925
+ * (the minimum-amount fallback where one larger leaf alone suffices).
926
+ */
927
+ _slimCandidates(eligible, maxTarget) {
928
+ const small = eligible.filter((l) => l.value <= maxTarget);
929
+ let smallestBig = null;
930
+ for (const l of eligible) if (l.value > maxTarget && (smallestBig === null || l.value < smallestBig.value)) smallestBig = l;
931
+ return smallestBig ? [...small, smallestBig] : small;
932
+ }
933
+ _maxTargetForPrefilter(targetAmounts) {
934
+ if (!targetAmounts) return Number.MAX_SAFE_INTEGER;
935
+ if (targetAmounts.type === "amountAndFee") return targetAmounts.amountSats + (targetAmounts.feeSats || 0);
936
+ if (targetAmounts.type === "exactDenominations") return targetAmounts.denominations.reduce((m, v) => m + v, 0);
937
+ return Number.MAX_SAFE_INTEGER;
938
+ }
939
+ _totalSats(targetAmounts) {
940
+ if (targetAmounts.type === "amountAndFee") return targetAmounts.amountSats + (targetAmounts.feeSats || 0);
941
+ if (targetAmounts.type === "exactDenominations") return targetAmounts.denominations.reduce((sum, d) => sum + d, 0);
942
+ return 0;
943
+ }
944
+ _selectLeavesByTargetAmounts(leaves, targetAmounts) {
945
+ if (!targetAmounts) return [...leaves];
946
+ if (targetAmounts.type === "amountAndFee") {
947
+ const amountLeaves = this._selectLeavesByExactAmount(leaves, targetAmounts.amountSats);
948
+ if (amountLeaves === null) return null;
949
+ if (targetAmounts.feeSats != null && targetAmounts.feeSats > 0) {
950
+ const amountIds = new Set(amountLeaves.map((l) => l.id));
951
+ const remaining = leaves.filter((l) => !amountIds.has(l.id));
952
+ const feeLeaves = this._selectLeavesByExactAmount(remaining, targetAmounts.feeSats);
953
+ if (feeLeaves === null) return null;
954
+ return [...amountLeaves, ...feeLeaves];
955
+ }
956
+ return amountLeaves;
957
+ }
958
+ if (targetAmounts.type === "exactDenominations") return this._selectLeavesByExactDenominations(leaves, targetAmounts.denominations);
959
+ return null;
960
+ }
961
+ _selectLeavesByExactAmount(leaves, targetAmount) {
962
+ if (targetAmount === 0) return null;
963
+ if (leaves.reduce((sum, l) => sum + l.value, 0) < targetAmount) return null;
964
+ const single = leaves.find((l) => l.value === targetAmount);
965
+ if (single) return [single];
966
+ return this._findExactMultipleMatch(leaves, targetAmount);
967
+ }
968
+ _selectLeavesByExactDenominations(leaves, denominations) {
969
+ const remaining = [...leaves];
970
+ const selected = [];
971
+ for (const denomination of denominations) {
972
+ const idx = remaining.findIndex((l) => l.value === denomination);
973
+ if (idx === -1) return null;
974
+ selected.push(remaining[idx]);
975
+ remaining.splice(idx, 1);
976
+ }
977
+ return selected;
978
+ }
979
+ _selectLeavesByMinimumAmount(leaves, targetAmount) {
980
+ if (targetAmount === 0) return null;
981
+ if (leaves.reduce((sum, l) => sum + l.value, 0) < targetAmount) return null;
982
+ const result = [];
983
+ let sum = 0;
984
+ for (const leaf of leaves) {
985
+ sum += leaf.value;
986
+ result.push(leaf);
987
+ if (sum >= targetAmount) break;
988
+ }
989
+ return sum >= targetAmount ? result : null;
990
+ }
991
+ _findExactMultipleMatch(leaves, targetAmount) {
992
+ if (targetAmount === 0) return [];
993
+ if (leaves.length === 0) return null;
994
+ const result = this._greedyExactMatch(leaves, targetAmount);
995
+ if (result) return result;
996
+ const powerOfTwoLeaves = leaves.filter((l) => this._isPowerOfTwo(l.value));
997
+ if (powerOfTwoLeaves.length === leaves.length) return null;
998
+ return this._greedyExactMatch(powerOfTwoLeaves, targetAmount);
999
+ }
1000
+ _greedyExactMatch(leaves, targetAmount) {
1001
+ const sorted = [...leaves].sort((a, b) => b.value - a.value);
1002
+ const result = [];
1003
+ let remaining = targetAmount;
1004
+ for (const leaf of sorted) {
1005
+ if (leaf.value > remaining) continue;
1006
+ remaining -= leaf.value;
1007
+ result.push(leaf);
1008
+ if (remaining === 0) return result;
1009
+ }
1010
+ return null;
1011
+ }
1012
+ _isPowerOfTwo(value) {
1013
+ return value > 0 && (value & value - 1) === 0;
1014
+ }
1015
+ };
1016
+ /**
1017
+ * Opens (or creates) the IndexedDB database and returns a ready tree store.
1018
+ *
1019
+ * @param {string} dbName - Database name (one database per SDK instance).
1020
+ * @param {object} [logger] - Optional logger.
1021
+ * @returns {Promise<WebTreeStore>}
1022
+ */
1023
+ async function createWebTreeStore(dbName, logger = null) {
1024
+ const store = new WebTreeStore(dbName, logger);
1025
+ await store.initialize();
1026
+ return store;
1027
+ }
1028
+ //#endregion
1029
+ export { createWebTreeStore };
1030
+
1031
+ //# sourceMappingURL=tree-store-N362nuzZ.js.map