diff --git a/src/popup/views/approval.js b/src/popup/views/approval.js
index 1226d9a..241b678 100644
--- a/src/popup/views/approval.js
+++ b/src/popup/views/approval.js
@@ -1,6 +1,8 @@
const { $, addressDotHtml, escapeHtml, showView } = require("./helpers");
const { state, saveState } = require("../../shared/state");
-const { formatEther } = require("ethers");
+const { formatEther, formatUnits, Interface } = require("ethers");
+const { ERC20_ABI } = require("../../shared/constants");
+const { TOKENS } = require("../../shared/tokens");
const runtime =
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
@@ -12,6 +14,14 @@ const EXT_ICON =
`
` +
``;
+const erc20Iface = new Interface(ERC20_ABI);
+
+// Build address→token lookup from known token list
+const TOKEN_BY_ADDRESS = new Map();
+for (const t of TOKENS) {
+ TOKEN_BY_ADDRESS.set(t.address.toLowerCase(), t);
+}
+
function approvalAddressHtml(address) {
const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`;
@@ -26,28 +36,161 @@ function formatTxValue(val) {
return parts[0] + "." + dec;
}
-let approvalId = null;
+function tokenLabel(address) {
+ const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
+ return t ? t.symbol : null;
+}
+
+function etherscanTokenLink(address) {
+ return `https://etherscan.io/token/${address}`;
+}
+
+// Try to decode calldata using the ERC-20 ABI.
+// Returns { name, description, details } or null.
+function decodeCalldata(data, toAddress) {
+ if (!data || data === "0x" || data.length < 10) return null;
+
+ try {
+ const parsed = erc20Iface.parseTransaction({ data });
+ if (!parsed) return null;
+
+ 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 },
+ ],
+ };
+ }
+
+ return null;
+ } catch {
+ return null;
+ }
+}
function showTxApproval(details) {
$("approve-tx-hostname").textContent = details.hostname;
$("approve-tx-from").innerHTML = approvalAddressHtml(state.activeAddress);
const toAddr = details.txParams.to;
- $("approve-tx-to").innerHTML = toAddr
- ? approvalAddressHtml(toAddr)
- : escapeHtml("(contract creation)");
+
+ // Show token symbol next to contract address if known
+ const symbol = toAddr ? tokenLabel(toAddr) : null;
+ if (toAddr) {
+ let toHtml = "";
+ if (symbol) {
+ toHtml += `
${escapeHtml(symbol)}
`;
+ }
+ toHtml += approvalAddressHtml(toAddr);
+ if (symbol) {
+ const link = etherscanTokenLink(toAddr);
+ toHtml = toHtml.replace("
", "") + ""; // approvalAddressHtml already has etherscan link
+ }
+ $("approve-tx-to").innerHTML = toHtml;
+ } else {
+ $("approve-tx-to").innerHTML = escapeHtml("(contract creation)");
+ }
+
$("approve-tx-value").textContent =
formatTxValue(formatEther(details.txParams.value || "0")) + " ETH";
+
+ // Decode calldata
+ const decoded = decodeCalldata(details.txParams.data, toAddr || "");
+ const decodedEl = $("approve-tx-decoded");
+ if (decoded) {
+ $("approve-tx-action").textContent = decoded.name;
+ let detailsHtml = "";
+ if (decoded.description) {
+ detailsHtml += `
`;
+ }
+ for (const d of decoded.details) {
+ detailsHtml += `
`;
+ detailsHtml += `
${escapeHtml(d.label)}
`;
+ if (d.address) {
+ if (d.isToken) {
+ const tLink = etherscanTokenLink(d.address);
+ detailsHtml += `
${escapeHtml(tokenLabel(d.address) || "Unknown token")}
`;
+ detailsHtml += approvalAddressHtml(d.address);
+ } else {
+ detailsHtml += approvalAddressHtml(d.address);
+ }
+ } else {
+ detailsHtml += `
${escapeHtml(d.value)}
`;
+ }
+ detailsHtml += `
`;
+ }
+ $("approve-tx-decoded-details").innerHTML = detailsHtml;
+ decodedEl.classList.remove("hidden");
+ } else {
+ decodedEl.classList.add("hidden");
+ }
+
+ // Always show raw data when present
if (details.txParams.data && details.txParams.data !== "0x") {
$("approve-tx-data").textContent = details.txParams.data;
$("approve-tx-data-section").classList.remove("hidden");
+ } else {
+ $("approve-tx-data-section").classList.add("hidden");
}
+
showView("approve-tx");
}
function show(id) {
approvalId = id;
- // Connect a port so the background detects if the popup closes
- // without an explicit response (e.g. user clicks away).
runtime.connect({ name: "approval:" + id });
runtime.sendMessage({ type: "AUTISTMASK_GET_APPROVAL", id }, (details) => {
if (!details) {
@@ -66,6 +209,8 @@ function show(id) {
});
}
+let approvalId = null;
+
function init(ctx) {
$("approve-remember").addEventListener("change", async () => {
state.rememberSiteChoice = $("approve-remember").checked;