From 256acbeeece41a44b4b79200bc8d2cc676fd59ce Mon Sep 17 00:00:00 2001 From: user Date: Sat, 28 Feb 2026 12:08:38 -0800 Subject: [PATCH] fix: preserve multiple token transfers per tx hash in transaction history When a swap produces multiple ERC-20 token transfers with the same tx hash (e.g. send WETH + receive USDC), only the last transfer was kept because txsByHash was keyed by bare tx hash. This caused the address-token view to show no transactions for tokens obtained via swap. Use a composite key (hash:contractAddress) so all token transfers are preserved. The bare-hash normal-tx entry is removed when token transfers replace it to avoid duplication. Closes #72 --- src/shared/transactions.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/shared/transactions.js b/src/shared/transactions.js index e12d310..5e2d9d1 100644 --- a/src/shared/transactions.js +++ b/src/shared/transactions.js @@ -153,9 +153,14 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) { // When a token transfer shares a hash with a normal tx, the normal tx // is the contract call (0 ETH) and the token transfer has the real - // amount and symbol. Replace the normal tx with the token transfer, - // but preserve contract call metadata (direction, label, method) so - // swaps and other contract interactions display correctly. + // amount and symbol. Preserve contract call metadata (direction, label, + // method) so swaps and other contract interactions display correctly. + // + // A single tx hash can produce multiple token transfers (e.g. a swap + // sends token A and receives token B). Use a composite key + // (hash:contractAddress) so every transfer is preserved. The original + // normal-tx entry (keyed by bare hash) is removed when at least one + // token transfer replaces it. for (const tt of ttJson.items || []) { const parsed = parseTokenTransfer(tt, addrLower); const existing = txsByHash.get(parsed.hash); @@ -164,8 +169,12 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) { parsed.directionLabel = existing.directionLabel; parsed.isContractCall = true; parsed.method = existing.method; + // Remove the bare-hash normal tx so it isn't duplicated + txsByHash.delete(parsed.hash); } - txsByHash.set(parsed.hash, parsed); + // Use composite key so multiple token transfers per tx are kept + const compositeKey = parsed.hash + ":" + (parsed.contractAddress || ""); + txsByHash.set(compositeKey, parsed); } const txs = [...txsByHash.values()]; -- 2.49.1