@breeztech/breez-sdk-spark 0.27.0-dev2 → 0.27.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 (38) hide show
  1. package/breez-sdk-spark.tgz +0 -0
  2. package/bundler/breez_sdk_spark_wasm.d.ts +347 -95
  3. package/bundler/breez_sdk_spark_wasm_bg.js +210 -92
  4. package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
  5. package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +28 -14
  6. package/bundler/storage/index.js +251 -20
  7. package/bundler/tree-store/index.js +39 -6
  8. package/deno/breez_sdk_spark_wasm.d.ts +347 -95
  9. package/deno/breez_sdk_spark_wasm.js +210 -92
  10. package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
  11. package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +28 -14
  12. package/itest/database-drivers.test.js +45 -0
  13. package/nodejs/breez_sdk_spark_wasm.d.ts +347 -95
  14. package/nodejs/breez_sdk_spark_wasm.js +210 -92
  15. package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
  16. package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +28 -14
  17. package/nodejs/index.js +6 -0
  18. package/nodejs/mysql-session-store/index.cjs +3 -1
  19. package/nodejs/mysql-storage/index.cjs +238 -20
  20. package/nodejs/mysql-storage/migrations.cjs +49 -1
  21. package/nodejs/mysql-token-store/index.cjs +3 -7
  22. package/nodejs/mysql-tree-store/index.cjs +26 -1
  23. package/nodejs/postgres-session-store/index.cjs +3 -1
  24. package/nodejs/postgres-storage/index.cjs +228 -19
  25. package/nodejs/postgres-storage/migrations.cjs +49 -0
  26. package/nodejs/postgres-token-store/index.cjs +3 -1
  27. package/nodejs/postgres-tree-store/index.cjs +25 -1
  28. package/nodejs/postgres-tree-store/migrations.cjs +10 -0
  29. package/nodejs/storage/index.cjs +210 -19
  30. package/nodejs/storage/migrations.cjs +38 -0
  31. package/nodejs/tree-store/index.cjs +35 -0
  32. package/package.json +1 -1
  33. package/web/breez_sdk_spark_wasm.d.ts +375 -109
  34. package/web/breez_sdk_spark_wasm.js +210 -92
  35. package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
  36. package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +28 -14
  37. package/web/storage/index.js +251 -20
  38. package/web/tree-store/index.js +39 -6
@@ -22,16 +22,20 @@ export const bitcoinchainservicehandle_tipHeight: (a: number) => any;
22
22
  export const breezsdk_addContact: (a: number, b: any) => any;
23
23
  export const breezsdk_addEventListener: (a: number, b: any) => any;
24
24
  export const breezsdk_authorizeLightningAddressTransfer: (a: number, b: any) => any;
25
+ export const breezsdk_bridgeFromCashApp: (a: number, b: any) => any;
26
+ export const breezsdk_bridgeToCashApp: (a: number, b: any) => any;
25
27
  export const breezsdk_buildUnsignedBatchPackage: (a: number, b: any) => any;
26
28
  export const breezsdk_buildUnsignedLnurlPayPackage: (a: number, b: any) => any;
29
+ export const breezsdk_buildUnsignedPullPackage: (a: number, b: any) => any;
27
30
  export const breezsdk_buildUnsignedTransferPackage: (a: number, b: any) => any;
28
31
  export const breezsdk_buyBitcoin: (a: number, b: any) => any;
29
32
  export const breezsdk_checkLightningAddressAvailable: (a: number, b: any) => any;
30
33
  export const breezsdk_checkMessage: (a: number, b: any) => any;
31
- export const breezsdk_checkUnilateralExit: (a: number, b: any) => any;
34
+ export const breezsdk_checkRecoverFunds: (a: number, b: any) => any;
32
35
  export const breezsdk_claimDeposit: (a: number, b: any) => any;
33
36
  export const breezsdk_claimHtlcPayment: (a: number, b: any) => any;
34
37
  export const breezsdk_claimLightningAddressTransfer: (a: number, b: any) => any;
38
+ export const breezsdk_createTokenAllowance: (a: number, b: any) => any;
35
39
  export const breezsdk_deleteContact: (a: number, b: number, c: number) => any;
36
40
  export const breezsdk_deleteLightningAddress: (a: number) => any;
37
41
  export const breezsdk_disconnect: (a: number) => any;
@@ -50,6 +54,7 @@ export const breezsdk_listContacts: (a: number, b: any) => any;
50
54
  export const breezsdk_listFiatCurrencies: (a: number) => any;
