${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);
}