@unicitylabs/sphere-sdk 0.5.0 → 0.5.1

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 (41) hide show
  1. package/dist/connect/index.cjs +3 -1
  2. package/dist/connect/index.cjs.map +1 -1
  3. package/dist/connect/index.js +3 -1
  4. package/dist/connect/index.js.map +1 -1
  5. package/dist/core/index.cjs +205 -41
  6. package/dist/core/index.cjs.map +1 -1
  7. package/dist/core/index.d.cts +22 -0
  8. package/dist/core/index.d.ts +22 -0
  9. package/dist/core/index.js +205 -41
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/impl/browser/connect/index.cjs +3 -1
  12. package/dist/impl/browser/connect/index.cjs.map +1 -1
  13. package/dist/impl/browser/connect/index.js +3 -1
  14. package/dist/impl/browser/connect/index.js.map +1 -1
  15. package/dist/impl/browser/index.cjs +3 -1
  16. package/dist/impl/browser/index.cjs.map +1 -1
  17. package/dist/impl/browser/index.js +3 -1
  18. package/dist/impl/browser/index.js.map +1 -1
  19. package/dist/impl/browser/ipfs.cjs +3 -1
  20. package/dist/impl/browser/ipfs.cjs.map +1 -1
  21. package/dist/impl/browser/ipfs.js +3 -1
  22. package/dist/impl/browser/ipfs.js.map +1 -1
  23. package/dist/impl/nodejs/connect/index.cjs +3 -1
  24. package/dist/impl/nodejs/connect/index.cjs.map +1 -1
  25. package/dist/impl/nodejs/connect/index.js +3 -1
  26. package/dist/impl/nodejs/connect/index.js.map +1 -1
  27. package/dist/impl/nodejs/index.cjs +3 -1
  28. package/dist/impl/nodejs/index.cjs.map +1 -1
  29. package/dist/impl/nodejs/index.js +3 -1
  30. package/dist/impl/nodejs/index.js.map +1 -1
  31. package/dist/index.cjs +205 -41
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +26 -0
  34. package/dist/index.d.ts +26 -0
  35. package/dist/index.js +205 -41
  36. package/dist/index.js.map +1 -1
  37. package/dist/l1/index.cjs +3 -1
  38. package/dist/l1/index.cjs.map +1 -1
  39. package/dist/l1/index.js +3 -1
  40. package/dist/l1/index.js.map +1 -1
  41. package/package.json +1 -1
