diff --git a/src/popup/index.html b/src/popup/index.html
index 6922bcd..b8f6f4e 100644
--- a/src/popup/index.html
+++ b/src/popup/index.html
@@ -943,6 +943,23 @@
Transaction hash
+
+
diff --git a/src/popup/views/approval.js b/src/popup/views/approval.js
index 359b506..7bce818 100644
--- a/src/popup/views/approval.js
+++ b/src/popup/views/approval.js
@@ -3,6 +3,7 @@ const { state, saveState } = require("../../shared/state");
const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers");
const { ERC20_ABI } = require("../../shared/constants");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
+const { decodeCalldata } = require("../../shared/decodeCalldata");
const txStatus = require("./txStatus");
const uniswap = require("../../shared/uniswap");
@@ -41,91 +42,7 @@ function etherscanTokenLink(address) {
return `https://etherscan.io/token/${address}`;
}
-// Try to decode calldata using known ABIs.
-// Returns { name, description, details } or null.
-function decodeCalldata(data, toAddress) {
- if (!data || data === "0x" || data.length < 10) return null;
-
- // Try ERC-20 (approve / transfer)
- try {
- const parsed = erc20Iface.parseTransaction({ data });
- if (parsed) {
- const token = TOKEN_BY_ADDRESS.get(toAddress.toLowerCase());
- const tokenSymbol = token ? token.symbol : null;
- const tokenDecimals = token ? token.decimals : 18;
- const contractLabel = tokenSymbol
- ? tokenSymbol + " (" + toAddress + ")"
- : toAddress;
-
- if (parsed.name === "approve") {
- const spender = parsed.args[0];
- const rawAmount = parsed.args[1];
- const maxUint = BigInt(
- "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
- );
- const isUnlimited = rawAmount === maxUint;
- const amountStr = isUnlimited
- ? "Unlimited"
- : formatTxValue(formatUnits(rawAmount, tokenDecimals)) +
- (tokenSymbol ? " " + tokenSymbol : "");
-
- return {
- name: "Token Approval",
- description: tokenSymbol
- ? "Approve spending of your " + tokenSymbol
- : "Approve spending of an ERC-20 token",
- details: [
- {
- label: "Token",
- value: contractLabel,
- address: toAddress,
- isToken: true,
- },
- {
- label: "Spender",
- value: spender,
- address: spender,
- },
- { label: "Amount", value: amountStr },
- ],
- };
- }
-
- if (parsed.name === "transfer") {
- const to = parsed.args[0];
- const rawAmount = parsed.args[1];
- const amountStr =
- formatTxValue(formatUnits(rawAmount, tokenDecimals)) +
- (tokenSymbol ? " " + tokenSymbol : "");
-
- return {
- name: "Token Transfer",
- description: tokenSymbol
- ? "Transfer " + tokenSymbol
- : "Transfer ERC-20 token",
- details: [
- {
- label: "Token",
- value: contractLabel,
- address: toAddress,
- isToken: true,
- },
- { label: "Recipient", value: to, address: to },
- { label: "Amount", value: amountStr },
- ],
- };
- }
- }
- } catch {
- // Not ERC-20 — fall through
- }
-
- // Try Uniswap Universal Router
- const routerResult = uniswap.decode(data, toAddress);
- if (routerResult) return routerResult;
-
- return null;
-}
+// decodeCalldata is now in ../../shared/decodeCalldata.js
function showTxApproval(details) {
const toAddr = details.txParams.to;
diff --git a/src/popup/views/transactionDetail.js b/src/popup/views/transactionDetail.js
index 8ecbfe3..9961e28 100644
--- a/src/popup/views/transactionDetail.js
+++ b/src/popup/views/transactionDetail.js
@@ -12,6 +12,8 @@ const {
timeAgo,
} = require("./helpers");
const { state } = require("../../shared/state");
+const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
+const { decodeCalldata } = require("../../shared/decodeCalldata");
const makeBlockie = require("ethereum-blockies-base64");
const EXT_ICON =
@@ -85,6 +87,8 @@ function show(tx) {
fromEns: tx.fromEns || null,
toEns: tx.toEns || null,
directionLabel: tx.directionLabel || null,
+ rawInput: tx.rawInput || null,
+ decoded: tx.decoded || null,
},
};
render();
@@ -124,6 +128,58 @@ function render() {
$("tx-detail-time").textContent =
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
+
+ // Decoded calldata section — matches approval screen format
+ const decodedEl = $("tx-detail-decoded");
+ const rawDataEl = $("tx-detail-rawdata-section");
+ const decoded = tx.decoded;
+
+ if (decoded) {
+ $("tx-detail-action").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) {
+ if (d.isToken) {
+ const t = TOKEN_BY_ADDRESS.get(d.address.toLowerCase());
+ const label = t ? t.symbol : "Unknown token";
+ detailsHtml += `
${escapeHtml(label)}
`;
+ }
+ const dot = addressDotHtml(d.address);
+ const link = `https://etherscan.io/address/${d.address}`;
+ detailsHtml +=
+ `
${dot}` +
+ `
${escapeHtml(d.address)}` +
+ `
${EXT_ICON}`;
+ } else {
+ detailsHtml += `
${escapeHtml(d.value)}
`;
+ }
+ detailsHtml += `
`;
+ }
+ $("tx-detail-decoded-details").innerHTML = detailsHtml;
+ decodedEl.classList.remove("hidden");
+ } else {
+ decodedEl.classList.add("hidden");
+ }
+
+ // Raw calldata section — shown for unknown contract calls (full, not truncated)
+ if (tx.rawInput && tx.rawInput !== "0x" && !decoded) {
+ $("tx-detail-rawdata").textContent = tx.rawInput;
+ rawDataEl.classList.remove("hidden");
+ // Label as unknown contract call
+ $("tx-detail-action").textContent = "Unknown Contract Call";
+ $("tx-detail-decoded-details").innerHTML = "";
+ decodedEl.classList.remove("hidden");
+ } else if (!decoded) {
+ rawDataEl.classList.add("hidden");
+ } else {
+ rawDataEl.classList.add("hidden");
+ }
+
showView("transaction");
document
diff --git a/src/shared/decodeCalldata.js b/src/shared/decodeCalldata.js
new file mode 100644
index 0000000..e93c058
--- /dev/null
+++ b/src/shared/decodeCalldata.js
@@ -0,0 +1,103 @@
+// Decode transaction calldata into human-readable details.
+// Shared between the approval screen and transaction history views.
+// Returns { name, description, details } or null.
+
+const { Interface, formatUnits } = require("ethers");
+const { ERC20_ABI } = require("./constants");
+const { TOKEN_BY_ADDRESS } = require("./tokenList");
+const uniswap = require("./uniswap");
+
+const erc20Iface = new Interface(ERC20_ABI);
+
+function formatTxValue(val) {
+ const parts = val.split(".");
+ if (parts.length === 1) return val + ".0000";
+ const dec = (parts[1] + "0000").slice(0, 4);
+ return parts[0] + "." + dec;
+}
+
+function decodeCalldata(data, toAddress) {
+ if (!data || data === "0x" || data.length < 10) return null;
+
+ // Try ERC-20 (approve / transfer)
+ try {
+ const parsed = erc20Iface.parseTransaction({ data });
+ if (parsed) {
+ const token = TOKEN_BY_ADDRESS.get(toAddress.toLowerCase());
+ const tokenSymbol = token ? token.symbol : null;
+ const tokenDecimals = token ? token.decimals : 18;
+ const contractLabel = tokenSymbol
+ ? tokenSymbol + " (" + toAddress + ")"
+ : toAddress;
+
+ if (parsed.name === "approve") {
+ const spender = parsed.args[0];
+ const rawAmount = parsed.args[1];
+ const maxUint = BigInt(
+ "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ );
+ const isUnlimited = rawAmount === maxUint;
+ const amountStr = isUnlimited
+ ? "Unlimited"
+ : formatTxValue(formatUnits(rawAmount, tokenDecimals)) +
+ (tokenSymbol ? " " + tokenSymbol : "");
+
+ return {
+ name: "Token Approval",
+ description: tokenSymbol
+ ? "Approve spending of your " + tokenSymbol
+ : "Approve spending of an ERC-20 token",
+ details: [
+ {
+ label: "Token",
+ value: contractLabel,
+ address: toAddress,
+ isToken: true,
+ },
+ {
+ label: "Spender",
+ value: spender,
+ address: spender,
+ },
+ { label: "Amount", value: amountStr },
+ ],
+ };
+ }
+
+ if (parsed.name === "transfer") {
+ const to = parsed.args[0];
+ const rawAmount = parsed.args[1];
+ const amountStr =
+ formatTxValue(formatUnits(rawAmount, tokenDecimals)) +
+ (tokenSymbol ? " " + tokenSymbol : "");
+
+ return {
+ name: "Token Transfer",
+ description: tokenSymbol
+ ? "Transfer " + tokenSymbol
+ : "Transfer ERC-20 token",
+ details: [
+ {
+ label: "Token",
+ value: contractLabel,
+ address: toAddress,
+ isToken: true,
+ },
+ { label: "Recipient", value: to, address: to },
+ { label: "Amount", value: amountStr },
+ ],
+ };
+ }
+ }
+ } catch {
+ // Not ERC-20 — fall through
+ }
+
+ // Try Uniswap Universal Router
+ const routerResult = uniswap.decode(data, toAddress);
+ if (routerResult) return routerResult;
+
+ return null;
+}
+
+module.exports = { decodeCalldata };
diff --git a/src/shared/transactions.js b/src/shared/transactions.js
index f926784..dee0d06 100644
--- a/src/shared/transactions.js
+++ b/src/shared/transactions.js
@@ -9,6 +9,7 @@
const { formatEther, formatUnits } = require("ethers");
const { log, debugFetch } = require("./log");
const { KNOWN_SYMBOLS, TOKEN_BY_ADDRESS } = require("./tokenList");
+const { decodeCalldata } = require("./decodeCalldata");
function formatTxValue(val) {
const parts = val.split(".");
@@ -23,6 +24,7 @@ function parseTx(tx, addrLower) {
const rawWei = tx.value || "0";
const toIsContract = tx.to?.is_contract || false;
const method = tx.method || null;
+ const rawInput = tx.raw_input || null;
// For contract calls, produce a meaningful label instead of "0.0000 ETH"
let symbol = "ETH";
@@ -32,14 +34,41 @@ function parseTx(tx, addrLower) {
let rawUnit = "wei";
let direction = from.toLowerCase() === addrLower ? "sent" : "received";
let directionLabel = direction === "sent" ? "Sent" : "Received";
- if (toIsContract && method && method !== "transfer") {
- const token = TOKEN_BY_ADDRESS.get(to.toLowerCase());
- if (token) {
- symbol = token.symbol;
+ let decoded = null;
+
+ if (rawInput && rawInput !== "0x" && rawInput.length >= 10) {
+ decoded = decodeCalldata(rawInput, to);
+ if (decoded) {
+ // Uniswap swaps: show "Swap" with token pair
+ if (decoded.name && decoded.name.startsWith("Swap")) {
+ direction = "contract";
+ directionLabel = decoded.name;
+ value = "";
+ exactValue = "";
+ rawAmount = "";
+ rawUnit = "";
+ } else if (decoded.name === "Token Approval") {
+ direction = "contract";
+ directionLabel = "Approve";
+ value = "";
+ exactValue = "";
+ rawAmount = "";
+ rawUnit = "";
+ }
+ // Token Transfer: keep as Sent/Received (handled by token transfer overlay)
+ } else if (toIsContract && method && method !== "transfer") {
+ // Unknown contract call
+ direction = "contract";
+ directionLabel = "Contract Call";
+ value = "";
+ exactValue = "";
+ rawAmount = "";
+ rawUnit = "";
}
- const label = method.charAt(0).toUpperCase() + method.slice(1);
+ } else if (toIsContract && method && method !== "transfer") {
+ // Contract call without raw input data
direction = "contract";
- directionLabel = label;
+ directionLabel = "Contract Call";
value = "";
exactValue = "";
rawAmount = "";
@@ -65,6 +94,8 @@ function parseTx(tx, addrLower) {
holders: null,
isContractCall: toIsContract,
method: method,
+ rawInput: rawInput,
+ decoded: decoded,
};
}