51
55
  export const breezsdk_listFiatRates: (a: number) => any;
52
56
  export const breezsdk_listPayments: (a: number, b: any) => any;
57
+ export const breezsdk_listTokenAllowances: (a: number, b: any) => any;
53
58
  export const breezsdk_listUnclaimedDeposits: (a: number, b: any) => any;
54
59
  export const breezsdk_listWebhooks: (a: number) => any;
55
60
  export const breezsdk_lnurlAuth: (a: number, b: any) => any;
@@ -59,23 +64,27 @@ export const breezsdk_optimizeLeaves: (a: number, b: any) => any;
59
64
  export const breezsdk_parse: (a: number, b: number, c: number) => any;
60
65
  export const breezsdk_prepareLnurlPay: (a: number, b: any) => any;
61
66
  export const breezsdk_preparePaymentLink: (a: number, b: any) => any;
67
+ export const breezsdk_preparePullPayment: (a: number, b: any) => any;
68
+ export const breezsdk_prepareRecoverFunds: (a: number, b: any) => any;
62
69
  export const breezsdk_prepareSendBatch: (a: number, b: any) => any;
63
70
  export const breezsdk_prepareSendPayment: (a: number, b: any) => any;
64
- export const breezsdk_prepareUnilateralExit: (a: number, b: any) => any;
65
71
  export const breezsdk_publishSignedLnurlPayPackage: (a: number, b: any) => any;
72
+ export const breezsdk_publishSignedPullPackage: (a: number, b: any) => any;
66
73
  export const breezsdk_publishSignedTransferPackage: (a: number, b: any) => any;
74
+ export const breezsdk_pullPayment: (a: number, b: any) => any;
67
75
  export const breezsdk_receivePayment: (a: number, b: any) => any;
68
76
  export const breezsdk_recommendedFees: (a: number) => any;
77
+ export const breezsdk_recoverFunds: (a: number, b: any, c: number) => any;
69
78
  export const breezsdk_refundDeposit: (a: number, b: any) => any;
70
79
  export const breezsdk_refundPendingConversions: (a: number) => any;
71
80
  export const breezsdk_registerLightningAddress: (a: number, b: any) => any;
72
81
  export const breezsdk_registerWebhook: (a: number, b: any) => any;
73
82
  export const breezsdk_removeEventListener: (a: number, b: number, c: number) => any;
83
+ export const breezsdk_revokeTokenAllowance: (a: number, b: any) => any;
74
84
  export const breezsdk_sendBatch: (a: number, b: any) => any;
75
85
  export const breezsdk_sendPayment: (a: number, b: any) => any;
76
86
  export const breezsdk_signMessage: (a: number, b: any) => any;
77
87
  export const breezsdk_syncWallet: (a: number, b: any) => any;
78
- export const breezsdk_unilateralExit: (a: number, b: any, c: any) => any;
79
88
  export const breezsdk_unregisterWebhook: (a: number, b: any) => any;
80
89
  export const breezsdk_updateContact: (a: number, b: any) => any;
81
90
  export const breezsdk_updateUserSettings: (a: number, b: any) => any;
