Merge pull request 'fix: correct swap display — output token, min received, from address, and amount' (#128) from fix/issue-127-swap-amount-display into main
All checks were successful
check / check (push) Successful in 9s

Reviewed-on: #128
This commit was merged in pull request #128.
This commit is contained in:
2026-03-01 16:19:41 +01:00
2 changed files with 37 additions and 21 deletions

View File

@@ -153,24 +153,38 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
// When a token transfer shares a hash with a normal tx, the normal tx // 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 // is the contract call (0 ETH) and the token transfer has the real
// amount and symbol. A single transaction (e.g. a swap) can produce // amount and symbol. For contract calls (swaps), a single transaction
// multiple token transfers (one per token involved), so we key token // can produce multiple token transfers (input, intermediates, output).
// transfers by hash + contract address to keep all of them. We also // We consolidate these into the original tx entry using the token
// preserve contract-call metadata (direction, label, method) from the // transfer where the user *receives* tokens (the swap output), so
// matching normal tx so swaps display correctly. // the transaction list shows the final result rather than confusing
// intermediate hops. We preserve the original tx's from/to so the
// user sees their own address, not a router or Permit2 contract.
for (const tt of ttJson.items || []) { for (const tt of ttJson.items || []) {
const parsed = parseTokenTransfer(tt, addrLower); const parsed = parseTokenTransfer(tt, addrLower);
const existing = txsByHash.get(parsed.hash); const existing = txsByHash.get(parsed.hash);
if (existing && existing.direction === "contract") { if (existing && existing.direction === "contract") {
parsed.direction = "contract"; // For contract calls (swaps), consolidate into the original
parsed.directionLabel = existing.directionLabel; // tx entry. Prefer the "received" transfer (swap output)
parsed.isContractCall = true; // for the display amount. If no received transfer exists,
parsed.method = existing.method; // fall back to the first "sent" transfer (swap input).
// Remove the bare-hash normal tx so it doesn't appear as a const isReceived = parsed.direction === "received";
// duplicate with empty value; token transfers replace it. const needsAmount = !existing.exactValue;
txsByHash.delete(parsed.hash); if (isReceived || needsAmount) {
existing.value = parsed.value;
existing.exactValue = parsed.exactValue;
existing.rawAmount = parsed.rawAmount;
existing.rawUnit = parsed.rawUnit;
existing.symbol = parsed.symbol;
existing.contractAddress = parsed.contractAddress;
existing.holders = parsed.holders;
} }
// Use composite key so multiple token transfers per tx are kept. // Keep the original tx's from/to (the user's address and the
// contract they called), not the token transfer's from/to
// which may be a router or Permit2 contract.
continue;
}
// Non-contract token transfers get their own entries.
const ttKey = parsed.hash + ":" + (parsed.contractAddress || ""); const ttKey = parsed.hash + ":" + (parsed.contractAddress || "");
txsByHash.set(ttKey, parsed); txsByHash.set(ttKey, parsed);
} }

View File

@@ -359,9 +359,12 @@ function decode(data, toAddress) {
const s = decodeV3SwapExactIn(inputs[i]); const s = decodeV3SwapExactIn(inputs[i]);
if (s) { if (s) {
if (!inputToken) inputToken = s.tokenIn; if (!inputToken) inputToken = s.tokenIn;
if (!outputToken) outputToken = s.tokenOut;
if (!inputAmount) inputAmount = s.amountIn; if (!inputAmount) inputAmount = s.amountIn;
if (!minOutput) minOutput = s.amountOutMin; // Always update output: in multi-step swaps (V3 → V4),
// the last swap step determines the final output token
// and minimum received amount.
outputToken = s.tokenOut;
minOutput = s.amountOutMin;
} }
} }
@@ -369,9 +372,9 @@ function decode(data, toAddress) {
const s = decodeV2SwapExactIn(inputs[i]); const s = decodeV2SwapExactIn(inputs[i]);
if (s) { if (s) {
if (!inputToken) inputToken = s.tokenIn; if (!inputToken) inputToken = s.tokenIn;
if (!outputToken) outputToken = s.tokenOut;
if (!inputAmount) inputAmount = s.amountIn; if (!inputAmount) inputAmount = s.amountIn;
if (!minOutput) minOutput = s.amountOutMin; outputToken = s.tokenOut;
minOutput = s.amountOutMin;
} }
} }
@@ -388,12 +391,11 @@ function decode(data, toAddress) {
const v4 = decodeV4Swap(inputs[i]); const v4 = decodeV4Swap(inputs[i]);
if (v4) { if (v4) {
if (!inputToken && v4.tokenIn) inputToken = v4.tokenIn; if (!inputToken && v4.tokenIn) inputToken = v4.tokenIn;
if (!outputToken && v4.tokenOut)
outputToken = v4.tokenOut;
if (!inputAmount && v4.amountIn) if (!inputAmount && v4.amountIn)
inputAmount = v4.amountIn; inputAmount = v4.amountIn;
if (!minOutput && v4.amountOutMin) // Always update output: last swap step wins
minOutput = v4.amountOutMin; if (v4.tokenOut) outputToken = v4.tokenOut;
if (v4.amountOutMin) minOutput = v4.amountOutMin;
} }
} }