@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
@@ -87,7 +87,9 @@ var init_constants = __esm({
87
87
  /** Group chat: members for this address */
88
88
  GROUP_CHAT_MEMBERS: "group_chat_members",
89
89
  /** Group chat: processed event IDs for deduplication */
90
- GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events"
90
+ GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
91
+ /** Processed V5 split group IDs for Nostr re-delivery dedup */
92
+ PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
91
93
  };
92
94
  STORAGE_KEYS = {
93
95
  ...STORAGE_KEYS_GLOBAL,
@@ -3418,7 +3420,7 @@ var InstantSplitExecutor = class {
3418
3420
  token: JSON.stringify(bundle),
3419
3421
  proof: null,
3420
3422
  // Proof is included in the bundle
3421
- memo: "INSTANT_SPLIT_V5",
3423
+ memo: options?.memo,
3422
3424
  sender: {
3423
3425
  transportPubkey: senderPubkey
3424
3426
  }
@@ -3977,6 +3979,11 @@ import { MintCommitment as MintCommitment3 } from "@unicitylabs/state-transition
3977
3979
  import { MintTransactionData as MintTransactionData3 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData";
3978
3980
  import { waitInclusionProof as waitInclusionProof5 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
3979
3981
  import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof";
3982
+ function computeHistoryDedupKey(type, tokenId, transferId) {
3983
+ if (type === "SENT" && transferId) return `${type}_transfer_${transferId}`;
3984
+ if (tokenId) return `${type}_${tokenId}`;
3985
+ return `${type}_${crypto.randomUUID()}`;
3986
+ }
3980
3987
  function enrichWithRegistry(info) {
3981
3988
  const registry = TokenRegistry.getInstance();
3982
3989
  const def = registry.getDefinition(info.coinId);
@@ -4275,7 +4282,7 @@ var PaymentsModule = class _PaymentsModule {
4275
4282
  tombstones = [];
4276
4283
  archivedTokens = /* @__PURE__ */ new Map();
4277
4284
  forkedTokens = /* @__PURE__ */ new Map();
4278
- transactionHistory = [];
4285
+ _historyCache = [];
4279
4286
  nametags = [];
4280
4287
  // Payment Requests State (Incoming)
4281
4288
  paymentRequests = [];
@@ -4295,6 +4302,17 @@ var PaymentsModule = class _PaymentsModule {
4295
4302
  // Poll every 2s
4296
4303
  static PROOF_POLLING_MAX_ATTEMPTS = 30;
4297
4304
  // Max 30 attempts (~60s)
4305
+ // Periodic retry for resolveUnconfirmed (V5 lazy finalization)
4306
+ resolveUnconfirmedTimer = null;
4307
+ static RESOLVE_UNCONFIRMED_INTERVAL_MS = 1e4;
4308
+ // Retry every 10s
4309
+ // Guard: ensure load() completes before processing incoming bundles
4310
+ loadedPromise = null;
4311
+ loaded = false;
4312
+ // Persistent dedup: tracks splitGroupIds that have been fully processed.
4313
+ // Survives page reloads via KV storage so Nostr re-deliveries are ignored
4314
+ // even when the confirmed token's in-memory ID differs from v5split_{id}.
4315
+ processedSplitGroupIds = /* @__PURE__ */ new Set();
4298
4316
  // Storage event subscriptions (push-based sync)
4299
4317
  storageEventUnsubscribers = [];
4300
4318
  syncDebounceTimer = null;
@@ -4344,7 +4362,7 @@ var PaymentsModule = class _PaymentsModule {
4344
4362
  this.tombstones = [];
4345
4363
  this.archivedTokens.clear();
4346
4364
  this.forkedTokens.clear();
4347
- this.transactionHistory = [];
4365
+ this._historyCache = [];
4348
4366
  this.nametags = [];
4349
4367
  this.deps = deps;
4350
4368
  this.priceProvider = deps.price ?? null;
@@ -4380,38 +4398,40 @@ var PaymentsModule = class _PaymentsModule {
4380
4398
  */
4381
4399
  async load() {
4382
4400
  this.ensureInitialized();
4383
- await TokenRegistry.waitForReady();
4384
- const providers = this.getTokenStorageProviders();
4385
- for (const [id, provider] of providers) {
4386
- try {
4387
- const result = await provider.load();
4388
- if (result.success && result.data) {
4389
- this.loadFromStorageData(result.data);
4390
- this.log(`Loaded metadata from provider ${id}`);
4391
- break;
4401
+ const doLoad = async () => {
4402
+ await TokenRegistry.waitForReady();
4403
+ const providers = this.getTokenStorageProviders();
4404
+ for (const [id, provider] of providers) {
4405
+ try {
4406
+ const result = await provider.load();
4407
+ if (result.success && result.data) {
4408
+ this.loadFromStorageData(result.data);
4409
+ this.log(`Loaded metadata from provider ${id}`);
4410
+ break;
4411
+ }
4412
+ } catch (err) {
4413
+ console.error(`[Payments] Failed to load from provider ${id}:`, err);
4392
4414
  }
4393
- } catch (err) {
4394
- console.error(`[Payments] Failed to load from provider ${id}:`, err);
4395
- }
4396
- }
4397
- await this.loadPendingV5Tokens();
4398
- const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
4399
- if (historyData) {
4400
- try {
4401
- this.transactionHistory = JSON.parse(historyData);
4402
- } catch {
4403
- this.transactionHistory = [];
4404
4415
  }
4405
- }
4406
- const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4407
- if (pending2) {
4408
- const transfers = JSON.parse(pending2);
4409
- for (const transfer of transfers) {
4410
- this.pendingTransfers.set(transfer.id, transfer);
4416
+ const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
4417
+ console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
4418
+ await this.loadPendingV5Tokens();
4419
+ await this.loadProcessedSplitGroupIds();
4420
+ await this.loadHistory();
4421
+ const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4422
+ if (pending2) {
4423
+ const transfers = JSON.parse(pending2);
4424
+ for (const transfer of transfers) {
4425
+ this.pendingTransfers.set(transfer.id, transfer);
4426
+ }
4411
4427
  }
4412
- }
4428
+ this.loaded = true;
4429
+ };
4430
+ this.loadedPromise = doLoad();
4431
+ await this.loadedPromise;
4413
4432
  this.resolveUnconfirmed().catch(() => {
4414
4433
  });
4434
+ this.scheduleResolveUnconfirmed();
4415
4435
  }
4416
4436
  /**
4417
4437
  * Cleanup all subscriptions, polling jobs, and pending resolvers.
@@ -4430,6 +4450,7 @@ var PaymentsModule = class _PaymentsModule {
4430
4450
  this.paymentRequestResponseHandlers.clear();
4431
4451
  this.stopProofPolling();
4432
4452
  this.proofPollingJobs.clear();
4453
+ this.stopResolveUnconfirmedPolling();
4433
4454
  for (const [, resolver] of this.pendingResponseResolvers) {
4434
4455
  clearTimeout(resolver.timeout);
4435
4456
  resolver.reject(new Error("Module destroyed"));
@@ -4489,7 +4510,7 @@ var PaymentsModule = class _PaymentsModule {
4489
4510
  }
4490
4511
  await this.saveToOutbox(result, recipientPubkey);
4491
4512
  result.status = "submitted";
4492
- const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
4513
+ const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
4493
4514
  const transferMode = request.transferMode ?? "instant";
4494
4515
  if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
4495
4516
  if (transferMode === "conservative") {
@@ -4520,7 +4541,7 @@ var PaymentsModule = class _PaymentsModule {
4520
4541
  updatedAt: Date.now(),
4521
4542
  sdkData: JSON.stringify(changeTokenData)
4522
4543
  };
4523
- await this.addToken(changeUiToken, true);
4544
+ await this.addToken(changeUiToken);
4524
4545
  this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
4525
4546
  await this.deps.transport.sendTokenTransfer(recipientPubkey, {
4526
4547
  sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
@@ -4529,7 +4550,7 @@ var PaymentsModule = class _PaymentsModule {
4529
4550
  });
4530
4551
  const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
4531
4552
  const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
4532
- await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
4553
+ await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
4533
4554
  result.tokenTransfers.push({
4534
4555
  sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
4535
4556
  method: "split",
@@ -4554,6 +4575,7 @@ var PaymentsModule = class _PaymentsModule {
4554
4575
  this.deps.transport,
4555
4576
  recipientPubkey,
4556
4577
  {
4578
+ memo: request.memo,
4557
4579
  onChangeTokenCreated: async (changeToken) => {
4558
4580
  const changeTokenData = changeToken.toJSON();
4559
4581
  const uiToken = {
@@ -4569,7 +4591,7 @@ var PaymentsModule = class _PaymentsModule {
4569
4591
  updatedAt: Date.now(),
4570
4592
  sdkData: JSON.stringify(changeTokenData)
4571
4593
  };
4572
- await this.addToken(uiToken, true);
4594
+ await this.addToken(uiToken);
4573
4595
  this.log(`Change token saved via background: ${uiToken.id}`);
4574
4596
  },
4575
4597
  onStorageSync: async () => {
@@ -4584,7 +4606,7 @@ var PaymentsModule = class _PaymentsModule {
4584
4606
  if (instantResult.backgroundPromise) {
4585
4607
  this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
4586
4608
  }
4587
- await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
4609
+ await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
4588
4610
  result.tokenTransfers.push({
4589
4611
  sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
4590
4612
  method: "split",
@@ -4631,20 +4653,25 @@ var PaymentsModule = class _PaymentsModule {
4631
4653
  requestIdHex
4632
4654
  });
4633
4655
  this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
4634
- await this.removeToken(token.id, recipientNametag, true);
4656
+ await this.removeToken(token.id);
4635
4657
  }
4636
4658
  result.status = "delivered";
4637
4659
  await this.save();
4638
4660
  await this.removeFromOutbox(result.id);
4639
4661
  result.status = "completed";
4662
+ const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
4640
4663
  await this.addToHistory({
4641
4664
  type: "SENT",
4642
4665
  amount: request.amount,
4643
4666
  coinId: request.coinId,
4644
4667
  symbol: this.getCoinSymbol(request.coinId),
4645
4668
  timestamp: Date.now(),
4669
+ recipientPubkey,
4646
4670
  recipientNametag,
4647
- transferId: result.id
4671
+ recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
4672
+ memo: request.memo,
4673
+ transferId: result.id,
4674
+ tokenId: sentTokenId || void 0
4648
4675
  });
4649
4676
  this.deps.emitEvent("transfer:confirmed", result);
4650
4677
  return result;
@@ -4755,6 +4782,7 @@ var PaymentsModule = class _PaymentsModule {
4755
4782
  recipientPubkey,
4756
4783
  {
4757
4784
  ...options,
4785
+ memo: request.memo,
4758
4786
  onChangeTokenCreated: async (changeToken) => {
4759
4787
  const changeTokenData = changeToken.toJSON();
4760
4788
  const uiToken = {
@@ -4770,7 +4798,7 @@ var PaymentsModule = class _PaymentsModule {
4770
4798
  updatedAt: Date.now(),
4771
4799
  sdkData: JSON.stringify(changeTokenData)
4772
4800
  };
4773
- await this.addToken(uiToken, true);
4801
+ await this.addToken(uiToken);
4774
4802
  this.log(`Change token saved via background: ${uiToken.id}`);
4775
4803
  },
4776
4804
  onStorageSync: async () => {
@@ -4783,15 +4811,20 @@ var PaymentsModule = class _PaymentsModule {
4783
4811
  if (result.backgroundPromise) {
4784
4812
  this.pendingBackgroundTasks.push(result.backgroundPromise);
4785
4813
  }
4786
- const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
4787
- await this.removeToken(tokenToSplit.id, recipientNametag, true);
4814
+ await this.removeToken(tokenToSplit.id);
4815
+ const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
4816
+ const splitTokenId = extractTokenIdFromSdkData(tokenToSplit.sdkData);
4788
4817
  await this.addToHistory({
4789
4818
  type: "SENT",
4790
4819
  amount: request.amount,
4791
4820
  coinId: request.coinId,
4792
4821
  symbol: this.getCoinSymbol(request.coinId),
4793
4822
  timestamp: Date.now(),
4794
- recipientNametag
4823
+ recipientPubkey,
4824
+ recipientNametag,
4825
+ recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
4826
+ memo: request.memo,
4827
+ tokenId: splitTokenId || void 0
4795
4828
  });
4796
4829
  await this.save();
4797
4830
  } else {
@@ -4822,15 +4855,18 @@ var PaymentsModule = class _PaymentsModule {
4822
4855
  * @param senderPubkey - Sender's public key for verification
4823
4856
  * @returns Processing result with finalized token
4824
4857
  */
4825
- async processInstantSplitBundle(bundle, senderPubkey) {
4858
+ async processInstantSplitBundle(bundle, senderPubkey, memo) {
4826
4859
  this.ensureInitialized();
4860
+ if (!this.loaded && this.loadedPromise) {
4861
+ await this.loadedPromise;
4862
+ }
4827
4863
  if (!isInstantSplitBundleV5(bundle)) {
4828
- return this.processInstantSplitBundleSync(bundle, senderPubkey);
4864
+ return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
4829
4865
  }
4830
4866
  try {
4831
4867
  const deterministicId = `v5split_${bundle.splitGroupId}`;
4832
- if (this.tokens.has(deterministicId)) {
4833
- this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
4868
+ if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
4869
+ console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
4834
4870
  return { success: true, durationMs: 0 };
4835
4871
  }
4836
4872
  const registry = TokenRegistry.getInstance();
@@ -4855,17 +4891,33 @@ var PaymentsModule = class _PaymentsModule {
4855
4891
  updatedAt: Date.now(),
4856
4892
  sdkData: JSON.stringify({ _pendingFinalization: pendingData })
4857
4893
  };
4858
- await this.addToken(uiToken, false);
4859
- this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
4894
+ await this.addToken(uiToken);
4895
+ this.processedSplitGroupIds.add(bundle.splitGroupId);
4896
+ await this.saveProcessedSplitGroupIds();
4897
+ const senderInfo = await this.resolveSenderInfo(senderPubkey);
4898
+ await this.addToHistory({
4899
+ type: "RECEIVED",
4900
+ amount: bundle.amount,
4901
+ coinId: bundle.coinId,
4902
+ symbol: uiToken.symbol,
4903
+ timestamp: Date.now(),
4904
+ senderPubkey,
4905
+ ...senderInfo,
4906
+ memo,
4907
+ tokenId: deterministicId
4908
+ });
4860
4909
  this.deps.emitEvent("transfer:incoming", {
4861
4910
  id: bundle.splitGroupId,
4862
4911
  senderPubkey,
4912
+ senderNametag: senderInfo.senderNametag,
4863
4913
  tokens: [uiToken],
4914
+ memo,
4864
4915
  receivedAt: Date.now()
4865
4916
  });
4866
4917
  await this.save();
4867
4918
  this.resolveUnconfirmed().catch(() => {
4868
4919
  });
4920
+ this.scheduleResolveUnconfirmed();
4869
4921
  return { success: true, durationMs: 0 };
4870
4922
  } catch (error) {
4871
4923
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -4880,7 +4932,7 @@ var PaymentsModule = class _PaymentsModule {
4880
4932
  * Synchronous V4 bundle processing (dev mode only).
4881
4933
  * Kept for backward compatibility with V4 bundles.
4882
4934
  */
4883
- async processInstantSplitBundleSync(bundle, senderPubkey) {
4935
+ async processInstantSplitBundleSync(bundle, senderPubkey, memo) {
4884
4936
  try {
4885
4937
  const signingService = await this.createSigningService();
4886
4938
  const stClient = this.deps.oracle.getStateTransitionClient?.();
@@ -4940,19 +4992,26 @@ var PaymentsModule = class _PaymentsModule {
4940
4992
  sdkData: JSON.stringify(tokenData)
4941
4993
  };
4942
4994
  await this.addToken(uiToken);
4995
+ const receivedTokenId = extractTokenIdFromSdkData(uiToken.sdkData);
4996
+ const senderInfo = await this.resolveSenderInfo(senderPubkey);
4943
4997
  await this.addToHistory({
4944
4998
  type: "RECEIVED",
4945
4999
  amount: bundle.amount,
4946
5000
  coinId: info.coinId,
4947
5001
  symbol: info.symbol,
4948
5002
  timestamp: Date.now(),
4949
- senderPubkey
5003
+ senderPubkey,
5004
+ ...senderInfo,
5005
+ memo,
5006
+ tokenId: receivedTokenId || uiToken.id
4950
5007
  });
4951
5008
  await this.save();
4952
5009
  this.deps.emitEvent("transfer:incoming", {
4953
5010
  id: bundle.splitGroupId,
4954
5011
  senderPubkey,
5012
+ senderNametag: senderInfo.senderNametag,
4955
5013
  tokens: [uiToken],
5014
+ memo,
4956
5015
  receivedAt: Date.now()
4957
5016
  });
4958
5017
  }
@@ -5605,28 +5664,70 @@ var PaymentsModule = class _PaymentsModule {
5605
5664
  };
5606
5665
  const stClient = this.deps.oracle.getStateTransitionClient?.();
5607
5666
  const trustBase = this.deps.oracle.getTrustBase?.();
5608
- if (!stClient || !trustBase) return result;
5667
+ if (!stClient || !trustBase) {
5668
+ console.log(`[V5-RESOLVE] resolveUnconfirmed: EARLY EXIT \u2014 stClient=${!!stClient} trustBase=${!!trustBase}`);
5669
+ return result;
5670
+ }
5609
5671
  const signingService = await this.createSigningService();
5672
+ const submittedCount = Array.from(this.tokens.values()).filter((t) => t.status === "submitted").length;
5673
+ console.log(`[V5-RESOLVE] resolveUnconfirmed: ${submittedCount} submitted token(s) to process`);
5610
5674
  for (const [tokenId, token] of this.tokens) {
5611
5675
  if (token.status !== "submitted") continue;
5612
5676
  const pending2 = this.parsePendingFinalization(token.sdkData);
5613
5677
  if (!pending2) {
5678
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
5614
5679
  result.stillPending++;
5615
5680
  continue;
5616
5681
  }
5617
5682
  if (pending2.type === "v5_bundle") {
5683
+ console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
5618
5684
  const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
5685
+ console.log(`[V5-RESOLVE] Result for ${tokenId.slice(0, 16)}...: ${progress} (stage now: ${pending2.stage})`);
5619
5686
  result.details.push({ tokenId, stage: pending2.stage, status: progress });
5620
5687
  if (progress === "resolved") result.resolved++;
5621
5688
  else if (progress === "failed") result.failed++;
5622
5689
  else result.stillPending++;
5623
5690
  }
5624
5691
  }
5625
- if (result.resolved > 0 || result.failed > 0) {
5692
+ if (result.resolved > 0 || result.failed > 0 || result.stillPending > 0) {
5693
+ console.log(`[V5-RESOLVE] Saving: resolved=${result.resolved} failed=${result.failed} stillPending=${result.stillPending}`);
5626
5694
  await this.save();
5627
5695
  }
5628
5696
  return result;
5629
5697
  }
5698
+ /**
5699
+ * Start a periodic interval that retries resolveUnconfirmed() until all
5700
+ * tokens are confirmed or failed. Stops automatically when nothing is
5701
+ * pending and is cleaned up by destroy().
5702
+ */
5703
+ scheduleResolveUnconfirmed() {
5704
+ if (this.resolveUnconfirmedTimer) return;
5705
+ const hasUnconfirmed = Array.from(this.tokens.values()).some(
5706
+ (t) => t.status === "submitted"
5707
+ );
5708
+ if (!hasUnconfirmed) {
5709
+ console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: no submitted tokens, not starting timer`);
5710
+ return;
5711
+ }
5712
+ console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: starting periodic retry (every ${_PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS}ms)`);
5713
+ this.resolveUnconfirmedTimer = setInterval(async () => {
5714
+ try {
5715
+ const result = await this.resolveUnconfirmed();
5716
+ if (result.stillPending === 0) {
5717
+ console.log(`[V5-RESOLVE] All tokens resolved, stopping periodic retry`);
5718
+ this.stopResolveUnconfirmedPolling();
5719
+ }
5720
+ } catch (err) {
5721
+ console.log(`[V5-RESOLVE] Periodic retry error:`, err);
5722
+ }
5723
+ }, _PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS);
5724
+ }
5725
+ stopResolveUnconfirmedPolling() {
5726
+ if (this.resolveUnconfirmedTimer) {
5727
+ clearInterval(this.resolveUnconfirmedTimer);
5728
+ this.resolveUnconfirmedTimer = null;
5729
+ }
5730
+ }
5630
5731
  // ===========================================================================
5631
5732
  // Private - V5 Lazy Resolution Helpers
5632
5733
  // ===========================================================================
@@ -5639,10 +5740,12 @@ var PaymentsModule = class _PaymentsModule {
5639
5740
  pending2.lastAttemptAt = Date.now();
5640
5741
  try {
5641
5742
  if (pending2.stage === "RECEIVED") {
5743
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
5642
5744
  const mintDataJson = JSON.parse(bundle.recipientMintData);
5643
5745
  const mintData = await MintTransactionData3.fromJSON(mintDataJson);
5644
5746
  const mintCommitment = await MintCommitment3.create(mintData);
5645
5747
  const mintResponse = await stClient.submitMintCommitment(mintCommitment);
5748
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
5646
5749
  if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
5647
5750
  throw new Error(`Mint submission failed: ${mintResponse.status}`);
5648
5751
  }
@@ -5650,22 +5753,27 @@ var PaymentsModule = class _PaymentsModule {
5650
5753
  this.updatePendingFinalization(token, pending2);
5651
5754
  }
5652
5755
  if (pending2.stage === "MINT_SUBMITTED") {
5756
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
5653
5757
  const mintDataJson = JSON.parse(bundle.recipientMintData);
5654
5758
  const mintData = await MintTransactionData3.fromJSON(mintDataJson);
5655
5759
  const mintCommitment = await MintCommitment3.create(mintData);
5656
5760
  const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
5657
5761
  if (!proof) {
5762
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
5658
5763
  this.updatePendingFinalization(token, pending2);
5659
5764
  return "pending";
5660
5765
  }
5766
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
5661
5767
  pending2.mintProofJson = JSON.stringify(proof);
5662
5768
  pending2.stage = "MINT_PROVEN";
5663
5769
  this.updatePendingFinalization(token, pending2);
5664
5770
  }
5665
5771
  if (pending2.stage === "MINT_PROVEN") {
5772
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
5666
5773
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5667
5774
  const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
5668
5775
  const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
5776
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
5669
5777
  if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
5670
5778
  throw new Error(`Transfer submission failed: ${transferResponse.status}`);
5671
5779
  }
@@ -5673,13 +5781,16 @@ var PaymentsModule = class _PaymentsModule {
5673
5781
  this.updatePendingFinalization(token, pending2);
5674
5782
  }
5675
5783
  if (pending2.stage === "TRANSFER_SUBMITTED") {
5784
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
5676
5785
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5677
5786
  const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
5678
5787
  const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
5679
5788
  if (!proof) {
5789
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
5680
5790
  this.updatePendingFinalization(token, pending2);
5681
5791
  return "pending";
5682
5792
  }
5793
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
5683
5794
  const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
5684
5795
  const confirmedToken = {
5685
5796
  id: token.id,
@@ -5695,13 +5806,11 @@ var PaymentsModule = class _PaymentsModule {
5695
5806
  sdkData: JSON.stringify(finalizedToken.toJSON())
5696
5807
  };
5697
5808
  this.tokens.set(tokenId, confirmedToken);
5698
- await this.addToHistory({
5699
- type: "RECEIVED",
5700
- amount: confirmedToken.amount,
5701
- coinId: confirmedToken.coinId,
5702
- symbol: confirmedToken.symbol || "UNK",
5703
- timestamp: Date.now(),
5704
- senderPubkey: pending2.senderPubkey
5809
+ this.deps.emitEvent("transfer:confirmed", {
5810
+ id: crypto.randomUUID(),
5811
+ status: "completed",
5812
+ tokens: [confirmedToken],
5813
+ tokenTransfers: []
5705
5814
  });
5706
5815
  this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
5707
5816
  return "resolved";
@@ -5844,11 +5953,20 @@ var PaymentsModule = class _PaymentsModule {
5844
5953
  }
5845
5954
  }
5846
5955
  if (pendingTokens.length > 0) {
5956
+ const json = JSON.stringify(pendingTokens);
5957
+ this.log(`[V5-PERSIST] Saving ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")} (${json.length} bytes)`);
5847
5958
  await this.deps.storage.set(
5848
5959
  STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
5849
- JSON.stringify(pendingTokens)
5960
+ json
5850
5961
  );
5962
+ const verify = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
5963
+ if (!verify) {
5964
+ console.error("[Payments][V5-PERSIST] CRITICAL: KV write succeeded but read-back is empty!");
5965
+ } else {
5966
+ this.log(`[V5-PERSIST] Verified: read-back ${verify.length} bytes`);
5967
+ }
5851
5968
  } else {
5969
+ this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
5852
5970
  await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
5853
5971
  }
5854
5972
  }
@@ -5858,16 +5976,47 @@ var PaymentsModule = class _PaymentsModule {
5858
5976
  */
5859
5977
  async loadPendingV5Tokens() {
5860
5978
  const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
5979
+ this.log(`[V5-PERSIST] loadPendingV5Tokens: KV data = ${data ? `${data.length} bytes` : "null/empty"}`);
5861
5980
  if (!data) return;
5862
5981
  try {
5863
5982
  const pendingTokens = JSON.parse(data);
5983
+ this.log(`[V5-PERSIST] Parsed ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")}`);
5864
5984
  for (const token of pendingTokens) {
5865
5985
  if (!this.tokens.has(token.id)) {
5866
5986
  this.tokens.set(token.id, token);
5987
+ this.log(`[V5-PERSIST] Restored token ${token.id.slice(0, 16)} (status=${token.status})`);
5988
+ } else {
5989
+ this.log(`[V5-PERSIST] Token ${token.id.slice(0, 16)} already in map, skipping`);
5867
5990
  }
5868
5991
  }
5869
- if (pendingTokens.length > 0) {
5870
- this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
5992
+ } catch (err) {
5993
+ console.error("[Payments][V5-PERSIST] Failed to parse pending V5 tokens:", err);
5994
+ }
5995
+ }
5996
+ /**
5997
+ * Persist the set of processed splitGroupIds to KV storage.
5998
+ * This ensures Nostr re-deliveries are ignored across page reloads,
5999
+ * even when the confirmed token's in-memory ID differs from v5split_{id}.
6000
+ */
6001
+ async saveProcessedSplitGroupIds() {
6002
+ const ids = Array.from(this.processedSplitGroupIds);
6003
+ if (ids.length > 0) {
6004
+ await this.deps.storage.set(
6005
+ STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS,
6006
+ JSON.stringify(ids)
6007
+ );
6008
+ }
6009
+ }
6010
+ /**
6011
+ * Load processed splitGroupIds from KV storage.
6012
+ */
6013
+ async loadProcessedSplitGroupIds() {
6014
+ const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS);
6015
+ if (!data) return;
6016
+ try {
6017
+ const ids = JSON.parse(data);
6018
+ for (const id of ids) {
6019
+ this.processedSplitGroupIds.add(id);
5871
6020
  }
5872
6021
  } catch {
5873
6022
  }
@@ -5886,10 +6035,9 @@ var PaymentsModule = class _PaymentsModule {
5886
6035
  * the old state is archived and replaced with the incoming one.
5887
6036
  *
5888
6037
  * @param token - The token to add.
5889
- * @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
5890
6038
  * @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
5891
6039
  */
5892
- async addToken(token, skipHistory = false) {
6040
+ async addToken(token) {
5893
6041
  this.ensureInitialized();
5894
6042
  const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
5895
6043
  const incomingStateHash = extractStateHashFromSdkData(token.sdkData);
@@ -5935,15 +6083,6 @@ var PaymentsModule = class _PaymentsModule {
5935
6083
  }
5936
6084
  this.tokens.set(token.id, token);
5937
6085
  await this.archiveToken(token);
5938
- if (!skipHistory && token.coinId && token.amount) {
5939
- await this.addToHistory({
5940
- type: "RECEIVED",
5941
- amount: token.amount,
5942
- coinId: token.coinId,
5943
- symbol: token.symbol || "UNK",
5944
- timestamp: token.createdAt || Date.now()
5945
- });
5946
- }
5947
6086
  await this.save();
5948
6087
  this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
5949
6088
  return true;
@@ -5970,7 +6109,7 @@ var PaymentsModule = class _PaymentsModule {
5970
6109
  }
5971
6110
  }
5972
6111
  if (!found) {
5973
- await this.addToken(token, true);
6112
+ await this.addToken(token);
5974
6113
  return;
5975
6114
  }
5976
6115
  await this.archiveToken(token);
@@ -5985,10 +6124,8 @@ var PaymentsModule = class _PaymentsModule {
5985
6124
  * entry is created unless `skipHistory` is `true`.
5986
6125
  *
5987
6126
  * @param tokenId - Local UUID of the token to remove.
5988
- * @param recipientNametag - Optional nametag of the transfer recipient (for history).
5989
- * @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
5990
6127
  */
5991
- async removeToken(tokenId, recipientNametag, skipHistory = false) {
6128
+ async removeToken(tokenId) {
5992
6129
  this.ensureInitialized();
5993
6130
  const token = this.tokens.get(tokenId);
5994
6131
  if (!token) return;
@@ -6006,16 +6143,6 @@ var PaymentsModule = class _PaymentsModule {
6006
6143
  this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
6007
6144
  }
6008
6145
  this.tokens.delete(tokenId);
6009
- if (!skipHistory && token.coinId && token.amount) {
6010
- await this.addToHistory({
6011
- type: "SENT",
6012
- amount: token.amount,
6013
- coinId: token.coinId,
6014
- symbol: token.symbol || "UNK",
6015
- timestamp: Date.now(),
6016
- recipientNametag
6017
- });
6018
- }
6019
6146
  await this.save();
6020
6147
  }
6021
6148
  // ===========================================================================
@@ -6240,26 +6367,104 @@ var PaymentsModule = class _PaymentsModule {
6240
6367
  * @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
6241
6368
  */
6242
6369
  getHistory() {
6243
- return [...this.transactionHistory].sort((a, b) => b.timestamp - a.timestamp);
6370
+ return [...this._historyCache].sort((a, b) => b.timestamp - a.timestamp);
6371
+ }
6372
+ /**
6373
+ * Best-effort resolve sender's DIRECT address and nametag from their transport pubkey.
6374
+ * Returns empty object if transport doesn't support resolution or lookup fails.
6375
+ */
6376
+ async resolveSenderInfo(senderTransportPubkey) {
6377
+ try {
6378
+ if (this.deps?.transport?.resolveTransportPubkeyInfo) {
6379
+ const peerInfo = await this.deps.transport.resolveTransportPubkeyInfo(senderTransportPubkey);
6380
+ if (peerInfo) {
6381
+ return {
6382
+ senderAddress: peerInfo.directAddress || void 0,
6383
+ senderNametag: peerInfo.nametag || void 0
6384
+ };
6385
+ }
6386
+ }
6387
+ } catch {
6388
+ }
6389
+ return {};
6244
6390
  }
6245
6391
  /**
6246
6392
  * Append an entry to the transaction history.
6247
6393
  *
6248
- * A unique `id` is auto-generated. The entry is immediately persisted to storage.
6394
+ * A unique `id` and `dedupKey` are auto-generated. The entry is persisted to
6395
+ * the local token storage provider's `history` store (IndexedDB / file).
6396
+ * Duplicate entries with the same `dedupKey` are silently ignored (upsert).
6249
6397
  *
6250
- * @param entry - History entry fields (without `id`).
6398
+ * @param entry - History entry fields (without `id` and `dedupKey`).
6251
6399
  */
6252
6400
  async addToHistory(entry) {
6253
6401
  this.ensureInitialized();
6402
+ const dedupKey = computeHistoryDedupKey(entry.type, entry.tokenId, entry.transferId);
6254
6403
  const historyEntry = {
6255
6404
  id: crypto.randomUUID(),
6405
+ dedupKey,
6256
6406
  ...entry
6257
6407
  };
6258
- this.transactionHistory.push(historyEntry);
6259
- await this.deps.storage.set(
6260
- STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY,
6261
- JSON.stringify(this.transactionHistory)
6262
- );
6408
+ const provider = this.getLocalTokenStorageProvider();
6409
+ if (provider?.addHistoryEntry) {
6410
+ await provider.addHistoryEntry(historyEntry);
6411
+ }
6412
+ const existingIdx = this._historyCache.findIndex((e) => e.dedupKey === dedupKey);
6413
+ if (existingIdx >= 0) {
6414
+ this._historyCache[existingIdx] = historyEntry;
6415
+ } else {
6416
+ this._historyCache.push(historyEntry);
6417
+ }
6418
+ this.deps.emitEvent("history:updated", historyEntry);
6419
+ }
6420
+ /**
6421
+ * Load history from the local token storage provider into the in-memory cache.
6422
+ * Also performs one-time migration from legacy KV storage.
6423
+ */
6424
+ async loadHistory() {
6425
+ const provider = this.getLocalTokenStorageProvider();
6426
+ if (provider?.getHistoryEntries) {
6427
+ this._historyCache = await provider.getHistoryEntries();
6428
+ const legacyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
6429
+ if (legacyData) {
6430
+ try {
6431
+ const legacyEntries = JSON.parse(legacyData);
6432
+ const records = legacyEntries.map((e) => ({
6433
+ ...e,
6434
+ dedupKey: e.dedupKey || computeHistoryDedupKey(e.type, e.tokenId, e.transferId)
6435
+ }));
6436
+ const imported = await provider.importHistoryEntries?.(records) ?? 0;
6437
+ if (imported > 0) {
6438
+ this._historyCache = await provider.getHistoryEntries();
6439
+ this.log(`Migrated ${imported} history entries from KV to history store`);
6440
+ }
6441
+ await this.deps.storage.remove(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
6442
+ } catch {
6443
+ }
6444
+ }
6445
+ } else {
6446
+ const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
6447
+ if (historyData) {
6448
+ try {
6449
+ this._historyCache = JSON.parse(historyData);
6450
+ } catch {
6451
+ this._historyCache = [];
6452
+ }
6453
+ }
6454
+ }
6455
+ }
6456
+ /**
6457
+ * Get the first local token storage provider (for history operations).
6458
+ */
6459
+ getLocalTokenStorageProvider() {
6460
+ const providers = this.getTokenStorageProviders();
6461
+ for (const [, provider] of providers) {
6462
+ if (provider.type === "local") return provider;
6463
+ }
6464
+ for (const [, provider] of providers) {
6465
+ return provider;
6466
+ }
6467
+ return null;
6263
6468
  }
6264
6469
  // ===========================================================================
6265
6470
  // Public API - Nametag
@@ -6466,7 +6671,32 @@ var PaymentsModule = class _PaymentsModule {
6466
6671
  try {
6467
6672
  const result = await provider.sync(localData);
6468
6673
  if (result.success && result.merged) {
6674
+ const savedTokens = new Map(this.tokens);
6469
6675
  this.loadFromStorageData(result.merged);
6676
+ let restoredCount = 0;
6677
+ for (const [tokenId, token] of savedTokens) {
6678
+ if (this.tokens.has(tokenId)) continue;
6679
+ const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
6680
+ const stateHash = extractStateHashFromSdkData(token.sdkData);
6681
+ if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
6682
+ continue;
6683
+ }
6684
+ if (sdkTokenId) {
6685
+ let hasEquivalent = false;
6686
+ for (const existing of this.tokens.values()) {
6687
+ if (extractTokenIdFromSdkData(existing.sdkData) === sdkTokenId) {
6688
+ hasEquivalent = true;
6689
+ break;
6690
+ }
6691
+ }
6692
+ if (hasEquivalent) continue;
6693
+ }
6694
+ this.tokens.set(tokenId, token);
6695
+ restoredCount++;
6696
+ }
6697
+ if (restoredCount > 0) {
6698
+ console.log(`[Payments] Sync: restored ${restoredCount} token(s) lost by loadFromStorageData`);
6699
+ }
6470
6700
  if (this.nametags.length === 0 && savedNametags.length > 0) {
6471
6701
  this.nametags = savedNametags;
6472
6702
  }
@@ -6800,16 +7030,30 @@ var PaymentsModule = class _PaymentsModule {
6800
7030
  return;
6801
7031
  }
6802
7032
  this.tokens.set(token.id, token);
7033
+ console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
6803
7034
  await this.save();
6804
- this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
7035
+ console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
7036
+ const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
6805
7037
  const incomingTransfer = {
6806
7038
  id: transfer.id,
6807
7039
  senderPubkey: transfer.senderTransportPubkey,
7040
+ senderNametag: senderInfo.senderNametag,
6808
7041
  tokens: [token],
6809
7042
  memo: payload.memo,
6810
7043
  receivedAt: transfer.timestamp
6811
7044
  };
6812
7045
  this.deps.emitEvent("transfer:incoming", incomingTransfer);
7046
+ await this.addToHistory({
7047
+ type: "RECEIVED",
7048
+ amount: token.amount,
7049
+ coinId: token.coinId,
7050
+ symbol: token.symbol,
7051
+ timestamp: Date.now(),
7052
+ senderPubkey: transfer.senderTransportPubkey,
7053
+ ...senderInfo,
7054
+ memo: payload.memo,
7055
+ tokenId: nostrTokenId || token.id
7056
+ });
6813
7057
  try {
6814
7058
  const commitment = await TransferCommitment4.fromJSON(commitmentInput);
6815
7059
  const requestIdBytes = commitment.requestId;
@@ -6827,7 +7071,7 @@ var PaymentsModule = class _PaymentsModule {
6827
7071
  attemptCount: 0,
6828
7072
  lastAttemptAt: 0,
6829
7073
  onProofReceived: async (tokenId) => {
6830
- await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, transfer.senderTransportPubkey);
7074
+ await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
6831
7075
  }
6832
7076
  });
6833
7077
  } catch (err) {
@@ -6886,7 +7130,7 @@ var PaymentsModule = class _PaymentsModule {
6886
7130
  /**
6887
7131
  * Finalize a received token after proof is available
6888
7132
  */
6889
- async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, senderPubkey) {
7133
+ async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput) {
6890
7134
  try {
6891
7135
  const token = this.tokens.get(tokenId);
6892
7136
  if (!token) {
@@ -6934,14 +7178,6 @@ var PaymentsModule = class _PaymentsModule {
6934
7178
  tokens: [finalizedToken],
6935
7179
  tokenTransfers: []
6936
7180
  });
6937
- await this.addToHistory({
6938
- type: "RECEIVED",
6939
- amount: finalizedToken.amount,
6940
- coinId: finalizedToken.coinId,
6941
- symbol: finalizedToken.symbol,
6942
- timestamp: Date.now(),
6943
- senderPubkey
6944
- });
6945
7181
  } catch (error) {
6946
7182
  console.error("[Payments] Failed to finalize received token:", error);
6947
7183
  const token = this.tokens.get(tokenId);
@@ -6953,8 +7189,12 @@ var PaymentsModule = class _PaymentsModule {
6953
7189
  }
6954
7190
  }
6955
7191
  async handleIncomingTransfer(transfer) {
7192
+ if (!this.loaded && this.loadedPromise) {
7193
+ await this.loadedPromise;
7194
+ }
6956
7195
  try {
6957
7196
  const payload = transfer.payload;
7197
+ console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
6958
7198
  let instantBundle = null;
6959
7199
  if (isInstantSplitBundle(payload)) {
6960
7200
  instantBundle = payload;
@@ -6972,7 +7212,8 @@ var PaymentsModule = class _PaymentsModule {
6972
7212
  try {
6973
7213
  const result = await this.processInstantSplitBundle(
6974
7214
  instantBundle,
6975
- transfer.senderTransportPubkey
7215
+ transfer.senderTransportPubkey,
7216
+ payload.memo
6976
7217
  );
6977
7218
  if (result.success) {
6978
7219
  this.log("INSTANT_SPLIT processed successfully");
@@ -6985,7 +7226,7 @@ var PaymentsModule = class _PaymentsModule {
6985
7226
  return;
6986
7227
  }
6987
7228
  if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
6988
- this.log("Processing NOSTR-FIRST commitment-only transfer...");
7229
+ console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
6989
7230
  await this.handleCommitmentOnlyTransfer(transfer, payload);
6990
7231
  return;
6991
7232
  }
@@ -7090,10 +7331,26 @@ var PaymentsModule = class _PaymentsModule {
7090
7331
  updatedAt: Date.now(),
7091
7332
  sdkData: typeof tokenData === "string" ? tokenData : JSON.stringify(tokenData)
7092
7333
  };
7093
- await this.addToken(token);
7334
+ const added = await this.addToken(token);
7335
+ const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
7336
+ if (added) {
7337
+ const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
7338
+ await this.addToHistory({
7339
+ type: "RECEIVED",
7340
+ amount: token.amount,
7341
+ coinId: token.coinId,
7342
+ symbol: token.symbol,
7343
+ timestamp: Date.now(),
7344
+ senderPubkey: transfer.senderTransportPubkey,
7345
+ ...senderInfo,
7346
+ memo: payload.memo,
7347
+ tokenId: incomingTokenId || token.id
7348
+ });
7349
+ }
7094
7350
  const incomingTransfer = {
7095
7351
  id: transfer.id,
7096
7352
  senderPubkey: transfer.senderTransportPubkey,
7353
+ senderNametag: senderInfo.senderNametag,
7097
7354
  tokens: [token],
7098
7355
  memo: payload.memo,
7099
7356
  receivedAt: transfer.timestamp
@@ -7132,17 +7389,24 @@ var PaymentsModule = class _PaymentsModule {
7132
7389
  // ===========================================================================
7133
7390
  async save() {
7134
7391
  const providers = this.getTokenStorageProviders();
7135
- if (providers.size === 0) {
7136
- this.log("No token storage providers - tokens not persisted");
7137
- return;
7138
- }
7139
- const data = await this.createStorageData();
7140
- for (const [id, provider] of providers) {
7141
- try {
7142
- await provider.save(data);
7143
- } catch (err) {
7144
- console.error(`[Payments] Failed to save to provider ${id}:`, err);
7392
+ const tokenStats = Array.from(this.tokens.values()).map((t) => {
7393
+ const txf = tokenToTxf(t);
7394
+ return `${t.id.slice(0, 12)}(${t.status},txf=${!!txf})`;
7395
+ });
7396
+ console.log(`[Payments][DEBUG] save(): providers=${providers.size}, tokens=[${tokenStats.join(", ")}]`);
7397
+ if (providers.size > 0) {
7398
+ const data = await this.createStorageData();
7399
+ const dataKeys = Object.keys(data).filter((k) => k.startsWith("token-"));
7400
+ console.log(`[Payments][DEBUG] save(): TXF keys=${dataKeys.length} (${dataKeys.join(", ")})`);
7401
+ for (const [id, provider] of providers) {
7402
+ try {
7403
+ await provider.save(data);
7404
+ } catch (err) {
7405
+ console.error(`[Payments] Failed to save to provider ${id}:`, err);
7406
+ }
7145
7407
  }
7408
+ } else {
7409
+ console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
7146
7410
  }
7147
7411
  await this.savePendingV5Tokens();
7148
7412
  }
@@ -7178,6 +7442,7 @@ var PaymentsModule = class _PaymentsModule {
7178
7442
  }
7179
7443
  loadFromStorageData(data) {
7180
7444
  const parsed = parseTxfStorageData(data);
7445
+ console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
7181
7446
  this.tombstones = parsed.tombstones;
7182
7447
  this.tokens.clear();
7183
7448
  for (const token of parsed.tokens) {