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 @@
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);
}