const { $, addressDotHtml, addressTitle, escapeHtml, showView, showError, hideError, } = require("./helpers"); const { state, saveState, currentNetwork } = require("../../shared/state"); const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers"); const { ERC20_ABI } = require("../../shared/constants"); const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList"); const txStatus = require("./txStatus"); const uniswap = require("../../shared/uniswap"); const runtime = typeof browser !== "undefined" ? browser.runtime : chrome.runtime; const EXT_ICON = `` + `` + `` + `` + ``; const erc20Iface = new Interface(ERC20_ABI); function approvalAddressHtml(address) { const dot = addressDotHtml(address); const link = `${currentNetwork().explorerUrl}/address/${address}`; const extLink = `${EXT_ICON}`; const title = addressTitle(address, state.wallets); let html = ""; if (title) { html += `
${dot}${escapeHtml(title)}
`; html += `
${escapeHtml(address)}${extLink}
`; } else { html += `
${dot}${escapeHtml(address)}${extLink}
`; } return html; } 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 tokenLabel(address) { const t = TOKEN_BY_ADDRESS.get(address.toLowerCase()); return t ? t.symbol : null; } function etherscanTokenLink(address) { return `${currentNetwork().explorerUrl}/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 amountRaw = isUnlimited ? "Unlimited" : formatTxValue(formatUnits(rawAmount, tokenDecimals)); const amountStr = isUnlimited ? "Unlimited" : amountRaw + (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, rawValue: amountRaw, }, ], }; } if (parsed.name === "transfer") { const to = parsed.args[0]; const rawAmount = parsed.args[1]; const amountRaw = formatTxValue( formatUnits(rawAmount, tokenDecimals), ); const amountStr = amountRaw + (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, rawValue: amountRaw, }, ], }; } } } catch { // Not ERC-20 — fall through } // Try Uniswap Universal Router const routerResult = uniswap.decode(data, toAddress); if (routerResult) return routerResult; return null; } function showPhishingWarning(elementId, isPhishing) { const el = $(elementId); if (!el) return; // The background script performs the authoritative phishing domain check // and passes the result via the isPhishingDomain flag. if (isPhishing) { el.classList.remove("hidden"); } else { el.classList.add("hidden"); } } function showTxApproval(details) { showPhishingWarning( "approve-tx-phishing-warning", details.isPhishingDomain, ); const toAddr = details.txParams.to; const token = toAddr ? TOKEN_BY_ADDRESS.get(toAddr.toLowerCase()) : null; const ethValue = formatEther(details.txParams.value || "0"); // Build txInfo for status screens pendingTxDetails = { from: state.activeAddress, to: toAddr || "", amount: formatTxValue(ethValue), token: "ETH", tokenSymbol: token ? token.symbol : null, }; // If this is an ERC-20 call, try to extract the real recipient and amount const decoded = decodeCalldata(details.txParams.data, toAddr || ""); if (decoded && decoded.details) { let decodedTokenAddr = null; let decodedTokenSymbol = null; for (const d of decoded.details) { if (d.label === "Recipient" && d.address) { pendingTxDetails.to = d.address; } if (d.label === "Amount") { pendingTxDetails.amount = d.rawValue || d.value; } if (d.label === "Token In" && d.isToken && d.address) { const t = TOKEN_BY_ADDRESS.get(d.address.toLowerCase()); if (t) { decodedTokenAddr = d.address; decodedTokenSymbol = t.symbol; } } } if (token) { pendingTxDetails.token = toAddr; pendingTxDetails.tokenSymbol = token.symbol; } else if (decodedTokenAddr) { pendingTxDetails.token = decodedTokenAddr; pendingTxDetails.tokenSymbol = decodedTokenSymbol; } } // Carry decoded calldata info through to success/error views if (decoded) { pendingTxDetails.decoded = { name: decoded.name, description: decoded.description, details: decoded.details, }; } $("approve-tx-hostname").textContent = details.hostname; $("approve-tx-from").innerHTML = approvalAddressHtml(state.activeAddress); // 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 (reuse decoded from above) const decodedEl = $("approve-tx-decoded"); if (decoded) { $("approve-tx-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 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"); } $("approve-tx-password").value = ""; hideError("approve-tx-error"); showView("approve-tx"); } function decodeHexMessage(hex) { try { const bytes = Uint8Array.from( hex .slice(2) .match(/.{1,2}/g) .map((b) => parseInt(b, 16)), ); return toUtf8String(bytes); } catch { return null; } } function formatTypedDataHtml(jsonStr) { try { const data = JSON.parse(jsonStr); let html = ""; if (data.domain) { html += `
Domain
`; for (const [key, val] of Object.entries(data.domain)) { html += `
${escapeHtml(key)}: ${escapeHtml(String(val))}
`; } html += `
`; } if (data.primaryType) { html += `
Primary type
`; html += `
${escapeHtml(data.primaryType)}
`; } if (data.message) { html += `
Message
`; for (const [key, val] of Object.entries(data.message)) { const display = typeof val === "object" ? JSON.stringify(val) : String(val); html += `
${escapeHtml(key)}: ${escapeHtml(display)}
`; } html += `
`; } return html; } catch { return `
${escapeHtml(jsonStr)}
`; } } function showSignApproval(details) { showPhishingWarning( "approve-sign-phishing-warning", details.isPhishingDomain, ); const sp = details.signParams; $("approve-sign-hostname").textContent = details.hostname; $("approve-sign-from").innerHTML = approvalAddressHtml(sp.from); const isTyped = sp.method === "eth_signTypedData_v4" || sp.method === "eth_signTypedData"; $("approve-sign-type").textContent = isTyped ? "Typed data (EIP-712)" : "Personal message"; if (isTyped) { $("approve-sign-message").innerHTML = formatTypedDataHtml(sp.typedData); } else { const decoded = decodeHexMessage(sp.message); if (decoded !== null) { $("approve-sign-message").textContent = decoded; } else { $("approve-sign-message").textContent = sp.message; } } // Display danger warning for eth_sign (raw hash signing) const warningEl = $("approve-sign-danger-warning"); if (warningEl) { if (sp.dangerWarning) { warningEl.textContent = sp.dangerWarning; warningEl.style.visibility = "visible"; } else { warningEl.textContent = ""; warningEl.style.visibility = "hidden"; } } $("approve-sign-password").value = ""; hideError("approve-sign-error"); $("btn-approve-sign").disabled = false; $("btn-approve-sign").classList.remove("text-muted"); showView("approve-sign"); } function show(id) { approvalId = id; runtime.connect({ name: "approval:" + id }); runtime.sendMessage({ type: "AUTISTMASK_GET_APPROVAL", id }, (details) => { if (!details) { window.close(); return; } if (details.type === "tx") { showTxApproval(details); return; } if (details.type === "sign") { showSignApproval(details); return; } // Site connection approval showPhishingWarning( "approve-site-phishing-warning", details.isPhishingDomain, ); $("approve-hostname").textContent = details.hostname; $("approve-address").innerHTML = approvalAddressHtml( state.activeAddress, ); $("approve-remember").checked = state.rememberSiteChoice; }); } let approvalId = null; let pendingTxDetails = null; function init(ctx) { $("approve-remember").addEventListener("change", async () => { state.rememberSiteChoice = $("approve-remember").checked; await saveState(); }); $("btn-approve").addEventListener("click", () => { const remember = $("approve-remember").checked; runtime.sendMessage({ type: "AUTISTMASK_APPROVAL_RESPONSE", id: approvalId, approved: true, remember, }); window.close(); }); $("btn-reject").addEventListener("click", () => { const remember = $("approve-remember").checked; runtime.sendMessage({ type: "AUTISTMASK_APPROVAL_RESPONSE", id: approvalId, approved: false, remember, }); window.close(); }); $("btn-approve-tx").addEventListener("click", () => { const password = $("approve-tx-password").value; if (!password) { showError("approve-tx-error", "Please enter your password."); return; } hideError("approve-tx-error"); $("btn-approve-tx").disabled = true; $("btn-approve-tx").classList.add("text-muted"); runtime.sendMessage( { type: "AUTISTMASK_TX_RESPONSE", id: approvalId, approved: true, // TODO(security): Move decryption to popup to avoid sending password via runtime.sendMessage password: password, }, (response) => { if (response && response.txHash) { txStatus.showWait(pendingTxDetails, response.txHash); } else { const msg = (response && response.error) || "Transaction failed."; txStatus.showError(pendingTxDetails, null, msg); } }, ); }); $("btn-reject-tx").addEventListener("click", () => { runtime.sendMessage({ type: "AUTISTMASK_TX_RESPONSE", id: approvalId, approved: false, }); window.close(); }); $("btn-approve-sign").addEventListener("click", () => { const password = $("approve-sign-password").value; if (!password) { showError("approve-sign-error", "Please enter your password."); return; } hideError("approve-sign-error"); $("btn-approve-sign").disabled = true; $("btn-approve-sign").classList.add("text-muted"); runtime.sendMessage( { type: "AUTISTMASK_SIGN_RESPONSE", id: approvalId, approved: true, // TODO(security): Move decryption to popup to avoid sending password via runtime.sendMessage password: password, }, (response) => { if (response && response.signature) { window.close(); } else { const msg = (response && response.error) || "Signing failed."; showError("approve-sign-error", msg); $("btn-approve-sign").disabled = false; $("btn-approve-sign").classList.remove("text-muted"); } }, ); }); $("btn-reject-sign").addEventListener("click", () => { runtime.sendMessage({ type: "AUTISTMASK_SIGN_RESPONSE", id: approvalId, approved: false, }); window.close(); }); } module.exports = { init, show, decodeCalldata };