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:
@@ -945,6 +945,15 @@
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="tx-detail-to" class="text-xs break-all"></div>
|
||||
</div>
|
||||
<div id="tx-detail-calldata-section" class="mb-4 hidden">
|
||||
<div class="text-xs text-muted mb-1">
|
||||
Contract interaction
|
||||
</div>
|
||||
<div
|
||||
id="tx-detail-calldata"
|
||||
class="text-xs break-all border border-border border-dashed p-2"
|
||||
></div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="text-xs text-muted mb-1">Transaction hash</div>
|
||||
<div id="tx-detail-hash" class="text-xs break-all"></div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -453,4 +453,4 @@ function init(ctx) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { init, show };
|
||||
module.exports = { init, show, decodeCalldata };
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user