@breeztech/breez-sdk-spark 0.7.21 → 0.8.2

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.
@@ -148,6 +148,8 @@ class MigrationManager {
148
148
  {
149
149
  name: "Add sync tables",
150
150
  upgrade: (db, transaction) => {
151
+ // sync_revision: tracks the last committed revision (from server-acknowledged
152
+ // or server-received records). Does NOT include pending outgoing revisions.
151
153
  if (!db.objectStoreNames.contains("sync_revision")) {
152
154
  const syncRevisionStore = db.createObjectStore("sync_revision", {
153
155
  keyPath: "id",
@@ -178,15 +180,17 @@ class MigrationManager {
178
180
  if (!db.objectStoreNames.contains("sync_state")) {
179
181
  db.createObjectStore("sync_state", { keyPath: ["type", "dataId"] });
180
182
  }
181
- }
183
+ },
182
184
  },
183
185
  {
184
186
  name: "Create lnurl_receive_metadata store",
185
187
  upgrade: (db) => {
186
188
  if (!db.objectStoreNames.contains("lnurl_receive_metadata")) {
187
- db.createObjectStore("lnurl_receive_metadata", { keyPath: "paymentHash" });
189
+ db.createObjectStore("lnurl_receive_metadata", {
190
+ keyPath: "paymentHash",
191
+ });
188
192
  }
189
- }
193
+ },
190
194
  },
191
195
  {
192
196
  // Delete all unclaimed deposits to clear old claim_error JSON format.
@@ -197,7 +201,7 @@ class MigrationManager {
197
201
  const store = transaction.objectStore("unclaimed_deposits");
198
202
  store.clear();
199
203
  }
200
- }
204
+ },
201
205
  },
202
206
  {
203
207
  name: "Clear sync tables for BreezSigner backward compatibility",
@@ -236,6 +240,97 @@ class MigrationManager {
236
240
  settings.delete("sync_initial_complete");
237
241
  }
238
242
  }
243
+ },
244
+ {
245
+ name: "Create parentPaymentId index for related payments lookup",
246
+ upgrade: (db, transaction) => {
247
+ if (db.objectStoreNames.contains("payment_metadata")) {
248
+ const metadataStore = transaction.objectStore("payment_metadata");
249
+ if (!metadataStore.indexNames.contains("parentPaymentId")) {
250
+ metadataStore.createIndex("parentPaymentId", "parentPaymentId", { unique: false });
251
+ }
252
+ }
253
+ }
254
+ },
255
+ {
256
+ name: "Add tx_type to token payments and trigger token re-sync",
257
+ upgrade: (db, transaction) => {
258
+ // Update all existing token payments to have a default txType
259
+ if (db.objectStoreNames.contains("payments")) {
260
+ const paymentStore = transaction.objectStore("payments");
261
+ const getAllRequest = paymentStore.getAll();
262
+
263
+ getAllRequest.onsuccess = () => {
264
+ const payments = getAllRequest.result;
265
+
266
+ payments.forEach((payment) => {
267
+ // Parse details if it's a string
268
+ let details = null;
269
+ if (payment.details && typeof payment.details === "string") {
270
+ try {
271
+ details = JSON.parse(payment.details);
272
+ } catch (e) {
273
+ return; // Skip this payment if parsing fails
274
+ }
275
+ } else {
276
+ details = payment.details;
277
+ }
278
+
279
+ // Add default txType to token payments
280
+ if (details && details.type === "token" && !details.txType) {
281
+ details.txType = "transfer";
282
+ payment.details = JSON.stringify(details);
283
+ paymentStore.put(payment);
284
+ }
285
+ });
286
+ };
287
+ }
288
+
289
+ // Reset sync cache to trigger token re-sync
290
+ if (db.objectStoreNames.contains("settings")) {
291
+ const settingsStore = transaction.objectStore("settings");
292
+ const getRequest = settingsStore.get("sync_offset");
293
+
294
+ getRequest.onsuccess = () => {
295
+ const syncCache = getRequest.result;
296
+ if (syncCache && syncCache.value) {
297
+ try {
298
+ const syncInfo = JSON.parse(syncCache.value);
299
+ // Reset only the token sync position, keep the bitcoin offset
300
+ syncInfo.last_synced_final_token_payment_id = null;
301
+ settingsStore.put({
302
+ key: "sync_offset",
303
+ value: JSON.stringify(syncInfo),
304
+ });
305
+ } catch (e) {
306
+ // If parsing fails, just continue
307
+ }
308
+ }
309
+ };
310
+ }
311
+ },
312
+ },
313
+ {
314
+ name: "Clear sync tables to force re-sync",
315
+ upgrade: (db, transaction) => {
316
+ if (db.objectStoreNames.contains("sync_outgoing")) {
317
+ transaction.objectStore("sync_outgoing").clear();
318
+ }
319
+ if (db.objectStoreNames.contains("sync_incoming")) {
320
+ transaction.objectStore("sync_incoming").clear();
321
+ }
322
+ if (db.objectStoreNames.contains("sync_state")) {
323
+ transaction.objectStore("sync_state").clear();
324
+ }
325
+ if (db.objectStoreNames.contains("sync_revision")) {
326
+ const syncRevision = transaction.objectStore("sync_revision");
327
+ syncRevision.clear();
328
+ syncRevision.put({ id: 1, revision: "0" });
329
+ }
330
+ if (db.objectStoreNames.contains("settings")) {
331
+ transaction.objectStore("settings").delete("sync_initial_complete");
332
+ }
333
+ }
239
334
  }
