Compare commits

..

1 Commits

Author SHA1 Message Date
user
256acbeeec fix: preserve multiple token transfers per tx hash in transaction history
All checks were successful
check / check (push) Successful in 22s
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
2026-02-28 12:08:38 -08:00
2 changed files with 15 additions and 18 deletions

View File

@@ -14,7 +14,6 @@ const {
showError,
hideError,
showView,
showFlash,
addressTitle,
addressDotHtml,
escapeHtml,
@@ -96,22 +95,11 @@ function show(txInfo) {
// Token contract section (ERC-20 only)
const tokenSection = $("confirm-token-section");
if (isErc20) {
const dot = addressDotHtml(txInfo.token);
const link = etherscanTokenLink(txInfo.token);
$("confirm-token-contract").innerHTML =
`<div class="flex items-center">${dot}` +
`<span class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(txInfo.token)}">${escapeHtml(txInfo.token)}</span>` +
`<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>` +
`</div>`;
escapeHtml(txInfo.token) +
` <a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
tokenSection.classList.remove("hidden");
// Attach click-to-copy on the contract address
const copyEl = tokenSection.querySelector("[data-copy]");
if (copyEl) {
copyEl.onclick = () => {
navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!");
};
}
} else {
tokenSection.classList.add("hidden");
}

View File

@@ -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()];