@breeztech/breez-sdk-spark 0.15.1 → 0.17.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 (51) hide show
  1. package/breez-sdk-spark.tgz +0 -0
  2. package/bundler/breez_sdk_spark_wasm.d.ts +604 -237
  3. package/bundler/breez_sdk_spark_wasm.js +1 -1
  4. package/bundler/breez_sdk_spark_wasm_bg.js +729 -434
  5. package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
  6. package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +63 -49
  7. package/bundler/storage/index.js +356 -34
  8. package/deno/breez_sdk_spark_wasm.d.ts +604 -237
  9. package/deno/breez_sdk_spark_wasm.js +729 -434
  10. package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
  11. package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +63 -49
  12. package/nodejs/breez_sdk_spark_wasm.d.ts +604 -237
  13. package/nodejs/breez_sdk_spark_wasm.js +741 -441
  14. package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
  15. package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +63 -49
  16. package/nodejs/index.js +10 -10
  17. package/nodejs/index.mjs +13 -8
  18. package/nodejs/mysql-session-store/errors.cjs +13 -0
  19. package/nodejs/{mysql-session-manager → mysql-session-store}/index.cjs +24 -21
  20. package/nodejs/{mysql-session-manager → mysql-session-store}/migrations.cjs +17 -11
  21. package/nodejs/mysql-session-store/package.json +9 -0
  22. package/nodejs/mysql-storage/index.cjs +358 -125
  23. package/nodejs/mysql-storage/migrations.cjs +67 -2
  24. package/nodejs/mysql-token-store/index.cjs +99 -79
  25. package/nodejs/mysql-token-store/migrations.cjs +59 -2
  26. package/nodejs/mysql-tree-store/index.cjs +15 -9
  27. package/nodejs/mysql-tree-store/migrations.cjs +16 -2
  28. package/nodejs/package.json +2 -2
  29. package/nodejs/postgres-session-store/errors.cjs +13 -0
  30. package/nodejs/{postgres-session-manager → postgres-session-store}/index.cjs +23 -23
  31. package/nodejs/{postgres-session-manager → postgres-session-store}/migrations.cjs +14 -14
  32. package/nodejs/postgres-session-store/package.json +9 -0
  33. package/nodejs/postgres-storage/index.cjs +296 -119
  34. package/nodejs/postgres-storage/migrations.cjs +51 -0
  35. package/nodejs/postgres-token-store/index.cjs +89 -64
  36. package/nodejs/postgres-token-store/migrations.cjs +44 -0
  37. package/nodejs/storage/index.cjs +285 -125
  38. package/nodejs/storage/migrations.cjs +47 -0
  39. package/package.json +6 -1
  40. package/ssr/index.js +57 -28
  41. package/web/breez_sdk_spark_wasm.d.ts +667 -286
  42. package/web/breez_sdk_spark_wasm.js +729 -434
  43. package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
  44. package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +63 -49
  45. package/web/passkey-prf-provider/index.d.ts +203 -0
  46. package/web/passkey-prf-provider/index.js +733 -0
  47. package/web/storage/index.js +356 -34
  48. package/nodejs/mysql-session-manager/errors.cjs +0 -13
  49. package/nodejs/mysql-session-manager/package.json +0 -9
  50. package/nodejs/postgres-session-manager/errors.cjs +0 -13
  51. package/nodejs/postgres-session-manager/package.json +0 -9
@@ -2,6 +2,8 @@
2
2
  * CommonJS implementation for Node.js PostgreSQL Storage
3
3
  */
4
4
 
5
+ const crypto = require("crypto");
6
+
5
7
  let pg;
