@breeztech/breez-sdk-spark 0.15.0 → 0.16.1-dev1

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 +511 -215
  3. package/bundler/breez_sdk_spark_wasm.js +1 -1
  4. package/bundler/breez_sdk_spark_wasm_bg.js +567 -414
  5. package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
  6. package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +55 -46
  7. package/bundler/storage/index.js +205 -15
  8. package/deno/breez_sdk_spark_wasm.d.ts +511 -215
  9. package/deno/breez_sdk_spark_wasm.js +567 -414
  10. package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
  11. package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +55 -46
  12. package/nodejs/breez_sdk_spark_wasm.d.ts +511 -215
  13. package/nodejs/breez_sdk_spark_wasm.js +578 -421
  14. package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
  15. package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +55 -46
  16. package/nodejs/index.js +10 -10
  17. package/nodejs/index.mjs +12 -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 +229 -111
  23. package/nodejs/mysql-storage/migrations.cjs +37 -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 +174 -107
  34. package/nodejs/postgres-storage/migrations.cjs +24 -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 +167 -113
  38. package/nodejs/storage/migrations.cjs +23 -0
  39. package/package.json +6 -1
  40. package/ssr/index.js +52 -28
  41. package/web/breez_sdk_spark_wasm.d.ts +566 -261
  42. package/web/breez_sdk_spark_wasm.js +567 -414
  43. package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
  44. package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +55 -46
  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 +205 -15
  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
@@ -16,6 +16,8 @@
16
16
  * → `conn.beginTransaction()`/`conn.commit()`/`conn.rollback()`.
17
17
  */
18
18
 
19
+ const crypto = require("crypto");
20
+
19
21
  let mysql;