@@ -110,6 +119,7 @@ export const externalsigningsignerhandle_signHashSchnorr: (a: number, b: number,
110
119
  export const externalsparksignerhandle_getIdentityPublicKey: (a: number) => any;
111
120
  export const externalsparksignerhandle_getPublicKeyForLeaf: (a: number, b: any) => any;
112
121
  export const externalsparksignerhandle_getStaticDepositPublicKey: (a: number, b: number) => any;
122
+ export const externalsparksignerhandle_isRemote: (a: number) => number;
113
123
  export const externalsparksignerhandle_prepareClaim: (a: number, b: any) => any;
114
124
  export const externalsparksignerhandle_prepareLightningReceive: (a: number, b: any) => any;
115
125
  export const externalsparksignerhandle_prepareStaticDeposit: (a: number, b: any) => any;
@@ -122,7 +132,9 @@ export const externalsparksignerhandle_signLeafRefundSpend: (a: number, b: any,
122
132
  export const externalsparksignerhandle_signMessage: (a: number, b: number, c: number) => any;
123
133
  export const externalsparksignerhandle_signSparkInvoice: (a: number, b: any) => any;
124
134
  export const externalsparksignerhandle_signStaticDepositRefund: (a: number, b: any) => any;
135
+ export const externalsparksignerhandle_signWatchtowerExitRecovery: (a: number, b: any) => any;
125
136
  export const externalsparksignerhandle_startStaticDepositRefund: (a: number, b: any) => any;
137
+ export const externalsparksignerhandle_startWatchtowerExitRecovery: (a: number, b: any) => any;
126
138
  export const getSparkStatus: () => any;
127
139
  export const initLogging: (a: any, b: number, c: number) => any;
128
140
  export const mysqlStorage: (a: any) => number;
@@ -185,21 +197,23 @@ export const intounderlyingsource_cancel: (a: number) => void;
185
197
  export const intounderlyingsource_pull: (a: number, b: any) => any;
186
198
  export const __wbg_externalbreezsignerhandle_free: (a: number, b: number) => void;
187
199
  export const __wbg_defaultsessionstore_free: (a: number, b: number) => void;
188
- export const __wbg_externalsigningsignerhandle_free: (a: number, b: number) => void;
189
200
  export const __wbg_externalsparksignerhandle_free: (a: number, b: number) => void;
201
+ export const __wbg_externalsigningsignerhandle_free: (a: number, b: number) => void;
190
202
  export const __wbg_signingonlyexternalsigners_free: (a: number, b: number) => void;
191
203
  export const signingonlyexternalsigners_breezSigner: (a: number) => number;
192
204
  export const signingonlyexternalsigners_sparkSigner: (a: number) => number;
193
- export const wasm_bindgen__convert__closures_____invoke__h0a7e586e9273e375: (a: number, b: number, c: any) => [number, number];
194
- export const wasm_bindgen__convert__closures_____invoke__h0a7e586e9273e375_4: (a: number, b: number, c: any) => [number, number];
195
- export const wasm_bindgen__convert__closures_____invoke__h0a7e586e9273e375_5: (a: number, b: number, c: any) => [number, number];
196
- export const wasm_bindgen__convert__closures_____invoke__h0a7e586e9273e375_6: (a: number, b: number, c: any) => [number, number];
197
- export const wasm_bindgen__convert__closures_____invoke__h0a7e586e9273e375_7: (a: number, b: number, c: any) => [number, number];
198
- export const wasm_bindgen__convert__closures_____invoke__h41057d61edf43a32: (a: number, b: number, c: any, d: any) => void;
199
- export const wasm_bindgen__convert__closures_____invoke__h3b28fbbc8b451a25: (a: number, b: number, c: any) => void;
200
- export const wasm_bindgen__convert__closures_____invoke__h3b28fbbc8b451a25_2: (a: number, b: number, c: any) => void;
201
- export const wasm_bindgen__convert__closures_____invoke__h3b28fbbc8b451a25_3: (a: number, b: number, c: any) => void;
202
- export const wasm_bindgen__convert__closures_____invoke__h484cd36e13f37bd7: (a: number, b: number) => void;
205
+ export const wasm_bindgen__convert__closures_____invoke__h6a948436da8c8a31: (a: number, b: number, c: any) => [number, number];
206
+ export const wasm_bindgen__convert__closures_____invoke__h6a948436da8c8a31_6: (a: number, b: number, c: any) => [number, number];
207
+ export const wasm_bindgen__convert__closures_____invoke__h6a948436da8c8a31_7: (a: number, b: number, c: any) => [number, number];
208
+ export const wasm_bindgen__convert__closures_____invoke__h6a948436da8c8a31_8: (a: number, b: number, c: any) => [number, number];
209
+ export const wasm_bindgen__convert__closures_____invoke__h6a948436da8c8a31_9: (a: number, b: number, c: any) => [number, number];
210
+ export const wasm_bindgen__convert__closures_____invoke__h118cfc65b353d9fd: (a: number, b: number, c: any, d: any) => void;
211
+ export const wasm_bindgen__convert__closures_____invoke__h06397208c6d2b4ed: (a: number, b: number, c: any) => void;
212
+ export const wasm_bindgen__convert__closures_____invoke__h06397208c6d2b4ed_2: (a: number, b: number, c: any) => void;
213
+ export const wasm_bindgen__convert__closures_____invoke__h06397208c6d2b4ed_3: (a: number, b: number, c: any) => void;
214
+ export const wasm_bindgen__convert__closures_____invoke__h06397208c6d2b4ed_4: (a: number, b: number, c: any) => void;
215
+ export const wasm_bindgen__convert__closures_____invoke__h06397208c6d2b4ed_5: (a: number, b: number, c: any) => void;
216
+ export const wasm_bindgen__convert__closures_____invoke__h02848814aa3937de: (a: number, b: number) => void;
203
217
  export const __wbindgen_malloc: (a: number, b: number) => number;
204
218
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
205
219
  export const __wbindgen_free: (a: number, b: number, c: number) => void;
@@ -3,6 +3,14 @@
3
3
  * This provides an ES6 interface to IndexedDB storage for web browsers
4
4
  */
5
5
 
6
+ // JSON has no BigInt, so u128 amounts are stored as decimal strings, which the
7
+ // SDK reads back as BigInts.
8
+ function toJson(value) {
9
+ return JSON.stringify(value, (_key, v) =>
10
+ typeof v === "bigint" ? v.toString() : v
11
+ );
12
+ }
13
+
6
14
  class MigrationManager {
7
15
  constructor(db, StorageError, logger = null) {
8
16
  this.db = db;
@@ -151,7 +159,7 @@ class MigrationManager {
151
159
  // sync_revision: tracks the last committed revision (from server-acknowledged
152
160
  // or server-received records). Does NOT include pending outgoing revisions.
153
161
  if (!db.objectStoreNames.contains("sync_revision")) {
154
- const syncRevisionStore = db.createObjectStore("sync_revision", {
162
+ db.createObjectStore("sync_revision", {
155
163
  keyPath: "id",
156
164
  });
157
165
  transaction
@@ -279,7 +287,7 @@ class MigrationManager {
279
287
  // Add default txType to token payments
280
288
  if (details && details.type === "token" && !details.txType) {
281
289
  details.txType = "transfer";
282
- payment.details = JSON.stringify(details);
290
+ payment.details = toJson(details);
283
291
  paymentStore.put(payment);
284
292
  }
285
293
  });
@@ -300,7 +308,7 @@ class MigrationManager {
300
308
  syncInfo.last_synced_final_token_payment_id = null;
301
309
  settingsStore.put({
302
310
  key: "sync_offset",
303
- value: JSON.stringify(syncInfo),
311
+ value: toJson(syncInfo),
304
312
  });
305
313
  } catch (e) {
306
314
  // If parsing fails, just continue
@@ -379,7 +387,7 @@ class MigrationManager {
379
387
  expiryTime: 0,
380
388
  status: htlcStatus,
381
389
  };
382
- payment.details = JSON.stringify(details);
390
+ payment.details = toJson(details);
383
391
  paymentStore.put(payment);
384
392
  }
385
393
  });
@@ -399,7 +407,7 @@ class MigrationManager {
399
407
  syncInfo.offset = 0;
400
408
  settingsStore.put({
401
409
  key: "sync_offset",
402
- value: JSON.stringify(syncInfo),
410
+ value: toJson(syncInfo),
403
411
  });
404
412
  } catch (e) {
405
413
  // If parsing fails, just continue
@@ -535,7 +543,7 @@ class MigrationManager {
535
543
  syncInfo.offset = 0;
536
544
  settingsStore.put({
537
545
  key: "sync_offset",
538
- value: JSON.stringify(syncInfo),
546
+ value: toJson(syncInfo),
539
547
  });
540
548
  } catch (e) {
541
549
  // If parsing fails, just continue
@@ -562,7 +570,7 @@ class MigrationManager {
562
570
  : record.conversionInfo;
563
571
  if (!ci.type) {
564
572
  ci.type = "amm";
565
- record.conversionInfo = JSON.stringify(ci);
573
+ record.conversionInfo = toJson(ci);
566
574
  cursor.update(record);
567
575
  }
568
576
  } catch (e) {
@@ -624,6 +632,27 @@ class MigrationManager {
624
632
  }
625
633
  },
626
634
  },
635
+ {
636
+ // Leaf recoveries. What the chain service reported about leaves with
637
+ // funds to recover on-chain, and the recoveries the operators co-signed.
638
+ // Every block height is in chain_transactions, keyed by txid.
639
+ name: "Create leaf recovery stores",
640
+ upgrade: (db) => {
641
+ const keyPaths = {
642
+ leaf_recoveries: "leafId",
643
+ chain_transactions: "txid",
644
+ watchtower_exit_outputs: "leafId",
645
+ watchtower_exit_recoveries: ["leafId", "txid"],
646
+ watchtower_exit_spends: "leafId",
647
+ unilateral_exit_sweeps: "leafId",
648
+ };
649
+ for (const [name, keyPath] of Object.entries(keyPaths)) {
650
+ if (!db.objectStoreNames.contains(name)) {
651
+ db.createObjectStore(name, { keyPath });
652
+ }
653
+ }
654
+ },
655
+ },
627
656
  ];
628
657
  }
629
658
  }
@@ -652,7 +681,7 @@ class IndexedDBStorage {
652
681
  // so existing databases depend on indices never shifting. Never insert,
653
682
  // reorder, or delete a migration — only append. dbVersion MUST equal the
654
683
  // number of migrations (enforced by the guard in initialize()).
655
- this.dbVersion = 22; // Current schema version (= migration count)
684
+ this.dbVersion = 23; // Current schema version (= migration count)
656
685
  }
657
686
 
658
687
  /**
@@ -976,7 +1005,7 @@ class IndexedDBStorage {
976
1005
  cursorRequest.onerror = () => {
977
1006
  reject(
978
1007
  new StorageError(
979
- `Failed to list payments (request: ${JSON.stringify(request)}: ${cursorRequest.error?.message || "Unknown error"
1008
+ `Failed to list payments (request: ${toJson(request)}: ${cursorRequest.error?.message || "Unknown error"
980
1009
  }`,
981
1010
  cursorRequest.error
982
1011
  )
@@ -1393,14 +1422,14 @@ class IndexedDBStorage {
1393
1422
  paymentId,
1394
1423
  parentPaymentId: metadata.parentPaymentId ?? existing.parentPaymentId ?? null,
1395
1424
  lnurlPayInfo: metadata.lnurlPayInfo
1396
- ? JSON.stringify(metadata.lnurlPayInfo)
1425
+ ? toJson(metadata.lnurlPayInfo)
1397
1426
  : existing.lnurlPayInfo ?? null,
1398
1427
  lnurlWithdrawInfo: metadata.lnurlWithdrawInfo
1399
- ? JSON.stringify(metadata.lnurlWithdrawInfo)
1428
+ ? toJson(metadata.lnurlWithdrawInfo)
1400
1429
  : existing.lnurlWithdrawInfo ?? null,
1401
1430
  lnurlDescription: metadata.lnurlDescription ?? existing.lnurlDescription ?? null,
1402
1431
  conversionInfo: metadata.conversionInfo
1403
- ? JSON.stringify(metadata.conversionInfo)
1432
+ ? toJson(metadata.conversionInfo)
1404
1433
  : existing.conversionInfo ?? null,
1405
1434
  conversionStatus: metadata.conversionStatus ?? existing.conversionStatus ?? null,
1406
1435
  };
@@ -1575,25 +1604,25 @@ class IndexedDBStorage {
1575
1604
  return;
1576
1605
  }
1577
1606
 
1578
- let updatedDeposit = { ...existingDeposit };
1607
+ const updatedDeposit = { ...existingDeposit };
1579
1608
 
1580
1609
  if (payload.type === "claimError") {
1581
- updatedDeposit.claimError = JSON.stringify(payload.error);
1610
+ updatedDeposit.claimError = toJson(payload.error);
1582
1611
  } else if (payload.type === "refund") {
1583
1612
  updatedDeposit.refundTx = payload.refundTx;
1584
1613
  updatedDeposit.refundTxId = payload.refundTxid;
1585
- updatedDeposit.refundState = JSON.stringify(payload.state);
1614
+ updatedDeposit.refundState = toJson(payload.state);
1586
1615
  updatedDeposit.claimError = null;
1587
1616
  } else if (payload.type === "instantClaim") {
1588
- updatedDeposit.instantClaimStatus = JSON.stringify(payload.status);
1617
+ updatedDeposit.instantClaimStatus = toJson(payload.status);
1589
1618
  } else if (payload.type === "refundBroadcastState") {
1590
1619
  // Only while this is still the stored refund.
1591
1620
  if (existingDeposit.refundTxId === payload.refundTxid) {
1592
- updatedDeposit.refundState = JSON.stringify(payload.state);
1621
+ updatedDeposit.refundState = toJson(payload.state);
1593
1622
  }
1594
1623
  } else if (payload.type === "maxClaimFee") {
1595
1624
  updatedDeposit.maxClaimFee = payload.maxFee
1596
- ? JSON.stringify(payload.maxFee)
1625
+ ? toJson(payload.maxFee)
1597
1626
  : null;
1598
1627
  } else {
1599
1628
  reject(new StorageError(`Unknown payload type: ${payload.type}`));
@@ -1750,6 +1779,208 @@ class IndexedDBStorage {
1750
1779
  });
1751
1780
  }
1752
1781
 
1782
+ async listLeafRecoveries() {
1783
+ if (!this.db) {
1784
+ throw new StorageError("Database not initialized");
1785
+ }
1786
+
1787
+ return new Promise((resolve, reject) => {
1788
+ const storeNames = [
1789
+ "leaf_recoveries",
1790
+ "chain_transactions",
1791
+ "watchtower_exit_outputs",
1792
+ "watchtower_exit_recoveries",
1793
+ "watchtower_exit_spends",
1794
+ "unilateral_exit_sweeps",
1795
+ ];
1796
+ const transaction = this.db.transaction(storeNames, "readonly");
1797
+ const entries = {};
1798
+ // getAll returns the entries of a store in key order: the leaf recoveries by
1799
+ // leaf id, and the recoveries of a leaf by txid.
1800
+ for (const name of storeNames) {
1801
+ const request = transaction.objectStore(name).getAll();
1802
+ request.onsuccess = () => {
1803
+ entries[name] = request.result;
1804
+ };
1805
+ }
1806
+
1807
+ transaction.oncomplete = () => {
1808
+ const blockHeights = new Map(
1809
+ entries.chain_transactions.map((entry) => [
1810
+ entry.txid,
1811
+ entry.blockHeight,
1812
+ ])
1813
+ );
1814
+ const chainTransaction = (entry) =>
1815
+ entry && blockHeights.has(entry.txid)
1816
+ ? { txid: entry.txid, blockHeight: blockHeights.get(entry.txid) }
1817
+ : null;
1818
+ const byLeafId = (name) =>
1819
+ new Map(entries[name].map((entry) => [entry.leafId, entry]));
1820
+ const outputs = byLeafId("watchtower_exit_outputs");
1821
+ const spends = byLeafId("watchtower_exit_spends");
1822
+ const sweeps = byLeafId("unilateral_exit_sweeps");
1823
+ const recoveries = new Map();
1824
+ for (const entry of entries.watchtower_exit_recoveries) {
1825
+ if (!recoveries.has(entry.leafId)) {
1826
+ recoveries.set(entry.leafId, []);
1827
+ }
1828
+ recoveries.get(entry.leafId).push({
1829
+ txid: entry.txid,
1830
+ transactionHex: entry.transactionHex,
1831
+ outputAmountSats: entry.outputAmountSats,
1832
+ });
1833
+ }
1834
+
1835
+ resolve(
1836
+ entries.leaf_recoveries.map(({ leafId, chainCheckedAt }) => {
1837
+ const output = outputs.get(leafId);
1838
+ return {
1839
+ leafId,
1840
+ chainCheckedAt,
1841
+ watchtowerExitOutput:
1842
+ output && blockHeights.has(output.txid)
1843
+ ? {
1844
+ txid: output.txid,
1845
+ vout: output.vout,
1846
+ amountSats: output.amountSats,
1847
+ scriptPubkey: output.scriptPubkey,
1848
+ blockHeight: blockHeights.get(output.txid),
1849
+ }
1850
+ : null,
1851
+ watchtowerExitRecoveries: recoveries.get(leafId) ?? [],
1852
+ watchtowerExitSpend: chainTransaction(spends.get(leafId)),
1853
+ unilateralExitSweep: chainTransaction(sweeps.get(leafId)),
1854
+ };
1855
+ })
1856
+ );
1857
+ };
1858
+ transaction.onabort = () =>
1859
+ reject(
1860
+ new StorageError(
1861
+ `Failed to list leaf recoveries: ${transaction.error?.message || "Unknown error"}`,
1862
+ transaction.error
1863
+ )
1864
+ );
1865
+ });
1866
+ }
1867
+
1868
+ async updateLeafRecovery(update) {
1869
+ if (!this.db) {
1870
+ throw new StorageError("Database not initialized");
1871
+ }
1872
+
1873
+ return new Promise((resolve, reject) => {
1874
+ const {
1875
+ leafId,
1876
+ chainCheckedAt,
1877
+ watchtowerExitOutput: output,
1878
+ watchtowerExitRecovery: recovery,
1879
+ watchtowerExitSpend: spend,
1880
+ unilateralExitSweep: sweep,
1881
+ } = update;
1882
+ const storeNames = ["leaf_recoveries"];
1883
+ if (output != null) {
1884
+ storeNames.push("watchtower_exit_outputs");
1885
+ }
1886
+ if (output != null || recovery != null) {
1887
+ storeNames.push("watchtower_exit_recoveries");
1888
+ }
1889
+ if (output != null || spend != null) {
1890
+ storeNames.push("watchtower_exit_spends");
1891
+ }
1892
+ if (sweep != null) {
1893
+ storeNames.push("unilateral_exit_sweeps");
1894
+ }
1895
+ if (output != null || spend != null || sweep != null) {
1896
+ storeNames.push("chain_transactions");
1897
+ }
1898
+ const transaction = this.db.transaction(storeNames, "readwrite");
1899
+ const store = (name) => transaction.objectStore(name);
1900
+
1901
+ transaction.oncomplete = () => resolve();
1902
+ transaction.onabort = () =>
1903
+ reject(
1904
+ new StorageError(
1905
+ `Failed to update leaf recovery '${leafId}': ${transaction.error?.message || "Unknown error"}`,
1906
+ transaction.error
1907
+ )
1908
+ );
1909
+
1910
+ const leafRecoveryRequest = store("leaf_recoveries").get(leafId);
1911
+ leafRecoveryRequest.onsuccess = () => {
1912
+ const stored = leafRecoveryRequest.result?.chainCheckedAt ?? null;
1913
+ const later =
1914
+ chainCheckedAt != null && (stored == null || chainCheckedAt > stored)
1915
+ ? chainCheckedAt
1916
+ : stored;
1917
+ store("leaf_recoveries").put({ leafId, chainCheckedAt: later });
1918
+ };
1919
+
1920
+ const setChainTransaction = ({ txid, blockHeight }) =>
1921
+ store("chain_transactions").put({ txid, blockHeight });
1922
+
1923
+ const storeRecoverySpendSweep = () => {
1924
+ if (recovery != null) {
1925
+ const recoveries = store("watchtower_exit_recoveries");
1926
+ const keyRequest = recoveries.getKey([leafId, recovery.txid]);
1927
+ keyRequest.onsuccess = () => {
1928
+ if (keyRequest.result === undefined) {
1929
+ recoveries.put({
1930
+ leafId,
1931
+ txid: recovery.txid,
1932
+ transactionHex: recovery.transactionHex,
1933
+ outputAmountSats: recovery.outputAmountSats,
1934
+ });
1935
+ }
1936
+ };
1937
+ }
1938
+ if (spend != null) {
1939
+ setChainTransaction(spend);
1940
+ store("watchtower_exit_spends").put({ leafId, txid: spend.txid });
1941
+ }
1942
+ if (sweep != null) {
1943
+ setChainTransaction(sweep);
1944
+ store("unilateral_exit_sweeps").put({ leafId, txid: sweep.txid });
1945
+ }
1946
+ };
1947
+
1948
+ if (output == null) {
1949
+ storeRecoverySpendSweep();
1950
+ return;
1951
+ }
1952
+
1953
+ const outputs = store("watchtower_exit_outputs");
1954
+ const outputRequest = outputs.get(leafId);
1955
+ outputRequest.onsuccess = () => {
1956
+ const stored = outputRequest.result;
1957
+ if (
1958
+ stored &&
1959
+ (stored.txid !== output.txid || stored.vout !== output.vout)
1960
+ ) {
1961
+ // IndexedDB orders an array after every string, so the range holds
1962
+ // every recovery of the leaf.
1963
+ store("watchtower_exit_recoveries").delete(
1964
+ IDBKeyRange.bound([leafId], [leafId, []])
1965
+ );
1966
+ store("watchtower_exit_spends").delete(leafId);
1967
+ }
1968
+ setChainTransaction(output);
1969
+ outputs.put({
1970
+ leafId,
1971
+ txid: output.txid,
1972
+ vout: output.vout,
1973
+ amountSats: output.amountSats,
1974
+ scriptPubkey: output.scriptPubkey,
1975
+ });
1976
+ // The browser runs a transaction's requests in the order this method
1977
+ // makes them, so it stores the recovery and the spend of this update
1978
+ // after it has deleted those of the output replaced.
1979
+ storeRecoverySpendSweep();
1980
+ };
1981
+ });
1982
+ }
1983
+
1753
1984
  async setLnurlMetadata(metadata) {
1754
1985
  if (!this.db) {
1755
1986
  throw new StorageError("Database not initialized");
@@ -2489,8 +2720,8 @@ class IndexedDBStorage {
2489
2720
  // Ensure details and method are serialized properly
2490
2721
  return {
2491
2722
  ...payment,
2492
- details: payment.details ? JSON.stringify(payment.details) : null,
2493
- method: payment.method ? JSON.stringify(payment.method) : null,
2723
+ details: payment.details ? toJson(payment.details) : null,
2724
+ method: payment.method ? toJson(payment.method) : null,
2494
2725
  };
2495
2726
  }
2496
2727
 
@@ -21,7 +21,7 @@ const RESERVATION_TIMEOUT_MS = 300 * 1000; // 5 minutes
21
21
  /** Spent-leaf markers older than this (relative to a refresh) are pruned. */
22
22
  const SPENT_MARKER_CLEANUP_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
23
23
 
24
- const DB_VERSION = 5;
24
+ const DB_VERSION = 6;
25
25
 
26
26
  const STORE_LEAVES = "leaves";
27
27
  const STORE_ANCESTORS = "ancestors";
@@ -111,6 +111,7 @@ class WebTreeStore {
111
111
  leaves.createIndex("chain_complete", "chain_complete", {
112
112
  unique: false,
113
113
  });
114
+ leaves.createIndex("status", "status", { unique: false });
114
115
  }
115
116
 
116
117
  // Intermediate exit-chain nodes, kept separate from the leaf pool and
@@ -134,8 +135,10 @@ class WebTreeStore {
134
135
  // An existing wallet gets the flag derived from the ancestor rows it
135
136
  // already holds, so it does not refetch every chain. The index is
136
137
  // created here and populated by the rewrite below, since createIndex
137
- // cannot run from an async callback.
138
- if (event.oldVersion > 0 && event.oldVersion < DB_VERSION) {
138
+ // cannot run from an async callback. The flag has had its current
139
+ // meaning since v5, so a database already at v5 or later keeps the
140
+ // flags it holds.
141
+ if (event.oldVersion > 0 && event.oldVersion < 5) {
139
142
  const tx = event.target.transaction;
140
143
  const leaves = tx.objectStore(STORE_LEAVES);
141
144
  if (!leaves.indexNames.contains("chain_complete")) {
@@ -168,6 +171,14 @@ class WebTreeStore {
168
171
  };
169
172
  }
170
173
 
174
+ // Every leaf row already carries its status, and IndexedDB builds a new
175
+ // index from the rows a store holds, so this needs no rewrite.
176
+ if (event.oldVersion > 0 && event.oldVersion < 6) {
177
+ event.target.transaction
178
+ .objectStore(STORE_LEAVES)
179
+ .createIndex("status", "status", { unique: false });
180
+ }
181
+
171
182
  if (!db.objectStoreNames.contains(STORE_RESERVATIONS)) {
172
183
  db.createObjectStore(STORE_RESERVATIONS, { keyPath: "id" });
173
184
  }
@@ -483,6 +494,31 @@ class WebTreeStore {
483
494
  }
484
495
  }
485
496
 
497
+ async getLeavesWithStatus(statuses) {
498
+ try {
499
+ const distinct = [...new Set(statuses)];
500
+ if (distinct.length === 0) return [];
501
+ return await this._txRun(
502
+ [STORE_LEAVES],
503
+ "readonly",
504
+ distinct.map((status) => ({
505
+ name: status,
506
+ store: STORE_LEAVES,
507
+ index: "status",
508
+ key: status,
509
+ all: true,
510
+ })),
511
+ (res) => distinct.flatMap((status) => res[status].map((row) => row.data))
512
+ );
513
+ } catch (error) {
514
+ if (error instanceof TreeStoreError) throw error;
515
+ throw new TreeStoreError(
516
+ `Failed to get leaves with status: ${error.message}`,
517
+ error
518
+ );
519
+ }
520
+ }
521
+
486
522
  async getAvailableBalance() {
487
523
  try {
488
524
  return await this._txRun(
@@ -940,9 +976,6 @@ class WebTreeStore {
940
976
  }
941
977
  }
942
978
 
943
-
944
- const leafNodes = change.concat(reserved);
945
-
946
979
  // Change leaves go back to the available pool.
947
980
  for (const p of change) {
948
981
  const row = this._leafRow(p, false, leafMap.get(p.id));