@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
package/dist/index.cjs CHANGED
@@ -107,7 +107,9 @@ var init_constants = __esm({
107
107
  /** Group chat: members for this address */
108
108
  GROUP_CHAT_MEMBERS: "group_chat_members",
109
109
  /** Group chat: processed event IDs for deduplication */
110
- GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events"
110
+ GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
111
+ /** Processed V5 split group IDs for Nostr re-delivery dedup */
112
+ PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
111
113
  };
112
114
  STORAGE_KEYS = {
113
115
  ...STORAGE_KEYS_GLOBAL,
@@ -4756,6 +4758,17 @@ var PaymentsModule = class _PaymentsModule {
4756
4758
  // Poll every 2s
4757
4759
  static PROOF_POLLING_MAX_ATTEMPTS = 30;
4758
4760
  // Max 30 attempts (~60s)
4761
+ // Periodic retry for resolveUnconfirmed (V5 lazy finalization)
4762
+ resolveUnconfirmedTimer = null;
4763
+ static RESOLVE_UNCONFIRMED_INTERVAL_MS = 1e4;
4764
+ // Retry every 10s
4765
+ // Guard: ensure load() completes before processing incoming bundles
4766
+ loadedPromise = null;
4767
+ loaded = false;
4768
+ // Persistent dedup: tracks splitGroupIds that have been fully processed.
4769
+ // Survives page reloads via KV storage so Nostr re-deliveries are ignored
4770
+ // even when the confirmed token's in-memory ID differs from v5split_{id}.
4771
+ processedSplitGroupIds = /* @__PURE__ */ new Set();
4759
4772
  // Storage event subscriptions (push-based sync)
4760
4773
  storageEventUnsubscribers = [];
4761
4774
  syncDebounceTimer = null;
@@ -4841,31 +4854,40 @@ var PaymentsModule = class _PaymentsModule {
4841
4854
  */
4842
4855
  async load() {
4843
4856
  this.ensureInitialized();
4844
- await TokenRegistry.waitForReady();
4845
- const providers = this.getTokenStorageProviders();
4846
- for (const [id, provider] of providers) {
4847
- try {
4848
- const result = await provider.load();
4849
- if (result.success && result.data) {
4850
- this.loadFromStorageData(result.data);
4851
- this.log(`Loaded metadata from provider ${id}`);
4852
- break;
4857
+ const doLoad = async () => {
4858
+ await TokenRegistry.waitForReady();
4859
+ const providers = this.getTokenStorageProviders();
4860
+ for (const [id, provider] of providers) {
4861
+ try {
4862
+ const result = await provider.load();
4863
+ if (result.success && result.data) {
4864
+ this.loadFromStorageData(result.data);
4865
+ this.log(`Loaded metadata from provider ${id}`);
4866
+ break;
4867
+ }
4868
+ } catch (err) {
4869
+ console.error(`[Payments] Failed to load from provider ${id}:`, err);
4853
4870
  }
4854
- } catch (err) {
4855
- console.error(`[Payments] Failed to load from provider ${id}:`, err);
4856
4871
  }
4857
- }
4858
- await this.loadPendingV5Tokens();
4859
- await this.loadHistory();
4860
- const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4861
- if (pending2) {
4862
- const transfers = JSON.parse(pending2);
4863
- for (const transfer of transfers) {
4864
- this.pendingTransfers.set(transfer.id, transfer);
4872
+ const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
4873
+ console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
4874
+ await this.loadPendingV5Tokens();
4875
+ await this.loadProcessedSplitGroupIds();
4876
+ await this.loadHistory();
4877
+ const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4878
+ if (pending2) {
4879
+ const transfers = JSON.parse(pending2);
4880
+ for (const transfer of transfers) {
4881
+ this.pendingTransfers.set(transfer.id, transfer);
4882
+ }
4865
4883
  }
4866
- }
4884
+ this.loaded = true;
4885
+ };
4886
+ this.loadedPromise = doLoad();
4887
+ await this.loadedPromise;
4867
4888
  this.resolveUnconfirmed().catch(() => {
4868
4889
  });
4890
+ this.scheduleResolveUnconfirmed();
4869
4891
  }
4870
4892
  /**
4871
4893
  * Cleanup all subscriptions, polling jobs, and pending resolvers.
@@ -4884,6 +4906,7 @@ var PaymentsModule = class _PaymentsModule {
4884
4906
  this.paymentRequestResponseHandlers.clear();
4885
4907
  this.stopProofPolling();
4886
4908
  this.proofPollingJobs.clear();
4909
+ this.stopResolveUnconfirmedPolling();
4887
4910
  for (const [, resolver] of this.pendingResponseResolvers) {
4888
4911
  clearTimeout(resolver.timeout);
4889
4912
  resolver.reject(new Error("Module destroyed"));
@@ -5290,13 +5313,16 @@ var PaymentsModule = class _PaymentsModule {
5290
5313
  */
5291
5314
  async processInstantSplitBundle(bundle, senderPubkey, memo) {
5292
5315
  this.ensureInitialized();
5316
+ if (!this.loaded && this.loadedPromise) {
5317
+ await this.loadedPromise;
5318
+ }
5293
5319
  if (!isInstantSplitBundleV5(bundle)) {
5294
5320
  return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
5295
5321
  }
5296
5322
  try {
5297
5323
  const deterministicId = `v5split_${bundle.splitGroupId}`;
5298
- if (this.tokens.has(deterministicId)) {
5299
- this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
5324
+ if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
5325
+ console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
5300
5326
  return { success: true, durationMs: 0 };
5301
5327
  }
5302
5328
  const registry = TokenRegistry.getInstance();
@@ -5322,7 +5348,8 @@ var PaymentsModule = class _PaymentsModule {
5322
5348
  sdkData: JSON.stringify({ _pendingFinalization: pendingData })
5323
5349
  };
5324
5350
  await this.addToken(uiToken);
5325
- this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
5351
+ this.processedSplitGroupIds.add(bundle.splitGroupId);
5352
+ await this.saveProcessedSplitGroupIds();
5326
5353
  const senderInfo = await this.resolveSenderInfo(senderPubkey);
5327
5354
  await this.addToHistory({
5328
5355
  type: "RECEIVED",
@@ -5346,6 +5373,7 @@ var PaymentsModule = class _PaymentsModule {
5346
5373
  await this.save();
5347
5374
  this.resolveUnconfirmed().catch(() => {
5348
5375
  });
5376
+ this.scheduleResolveUnconfirmed();
5349
5377
  return { success: true, durationMs: 0 };
5350
5378
  } catch (error) {
5351
5379
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -6092,28 +6120,70 @@ var PaymentsModule = class _PaymentsModule {
6092
6120
  };
6093
6121
  const stClient = this.deps.oracle.getStateTransitionClient?.();
6094
6122
  const trustBase = this.deps.oracle.getTrustBase?.();
6095
- if (!stClient || !trustBase) return result;
6123
+ if (!stClient || !trustBase) {
6124
+ console.log(`[V5-RESOLVE] resolveUnconfirmed: EARLY EXIT \u2014 stClient=${!!stClient} trustBase=${!!trustBase}`);
6125
+ return result;
6126
+ }
6096
6127
  const signingService = await this.createSigningService();
6128
+ const submittedCount = Array.from(this.tokens.values()).filter((t) => t.status === "submitted").length;
6129
+ console.log(`[V5-RESOLVE] resolveUnconfirmed: ${submittedCount} submitted token(s) to process`);
6097
6130
  for (const [tokenId, token] of this.tokens) {
6098
6131
  if (token.status !== "submitted") continue;
6099
6132
  const pending2 = this.parsePendingFinalization(token.sdkData);
6100
6133
  if (!pending2) {
6134
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
6101
6135
  result.stillPending++;
6102
6136
  continue;
6103
6137
  }
6104
6138
  if (pending2.type === "v5_bundle") {
6139
+ console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
6105
6140
  const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
6141
+ console.log(`[V5-RESOLVE] Result for ${tokenId.slice(0, 16)}...: ${progress} (stage now: ${pending2.stage})`);
6106
6142
  result.details.push({ tokenId, stage: pending2.stage, status: progress });
6107
6143
  if (progress === "resolved") result.resolved++;
6108
6144
  else if (progress === "failed") result.failed++;
6109
6145
  else result.stillPending++;
6110
6146
  }
6111
6147
  }
6112
- if (result.resolved > 0 || result.failed > 0) {
6148
+ if (result.resolved > 0 || result.failed > 0 || result.stillPending > 0) {
6149
+ console.log(`[V5-RESOLVE] Saving: resolved=${result.resolved} failed=${result.failed} stillPending=${result.stillPending}`);
6113
6150
  await this.save();
6114
6151
  }
6115
6152
  return result;
6116
6153
  }
6154
+ /**
6155
+ * Start a periodic interval that retries resolveUnconfirmed() until all
6156
+ * tokens are confirmed or failed. Stops automatically when nothing is
6157
+ * pending and is cleaned up by destroy().
6158
+ */
6159
+ scheduleResolveUnconfirmed() {
6160
+ if (this.resolveUnconfirmedTimer) return;
6161
+ const hasUnconfirmed = Array.from(this.tokens.values()).some(
6162
+ (t) => t.status === "submitted"
6163
+ );
6164
+ if (!hasUnconfirmed) {
6165
+ console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: no submitted tokens, not starting timer`);
6166
+ return;
6167
+ }
6168
+ console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: starting periodic retry (every ${_PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS}ms)`);
6169
+ this.resolveUnconfirmedTimer = setInterval(async () => {
6170
+ try {
6171
+ const result = await this.resolveUnconfirmed();
6172
+ if (result.stillPending === 0) {
6173
+ console.log(`[V5-RESOLVE] All tokens resolved, stopping periodic retry`);
6174
+ this.stopResolveUnconfirmedPolling();
6175
+ }
6176
+ } catch (err) {
6177
+ console.log(`[V5-RESOLVE] Periodic retry error:`, err);
6178
+ }
6179
+ }, _PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS);
6180
+ }
6181
+ stopResolveUnconfirmedPolling() {
6182
+ if (this.resolveUnconfirmedTimer) {
6183
+ clearInterval(this.resolveUnconfirmedTimer);
6184
+ this.resolveUnconfirmedTimer = null;
6185
+ }
6186
+ }
6117
6187
  // ===========================================================================
6118
6188
  // Private - V5 Lazy Resolution Helpers
6119
6189
  // ===========================================================================
@@ -6126,10 +6196,12 @@ var PaymentsModule = class _PaymentsModule {
6126
6196
  pending2.lastAttemptAt = Date.now();
6127
6197
  try {
6128
6198
  if (pending2.stage === "RECEIVED") {
6199
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
6129
6200
  const mintDataJson = JSON.parse(bundle.recipientMintData);
6130
6201
  const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
6131
6202
  const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
6132
6203
  const mintResponse = await stClient.submitMintCommitment(mintCommitment);
6204
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
6133
6205
  if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
6134
6206
  throw new Error(`Mint submission failed: ${mintResponse.status}`);
6135
6207
  }
@@ -6137,22 +6209,27 @@ var PaymentsModule = class _PaymentsModule {
6137
6209
  this.updatePendingFinalization(token, pending2);
6138
6210
  }
6139
6211
  if (pending2.stage === "MINT_SUBMITTED") {
6212
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
6140
6213
  const mintDataJson = JSON.parse(bundle.recipientMintData);
6141
6214
  const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
6142
6215
  const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
6143
6216
  const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
6144
6217
  if (!proof) {
6218
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
6145
6219
  this.updatePendingFinalization(token, pending2);
6146
6220
  return "pending";
6147
6221
  }
6222
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
6148
6223
  pending2.mintProofJson = JSON.stringify(proof);
6149
6224
  pending2.stage = "MINT_PROVEN";
6150
6225
  this.updatePendingFinalization(token, pending2);
6151
6226
  }
6152
6227
  if (pending2.stage === "MINT_PROVEN") {
6228
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
6153
6229
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
6154
6230
  const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
6155
6231
  const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
6232
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
6156
6233
  if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
6157
6234
  throw new Error(`Transfer submission failed: ${transferResponse.status}`);
6158
6235
  }
@@ -6160,13 +6237,16 @@ var PaymentsModule = class _PaymentsModule {
6160
6237
  this.updatePendingFinalization(token, pending2);
6161
6238
  }
6162
6239
  if (pending2.stage === "TRANSFER_SUBMITTED") {
6240
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
6163
6241
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
6164
6242
  const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
6165
6243
  const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
6166
6244
  if (!proof) {
6245
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
6167
6246
  this.updatePendingFinalization(token, pending2);
6168
6247
  return "pending";
6169
6248
  }
6249
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
6170
6250
  const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
6171
6251
  const confirmedToken = {
6172
6252
  id: token.id,
@@ -6182,6 +6262,12 @@ var PaymentsModule = class _PaymentsModule {
6182
6262
  sdkData: JSON.stringify(finalizedToken.toJSON())
6183
6263
  };
6184
6264
  this.tokens.set(tokenId, confirmedToken);
6265
+ this.deps.emitEvent("transfer:confirmed", {
6266
+ id: crypto.randomUUID(),
6267
+ status: "completed",
6268
+ tokens: [confirmedToken],
6269
+ tokenTransfers: []
6270
+ });
6185
6271
  this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
6186
6272
  return "resolved";
6187
6273
  }
@@ -6323,11 +6409,20 @@ var PaymentsModule = class _PaymentsModule {
6323
6409
  }
6324
6410
  }
6325
6411
  if (pendingTokens.length > 0) {
6412
+ const json = JSON.stringify(pendingTokens);
6413
+ this.log(`[V5-PERSIST] Saving ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")} (${json.length} bytes)`);
6326
6414
  await this.deps.storage.set(
6327
6415
  STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
6328
- JSON.stringify(pendingTokens)
6416
+ json
6329
6417
  );
6418
+ const verify = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
6419
+ if (!verify) {
6420
+ console.error("[Payments][V5-PERSIST] CRITICAL: KV write succeeded but read-back is empty!");
6421
+ } else {
6422
+ this.log(`[V5-PERSIST] Verified: read-back ${verify.length} bytes`);
6423
+ }
6330
6424
  } else {
6425
+ this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
6331
6426
  await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
6332
6427
  }
6333
6428
  }
@@ -6337,16 +6432,47 @@ var PaymentsModule = class _PaymentsModule {
6337
6432
  */
6338
6433
  async loadPendingV5Tokens() {
6339
6434
  const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
6435
+ this.log(`[V5-PERSIST] loadPendingV5Tokens: KV data = ${data ? `${data.length} bytes` : "null/empty"}`);
6340
6436
  if (!data) return;
6341
6437
  try {
6342
6438
  const pendingTokens = JSON.parse(data);
6439
+ this.log(`[V5-PERSIST] Parsed ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")}`);
6343
6440
  for (const token of pendingTokens) {
6344
6441
  if (!this.tokens.has(token.id)) {
6345
6442
  this.tokens.set(token.id, token);
6443
+ this.log(`[V5-PERSIST] Restored token ${token.id.slice(0, 16)} (status=${token.status})`);
6444
+ } else {
6445
+ this.log(`[V5-PERSIST] Token ${token.id.slice(0, 16)} already in map, skipping`);
6346
6446
  }
6347
6447
  }
6348
- if (pendingTokens.length > 0) {
6349
- this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
6448
+ } catch (err) {
6449
+ console.error("[Payments][V5-PERSIST] Failed to parse pending V5 tokens:", err);
6450
+ }
6451
+ }
6452
+ /**
6453
+ * Persist the set of processed splitGroupIds to KV storage.
6454
+ * This ensures Nostr re-deliveries are ignored across page reloads,
6455
+ * even when the confirmed token's in-memory ID differs from v5split_{id}.
6456
+ */
6457
+ async saveProcessedSplitGroupIds() {
6458
+ const ids = Array.from(this.processedSplitGroupIds);
6459
+ if (ids.length > 0) {
6460
+ await this.deps.storage.set(
6461
+ STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS,
6462
+ JSON.stringify(ids)
6463
+ );
6464
+ }
6465
+ }
6466
+ /**
6467
+ * Load processed splitGroupIds from KV storage.
6468
+ */
6469
+ async loadProcessedSplitGroupIds() {
6470
+ const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS);
6471
+ if (!data) return;
6472
+ try {
6473
+ const ids = JSON.parse(data);
6474
+ for (const id of ids) {
6475
+ this.processedSplitGroupIds.add(id);
6350
6476
  }
6351
6477
  } catch {
6352
6478
  }
@@ -7001,7 +7127,32 @@ var PaymentsModule = class _PaymentsModule {
7001
7127
  try {
7002
7128
  const result = await provider.sync(localData);
7003
7129
  if (result.success && result.merged) {
7130
+ const savedTokens = new Map(this.tokens);
7004
7131
  this.loadFromStorageData(result.merged);
7132
+ let restoredCount = 0;
7133
+ for (const [tokenId, token] of savedTokens) {
7134
+ if (this.tokens.has(tokenId)) continue;
7135
+ const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
7136
+ const stateHash = extractStateHashFromSdkData(token.sdkData);
7137
+ if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
7138
+ continue;
7139
+ }
7140
+ if (sdkTokenId) {
7141
+ let hasEquivalent = false;
7142
+ for (const existing of this.tokens.values()) {
7143
+ if (extractTokenIdFromSdkData(existing.sdkData) === sdkTokenId) {
7144
+ hasEquivalent = true;
7145
+ break;
7146
+ }
7147
+ }
7148
+ if (hasEquivalent) continue;
7149
+ }
7150
+ this.tokens.set(tokenId, token);
7151
+ restoredCount++;
7152
+ }
7153
+ if (restoredCount > 0) {
7154
+ console.log(`[Payments] Sync: restored ${restoredCount} token(s) lost by loadFromStorageData`);
7155
+ }
7005
7156
  if (this.nametags.length === 0 && savedNametags.length > 0) {
7006
7157
  this.nametags = savedNametags;
7007
7158
  }
@@ -7335,8 +7486,9 @@ var PaymentsModule = class _PaymentsModule {
7335
7486
  return;
7336
7487
  }
7337
7488
  this.tokens.set(token.id, token);
7489
+ console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
7338
7490
  await this.save();
7339
- this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
7491
+ console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
7340
7492
  const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
7341
7493
  const incomingTransfer = {
7342
7494
  id: transfer.id,
@@ -7493,8 +7645,12 @@ var PaymentsModule = class _PaymentsModule {
7493
7645
  }
7494
7646
  }
7495
7647
  async handleIncomingTransfer(transfer) {
7648
+ if (!this.loaded && this.loadedPromise) {
7649
+ await this.loadedPromise;
7650
+ }
7496
7651
  try {
7497
7652
  const payload = transfer.payload;
7653
+ console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
7498
7654
  let instantBundle = null;
7499
7655
  if (isInstantSplitBundle(payload)) {
7500
7656
  instantBundle = payload;
@@ -7526,7 +7682,7 @@ var PaymentsModule = class _PaymentsModule {
7526
7682
  return;
7527
7683
  }
7528
7684
  if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
7529
- this.log("Processing NOSTR-FIRST commitment-only transfer...");
7685
+ console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
7530
7686
  await this.handleCommitmentOnlyTransfer(transfer, payload);
7531
7687
  return;
7532
7688
  }
@@ -7689,17 +7845,24 @@ var PaymentsModule = class _PaymentsModule {
7689
7845
  // ===========================================================================
7690
7846
  async save() {
7691
7847
  const providers = this.getTokenStorageProviders();
7692
- if (providers.size === 0) {
7693
- this.log("No token storage providers - tokens not persisted");
7694
- return;
7695
- }
7696
- const data = await this.createStorageData();
7697
- for (const [id, provider] of providers) {
7698
- try {
7699
- await provider.save(data);
7700
- } catch (err) {
7701
- console.error(`[Payments] Failed to save to provider ${id}:`, err);
7848
+ const tokenStats = Array.from(this.tokens.values()).map((t) => {
7849
+ const txf = tokenToTxf(t);
7850
+ return `${t.id.slice(0, 12)}(${t.status},txf=${!!txf})`;
7851
+ });
7852
+ console.log(`[Payments][DEBUG] save(): providers=${providers.size}, tokens=[${tokenStats.join(", ")}]`);
7853
+ if (providers.size > 0) {
7854
+ const data = await this.createStorageData();
7855
+ const dataKeys = Object.keys(data).filter((k) => k.startsWith("token-"));
7856
+ console.log(`[Payments][DEBUG] save(): TXF keys=${dataKeys.length} (${dataKeys.join(", ")})`);
7857
+ for (const [id, provider] of providers) {
7858
+ try {
7859
+ await provider.save(data);
7860
+ } catch (err) {
7861
+ console.error(`[Payments] Failed to save to provider ${id}:`, err);
7862
+ }
7702
7863
  }
7864
+ } else {
7865
+ console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
7703
7866
  }
7704
7867
  await this.savePendingV5Tokens();
7705
7868
  }
@@ -7735,6 +7898,7 @@ var PaymentsModule = class _PaymentsModule {
7735
7898
  }
7736
7899
  loadFromStorageData(data) {
7737
7900
  const parsed = parseTxfStorageData(data);
7901
+ console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
7738
7902
  this.tombstones = parsed.tombstones;
7739
7903
  this.tokens.clear();
7740
7904
  for (const token of parsed.tokens) {