@breeztech/breez-sdk-spark 0.15.1 → 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
@@ -1,7 +1,7 @@
1
1
  /**
2
- * CommonJS implementation for Node.js PostgreSQL Session Manager.
2
+ * CommonJS implementation for Node.js PostgreSQL Session Store.
3
3
  *
4
- * Implements the JS-side `SessionManager` interface consumed by the Breez
4
+ * Implements the JS-side `SessionStore` interface consumed by the Breez
5
5
  * SDK WASM bindings: `getSession(serviceIdentityKey)` returns the cached
6
6
  * session for the (tenant, service) pair or `null` when not found, and
7
7
  * `setSession(serviceIdentityKey, session)` upserts a session.
@@ -29,10 +29,10 @@ try {
29
29
  }
30
30
  }
31
31
 
32
- const { SessionManagerError } = require("./errors.cjs");
33
- const { SessionManagerMigrationManager } = require("./migrations.cjs");
32
+ const { SessionStoreError } = require("./errors.cjs");
33
+ const { SessionStoreMigrationManager } = require("./migrations.cjs");
34
34
 
35
- class PostgresSessionManager {
35
+ class PostgresSessionStore {
36
36
  /**
37
37
  * @param {import('pg').Pool} pool
38
38
  * @param {Buffer|Uint8Array} identity - 33-byte secp256k1 compressed pubkey
@@ -41,7 +41,7 @@ class PostgresSessionManager {
41
41
  */
