Merge pull request 'fix: display swaps and contract calls correctly in tx history (closes #3)' (#10) from fix/swap-display into main
All checks were successful
check / check (push) Successful in 9s
All checks were successful
check / check (push) Successful in 9s
Reviewed-on: #10
This commit was merged in pull request #10.
This commit is contained in:
@@ -953,7 +953,13 @@
|
|||||||
>
|
>
|
||||||
< Back
|
< Back
|
||||||
</button>
|
</button>
|
||||||
<h2 class="font-bold mb-2">Transaction</h2>
|
<h2 id="tx-detail-heading" class="font-bold mb-2">
|
||||||
|
Transaction
|
||||||
|
</h2>
|
||||||
|
<div id="tx-detail-type-section" class="mb-4 hidden">
|
||||||
|
<div class="text-xs text-muted mb-1">Type</div>
|
||||||
|
<div id="tx-detail-type" class="text-xs font-bold"></div>
|
||||||
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="text-xs text-muted mb-1">Status</div>
|
<div class="text-xs text-muted mb-1">Status</div>
|
||||||
<div id="tx-detail-status" class="text-xs"></div>
|
<div id="tx-detail-status" class="text-xs"></div>
|
||||||
@@ -978,6 +984,29 @@
|
|||||||
<div class="text-xs text-muted mb-1">To</div>
|
<div class="text-xs text-muted mb-1">To</div>
|
||||||
<div id="tx-detail-to" class="text-xs break-all"></div>
|
<div id="tx-detail-to" class="text-xs break-all"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tx-detail-calldata-section" class="mb-4 hidden">
|
||||||
|
<div
|
||||||
|
id="tx-detail-calldata-well"
|
||||||
|
class="mb-3 border border-border border-dashed p-2"
|
||||||
|
>
|
||||||
|
<div class="text-xs text-muted mb-1">Action</div>
|
||||||
|
<div
|
||||||
|
id="tx-detail-calldata-action"
|
||||||
|
class="text-xs font-bold mb-2"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
id="tx-detail-calldata-details"
|
||||||
|
class="text-xs"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div id="tx-detail-rawdata-section" class="hidden">
|
||||||
|
<div class="text-xs text-muted mb-1">Raw data</div>
|
||||||
|
<div
|
||||||
|
id="tx-detail-rawdata"
|
||||||
|
class="text-xs break-all font-mono border border-border border-dashed p-2"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="text-xs text-muted mb-1">Transaction hash</div>
|
<div class="text-xs text-muted mb-1">Transaction hash</div>
|
||||||
<div id="tx-detail-hash" class="text-xs break-all"></div>
|
<div id="tx-detail-hash" class="text-xs break-all"></div>
|
||||||
|
|||||||
@@ -185,7 +185,10 @@ function renderTransactions(txs) {
|
|||||||
let html = "";
|
let html = "";
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const tx of txs) {
|
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 ensName = ensNameMap.get(counterparty) || null;
|
||||||
const dirLabel = tx.directionLabel;
|
const dirLabel = tx.directionLabel;
|
||||||
const amountStr = tx.value
|
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 html = "";
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const tx of homeTxs) {
|
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 dirLabel = tx.directionLabel;
|
||||||
const amountStr = tx.value
|
const amountStr = tx.value
|
||||||
? escapeHtml(tx.value + " " + tx.symbol)
|
? escapeHtml(tx.value + " " + tx.symbol)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const {
|
|||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state } = require("../../shared/state");
|
const { state } = require("../../shared/state");
|
||||||
const makeBlockie = require("ethereum-blockies-base64");
|
const makeBlockie = require("ethereum-blockies-base64");
|
||||||
|
const { log, debugFetch } = require("../../shared/log");
|
||||||
|
const { decodeCalldata } = require("./approval");
|
||||||
|
|
||||||
const EXT_ICON =
|
const EXT_ICON =
|
||||||
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
|
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
|
||||||
@@ -42,11 +44,11 @@ function txAddressHtml(address, ensName, title) {
|
|||||||
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
|
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
|
||||||
let html = `<div class="mb-1">${blockie}</div>`;
|
let html = `<div class="mb-1">${blockie}</div>`;
|
||||||
if (title) {
|
if (title) {
|
||||||
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
|
html += `<div class="font-bold">${escapeHtml(title)}</div>`;
|
||||||
}
|
}
|
||||||
if (ensName) {
|
if (ensName) {
|
||||||
html +=
|
html +=
|
||||||
`<div class="flex items-center">${title ? "" : dot}` +
|
`<div class="flex items-center">${dot}` +
|
||||||
copyableHtml(ensName, "") +
|
copyableHtml(ensName, "") +
|
||||||
extLink +
|
extLink +
|
||||||
`</div>` +
|
`</div>` +
|
||||||
@@ -55,7 +57,7 @@ function txAddressHtml(address, ensName, title) {
|
|||||||
`</div>`;
|
`</div>`;
|
||||||
} else {
|
} else {
|
||||||
html +=
|
html +=
|
||||||
`<div class="flex items-center">${title ? "" : dot}` +
|
`<div class="flex items-center">${dot}` +
|
||||||
copyableHtml(address, "break-all") +
|
copyableHtml(address, "break-all") +
|
||||||
extLink +
|
extLink +
|
||||||
`</div>`;
|
`</div>`;
|
||||||
@@ -85,9 +87,15 @@ function show(tx) {
|
|||||||
fromEns: tx.fromEns || null,
|
fromEns: tx.fromEns || null,
|
||||||
toEns: tx.toEns || null,
|
toEns: tx.toEns || null,
|
||||||
directionLabel: tx.directionLabel || null,
|
directionLabel: tx.directionLabel || null,
|
||||||
|
direction: tx.direction || null,
|
||||||
|
isContractCall: tx.isContractCall || false,
|
||||||
|
method: tx.method || null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
render();
|
render();
|
||||||
|
if (tx.isContractCall || tx.direction === "contract") {
|
||||||
|
loadCalldata(tx.hash, tx.to);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
@@ -121,6 +129,25 @@ function render() {
|
|||||||
nativeEl.parentElement.classList.add("hidden");
|
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 =
|
$("tx-detail-time").textContent =
|
||||||
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
|
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
|
||||||
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
|
$("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 += `<div class="mb-2">${escapeHtml(decoded.description)}</div>`;
|
||||||
|
}
|
||||||
|
for (const d of decoded.details || []) {
|
||||||
|
detailsHtml += `<div class="mb-2">`;
|
||||||
|
detailsHtml += `<div class="text-muted">${escapeHtml(d.label)}</div>`;
|
||||||
|
if (d.address) {
|
||||||
|
const dot = addressDotHtml(d.address);
|
||||||
|
detailsHtml += `<div>${dot}${copyableHtml(d.value, "break-all")}</div>`;
|
||||||
|
} else {
|
||||||
|
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
|
||||||
|
}
|
||||||
|
detailsHtml += `</div>`;
|
||||||
|
}
|
||||||
|
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) {
|
function init(_ctx) {
|
||||||
ctx = _ctx;
|
ctx = _ctx;
|
||||||
$("btn-tx-back").addEventListener("click", () => {
|
$("btn-tx-back").addEventListener("click", () => {
|
||||||
|
|||||||
@@ -37,7 +37,21 @@ function parseTx(tx, addrLower) {
|
|||||||
if (token) {
|
if (token) {
|
||||||
symbol = token.symbol;
|
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";
|
direction = "contract";
|
||||||
directionLabel = label;
|
directionLabel = label;
|
||||||
value = "";
|
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
|
// 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
|
// 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 || []) {
|
for (const tt of ttJson.items || []) {
|
||||||
const parsed = parseTokenTransfer(tt, addrLower);
|
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);
|
txsByHash.set(parsed.hash, parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user