@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,10 +22,12 @@ try {
22
22
  try {
23
23
  pg = require("pg");
24
24
  } catch (fallbackError) {
25
- throw new Error(
25
+ const loadError = new Error(
26
26
  `pg not found. Please install it in your project: npm install pg@^8.18.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
 
@@ -16,16 +16,26 @@ try {
16
16
  try {
17
17
  pg = require("pg");
18
18
  } catch (fallbackError) {
19
- throw new Error(
19
+ const loadError = new Error(
20
20
  `pg not found. Please install it in your project: npm install pg@^8.18.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
 
26
28
  const { StorageError } = require("./errors.cjs");
27
29
  const { PostgresMigrationManager } = require("./migrations.cjs");
28
30
 
31
+ // JSON has no BigInt, so u128 amounts are stored as decimal strings, which the
32
+ // SDK reads back as BigInts.
33
+ function toJson(value) {
34
+ return JSON.stringify(value, (_key, v) =>
35
+ typeof v === "bigint" ? v.toString() : v
36
+ );
37
+ }
38
+
29
39
  /**
30
40
  * Base query for payment lookups.
31
41
  * All columns are accessed by name in _rowToPayment.
@@ -371,7 +381,7 @@ class PostgresStorage {
371
381
  } catch (error) {
372
382
  if (error instanceof StorageError) throw error;
373
383
  throw new StorageError(
374
- `Failed to list payments (request: ${JSON.stringify(request)}): ${error.message}`,
384
+ `Failed to list payments (request: ${toJson(request)}): ${error.message}`,
375
385
  error
376
386
  );
377
387
  }
@@ -462,7 +472,7 @@ class PostgresStorage {
462
472
  payment.amount.toString(),
463
473
  payment.fees.toString(),
464
474
  payment.timestamp,
465
- payment.method ? JSON.stringify(payment.method) : null,
475
+ payment.method ? toJson(payment.method) : null,
466
476
  withdrawTxId,
467
477
  spark,
468
478
  ]
@@ -494,10 +504,10 @@ class PostgresStorage {
494
504
  this.identity,
495
505
  payment.id,
496
506
  payment.details.invoiceDetails
497
- ? JSON.stringify(payment.details.invoiceDetails)
507
+ ? toJson(payment.details.invoiceDetails)
498
508
  : null,
499
509
  payment.details.htlcDetails
500
- ? JSON.stringify(payment.details.htlcDetails)
510
+ ? toJson(payment.details.htlcDetails)
501
511
  : null,
502
512
  ]
503
513
  );
@@ -543,11 +553,11 @@ class PostgresStorage {
543
553
  [
544
554
  this.identity,
545
555
  payment.id,
546
- JSON.stringify(payment.details.metadata),
556
+ toJson(payment.details.metadata),
547
557
  payment.details.txHash,
548
558
  payment.details.txType,
549
559
  payment.details.invoiceDetails
550
- ? JSON.stringify(payment.details.invoiceDetails)
560
+ ? toJson(payment.details.invoiceDetails)
551
561
  : null,
552
562
  ]
553
563
  );
@@ -675,14 +685,14 @@ class PostgresStorage {
675
685
  paymentId,
676
686
  metadata.parentPaymentId,
677
687
  metadata.lnurlPayInfo
678
- ? JSON.stringify(metadata.lnurlPayInfo)
688
+ ? toJson(metadata.lnurlPayInfo)
679
689
  : null,
680
690
  metadata.lnurlWithdrawInfo
681
- ? JSON.stringify(metadata.lnurlWithdrawInfo)
691
+ ? toJson(metadata.lnurlWithdrawInfo)
682
692
  : null,
683
693
  metadata.lnurlDescription,
684
694
  metadata.conversionInfo
685
- ? JSON.stringify(metadata.conversionInfo)
695
+ ? toJson(metadata.conversionInfo)
686
696
  : null,
687
697
  metadata.conversionStatus ?? null,
688
698
  ]
@@ -761,7 +771,7 @@ class PostgresStorage {
761
771
  `UPDATE brz_unclaimed_deposits
762
772
  SET claim_error = $1
763
773
  WHERE user_id = $2 AND txid = $3 AND vout = $4`,
764
- [JSON.stringify(payload.error), this.identity, txid, vout]
774
+ [toJson(payload.error), this.identity, txid, vout]
765
775
  );
766
776
  } else if (payload.type === "refund") {
767
777
  await this.pool.query(
@@ -771,7 +781,7 @@ class PostgresStorage {
771
781
  [
772
782
  payload.refundTx,
773
783
  payload.refundTxid,
774
- JSON.stringify(payload.state),
784
+ toJson(payload.state),
775
785
  this.identity,
776
786
  txid,
777
787
  vout,
@@ -782,7 +792,7 @@ class PostgresStorage {
782
792
  `UPDATE brz_unclaimed_deposits
783
793
  SET instant_claim_status = $1
784
794
  WHERE user_id = $2 AND txid = $3 AND vout = $4`,
785
- [JSON.stringify(payload.status), this.identity, txid, vout]
795
+ [toJson(payload.status), this.identity, txid, vout]
786
796
  );
787
797
  } else if (payload.type === "refundBroadcastState") {
788
798
  await this.pool.query(
@@ -790,7 +800,7 @@ class PostgresStorage {
790
800
  SET refund_state = $1
791
801
  WHERE user_id = $2 AND txid = $3 AND vout = $4 AND refund_tx_id = $5`,
792
802
  [
793
- JSON.stringify(payload.state),
803
+ toJson(payload.state),
794
804
  this.identity,
795
805
  txid,
796
806
  vout,
@@ -803,7 +813,7 @@ class PostgresStorage {
803
813
  SET max_claim_fee = $1
804
814
  WHERE user_id = $2 AND txid = $3 AND vout = $4`,
805
815
  [
806
- payload.maxFee ? JSON.stringify(payload.maxFee) : null,
816
+ payload.maxFee ? toJson(payload.maxFee) : null,
807
817
  this.identity,
808
818
  txid,
809
819
  vout,
@@ -874,6 +884,153 @@ class PostgresStorage {
874
884
  }
875
885
  }
876
886
 
887
+ async listLeafRecoveries() {
888
+ try {
889
+ const result = await this.pool.query(
890
+ `SELECT l.leaf_id,
891
+ l.chain_checked_at,
892
+ o.txid AS output_txid,
893
+ o.vout AS output_vout,
894
+ o.amount_sats AS output_amount_sats,
895
+ o.script_pubkey AS output_script_pubkey,
896
+ ot.block_height AS output_block_height,
897
+ s.txid AS spend_txid,
898
+ st.block_height AS spend_block_height,
899
+ w.txid AS sweep_txid,
900
+ wt.block_height AS sweep_block_height,
901
+ r.txid AS recovery_txid,
902
+ r.transaction_hex AS recovery_transaction_hex,
903
+ r.output_amount_sats AS recovery_output_amount_sats
904
+ FROM brz_leaf_recoveries l
905
+ LEFT JOIN brz_watchtower_exit_outputs o
906
+ ON o.user_id = l.user_id AND o.leaf_id = l.leaf_id
907
+ LEFT JOIN brz_chain_transactions ot
908
+ ON ot.user_id = o.user_id AND ot.txid = o.txid
909
+ LEFT JOIN brz_watchtower_exit_spends s
910
+ ON s.user_id = l.user_id AND s.leaf_id = l.leaf_id
911
+ LEFT JOIN brz_chain_transactions st
912
+ ON st.user_id = s.user_id AND st.txid = s.txid
913
+ LEFT JOIN brz_unilateral_exit_sweeps w
914
+ ON w.user_id = l.user_id AND w.leaf_id = l.leaf_id
915
+ LEFT JOIN brz_chain_transactions wt
916
+ ON wt.user_id = w.user_id AND wt.txid = w.txid
917
+ LEFT JOIN brz_watchtower_exit_recoveries r
918
+ ON r.user_id = l.user_id AND r.leaf_id = l.leaf_id
919
+ WHERE l.user_id = $1
920
+ ORDER BY l.leaf_id, r.txid`,
921
+ [this.identity]
922
+ );
923
+ return leafRecoveriesFromRows(result.rows);
924
+ } catch (error) {
925
+ throw new StorageError(
926
+ `Failed to list leaf recoveries: ${error.message}`,
927
+ error
928
+ );
929
+ }
930
+ }
931
+
932
+ async updateLeafRecovery(update) {
933
+ try {
934
+ const leafId = update.leafId;
935
+ await this._withTransaction(async (client) => {
936
+ const setChainTransaction = (txid, blockHeight) =>
937
+ client.query(
938
+ `INSERT INTO brz_chain_transactions (user_id, txid, block_height)
939
+ VALUES ($1, $2, $3)
940
+ ON CONFLICT(user_id, txid) DO UPDATE SET block_height = EXCLUDED.block_height`,
941
+ [this.identity, txid, blockHeight]
942
+ );
943
+
944
+ // PostgreSQL locks the leaf's row, so writers of one leaf do not interleave.
945
+ await client.query(
946
+ `INSERT INTO brz_leaf_recoveries (user_id, leaf_id) VALUES ($1, $2)
947
+ ON CONFLICT(user_id, leaf_id) DO UPDATE SET leaf_id = EXCLUDED.leaf_id`,
948
+ [this.identity, leafId]
949
+ );
950
+ if (update.chainCheckedAt != null) {
951
+ await client.query(
952
+ `UPDATE brz_leaf_recoveries
953
+ SET chain_checked_at = GREATEST(COALESCE(chain_checked_at, 0), $3)
954
+ WHERE user_id = $1 AND leaf_id = $2`,
955
+ [this.identity, leafId, String(update.chainCheckedAt)]
956
+ );
957
+ }
958
+ if (update.watchtowerExitOutput != null) {
959
+ const output = update.watchtowerExitOutput;
960
+ for (const table of [
961
+ "brz_watchtower_exit_recoveries",
962
+ "brz_watchtower_exit_spends",
963
+ ]) {
964
+ await client.query(
965
+ `DELETE FROM ${table}
966
+ WHERE user_id = $1 AND leaf_id = $2 AND EXISTS (
967
+ SELECT 1 FROM brz_watchtower_exit_outputs
968
+ WHERE user_id = $1 AND leaf_id = $2 AND (txid <> $3 OR vout <> $4))`,
969
+ [this.identity, leafId, output.txid, output.vout]
970
+ );
971
+ }
972
+ await setChainTransaction(output.txid, output.blockHeight);
973
+ await client.query(
974
+ `INSERT INTO brz_watchtower_exit_outputs
975
+ (user_id, leaf_id, txid, vout, amount_sats, script_pubkey)
976
+ VALUES ($1, $2, $3, $4, $5, $6)
977
+ ON CONFLICT(user_id, leaf_id) DO UPDATE SET
978
+ txid = EXCLUDED.txid,
979
+ vout = EXCLUDED.vout,
980
+ amount_sats = EXCLUDED.amount_sats,
981
+ script_pubkey = EXCLUDED.script_pubkey`,
982
+ [
983
+ this.identity,
984
+ leafId,
985
+ output.txid,
986
+ output.vout,
987
+ String(output.amountSats),
988
+ output.scriptPubkey,
989
+ ]
990
+ );
991
+ }
992
+ if (update.watchtowerExitRecovery != null) {
993
+ const recovery = update.watchtowerExitRecovery;
994
+ await client.query(
995
+ `INSERT INTO brz_watchtower_exit_recoveries
996
+ (user_id, leaf_id, txid, transaction_hex, output_amount_sats)
997
+ VALUES ($1, $2, $3, $4, $5)
998
+ ON CONFLICT(user_id, leaf_id, txid) DO NOTHING`,
999
+ [
1000
+ this.identity,
1001
+ leafId,
1002
+ recovery.txid,
1003
+ recovery.transactionHex,
1004
+ String(recovery.outputAmountSats),
1005
+ ]
1006
+ );
1007
+ }
1008
+ for (const [table, chainTransaction] of [
1009
+ ["brz_watchtower_exit_spends", update.watchtowerExitSpend],
1010
+ ["brz_unilateral_exit_sweeps", update.unilateralExitSweep],
1011
+ ]) {
1012
+ if (chainTransaction == null) {
1013
+ continue;
1014
+ }
1015
+ await setChainTransaction(
1016
+ chainTransaction.txid,
1017
+ chainTransaction.blockHeight
1018
+ );
1019
+ await client.query(
1020
+ `INSERT INTO ${table} (user_id, leaf_id, txid) VALUES ($1, $2, $3)
1021
+ ON CONFLICT(user_id, leaf_id) DO UPDATE SET txid = EXCLUDED.txid`,
1022
+ [this.identity, leafId, chainTransaction.txid]
1023
+ );
1024
+ }
1025
+ });
1026
+ } catch (error) {
1027
+ throw new StorageError(
1028
+ `Failed to update leaf recovery '${update.leafId}': ${error.message}`,
1029
+ error
1030
+ );
1031
+ }
1032
+ }
1033
+
877
1034
  async setLnurlMetadata(metadata) {
878
1035
  try {
879
1036
  await this._withTransaction(async (client) => {
@@ -1235,7 +1392,7 @@ class PostgresStorage {
1235
1392
  record.id.dataId,
1236
1393
  record.schemaVersion,
1237
1394
  Math.floor(Date.now() / 1000),
1238
- JSON.stringify(record.updatedFields),
1395
+ toJson(record.updatedFields),
1239
1396
  revision.toString(),
1240
1397
  ]
1241
1398
  );
@@ -1295,7 +1452,7 @@ class PostgresStorage {
1295
1452
  record.revision.toString(),
1296
1453
  record.schemaVersion,
1297
1454
  Math.floor(Date.now() / 1000),
1298
- JSON.stringify(record.data),
1455
+ toJson(record.data),
1299
1456
  ]
1300
1457
  );
1301
1458
 
@@ -1426,7 +1583,7 @@ class PostgresStorage {
1426
1583
  record.id.dataId,
1427
1584
  record.schemaVersion,
1428
1585
  Math.floor(Date.now() / 1000),
1429
- JSON.stringify(record.data),
1586
+ toJson(record.data),
1430
1587
  record.revision.toString(),
1431
1588
  ]
1432
1589
  );
@@ -1613,7 +1770,7 @@ class PostgresStorage {
1613
1770
  record.revision.toString(),
1614
1771
  record.schemaVersion,
1615
1772
  Math.floor(Date.now() / 1000),
1616
- JSON.stringify(record.data),
1773
+ toJson(record.data),
1617
1774
  ]
1618
1775
  );
1619
1776
 
@@ -1648,6 +1805,58 @@ function crossChainSwapFromRow(row) {
1648
1805
  };
1649
1806
  }
1650
1807
 
1808
+ /**
1809
+ * A row holds a leaf recovery and one of its watchtower exit recoveries, so
1810
+ * a leaf recovery with several of them spans several rows.
1811
+ */
1812
+ function leafRecoveriesFromRows(rows) {
1813
+ const leafRecoveries = [];
1814
+ for (const row of rows) {
1815
+ let leafRecovery = leafRecoveries[leafRecoveries.length - 1];
1816
+ if (leafRecovery?.leafId !== row.leaf_id) {
1817
+ leafRecovery = {
1818
+ leafId: row.leaf_id,
1819
+ chainCheckedAt:
1820
+ row.chain_checked_at != null ? BigInt(row.chain_checked_at) : null,
1821
+ watchtowerExitOutput:
1822
+ row.output_block_height != null
1823
+ ? {
1824
+ txid: row.output_txid,
1825
+ vout: Number(row.output_vout),
1826
+ amountSats: BigInt(row.output_amount_sats),
1827
+ scriptPubkey: row.output_script_pubkey,
1828
+ blockHeight: Number(row.output_block_height),
1829
+ }
1830
+ : null,
1831
+ watchtowerExitRecoveries: [],
1832
+ watchtowerExitSpend:
1833
+ row.spend_block_height != null
1834
+ ? {
1835
+ txid: row.spend_txid,
1836
+ blockHeight: Number(row.spend_block_height),
1837
+ }
1838
+ : null,
1839
+ unilateralExitSweep:
1840
+ row.sweep_block_height != null
1841
+ ? {
1842
+ txid: row.sweep_txid,
1843
+ blockHeight: Number(row.sweep_block_height),
1844
+ }
1845
+ : null,
1846
+ };
1847
+ leafRecoveries.push(leafRecovery);
1848
+ }
1849
+ if (row.recovery_txid != null) {
1850
+ leafRecovery.watchtowerExitRecoveries.push({
1851
+ txid: row.recovery_txid,
1852
+ transactionHex: row.recovery_transaction_hex,
1853
+ outputAmountSats: BigInt(row.recovery_output_amount_sats),
1854
+ });
1855
+ }
1856
+ }
1857
+ return leafRecoveries;
1858
+ }
1859
+
1651
1860
  /**
1652
1861
  * Creates a PostgresStorageConfig with the given connection string and default pool settings.
1653
1862
  *
@@ -563,6 +563,55 @@ class PostgresMigrationManager {
563
563
  `ALTER TABLE brz_unclaimed_deposits ADD COLUMN max_claim_fee JSONB`,
564
564
  ],
565
565
  },
566
+ {
567
+ // Leaf recoveries. What the chain service reported about leaves with
568
+ // funds to recover on-chain, and the recoveries the operators co-signed.
569
+ // Every block height is in brz_chain_transactions, keyed by txid.
570
+ name: "Add leaf recovery tables",
571
+ sql: [
572
+ `CREATE TABLE IF NOT EXISTS brz_leaf_recoveries (
573
+ user_id BYTEA NOT NULL,
574
+ leaf_id TEXT NOT NULL,
575
+ chain_checked_at BIGINT,
576
+ PRIMARY KEY (user_id, leaf_id)
577
+ )`,
578
+ `CREATE TABLE IF NOT EXISTS brz_chain_transactions (
579
+ user_id BYTEA NOT NULL,
580
+ txid TEXT NOT NULL,
581
+ block_height BIGINT NOT NULL,
582
+ PRIMARY KEY (user_id, txid)
583
+ )`,
584
+ `CREATE TABLE IF NOT EXISTS brz_watchtower_exit_outputs (
585
+ user_id BYTEA NOT NULL,
586
+ leaf_id TEXT NOT NULL,
587
+ txid TEXT NOT NULL,
588
+ vout BIGINT NOT NULL,
589
+ amount_sats BIGINT NOT NULL,
590
+ script_pubkey TEXT NOT NULL,
591
+ PRIMARY KEY (user_id, leaf_id)
592
+ )`,
593
+ `CREATE TABLE IF NOT EXISTS brz_watchtower_exit_recoveries (
594
+ user_id BYTEA NOT NULL,
595
+ leaf_id TEXT NOT NULL,
596
+ txid TEXT NOT NULL,
597
+ transaction_hex TEXT NOT NULL,
598
+ output_amount_sats BIGINT NOT NULL,
599
+ PRIMARY KEY (user_id, leaf_id, txid)
600
+ )`,
601
+ `CREATE TABLE IF NOT EXISTS brz_watchtower_exit_spends (
602
+ user_id BYTEA NOT NULL,
603
+ leaf_id TEXT NOT NULL,
604
+ txid TEXT NOT NULL,
605
+ PRIMARY KEY (user_id, leaf_id)
606
+ )`,
607
+ `CREATE TABLE IF NOT EXISTS brz_unilateral_exit_sweeps (
608
+ user_id BYTEA NOT NULL,
609
+ leaf_id TEXT NOT NULL,
610
+ txid TEXT NOT NULL,
611
+ PRIMARY KEY (user_id, leaf_id)
612
+ )`,
613
+ ],
614
+ },
566
615
  ];
567
616
  }
568
617
  }
@@ -14,10 +14,12 @@ try {
14
14
  try {
15
15
  pg = require("pg");
16
16
  } catch (fallbackError) {
17
- throw new Error(
17
+ const loadError = new Error(
18
18
  `pg not found. Please install it in your project: npm install pg@^8.18.0\n` +
19
19
  `Original error: ${error.message}\nFallback error: ${fallbackError.message}`
20
20
  );
21
+ loadError.code = fallbackError.code;
22
+ throw loadError;
21
23
  }
22
24
  }
23
25
 
@@ -14,10 +14,12 @@ try {
14
14
  try {
15
15
  pg = require("pg");
16
16
  } catch (fallbackError) {
17
- throw new Error(
17
+ const loadError = new Error(
18
18
  `pg not found. Please install it in your project: npm install pg@^8.18.0\n` +
19
19
  `Original error: ${error.message}\nFallback error: ${fallbackError.message}`
20
20
  );
21
+ loadError.code = fallbackError.code;
22
+ throw loadError;
21
23
  }
22
24
  }
23
25
 
@@ -475,6 +477,28 @@ class PostgresTreeStore {
475
477
  }
476
478
  }
477
479
 
480
+ /**
481
+ * Return the leaves, reserved or not, whose status is one of `statuses`.
482
+ * @param {Array<string>} statuses
483
+ * @returns {Promise<Array<object>>}
484
+ */
485
+ async getLeavesWithStatus(statuses) {
486
+ try {
487
+ if (!statuses || statuses.length === 0) return [];
488
+ const result = await this.pool.query(
489
+ "SELECT data FROM brz_tree_leaves WHERE user_id = $1 AND status = ANY($2)",
490
+ [this.identity, statuses]
491
+ );
492
+ return result.rows.map((row) => row.data);
493
+ } catch (error) {
494
+ if (error instanceof TreeStoreError) throw error;
495
+ throw new TreeStoreError(
496
+ `Failed to get leaves with status: ${error.message}`,
497
+ error
498
+ );
499
+ }
500
+ }
501
+
478
502
  /**
479
503
  * Set leaves from a refresh operation.
480
504
  * @param {Array} leaves - Available leaves from operators
@@ -346,6 +346,16 @@ class TreeStoreMigrationManager {
346
346
  signing_public_key = data->'signing_keyshare'->>'public_key'`,
347
347
  ],
348
348
  },
349
+ {
350
+ // Mirrors Rust migration 6 in spark-postgres/src/tree_store.rs. Indexes
351
+ // the leaves outside the `Available` status, so reading them by status
352
+ // does not scan the spendable pool.
353
+ name: "Index leaves outside the Available status",
354
+ sql: [
355
+ `CREATE INDEX IF NOT EXISTS brz_idx_tree_leaves_user_unavailable
356
+ ON brz_tree_leaves(user_id, status) WHERE status <> 'Available'`,
357
+ ],
358
+ },
349
359
  ];
350
360
  }
351
361
  }