@breeztech/breez-sdk-spark 0.7.13 → 0.7.15-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.
@@ -236,6 +236,17 @@ class MigrationManager {
236
236
  settings.delete("sync_initial_complete");
237
237
  }
238
238
  }
239
+ },
240
+ {
241
+ name: "Create parentPaymentId index for related payments lookup",
242
+ upgrade: (db, transaction) => {
243
+ if (db.objectStoreNames.contains("payment_metadata")) {
244
+ const metadataStore = transaction.objectStore("payment_metadata");
245
+ if (!metadataStore.indexNames.contains("parentPaymentId")) {
246
+ metadataStore.createIndex("parentPaymentId", "parentPaymentId", { unique: false });
247
+ }
248
+ }
249
+ }
239
250
  }
240
251
  ];
241
252
  }
@@ -260,7 +271,7 @@ class IndexedDBStorage {
260
271
  this.db = null;
261
272
  this.migrationManager = null;
262
273
  this.logger = logger;
263
- this.dbVersion = 7; // Current schema version
274
+ this.dbVersion = 8; // Current schema version
264
275
  }
265
276
 
266
277
  /**
@@ -418,6 +429,59 @@ class IndexedDBStorage {
418
429
 
419
430
  // ===== Payment Operations =====
420
431
 
432
+ /**
433
+ * Gets the set of payment IDs that are related payments (have a parentPaymentId).
434
+ * Uses the parentPaymentId index for efficient lookup.
435
+ * @param {IDBObjectStore} metadataStore - The payment_metadata object store
436
+ * @returns {Promise<Set<string>>} Set of payment IDs that are related payments
437
+ */
438
+ _getRelatedPaymentIds(metadataStore) {
439
+ return new Promise((resolve) => {
440
+ const relatedPaymentIds = new Set();
441
+
442
+ // Check if the parentPaymentId index exists (added in migration)
443
+ if (!metadataStore.indexNames.contains("parentPaymentId")) {
444
+ // Index doesn't exist yet, fall back to scanning all metadata
445
+ const cursorRequest = metadataStore.openCursor();
446
+ cursorRequest.onsuccess = (event) => {
447
+ const cursor = event.target.result;
448
+ if (cursor) {
449
+ if (cursor.value.parentPaymentId) {
450
+ relatedPaymentIds.add(cursor.value.paymentId);
451
+ }
452
+ cursor.continue();
453
+ } else {
454
+ resolve(relatedPaymentIds);
455
+ }
456
+ };
457
+ cursorRequest.onerror = () => resolve(new Set());
458
+ return;
459
+ }
460
+
461
+ // Use the parentPaymentId index to find all metadata entries with a parent
462
+ const index = metadataStore.index("parentPaymentId");
463
+ const cursorRequest = index.openCursor();
464
+
465
+ cursorRequest.onsuccess = (event) => {
466
+ const cursor = event.target.result;
467
+ if (cursor) {
468
+ // Only add if parentPaymentId is truthy (not null/undefined)
469
+ if (cursor.value.parentPaymentId) {
470
+ relatedPaymentIds.add(cursor.value.paymentId);
471
+ }
472
+ cursor.continue();
473
+ } else {
474
+ resolve(relatedPaymentIds);
475
+ }
476
+ };
477
+
478
+ cursorRequest.onerror = () => {
479
+ // If index lookup fails, return empty set and fall back to per-payment lookup
480
+ resolve(new Set());
481
+ };
482
+ });
483
+ }
484
+
421
485
  async listPayments(request) {
422
486
  if (!this.db) {
423
487
  throw new StorageError("Database not initialized");
@@ -427,15 +491,18 @@ class IndexedDBStorage {
427
491
  const actualOffset = request.offset !== null ? request.offset : 0;
428
492
  const actualLimit = request.limit !== null ? request.limit : 4294967295; // u32::MAX
429
493
 
430
- return new Promise((resolve, reject) => {
431
- const transaction = this.db.transaction(
432
- ["payments", "payment_metadata", "lnurl_receive_metadata"],
433
- "readonly"
434
- );
435
- const paymentStore = transaction.objectStore("payments");
436
- const metadataStore = transaction.objectStore("payment_metadata");
437
- const lnurlReceiveMetadataStore = transaction.objectStore("lnurl_receive_metadata");
494
+ const transaction = this.db.transaction(
495
+ ["payments", "payment_metadata", "lnurl_receive_metadata"],
496
+ "readonly"
497
+ );
498
+ const paymentStore = transaction.objectStore("payments");
499
+ const metadataStore = transaction.objectStore("payment_metadata");
500
+ const lnurlReceiveMetadataStore = transaction.objectStore("lnurl_receive_metadata");
438
501
 
502
+ // Build set of related payment IDs upfront for O(1) filtering
503
+ const relatedPaymentIds = await this._getRelatedPaymentIds(metadataStore);
504
+
505
+ return new Promise((resolve, reject) => {
439
506
  const payments = [];
440
507
  let count = 0;
441
508
  let skipped = 0;
@@ -458,16 +525,23 @@ class IndexedDBStorage {
458
525
 
459
526
  const payment = cursor.value;
460
527
 
528
+ // Skip related payments (those with a parentPaymentId)
529
+ if (relatedPaymentIds.has(payment.id)) {
530
+ cursor.continue();
531
+ return;
532
+ }
533
+
461
534
  if (skipped < actualOffset) {
462
535
  skipped++;
463
536
  cursor.continue();
464
537
  return;
465
538
  }
466
539
 
467
- // Get metadata for this payment
540
+ // Get metadata for this payment (now only for non-related payments)
468
541
  const metadataRequest = metadataStore.get(payment.id);
469
542
  metadataRequest.onsuccess = () => {
470
543
  const metadata = metadataRequest.result;
544
+
471
545
  const paymentWithMetadata = this._mergePaymentMetadata(
472
546
  payment,
473
547
  metadata
@@ -478,7 +552,7 @@ class IndexedDBStorage {
478
552
  cursor.continue();
479
553
  return;
480
554
  }
481
-
555
+
482
556
  // Fetch lnurl receive metadata if it's a lightning payment
483
557
  this._fetchLnurlReceiveMetadata(paymentWithMetadata, lnurlReceiveMetadataStore)
484
558
  .then((mergedPayment) => {
@@ -667,7 +741,173 @@ class IndexedDBStorage {
667
741
  });
668
742
  }
669
743
 
670
- async setPaymentMetadata(paymentId, metadata) {
744
+ /**
745
+ * Checks if any related payments exist (payments with a parentPaymentId).
746
+ * Uses the parentPaymentId index for efficient lookup.
747
+ * @param {IDBObjectStore} metadataStore - The payment_metadata object store
748
+ * @returns {Promise<boolean>} True if any related payments exist
749
+ */
750
+ _hasRelatedPayments(metadataStore) {
751
+ return new Promise((resolve) => {
752
+ // Check if the parentPaymentId index exists (added in migration)
753
+ if (!metadataStore.indexNames.contains("parentPaymentId")) {
754
+ // Index doesn't exist yet, fall back to scanning all metadata
755
+ const cursorRequest = metadataStore.openCursor();
756
+ cursorRequest.onsuccess = (event) => {
757
+ const cursor = event.target.result;
758
+ if (cursor) {
759
+ if (cursor.value.parentPaymentId) {
760
+ resolve(true);
761
+ return;
762
+ }
763
+ cursor.continue();
764
+ } else {
765
+ resolve(false);
766
+ }
767
+ };
768
+ cursorRequest.onerror = () => resolve(true); // Assume there might be related payments on error
769
+ return;
770
+ }
771
+
772
+ const index = metadataStore.index("parentPaymentId");
773
+ const cursorRequest = index.openCursor();
774
+
775
+ cursorRequest.onsuccess = (event) => {
776
+ const cursor = event.target.result;
777
+ if (cursor && cursor.value.parentPaymentId) {
778
+ // Found at least one related payment
779
+ resolve(true);
780
+ } else if (cursor) {
781
+ // Entry with null parentPaymentId, continue searching
782
+ cursor.continue();
783
+ } else {
784
+ // No more entries
785
+ resolve(false);
786
+ }
787
+ };
788
+
789
+ cursorRequest.onerror = () => {
790
+ // If index lookup fails, assume there might be related payments
791
+ resolve(true);
792
+ };
793
+ });
794
+ }
795
+
796
+ /**
797
+ * Gets payments that have any of the specified parent payment IDs.
798
+ * @param {string[]} parentPaymentIds - Array of parent payment IDs
799
+ * @returns {Promise<Object>} Map of parentPaymentId -> array of RelatedPayment objects
800
+ */
801
+ async getPaymentsByParentIds(parentPaymentIds) {
802
+ if (!this.db) {
803
+ throw new StorageError("Database not initialized");
804
+ }
805
+
806
+ if (!parentPaymentIds || parentPaymentIds.length === 0) {
807
+ return {};
808
+ }
809
+
810
+ const transaction = this.db.transaction(
811
+ ["payments", "payment_metadata", "lnurl_receive_metadata"],
812
+ "readonly"
813
+ );
814
+ const metadataStore = transaction.objectStore("payment_metadata");
815
+
816
+ // Early exit if no related payments exist
817
+ const hasRelated = await this._hasRelatedPayments(metadataStore);
818
+ if (!hasRelated) {
819
+ return {};
820
+ }
821
+
822
+ const parentIdSet = new Set(parentPaymentIds);
823
+ const paymentStore = transaction.objectStore("payments");
824
+ const lnurlReceiveMetadataStore = transaction.objectStore("lnurl_receive_metadata");
825
+
826
+ return new Promise((resolve, reject) => {
827
+ const result = {};
828
+ const fetchedMetadata = [];
829
+
830
+ // Query all metadata records and filter by parentPaymentId
831
+ const cursorRequest = metadataStore.openCursor();
832
+
833
+ cursorRequest.onsuccess = (event) => {
834
+ const cursor = event.target.result;
835
+ if (!cursor) {
836
+ // All metadata processed, now fetch payment details
837
+ if (fetchedMetadata.length === 0) {
838
+ resolve(result);
839
+ return;
840
+ }
841
+
842
+ let processed = 0;
843
+ for (const metadata of fetchedMetadata) {
844
+ const parentId = metadata.parentPaymentId;
845
+ const paymentRequest = paymentStore.get(metadata.paymentId);
846
+ paymentRequest.onsuccess = () => {
847
+ const payment = paymentRequest.result;
848
+ if (payment) {
849
+ const paymentWithMetadata = this._mergePaymentMetadata(payment, metadata);
850
+
851
+ if (!result[parentId]) {
852
+ result[parentId] = [];
853
+ }
854
+
855
+ // Fetch lnurl receive metadata if applicable
856
+ this._fetchLnurlReceiveMetadata(paymentWithMetadata, lnurlReceiveMetadataStore)
857
+ .then((mergedPayment) => {
858
+ result[parentId].push(mergedPayment);
859
+ })
860
+ .catch(() => {
861
+ result[parentId].push(paymentWithMetadata);
862
+ })
863
+ .finally(() => {
864
+ processed++;
865
+ if (processed === fetchedMetadata.length) {
866
+ // Sort each parent's children by timestamp
867
+ for (const parentId of Object.keys(result)) {
868
+ result[parentId].sort((a, b) => a.timestamp - b.timestamp);
869
+ }
870
+ resolve(result);
871
+ }
872
+ });
873
+ } else {
874
+ processed++;
875
+ if (processed === fetchedMetadata.length) {
876
+ resolve(result);
877
+ }
878
+ }
879
+ };
880
+ paymentRequest.onerror = () => {
881
+ processed++;
882
+ if (processed === fetchedMetadata.length) {
883
+ resolve(result);
884
+ }
885
+ };
886
+ }
887
+ return;
888
+ }
889
+
890
+ const metadata = cursor.value;
891
+ if (metadata.parentPaymentId && parentIdSet.has(metadata.parentPaymentId)) {
892
+ fetchedMetadata.push(metadata);
893
+ }
894
+ cursor.continue();
895
+ };
896
+
897
+ cursorRequest.onerror = () => {
898
+ reject(
899
+ new StorageError(
900
+ `Failed to get payments by parent ids: ${
901
+ cursorRequest.error?.message || "Unknown error"
902
+ }`,
903
+ cursorRequest.error
904
+ )
905
+ );
906
+ };
907
+ });
908
+ }
909
+
910
+ async insertPaymentMetadata(paymentId, metadata) {
671
911
  if (!this.db) {
672
912
  throw new StorageError("Database not initialized");
673
913
  }
@@ -676,30 +916,47 @@ class IndexedDBStorage {
676
916
  const transaction = this.db.transaction("payment_metadata", "readwrite");
677
917
  const store = transaction.objectStore("payment_metadata");
678
918
 
679
- const metadataToStore = {
680
- paymentId,
681
- parentPaymentId: metadata.parentPaymentId,
682
- lnurlPayInfo: metadata.lnurlPayInfo
683
- ? JSON.stringify(metadata.lnurlPayInfo)
684
- : null,
685
- lnurlWithdrawInfo: metadata.lnurlWithdrawInfo
686
- ? JSON.stringify(metadata.lnurlWithdrawInfo)
687
- : null,
688
- lnurlDescription: metadata.lnurlDescription,
689
- conversionInfo: metadata.conversionInfo
690
- ? JSON.stringify(metadata.conversionInfo)
691
- : null,
692
- };
919
+ // First get existing record to merge with
920
+ const getRequest = store.get(paymentId);
921
+ getRequest.onsuccess = () => {
922
+ const existing = getRequest.result || {};
923
+
924
+ // Use COALESCE-like behavior: new value if non-null, otherwise keep existing
925
+ const metadataToStore = {
926
+ paymentId,
927
+ parentPaymentId: metadata.parentPaymentId ?? existing.parentPaymentId ?? null,
928
+ lnurlPayInfo: metadata.lnurlPayInfo
929
+ ? JSON.stringify(metadata.lnurlPayInfo)
930
+ : existing.lnurlPayInfo ?? null,
931
+ lnurlWithdrawInfo: metadata.lnurlWithdrawInfo
932
+ ? JSON.stringify(metadata.lnurlWithdrawInfo)
933
+ : existing.lnurlWithdrawInfo ?? null,
934
+ lnurlDescription: metadata.lnurlDescription ?? existing.lnurlDescription ?? null,
935
+ conversionInfo: metadata.conversionInfo
936
+ ? JSON.stringify(metadata.conversionInfo)
937
+ : existing.conversionInfo ?? null,
938
+ };
693
939
 
694
- const request = store.put(metadataToStore);
695
- request.onsuccess = () => resolve();
696
- request.onerror = () => {
940
+ const putRequest = store.put(metadataToStore);
941
+ putRequest.onsuccess = () => resolve();
942
+ putRequest.onerror = () => {
943
+ reject(
944
+ new StorageError(
945
+ `Failed to set payment metadata for '${paymentId}': ${
946
+ putRequest.error?.message || "Unknown error"
947
+ }`,
948
+ putRequest.error
949
+ )
950
+ );
951
+ };
952
+ };
953
+ getRequest.onerror = () => {
697
954
  reject(
698
955
  new StorageError(
699
- `Failed to set payment metadata for '${paymentId}': ${
700
- request.error?.message || "Unknown error"
956
+ `Failed to get existing payment metadata for '${paymentId}': ${
957
+ getRequest.error?.message || "Unknown error"
701
958
  }`,
702
- request.error
959
+ getRequest.error
703
960
  )
704
961
  );
705
962
  };