@unicitylabs/sphere-sdk 0.4.9 → 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 (43) 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 +384 -119
  6. package/dist/core/index.cjs.map +1 -1
  7. package/dist/core/index.d.cts +244 -194
  8. package/dist/core/index.d.ts +244 -194
  9. package/dist/core/index.js +384 -119
  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 +67 -3
  16. package/dist/impl/browser/index.cjs.map +1 -1
  17. package/dist/impl/browser/index.js +67 -3
  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 +64 -5
  28. package/dist/impl/nodejs/index.cjs.map +1 -1
  29. package/dist/impl/nodejs/index.d.cts +668 -628
  30. package/dist/impl/nodejs/index.d.ts +668 -628
  31. package/dist/impl/nodejs/index.js +64 -5
  32. package/dist/impl/nodejs/index.js.map +1 -1
  33. package/dist/index.cjs +384 -119
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +248 -194
  36. package/dist/index.d.ts +248 -194
  37. package/dist/index.js +384 -119
  38. package/dist/index.js.map +1 -1
  39. package/dist/l1/index.cjs +3 -1
  40. package/dist/l1/index.cjs.map +1 -1
  41. package/dist/l1/index.js +3 -1
  42. package/dist/l1/index.js.map +1 -1
  43. 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,
@@ -3874,7 +3876,7 @@ var InstantSplitExecutor = class {
3874
3876
  token: JSON.stringify(bundle),
3875
3877
  proof: null,
3876
3878
  // Proof is included in the bundle
3877
- memo: "INSTANT_SPLIT_V5",
3879
+ memo: options?.memo,
3878
3880
  sender: {
3879
3881
  transportPubkey: senderPubkey
3880
3882
  }
@@ -4433,6 +4435,11 @@ var import_MintCommitment3 = require("@unicitylabs/state-transition-sdk/lib/tran
4433
4435
  var import_MintTransactionData3 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData");
4434
4436
  var import_InclusionProofUtils5 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
4435
4437
  var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof");
4438
+ function computeHistoryDedupKey(type, tokenId, transferId) {
4439
+ if (type === "SENT" && transferId) return `${type}_transfer_${transferId}`;
4440
+ if (tokenId) return `${type}_${tokenId}`;
4441
+ return `${type}_${crypto.randomUUID()}`;
4442
+ }
4436
4443
  function enrichWithRegistry(info) {
4437
4444
  const registry = TokenRegistry.getInstance();
4438
4445
  const def = registry.getDefinition(info.coinId);
@@ -4731,7 +4738,7 @@ var PaymentsModule = class _PaymentsModule {
4731
4738
  tombstones = [];
4732
4739
  archivedTokens = /* @__PURE__ */ new Map();
4733
4740
  forkedTokens = /* @__PURE__ */ new Map();
4734
- transactionHistory = [];
4741
+ _historyCache = [];
4735
4742
  nametags = [];
4736
4743
  // Payment Requests State (Incoming)
4737
4744
  paymentRequests = [];
@@ -4751,6 +4758,17 @@ var PaymentsModule = class _PaymentsModule {
4751
4758
  // Poll every 2s
4752
4759
  static PROOF_POLLING_MAX_ATTEMPTS = 30;
4753
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();
4754
4772
  // Storage event subscriptions (push-based sync)
4755
4773
  storageEventUnsubscribers = [];
4756
4774
  syncDebounceTimer = null;
@@ -4800,7 +4818,7 @@ var PaymentsModule = class _PaymentsModule {
4800
4818
  this.tombstones = [];
4801
4819
  this.archivedTokens.clear();
4802
4820
  this.forkedTokens.clear();
4803
- this.transactionHistory = [];
4821
+ this._historyCache = [];
4804
4822
  this.nametags = [];
4805
4823
  this.deps = deps;
4806
4824
  this.priceProvider = deps.price ?? null;
@@ -4836,38 +4854,40 @@ var PaymentsModule = class _PaymentsModule {
4836
4854
  */
4837
4855
  async load() {
4838
4856
  this.ensureInitialized();
4839
- await TokenRegistry.waitForReady();
4840
- const providers = this.getTokenStorageProviders();
4841
- for (const [id, provider] of providers) {
4842
- try {
4843
- const result = await provider.load();
4844
- if (result.success && result.data) {
4845
- this.loadFromStorageData(result.data);
4846
- this.log(`Loaded metadata from provider ${id}`);
4847
- 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);
4848
4870
  }
4849
- } catch (err) {
4850
- console.error(`[Payments] Failed to load from provider ${id}:`, err);
4851
- }
4852
- }
4853
- await this.loadPendingV5Tokens();
4854
- const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
4855
- if (historyData) {
4856
- try {
4857
- this.transactionHistory = JSON.parse(historyData);
4858
- } catch {
4859
- this.transactionHistory = [];
4860
4871
  }
4861
- }
4862
- const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4863
- if (pending2) {
4864
- const transfers = JSON.parse(pending2);
4865
- for (const transfer of transfers) {
4866
- 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
+ }
4867
4883
  }
4868
- }
4884
+ this.loaded = true;
4885
+ };
4886
+ this.loadedPromise = doLoad();
4887
+ await this.loadedPromise;
4869
4888
  this.resolveUnconfirmed().catch(() => {
4870
4889
  });
4890
+ this.scheduleResolveUnconfirmed();
4871
4891
  }
4872
4892
  /**
4873
4893
  * Cleanup all subscriptions, polling jobs, and pending resolvers.
@@ -4886,6 +4906,7 @@ var PaymentsModule = class _PaymentsModule {
4886
4906
  this.paymentRequestResponseHandlers.clear();
4887
4907
  this.stopProofPolling();
4888
4908
  this.proofPollingJobs.clear();
4909
+ this.stopResolveUnconfirmedPolling();
4889
4910
  for (const [, resolver] of this.pendingResponseResolvers) {
4890
4911
  clearTimeout(resolver.timeout);
4891
4912
  resolver.reject(new Error("Module destroyed"));
@@ -4945,7 +4966,7 @@ var PaymentsModule = class _PaymentsModule {
4945
4966
  }
4946
4967
  await this.saveToOutbox(result, recipientPubkey);
4947
4968
  result.status = "submitted";
4948
- const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
4969
+ const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
4949
4970
  const transferMode = request.transferMode ?? "instant";
4950
4971
  if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
4951
4972
  if (transferMode === "conservative") {
@@ -4976,7 +4997,7 @@ var PaymentsModule = class _PaymentsModule {
4976
4997
  updatedAt: Date.now(),
4977
4998
  sdkData: JSON.stringify(changeTokenData)
4978
4999
  };
4979
- await this.addToken(changeUiToken, true);
5000
+ await this.addToken(changeUiToken);
4980
5001
  this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
4981
5002
  await this.deps.transport.sendTokenTransfer(recipientPubkey, {
4982
5003
  sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
@@ -4985,7 +5006,7 @@ var PaymentsModule = class _PaymentsModule {
4985
5006
  });
4986
5007
  const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
4987
5008
  const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
4988
- await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
5009
+ await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
4989
5010
  result.tokenTransfers.push({
4990
5011
  sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
4991
5012
  method: "split",
@@ -5010,6 +5031,7 @@ var PaymentsModule = class _PaymentsModule {
5010
5031
  this.deps.transport,
5011
5032
  recipientPubkey,
5012
5033
  {
5034
+ memo: request.memo,
5013
5035
  onChangeTokenCreated: async (changeToken) => {
5014
5036
  const changeTokenData = changeToken.toJSON();
5015
5037
  const uiToken = {
@@ -5025,7 +5047,7 @@ var PaymentsModule = class _PaymentsModule {
5025
5047
  updatedAt: Date.now(),
5026
5048
  sdkData: JSON.stringify(changeTokenData)
5027
5049
  };
5028
- await this.addToken(uiToken, true);
5050
+ await this.addToken(uiToken);
5029
5051
  this.log(`Change token saved via background: ${uiToken.id}`);
5030
5052
  },
5031
5053
  onStorageSync: async () => {
@@ -5040,7 +5062,7 @@ var PaymentsModule = class _PaymentsModule {
5040
5062
  if (instantResult.backgroundPromise) {
5041
5063
  this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
5042
5064
  }
5043
- await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
5065
+ await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
5044
5066
  result.tokenTransfers.push({
5045
5067
  sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
5046
5068
  method: "split",
@@ -5087,20 +5109,25 @@ var PaymentsModule = class _PaymentsModule {
5087
5109
  requestIdHex
5088
5110
  });
5089
5111
  this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
5090
- await this.removeToken(token.id, recipientNametag, true);
5112
+ await this.removeToken(token.id);
5091
5113
  }
5092
5114
  result.status = "delivered";
5093
5115
  await this.save();
5094
5116
  await this.removeFromOutbox(result.id);
5095
5117
  result.status = "completed";
5118
+ const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
5096
5119
  await this.addToHistory({
5097
5120
  type: "SENT",
5098
5121
  amount: request.amount,
5099
5122
  coinId: request.coinId,
5100
5123
  symbol: this.getCoinSymbol(request.coinId),
5101
5124
  timestamp: Date.now(),
5125
+ recipientPubkey,
5102
5126
  recipientNametag,
5103
- transferId: result.id
5127
+ recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
5128
+ memo: request.memo,
5129
+ transferId: result.id,
5130
+ tokenId: sentTokenId || void 0
5104
5131
  });
5105
5132
  this.deps.emitEvent("transfer:confirmed", result);
5106
5133
  return result;
@@ -5211,6 +5238,7 @@ var PaymentsModule = class _PaymentsModule {
5211
5238
  recipientPubkey,
5212
5239
  {
5213
5240
  ...options,
5241
+ memo: request.memo,
5214
5242
  onChangeTokenCreated: async (changeToken) => {
5215
5243
  const changeTokenData = changeToken.toJSON();
5216
5244
  const uiToken = {
@@ -5226,7 +5254,7 @@ var PaymentsModule = class _PaymentsModule {
5226
5254
  updatedAt: Date.now(),
5227
5255
  sdkData: JSON.stringify(changeTokenData)
5228
5256
  };
5229
- await this.addToken(uiToken, true);
5257
+ await this.addToken(uiToken);
5230
5258
  this.log(`Change token saved via background: ${uiToken.id}`);
5231
5259
  },
5232
5260
  onStorageSync: async () => {
@@ -5239,15 +5267,20 @@ var PaymentsModule = class _PaymentsModule {
5239
5267
  if (result.backgroundPromise) {
5240
5268
  this.pendingBackgroundTasks.push(result.backgroundPromise);
5241
5269
  }
5242
- const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
5243
- await this.removeToken(tokenToSplit.id, recipientNametag, true);
5270
+ await this.removeToken(tokenToSplit.id);
5271
+ const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
5272
+ const splitTokenId = extractTokenIdFromSdkData(tokenToSplit.sdkData);
5244
5273
  await this.addToHistory({
5245
5274
  type: "SENT",
5246
5275
  amount: request.amount,
5247
5276
  coinId: request.coinId,
5248
5277
  symbol: this.getCoinSymbol(request.coinId),
5249
5278
  timestamp: Date.now(),
5250
- recipientNametag
5279
+ recipientPubkey,
5280
+ recipientNametag,
5281
+ recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
5282
+ memo: request.memo,
5283
+ tokenId: splitTokenId || void 0
5251
5284
  });
5252
5285
  await this.save();
5253
5286
  } else {
@@ -5278,15 +5311,18 @@ var PaymentsModule = class _PaymentsModule {
5278
5311
  * @param senderPubkey - Sender's public key for verification
5279
5312
  * @returns Processing result with finalized token
5280
5313
  */
5281
- async processInstantSplitBundle(bundle, senderPubkey) {
5314
+ async processInstantSplitBundle(bundle, senderPubkey, memo) {
5282
5315
  this.ensureInitialized();
5316
+ if (!this.loaded && this.loadedPromise) {
5317
+ await this.loadedPromise;
5318
+ }
5283
5319
  if (!isInstantSplitBundleV5(bundle)) {
5284
- return this.processInstantSplitBundleSync(bundle, senderPubkey);
5320
+ return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
5285
5321
  }
5286
5322
  try {
5287
5323
  const deterministicId = `v5split_${bundle.splitGroupId}`;
5288
- if (this.tokens.has(deterministicId)) {
5289
- 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`);
5290
5326
  return { success: true, durationMs: 0 };
5291
5327
  }
5292
5328
  const registry = TokenRegistry.getInstance();
@@ -5311,17 +5347,33 @@ var PaymentsModule = class _PaymentsModule {
5311
5347
  updatedAt: Date.now(),
5312
5348
  sdkData: JSON.stringify({ _pendingFinalization: pendingData })
5313
5349
  };
5314
- await this.addToken(uiToken, false);
5315
- this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
5350
+ await this.addToken(uiToken);
5351
+ this.processedSplitGroupIds.add(bundle.splitGroupId);
5352
+ await this.saveProcessedSplitGroupIds();
5353
+ const senderInfo = await this.resolveSenderInfo(senderPubkey);
5354
+ await this.addToHistory({
5355
+ type: "RECEIVED",
5356
+ amount: bundle.amount,
5357
+ coinId: bundle.coinId,
5358
+ symbol: uiToken.symbol,
5359
+ timestamp: Date.now(),
5360
+ senderPubkey,
5361
+ ...senderInfo,
5362
+ memo,
5363
+ tokenId: deterministicId
5364
+ });
5316
5365
  this.deps.emitEvent("transfer:incoming", {
5317
5366
  id: bundle.splitGroupId,
5318
5367
  senderPubkey,
5368
+ senderNametag: senderInfo.senderNametag,
5319
5369
  tokens: [uiToken],
5370
+ memo,
5320
5371
  receivedAt: Date.now()
5321
5372
  });
5322
5373
  await this.save();
5323
5374
  this.resolveUnconfirmed().catch(() => {
5324
5375
  });
5376
+ this.scheduleResolveUnconfirmed();
5325
5377
  return { success: true, durationMs: 0 };
5326
5378
  } catch (error) {
5327
5379
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -5336,7 +5388,7 @@ var PaymentsModule = class _PaymentsModule {
5336
5388
  * Synchronous V4 bundle processing (dev mode only).
5337
5389
  * Kept for backward compatibility with V4 bundles.
5338
5390
  */
5339
- async processInstantSplitBundleSync(bundle, senderPubkey) {
5391
+ async processInstantSplitBundleSync(bundle, senderPubkey, memo) {
5340
5392
  try {
5341
5393
  const signingService = await this.createSigningService();
5342
5394
  const stClient = this.deps.oracle.getStateTransitionClient?.();
@@ -5396,19 +5448,26 @@ var PaymentsModule = class _PaymentsModule {
5396
5448
  sdkData: JSON.stringify(tokenData)
5397
5449
  };
5398
5450
  await this.addToken(uiToken);
5451
+ const receivedTokenId = extractTokenIdFromSdkData(uiToken.sdkData);
5452
+ const senderInfo = await this.resolveSenderInfo(senderPubkey);
5399
5453
  await this.addToHistory({
5400
5454
  type: "RECEIVED",
5401
5455
  amount: bundle.amount,
5402
5456
  coinId: info.coinId,
5403
5457
  symbol: info.symbol,
5404
5458
  timestamp: Date.now(),
5405
- senderPubkey
5459
+ senderPubkey,
5460
+ ...senderInfo,
5461
+ memo,
5462
+ tokenId: receivedTokenId || uiToken.id
5406
5463
  });
5407
5464
  await this.save();
5408
5465
  this.deps.emitEvent("transfer:incoming", {
5409
5466
  id: bundle.splitGroupId,
5410
5467
  senderPubkey,
5468
+ senderNametag: senderInfo.senderNametag,
5411
5469
  tokens: [uiToken],
5470
+ memo,
5412
5471
  receivedAt: Date.now()
5413
5472
  });
5414
5473
  }
@@ -6061,28 +6120,70 @@ var PaymentsModule = class _PaymentsModule {
6061
6120
  };
6062
6121
  const stClient = this.deps.oracle.getStateTransitionClient?.();
6063
6122
  const trustBase = this.deps.oracle.getTrustBase?.();
6064
- 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
+ }
6065
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`);
6066
6130
  for (const [tokenId, token] of this.tokens) {
6067
6131
  if (token.status !== "submitted") continue;
6068
6132
  const pending2 = this.parsePendingFinalization(token.sdkData);
6069
6133
  if (!pending2) {
6134
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
6070
6135
  result.stillPending++;
6071
6136
  continue;
6072
6137
  }
6073
6138
  if (pending2.type === "v5_bundle") {
6139
+ console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
6074
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})`);
6075
6142
  result.details.push({ tokenId, stage: pending2.stage, status: progress });
6076
6143
  if (progress === "resolved") result.resolved++;
6077
6144
  else if (progress === "failed") result.failed++;
6078
6145
  else result.stillPending++;
6079
6146
  }
6080
6147
  }
6081
- 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}`);
6082
6150
  await this.save();
6083
6151
  }
6084
6152
  return result;
6085
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
+ }
6086
6187
  // ===========================================================================
6087
6188
  // Private - V5 Lazy Resolution Helpers
6088
6189
  // ===========================================================================
@@ -6095,10 +6196,12 @@ var PaymentsModule = class _PaymentsModule {
6095
6196
  pending2.lastAttemptAt = Date.now();
6096
6197
  try {
6097
6198
  if (pending2.stage === "RECEIVED") {
6199
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
6098
6200
  const mintDataJson = JSON.parse(bundle.recipientMintData);
6099
6201
  const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
6100
6202
  const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
6101
6203
  const mintResponse = await stClient.submitMintCommitment(mintCommitment);
6204
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
6102
6205
  if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
6103
6206
  throw new Error(`Mint submission failed: ${mintResponse.status}`);
6104
6207
  }
@@ -6106,22 +6209,27 @@ var PaymentsModule = class _PaymentsModule {
6106
6209
  this.updatePendingFinalization(token, pending2);
6107
6210
  }
6108
6211
  if (pending2.stage === "MINT_SUBMITTED") {
6212
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
6109
6213
  const mintDataJson = JSON.parse(bundle.recipientMintData);
6110
6214
  const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
6111
6215
  const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
6112
6216
  const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
6113
6217
  if (!proof) {
6218
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
6114
6219
  this.updatePendingFinalization(token, pending2);
6115
6220
  return "pending";
6116
6221
  }
6222
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
6117
6223
  pending2.mintProofJson = JSON.stringify(proof);
6118
6224
  pending2.stage = "MINT_PROVEN";
6119
6225
  this.updatePendingFinalization(token, pending2);
6120
6226
  }
6121
6227
  if (pending2.stage === "MINT_PROVEN") {
6228
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
6122
6229
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
6123
6230
  const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
6124
6231
  const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
6232
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
6125
6233
  if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
6126
6234
  throw new Error(`Transfer submission failed: ${transferResponse.status}`);
6127
6235
  }
@@ -6129,13 +6237,16 @@ var PaymentsModule = class _PaymentsModule {
6129
6237
  this.updatePendingFinalization(token, pending2);
6130
6238
  }
6131
6239
  if (pending2.stage === "TRANSFER_SUBMITTED") {
6240
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
6132
6241
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
6133
6242
  const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
6134
6243
  const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
6135
6244
  if (!proof) {
6245
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
6136
6246
  this.updatePendingFinalization(token, pending2);
6137
6247
  return "pending";
6138
6248
  }
6249
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
6139
6250
  const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
6140
6251
  const confirmedToken = {
6141
6252
  id: token.id,
@@ -6151,13 +6262,11 @@ var PaymentsModule = class _PaymentsModule {
6151
6262
  sdkData: JSON.stringify(finalizedToken.toJSON())
6152
6263
  };
6153
6264
  this.tokens.set(tokenId, confirmedToken);
6154
- await this.addToHistory({
6155
- type: "RECEIVED",
6156
- amount: confirmedToken.amount,
6157
- coinId: confirmedToken.coinId,
6158
- symbol: confirmedToken.symbol || "UNK",
6159
- timestamp: Date.now(),
6160
- senderPubkey: pending2.senderPubkey
6265
+ this.deps.emitEvent("transfer:confirmed", {
6266
+ id: crypto.randomUUID(),
6267
+ status: "completed",
6268
+ tokens: [confirmedToken],
6269
+ tokenTransfers: []
6161
6270
  });
6162
6271
  this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
6163
6272
  return "resolved";
@@ -6300,11 +6409,20 @@ var PaymentsModule = class _PaymentsModule {
6300
6409
  }
6301
6410
  }
6302
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)`);
6303
6414
  await this.deps.storage.set(
6304
6415
  STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
6305
- JSON.stringify(pendingTokens)
6416
+ json
6306
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
+ }
6307
6424
  } else {
6425
+ this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
6308
6426
  await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
6309
6427
  }
6310
6428
  }
@@ -6314,16 +6432,47 @@ var PaymentsModule = class _PaymentsModule {
6314
6432
  */
6315
6433
  async loadPendingV5Tokens() {
6316
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"}`);
6317
6436
  if (!data) return;
6318
6437
  try {
6319
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(", ")}`);
6320
6440
  for (const token of pendingTokens) {
6321
6441
  if (!this.tokens.has(token.id)) {
6322
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`);
6323
6446
  }
6324
6447
  }
6325
- if (pendingTokens.length > 0) {
6326
- 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);
6327
6476
  }
6328
6477
  } catch {
6329
6478
  }
@@ -6342,10 +6491,9 @@ var PaymentsModule = class _PaymentsModule {
6342
6491
  * the old state is archived and replaced with the incoming one.
6343
6492
  *
6344
6493
  * @param token - The token to add.
6345
- * @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
6346
6494
  * @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
6347
6495
  */
6348
- async addToken(token, skipHistory = false) {
6496
+ async addToken(token) {
6349
6497
  this.ensureInitialized();
6350
6498
  const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
6351
6499
  const incomingStateHash = extractStateHashFromSdkData(token.sdkData);
@@ -6391,15 +6539,6 @@ var PaymentsModule = class _PaymentsModule {
6391
6539
  }
6392
6540
  this.tokens.set(token.id, token);
6393
6541
  await this.archiveToken(token);
6394
- if (!skipHistory && token.coinId && token.amount) {
6395
- await this.addToHistory({
6396
- type: "RECEIVED",
6397
- amount: token.amount,
6398
- coinId: token.coinId,
6399
- symbol: token.symbol || "UNK",
6400
- timestamp: token.createdAt || Date.now()
6401
- });
6402
- }
6403
6542
  await this.save();
6404
6543
  this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
6405
6544
  return true;
@@ -6426,7 +6565,7 @@ var PaymentsModule = class _PaymentsModule {
6426
6565
  }
6427
6566
  }
6428
6567
  if (!found) {
6429
- await this.addToken(token, true);
6568
+ await this.addToken(token);
6430
6569
  return;
6431
6570
  }
6432
6571
  await this.archiveToken(token);
@@ -6441,10 +6580,8 @@ var PaymentsModule = class _PaymentsModule {
6441
6580
  * entry is created unless `skipHistory` is `true`.
6442
6581
  *
6443
6582
  * @param tokenId - Local UUID of the token to remove.
6444
- * @param recipientNametag - Optional nametag of the transfer recipient (for history).
6445
- * @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
6446
6583
  */
6447
- async removeToken(tokenId, recipientNametag, skipHistory = false) {
6584
+ async removeToken(tokenId) {
6448
6585
  this.ensureInitialized();
6449
6586
  const token = this.tokens.get(tokenId);
6450
6587
  if (!token) return;
@@ -6462,16 +6599,6 @@ var PaymentsModule = class _PaymentsModule {
6462
6599
  this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
6463
6600
  }
6464
6601
  this.tokens.delete(tokenId);
6465
- if (!skipHistory && token.coinId && token.amount) {
6466
- await this.addToHistory({
6467
- type: "SENT",
6468
- amount: token.amount,
6469
- coinId: token.coinId,
6470
- symbol: token.symbol || "UNK",
6471
- timestamp: Date.now(),
6472
- recipientNametag
6473
- });
6474
- }
6475
6602
  await this.save();
6476
6603
  }
6477
6604
  // ===========================================================================
@@ -6696,26 +6823,104 @@ var PaymentsModule = class _PaymentsModule {
6696
6823
  * @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
6697
6824
  */
6698
6825
  getHistory() {
6699
- return [...this.transactionHistory].sort((a, b) => b.timestamp - a.timestamp);
6826
+ return [...this._historyCache].sort((a, b) => b.timestamp - a.timestamp);
6827
+ }
6828
+ /**
6829
+ * Best-effort resolve sender's DIRECT address and nametag from their transport pubkey.
6830
+ * Returns empty object if transport doesn't support resolution or lookup fails.
6831
+ */
6832
+ async resolveSenderInfo(senderTransportPubkey) {
6833
+ try {
6834
+ if (this.deps?.transport?.resolveTransportPubkeyInfo) {
6835
+ const peerInfo = await this.deps.transport.resolveTransportPubkeyInfo(senderTransportPubkey);
6836
+ if (peerInfo) {
6837
+ return {
6838
+ senderAddress: peerInfo.directAddress || void 0,
6839
+ senderNametag: peerInfo.nametag || void 0
6840
+ };
6841
+ }
6842
+ }
6843
+ } catch {
6844
+ }
6845
+ return {};
6700
6846
  }
6701
6847
  /**
6702
6848
  * Append an entry to the transaction history.
6703
6849
  *
6704
- * A unique `id` is auto-generated. The entry is immediately persisted to storage.
6850
+ * A unique `id` and `dedupKey` are auto-generated. The entry is persisted to
6851
+ * the local token storage provider's `history` store (IndexedDB / file).
6852
+ * Duplicate entries with the same `dedupKey` are silently ignored (upsert).
6705
6853
  *
6706
- * @param entry - History entry fields (without `id`).
6854
+ * @param entry - History entry fields (without `id` and `dedupKey`).
6707
6855
  */
6708
6856
  async addToHistory(entry) {
6709
6857
  this.ensureInitialized();
6858
+ const dedupKey = computeHistoryDedupKey(entry.type, entry.tokenId, entry.transferId);
6710
6859
  const historyEntry = {
6711
6860
  id: crypto.randomUUID(),
6861
+ dedupKey,
6712
6862
  ...entry
6713
6863
  };
6714
- this.transactionHistory.push(historyEntry);
6715
- await this.deps.storage.set(
6716
- STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY,
6717
- JSON.stringify(this.transactionHistory)
6718
- );
6864
+ const provider = this.getLocalTokenStorageProvider();
6865
+ if (provider?.addHistoryEntry) {
6866
+ await provider.addHistoryEntry(historyEntry);
6867
+ }
6868
+ const existingIdx = this._historyCache.findIndex((e) => e.dedupKey === dedupKey);
6869
+ if (existingIdx >= 0) {
6870
+ this._historyCache[existingIdx] = historyEntry;
6871
+ } else {
6872
+ this._historyCache.push(historyEntry);
6873
+ }
6874
+ this.deps.emitEvent("history:updated", historyEntry);
6875
+ }
6876
+ /**
6877
+ * Load history from the local token storage provider into the in-memory cache.
6878
+ * Also performs one-time migration from legacy KV storage.
6879
+ */
6880
+ async loadHistory() {
6881
+ const provider = this.getLocalTokenStorageProvider();
6882
+ if (provider?.getHistoryEntries) {
6883
+ this._historyCache = await provider.getHistoryEntries();
6884
+ const legacyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
6885
+ if (legacyData) {
6886
+ try {
6887
+ const legacyEntries = JSON.parse(legacyData);
6888
+ const records = legacyEntries.map((e) => ({
6889
+ ...e,
6890
+ dedupKey: e.dedupKey || computeHistoryDedupKey(e.type, e.tokenId, e.transferId)
6891
+ }));
6892
+ const imported = await provider.importHistoryEntries?.(records) ?? 0;
6893
+ if (imported > 0) {
6894
+ this._historyCache = await provider.getHistoryEntries();
6895
+ this.log(`Migrated ${imported} history entries from KV to history store`);
6896
+ }
6897
+ await this.deps.storage.remove(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
6898
+ } catch {
6899
+ }
6900
+ }
6901
+ } else {
6902
+ const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
6903
+ if (historyData) {
6904
+ try {
6905
+ this._historyCache = JSON.parse(historyData);
6906
+ } catch {
6907
+ this._historyCache = [];
6908
+ }
6909
+ }
6910
+ }
6911
+ }
6912
+ /**
6913
+ * Get the first local token storage provider (for history operations).
6914
+ */
6915
+ getLocalTokenStorageProvider() {
6916
+ const providers = this.getTokenStorageProviders();
6917
+ for (const [, provider] of providers) {
6918
+ if (provider.type === "local") return provider;
6919
+ }
6920
+ for (const [, provider] of providers) {
6921
+ return provider;
6922
+ }
6923
+ return null;
6719
6924
  }
6720
6925
  // ===========================================================================
6721
6926
  // Public API - Nametag
@@ -6922,7 +7127,32 @@ var PaymentsModule = class _PaymentsModule {
6922
7127
  try {
6923
7128
  const result = await provider.sync(localData);
6924
7129
  if (result.success && result.merged) {
7130
+ const savedTokens = new Map(this.tokens);
6925
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
+ }
6926
7156
  if (this.nametags.length === 0 && savedNametags.length > 0) {
6927
7157
  this.nametags = savedNametags;
6928
7158
  }
@@ -7256,16 +7486,30 @@ var PaymentsModule = class _PaymentsModule {
7256
7486
  return;
7257
7487
  }
7258
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}`);
7259
7490
  await this.save();
7260
- 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}`);
7492
+ const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
7261
7493
  const incomingTransfer = {
7262
7494
  id: transfer.id,
7263
7495
  senderPubkey: transfer.senderTransportPubkey,
7496
+ senderNametag: senderInfo.senderNametag,
7264
7497
  tokens: [token],
7265
7498
  memo: payload.memo,
7266
7499
  receivedAt: transfer.timestamp
7267
7500
  };
7268
7501
  this.deps.emitEvent("transfer:incoming", incomingTransfer);
7502
+ await this.addToHistory({
7503
+ type: "RECEIVED",
7504
+ amount: token.amount,
7505
+ coinId: token.coinId,
7506
+ symbol: token.symbol,
7507
+ timestamp: Date.now(),
7508
+ senderPubkey: transfer.senderTransportPubkey,
7509
+ ...senderInfo,
7510
+ memo: payload.memo,
7511
+ tokenId: nostrTokenId || token.id
7512
+ });
7269
7513
  try {
7270
7514
  const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
7271
7515
  const requestIdBytes = commitment.requestId;
@@ -7283,7 +7527,7 @@ var PaymentsModule = class _PaymentsModule {
7283
7527
  attemptCount: 0,
7284
7528
  lastAttemptAt: 0,
7285
7529
  onProofReceived: async (tokenId) => {
7286
- await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, transfer.senderTransportPubkey);
7530
+ await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
7287
7531
  }
7288
7532
  });
7289
7533
  } catch (err) {
@@ -7342,7 +7586,7 @@ var PaymentsModule = class _PaymentsModule {
7342
7586
  /**
7343
7587
  * Finalize a received token after proof is available
7344
7588
  */
7345
- async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, senderPubkey) {
7589
+ async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput) {
7346
7590
  try {
7347
7591
  const token = this.tokens.get(tokenId);
7348
7592
  if (!token) {
@@ -7390,14 +7634,6 @@ var PaymentsModule = class _PaymentsModule {
7390
7634
  tokens: [finalizedToken],
7391
7635
  tokenTransfers: []
7392
7636
  });
7393
- await this.addToHistory({
7394
- type: "RECEIVED",
7395
- amount: finalizedToken.amount,
7396
- coinId: finalizedToken.coinId,
7397
- symbol: finalizedToken.symbol,
7398
- timestamp: Date.now(),
7399
- senderPubkey
7400
- });
7401
7637
  } catch (error) {
7402
7638
  console.error("[Payments] Failed to finalize received token:", error);
7403
7639
  const token = this.tokens.get(tokenId);
@@ -7409,8 +7645,12 @@ var PaymentsModule = class _PaymentsModule {
7409
7645
  }
7410
7646
  }
7411
7647
  async handleIncomingTransfer(transfer) {
7648
+ if (!this.loaded && this.loadedPromise) {
7649
+ await this.loadedPromise;
7650
+ }
7412
7651
  try {
7413
7652
  const payload = transfer.payload;
7653
+ console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
7414
7654
  let instantBundle = null;
7415
7655
  if (isInstantSplitBundle(payload)) {
7416
7656
  instantBundle = payload;
@@ -7428,7 +7668,8 @@ var PaymentsModule = class _PaymentsModule {
7428
7668
  try {
7429
7669
  const result = await this.processInstantSplitBundle(
7430
7670
  instantBundle,
7431
- transfer.senderTransportPubkey
7671
+ transfer.senderTransportPubkey,
7672
+ payload.memo
7432
7673
  );
7433
7674
  if (result.success) {
7434
7675
  this.log("INSTANT_SPLIT processed successfully");
@@ -7441,7 +7682,7 @@ var PaymentsModule = class _PaymentsModule {
7441
7682
  return;
7442
7683
  }
7443
7684
  if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
7444
- this.log("Processing NOSTR-FIRST commitment-only transfer...");
7685
+ console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
7445
7686
  await this.handleCommitmentOnlyTransfer(transfer, payload);
7446
7687
  return;
7447
7688
  }
@@ -7546,10 +7787,26 @@ var PaymentsModule = class _PaymentsModule {
7546
7787
  updatedAt: Date.now(),
7547
7788
  sdkData: typeof tokenData === "string" ? tokenData : JSON.stringify(tokenData)
7548
7789
  };
7549
- await this.addToken(token);
7790
+ const added = await this.addToken(token);
7791
+ const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
7792
+ if (added) {
7793
+ const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
7794
+ await this.addToHistory({
7795
+ type: "RECEIVED",
7796
+ amount: token.amount,
7797
+ coinId: token.coinId,
7798
+ symbol: token.symbol,
7799
+ timestamp: Date.now(),
7800
+ senderPubkey: transfer.senderTransportPubkey,
7801
+ ...senderInfo,
7802
+ memo: payload.memo,
7803
+ tokenId: incomingTokenId || token.id
7804
+ });
7805
+ }
7550
7806
  const incomingTransfer = {
7551
7807
  id: transfer.id,
7552
7808
  senderPubkey: transfer.senderTransportPubkey,
7809
+ senderNametag: senderInfo.senderNametag,
7553
7810
  tokens: [token],
7554
7811
  memo: payload.memo,
7555
7812
  receivedAt: transfer.timestamp
@@ -7588,17 +7845,24 @@ var PaymentsModule = class _PaymentsModule {
7588
7845
  // ===========================================================================
7589
7846
  async save() {
7590
7847
  const providers = this.getTokenStorageProviders();
7591
- if (providers.size === 0) {
7592
- this.log("No token storage providers - tokens not persisted");
7593
- return;
7594
- }
7595
- const data = await this.createStorageData();
7596
- for (const [id, provider] of providers) {
7597
- try {
7598
- await provider.save(data);
7599
- } catch (err) {
7600
- 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
+ }
7601
7863
  }
7864
+ } else {
7865
+ console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
7602
7866
  }
7603
7867
  await this.savePendingV5Tokens();
7604
7868
  }
@@ -7634,6 +7898,7 @@ var PaymentsModule = class _PaymentsModule {
7634
7898
  }
7635
7899
  loadFromStorageData(data) {
7636
7900
  const parsed = parseTxfStorageData(data);
7901
+ console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
7637
7902
  this.tombstones = parsed.tombstones;
7638
7903
  this.tokens.clear();
7639
7904
  for (const token of parsed.tokens) {