6
8
  try {
7
9
  const mainModule = require.main;
@@ -38,7 +40,8 @@ const SELECT_PAYMENT_SQL = `
38
40
  p.timestamp,
39
41
  p.method,
40
42
  p.withdraw_tx_id,
41
- p.deposit_tx_id,
43
+ pd.tx_id AS deposit_tx_id,
44
+ pd.vout AS deposit_vout,
42
45
  p.spark,
43
46
  l.invoice AS lightning_invoice,
44
47
  l.payment_hash AS lightning_payment_hash,
@@ -66,6 +69,7 @@ const SELECT_PAYMENT_SQL = `
66
69
  LEFT JOIN brz_payment_details_lightning l ON p.id = l.payment_id AND p.user_id = l.user_id
67
70
  LEFT JOIN brz_payment_details_token t ON p.id = t.payment_id AND p.user_id = t.user_id
68
71
  LEFT JOIN brz_payment_details_spark s ON p.id = s.payment_id AND p.user_id = s.user_id
72
+ LEFT JOIN brz_payment_details_deposit pd ON p.id = pd.payment_id AND p.user_id = pd.user_id
69
73
  LEFT JOIN brz_payment_metadata pm ON p.id = pm.payment_id AND p.user_id = pm.user_id
70
74
  LEFT JOIN brz_lnurl_receive_metadata lrm ON l.payment_hash = lrm.payment_hash AND l.user_id = lrm.user_id`;
71
75
 
@@ -235,6 +239,15 @@ class PostgresStorage {
235
239
  for (const paymentDetailsFilter of request.paymentDetailsFilter) {
236
240
  const paymentDetailsClauses = [];
237
241
 
242
+ // Base type check: ensure payment type matches the filter type
243
+ if (paymentDetailsFilter.type === "spark") {
244
+ paymentDetailsClauses.push("p.spark = true");
245
+ } else if (paymentDetailsFilter.type === "token") {
246
+ paymentDetailsClauses.push("p.spark IS NULL AND t.tx_hash IS NOT NULL");
247
+ } else if (paymentDetailsFilter.type === "lightning") {
248
+ paymentDetailsClauses.push("l.htlc_status IS NOT NULL");
249
+ }
250
+
238
251
  // Filter by HTLC status (Spark or Lightning)
239
252
  const htlcAlias =
240
253
  paymentDetailsFilter.type === "spark"
@@ -262,24 +275,35 @@ class PostgresStorage {
262
275
  params.push(...paymentDetailsFilter.htlcStatus);
263
276
  }
264
277
 
265
- // Filter by conversion refund needed
278
+ // Filter by conversion type + status
266
279
  if (
267
280
  (paymentDetailsFilter.type === "spark" ||
268
- paymentDetailsFilter.type === "token") &&
269
- paymentDetailsFilter.conversionRefundNeeded !== undefined
281
+ paymentDetailsFilter.type === "token" ||
282
+ paymentDetailsFilter.type === "lightning") &&
283
+ paymentDetailsFilter.conversionFilter !== undefined
270
284
  ) {
285
+ // Lightning carries the Boltz conversion; its `conversion_info.type`
286
+ // is the authoritative discriminator, so no payment-type column check.
271
287
  const typeCheck =
272
288
  paymentDetailsFilter.type === "spark"
273
289
  ? "p.spark = true"
274
- : "p.spark IS NULL";
275
- const refundNeeded =
276
- paymentDetailsFilter.conversionRefundNeeded === true
277
- ? "= 'refundNeeded'"
278
- : "!= 'refundNeeded'";
279
- paymentDetailsClauses.push(
280
- `${typeCheck} AND pm.conversion_info IS NOT NULL AND
281
- pm.conversion_info::jsonb->>'status' ${refundNeeded}`
282
- );
290
+ : paymentDetailsFilter.type === "token"
291
+ ? "p.spark IS NULL"
292
+ : null;
293
+ let statusClause;
294
+ if (paymentDetailsFilter.conversionFilter === "ammRefundNeeded") {
295
+ statusClause = "pm.conversion_info::jsonb->>'type' = 'amm' AND pm.conversion_info::jsonb->>'status' = 'refundNeeded'";
296
+ } else if (paymentDetailsFilter.conversionFilter === "orchestraPending") {
297
+ statusClause = "pm.conversion_info::jsonb->>'type' = 'orchestra' AND pm.conversion_info::jsonb->>'status' NOT IN ('completed', 'failed', 'refunded')";
298
+ } else if (paymentDetailsFilter.conversionFilter === "boltzPending") {
299
+ statusClause = "pm.conversion_info::jsonb->>'type' = 'boltz' AND pm.conversion_info::jsonb->>'status' NOT IN ('completed', 'failed', 'refunded')";
300
+ }
301
+ if (statusClause) {
302
+ const prefix = typeCheck ? `${typeCheck} AND ` : "";
303
+ paymentDetailsClauses.push(
304
+ `${prefix}pm.conversion_info IS NOT NULL AND ${statusClause}`
305
+ );
306
+ }
283
307
  }
284
308
 
285
309
  // Filter by token transaction hash
@@ -353,130 +377,192 @@ class PostgresStorage {
353
377
  }
354
378
  }
355
379
 
356
- async insertPayment(payment) {
357
- try {
358
- if (!payment) {
359
- throw new StorageError("Payment cannot be null or undefined");
360
- }
361
-
362
- await this._withTransaction(async (client) => {
363
- const withdrawTxId =
364
- payment.details?.type === "withdraw" ? payment.details.txId : null;
365
- const depositTxId =
366
- payment.details?.type === "deposit" ? payment.details.txId : null;
367
- const spark = payment.details?.type === "spark" ? true : null;
380
+ async applyPaymentUpdate(payment) {
381
+ if (!payment) {
382
+ throw new StorageError("Payment cannot be null or undefined");
383
+ }
368
384
 
385
+ try {
386
+ return await this._withTransaction(async (client) => {
387
+ const lockKey = this._paymentUpdateLockKey(payment.id);
369
388
  await client.query(
370
- `INSERT INTO brz_payments (user_id, id, payment_type, status, amount, fees, timestamp, method, withdraw_tx_id, deposit_tx_id, spark)
371
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
372
- ON CONFLICT(user_id, id) DO UPDATE SET
373
- payment_type=EXCLUDED.payment_type,
374
- status=EXCLUDED.status,
375
- amount=EXCLUDED.amount,
376
- fees=EXCLUDED.fees,
377
- timestamp=EXCLUDED.timestamp,
378
- method=EXCLUDED.method,
379
- withdraw_tx_id=EXCLUDED.withdraw_tx_id,
380
- deposit_tx_id=EXCLUDED.deposit_tx_id,
381
- spark=EXCLUDED.spark`,
382
- [
383
- this.identity,
384
- payment.id,
385
- payment.paymentType,
386
- payment.status,
387
- payment.amount.toString(),
388
- payment.fees.toString(),
389
- payment.timestamp,
390
- payment.method ? JSON.stringify(payment.method) : null,
391
- withdrawTxId,
392
- depositTxId,
393
- spark,
394
- ]
389
+ "SELECT pg_advisory_xact_lock($1::bigint)",
390
+ [lockKey]
395
391
  );
396
392
 
397
- if (
398
- payment.details?.type === "spark" &&
399
- (payment.details.invoiceDetails != null ||
400
- payment.details.htlcDetails != null)
401
- ) {
402
- await client.query(
403
- `INSERT INTO brz_payment_details_spark (user_id, payment_id, invoice_details, htlc_details)
404
- VALUES ($1, $2, $3, $4)
405
- ON CONFLICT(user_id, payment_id) DO UPDATE SET
406
- invoice_details=COALESCE(EXCLUDED.invoice_details, brz_payment_details_spark.invoice_details),
407
- htlc_details=COALESCE(EXCLUDED.htlc_details, brz_payment_details_spark.htlc_details)`,
408
- [
409
- this.identity,
410
- payment.id,
411
- payment.details.invoiceDetails
412
- ? JSON.stringify(payment.details.invoiceDetails)
413
- : null,
414
- payment.details.htlcDetails
415
- ? JSON.stringify(payment.details.htlcDetails)
416
- : null,
417
- ]
418
- );
419
- }
420
-
421
- if (payment.details?.type === "lightning") {
422
- await client.query(
423
- `INSERT INTO brz_payment_details_lightning
424
- (user_id, payment_id, invoice, payment_hash, destination_pubkey, description, preimage, htlc_status, htlc_expiry_time)
425
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
426
- ON CONFLICT(user_id, payment_id) DO UPDATE SET
427
- invoice=EXCLUDED.invoice,
428
- payment_hash=EXCLUDED.payment_hash,
429
- destination_pubkey=EXCLUDED.destination_pubkey,
430
- description=EXCLUDED.description,
431
- preimage=COALESCE(EXCLUDED.preimage, brz_payment_details_lightning.preimage),
432
- htlc_status=COALESCE(EXCLUDED.htlc_status, brz_payment_details_lightning.htlc_status),
433
- htlc_expiry_time=COALESCE(EXCLUDED.htlc_expiry_time, brz_payment_details_lightning.htlc_expiry_time)`,
434
- [
435
- this.identity,
436
- payment.id,
437
- payment.details.invoice,
438
- payment.details.htlcDetails.paymentHash,
439
- payment.details.destinationPubkey,
440
- payment.details.description,
441
- payment.details.htlcDetails?.preimage,
442
- payment.details.htlcDetails?.status ?? null,
443
- payment.details.htlcDetails?.expiryTime ?? 0,
444
- ]
393
+ const { rows } = await client.query(
394
+ "SELECT status FROM brz_payments WHERE user_id = $1 AND id = $2 FOR UPDATE",
395
+ [this.identity, payment.id]
396
+ );
397
+ const stored = rows.length > 0
398
+ ? this._normalizePaymentStatus(rows[0].status)
399
+ : null;
400
+ const next = this._normalizePaymentStatus(payment.status);
401
+
402
+ if (stored != null
403
+ && this._isFinalPaymentStatus(stored)
404
+ && stored !== next) {
405
+ console.warn(
406
+ `Skipping payment update (would replace terminal status): id=${payment.id} stored=${stored} new=${next}`
445
407
  );
408
+ return false;
446
409
  }
447
410
 
448
- if (payment.details?.type === "token") {
449
- await client.query(
450
- `INSERT INTO brz_payment_details_token
451
- (user_id, payment_id, metadata, tx_hash, tx_type, invoice_details)
452
- VALUES ($1, $2, $3, $4, $5, $6)
453
- ON CONFLICT(user_id, payment_id) DO UPDATE SET
454
- metadata=EXCLUDED.metadata,
455
- tx_hash=EXCLUDED.tx_hash,
456
- tx_type=EXCLUDED.tx_type,
457
- invoice_details=COALESCE(EXCLUDED.invoice_details, brz_payment_details_token.invoice_details)`,
458
- [
459
- this.identity,
460
- payment.id,
461
- JSON.stringify(payment.details.metadata),
462
- payment.details.txHash,
463
- payment.details.txType,
464
- payment.details.invoiceDetails
465
- ? JSON.stringify(payment.details.invoiceDetails)
466
- : null,
467
- ]
411
+ const sameStatus = stored === next;
412
+ if (sameStatus) {
413
+ console.debug(
414
+ `Skipping redundant payment event: id=${payment.id} status=${next}`
468
415
  );
469
416
  }
417
+ await this._runPaymentUpsert(client, payment);
418
+ return !sameStatus;
470
419
  });
471
420
  } catch (error) {
472
421
  if (error instanceof StorageError) throw error;
473
422
  throw new StorageError(
474
- `Failed to insert payment '${payment.id}': ${error.message}`,
423
+ `Failed to apply payment update '${payment.id}': ${error.message}`,
475
424
  error
476
425
  );
477
426
  }
478
427
  }
479
428
 
429
+ _paymentUpdateLockKey(paymentId) {
430
+ return crypto
431
+ .createHash("sha256")
432
+ .update("brz_payment_update")
433
+ .update(this.identity)
434
+ .update(Buffer.from(paymentId))
435
+ .digest()
436
+ .readBigInt64BE(0)
437
+ .toString();
438
+ }
439
+
440
+ async _runPaymentUpsert(client, payment) {
441
+ const withdrawTxId =
442
+ payment.details?.type === "withdraw" ? payment.details.txId : null;
443
+ const spark = payment.details?.type === "spark" ? true : null;
444
+
445
+ await client.query(
446
+ `INSERT INTO brz_payments (user_id, id, payment_type, status, amount, fees, timestamp, method, withdraw_tx_id, spark)
447
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
448
+ ON CONFLICT(user_id, id) DO UPDATE SET
449
+ payment_type=EXCLUDED.payment_type,
450
+ status=EXCLUDED.status,
451
+ amount=EXCLUDED.amount,
452
+ fees=EXCLUDED.fees,
453
+ timestamp=EXCLUDED.timestamp,
454
+ method=EXCLUDED.method,
455
+ withdraw_tx_id=EXCLUDED.withdraw_tx_id,
456
+ spark=EXCLUDED.spark`,
457
+ [
458
+ this.identity,
459
+ payment.id,
460
+ payment.paymentType,
461
+ payment.status,
462
+ payment.amount.toString(),
463
+ payment.fees.toString(),
464
+ payment.timestamp,
465
+ payment.method ? JSON.stringify(payment.method) : null,
466
+ withdrawTxId,
467
+ spark,
468
+ ]
469
+ );
470
+
471
+ if (payment.details?.type === "deposit") {
472
+ await client.query(
473
+ `INSERT INTO brz_payment_details_deposit (user_id, payment_id, tx_id, vout)
474
+ VALUES ($1, $2, $3, $4)
475
+ ON CONFLICT(user_id, payment_id) DO UPDATE SET
476
+ tx_id=EXCLUDED.tx_id,
477
+ vout=EXCLUDED.vout`,
478
+ [this.identity, payment.id, payment.details.txId, payment.details.vout]
479
+ );
480
+ }
481
+
482
+ if (
483
+ payment.details?.type === "spark" &&
484
+ (payment.details.invoiceDetails != null ||
485
+ payment.details.htlcDetails != null)
486
+ ) {
487
+ await client.query(
488
+ `INSERT INTO brz_payment_details_spark (user_id, payment_id, invoice_details, htlc_details)
489
+ VALUES ($1, $2, $3, $4)
490
+ ON CONFLICT(user_id, payment_id) DO UPDATE SET
491
+ invoice_details=COALESCE(EXCLUDED.invoice_details, brz_payment_details_spark.invoice_details),
492
+ htlc_details=COALESCE(EXCLUDED.htlc_details, brz_payment_details_spark.htlc_details)`,
493
+ [
494
+ this.identity,
495
+ payment.id,
496
+ payment.details.invoiceDetails
497
+ ? JSON.stringify(payment.details.invoiceDetails)
498
+ : null,
499
+ payment.details.htlcDetails
500
+ ? JSON.stringify(payment.details.htlcDetails)
501
+ : null,
502
+ ]
503
+ );
504
+ }
505
+
506
+ if (payment.details?.type === "lightning") {
507
+ await client.query(
508
+ `INSERT INTO brz_payment_details_lightning
509
+ (user_id, payment_id, invoice, payment_hash, destination_pubkey, description, preimage, htlc_status, htlc_expiry_time)
510
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
511
+ ON CONFLICT(user_id, payment_id) DO UPDATE SET
512
+ invoice=EXCLUDED.invoice,
513
+ payment_hash=EXCLUDED.payment_hash,
514
+ destination_pubkey=EXCLUDED.destination_pubkey,
515
+ description=EXCLUDED.description,
516
+ preimage=COALESCE(EXCLUDED.preimage, brz_payment_details_lightning.preimage),
517
+ htlc_status=COALESCE(EXCLUDED.htlc_status, brz_payment_details_lightning.htlc_status),
518
+ htlc_expiry_time=COALESCE(EXCLUDED.htlc_expiry_time, brz_payment_details_lightning.htlc_expiry_time)`,
519
+ [
520
+ this.identity,
521
+ payment.id,
522
+ payment.details.invoice,
523
+ payment.details.htlcDetails.paymentHash,
524
+ payment.details.destinationPubkey,
525
+ payment.details.description,
526
+ payment.details.htlcDetails?.preimage,
527
+ payment.details.htlcDetails?.status ?? null,
528
+ payment.details.htlcDetails?.expiryTime ?? 0,
529
+ ]
530
+ );
531
+ }
532
+
533
+ if (payment.details?.type === "token") {
534
+ await client.query(
535
+ `INSERT INTO brz_payment_details_token
536
+ (user_id, payment_id, metadata, tx_hash, tx_type, invoice_details)
537
+ VALUES ($1, $2, $3, $4, $5, $6)
538
+ ON CONFLICT(user_id, payment_id) DO UPDATE SET
539
+ metadata=EXCLUDED.metadata,
540
+ tx_hash=EXCLUDED.tx_hash,
541
+ tx_type=EXCLUDED.tx_type,
542
+ invoice_details=COALESCE(EXCLUDED.invoice_details, brz_payment_details_token.invoice_details)`,
543
+ [
544
+ this.identity,
545
+ payment.id,
546
+ JSON.stringify(payment.details.metadata),
547
+ payment.details.txHash,
548
+ payment.details.txType,
549
+ payment.details.invoiceDetails
550
+ ? JSON.stringify(payment.details.invoiceDetails)
551
+ : null,
552
+ ]
553
+ );
554
+ }
555
+ }
556
+
557
+ _normalizePaymentStatus(status) {
558
+ return typeof status === "string" ? status.toLowerCase() : status;
559
+ }
560
+
561
+ _isFinalPaymentStatus(status) {
562
+ const normalized = this._normalizePaymentStatus(status);
563
+ return normalized === "completed" || normalized === "failed";
564
+ }
565
+
480
566
  async getPaymentById(id) {
481
567
  try {
482
568
  if (!id) {
@@ -769,6 +855,13 @@ class PostgresStorage {
769
855
  senderComment: row.lnurl_sender_comment || null,
770
856
  };
771
857
  }
858
+
859
+ if (row.conversion_info) {
860
+ details.conversionInfo =
861
+ typeof row.conversion_info === "string"
862
+ ? JSON.parse(row.conversion_info)
863
+ : row.conversion_info;
864
+ }
772
865
  } else if (row.withdraw_tx_id) {
773
866
  details = {
774
867
  type: "withdraw",
@@ -778,6 +871,7 @@ class PostgresStorage {
778
871
  details = {
779
872
  type: "deposit",
780
873
  txId: row.deposit_tx_id,
874
+ vout: Number(row.deposit_vout),
781
875
  };
782
876
  } else if (row.spark) {
783
877
  details = {
@@ -950,6 +1044,74 @@ class PostgresStorage {
950
1044
  }
951
1045
  }
952
1046
 
1047
+ // ===== Cross-Chain Swap Operations =====
1048
+
1049
+ async setCrossChainSwap(swap) {
1050
+ try {
1051
+ await this.pool.query(
1052
+ `INSERT INTO brz_cross_chain_swaps
1053
+ (user_id, provider, id, is_terminal, updated_at, data, secrets)
1054
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
1055
+ ON CONFLICT(user_id, provider, id) DO UPDATE SET
1056
+ is_terminal = EXCLUDED.is_terminal,
1057
+ updated_at = EXCLUDED.updated_at,
1058
+ data = EXCLUDED.data,
1059
+ secrets = EXCLUDED.secrets`,
1060
+ [
1061
+ this.identity,
1062
+ swap.provider,
1063
+ swap.id,
1064
+ swap.isTerminal,
1065
+ swap.updatedAt,
1066
+ swap.data,
1067
+ swap.secrets,
1068
+ ]
1069
+ );
1070
+ } catch (error) {
1071
+ throw new StorageError(
1072
+ `Failed to set cross-chain swap: ${error.message}`,
1073
+ error
1074
+ );
1075
+ }
1076
+ }
1077
+
1078
+ async getCrossChainSwap(provider, id) {
1079
+ try {
1080
+ const result = await this.pool.query(
1081
+ `SELECT provider, id, is_terminal, updated_at, data, secrets
1082
+ FROM brz_cross_chain_swaps
1083
+ WHERE user_id = $1 AND provider = $2 AND id = $3`,
1084
+ [this.identity, provider, id]
1085
+ );
1086
+ if (result.rows.length === 0) {
1087
+ return null;
1088
+ }
1089
+ return crossChainSwapFromRow(result.rows[0]);
1090
+ } catch (error) {
1091
+ throw new StorageError(
1092
+ `Failed to get cross-chain swap: ${error.message}`,
1093
+ error
1094
+ );
1095
+ }
1096
+ }
1097
+
1098
+ async listActiveCrossChainSwaps(provider) {
1099
+ try {
1100
+ const result = await this.pool.query(
1101
+ `SELECT provider, id, is_terminal, updated_at, data, secrets
1102
+ FROM brz_cross_chain_swaps
1103
+ WHERE user_id = $1 AND provider = $2 AND is_terminal = FALSE`,
1104
+ [this.identity, provider]
1105
+ );
1106
+ return result.rows.map(crossChainSwapFromRow);
1107
+ } catch (error) {
1108
+ throw new StorageError(
1109
+ `Failed to list active cross-chain swaps: ${error.message}`,
1110
+ error
1111
+ );
1112
+ }
1113
+ }
1114
+
953
1115
  // ===== Sync Operations =====
954
1116
 
955
1117
  async syncAddOutgoingChange(record) {
@@ -1376,6 +1538,21 @@ class PostgresStorage {
1376
1538
  }
1377
1539
  }
1378
1540
 
1541
+ /**
1542
+ * Maps a brz_cross_chain_swaps row to the camelCase StoredCrossChainSwap shape
1543
+ * the SDK expects.
1544
+ */
1545
+ function crossChainSwapFromRow(row) {
1546
+ return {
1547
+ provider: row.provider,
1548
+ id: row.id,
1549
+ isTerminal: row.is_terminal,
1550
+ updatedAt: Number(row.updated_at),
1551
+ data: row.data,
1552
+ secrets: row.secrets,
1553
+ };
1554
+ }
1555
+
1379
1556
  /**
1380
1557
  * Creates a PostgresStorageConfig with the given connection string and default pool settings.
1381
1558
  *
@@ -467,6 +467,57 @@ class PostgresMigrationManager {
467
467
  ON brz_sync_incoming(user_id, revision)`,
468
468
  ],
