fix: display swaps and contract calls correctly in tx history (closes #3)

- Preserve contract call metadata (direction, label, method) when token
  transfers merge with normal txs in fetchRecentTransactions
- Handle 'contract' direction in counterparty display for home and
  address detail list views
- Add decoded calldata display to transaction detail view, fetching
  raw input from Blockscout and using decodeCalldata from approval.js
- Show 'Unknown contract call' with raw hex for unrecognized calldata
- Export decodeCalldata from approval.js for reuse
This commit is contained in:
clawbot
2026-02-27 12:03:57 -08:00
committed by user
parent 0ed7b8e61d
commit 76059c3674
6 changed files with 100 additions and 4 deletions

View File

@@ -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 =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
@@ -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 = `<div class="font-bold mb-1">${escapeHtml(decoded.name)}</div>`;
if (decoded.description) {
html += `<div class="text-muted mb-1">${escapeHtml(decoded.description)}</div>`;
}
for (const detail of decoded.details || []) {
html += `<div class="mb-1"><span class="text-muted">${escapeHtml(detail.label)}:</span> `;
if (detail.address) {
const dot = addressDotHtml(detail.address);
html += `${dot}${copyableHtml(detail.value, "break-all")}`;
} else {
html += escapeHtml(detail.value);
}
html += `</div>`;
}
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 = `<div class="font-bold mb-1">Unknown contract call</div>`;
html += `<div class="text-muted mb-1">${escapeHtml(method)}</div>`;
const displayData =
inputData.length > 202
? inputData.slice(0, 202) + "…"
: inputData;
html += `<div class="break-all font-mono text-xs">${copyableHtml(inputData, "break-all")}</div>`;
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", () => {