@breeztech/breez-sdk-spark 0.27.0-dev1 → 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
@@ -27,6 +27,14 @@ try {
27
27
  const { StorageError } = require("./errors.cjs");
28
28
  const { MigrationManager } = require("./migrations.cjs");
29
29
 
30
+ // JSON has no BigInt, so u128 amounts are stored as decimal strings, which the
31
+ // SDK reads back as BigInts.
32
+ function toJson(value) {
33
+ return JSON.stringify(value, (_key, v) =>
34
+ typeof v === "bigint" ? v.toString() : v
35
+ );
36
+ }
37
+
30
38
  /**
31
39
  * Base query for payment lookups.
32
40
  * All columns are accessed by name in _rowToPayment.
@@ -329,7 +337,7 @@ class SqliteStorage {
329
337
  } catch (error) {
330
338
  return Promise.reject(
331
339
  new StorageError(
332
- `Failed to list payments (request: ${JSON.stringify(request)}: ${
340
+ `Failed to list payments (request: ${toJson(request)}: ${
333
341
  error.message
334
342
  }`,
335
343
  error
@@ -446,7 +454,7 @@ class SqliteStorage {
446
454
  amount: payment.amount.toString(),
447
455
  fees: payment.fees.toString(),
448
456
  timestamp: payment.timestamp,
449
- method: payment.method ? JSON.stringify(payment.method) : null,
457
+ method: payment.method ? toJson(payment.method) : null,
450
458
  withdrawTxId:
451
459
  payment.details?.type === "withdraw" ? payment.details.txId : null,
452
460
  spark: payment.details?.type === "spark" ? 1 : null,
@@ -468,10 +476,10 @@ class SqliteStorage {
468
476
  sparkInsert.run({
469
477
  id: payment.id,
470
478
  invoiceDetails: payment.details.invoiceDetails
471
- ? JSON.stringify(payment.details.invoiceDetails)
479
+ ? toJson(payment.details.invoiceDetails)
472
480
  : null,
473
481
  htlcDetails: payment.details.htlcDetails
474
- ? JSON.stringify(payment.details.htlcDetails)
482
+ ? toJson(payment.details.htlcDetails)
475
483
  : null,
476
484
  });
477
485
  }
@@ -492,11 +500,11 @@ class SqliteStorage {
492
500
  if (payment.details?.type === "token") {
493
501
  tokenInsert.run({
494
502
  id: payment.id,
495
- metadata: JSON.stringify(payment.details.metadata),
503
+ metadata: toJson(payment.details.metadata),
496
504
  txHash: payment.details.txHash,
497
505
  txType: payment.details.txType,
498
506
  invoiceDetails: payment.details.invoiceDetails
499
- ? JSON.stringify(payment.details.invoiceDetails)
507
+ ? toJson(payment.details.invoiceDetails)
500
508
  : null,
501
509
  });
502
510
  }
@@ -560,7 +568,6 @@ class SqliteStorage {
560
568
  return Promise.resolve(this._rowToPayment(row));
561
569
  } catch (error) {
562
570
  if (error instanceof StorageError) return Promise.reject(error);
563
- const paymentId = id || "unknown";
564
571
  return Promise.reject(
565
572
  new StorageError(
566
573
  `Failed to get payment by invoice '${invoice}': ${error.message}`,
@@ -636,13 +643,13 @@ class SqliteStorage {
636
643
  stmt.run(
637
644
  paymentId,
638
645
  metadata.parentPaymentId,
639
- metadata.lnurlPayInfo ? JSON.stringify(metadata.lnurlPayInfo) : null,
646
+ metadata.lnurlPayInfo ? toJson(metadata.lnurlPayInfo) : null,
640
647
  metadata.lnurlWithdrawInfo
641
- ? JSON.stringify(metadata.lnurlWithdrawInfo)
648
+ ? toJson(metadata.lnurlWithdrawInfo)
642
649
  : null,
643
650
  metadata.lnurlDescription,
644
651
  metadata.conversionInfo
645
- ? JSON.stringify(metadata.conversionInfo)
652
+ ? toJson(metadata.conversionInfo)
646
653
  : null,
647
654
  metadata.conversionStatus ?? null
648
655
  );
@@ -739,7 +746,7 @@ class SqliteStorage {
739
746
  WHERE txid = ? AND vout = ?
740
747
  `);
741
748
 
742
- stmt.run(JSON.stringify(payload.error), txid, vout);
749
+ stmt.run(toJson(payload.error), txid, vout);
743
750
  } else if (payload.type === "refund") {
744
751
  const stmt = this.db.prepare(`
745
752
  UPDATE unclaimed_deposits
@@ -750,7 +757,7 @@ class SqliteStorage {
750
757
  stmt.run(
751
758
  payload.refundTx,
752
759
  payload.refundTxid,
753
- JSON.stringify(payload.state),
760
+ toJson(payload.state),
754
761
  txid,
755
762
  vout
756
763
  );
@@ -761,7 +768,7 @@ class SqliteStorage {
761
768
  WHERE txid = ? AND vout = ?
762
769
  `);
763
770
 
764
- stmt.run(JSON.stringify(payload.status), txid, vout);
771
+ stmt.run(toJson(payload.status), txid, vout);
765
772
  } else if (payload.type === "refundBroadcastState") {
766
773
  const stmt = this.db.prepare(`
767
774
  UPDATE unclaimed_deposits
@@ -769,7 +776,7 @@ class SqliteStorage {
769
776
  WHERE txid = ? AND vout = ? AND refund_tx_id = ?
770
777
  `);
771
778
 
772
- stmt.run(JSON.stringify(payload.state), txid, vout, payload.refundTxid);
779
+ stmt.run(toJson(payload.state), txid, vout, payload.refundTxid);
773
780
  } else if (payload.type === "maxClaimFee") {
774
781
  const stmt = this.db.prepare(`
775
782
  UPDATE unclaimed_deposits
@@ -778,7 +785,7 @@ class SqliteStorage {
778
785
  `);
779
786
 
780
787
  stmt.run(
781
- payload.maxFee ? JSON.stringify(payload.maxFee) : null,
788
+ payload.maxFee ? toJson(payload.maxFee) : null,
782
789
  txid,
783
790
  vout
784
791
  );
@@ -864,6 +871,147 @@ class SqliteStorage {
864
871
  }
865
872
  }
866
873
 
874
+ listLeafRecoveries() {
875
+ try {
876
+ const rows = this.db
877
+ .prepare(
878
+ `SELECT l.leaf_id,
879
+ l.chain_checked_at,
880
+ o.txid AS output_txid,
881
+ o.vout AS output_vout,
882
+ o.amount_sats AS output_amount_sats,
883
+ o.script_pubkey AS output_script_pubkey,
884
+ ot.block_height AS output_block_height,
885
+ s.txid AS spend_txid,
886
+ st.block_height AS spend_block_height,
887
+ w.txid AS sweep_txid,
888
+ wt.block_height AS sweep_block_height,
889
+ r.txid AS recovery_txid,
890
+ r.transaction_hex AS recovery_transaction_hex,
891
+ r.output_amount_sats AS recovery_output_amount_sats
892
+ FROM leaf_recoveries l
893
+ LEFT JOIN watchtower_exit_outputs o ON o.leaf_id = l.leaf_id
894
+ LEFT JOIN chain_transactions ot ON ot.txid = o.txid
895
+ LEFT JOIN watchtower_exit_spends s ON s.leaf_id = l.leaf_id
896
+ LEFT JOIN chain_transactions st ON st.txid = s.txid
897
+ LEFT JOIN unilateral_exit_sweeps w ON w.leaf_id = l.leaf_id
898
+ LEFT JOIN chain_transactions wt ON wt.txid = w.txid
899
+ LEFT JOIN watchtower_exit_recoveries r ON r.leaf_id = l.leaf_id
900
+ ORDER BY l.leaf_id, r.txid`
901
+ )
902
+ .all();
903
+ return Promise.resolve(leafRecoveriesFromRows(rows));
904
+ } catch (error) {
905
+ return Promise.reject(
906
+ new StorageError(`Failed to list leaf recoveries: ${error.message}`, error)
907
+ );
908
+ }
909
+ }
910
+
911
+ updateLeafRecovery(update) {
912
+ try {
913
+ const leafId = update.leafId;
914
+ const setChainTransaction = this.db.prepare(
915
+ `INSERT INTO chain_transactions (txid, block_height) VALUES (?, ?)
916
+ ON CONFLICT(txid) DO UPDATE SET block_height = excluded.block_height`
917
+ );
918
+ const transaction = this.db.transaction(() => {
919
+ this.db
920
+ .prepare(
921
+ "INSERT INTO leaf_recoveries (leaf_id) VALUES (?) ON CONFLICT(leaf_id) DO NOTHING"
922
+ )
923
+ .run(leafId);
924
+ if (update.chainCheckedAt != null) {
925
+ this.db
926
+ .prepare(
927
+ `UPDATE leaf_recoveries
928
+ SET chain_checked_at = MAX(COALESCE(chain_checked_at, 0), ?)
929
+ WHERE leaf_id = ?`
930
+ )
931
+ .run(update.chainCheckedAt, leafId);
932
+ }
933
+ if (update.watchtowerExitOutput != null) {
934
+ const output = update.watchtowerExitOutput;
935
+ for (const table of [
936
+ "watchtower_exit_recoveries",
937
+ "watchtower_exit_spends",
938
+ ]) {
939
+ this.db
940
+ .prepare(
941
+ `DELETE FROM ${table} WHERE leaf_id = @leafId AND EXISTS (
942
+ SELECT 1 FROM watchtower_exit_outputs
943
+ WHERE leaf_id = @leafId AND (txid <> @txid OR vout <> @vout))`
944
+ )
945
+ .run({ leafId, txid: output.txid, vout: output.vout });
946
+ }
947
+ setChainTransaction.run(output.txid, output.blockHeight);
948
+ this.db
949
+ .prepare(
950
+ `INSERT INTO watchtower_exit_outputs
951
+ (leaf_id, txid, vout, amount_sats, script_pubkey)
952
+ VALUES (?, ?, ?, ?, ?)
953
+ ON CONFLICT(leaf_id) DO UPDATE SET
954
+ txid = excluded.txid,
955
+ vout = excluded.vout,
956
+ amount_sats = excluded.amount_sats,
957
+ script_pubkey = excluded.script_pubkey`
958
+ )
959
+ .run(
960
+ leafId,
961
+ output.txid,
962
+ output.vout,
963
+ output.amountSats,
964
+ output.scriptPubkey
965
+ );
966
+ }
967
+ if (update.watchtowerExitRecovery != null) {
968
+ const recovery = update.watchtowerExitRecovery;
969
+ this.db
970
+ .prepare(
971
+ `INSERT INTO watchtower_exit_recoveries
972
+ (leaf_id, txid, transaction_hex, output_amount_sats)
973
+ VALUES (?, ?, ?, ?)
974
+ ON CONFLICT(leaf_id, txid) DO NOTHING`
975
+ )
976
+ .run(
977
+ leafId,
978
+ recovery.txid,
979
+ recovery.transactionHex,
980
+ recovery.outputAmountSats
981
+ );
982
+ }
983
+ for (const [table, chainTransaction] of [
984
+ ["watchtower_exit_spends", update.watchtowerExitSpend],
985
+ ["unilateral_exit_sweeps", update.unilateralExitSweep],
986
+ ]) {
987
+ if (chainTransaction == null) {
988
+ continue;
989
+ }
990
+ setChainTransaction.run(
991
+ chainTransaction.txid,
992
+ chainTransaction.blockHeight
993
+ );
994
+ this.db
995
+ .prepare(
996
+ `INSERT INTO ${table} (leaf_id, txid) VALUES (?, ?)
997
+ ON CONFLICT(leaf_id) DO UPDATE SET txid = excluded.txid`
998
+ )
999
+ .run(leafId, chainTransaction.txid);
1000
+ }
1001
+ });
1002
+
1003
+ transaction.immediate();
1004
+ return Promise.resolve();
1005
+ } catch (error) {
1006
+ return Promise.reject(
1007
+ new StorageError(
1008
+ `Failed to update leaf recovery '${update.leafId}': ${error.message}`,
1009
+ error
1010
+ )
1011
+ );
1012
+ }
1013
+ }
1014
+
867
1015
  setLnurlMetadata(metadata) {
868
1016
  try {
869
1017
  const stmt = this.db.prepare(
@@ -1048,7 +1196,7 @@ class SqliteStorage {
1048
1196
  record.id.dataId,
1049
1197
  record.schemaVersion,
1050
1198
  Math.floor(Date.now() / 1000),
1051
- JSON.stringify(record.updatedFields),
1199
+ toJson(record.updatedFields),
1052
1200
  revision.toString()
1053
1201
  );
1054
1202
 
@@ -1099,7 +1247,7 @@ class SqliteStorage {
1099
1247
  record.revision.toString(),
1100
1248
  record.schemaVersion,
1101
1249
  Math.floor(Date.now() / 1000),
1102
- JSON.stringify(record.data)
1250
+ toJson(record.data)
1103
1251
  );
1104
1252
 
1105
1253
  // Update sync_revision to track the highest known revision
@@ -1225,7 +1373,7 @@ class SqliteStorage {
1225
1373
  record.id.dataId,
1226
1374
  record.schemaVersion,
1227
1375
  Math.floor(Date.now() / 1000),
1228
- JSON.stringify(record.data),
1376
+ toJson(record.data),
1229
1377
  record.revision.toString()
1230
1378
  );
1231
1379
  }
@@ -1419,7 +1567,7 @@ class SqliteStorage {
1419
1567
  record.revision.toString(),
1420
1568
  record.schemaVersion,
1421
1569
  Math.floor(Date.now() / 1000),
1422
- JSON.stringify(record.data)
1570
+ toJson(record.data)
1423
1571
  );
1424
1572
 
1425
1573
  // Update sync_revision to track the highest known revision
@@ -1592,6 +1740,49 @@ function crossChainSwapFromRow(row) {
1592
1740
  };
1593
1741
  }
1594
1742
 
1743
+ /// A row holds a leaf recovery and one of its watchtower exit recoveries, so
1744
+ /// a leaf recovery with several of them spans several rows.
1745
+ function leafRecoveriesFromRows(rows) {
1746
+ const leafRecoveries = [];
1747
+ for (const row of rows) {
1748
+ let leafRecovery = leafRecoveries[leafRecoveries.length - 1];
1749
+ if (leafRecovery?.leafId !== row.leaf_id) {
1750
+ leafRecovery = {
1751
+ leafId: row.leaf_id,
1752
+ chainCheckedAt: row.chain_checked_at,
1753
+ watchtowerExitOutput:
1754
+ row.output_block_height != null
1755
+ ? {
1756
+ txid: row.output_txid,
1757
+ vout: row.output_vout,
1758
+ amountSats: row.output_amount_sats,
1759
+ scriptPubkey: row.output_script_pubkey,
1760
+ blockHeight: row.output_block_height,
1761
+ }
1762
+ : null,
1763
+ watchtowerExitRecoveries: [],
1764
+ watchtowerExitSpend:
1765
+ row.spend_block_height != null
1766
+ ? { txid: row.spend_txid, blockHeight: row.spend_block_height }
1767
+ : null,
1768
+ unilateralExitSweep:
1769
+ row.sweep_block_height != null
1770
+ ? { txid: row.sweep_txid, blockHeight: row.sweep_block_height }
1771
+ : null,
1772
+ };
1773
+ leafRecoveries.push(leafRecovery);
1774
+ }
1775
+ if (row.recovery_txid != null) {
1776
+ leafRecovery.watchtowerExitRecoveries.push({
1777
+ txid: row.recovery_txid,
1778
+ transactionHex: row.recovery_transaction_hex,
1779
+ outputAmountSats: row.recovery_output_amount_sats,
1780
+ });
1781
+ }
1782
+ }
1783
+ return leafRecoveries;
1784
+ }
1785
+
1595
1786
  async function createDefaultStorage(dataDir, logger = null) {
1596
1787
  const path = require("path");
1597
1788
  const fs = require("fs").promises;
@@ -521,6 +521,44 @@ class MigrationManager {
521
521
  `ALTER TABLE unclaimed_deposits ADD COLUMN max_claim_fee TEXT`,
522
522
  ],
523
523
  },
524
+ {
525
+ // Leaf recoveries. What the chain service reported about leaves with
526
+ // funds to recover on-chain, and the recoveries the operators co-signed.
527
+ // Every block height is in chain_transactions, keyed by txid.
528
+ name: "Add leaf recovery tables",
529
+ sql: [
530
+ `CREATE TABLE IF NOT EXISTS leaf_recoveries (
531
+ leaf_id TEXT NOT NULL PRIMARY KEY,
532
+ chain_checked_at INTEGER
533
+ )`,
534
+ `CREATE TABLE IF NOT EXISTS chain_transactions (
535
+ txid TEXT NOT NULL PRIMARY KEY,
536
+ block_height INTEGER NOT NULL
537
+ )`,
538
+ `CREATE TABLE IF NOT EXISTS watchtower_exit_outputs (
539
+ leaf_id TEXT NOT NULL PRIMARY KEY,
540
+ txid TEXT NOT NULL,
541
+ vout INTEGER NOT NULL,
542
+ amount_sats INTEGER NOT NULL,
543
+ script_pubkey TEXT NOT NULL
544
+ )`,
545
+ `CREATE TABLE IF NOT EXISTS watchtower_exit_recoveries (
546
+ leaf_id TEXT NOT NULL,
547
+ txid TEXT NOT NULL,
548
+ transaction_hex TEXT NOT NULL,
549
+ output_amount_sats INTEGER NOT NULL,
550
+ PRIMARY KEY (leaf_id, txid)
551
+ )`,
552
+ `CREATE TABLE IF NOT EXISTS watchtower_exit_spends (
553
+ leaf_id TEXT NOT NULL PRIMARY KEY,
554
+ txid TEXT NOT NULL
555
+ )`,
556
+ `CREATE TABLE IF NOT EXISTS unilateral_exit_sweeps (
557
+ leaf_id TEXT NOT NULL PRIMARY KEY,
558
+ txid TEXT NOT NULL
559
+ )`,
560
+ ],
561
+ },
524
562
  ];
525
563
  }
526
564
  }
@@ -94,6 +94,17 @@ const SLIM_LEAF_CANDIDATES_SQL = `
94
94
  )
95
95
  `;
96
96
 
97
+ /**
98
+ * Takes one status, bound twice: given several through IN, SQLite reads every
99
+ * unreserved leaf instead of using the status index.
100
+ */
101
+ const LEAVES_WITH_STATUS_SQL = `
102
+ SELECT data FROM brz_tree_leaves WHERE status = ? AND reservation_id IS NULL
103
+ UNION ALL
104
+ SELECT data FROM brz_tree_leaves
105
+ WHERE status = ? AND reservation_id IN (SELECT id FROM brz_tree_reservations)
106
+ `;
107
+
97
108
  /**
98
109
  * Pair a leaf with its ancestors (nearest first) by walking `parent_node_id`
99
110
  * through `nodes`. Returns null if the leaf itself is absent; stops at a gap or
@@ -414,6 +425,30 @@ class NodeTreeStore {
414
425
  }
415
426
  }
416
427
 
428
+ /**
429
+ * Return the leaves, reserved or not, whose status is one of `statuses`.
430
+ * @param {Array<string>} statuses
431
+ * @returns {Promise<Array<object>>}
432
+ */
433
+ async getLeavesWithStatus(statuses) {
434
+ try {
435
+ const stmt = this.db.prepare(LEAVES_WITH_STATUS_SQL);
436
+ // One snapshot across the statements, so a leaf whose status changes
437
+ // between them is not read twice or missed.
438
+ return this.db.transaction(() =>
439
+ [...new Set(statuses)].flatMap((status) =>
440
+ stmt.all(status, status).map((row) => JSON.parse(row.data))
441
+ )
442
+ )();
443
+ } catch (error) {
444
+ if (error instanceof TreeStoreError) throw error;
445
+ throw new TreeStoreError(
446
+ `Failed to get leaves with status: ${error.message}`,
447
+ error
448
+ );
449
+ }
450
+ }
451
+
417
452
  /**
418
453
  * Replace the pool from a refresh. Skipped while a swap is in flight or one
419
454
  * completed during the refresh, so a swap's leaves are never clobbered.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@breeztech/breez-sdk-spark",
3
- "version": "0.27.0-dev1",
3
+ "version": "0.27.0",
4
4
  "description": "Breez Spark SDK",
5
5
  "repository": "https://github.com/breez/spark-sdk",
6
6
  "author": "Breez <contact@breez.technology> (https://github.com/breez)",