469
469
  },
470
+ {
471
+ // Move deposit details into their own table so vout can be NOT NULL and
472
+ // the schema matches brz_payment_details_lightning / _token / _spark. We
473
+ // can't safely backfill the new table from the dropped deposit_tx_id
474
+ // column: we never stored the original SSP output_index, and vout=0 is a
475
+ // valid output index, so defaulting would silently mislabel. Drop the
476
+ // column and leave the brz_payments row in place. The read path sees an
477
+ // unjoined deposit row as `details: None` until the resync re-fetches the
478
+ // SSP user_request and the upsert inserts the new details row.
479
+ name: "Move deposit details into brz_payment_details_deposit table",
480
+ sql: [
481
+ `CREATE TABLE IF NOT EXISTS brz_payment_details_deposit (
482
+ user_id BYTEA NOT NULL,
483
+ payment_id TEXT NOT NULL,
484
+ tx_id TEXT NOT NULL,
485
+ vout BIGINT NOT NULL,
486
+ PRIMARY KEY (user_id, payment_id)
487
+ )`,
488
+ `ALTER TABLE brz_payments DROP COLUMN IF EXISTS deposit_tx_id`,
489
+ `UPDATE brz_settings
490
+ SET value = jsonb_set(value::jsonb, '{offset}', '0')::text
491
+ WHERE key = 'sync_offset' AND value IS NOT NULL`,
492
+ ],
493
+ },
494
+ {
495
+ name: "Backfill conversion_info type discriminator",
496
+ sql: [
497
+ `UPDATE brz_payment_metadata SET conversion_info = conversion_info::jsonb || '{"type": "amm"}'::jsonb WHERE conversion_info IS NOT NULL AND conversion_info::jsonb->>'type' IS NULL`,
498
+ ],
499
+ },
500
+ {
501
+ // Cross-chain swap rows, synced for cross-instance recovery. Shared
502
+ // across providers, discriminated by `provider`. Born multi-tenant
503
+ // (user_id in the PK). `data` is provider-opaque JSON; `secrets` is
504
+ // provider-opaque ciphertext (empty when the provider has none).
505
+ name: "Create brz_cross_chain_swaps table",
506
+ sql: [
507
+ `CREATE TABLE IF NOT EXISTS brz_cross_chain_swaps (
508
+ user_id BYTEA NOT NULL,
509
+ provider TEXT NOT NULL,
510
+ id TEXT NOT NULL,
511
+ is_terminal BOOLEAN NOT NULL,
512
+ updated_at BIGINT NOT NULL,
513
+ data TEXT NOT NULL,
514
+ secrets TEXT NOT NULL,
515
+ PRIMARY KEY (user_id, provider, id)
516
+ )`,
517
+ `CREATE INDEX brz_idx_cross_chain_swaps_user_provider_is_terminal
518
+ ON brz_cross_chain_swaps(user_id, provider, is_terminal)`,
519
+ ],
520
+ },
470
521
  ];
471
522
  }
472
523
  }