240
335
  ];
241
336
  }
@@ -260,7 +355,7 @@ class IndexedDBStorage {
260
355
  this.db = null;
261
356
  this.migrationManager = null;
262
357
  this.logger = logger;
263
- this.dbVersion = 7; // Current schema version
358
+ this.dbVersion = 10; // Current schema version
264
359
  }
265
360
 
266
361
  /**
@@ -418,6 +513,59 @@ class IndexedDBStorage {
418
513
 
419
514
  // ===== Payment Operations =====
420
515
 
516
+ /**
517
+ * Gets the set of payment IDs that are related payments (have a parentPaymentId).
518
+ * Uses the parentPaymentId index for efficient lookup.
519
+ * @param {IDBObjectStore} metadataStore - The payment_metadata object store
520
+ * @returns {Promise<Set<string>>} Set of payment IDs that are related payments
521
+ */
522
+ _getRelatedPaymentIds(metadataStore) {
523
+ return new Promise((resolve) => {
524
+ const relatedPaymentIds = new Set();
525
+
526
+ // Check if the parentPaymentId index exists (added in migration)
527
+ if (!metadataStore.indexNames.contains("parentPaymentId")) {
528
+ // Index doesn't exist yet, fall back to scanning all metadata
529
+ const cursorRequest = metadataStore.openCursor();
530
+ cursorRequest.onsuccess = (event) => {
531
+ const cursor = event.target.result;
532
+ if (cursor) {
533
+ if (cursor.value.parentPaymentId) {
534
+ relatedPaymentIds.add(cursor.value.paymentId);
535
+ }
536
+ cursor.continue();
537
+ } else {
538
+ resolve(relatedPaymentIds);
539
+ }
540
+ };
541
+ cursorRequest.onerror = () => resolve(new Set());
542
+ return;
543
+ }
544
+
545
+ // Use the parentPaymentId index to find all metadata entries with a parent
546
+ const index = metadataStore.index("parentPaymentId");
547
+ const cursorRequest = index.openCursor();
548
+
549
+ cursorRequest.onsuccess = (event) => {
550
+ const cursor = event.target.result;
551
+ if (cursor) {
552
+ // Only add if parentPaymentId is truthy (not null/undefined)
553
+ if (cursor.value.parentPaymentId) {
554
+ relatedPaymentIds.add(cursor.value.paymentId);
555
+ }
556
+ cursor.continue();
557
+ } else {
558
+ resolve(relatedPaymentIds);
559
+ }
560
+ };
561
+
562
+ cursorRequest.onerror = () => {
563
+ // If index lookup fails, return empty set and fall back to per-payment lookup
564
+ resolve(new Set());
565
+ };
566
+ });
567
+ }
568
+
421
569
  async listPayments(request) {
422
570
  if (!this.db) {
423
571
  throw new StorageError("Database not initialized");
@@ -427,15 +575,18 @@ class IndexedDBStorage {
427
575
  const actualOffset = request.offset !== null ? request.offset : 0;
428
576
  const actualLimit = request.limit !== null ? request.limit : 4294967295; // u32::MAX
429
577
 
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");
578
+ const transaction = this.db.transaction(
579
+ ["payments", "payment_metadata", "lnurl_receive_metadata"],
580
+ "readonly"
581
+ );
582
+ const paymentStore = transaction.objectStore("payments");
583
+ const metadataStore = transaction.objectStore("payment_metadata");
584
+ const lnurlReceiveMetadataStore = transaction.objectStore("lnurl_receive_metadata");
438
585
 
586
+ // Build set of related payment IDs upfront for O(1) filtering
587
+ const relatedPaymentIds = await this._getRelatedPaymentIds(metadataStore);
588
+
589
+ return new Promise((resolve, reject) => {
439
590
  const payments = [];
440
591
  let count = 0;
441
592
  let skipped = 0;
@@ -458,16 +609,23 @@ class IndexedDBStorage {
458
609
 
459
610
  const payment = cursor.value;
460
611
 
612
+ // Skip related payments (those with a parentPaymentId)
613
+ if (relatedPaymentIds.has(payment.id)) {
614
+ cursor.continue();
615
+ return;
616
+ }
617
+
461
618
  if (skipped < actualOffset) {
462
619
  skipped++;
463
620
  cursor.continue();
464
621
  return;
465
622
  }
466
623
 
467
- // Get metadata for this payment
624
+ // Get metadata for this payment (now only for non-related payments)
468
625
  const metadataRequest = metadataStore.get(payment.id);
469
626
  metadataRequest.onsuccess = () => {
470
627
  const metadata = metadataRequest.result;
628
+
471
629
  const paymentWithMetadata = this._mergePaymentMetadata(
472
630
  payment,
473
631
  metadata
@@ -478,9 +636,12 @@ class IndexedDBStorage {
478
636
  cursor.continue();
479
637
  return;
480
638
  }
481
-
639
+
482
640
  // Fetch lnurl receive metadata if it's a lightning payment
483
- this._fetchLnurlReceiveMetadata(paymentWithMetadata, lnurlReceiveMetadataStore)
641
+ this._fetchLnurlReceiveMetadata(
642
+ paymentWithMetadata,
643
+ lnurlReceiveMetadataStore
644
+ )
484
645
  .then((mergedPayment) => {
485
646
  payments.push(mergedPayment);
486
647
  count++;
@@ -560,7 +721,9 @@ class IndexedDBStorage {
560
721
  );
561
722
  const paymentStore = transaction.objectStore("payments");
562
723
  const metadataStore = transaction.objectStore("payment_metadata");
563
- const lnurlReceiveMetadataStore = transaction.objectStore("lnurl_receive_metadata");
724
+ const lnurlReceiveMetadataStore = transaction.objectStore(
725
+ "lnurl_receive_metadata"
726
+ );
564
727
 
565
728
  const paymentRequest = paymentStore.get(id);
566
729
 
@@ -579,9 +742,12 @@ class IndexedDBStorage {
579
742
  payment,
580
743
  metadata
581
744
  );
582
-
745
+
583
746
  // Fetch lnurl receive metadata if it's a lightning payment
584
- this._fetchLnurlReceiveMetadata(paymentWithMetadata, lnurlReceiveMetadataStore)
747
+ this._fetchLnurlReceiveMetadata(
748
+ paymentWithMetadata,
749
+ lnurlReceiveMetadataStore
750
+ )
585
751
  .then(resolve)
586
752
  .catch(() => {
587
753
  // Continue without lnurl receive metadata if fetch fails
@@ -620,7 +786,9 @@ class IndexedDBStorage {
620
786
  const paymentStore = transaction.objectStore("payments");
621
787
  const invoiceIndex = paymentStore.index("invoice");
622
788
  const metadataStore = transaction.objectStore("payment_metadata");
623
- const lnurlReceiveMetadataStore = transaction.objectStore("lnurl_receive_metadata");
789
+ const lnurlReceiveMetadataStore = transaction.objectStore(
790
+ "lnurl_receive_metadata"
791
+ );
624
792
 
625
793
  const paymentRequest = invoiceIndex.get(invoice);
626
794
 
@@ -639,9 +807,12 @@ class IndexedDBStorage {
639
807
  payment,
640
808
  metadata
641
809
  );
642
-
810
+
643
811
  // Fetch lnurl receive metadata if it's a lightning payment
644
- this._fetchLnurlReceiveMetadata(paymentWithMetadata, lnurlReceiveMetadataStore)
812
+ this._fetchLnurlReceiveMetadata(
813
+ paymentWithMetadata,
814
+ lnurlReceiveMetadataStore
815
+ )
645
816
  .then(resolve)
646
817
  .catch(() => {
647
818
  // Continue without lnurl receive metadata if fetch fails
@@ -667,7 +838,173 @@ class IndexedDBStorage {
667
838
  });
668
839
  }
669
840
 
670
- async setPaymentMetadata(paymentId, metadata) {
841
+ /**
842
+ * Checks if any related payments exist (payments with a parentPaymentId).
843
+ * Uses the parentPaymentId index for efficient lookup.
844
+ * @param {IDBObjectStore} metadataStore - The payment_metadata object store
845
+ * @returns {Promise<boolean>} True if any related payments exist
846
+ */
847
+ _hasRelatedPayments(metadataStore) {
848
+ return new Promise((resolve) => {
849
+ // Check if the parentPaymentId index exists (added in migration)
850
+ if (!metadataStore.indexNames.contains("parentPaymentId")) {
851
+ // Index doesn't exist yet, fall back to scanning all metadata
852
+ const cursorRequest = metadataStore.openCursor();
853
+ cursorRequest.onsuccess = (event) => {
854
+ const cursor = event.target.result;
855
+ if (cursor) {
856
+ if (cursor.value.parentPaymentId) {
857
+ resolve(true);
858
+ return;
859
+ }
860
+ cursor.continue();
861
+ } else {
862
+ resolve(false);
863
+ }
864
+ };
865
+ cursorRequest.onerror = () => resolve(true); // Assume there might be related payments on error
866
+ return;
867
+ }
868
+
869
+ const index = metadataStore.index("parentPaymentId");
870
+ const cursorRequest = index.openCursor();
871
+
872
+ cursorRequest.onsuccess = (event) => {
873
+ const cursor = event.target.result;
874
+ if (cursor && cursor.value.parentPaymentId) {
875
+ // Found at least one related payment
876
+ resolve(true);
877
+ } else if (cursor) {
878
+ // Entry with null parentPaymentId, continue searching
879
+ cursor.continue();
880
+ } else {
881
+ // No more entries
882
+ resolve(false);
883
+ }
884
+ };
885
+
886
+ cursorRequest.onerror = () => {
887
+ // If index lookup fails, assume there might be related payments
888
+ resolve(true);
889
+ };
890
+ });
891
+ }
892
+
893
+ /**
894
+ * Gets payments that have any of the specified parent payment IDs.
895
+ * @param {string[]} parentPaymentIds - Array of parent payment IDs
896
+ * @returns {Promise<Object>} Map of parentPaymentId -> array of RelatedPayment objects
897
+ */
898
+ async getPaymentsByParentIds(parentPaymentIds) {
899
+ if (!this.db) {
900
+ throw new StorageError("Database not initialized");
901
+ }
902
+
903
+ if (!parentPaymentIds || parentPaymentIds.length === 0) {
904
+ return {};
905
+ }
906
+
907
+ const transaction = this.db.transaction(
908
+ ["payments", "payment_metadata", "lnurl_receive_metadata"],
909
+ "readonly"
910
+ );
911
+ const metadataStore = transaction.objectStore("payment_metadata");
912
+
913
+ // Early exit if no related payments exist
914
+ const hasRelated = await this._hasRelatedPayments(metadataStore);
915
+ if (!hasRelated) {
916
+ return {};
917
+ }
918
+
919
+ const parentIdSet = new Set(parentPaymentIds);
920
+ const paymentStore = transaction.objectStore("payments");
921
+ const lnurlReceiveMetadataStore = transaction.objectStore("lnurl_receive_metadata");
922
+
923
+ return new Promise((resolve, reject) => {
924
+ const result = {};
925
+ const fetchedMetadata = [];
926
+
927
+ // Query all metadata records and filter by parentPaymentId
928
+ const cursorRequest = metadataStore.openCursor();
929
+
930
+ cursorRequest.onsuccess = (event) => {
931
+ const cursor = event.target.result;
932
+ if (!cursor) {
933
+ // All metadata processed, now fetch payment details
934
+ if (fetchedMetadata.length === 0) {
935
+ resolve(result);
936
+ return;
937
+ }
938
+
939
+ let processed = 0;
940
+ for (const metadata of fetchedMetadata) {
941
+ const parentId = metadata.parentPaymentId;
942
+ const paymentRequest = paymentStore.get(metadata.paymentId);
943
+ paymentRequest.onsuccess = () => {
944
+ const payment = paymentRequest.result;
945
+ if (payment) {
946
+ const paymentWithMetadata = this._mergePaymentMetadata(payment, metadata);
947
+
948
+ if (!result[parentId]) {
949
+ result[parentId] = [];
950
+ }
951
+
952
+ // Fetch lnurl receive metadata if applicable
953
+ this._fetchLnurlReceiveMetadata(paymentWithMetadata, lnurlReceiveMetadataStore)
954
+ .then((mergedPayment) => {
955
+ result[parentId].push(mergedPayment);
956
+ })
957
+ .catch(() => {
958
+ result[parentId].push(paymentWithMetadata);
959
+ })
960
+ .finally(() => {
961
+ processed++;
962
+ if (processed === fetchedMetadata.length) {
963
+ // Sort each parent's children by timestamp
964
+ for (const parentId of Object.keys(result)) {
965
+ result[parentId].sort((a, b) => a.timestamp - b.timestamp);
966
+ }
967
+ resolve(result);
968
+ }
969
+ });
970
+ } else {
971
+ processed++;
972
+ if (processed === fetchedMetadata.length) {
973
+ resolve(result);
974
+ }
975
+ }
976
+ };
977
+ paymentRequest.onerror = () => {
978
+ processed++;
979
+ if (processed === fetchedMetadata.length) {
980
+ resolve(result);
981
+ }
982
+ };
983
+ }
984
+ return;
985
+ }
986
+
987
+ const metadata = cursor.value;
988
+ if (metadata.parentPaymentId && parentIdSet.has(metadata.parentPaymentId)) {
989
+ fetchedMetadata.push(metadata);
990
+ }
991
+ cursor.continue();
992
+ };
993
+
994
+ cursorRequest.onerror = () => {
995
+ reject(
996
+ new StorageError(
997
+ `Failed to get payments by parent ids: ${
998
+ cursorRequest.error?.message || "Unknown error"
999
+ }`,
1000
+ cursorRequest.error
1001
+ )
1002
+ );
1003
+ };
1004
+ });
1005
+ }
1006
+
1007
+ async insertPaymentMetadata(paymentId, metadata) {
671
1008
  if (!this.db) {
672
1009
  throw new StorageError("Database not initialized");
673
1010
  }
@@ -676,30 +1013,47 @@ class IndexedDBStorage {
676
1013
  const transaction = this.db.transaction("payment_metadata", "readwrite");
677
1014
  const store = transaction.objectStore("payment_metadata");
678
1015
 
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
- };
1016
+ // First get existing record to merge with
1017
+ const getRequest = store.get(paymentId);
1018
+ getRequest.onsuccess = () => {
1019
+ const existing = getRequest.result || {};
1020
+
1021
+ // Use COALESCE-like behavior: new value if non-null, otherwise keep existing
1022
+ const metadataToStore = {
1023
+ paymentId,
1024
+ parentPaymentId: metadata.parentPaymentId ?? existing.parentPaymentId ?? null,
1025
+ lnurlPayInfo: metadata.lnurlPayInfo
1026
+ ? JSON.stringify(metadata.lnurlPayInfo)
1027
+ : existing.lnurlPayInfo ?? null,
1028
+ lnurlWithdrawInfo: metadata.lnurlWithdrawInfo
1029
+ ? JSON.stringify(metadata.lnurlWithdrawInfo)
1030
+ : existing.lnurlWithdrawInfo ?? null,
1031
+ lnurlDescription: metadata.lnurlDescription ?? existing.lnurlDescription ?? null,
1032
+ conversionInfo: metadata.conversionInfo
1033
+ ? JSON.stringify(metadata.conversionInfo)
1034
+ : existing.conversionInfo ?? null,
1035
+ };
693
1036
 
694
- const request = store.put(metadataToStore);
695
- request.onsuccess = () => resolve();
696
- request.onerror = () => {
1037
+ const putRequest = store.put(metadataToStore);
1038
+ putRequest.onsuccess = () => resolve();
1039
+ putRequest.onerror = () => {
1040
+ reject(
1041
+ new StorageError(
1042
+ `Failed to set payment metadata for '${paymentId}': ${
1043
+ putRequest.error?.message || "Unknown error"
1044
+ }`,
1045
+ putRequest.error
1046
+ )
1047
+ );
1048
+ };
1049
+ };
1050
+ getRequest.onerror = () => {
697
1051
  reject(
698
1052
  new StorageError(
699
- `Failed to set payment metadata for '${paymentId}': ${
700
- request.error?.message || "Unknown error"
1053
+ `Failed to get existing payment metadata for '${paymentId}': ${
1054
+ getRequest.error?.message || "Unknown error"
701
1055
  }`,
702
- request.error
1056
+ getRequest.error
703
1057
  )
704
1058
  );
705
1059
  };
@@ -909,9 +1263,9 @@ class IndexedDBStorage {
909
1263
  request.onerror = () => {
910
1264
  reject(
911
1265
  new StorageError(
912
- `Failed to add lnurl metadata for payment hash '${item.paymentHash}': ${
913
- request.error?.message || "Unknown error"
914
- }`,
1266
+ `Failed to add lnurl metadata for payment hash '${
1267
+ item.paymentHash
1268
+ }': ${request.error?.message || "Unknown error"}`,
915
1269
  request.error
916
1270
  )
917
1271
  );
@@ -926,72 +1280,56 @@ class IndexedDBStorage {
926
1280
  }
927
1281
 
928
1282
  return new Promise((resolve, reject) => {
929
- const transaction = this.db.transaction(
930
- ["sync_outgoing", "sync_revision"],
931
- "readwrite"
932
- );
1283
+ const transaction = this.db.transaction(["sync_outgoing"], "readwrite");
933
1284
 
934
- // Get the next revision
935
- const revisionStore = transaction.objectStore("sync_revision");
936
- const getRevisionRequest = revisionStore.get(1);
1285
+ // This revision is a local queue id for pending rows, not a server revision.
1286
+ const outgoingStore = transaction.objectStore("sync_outgoing");
1287
+ const getAllOutgoingRequest = outgoingStore.getAll();
1288
+
1289
+ getAllOutgoingRequest.onsuccess = () => {
1290
+ const records = getAllOutgoingRequest.result;
1291
+ let maxOutgoingRevision = BigInt(0);
1292
+ for (const storeRecord of records) {
1293
+ const rev = BigInt(
1294
+ storeRecord.record.localRevision ?? storeRecord.record.revision
1295
+ );
1296
+ if (rev > maxOutgoingRevision) {
1297
+ maxOutgoingRevision = rev;
1298
+ }
1299
+ }
1300
+ const nextRevision = maxOutgoingRevision + BigInt(1);
937
1301
 
938
- getRevisionRequest.onsuccess = () => {
939
- const revisionData = getRevisionRequest.result || {
940
- id: 1,
941
- revision: "0",
1302
+ const storeRecord = {
1303
+ type: record.id.type,
1304
+ dataId: record.id.dataId,
1305
+ revision: Number(nextRevision),
1306
+ record: {
1307
+ ...record,
1308
+ localRevision: nextRevision,
1309
+ },
942
1310
  };
943
- const nextRevision = BigInt(revisionData.revision) + BigInt(1);
944
-
945
- // Update the revision
946
- const updateRequest = revisionStore.put({
947
- id: 1,
948
- revision: nextRevision.toString(),
949
- });
950
1311
 
951
- updateRequest.onsuccess = () => {
952
- const outgoingStore = transaction.objectStore("sync_outgoing");
953
-
954
- const storeRecord = {
955
- type: record.id.type,
956
- dataId: record.id.dataId,
957
- revision: Number(nextRevision),
958
- record: {
959
- ...record,
960
- revision: nextRevision,
961
- },
962
- };
963
-
964
- const addRequest = outgoingStore.add(storeRecord);
1312
+ const addRequest = outgoingStore.add(storeRecord);
965
1313
 
966
- addRequest.onsuccess = () => {
967
- // Wait for transaction to complete before resolving
968
- transaction.oncomplete = () => {
969
- resolve(nextRevision);
970
- };
971
- };
972
-
973
- addRequest.onerror = (event) => {
974
- reject(
975
- new StorageError(
976
- `Failed to add outgoing change: ${event.target.error.message}`
977
- )
978
- );
1314
+ addRequest.onsuccess = () => {
1315
+ transaction.oncomplete = () => {
1316
+ resolve(nextRevision);
979
1317
  };
980
1318
  };
981
1319
 
982
- updateRequest.onerror = (event) => {
1320
+ addRequest.onerror = (event) => {
983
1321
  reject(
984
1322
  new StorageError(
985
- `Failed to update revision: ${event.target.error.message}`
1323
+ `Failed to add outgoing change: ${event.target.error.message}`
986
1324
  )
987
1325
  );
988
1326
  };
989
1327
  };
990
1328
 
991
- getRevisionRequest.onerror = (event) => {
1329
+ getAllOutgoingRequest.onerror = (event) => {
992
1330
  reject(
993
1331
  new StorageError(
994
- `Failed to get revision: ${event.target.error.message}`
1332
+ `Failed to get outgoing records: ${event.target.error.message}`
995
1333
  )
996
1334
  );
997
1335
  };
@@ -1004,23 +1342,24 @@ class IndexedDBStorage {
1004
1342
  });
1005
1343
  }
1006
1344
 
1007
- async syncCompleteOutgoingSync(record) {
1345
+ async syncCompleteOutgoingSync(record, localRevision) {
1008
1346
  if (!this.db) {
1009
1347
  throw new StorageError("Database not initialized");
1010
1348
  }
1011
1349
 
1012
1350
  return new Promise((resolve, reject) => {
1013
1351
  const transaction = this.db.transaction(
1014
- ["sync_outgoing", "sync_state"],
1352
+ ["sync_outgoing", "sync_state", "sync_revision"],
1015
1353
  "readwrite"
1016
1354
  );
1017
1355
  const outgoingStore = transaction.objectStore("sync_outgoing");
1018
1356
  const stateStore = transaction.objectStore("sync_state");
1357
+ const revisionStore = transaction.objectStore("sync_revision");
1019
1358
 
1020
1359
  const deleteRequest = outgoingStore.delete([
1021
1360
  record.id.type,
1022
1361
  record.id.dataId,
1023
- Number(record.revision),
1362
+ Number(localRevision),
1024
1363
  ]);
1025
1364
 
1026
1365
  deleteRequest.onsuccess = () => {
@@ -1030,7 +1369,25 @@ class IndexedDBStorage {
1030
1369
  record: record,
1031
1370
  };
1032
1371
  stateStore.put(stateRecord);
1033
- resolve();
1372
+
1373
+ // Update sync_revision to track the highest known revision
1374
+ const getRevisionRequest = revisionStore.get(1);
1375
+ getRevisionRequest.onsuccess = () => {
1376
+ const current = getRevisionRequest.result || { id: 1, revision: "0" };
1377
+ const currentRevision = BigInt(current.revision);
1378
+ const recordRevision = BigInt(record.revision);
1379
+ if (recordRevision > currentRevision) {
1380
+ revisionStore.put({ id: 1, revision: recordRevision.toString() });
1381
+ }
1382
+ resolve();
1383
+ };
1384
+ getRevisionRequest.onerror = (event) => {
1385
+ reject(
1386
+ new StorageError(
1387
+ `Failed to update sync revision: ${event.target.error.message}`
1388
+ )
1389
+ );
1390
+ };
1034
1391
  };
1035
1392
 
1036
1393
  deleteRequest.onerror = (event) => {
@@ -1067,7 +1424,11 @@ class IndexedDBStorage {
1067
1424
  const cursor = event.target.result;
1068
1425
  if (cursor && count < limit) {
1069
1426
  const storeRecord = cursor.value;
1070
- const change = storeRecord.record;
1427
+ const change = {
1428
+ ...storeRecord.record,
1429
+ localRevision:
1430
+ storeRecord.record.localRevision ?? storeRecord.record.revision,
1431
+ };
1071
1432
 
1072
1433
  // Look up parent record if it exists
1073
1434
  const stateRequest = stateStore.get([
@@ -1207,110 +1568,6 @@ class IndexedDBStorage {
1207
1568
  });
1208
1569
  }
1209
1570
 
1210
- async syncRebasePendingOutgoingRecords(revision) {
1211
- if (!this.db) {
1212
- throw new StorageError("Database not initialized");
1213
- }
1214
-
1215
- return new Promise((resolve, reject) => {
1216
- const transaction = this.db.transaction(
1217
- ["sync_outgoing", "sync_revision"],
1218
- "readwrite"
1219
- );
1220
- const outgoingStore = transaction.objectStore("sync_outgoing");
1221
- const revisionStore = transaction.objectStore("sync_revision");
1222
-
1223
- // Get the last revision from sync_revision table
1224
- const getRevisionRequest = revisionStore.get(1);
1225
-
1226
- getRevisionRequest.onsuccess = () => {
1227
- const revisionData = getRevisionRequest.result || {
1228
- id: 1,
1229
- revision: "0",
1230
- };
1231
- const lastRevision = BigInt(revisionData.revision);
1232
-
1233
- // Calculate the difference
1234
- const diff = revision - lastRevision;
1235
-
1236
- if (diff <= BigInt(0)) {
1237
- // No rebase needed
1238
- resolve();
1239
- return;
1240
- }
1241
-
1242
- // Get all records from sync_outgoing and update their revisions
1243
- const getAllRequest = outgoingStore.getAll();
1244
-
1245
- getAllRequest.onsuccess = () => {
1246
- const records = getAllRequest.result;
1247
- let updatesCompleted = 0;
1248
-
1249
- if (records.length === 0) {
1250
- resolve();
1251
- return;
1252
- }
1253
-
1254
- for (const storeRecord of records) {
1255
- // Delete the old record
1256
- const oldKey = [
1257
- storeRecord.type,
1258
- storeRecord.dataId,
1259
- storeRecord.revision,
1260
- ];
1261
- outgoingStore.delete(oldKey);
1262
-
1263
- // Update revision in both the key and the nested record
1264
- const newRevision = storeRecord.record.revision + diff;
1265
- const updatedRecord = {
1266
- type: storeRecord.type,
1267
- dataId: storeRecord.dataId,
1268
- revision: Number(newRevision),
1269
- record: {
1270
- ...storeRecord.record,
1271
- revision: newRevision,
1272
- },
1273
- };
1274
-
1275
- // Add the updated record
1276
- const putRequest = outgoingStore.put(updatedRecord);
1277
-
1278
- putRequest.onsuccess = () => {
1279
- updatesCompleted++;
1280
- if (updatesCompleted === records.length) {
1281
- resolve();
1282
- }
1283
- };
1284
-
1285
- putRequest.onerror = (event) => {
1286
- reject(
1287
- new StorageError(
1288
- `Failed to rebase outgoing record: ${event.target.error.message}`
1289
- )
1290
- );
1291
- };
1292
- }
1293
- };
1294
-
1295
- getAllRequest.onerror = (event) => {
1296
- reject(
1297
- new StorageError(
1298
- `Failed to get outgoing records for rebase: ${event.target.error.message}`
1299
- )
1300
- );
1301
- };
1302
- };
1303
-
1304
- getRevisionRequest.onerror = (event) => {
1305
- reject(
1306
- new StorageError(
1307
- `Failed to get last revision: ${event.target.error.message}`
1308
- )
1309
- );
1310
- };
1311
- });
1312
- }
1313
-
1314
1571
  async syncGetIncomingRecords(limit) {
1315
1572
  if (!this.db) {
1316
1573
  throw new StorageError("Database not initialized");
@@ -1400,7 +1657,11 @@ class IndexedDBStorage {
1400
1657
  const cursor = event.target.result;
1401
1658
  if (cursor) {
1402
1659
  const storeRecord = cursor.value;
1403
- const change = storeRecord.record;
1660
+ const change = {
1661
+ ...storeRecord.record,
1662
+ localRevision:
1663
+ storeRecord.record.localRevision ?? storeRecord.record.revision,
1664
+ };
1404
1665
 
1405
1666
  // Get the parent record
1406
1667
  const stateRequest = stateStore.get([
@@ -1446,8 +1707,9 @@ class IndexedDBStorage {
1446
1707
  }
1447
1708
 
1448
1709
  return new Promise((resolve, reject) => {
1449
- const transaction = this.db.transaction(["sync_state"], "readwrite");
1710
+ const transaction = this.db.transaction(["sync_state", "sync_revision"], "readwrite");
1450
1711
  const stateStore = transaction.objectStore("sync_state");
1712
+ const revisionStore = transaction.objectStore("sync_revision");
1451
1713
 
1452
1714
  const storeRecord = {
1453
1715
  type: record.id.type,
@@ -1458,7 +1720,24 @@ class IndexedDBStorage {
1458
1720
  const request = stateStore.put(storeRecord);
1459
1721
 
1460
1722
  request.onsuccess = () => {
1461
- resolve();
1723
+ // Update sync_revision to track the highest known revision
1724
+ const getRevisionRequest = revisionStore.get(1);
1725
+ getRevisionRequest.onsuccess = () => {
1726
+ const current = getRevisionRequest.result || { id: 1, revision: "0" };
1727
+ const currentRevision = BigInt(current.revision);
1728
+ const incomingRevision = BigInt(record.revision);
1729
+ if (incomingRevision > currentRevision) {
1730
+ revisionStore.put({ id: 1, revision: incomingRevision.toString() });
1731
+ }
1732
+ resolve();
1733
+ };
1734
+ getRevisionRequest.onerror = (event) => {
1735
+ reject(
1736
+ new StorageError(
1737
+ `Failed to update sync revision: ${event.target.error.message}`
1738
+ )
1739
+ );
1740
+ };
1462
1741
  };
1463
1742
 
1464
1743
  request.onerror = (event) => {
@@ -1502,7 +1781,10 @@ class IndexedDBStorage {
1502
1781
  }
1503
1782
 
1504
1783
  // Filter by payment details
1505
- if (request.paymentDetailsFilter && request.paymentDetailsFilter.length > 0) {
1784
+ if (
1785
+ request.paymentDetailsFilter &&
1786
+ request.paymentDetailsFilter.length > 0
1787
+ ) {
1506
1788
  let details = null;
1507
1789
 
1508
1790
  // Parse details if it's a string (stored in IndexedDB)
@@ -1533,7 +1815,9 @@ class IndexedDBStorage {
1533
1815
  if (
1534
1816
  details.type !== "spark" ||
1535
1817
  !details.htlcDetails ||
1536
- !paymentDetailsFilter.htlcStatus.includes(details.htlcDetails.status)
1818
+ !paymentDetailsFilter.htlcStatus.includes(
1819
+ details.htlcDetails.status
1820
+ )
1537
1821
  ) {
1538
1822
  continue;
1539
1823
  }
@@ -1570,11 +1854,23 @@ class IndexedDBStorage {
1570
1854
  continue;
1571
1855
  }
1572
1856
  }
1857
+ // Filter by token transaction type
1858
+ if (
1859
+ paymentDetailsFilter.type === "token" &&
1860
+ paymentDetailsFilter.txType != null
1861
+ ) {
1862
+ if (
1863
+ details.type !== "token" ||
1864
+ details.txType !== paymentDetailsFilter.txType
1865
+ ) {
1866
+ continue;
1867
+ }
1868
+ }
1573
1869
 
1574
1870
  paymentDetailsFilterMatches = true;
1575
1871
  break;
1576
1872
  }
1577
-
1873
+
1578
1874
  if (!paymentDetailsFilterMatches) {
1579
1875
  return false;
1580
1876
  }
@@ -1706,7 +2002,11 @@ class IndexedDBStorage {
1706
2002
 
1707
2003
  _fetchLnurlReceiveMetadata(payment, lnurlReceiveMetadataStore) {
1708
2004
  // Only fetch for lightning payments with a payment hash
1709
- if (!payment.details || payment.details.type !== "lightning" || !payment.details.paymentHash) {
2005
+ if (
2006
+ !payment.details ||
2007
+ payment.details.type !== "lightning" ||
2008
+ !payment.details.paymentHash
2009
+ ) {
1710
2010
  return Promise.resolve(payment);
1711
2011
  }
1712
2012
 
@@ -1715,11 +2015,17 @@ class IndexedDBStorage {
1715
2015
  }
1716
2016
 
1717
2017
  return new Promise((resolve, reject) => {
1718
- const lnurlReceiveRequest = lnurlReceiveMetadataStore.get(payment.details.paymentHash);
1719
-
2018
+ const lnurlReceiveRequest = lnurlReceiveMetadataStore.get(
2019
+ payment.details.paymentHash
2020
+ );
2021
+
1720
2022
  lnurlReceiveRequest.onsuccess = () => {
1721
2023
  const lnurlReceiveMetadata = lnurlReceiveRequest.result;
1722
- if (lnurlReceiveMetadata && (lnurlReceiveMetadata.nostrZapRequest || lnurlReceiveMetadata.senderComment)) {
2024
+ if (
2025
+ lnurlReceiveMetadata &&
2026
+ (lnurlReceiveMetadata.nostrZapRequest ||
2027
+ lnurlReceiveMetadata.senderComment)
2028
+ ) {
1723
2029
  payment.details.lnurlReceiveMetadata = {
1724
2030
  nostrZapRequest: lnurlReceiveMetadata.nostrZapRequest || null,
1725
2031
  nostrZapReceipt: lnurlReceiveMetadata.nostrZapReceipt || null,
@@ -1728,7 +2034,7 @@ class IndexedDBStorage {
1728
2034
  }
1729
2035
  resolve(payment);
1730
2036
  };
1731
-
2037
+
1732
2038
  lnurlReceiveRequest.onerror = () => {
1733
2039
  // Continue without lnurlReceiveMetadata if fetch fails
1734
2040
  reject(new Error("Failed to fetch lnurl receive metadata"));