@@ -103,7 +103,9 @@ var init_constants = __esm({
103
103
  /** Group chat: members for this address */
104
104
  GROUP_CHAT_MEMBERS: "group_chat_members",
105
105
  /** Group chat: processed event IDs for deduplication */
106
- GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events"
106
+ GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
107
+ /** Processed V5 split group IDs for Nostr re-delivery dedup */
108
+ PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
107
109
  };
108
110
  STORAGE_KEYS = {
109
111
  ...STORAGE_KEYS_GLOBAL,
@@ -4395,6 +4397,17 @@ var PaymentsModule = class _PaymentsModule {
4395
4397
  // Poll every 2s
4396
4398
  static PROOF_POLLING_MAX_ATTEMPTS = 30;
4397
4399
  // Max 30 attempts (~60s)
4400
+ // Periodic retry for resolveUnconfirmed (V5 lazy finalization)
4401
+ resolveUnconfirmedTimer = null;
4402
+ static RESOLVE_UNCONFIRMED_INTERVAL_MS = 1e4;
4403
+ // Retry every 10s
4404
+ // Guard: ensure load() completes before processing incoming bundles
4405
+ loadedPromise = null;
4406
+ loaded = false;
4407
+ // Persistent dedup: tracks splitGroupIds that have been fully processed.
4408
+ // Survives page reloads via KV storage so Nostr re-deliveries are ignored
4409
+ // even when the confirmed token's in-memory ID differs from v5split_{id}.
4410
+ processedSplitGroupIds = /* @__PURE__ */ new Set();
4398
4411
  // Storage event subscriptions (push-based sync)
4399
4412
  storageEventUnsubscribers = [];
4400
4413
  syncDebounceTimer = null;
@@ -4480,31 +4493,40 @@ var PaymentsModule = class _PaymentsModule {
4480
4493
  */
4481
4494
  async load() {
4482
4495
  this.ensureInitialized();
4483
- await TokenRegistry.waitForReady();
4484
- const providers = this.getTokenStorageProviders();
4485
- for (const [id, provider] of providers) {
4486
- try {
4487
- const result = await provider.load();
4488
- if (result.success && result.data) {
4489
- this.loadFromStorageData(result.data);
4490
- this.log(`Loaded metadata from provider ${id}`);
4491
- break;
4496
+ const doLoad = async () => {
4497
+ await TokenRegistry.waitForReady();
4498
+ const providers = this.getTokenStorageProviders();
4499
+ for (const [id, provider] of providers) {
4500
+ try {
4501
+ const result = await provider.load();
4502
+ if (result.success && result.data) {
4503
+ this.loadFromStorageData(result.data);
4504
+ this.log(`Loaded metadata from provider ${id}`);
4505
+ break;
4506
+ }
4507
+ } catch (err) {
4508
+ console.error(`[Payments] Failed to load from provider ${id}:`, err);
4492
4509
  }
4493
- } catch (err) {
4494
- console.error(`[Payments] Failed to load from provider ${id}:`, err);
4495
4510
  }
4496
- }
4497
- await this.loadPendingV5Tokens();
4498
- await this.loadHistory();
4499
- const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4500
- if (pending2) {
4501
- const transfers = JSON.parse(pending2);
4502
- for (const transfer of transfers) {
4503
- this.pendingTransfers.set(transfer.id, transfer);
4511
+ const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
4512
+ console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
4513
+ await this.loadPendingV5Tokens();
4514
+ await this.loadProcessedSplitGroupIds();
4515
+ await this.loadHistory();
4516
+ const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4517
+ if (pending2) {
4518
+ const transfers = JSON.parse(pending2);
4519
+ for (const transfer of transfers) {
4520
+ this.pendingTransfers.set(transfer.id, transfer);
4521
+ }
4504
4522
  }
4505
- }
4523
+ this.loaded = true;
4524
+ };
4525
+ this.loadedPromise = doLoad();
4526
+ await this.loadedPromise;
4506
4527
  this.resolveUnconfirmed().catch(() => {
4507
4528
  });
4529
+ this.scheduleResolveUnconfirmed();
4508
4530
  }
4509
4531
  /**
4510
4532
  * Cleanup all subscriptions, polling jobs, and pending resolvers.
@@ -4523,6 +4545,7 @@ var PaymentsModule = class _PaymentsModule {
4523
4545
  this.paymentRequestResponseHandlers.clear();
4524
4546
  this.stopProofPolling();
4525
4547
  this.proofPollingJobs.clear();
4548
+ this.stopResolveUnconfirmedPolling();
4526
4549
  for (const [, resolver] of this.pendingResponseResolvers) {
4527
4550
  clearTimeout(resolver.timeout);
4528
4551
  resolver.reject(new Error("Module destroyed"));
@@ -4929,13 +4952,16 @@ var PaymentsModule = class _PaymentsModule {
4929
4952
  */
4930
4953
  async processInstantSplitBundle(bundle, senderPubkey, memo) {
4931
4954
  this.ensureInitialized();
4955
+ if (!this.loaded && this.loadedPromise) {
4956
+ await this.loadedPromise;
4957
+ }
4932
4958
  if (!isInstantSplitBundleV5(bundle)) {
4933
4959
  return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
4934
4960
  }
4935
4961
  try {
4936
4962
  const deterministicId = `v5split_${bundle.splitGroupId}`;
4937
- if (this.tokens.has(deterministicId)) {
4938
- this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
4963
+ if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
4964
+ console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
4939
4965
  return { success: true, durationMs: 0 };
4940
4966
  }
4941
4967
  const registry = TokenRegistry.getInstance();
@@ -4961,7 +4987,8 @@ var PaymentsModule = class _PaymentsModule {
4961
4987
  sdkData: JSON.stringify({ _pendingFinalization: pendingData })
4962
4988
  };
4963
4989
  await this.addToken(uiToken);
4964
- this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
4990
+ this.processedSplitGroupIds.add(bundle.splitGroupId);
4991
+ await this.saveProcessedSplitGroupIds();
4965
4992
  const senderInfo = await this.resolveSenderInfo(senderPubkey);
4966
4993
  await this.addToHistory({
4967
4994
  type: "RECEIVED",
@@ -4985,6 +5012,7 @@ var PaymentsModule = class _PaymentsModule {
4985
5012
  await this.save();
4986
5013
  this.resolveUnconfirmed().catch(() => {
4987
5014
  });
5015
+ this.scheduleResolveUnconfirmed();
4988
5016
  return { success: true, durationMs: 0 };
4989
5017
  } catch (error) {
4990
5018
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -5731,28 +5759,70 @@ var PaymentsModule = class _PaymentsModule {
5731
5759
  };
5732
5760
  const stClient = this.deps.oracle.getStateTransitionClient?.();
5733
5761
  const trustBase = this.deps.oracle.getTrustBase?.();
5734
- if (!stClient || !trustBase) return result;
5762
+ if (!stClient || !trustBase) {
5763
+ console.log(`[V5-RESOLVE] resolveUnconfirmed: EARLY EXIT \u2014 stClient=${!!stClient} trustBase=${!!trustBase}`);
5764
+ return result;
5765
+ }
5735
5766
  const signingService = await this.createSigningService();
5767
+ const submittedCount = Array.from(this.tokens.values()).filter((t) => t.status === "submitted").length;
5768
+ console.log(`[V5-RESOLVE] resolveUnconfirmed: ${submittedCount} submitted token(s) to process`);
5736
5769
  for (const [tokenId, token] of this.tokens) {
5737
5770
  if (token.status !== "submitted") continue;
5738
5771
  const pending2 = this.parsePendingFinalization(token.sdkData);
5739
5772
  if (!pending2) {
5773
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
5740
5774
  result.stillPending++;
5741
5775
  continue;
5742
5776
  }
5743
5777
  if (pending2.type === "v5_bundle") {
5778
+ console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
5744
5779
  const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
5780
+ console.log(`[V5-RESOLVE] Result for ${tokenId.slice(0, 16)}...: ${progress} (stage now: ${pending2.stage})`);
5745
5781
  result.details.push({ tokenId, stage: pending2.stage, status: progress });
5746
5782
  if (progress === "resolved") result.resolved++;
5747
5783
  else if (progress === "failed") result.failed++;
5748
5784
  else result.stillPending++;
5749
5785
  }
5750
5786
  }
5751
- if (result.resolved > 0 || result.failed > 0) {
5787
+ if (result.resolved > 0 || result.failed > 0 || result.stillPending > 0) {
5788
+ console.log(`[V5-RESOLVE] Saving: resolved=${result.resolved} failed=${result.failed} stillPending=${result.stillPending}`);
5752
5789
  await this.save();
5753
5790
  }
5754
5791
  return result;
5755
5792
  }
5793
+ /**
5794
+ * Start a periodic interval that retries resolveUnconfirmed() until all
5795
+ * tokens are confirmed or failed. Stops automatically when nothing is
5796
+ * pending and is cleaned up by destroy().
5797
+ */
5798
+ scheduleResolveUnconfirmed() {
5799
+ if (this.resolveUnconfirmedTimer) return;
5800
+ const hasUnconfirmed = Array.from(this.tokens.values()).some(
5801
+ (t) => t.status === "submitted"
5802
+ );
5803
+ if (!hasUnconfirmed) {
5804
+ console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: no submitted tokens, not starting timer`);
5805
+ return;
5806
+ }
5807
+ console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: starting periodic retry (every ${_PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS}ms)`);
5808
+ this.resolveUnconfirmedTimer = setInterval(async () => {
5809
+ try {
5810
+ const result = await this.resolveUnconfirmed();
5811
+ if (result.stillPending === 0) {
5812
+ console.log(`[V5-RESOLVE] All tokens resolved, stopping periodic retry`);
5813
+ this.stopResolveUnconfirmedPolling();
5814
+ }
5815
+ } catch (err) {
5816
+ console.log(`[V5-RESOLVE] Periodic retry error:`, err);
5817
+ }
5818
+ }, _PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS);
5819
+ }
5820
+ stopResolveUnconfirmedPolling() {
5821
+ if (this.resolveUnconfirmedTimer) {
5822
+ clearInterval(this.resolveUnconfirmedTimer);
5823
+ this.resolveUnconfirmedTimer = null;
5824
+ }
5825
+ }
5756
5826
  // ===========================================================================
5757
5827
  // Private - V5 Lazy Resolution Helpers
5758
5828
  // ===========================================================================
@@ -5765,10 +5835,12 @@ var PaymentsModule = class _PaymentsModule {
5765
5835
  pending2.lastAttemptAt = Date.now();
5766
5836
  try {
5767
5837
  if (pending2.stage === "RECEIVED") {
5838
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
5768
5839
  const mintDataJson = JSON.parse(bundle.recipientMintData);
5769
5840
  const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
5770
5841
  const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
5771
5842
  const mintResponse = await stClient.submitMintCommitment(mintCommitment);
5843
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
5772
5844
  if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
5773
5845
  throw new Error(`Mint submission failed: ${mintResponse.status}`);
5774
5846
  }
@@ -5776,22 +5848,27 @@ var PaymentsModule = class _PaymentsModule {
5776
5848
  this.updatePendingFinalization(token, pending2);
5777
5849
  }
5778
5850
  if (pending2.stage === "MINT_SUBMITTED") {
5851
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
5779
5852
  const mintDataJson = JSON.parse(bundle.recipientMintData);
5780
5853
  const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
5781
5854
  const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
5782
5855
  const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
5783
5856
  if (!proof) {
5857
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
5784
5858
  this.updatePendingFinalization(token, pending2);
5785
5859
  return "pending";
5786
5860
  }
5861
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
5787
5862
  pending2.mintProofJson = JSON.stringify(proof);
5788
5863
  pending2.stage = "MINT_PROVEN";
5789
5864
  this.updatePendingFinalization(token, pending2);
5790
5865
  }
5791
5866
  if (pending2.stage === "MINT_PROVEN") {
5867
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
5792
5868
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5793
5869
  const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
5794
5870
  const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
5871
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
5795
5872
  if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
5796
5873
  throw new Error(`Transfer submission failed: ${transferResponse.status}`);
5797
5874
  }
@@ -5799,13 +5876,16 @@ var PaymentsModule = class _PaymentsModule {
5799
5876
  this.updatePendingFinalization(token, pending2);
5800
5877
  }
5801
5878
  if (pending2.stage === "TRANSFER_SUBMITTED") {
5879
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
5802
5880
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5803
5881
  const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
5804
5882
  const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
5805
5883
  if (!proof) {
5884
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
5806
5885
  this.updatePendingFinalization(token, pending2);
5807
5886
  return "pending";
5808
5887
  }
5888
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
5809
5889
  const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
5810
5890
  const confirmedToken = {
5811
5891
  id: token.id,
@@ -5821,6 +5901,12 @@ var PaymentsModule = class _PaymentsModule {
5821
5901
  sdkData: JSON.stringify(finalizedToken.toJSON())
5822
5902
  };
5823
5903
  this.tokens.set(tokenId, confirmedToken);
5904
+ this.deps.emitEvent("transfer:confirmed", {
5905
+ id: crypto.randomUUID(),
5906
+ status: "completed",
5907
+ tokens: [confirmedToken],
5908
+ tokenTransfers: []
5909
+ });
5824
5910
  this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
5825
5911
  return "resolved";
5826
5912
  }
@@ -5962,11 +6048,20 @@ var PaymentsModule = class _PaymentsModule {
5962
6048
  }
5963
6049
  }
5964
6050
  if (pendingTokens.length > 0) {
6051
+ const json = JSON.stringify(pendingTokens);
6052
+ this.log(`[V5-PERSIST] Saving ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")} (${json.length} bytes)`);
5965
6053
  await this.deps.storage.set(
5966
6054
  STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
5967
- JSON.stringify(pendingTokens)
6055
+ json
5968
6056
  );
6057
+ const verify = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
6058
+ if (!verify) {
6059
+ console.error("[Payments][V5-PERSIST] CRITICAL: KV write succeeded but read-back is empty!");
6060
+ } else {
6061
+ this.log(`[V5-PERSIST] Verified: read-back ${verify.length} bytes`);
6062
+ }
5969
6063
  } else {
6064
+ this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
5970
6065
  await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
5971
6066
  }
5972
6067
  }
@@ -5976,16 +6071,47 @@ var PaymentsModule = class _PaymentsModule {
5976
6071
  */
5977
6072
  async loadPendingV5Tokens() {
5978
6073
  const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
6074
+ this.log(`[V5-PERSIST] loadPendingV5Tokens: KV data = ${data ? `${data.length} bytes` : "null/empty"}`);
5979
6075
  if (!data) return;
5980
6076
  try {
5981
6077
  const pendingTokens = JSON.parse(data);
6078
+ this.log(`[V5-PERSIST] Parsed ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")}`);
5982
6079
  for (const token of pendingTokens) {
5983
6080
  if (!this.tokens.has(token.id)) {
5984
6081
  this.tokens.set(token.id, token);
6082
+ this.log(`[V5-PERSIST] Restored token ${token.id.slice(0, 16)} (status=${token.status})`);
6083
+ } else {
6084
+ this.log(`[V5-PERSIST] Token ${token.id.slice(0, 16)} already in map, skipping`);
5985
6085
  }
5986
6086
  }
5987
- if (pendingTokens.length > 0) {
5988
- this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
6087
+ } catch (err) {
6088
+ console.error("[Payments][V5-PERSIST] Failed to parse pending V5 tokens:", err);
6089
+ }
6090
+ }
6091
+ /**
6092
+ * Persist the set of processed splitGroupIds to KV storage.
6093
+ * This ensures Nostr re-deliveries are ignored across page reloads,
6094
+ * even when the confirmed token's in-memory ID differs from v5split_{id}.
6095
+ */
6096
+ async saveProcessedSplitGroupIds() {
6097
+ const ids = Array.from(this.processedSplitGroupIds);
6098
+ if (ids.length > 0) {
6099
+ await this.deps.storage.set(
6100
+ STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS,
6101
+ JSON.stringify(ids)
6102
+ );
6103
+ }
6104
+ }
6105
+ /**
6106
+ * Load processed splitGroupIds from KV storage.
6107
+ */
6108
+ async loadProcessedSplitGroupIds() {
6109
+ const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS);
6110
+ if (!data) return;
6111
+ try {
6112
+ const ids = JSON.parse(data);
6113
+ for (const id of ids) {
6114
+ this.processedSplitGroupIds.add(id);
5989
6115
  }
5990
6116
  } catch {
5991
6117
  }
@@ -6640,7 +6766,32 @@ var PaymentsModule = class _PaymentsModule {
6640
6766
  try {
6641
6767
  const result = await provider.sync(localData);
6642
6768
  if (result.success && result.merged) {
6769
+ const savedTokens = new Map(this.tokens);
6643
6770
  this.loadFromStorageData(result.merged);
6771
+ let restoredCount = 0;
6772
+ for (const [tokenId, token] of savedTokens) {
6773
+ if (this.tokens.has(tokenId)) continue;
6774
+ const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
6775
+ const stateHash = extractStateHashFromSdkData(token.sdkData);
6776
+ if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
6777
+ continue;
6778
+ }
6779
+ if (sdkTokenId) {
6780
+ let hasEquivalent = false;
6781
+ for (const existing of this.tokens.values()) {
6782
+ if (extractTokenIdFromSdkData(existing.sdkData) === sdkTokenId) {
6783
+ hasEquivalent = true;
6784
+ break;
6785
+ }
6786
+ }
6787
+ if (hasEquivalent) continue;
6788
+ }
6789
+ this.tokens.set(tokenId, token);
6790
+ restoredCount++;
6791
+ }
6792
+ if (restoredCount > 0) {
6793
+ console.log(`[Payments] Sync: restored ${restoredCount} token(s) lost by loadFromStorageData`);
6794
+ }
6644
6795
  if (this.nametags.length === 0 && savedNametags.length > 0) {
6645
6796
  this.nametags = savedNametags;
6646
6797
  }
@@ -6974,8 +7125,9 @@ var PaymentsModule = class _PaymentsModule {
6974
7125
  return;
6975
7126
  }
6976
7127
  this.tokens.set(token.id, token);
7128
+ console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
6977
7129
  await this.save();
6978
- this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
7130
+ console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
6979
7131
  const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
6980
7132
  const incomingTransfer = {
6981
7133
  id: transfer.id,
@@ -7132,8 +7284,12 @@ var PaymentsModule = class _PaymentsModule {
7132
7284
  }
7133
7285
  }
7134
7286
  async handleIncomingTransfer(transfer) {
7287
+ if (!this.loaded && this.loadedPromise) {
7288
+ await this.loadedPromise;
7289
+ }
7135
7290
  try {
7136
7291
  const payload = transfer.payload;
7292
+ console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
7137
7293
  let instantBundle = null;
7138
7294
  if (isInstantSplitBundle(payload)) {
7139
7295
  instantBundle = payload;
@@ -7165,7 +7321,7 @@ var PaymentsModule = class _PaymentsModule {
7165
7321
  return;
7166
7322
  }
7167
7323
  if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
7168
- this.log("Processing NOSTR-FIRST commitment-only transfer...");
7324
+ console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
7169
7325
  await this.handleCommitmentOnlyTransfer(transfer, payload);
7170
7326
  return;
7171
7327
  }
@@ -7328,17 +7484,24 @@ var PaymentsModule = class _PaymentsModule {
7328
7484
  // ===========================================================================
7329
7485
  async save() {
7330
7486
  const providers = this.getTokenStorageProviders();
7331
- if (providers.size === 0) {
7332
- this.log("No token storage providers - tokens not persisted");
7333
- return;
7334
- }
7335
- const data = await this.createStorageData();
7336
- for (const [id, provider] of providers) {
7337
- try {
7338
- await provider.save(data);
7339
- } catch (err) {
7340
- console.error(`[Payments] Failed to save to provider ${id}:`, err);
7487
+ const tokenStats = Array.from(this.tokens.values()).map((t) => {
7488
+ const txf = tokenToTxf(t);
7489
+ return `${t.id.slice(0, 12)}(${t.status},txf=${!!txf})`;
7490
+ });
7491
+ console.log(`[Payments][DEBUG] save(): providers=${providers.size}, tokens=[${tokenStats.join(", ")}]`);
7492
+ if (providers.size > 0) {
7493
+ const data = await this.createStorageData();
7494
+ const dataKeys = Object.keys(data).filter((k) => k.startsWith("token-"));
7495
+ console.log(`[Payments][DEBUG] save(): TXF keys=${dataKeys.length} (${dataKeys.join(", ")})`);
7496
+ for (const [id, provider] of providers) {
7497
+ try {
7498
+ await provider.save(data);
7499
+ } catch (err) {
7500
+ console.error(`[Payments] Failed to save to provider ${id}:`, err);
7501
+ }
7341
7502
  }
7503
+ } else {
7504
+ console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
7342
7505
  }
7343
7506
  await this.savePendingV5Tokens();
7344
7507
  }
@@ -7374,6 +7537,7 @@ var PaymentsModule = class _PaymentsModule {
7374
7537
  }
7375
7538
  loadFromStorageData(data) {
7376
7539
  const parsed = parseTxfStorageData(data);
7540
+ console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
7377
7541
  this.tombstones = parsed.tombstones;
7378
7542
  this.tokens.clear();
7379
7543
  for (const token of parsed.tokens) {