@breeztech/breez-sdk-spark 0.27.0-dev2 → 0.27.1

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;
package/nodejs/index.js CHANGED
@@ -35,6 +35,9 @@ try {
35
35
  if (error.code !== 'MODULE_NOT_FOUND') {
36
36
  console.warn('Breez SDK: Failed to load PostgreSQL storage:', error.message);
37
37
  }
38
+ // The WASM bridge builds every PostgreSQL store from a pool, so a caller that asks
39
+ // for PostgreSQL storage gets this load error.
40
+ global.createPostgresPool = () => { throw error; };
38
41
  }
39
42
 
40
43
  // Automatically import and set up the PostgreSQL tree store for Node.js
@@ -80,6 +83,9 @@ try {
80
83
  if (error.code !== 'MODULE_NOT_FOUND') {
81
84
  console.warn('Breez SDK: Failed to load MySQL storage:', error.message);
82
85
  }
86
+ // The WASM bridge builds every MySQL store from a pool, so a caller that asks
87
+ // for MySQL storage gets this load error.
88
+ global.createMysqlPool = () => { throw error; };
83
89
  }
84
90
 
85
91
  // Automatically import and set up the MySQL tree store for Node.js
@@ -16,10 +16,12 @@ try {
16
16
  try {
17
17
  mysql = require("mysql2/promise");
18
18
  } catch (fallbackError) {
19
- throw new Error(
19
+ const loadError = new Error(
20
20
  `mysql2 not found. Please install it in your project: npm install mysql2@^3.11.0\n` +
21
21
  `Original error: ${error.message}\nFallback error: ${fallbackError.message}`
22
22
  );
23
+ loadError.code = fallbackError.code;
24
+ throw loadError;
23
25
  }
24
26
  }
25
27
 
@@ -30,16 +30,26 @@ try {
30
30
  try {
31
31
  mysql = require("mysql2/promise");
32
32
  } catch (fallbackError) {
33
- throw new Error(
33
+ const loadError = new Error(
34
34
  `mysql2 not found. Please install it in your project: npm install mysql2@^3.11.0\n` +
35
35
  `Original error: ${error.message}\nFallback error: ${fallbackError.message}`
36
36
  );
37
+ loadError.code = fallbackError.code;
38
+ throw loadError;
37
39
  }
38
40
  }
39
41
 
40
42
  const { StorageError } = require("./errors.cjs");
41
43
  const { MysqlMigrationManager } = require("./migrations.cjs");
42
44
 
45
+ // JSON has no BigInt, so u128 amounts are stored as decimal strings, which the
46
+ // SDK reads back as BigInts.
47
+ function toJson(value) {
48
+ return JSON.stringify(value, (_key, v) =>
49
+ typeof v === "bigint" ? v.toString() : v
50
+ );
51
+ }
52
+
43
53
  const PAYMENT_UPDATE_LOCK_TIMEOUT_SECS = 10;
44
54
 
45
55
  /**
@@ -120,6 +130,58 @@ function crossChainSwapFromRow(row) {
120
130
  };
121
131
  }
122
132
 
133
+ /**
134
+ * A row holds a leaf recovery and one of its watchtower exit recoveries, so
135
+ * a leaf recovery with several of them spans several rows.
136
+ */
137
+ function leafRecoveriesFromRows(rows) {
138
+ const leafRecoveries = [];
139
+ for (const row of rows) {
140
+ let leafRecovery = leafRecoveries[leafRecoveries.length - 1];
141
+ if (leafRecovery?.leafId !== row.leaf_id) {
142
+ leafRecovery = {
143
+ leafId: row.leaf_id,
144
+ chainCheckedAt:
145
+ row.chain_checked_at != null ? BigInt(row.chain_checked_at) : null,
146
+ watchtowerExitOutput:
147
+ row.output_block_height != null
148
+ ? {
149
+ txid: row.output_txid,
150
+ vout: Number(row.output_vout),
151
+ amountSats: BigInt(row.output_amount_sats),
152
+ scriptPubkey: row.output_script_pubkey,
153
+ blockHeight: Number(row.output_block_height),
154
+ }
155
+ : null,
156
+ watchtowerExitRecoveries: [],
157
+ watchtowerExitSpend:
158
+ row.spend_block_height != null
159
+ ? {
160
+ txid: row.spend_txid,
161
+ blockHeight: Number(row.spend_block_height),
162
+ }
163
+ : null,
164
+ unilateralExitSweep:
165
+ row.sweep_block_height != null
166
+ ? {
167
+ txid: row.sweep_txid,
168
+ blockHeight: Number(row.sweep_block_height),
169
+ }
170
+ : null,
171
+ };
172
+ leafRecoveries.push(leafRecovery);
173
+ }
174
+ if (row.recovery_txid != null) {
175
+ leafRecovery.watchtowerExitRecoveries.push({
176
+ txid: row.recovery_txid,
177
+ transactionHex: row.recovery_transaction_hex,
178
+ outputAmountSats: BigInt(row.recovery_output_amount_sats),
179
+ });
180
+ }
181
+ }
182
+ return leafRecoveries;
183
+ }
184
+
123
185
  class MysqlStorage {
124
186
  /**
125
187
  * @param {import('mysql2/promise').Pool} pool - Connection pool (may be shared with other tenants).
@@ -403,7 +465,7 @@ class MysqlStorage {
403
465
  } catch (error) {
404
466
  if (error instanceof StorageError) throw error;
405
467
  throw new StorageError(
406
- `Failed to list payments (request: ${JSON.stringify(request)}): ${error.message}`,
468
+ `Failed to list payments (request: ${toJson(request)}): ${error.message}`,
407
469
  error
408
470
  );
409
471
  }
@@ -528,7 +590,7 @@ class MysqlStorage {
528
590
  payment.amount.toString(),
529
591
  payment.fees.toString(),
530
592
  payment.timestamp,
531
- payment.method ? JSON.stringify(payment.method) : null,
593
+ payment.method ? toJson(payment.method) : null,
532
594
  withdrawTxId,
533
595
  spark,
534
596
  ]
@@ -560,10 +622,10 @@ class MysqlStorage {
560
622
  this.identity,
561
623
  payment.id,
562
624
  payment.details.invoiceDetails
563
- ? JSON.stringify(payment.details.invoiceDetails)
625
+ ? toJson(payment.details.invoiceDetails)
564
626
  : null,
565
627
  payment.details.htlcDetails
566
- ? JSON.stringify(payment.details.htlcDetails)
628
+ ? toJson(payment.details.htlcDetails)
567
629
  : null,
568
630
  ]
569
631
  );
@@ -609,11 +671,11 @@ class MysqlStorage {
609
671
  [
610
672
  this.identity,
611
673
  payment.id,
612
- JSON.stringify(payment.details.metadata),
674
+ toJson(payment.details.metadata),
613
675
  payment.details.txHash,
614
676
  payment.details.txType,
615
677
  payment.details.invoiceDetails
616
- ? JSON.stringify(payment.details.invoiceDetails)
678
+ ? toJson(payment.details.invoiceDetails)
617
679
  : null,
618
680
  ]
619
681
  );
@@ -746,14 +808,14 @@ class MysqlStorage {
746
808
  paymentId,
747
809
  metadata.parentPaymentId,
748
810
  metadata.lnurlPayInfo
749
- ? JSON.stringify(metadata.lnurlPayInfo)
811
+ ? toJson(metadata.lnurlPayInfo)
750
812
  : null,
751
813
  metadata.lnurlWithdrawInfo
752
- ? JSON.stringify(metadata.lnurlWithdrawInfo)
814
+ ? toJson(metadata.lnurlWithdrawInfo)
753
815
  : null,
754
816
  metadata.lnurlDescription,
755
817
  metadata.conversionInfo
756
- ? JSON.stringify(metadata.conversionInfo)
818
+ ? toJson(metadata.conversionInfo)
757
819
  : null,
758
820
  metadata.conversionStatus ?? null,
759
821
  ]
@@ -833,7 +895,7 @@ class MysqlStorage {
833
895
  `UPDATE brz_unclaimed_deposits
834
896
  SET claim_error = ?
835
897
  WHERE user_id = ? AND txid = ? AND vout = ?`,
836
- [JSON.stringify(payload.error), this.identity, txid, vout]
898
+ [toJson(payload.error), this.identity, txid, vout]
837
899
  );
838
900
  } else if (payload.type === "refund") {
839
901
  await this.pool.query(
@@ -843,7 +905,7 @@ class MysqlStorage {
843
905
  [
844
906
  payload.refundTx,
845
907
  payload.refundTxid,
846
- JSON.stringify(payload.state),
908
+ toJson(payload.state),
847
909
  this.identity,
848
910
  txid,
849
911
  vout,
@@ -854,7 +916,7 @@ class MysqlStorage {
854
916
  `UPDATE brz_unclaimed_deposits
855
917
  SET instant_claim_status = ?
856
918
  WHERE user_id = ? AND txid = ? AND vout = ?`,
857
- [JSON.stringify(payload.status), this.identity, txid, vout]
919
+ [toJson(payload.status), this.identity, txid, vout]
858
920
  );
859
921
  } else if (payload.type === "refundBroadcastState") {
860
922
  await this.pool.query(
@@ -862,7 +924,7 @@ class MysqlStorage {
862
924
  SET refund_state = ?
863
925
  WHERE user_id = ? AND txid = ? AND vout = ? AND refund_tx_id = ?`,
864
926
  [
865
- JSON.stringify(payload.state),
927
+ toJson(payload.state),
866
928
  this.identity,
867
929
  txid,
868
930
  vout,
@@ -875,7 +937,7 @@ class MysqlStorage {
875
937
  SET max_claim_fee = ?
876
938
  WHERE user_id = ? AND txid = ? AND vout = ?`,
877
939
  [
878
- payload.maxFee ? JSON.stringify(payload.maxFee) : null,
940
+ payload.maxFee ? toJson(payload.maxFee) : null,
879
941
  this.identity,
880
942
  txid,
881
943
  vout,
@@ -946,6 +1008,163 @@ class MysqlStorage {
946
1008
  }
947
1009
  }
948
1010
 
1011
+ async listLeafRecoveries() {
1012
+ try {
1013
+ const [rows] = await this.pool.query(
1014
+ `SELECT l.leaf_id,
1015
+ l.chain_checked_at,
1016
+ o.txid AS output_txid,
1017
+ o.vout AS output_vout,
1018
+ o.amount_sats AS output_amount_sats,
1019
+ o.script_pubkey AS output_script_pubkey,
1020
+ ot.block_height AS output_block_height,
1021
+ s.txid AS spend_txid,
1022
+ st.block_height AS spend_block_height,
1023
+ w.txid AS sweep_txid,
1024
+ wt.block_height AS sweep_block_height,
1025
+ r.txid AS recovery_txid,
1026
+ r.transaction_hex AS recovery_transaction_hex,
1027
+ r.output_amount_sats AS recovery_output_amount_sats
1028
+ FROM brz_leaf_recoveries l
1029
+ LEFT JOIN brz_watchtower_exit_outputs o
1030
+ ON o.user_id = l.user_id AND o.leaf_id = l.leaf_id
1031
+ LEFT JOIN brz_chain_transactions ot
1032
+ ON ot.user_id = o.user_id AND ot.txid = o.txid
1033
+ LEFT JOIN brz_watchtower_exit_spends s
1034
+ ON s.user_id = l.user_id AND s.leaf_id = l.leaf_id
1035
+ LEFT JOIN brz_chain_transactions st
1036
+ ON st.user_id = s.user_id AND st.txid = s.txid
1037
+ LEFT JOIN brz_unilateral_exit_sweeps w
1038
+ ON w.user_id = l.user_id AND w.leaf_id = l.leaf_id
1039
+ LEFT JOIN brz_chain_transactions wt
1040
+ ON wt.user_id = w.user_id AND wt.txid = w.txid
1041
+ LEFT JOIN brz_watchtower_exit_recoveries r
1042
+ ON r.user_id = l.user_id AND r.leaf_id = l.leaf_id
1043
+ WHERE l.user_id = ?
1044
+ ORDER BY l.leaf_id, r.txid`,
1045
+ [this.identity]
1046
+ );
1047
+ return leafRecoveriesFromRows(rows);
1048
+ } catch (error) {
1049
+ throw new StorageError(
1050
+ `Failed to list leaf recoveries: ${error.message}`,
1051
+ error
1052
+ );
1053
+ }
1054
+ }
1055
+
1056
+ async updateLeafRecovery(update) {
1057
+ try {
1058
+ const leafId = update.leafId;
1059
+ await this._withTransaction(async (conn) => {
1060
+ const setChainTransaction = (txid, blockHeight) =>
1061
+ conn.query(
1062
+ `INSERT INTO brz_chain_transactions (user_id, txid, block_height)
1063
+ VALUES (?, ?, ?)
1064
+ ON DUPLICATE KEY UPDATE block_height = VALUES(block_height)`,
1065
+ [this.identity, txid, blockHeight]
1066
+ );
1067
+
1068
+ // MySQL locks the leaf's row, so writers of one leaf do not interleave.
1069
+ await conn.query(
1070
+ `INSERT INTO brz_leaf_recoveries (user_id, leaf_id) VALUES (?, ?)
1071
+ ON DUPLICATE KEY UPDATE leaf_id = leaf_id`,
1072
+ [this.identity, leafId]
1073
+ );
1074
+ if (update.chainCheckedAt != null) {
1075
+ // The parameter is a string, and MySQL would compare it with the
1076
+ // stored time as strings. CAST makes MySQL compare them as numbers.
1077
+ await conn.query(
1078
+ `UPDATE brz_leaf_recoveries
1079
+ SET chain_checked_at =
1080
+ GREATEST(COALESCE(chain_checked_at, 0), CAST(? AS SIGNED))
1081
+ WHERE user_id = ? AND leaf_id = ?`,
1082
+ [String(update.chainCheckedAt), this.identity, leafId]
1083
+ );
1084
+ }
1085
+ if (update.watchtowerExitOutput != null) {
1086
+ const output = update.watchtowerExitOutput;
1087
+ for (const table of [
1088
+ "brz_watchtower_exit_recoveries",
1089
+ "brz_watchtower_exit_spends",
1090
+ ]) {
1091
+ await conn.query(
1092
+ `DELETE FROM ${table}
1093
+ WHERE user_id = ? AND leaf_id = ? AND EXISTS (
1094
+ SELECT 1 FROM brz_watchtower_exit_outputs
1095
+ WHERE user_id = ? AND leaf_id = ? AND (txid <> ? OR vout <> ?))`,
1096
+ [
1097
+ this.identity,
1098
+ leafId,
1099
+ this.identity,
1100
+ leafId,
1101
+ output.txid,
1102
+ output.vout,
1103
+ ]
1104
+ );
1105
+ }
1106
+ await setChainTransaction(output.txid, output.blockHeight);
1107
+ await conn.query(
1108
+ `INSERT INTO brz_watchtower_exit_outputs
1109
+ (user_id, leaf_id, txid, vout, amount_sats, script_pubkey)
1110
+ VALUES (?, ?, ?, ?, ?, ?)
1111
+ ON DUPLICATE KEY UPDATE
1112
+ txid = VALUES(txid),
1113
+ vout = VALUES(vout),
1114
+ amount_sats = VALUES(amount_sats),
1115
+ script_pubkey = VALUES(script_pubkey)`,
1116
+ [
1117
+ this.identity,
1118
+ leafId,
1119
+ output.txid,
1120
+ output.vout,
1121
+ String(output.amountSats),
1122
+ output.scriptPubkey,
1123
+ ]
1124
+ );
1125
+ }
1126
+ if (update.watchtowerExitRecovery != null) {
1127
+ const recovery = update.watchtowerExitRecovery;
1128
+ await conn.query(
1129
+ `INSERT INTO brz_watchtower_exit_recoveries
1130
+ (user_id, leaf_id, txid, transaction_hex, output_amount_sats)
1131
+ VALUES (?, ?, ?, ?, ?)
1132
+ ON DUPLICATE KEY UPDATE txid = txid`,
1133
+ [
1134
+ this.identity,
1135
+ leafId,
1136
+ recovery.txid,
1137
+ recovery.transactionHex,
1138
+ String(recovery.outputAmountSats),
1139
+ ]
1140
+ );
1141
+ }
1142
+ for (const [table, chainTransaction] of [
1143
+ ["brz_watchtower_exit_spends", update.watchtowerExitSpend],
1144
+ ["brz_unilateral_exit_sweeps", update.unilateralExitSweep],
1145
+ ]) {
1146
+ if (chainTransaction == null) {
1147
+ continue;
1148
+ }
1149
+ await setChainTransaction(
1150
+ chainTransaction.txid,
1151
+ chainTransaction.blockHeight
1152
+ );
1153
+ await conn.query(
1154
+ `INSERT INTO ${table} (user_id, leaf_id, txid) VALUES (?, ?, ?)
1155
+ ON DUPLICATE KEY UPDATE txid = VALUES(txid)`,
1156
+ [this.identity, leafId, chainTransaction.txid]
1157
+ );
1158
+ }
1159
+ });
1160
+ } catch (error) {
1161
+ throw new StorageError(
1162
+ `Failed to update leaf recovery '${update.leafId}': ${error.message}`,
1163
+ error
1164
+ );
1165
+ }
1166
+ }
1167
+
949
1168
  async setLnurlMetadata(metadata) {
950
1169
  try {
951
1170
  await this._withTransaction(async (conn) => {
@@ -1268,7 +1487,7 @@ class MysqlStorage {
1268
1487
  record.id.dataId,
1269
1488
  record.schemaVersion,
1270
1489
  Math.floor(Date.now() / 1000),
1271
- JSON.stringify(record.updatedFields),
1490
+ toJson(record.updatedFields),
1272
1491
  revision.toString(),
1273
1492
  ]
1274
1493
  );
@@ -1297,7 +1516,6 @@ class MysqlStorage {
1297
1516
  if (this.logger && typeof this.logger.log === "function") {
1298
1517
  this.logger.log({ line: msg, level: "warn" });
1299
1518
  } else {
1300
- // eslint-disable-next-line no-console
1301
1519
  console.warn(`[MysqlStorage] ${msg}`);
1302
1520
  }
1303
1521
  }
@@ -1324,7 +1542,7 @@ class MysqlStorage {
1324
1542
  record.revision.toString(),
1325
1543
  record.schemaVersion,
1326
1544
  Math.floor(Date.now() / 1000),
1327
- JSON.stringify(record.data),
1545
+ toJson(record.data),
1328
1546
  ]
1329
1547
  );
1330
1548
 
@@ -1441,7 +1659,7 @@ class MysqlStorage {
1441
1659
  record.id.dataId,
1442
1660
  record.schemaVersion,
1443
1661
  Math.floor(Date.now() / 1000),
1444
- JSON.stringify(record.data),
1662
+ toJson(record.data),
1445
1663
  record.revision.toString(),
1446
1664
  ]
1447
1665
  );
@@ -1604,7 +1822,7 @@ class MysqlStorage {
1604
1822
  record.revision.toString(),
1605
1823
  record.schemaVersion,
1606
1824
  Math.floor(Date.now() / 1000),
1607
- JSON.stringify(record.data),
1825
+ toJson(record.data),
1608
1826
  ]
1609
1827
  );
1610
1828
 
@@ -149,7 +149,6 @@ class MysqlMigrationManager {
149
149
  if (this.logger && typeof this.logger.log === "function") {
150
150
  this.logger.log({ line: message, level });
151
151
  } else if (level === "error") {
152
- // eslint-disable-next-line no-console
153
152
  console.error(`[MysqlMigrationManager] ${message}`);
154
153
  }
155
154
  }
@@ -607,6 +606,55 @@ class MysqlMigrationManager {
607
606
  `ALTER TABLE brz_unclaimed_deposits ADD COLUMN max_claim_fee JSON NULL`,
608
607
  ],
609
608
  },
609
+ {
610
+ // Leaf recoveries. What the chain service reported about leaves with
611
+ // funds to recover on-chain, and the recoveries the operators co-signed.
612
+ // Every block height is in brz_chain_transactions, keyed by txid.
613
+ name: "Add leaf recovery tables",
614
+ sql: [
615
+ `CREATE TABLE IF NOT EXISTS brz_leaf_recoveries (
616
+ user_id VARBINARY(33) NOT NULL,
617
+ leaf_id VARCHAR(255) NOT NULL,
618
+ chain_checked_at BIGINT,
619
+ PRIMARY KEY (user_id, leaf_id)
620
+ )`,
621
+ `CREATE TABLE IF NOT EXISTS brz_chain_transactions (
622
+ user_id VARBINARY(33) NOT NULL,
623
+ txid VARCHAR(255) NOT NULL,
624
+ block_height BIGINT NOT NULL,
625
+ PRIMARY KEY (user_id, txid)
626
+ )`,
627
+ `CREATE TABLE IF NOT EXISTS brz_watchtower_exit_outputs (
628
+ user_id VARBINARY(33) NOT NULL,
629
+ leaf_id VARCHAR(255) NOT NULL,
630
+ txid VARCHAR(255) NOT NULL,
631
+ vout BIGINT NOT NULL,
632
+ amount_sats BIGINT NOT NULL,
633
+ script_pubkey TEXT NOT NULL,
634
+ PRIMARY KEY (user_id, leaf_id)
635
+ )`,
636
+ `CREATE TABLE IF NOT EXISTS brz_watchtower_exit_recoveries (
637
+ user_id VARBINARY(33) NOT NULL,
638
+ leaf_id VARCHAR(255) NOT NULL,
639
+ txid VARCHAR(255) NOT NULL,
640
+ transaction_hex TEXT NOT NULL,
641
+ output_amount_sats BIGINT NOT NULL,
642
+ PRIMARY KEY (user_id, leaf_id, txid)
643
+ )`,
644
+ `CREATE TABLE IF NOT EXISTS brz_watchtower_exit_spends (
645
+ user_id VARBINARY(33) NOT NULL,
646
+ leaf_id VARCHAR(255) NOT NULL,
647
+ txid VARCHAR(255) NOT NULL,
648
+ PRIMARY KEY (user_id, leaf_id)
649
+ )`,
650
+ `CREATE TABLE IF NOT EXISTS brz_unilateral_exit_sweeps (
651
+ user_id VARBINARY(33) NOT NULL,
652
+ leaf_id VARCHAR(255) NOT NULL,
653
+ txid VARCHAR(255) NOT NULL,
654
+ PRIMARY KEY (user_id, leaf_id)
655
+ )`,
656
+ ],
657
+ },
610
658
  ];
611
659
  }
612
660
  }
@@ -17,10 +17,12 @@ try {
17
17
  try {
18
18
  mysql = require("mysql2/promise");
19
19
  } catch (fallbackError) {
20
- throw new Error(
20
+ const loadError = new Error(
21
21
  `mysql2 not found. Please install it in your project: npm install mysql2@^3.11.0\n` +
22
22
  `Original error: ${error.message}\nFallback error: ${fallbackError.message}`
23
23
  );
24
+ loadError.code = fallbackError.code;
25
+ throw loadError;
24
26
  }
25
27
  }
26
28
 
@@ -50,12 +52,6 @@ function _identityLockName(prefix, identity) {
50
52
  return prefix + hash.digest("hex").slice(0, 16);
51
53
  }
52
54
 
53
- function parseJson(value) {
54
- if (value == null) return null;
55
- if (typeof value === "string") return JSON.parse(value);
56
- return value;
57
- }
58
-
59
55
  function toBool(value) {
60
56
  if (value == null) return null;
61
57
  if (typeof value === "boolean") return value;
@@ -22,10 +22,12 @@ try {
22
22
  try {
23
23
  mysql = require("mysql2/promise");
24
24
  } catch (fallbackError) {
25
- throw new Error(
25
+ const loadError = new Error(
26
26
  `mysql2 not found. Please install it in your project: npm install mysql2@^3.11.0\n` +
27
27
  `Original error: ${error.message}\nFallback error: ${fallbackError.message}`
28
28
  );
29
+ loadError.code = fallbackError.code;
30
+ throw loadError;
29
31
  }
30
32
  }
31
33
 
@@ -516,6 +518,29 @@ class MysqlTreeStore {
516
518
  }
517
519
  }
518
520
 
521
+ /**
522
+ * Return the leaves, reserved or not, whose status is one of `statuses`.
523
+ * @param {Array<string>} statuses
524
+ * @returns {Promise<Array<object>>}
525
+ */
526
+ async getLeavesWithStatus(statuses) {
527
+ try {
528
+ if (!statuses || statuses.length === 0) return [];
529
+ const [rows] = await this.pool.query(
530
+ `SELECT data FROM brz_tree_leaves
531
+ WHERE user_id = ? AND status IN (${buildPlaceholders(statuses.length)})`,
532
+ [this.identity, ...statuses]
533
+ );
534
+ return rows.map((row) => parseJson(row.data));
535
+ } catch (error) {
536
+ if (error instanceof TreeStoreError) throw error;
537
+ throw new TreeStoreError(
538
+ `Failed to get leaves with status: ${error.message}`,
539
+ error
540
+ );
541
+ }
542
+ }
543
+
519
544
  /**
520
545
  * Set leaves from a refresh operation.
521
546
  * @param {Array} leaves - Available leaves from operators