20
22
  try {
21
23
  const mainModule = require.main;
@@ -38,6 +40,8 @@ try {
38
40
  const { StorageError } = require("./errors.cjs");
39
41
  const { MysqlMigrationManager } = require("./migrations.cjs");
40
42
 
43
+ const PAYMENT_UPDATE_LOCK_TIMEOUT_SECS = 10;
44
+
41
45
  /**
42
46
  * Base query for payment lookups. All columns are accessed by name in _rowToPayment.
43
47
  * parent_payment_id is only used by getPaymentsByParentIds.
@@ -51,7 +55,8 @@ const SELECT_PAYMENT_SQL = `
51
55
  p.timestamp,
52
56
  p.method,
53
57
  p.withdraw_tx_id,
54
- p.deposit_tx_id,
58
+ pd.tx_id AS deposit_tx_id,
59
+ pd.vout AS deposit_vout,
55
60
  p.spark,
56
61
  l.invoice AS lightning_invoice,
57
62
  l.payment_hash AS lightning_payment_hash,
@@ -79,6 +84,7 @@ const SELECT_PAYMENT_SQL = `
79
84
  LEFT JOIN brz_payment_details_lightning l ON p.id = l.payment_id AND p.user_id = l.user_id
80
85
  LEFT JOIN brz_payment_details_token t ON p.id = t.payment_id AND p.user_id = t.user_id
81
86
  LEFT JOIN brz_payment_details_spark s ON p.id = s.payment_id AND p.user_id = s.user_id
87
+ LEFT JOIN brz_payment_details_deposit pd ON p.id = pd.payment_id AND p.user_id = pd.user_id
82
88
  LEFT JOIN brz_payment_metadata pm ON p.id = pm.payment_id AND p.user_id = pm.user_id
83
89
  LEFT JOIN brz_lnurl_receive_metadata lrm ON l.payment_hash = lrm.payment_hash AND l.user_id = lrm.user_id`;
84
90
 
@@ -360,130 +366,237 @@ class MysqlStorage {
360
366
  }
361
367
  }
362
368
 
363
- async insertPayment(payment) {
369
+ async applyPaymentUpdate(payment) {
370
+ if (!payment) {
371
+ throw new StorageError("Payment cannot be null or undefined");
372
+ }
373
+
374
+ let conn = null;
375
+ const lockName = this._paymentUpdateLockName(payment.id);
376
+ let acquired = false;
377
+ let shouldEmit = false;
378
+ let operationError = null;
379
+ let releaseError = null;
380
+
364
381
  try {
365
- if (!payment) {
366
- throw new StorageError("Payment cannot be null or undefined");
382
+ conn = await this.pool.getConnection();
383
+ const [lockRows] = await conn.query("SELECT GET_LOCK(?, ?) AS acquired", [
384
+ lockName,
385
+ PAYMENT_UPDATE_LOCK_TIMEOUT_SECS,
386
+ ]);
387
+ acquired = Number(lockRows?.[0]?.acquired) === 1;
388
+ if (!acquired) {
389
+ throw new StorageError(`Timed out acquiring payment update lock '${lockName}'`);
367
390
  }
368
391
 
369
- await this._withTransaction(async (conn) => {
370
- const withdrawTxId =
371
- payment.details?.type === "withdraw" ? payment.details.txId : null;
372
- const depositTxId =
373
- payment.details?.type === "deposit" ? payment.details.txId : null;
374
- const spark = payment.details?.type === "spark" ? 1 : null;
375
-
376
- await conn.query(
377
- `INSERT INTO brz_payments (user_id, id, payment_type, status, amount, fees, timestamp, method, withdraw_tx_id, deposit_tx_id, spark)
378
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
379
- ON DUPLICATE KEY UPDATE
380
- payment_type=VALUES(payment_type),
381
- status=VALUES(status),
382
- amount=VALUES(amount),
383
- fees=VALUES(fees),
384
- timestamp=VALUES(timestamp),
385
- method=VALUES(method),
386
- withdraw_tx_id=VALUES(withdraw_tx_id),
387
- deposit_tx_id=VALUES(deposit_tx_id),
388
- spark=VALUES(spark)`,
389
- [
390
- this.identity,
391
- payment.id,
392
- payment.paymentType,
393
- payment.status,
394
- payment.amount.toString(),
395
- payment.fees.toString(),
396
- payment.timestamp,
397
- payment.method ? JSON.stringify(payment.method) : null,
398
- withdrawTxId,
399
- depositTxId,
400
- spark,
401
- ]
392
+ await conn.query("SET TRANSACTION ISOLATION LEVEL READ COMMITTED");
393
+ await conn.beginTransaction();
394
+ try {
395
+ const [rows] = await conn.query(
396
+ "SELECT status FROM brz_payments WHERE user_id = ? AND id = ? FOR UPDATE",
397
+ [this.identity, payment.id]
402
398
  );
403
-
404
- if (
405
- payment.details?.type === "spark" &&
406
- (payment.details.invoiceDetails != null ||
407
- payment.details.htlcDetails != null)
408
- ) {
409
- await conn.query(
410
- `INSERT INTO brz_payment_details_spark (user_id, payment_id, invoice_details, htlc_details)
411
- VALUES (?, ?, ?, ?)
412
- ON DUPLICATE KEY UPDATE
413
- invoice_details=COALESCE(VALUES(invoice_details), invoice_details),
414
- htlc_details=COALESCE(VALUES(htlc_details), htlc_details)`,
415
- [
416
- this.identity,
417
- payment.id,
418
- payment.details.invoiceDetails
419
- ? JSON.stringify(payment.details.invoiceDetails)
420
- : null,
421
- payment.details.htlcDetails
422
- ? JSON.stringify(payment.details.htlcDetails)
423
- : null,
424
- ]
399
+ const stored = rows.length > 0
400
+ ? this._normalizePaymentStatus(rows[0].status)
401
+ : null;
402
+ const next = this._normalizePaymentStatus(payment.status);
403
+
404
+ if (stored == null) {
405
+ await this._runPaymentUpsert(conn, payment);
406
+ shouldEmit = true;
407
+ } else if (stored === next) {
408
+ console.debug(
409
+ `Skipping redundant payment event: id=${payment.id} status=${next}`
425
410
  );
426
- }
427
-
428
- if (payment.details?.type === "lightning") {
429
- await conn.query(
430
- `INSERT INTO brz_payment_details_lightning
431
- (user_id, payment_id, invoice, payment_hash, destination_pubkey, description, preimage, htlc_status, htlc_expiry_time)
432
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
433
- ON DUPLICATE KEY UPDATE
434
- invoice=VALUES(invoice),
435
- payment_hash=VALUES(payment_hash),
436
- destination_pubkey=VALUES(destination_pubkey),
437
- description=VALUES(description),
438
- preimage=COALESCE(VALUES(preimage), preimage),
439
- htlc_status=COALESCE(VALUES(htlc_status), htlc_status),
440
- htlc_expiry_time=COALESCE(VALUES(htlc_expiry_time), htlc_expiry_time)`,
441
- [
442
- this.identity,
443
- payment.id,
444
- payment.details.invoice,
445
- payment.details.htlcDetails.paymentHash,
446
- payment.details.destinationPubkey,
447
- payment.details.description,
448
- payment.details.htlcDetails?.preimage,
449
- payment.details.htlcDetails?.status ?? null,
450
- payment.details.htlcDetails?.expiryTime ?? 0,
451
- ]
411
+ await this._runPaymentUpsert(conn, payment);
412
+ shouldEmit = false;
413
+ } else if (this._isFinalPaymentStatus(stored)) {
414
+ console.warn(
415
+ `Skipping payment update (would replace terminal status): id=${payment.id} stored=${stored} new=${next}`
452
416
  );
417
+ shouldEmit = false;
418
+ } else {
419
+ await this._runPaymentUpsert(conn, payment);
420
+ shouldEmit = true;
453
421
  }
454
422
 
455
- if (payment.details?.type === "token") {
456
- await conn.query(
457
- `INSERT INTO brz_payment_details_token
458
- (user_id, payment_id, metadata, tx_hash, tx_type, invoice_details)
459
- VALUES (?, ?, ?, ?, ?, ?)
460
- ON DUPLICATE KEY UPDATE
461
- metadata=VALUES(metadata),
462
- tx_hash=VALUES(tx_hash),
463
- tx_type=VALUES(tx_type),
464
- invoice_details=COALESCE(VALUES(invoice_details), invoice_details)`,
465
- [
466
- this.identity,
467
- payment.id,
468
- JSON.stringify(payment.details.metadata),
469
- payment.details.txHash,
470
- payment.details.txType,
471
- payment.details.invoiceDetails
472
- ? JSON.stringify(payment.details.invoiceDetails)
473
- : null,
474
- ]
475
- );
476
- }
477
- });
423
+ await conn.commit();
424
+ } catch (error) {
425
+ await conn.rollback().catch(() => {});
426
+ throw error;
427
+ }
478
428
  } catch (error) {
479
- if (error instanceof StorageError) throw error;
429
+ operationError = error;
430
+ } finally {
431
+ if (conn && acquired) {
432
+ try {
433
+ await conn.query("SELECT RELEASE_LOCK(?)", [lockName]);
434
+ } catch (error) {
435
+ releaseError = error;
436
+ }
437
+ }
438
+ if (conn) {
439
+ conn.release();
440
+ }
441
+ }
442
+
443
+ if (operationError) {
444
+ if (operationError instanceof StorageError) {
445
+ throw operationError;
446
+ }
480
447
  throw new StorageError(
481
- `Failed to insert payment '${payment.id}': ${error.message}`,
482
- error
448
+ `Failed to apply payment update '${payment.id}': ${operationError.message}`,
449
+ operationError
483
450
  );
484
451
  }
452
+
453
+ if (releaseError) {
454
+ throw new StorageError(
455
+ `Failed to release payment update lock '${lockName}': ${releaseError.message}`,
456
+ releaseError
457
+ );
458
+ }
459
+
460
+ return shouldEmit;
485
461
  }
486
462
 
463
+ async _runPaymentUpsert(conn, payment) {
464
+ const withdrawTxId =
465
+ payment.details?.type === "withdraw" ? payment.details.txId : null;
466
+ const spark = payment.details?.type === "spark" ? 1 : null;
467
+
468
+ await conn.query(
469
+ `INSERT INTO brz_payments (user_id, id, payment_type, status, amount, fees, timestamp, method, withdraw_tx_id, spark)
470
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
471
+ ON DUPLICATE KEY UPDATE
472
+ payment_type=VALUES(payment_type),
473
+ status=VALUES(status),
474
+ amount=VALUES(amount),
475
+ fees=VALUES(fees),
476
+ timestamp=VALUES(timestamp),
477
+ method=VALUES(method),
478
+ withdraw_tx_id=VALUES(withdraw_tx_id),
479
+ spark=VALUES(spark)`,
480
+ [
481
+ this.identity,
482
+ payment.id,
483
+ payment.paymentType,
484
+ payment.status,
485
+ payment.amount.toString(),
486
+ payment.fees.toString(),
487
+ payment.timestamp,
488
+ payment.method ? JSON.stringify(payment.method) : null,
489
+ withdrawTxId,
490
+ spark,
491
+ ]
492
+ );
493
+
494
+ if (payment.details?.type === "deposit") {
495
+ await conn.query(
496
+ `INSERT INTO brz_payment_details_deposit (user_id, payment_id, tx_id, vout)
497
+ VALUES (?, ?, ?, ?)
498
+ ON DUPLICATE KEY UPDATE
499
+ tx_id=VALUES(tx_id),
500
+ vout=VALUES(vout)`,
501
+ [this.identity, payment.id, payment.details.txId, payment.details.vout]
502
+ );
503
+ }
504
+
505
+ if (
506
+ payment.details?.type === "spark" &&
507
+ (payment.details.invoiceDetails != null ||
508
+ payment.details.htlcDetails != null)
509
+ ) {
510
+ await conn.query(
511
+ `INSERT INTO brz_payment_details_spark (user_id, payment_id, invoice_details, htlc_details)
512
+ VALUES (?, ?, ?, ?)
513
+ ON DUPLICATE KEY UPDATE
514
+ invoice_details=COALESCE(VALUES(invoice_details), invoice_details),
515
+ htlc_details=COALESCE(VALUES(htlc_details), htlc_details)`,
516
+ [
517
+ this.identity,
518
+ payment.id,
519
+ payment.details.invoiceDetails
520
+ ? JSON.stringify(payment.details.invoiceDetails)
521
+ : null,
522
+ payment.details.htlcDetails
523
+ ? JSON.stringify(payment.details.htlcDetails)
524
+ : null,
525
+ ]
526
+ );
527
+ }
528
+
529
+ if (payment.details?.type === "lightning") {
530
+ await conn.query(
531
+ `INSERT INTO brz_payment_details_lightning
532
+ (user_id, payment_id, invoice, payment_hash, destination_pubkey, description, preimage, htlc_status, htlc_expiry_time)
533
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
534
+ ON DUPLICATE KEY UPDATE
535
+ invoice=VALUES(invoice),
536
+ payment_hash=VALUES(payment_hash),
537
+ destination_pubkey=VALUES(destination_pubkey),
538
+ description=VALUES(description),
539
+ preimage=COALESCE(VALUES(preimage), preimage),
540
+ htlc_status=COALESCE(VALUES(htlc_status), htlc_status),
541
+ htlc_expiry_time=COALESCE(VALUES(htlc_expiry_time), htlc_expiry_time)`,
542
+ [
543
+ this.identity,
544
+ payment.id,
545
+ payment.details.invoice,
546
+ payment.details.htlcDetails.paymentHash,
547
+ payment.details.destinationPubkey,
548
+ payment.details.description,
549
+ payment.details.htlcDetails?.preimage,
550
+ payment.details.htlcDetails?.status ?? null,
551
+ payment.details.htlcDetails?.expiryTime ?? 0,
552
+ ]
553
+ );
554
+ }
555
+
556
+ if (payment.details?.type === "token") {
557
+ await conn.query(
558
+ `INSERT INTO brz_payment_details_token
559
+ (user_id, payment_id, metadata, tx_hash, tx_type, invoice_details)
560
+ VALUES (?, ?, ?, ?, ?, ?)
561
+ ON DUPLICATE KEY UPDATE
562
+ metadata=VALUES(metadata),
563
+ tx_hash=VALUES(tx_hash),
564
+ tx_type=VALUES(tx_type),
565
+ invoice_details=COALESCE(VALUES(invoice_details), invoice_details)`,
566
+ [
567
+ this.identity,
568
+ payment.id,
569
+ JSON.stringify(payment.details.metadata),
570
+ payment.details.txHash,
571
+ payment.details.txType,
572
+ payment.details.invoiceDetails
573
+ ? JSON.stringify(payment.details.invoiceDetails)
574
+ : null,
575
+ ]
576
+ );
577
+ }
578
+ }
579
+
580
+ _paymentUpdateLockName(paymentId) {
581
+ return crypto
582
+ .createHash("sha256")
583
+ .update("brz_payment_update")
584
+ .update(this.identity)
585
+ .update(Buffer.from(paymentId))
586
+ .digest("hex")
587
+ .slice(0, 32);
588
+ }
589
+
590
+ _normalizePaymentStatus(status) {
591
+ return typeof status === "string" ? status.toLowerCase() : status;
592
+ }
593
+
594
+ _isFinalPaymentStatus(status) {
595
+ const normalized = this._normalizePaymentStatus(status);
596
+ return normalized === "completed" || normalized === "failed";
597
+ }
598
+
599
+
487
600
  async getPaymentById(id) {
488
601
  try {
489
602
  if (!id) {
@@ -773,6 +886,7 @@ class MysqlStorage {
773
886
  details = {
774
887
  type: "deposit",
775
888
  txId: row.deposit_tx_id,
889
+ vout: Number(row.deposit_vout),
776
890
  };
777
891
  } else if (toBool(row.spark)) {
778
892
  details = {
@@ -1332,6 +1446,10 @@ function createMysqlPool(config) {
1332
1446
  connectTimeout: (config.createTimeoutSecs || 0) * 1000 || 10000,
1333
1447
  idleTimeout: (config.recycleTimeoutSecs || 0) * 1000 || 10000,
1334
1448
  waitForConnections: true,
1449
+ // Serialize JS `Date` parameters as UTC strings rather than host-local
1450
+ // time. Paired with explicit `UTC_TIMESTAMP(6)` on the server side, this
1451
+ // keeps timestamp comparisons consistent regardless of the host TZ.
1452
+ timezone: "Z",
1335
1453
  });
1336
1454
  }
1337
1455
 
@@ -89,7 +89,7 @@ class MysqlMigrationManager {
89
89
  await conn.query(`
90
90
  CREATE TABLE IF NOT EXISTS brz_schema_migrations (
91
91
  version INT PRIMARY KEY,
92
- applied_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
92
+ applied_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))
93
93
  )
94
94
  `);
95
95
 
@@ -121,7 +121,7 @@ class MysqlMigrationManager {
121
121
  }
122
122
 
123
123
  await conn.query(
124
- "INSERT INTO brz_schema_migrations (version) VALUES (?)",
124
+ "INSERT INTO brz_schema_migrations (version, applied_at) VALUES (?, UTC_TIMESTAMP(6))",
125
125
  [version]
126
126
  );
127
127
  }
@@ -497,6 +497,41 @@ class MysqlMigrationManager {
497
497
  ON brz_sync_incoming(user_id, revision)`,
498
498
  ],
499
499
  },
500
+ {
501
+ // Pin the migration-tracking table's `applied_at` default to UTC.
502
+ // The migration manager already passes `UTC_TIMESTAMP(6)` explicitly
503
+ // on INSERT, but aligning the default keeps `SHOW CREATE TABLE`
504
+ // output consistent with the token-store / tree-store migrations
505
+ // table and avoids future mistakes if a callsite omits the column.
506
+ name: "Pin schema-migrations applied_at default to UTC",
507
+ sql: [
508
+ `ALTER TABLE brz_schema_migrations MODIFY COLUMN applied_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))`,
509
+ ],
510
+ },
511
+ {
512
+ // Move deposit details into their own table so vout can be NOT NULL and
513
+ // the schema matches brz_payment_details_lightning / _token / _spark. We
514
+ // can't safely backfill the new table from the dropped deposit_tx_id
515
+ // column: we never stored the original SSP output_index, and vout=0 is a
516
+ // valid output index — defaulting would silently mislabel. Drop the
517
+ // column and leave the brz_payments row in place. The read path sees an
518
+ // unjoined deposit row as `details: None` until the resync re-fetches the
519
+ // SSP user_request and the upsert inserts the new details row.
520
+ name: "Move deposit details into brz_payment_details_deposit table",
521
+ sql: [
522
+ `CREATE TABLE IF NOT EXISTS brz_payment_details_deposit (
523
+ user_id VARBINARY(33) NOT NULL,
524
+ payment_id VARCHAR(255) NOT NULL,
525
+ tx_id VARCHAR(255) NOT NULL,
526
+ vout INT UNSIGNED NOT NULL,
527
+ PRIMARY KEY (user_id, payment_id)
528
+ )`,
529
+ `ALTER TABLE brz_payments DROP COLUMN deposit_tx_id`,
530
+ `UPDATE brz_settings
531
+ SET value = JSON_SET(value, '$.offset', 0)
532
+ WHERE \`key\` = 'sync_offset' AND value IS NOT NULL`,
533
+ ],
534
+ },
500
535
  ];
501
536
  }
502
537
  }