diff --git a/src/popup/index.html b/src/popup/index.html index 660e4f7..74dfb69 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -953,7 +953,13 @@ > < Back -

Transaction

+

+ Transaction +

+
Status
@@ -978,6 +984,29 @@
To
+
Transaction hash
diff --git a/src/popup/views/addressDetail.js b/src/popup/views/addressDetail.js index a91cf20..3f4f4f1 100644 --- a/src/popup/views/addressDetail.js +++ b/src/popup/views/addressDetail.js @@ -185,7 +185,10 @@ function renderTransactions(txs) { let html = ""; let i = 0; for (const tx of txs) { - const counterparty = tx.direction === "sent" ? tx.to : tx.from; + const counterparty = + tx.direction === "sent" || tx.direction === "contract" + ? tx.to + : tx.from; const ensName = ensNameMap.get(counterparty) || null; const dirLabel = tx.directionLabel; const amountStr = tx.value diff --git a/src/popup/views/approval.js b/src/popup/views/approval.js index 359b506..58319ed 100644 --- a/src/popup/views/approval.js +++ b/src/popup/views/approval.js @@ -453,4 +453,4 @@ function init(ctx) { }); } -module.exports = { init, show }; +module.exports = { init, show, decodeCalldata }; diff --git a/src/popup/views/home.js b/src/popup/views/home.js index 62b352b..78fbff2 100644 --- a/src/popup/views/home.js +++ b/src/popup/views/home.js @@ -102,7 +102,10 @@ function renderHomeTxList(ctx) { let html = ""; let i = 0; for (const tx of homeTxs) { - const counterparty = tx.direction === "sent" ? tx.to : tx.from; + const counterparty = + tx.direction === "sent" || tx.direction === "contract" + ? tx.to + : tx.from; const dirLabel = tx.directionLabel; const amountStr = tx.value ? escapeHtml(tx.value + " " + tx.symbol) diff --git a/src/popup/views/transactionDetail.js b/src/popup/views/transactionDetail.js index 8ecbfe3..1091005 100644 --- a/src/popup/views/transactionDetail.js +++ b/src/popup/views/transactionDetail.js @@ -13,6 +13,8 @@ const { } = require("./helpers"); const { state } = require("../../shared/state"); const makeBlockie = require("ethereum-blockies-base64"); +const { log, debugFetch } = require("../../shared/log"); +const { decodeCalldata } = require("./approval"); const EXT_ICON = `` + @@ -42,11 +44,11 @@ function txAddressHtml(address, ensName, title) { const extLink = `${EXT_ICON}`; let html = `
${blockie}
`; if (title) { - html += `
${dot}${escapeHtml(title)}
`; + html += `
${escapeHtml(title)}
`; } if (ensName) { html += - `
${title ? "" : dot}` + + `
${dot}` + copyableHtml(ensName, "") + extLink + `
` + @@ -55,7 +57,7 @@ function txAddressHtml(address, ensName, title) { `
`; } else { html += - `
${title ? "" : dot}` + + `
${dot}` + copyableHtml(address, "break-all") + extLink + `
`; @@ -85,9 +87,15 @@ function show(tx) { fromEns: tx.fromEns || null, toEns: tx.toEns || null, directionLabel: tx.directionLabel || null, + direction: tx.direction || null, + isContractCall: tx.isContractCall || false, + method: tx.method || null, }, }; render(); + if (tx.isContractCall || tx.direction === "contract") { + loadCalldata(tx.hash, tx.to); + } } function render() { @@ -121,6 +129,25 @@ function render() { nativeEl.parentElement.classList.add("hidden"); } + // Show type label for contract interactions (Swap, Execute, etc.) + const typeSection = $("tx-detail-type-section"); + const typeEl = $("tx-detail-type"); + const headingEl = $("tx-detail-heading"); + if (tx.direction === "contract" && tx.directionLabel) { + if (typeSection) { + typeEl.textContent = tx.directionLabel; + typeSection.classList.remove("hidden"); + } + if (headingEl) headingEl.textContent = tx.directionLabel; + } else { + if (typeSection) typeSection.classList.add("hidden"); + if (headingEl) headingEl.textContent = "Transaction"; + } + + // Hide calldata section by default; loadCalldata will show it if needed + const calldataSection = $("tx-detail-calldata-section"); + if (calldataSection) calldataSection.classList.add("hidden"); + $("tx-detail-time").textContent = isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")"; $("tx-detail-status").textContent = tx.isError ? "Failed" : "Success"; @@ -137,6 +164,73 @@ function render() { }); } +async function loadCalldata(txHash, toAddress) { + const section = $("tx-detail-calldata-section"); + const actionEl = $("tx-detail-calldata-action"); + const detailsEl = $("tx-detail-calldata-details"); + const wellEl = $("tx-detail-calldata-well"); + const rawSection = $("tx-detail-rawdata-section"); + const rawEl = $("tx-detail-rawdata"); + if (!section || !actionEl || !detailsEl) return; + + try { + const resp = await debugFetch( + state.blockscoutUrl + "/transactions/" + txHash, + ); + if (!resp.ok) return; + const txData = await resp.json(); + const inputData = txData.raw_input || txData.input || null; + if (!inputData || inputData === "0x") return; + + const decoded = decodeCalldata(inputData, toAddress || ""); + if (decoded) { + // Render decoded calldata matching approval view style + actionEl.textContent = decoded.name; + let detailsHtml = ""; + if (decoded.description) { + detailsHtml += `
${escapeHtml(decoded.description)}
`; + } + for (const d of decoded.details || []) { + detailsHtml += `
`; + detailsHtml += `
${escapeHtml(d.label)}
`; + if (d.address) { + const dot = addressDotHtml(d.address); + detailsHtml += `
${dot}${copyableHtml(d.value, "break-all")}
`; + } else { + detailsHtml += `
${escapeHtml(d.value)}
`; + } + detailsHtml += `
`; + } + detailsEl.innerHTML = detailsHtml; + if (wellEl) wellEl.classList.remove("hidden"); + } else { + // Unknown contract call — show method name in well + const method = txData.method || "Unknown contract call"; + actionEl.textContent = method; + detailsEl.innerHTML = ""; + if (wellEl) wellEl.classList.remove("hidden"); + } + + // Always show raw data + if (rawSection && rawEl) { + rawEl.innerHTML = copyableHtml(inputData, "break-all"); + rawSection.classList.remove("hidden"); + } + + section.classList.remove("hidden"); + + // Bind copy handlers for new elements + section.querySelectorAll("[data-copy]").forEach((el) => { + el.onclick = () => { + navigator.clipboard.writeText(el.dataset.copy); + showFlash("Copied!"); + }; + }); + } catch (e) { + log.errorf("loadCalldata failed:", e.message); + } +} + function init(_ctx) { ctx = _ctx; $("btn-tx-back").addEventListener("click", () => { diff --git a/src/shared/transactions.js b/src/shared/transactions.js index f926784..e12d310 100644 --- a/src/shared/transactions.js +++ b/src/shared/transactions.js @@ -37,7 +37,21 @@ function parseTx(tx, addrLower) { if (token) { symbol = token.symbol; } - const label = method.charAt(0).toUpperCase() + method.slice(1); + // Map known DEX methods to "Swap" for cleaner display + const SWAP_METHODS = new Set([ + "execute", + "swap", + "swapExactTokensForTokens", + "swapTokensForExactTokens", + "swapExactETHForTokens", + "swapTokensForExactETH", + "swapExactTokensForETH", + "swapETHForExactTokens", + "multicall", + ]); + const label = SWAP_METHODS.has(method) + ? "Swap" + : method.charAt(0).toUpperCase() + method.slice(1); direction = "contract"; directionLabel = label; value = ""; @@ -139,9 +153,18 @@ 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. + // 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. for (const tt of ttJson.items || []) { const parsed = parseTokenTransfer(tt, addrLower); + const existing = txsByHash.get(parsed.hash); + if (existing && existing.direction === "contract") { + parsed.direction = "contract"; + parsed.directionLabel = existing.directionLabel; + parsed.isContractCall = true; + parsed.method = existing.method; + } txsByHash.set(parsed.hash, parsed); }