42
42
  constructor(pool, identity, logger = null, runMigration = true) {
43
43
  if (!identity || identity.length !== 33) {
44
- throw new SessionManagerError(
44
+ throw new SessionStoreError(
45
45
  "tenant identity (33-byte secp256k1 pubkey) is required"
46
46
  );
47
47
  }
@@ -54,13 +54,13 @@ class PostgresSessionManager {
54
54
  async initialize() {
55
55
  try {
56
56
  if (this.runMigration) {
57
- const migrationManager = new SessionManagerMigrationManager(this.logger);
57
+ const migrationManager = new SessionStoreMigrationManager(this.logger);
58
58
  await migrationManager.migrate(this.pool);
59
59
  }
60
60
  return this;
61
61
  } catch (error) {
62
- throw new SessionManagerError(
63
- `Failed to initialize PostgreSQL session manager: ${error.message}`,
62
+ throw new SessionStoreError(
63
+ `Failed to initialize PostgreSQL session store: ${error.message}`,
64
64
  error
65
65
  );
66
66
  }
@@ -76,7 +76,7 @@ class PostgresSessionManager {
76
76
  /**
77
77
  * Returns the cached session for the given service identity key, or `null`
78
78
  * if no session is cached. The Rust adapter maps `null` to
79
- * `SessionManagerError::NotFound`.
79
+ * `SessionStoreError::NotFound`.
80
80
  * @param {string} serviceIdentityKey - hex-encoded 33-byte secp256k1 pubkey
81
81
  * @returns {Promise<{token: string, expiration: number} | null>}
82
82
  */
@@ -97,7 +97,7 @@ class PostgresSessionManager {
97
97
  expiration: Number(row.expiration),
98
98
  };
99
99
  } catch (error) {
100
- throw new SessionManagerError(
100
+ throw new SessionStoreError(
101
101
  `Failed to read session: ${error.message}`,
102
102
  error
103
103
  );
@@ -120,7 +120,7 @@ class PostgresSessionManager {
120
120
  [this.identity, serviceKey, session.token, session.expiration]
121
121
  );
122
122
  } catch (error) {
123
- throw new SessionManagerError(
123
+ throw new SessionStoreError(
124
124
  `Failed to write session: ${error.message}`,
125
125
  error
126
126
  );
@@ -130,7 +130,7 @@ class PostgresSessionManager {
130
130
 
131
131
  function _decodePubkey(hex) {
132
132
  if (typeof hex !== "string" || hex.length !== 66) {
133
- throw new SessionManagerError(
133
+ throw new SessionStoreError(
134
134
  "service_identity_key must be a 66-character hex-encoded 33-byte pubkey"
135
135
  );
136
136
  }
@@ -139,13 +139,13 @@ function _decodePubkey(hex) {
139
139
 
140
140
  /**
141
141
  * Convenience factory: creates a pool from a Pool config and returns an
142
- * initialized `PostgresSessionManager`. Most callers should use
143
- * `createPostgresSessionManagerWithPool` instead so the pool can be shared
142
+ * initialized `PostgresSessionStore`. Most callers should use
143
+ * `createPostgresSessionStoreWithPool` instead so the pool can be shared
144
144
  * across stores.
145
145
  */
146
- async function createPostgresSessionManager(poolConfig, identity, logger = null) {
146
+ async function createPostgresSessionStore(poolConfig, identity, logger = null) {
147
147
  const pool = new pg.Pool(poolConfig);
148
- const manager = new PostgresSessionManager(
148
+ const manager = new PostgresSessionStore(
149
149
  pool,
150
150
  identity,
151
151
  logger,
@@ -159,13 +159,13 @@ async function createPostgresSessionManager(poolConfig, identity, logger = null)
159
159
  * Wraps an existing pool — useful when sharing the pool with the storage,
160
160
  * tree store, and token store implementations.
161
161
  */
162
- async function createPostgresSessionManagerWithPool(
162
+ async function createPostgresSessionStoreWithPool(
163
163
  pool,
164
164
  identity,
165
165
  logger = null,
166
166
  runMigration = true
167
167
  ) {
168
- const manager = new PostgresSessionManager(
168
+ const manager = new PostgresSessionStore(
169
169
  pool,
170
170
  identity,
171
171
  logger,
@@ -176,8 +176,8 @@ async function createPostgresSessionManagerWithPool(
176
176
  }
177
177
 
178
178
  module.exports = {
179
- PostgresSessionManager,
180
- createPostgresSessionManager,
181
- createPostgresSessionManagerWithPool,
182
- SessionManagerError,
179
+ PostgresSessionStore,
180
+ createPostgresSessionStore,
181
+ createPostgresSessionStoreWithPool,
182
+ SessionStoreError,
183
183
  };
@@ -1,22 +1,22 @@
1
1
  /**
2
- * Database Migration Manager for Breez SDK PostgreSQL Session Manager.
2
+ * Database Migration Manager for Breez SDK PostgreSQL Session Store.
3
3
  *
4
4
  * Uses a brz_session_schema_migrations table + pg_advisory_xact_lock to safely
5
5
  * run migrations from concurrent processes. Mirrors the schema produced by
6
- * the Rust `PostgresSessionManager`.
6
+ * the Rust `PostgresSessionStore`.
7
7
  */
8
8
 
9
- const { SessionManagerError } = require("./errors.cjs");
9
+ const { SessionStoreError } = require("./errors.cjs");
10
10
 
11
11
  /**
12
- * Advisory lock ID for session-manager migrations.
12
+ * Advisory lock ID for session-store migrations.
13
13
  * Uses a different lock ID from the storage / tree store / token store
14
14
  * migrations to avoid contention. Derived from ASCII bytes of "SESN"
15
15
  * (0x5345534E).
16
16
  */
17
17
  const MIGRATION_LOCK_ID = "1397245774"; // 0x5345534E as decimal string
18
18
 
19
- class SessionManagerMigrationManager {
19
+ class SessionStoreMigrationManager {
20
20
  constructor(logger = null) {
21
21
  this.logger = logger;
22
22
  }
@@ -51,7 +51,7 @@ class SessionManagerMigrationManager {
51
51
  if (currentVersion >= migrations.length) {
52
52
  this._log(
53
53
  "info",
54
- `Session manager database is up to date (version ${currentVersion})`
54
+ `Session store database is up to date (version ${currentVersion})`
55
55
  );
56
56
  await client.query("COMMIT");
57
57
  return;
@@ -59,7 +59,7 @@ class SessionManagerMigrationManager {
59
59
 
60
60
  this._log(
61
61
  "info",
62
- `Migrating session manager database from version ${currentVersion} to ${migrations.length}`
62
+ `Migrating session store database from version ${currentVersion} to ${migrations.length}`
63
63
  );
64
64
 
65
65
  for (let i = currentVersion; i < migrations.length; i++) {
@@ -67,7 +67,7 @@ class SessionManagerMigrationManager {
67
67
  const version = i + 1;
68
68
  this._log(
69
69
  "debug",
70
- `Running session manager migration ${version}: ${migration.name}`
70
+ `Running session store migration ${version}: ${migration.name}`
71
71
  );
72
72
 
73
73
  for (const sql of migration.sql) {
@@ -83,12 +83,12 @@ class SessionManagerMigrationManager {
83
83
  await client.query("COMMIT");
84
84
  this._log(
85
85
  "info",
86
- "Session manager database migration completed successfully"
86
+ "Session store database migration completed successfully"
87
87
  );
88
88
  } catch (error) {
89
89
  await client.query("ROLLBACK").catch(() => {});
90
- throw new SessionManagerError(
91
- `Session manager migration failed: ${error.message}`,
90
+ throw new SessionStoreError(
91
+ `Session store migration failed: ${error.message}`,
92
92
  error
93
93
  );
94
94
  } finally {
@@ -137,12 +137,12 @@ class SessionManagerMigrationManager {
137
137
  if (this.logger && typeof this.logger.log === "function") {
138
138
  this.logger.log({ line: message, level });
139
139
  } else if (level === "error") {
140
- console.error(`[SessionManagerMigrationManager] ${message}`);
140
+ console.error(`[SessionStoreMigrationManager] ${message}`);
141
141
  }
142
142
  }
143
143
 
144
144
  /**
145
- * Migrations matching the Rust PostgresSessionManager schema exactly.
145
+ * Migrations matching the Rust PostgresSessionStore schema exactly.
146
146
  */
147
147
  _getMigrations() {
148
148
  return [
@@ -162,4 +162,4 @@ class SessionManagerMigrationManager {
162
162
  }
163
163
  }
164
164
 
165
- module.exports = { SessionManagerMigrationManager };
165
+ module.exports = { SessionStoreMigrationManager };
@@ -0,0 +1,9 @@
1
+ {
2
+ "dependencies": {
3
+ "pg": "^8.18.0"
4
+ },
5
+ "description": "Node.js PostgreSQL session store implementation for Breez SDK WASM (CommonJS)",
6
+ "main": "index.cjs",
7
+ "name": "@breez-sdk/postgres-session-store",
8
+ "version": "1.0.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
 
@@ -353,130 +357,192 @@ class PostgresStorage {
353
357
  }
354
358
  }
355
359
 
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;
360
+ async applyPaymentUpdate(payment) {
361
+ if (!payment) {
362
+ throw new StorageError("Payment cannot be null or undefined");
363
+ }
368
364
 
365
+ try {
366
+ return await this._withTransaction(async (client) => {
367
+ const lockKey = this._paymentUpdateLockKey(payment.id);
369
368
  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
- ]
369
+ "SELECT pg_advisory_xact_lock($1::bigint)",
370
+ [lockKey]
395
371
  );
396
372
 
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
- ]
373
+ const { rows } = await client.query(
374
+ "SELECT status FROM brz_payments WHERE user_id = $1 AND id = $2 FOR UPDATE",
375
+ [this.identity, payment.id]
376
+ );
377
+ const stored = rows.length > 0
378
+ ? this._normalizePaymentStatus(rows[0].status)
379
+ : null;
380
+ const next = this._normalizePaymentStatus(payment.status);
381
+
382
+ if (stored != null
383
+ && this._isFinalPaymentStatus(stored)
384
+ && stored !== next) {
385
+ console.warn(
386
+ `Skipping payment update (would replace terminal status): id=${payment.id} stored=${stored} new=${next}`
445
387
  );
388
+ return false;
446
389
  }
447
390
 
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
- ]
391
+ const sameStatus = stored === next;
392
+ if (sameStatus) {
393
+ console.debug(
394
+ `Skipping redundant payment event: id=${payment.id} status=${next}`
468
395
  );
469
396
  }
397
+ await this._runPaymentUpsert(client, payment);
398
+ return !sameStatus;
470
399
  });
471
400
  } catch (error) {
472
401
  if (error instanceof StorageError) throw error;
473
402
  throw new StorageError(
474
- `Failed to insert payment '${payment.id}': ${error.message}`,
403
+ `Failed to apply payment update '${payment.id}': ${error.message}`,
475
404
  error
476
405
  );
477
406
  }
478
407
  }
479
408
 
409
+ _paymentUpdateLockKey(paymentId) {
410
+ return crypto
411
+ .createHash("sha256")
412
+ .update("brz_payment_update")
413
+ .update(this.identity)
414
+ .update(Buffer.from(paymentId))
415
+ .digest()
416
+ .readBigInt64BE(0)
417
+ .toString();
418
+ }
419
+
420
+ async _runPaymentUpsert(client, payment) {
421
+ const withdrawTxId =
422
+ payment.details?.type === "withdraw" ? payment.details.txId : null;
423
+ const spark = payment.details?.type === "spark" ? true : null;
424
+
425
+ await client.query(
426
+ `INSERT INTO brz_payments (user_id, id, payment_type, status, amount, fees, timestamp, method, withdraw_tx_id, spark)
427
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
428
+ ON CONFLICT(user_id, id) DO UPDATE SET
429
+ payment_type=EXCLUDED.payment_type,
430
+ status=EXCLUDED.status,
431
+ amount=EXCLUDED.amount,
432
+ fees=EXCLUDED.fees,
433
+ timestamp=EXCLUDED.timestamp,
434
+ method=EXCLUDED.method,
435
+ withdraw_tx_id=EXCLUDED.withdraw_tx_id,
436
+ spark=EXCLUDED.spark`,
437
+ [
438
+ this.identity,
439
+ payment.id,
440
+ payment.paymentType,
441
+ payment.status,
442
+ payment.amount.toString(),
443
+ payment.fees.toString(),
444
+ payment.timestamp,
445
+ payment.method ? JSON.stringify(payment.method) : null,
446
+ withdrawTxId,
447
+ spark,
448
+ ]
449
+ );
450
+
451
+ if (payment.details?.type === "deposit") {
452
+ await client.query(
453
+ `INSERT INTO brz_payment_details_deposit (user_id, payment_id, tx_id, vout)
454
+ VALUES ($1, $2, $3, $4)
455
+ ON CONFLICT(user_id, payment_id) DO UPDATE SET
456
+ tx_id=EXCLUDED.tx_id,
457
+ vout=EXCLUDED.vout`,
458
+ [this.identity, payment.id, payment.details.txId, payment.details.vout]
459
+ );
460
+ }
461
+
462
+ if (
463
+ payment.details?.type === "spark" &&
464
+ (payment.details.invoiceDetails != null ||
465
+ payment.details.htlcDetails != null)
466
+ ) {
467
+ await client.query(
468
+ `INSERT INTO brz_payment_details_spark (user_id, payment_id, invoice_details, htlc_details)
469
+ VALUES ($1, $2, $3, $4)
470
+ ON CONFLICT(user_id, payment_id) DO UPDATE SET
471
+ invoice_details=COALESCE(EXCLUDED.invoice_details, brz_payment_details_spark.invoice_details),
472
+ htlc_details=COALESCE(EXCLUDED.htlc_details, brz_payment_details_spark.htlc_details)`,
473
+ [
474
+ this.identity,
475
+ payment.id,
476
+ payment.details.invoiceDetails
477
+ ? JSON.stringify(payment.details.invoiceDetails)
478
+ : null,
479
+ payment.details.htlcDetails
480
+ ? JSON.stringify(payment.details.htlcDetails)
481
+ : null,
482
+ ]
483
+ );
484
+ }
485
+
486
+ if (payment.details?.type === "lightning") {
487
+ await client.query(
488
+ `INSERT INTO brz_payment_details_lightning
489
+ (user_id, payment_id, invoice, payment_hash, destination_pubkey, description, preimage, htlc_status, htlc_expiry_time)
490
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
491
+ ON CONFLICT(user_id, payment_id) DO UPDATE SET
492
+ invoice=EXCLUDED.invoice,
493
+ payment_hash=EXCLUDED.payment_hash,
494
+ destination_pubkey=EXCLUDED.destination_pubkey,
495
+ description=EXCLUDED.description,
496
+ preimage=COALESCE(EXCLUDED.preimage, brz_payment_details_lightning.preimage),
497
+ htlc_status=COALESCE(EXCLUDED.htlc_status, brz_payment_details_lightning.htlc_status),
498
+ htlc_expiry_time=COALESCE(EXCLUDED.htlc_expiry_time, brz_payment_details_lightning.htlc_expiry_time)`,
499
+ [
500
+ this.identity,
501
+ payment.id,
502
+ payment.details.invoice,
503
+ payment.details.htlcDetails.paymentHash,
504
+ payment.details.destinationPubkey,
505
+ payment.details.description,
506
+ payment.details.htlcDetails?.preimage,
507
+ payment.details.htlcDetails?.status ?? null,
508
+ payment.details.htlcDetails?.expiryTime ?? 0,
509
+ ]
510
+ );
511
+ }
512
+
513
+ if (payment.details?.type === "token") {
514
+ await client.query(
515
+ `INSERT INTO brz_payment_details_token
516
+ (user_id, payment_id, metadata, tx_hash, tx_type, invoice_details)
517
+ VALUES ($1, $2, $3, $4, $5, $6)
518
+ ON CONFLICT(user_id, payment_id) DO UPDATE SET
519
+ metadata=EXCLUDED.metadata,
520
+ tx_hash=EXCLUDED.tx_hash,
521
+ tx_type=EXCLUDED.tx_type,
522
+ invoice_details=COALESCE(EXCLUDED.invoice_details, brz_payment_details_token.invoice_details)`,
523
+ [
524
+ this.identity,
525
+ payment.id,
526
+ JSON.stringify(payment.details.metadata),
527
+ payment.details.txHash,
528
+ payment.details.txType,
529
+ payment.details.invoiceDetails
530
+ ? JSON.stringify(payment.details.invoiceDetails)
531
+ : null,
532
+ ]
533
+ );
534
+ }
535
+ }
536
+
537
+ _normalizePaymentStatus(status) {
538
+ return typeof status === "string" ? status.toLowerCase() : status;
539
+ }
540
+
541
+ _isFinalPaymentStatus(status) {
542
+ const normalized = this._normalizePaymentStatus(status);
543
+ return normalized === "completed" || normalized === "failed";
544
+ }
545
+
480
546
  async getPaymentById(id) {
481
547
  try {
482
548
  if (!id) {
@@ -778,6 +844,7 @@ class PostgresStorage {
778
844
  details = {
779
845
  type: "deposit",
780
846
  txId: row.deposit_tx_id,
847
+ vout: Number(row.deposit_vout),
781
848
  };
782
849
  } else if (row.spark) {
783
850
  details = {
@@ -467,6 +467,30 @@ 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 — 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
+ },
470
494
  ];
471
495
  }
472
496
  }