diff --git a/src/popup/index.html b/src/popup/index.html index 6922bcd..3987d57 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -939,6 +939,15 @@
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..53c2005 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 = `` + @@ -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,10 @@ function render() { nativeEl.parentElement.classList.add("hidden"); } + // 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 +149,66 @@ function render() { }); } +function renderDecodedCalldata(decoded) { + let html = `
${escapeHtml(decoded.name)}
`; + if (decoded.description) { + html += `
${escapeHtml(decoded.description)}
`; + } + for (const detail of decoded.details || []) { + html += `
${escapeHtml(detail.label)}: `; + if (detail.address) { + const dot = addressDotHtml(detail.address); + html += `${dot}${copyableHtml(detail.value, "break-all")}`; + } else { + html += escapeHtml(detail.value); + } + html += `
`; + } + return html; +} + +async function loadCalldata(txHash, toAddress) { + const section = $("tx-detail-calldata-section"); + const container = $("tx-detail-calldata"); + if (!section || !container) 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) { + container.innerHTML = renderDecodedCalldata(decoded); + } else { + const method = txData.method || "Unknown method"; + let html = `
Unknown contract call
`; + html += `
${escapeHtml(method)}
`; + const displayData = + inputData.length > 202 + ? inputData.slice(0, 202) + "…" + : inputData; + html += `
${copyableHtml(inputData, "break-all")}
`; + container.innerHTML = html; + } + 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..d58fb86 100644 --- a/src/shared/transactions.js +++ b/src/shared/transactions.js @@ -139,9 +139,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); }