const { $, addressTitle, escapeHtml, showView, showError, hideError, renderAddressHtml, attachCopyHandlers, } = require("./helpers"); const { state, saveState, currentNetwork } = require("../../shared/state"); const { formatEther, formatUnits, getBytes, Interface, toUtf8String, } = require("ethers"); const { getPrice, formatUsd } = require("../../shared/prices"); const { ERC20_ABI } = require("../../shared/constants"); const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList"); const { decryptWithPassword } = require("../../shared/vault"); const { getSignerForAddress } = require("../../shared/wallet"); const { walletDefect } = require("../../shared/walletDefects"); const { getProvider } = require("../../shared/balances"); const txStatus = require("./txStatus"); const uniswap = require("../../shared/uniswap"); const runtime = typeof browser !== "undefined" ? browser.runtime : chrome.runtime; const erc20Iface = new Interface(ERC20_ABI); function approvalAddressHtml(address) { const title = addressTitle(address, state.wallets); return renderAddressHtml(address, { title }); } 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; } // 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, ); pendingTxParams = details.txParams; 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); $("approve-tx-to").innerHTML = toHtml; } else { $("approve-tx-to").innerHTML = escapeHtml("(contract creation)"); } const ethValueFormatted = formatTxValue( formatEther(details.txParams.value || "0"), ); const ethPrice = getPrice("ETH"); const ethUsd = ethPrice ? parseFloat(ethValueFormatted) * ethPrice : null; const usdStr = formatUsd(ethUsd); $("approve-tx-value").textContent = ethValueFormatted + " ETH" + (usdStr ? " (" + usdStr + ")" : ""); // 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) { detailsHtml += `
${escapeHtml(tokenLabel(d.address) || "Unknown token")}
`; } 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"); attachCopyHandlers("view-approve-tx"); gateOnWalletDefect("approve-tx-error", "btn-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; pendingSignParams = sp; $("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"); attachCopyHandlers("view-approve-sign"); gateOnWalletDefect("approve-sign-error", "btn-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, ); attachCopyHandlers("view-approve-site"); $("approve-remember").checked = state.rememberSiteChoice; }); } let approvalId = null; let pendingTxDetails = null; // The exact parameters shown to the user, kept so the popup signs what it // displayed rather than re-fetching anything at approval time. Both are // repopulated by show() when the popup is closed and reopened. let pendingTxParams = null; let pendingSignParams = null; // Approve buttons stay disabled and muted while the popup derives the key and // signs, which is slow enough (Argon2id) that a double click is likely. function setTxButtonBusy(busy) { $("btn-approve-tx").disabled = busy; $("btn-approve-tx").classList.toggle("text-muted", busy); } function setSignButtonBusy(busy) { $("btn-approve-sign").disabled = busy; $("btn-approve-sign").classList.toggle("text-muted", busy); } // Say so on the approval screen itself, and disable the approve button, when // the active address belongs to a wallet whose keys cannot be derived. Without // this the screen would take a password and fail after deriving it. Reject // stays available; the wallet is not touched. Returns true when it gated. function gateOnWalletDefect(errorId, buttonId) { const active = findActiveWallet(); const defect = active ? walletDefect(active.wallet) : null; if (!defect) return false; showError(errorId, defect.shortMessage); $(buttonId).disabled = true; $(buttonId).classList.add("text-muted"); return true; } // Locate the wallet and the address index owning the currently active // address. Returns null when no wallet holds it. function findActiveWallet() { for (const wallet of state.wallets) { for (let i = 0; i < wallet.addresses.length; i++) { if (wallet.addresses[i].address === state.activeAddress) { return { wallet, addrIndex: i }; } } } return 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", async () => { let password = $("approve-tx-password").value; if (!password) { showError("approve-tx-error", "Please enter your password."); return; } hideError("approve-tx-error"); setTxButtonBusy(true); const active = findActiveWallet(); if (!active) { password = null; showError( "approve-tx-error", "No wallet was found for the active address.", ); setTxButtonBusy(false); return; } const defect = walletDefect(active.wallet); if (defect) { password = null; showError("approve-tx-error", defect.shortMessage); setTxButtonBusy(false); return; } // Decrypt here, in the popup. The password must never cross the // extension messaging boundary; only the signed transaction does. let decryptedSecret; try { decryptedSecret = await decryptWithPassword( active.wallet.encryptedSecret, password, ); } catch { showError( "approve-tx-error", "That password is incorrect. Please try again.", ); setTxButtonBusy(false); return; } finally { // Best-effort: drop the password as soon as the key derivation // is done. Note that JS strings are immutable; this clears the // reference but the original string may persist until GC. password = null; } const payload = { type: "AUTISTMASK_TX_RESPONSE", id: approvalId, approved: true, }; try { const signer = getSignerForAddress( active.wallet, active.addrIndex, decryptedSecret, ); const provider = getProvider(state.rpcUrl); const connected = signer.connect(provider); // This is the sequence ethers' own sendTransaction() runs // internally, so nonce, gas, fee and chain id population are // identical to when the background did the signing. const populated = await connected.populateTransaction(pendingTxParams); delete populated.from; payload.rawSignedTx = await connected.signTransaction(populated); } catch (e) { payload.error = e.shortMessage || e.message || "Transaction signing failed."; } finally { // Best-effort: clear the decrypted secret after use, with the // same immutability caveat as the password above. decryptedSecret = null; } runtime.sendMessage(payload, (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", async () => { let password = $("approve-sign-password").value; if (!password) { showError("approve-sign-error", "Please enter your password."); return; } hideError("approve-sign-error"); setSignButtonBusy(true); const active = findActiveWallet(); if (!active) { password = null; showError( "approve-sign-error", "No wallet was found for the active address.", ); setSignButtonBusy(false); return; } const defect = walletDefect(active.wallet); if (defect) { password = null; showError("approve-sign-error", defect.shortMessage); setSignButtonBusy(false); return; } // Decrypt here, in the popup. The password must never cross the // extension messaging boundary; only the signature does. let decryptedSecret; try { decryptedSecret = await decryptWithPassword( active.wallet.encryptedSecret, password, ); } catch { showError( "approve-sign-error", "That password is incorrect. Please try again.", ); setSignButtonBusy(false); return; } finally { // Best-effort: drop the password as soon as the key derivation // is done. Note that JS strings are immutable; this clears the // reference but the original string may persist until GC. password = null; } const payload = { type: "AUTISTMASK_SIGN_RESPONSE", id: approvalId, approved: true, }; try { const signer = getSignerForAddress( active.wallet, active.addrIndex, decryptedSecret, ); const sp = pendingSignParams; if (sp.method === "personal_sign" || sp.method === "eth_sign") { payload.signature = await signer.signMessage( getBytes(sp.message), ); } else { // eth_signTypedData_v4 / eth_signTypedData const typedData = JSON.parse(sp.typedData); const { domain, types, message } = typedData; // ethers handles EIP712Domain internally delete types.EIP712Domain; payload.signature = await signer.signTypedData( domain, types, message, ); } } catch (e) { payload.error = e.shortMessage || e.message || "Signing failed."; } finally { // Best-effort: clear the decrypted secret after use, with the // same immutability caveat as the password above. decryptedSecret = null; } runtime.sendMessage(payload, (response) => { if (response && response.signature) { window.close(); } else { const msg = (response && response.error) || "Signing failed."; showError("approve-sign-error", msg); setSignButtonBusy(false); } }); }); $("btn-reject-sign").addEventListener("click", () => { runtime.sendMessage({ type: "AUTISTMASK_SIGN_RESPONSE", id: approvalId, approved: false, }); window.close(); }); } module.exports = { init, show, decodeCalldata };