// AutistMask popup UI — view management and event wiring const { Mnemonic, HDNodeWallet, Wallet, JsonRpcProvider, formatEther, parseEther, } = require("ethers"); const { getTopTokenPrices } = require("../shared/tokens"); const { encryptWithPassword, decryptWithPassword } = require("../shared/vault"); const QRCode = require("qrcode"); const DEBUG = true; const DEBUG_MNEMONIC = "cube evolve unfold result inch risk jealous skill hotel bulb night wreck"; const BIP44_ETH_BASE = "m/44'/60'/0'/0"; const VIEWS = [ "welcome", "add-wallet", "import-key", "main", "address", "send", "receive", "add-token", "settings", "approve", ]; function showView(name) { for (const v of VIEWS) { const el = document.getElementById(`view-${v}`); if (el) { el.classList.toggle("hidden", v !== name); } } } // Browser-agnostic storage API const storageApi = typeof browser !== "undefined" ? browser.storage.local : chrome.storage.local; // Persisted state (unencrypted, public data only): // wallets[]: { type, name, xpub (for hd), addresses: [{ address, balance, tokens }], nextIndex } // Mnemonic/private key will be stored encrypted separately (not yet implemented). const DEFAULT_STATE = { hasWallet: false, wallets: [], rpcUrl: "https://eth.llamarpc.com", }; const state = { ...DEFAULT_STATE, selectedWallet: null, selectedAddress: null, }; async function saveState() { const persisted = { hasWallet: state.hasWallet, wallets: state.wallets, rpcUrl: state.rpcUrl, }; await storageApi.set({ autistmask: persisted }); } async function loadState() { const result = await storageApi.get("autistmask"); if (result.autistmask) { const saved = result.autistmask; state.hasWallet = saved.hasWallet; state.wallets = saved.wallets || []; state.rpcUrl = saved.rpcUrl || DEFAULT_STATE.rpcUrl; } } // -- helpers -- function $(id) { return document.getElementById(id); } function showError(id, msg) { const el = $(id); el.textContent = msg; el.classList.remove("hidden"); } function hideError(id) { $(id).classList.add("hidden"); } function generateMnemonic() { if (DEBUG) return DEBUG_MNEMONIC; const m = Mnemonic.fromEntropy( globalThis.crypto.getRandomValues(new Uint8Array(16)), ); return m.phrase; } // Derive an Ethereum address at index from an xpub string function deriveAddressFromXpub(xpub, index) { const node = HDNodeWallet.fromExtendedKey(xpub); const child = node.deriveChild(index); return child.address; } // Create an HD wallet from a mnemonic: returns { xpub, firstAddress } function hdWalletFromMnemonic(mnemonic) { const node = HDNodeWallet.fromPhrase(mnemonic, "", BIP44_ETH_BASE); const xpub = node.neuter().extendedKey; const firstAddress = node.deriveChild(0).address; return { xpub, firstAddress }; } // Get address from a private key function addressFromPrivateKey(key) { const w = new Wallet(key); return w.address; } // -- price fetching -- // { "ETH": 1234.56, "LINK": 8.60, ... } const prices = {}; async function refreshPrices() { try { const fetched = await getTopTokenPrices(25); Object.assign(prices, fetched); } catch (e) { // prices stay empty on error } } function formatUsd(amount) { if (amount === null || amount === undefined || isNaN(amount)) return ""; if (amount === 0) return "$0.00"; if (amount < 0.01) return "< $0.01"; return ( "$" + amount.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, }) ); } // Get an ethers Wallet (signer) for the currently selected address. // Requires the decrypted secret (mnemonic or private key). function getSignerForCurrentAddress(decryptedSecret) { const wallet = state.wallets[state.selectedWallet]; const addrIndex = state.selectedAddress; if (wallet.type === "hd") { const node = HDNodeWallet.fromPhrase( decryptedSecret, "", BIP44_ETH_BASE, ); return node.deriveChild(addrIndex); } else { return new Wallet(decryptedSecret); } } // -- balance fetching -- function getProvider() { return new JsonRpcProvider(state.rpcUrl); } function formatBalance(wei) { const eth = formatEther(wei); // Show up to 6 decimal places, trim trailing zeros const parts = eth.split("."); if (parts.length === 1) return eth + ".0"; const dec = parts[1].slice(0, 6).replace(/0+$/, "") || "0"; return parts[0] + "." + dec; } async function refreshBalances() { const provider = getProvider(); const updates = []; for (const wallet of state.wallets) { for (const addr of wallet.addresses) { // Fetch ETH balance updates.push( provider .getBalance(addr.address) .then((bal) => { addr.balance = formatBalance(bal); }) .catch(() => {}), ); // Reverse ENS lookup updates.push( provider .lookupAddress(addr.address) .then((name) => { addr.ensName = name || null; }) .catch(() => { addr.ensName = null; }), ); } } await Promise.all(updates); await saveState(); } // -- render wallet list on main view -- function renderWalletList() { const container = $("wallet-list"); if (state.wallets.length === 0) { container.innerHTML = '

No wallets yet. Add one to get started.

'; return; } let html = ""; state.wallets.forEach((wallet, wi) => { html += `
`; html += `
`; html += `${wallet.name}`; if (wallet.type === "hd") { html += ``; } html += `
`; wallet.addresses.forEach((addr, ai) => { html += `
`; if (addr.ensName) { html += `
${addr.ensName}
`; } html += `
${addr.address}
`; html += `
`; html += `${addr.balance} ETH`; const ethUsd = prices.ETH ? parseFloat(addr.balance) * prices.ETH : null; if (ethUsd !== null) { html += `${formatUsd(ethUsd)}`; } html += `
`; html += `
`; }); html += `
`; }); container.innerHTML = html; container.querySelectorAll(".address-row").forEach((row) => { row.addEventListener("click", () => { state.selectedWallet = parseInt(row.dataset.wallet, 10); state.selectedAddress = parseInt(row.dataset.address, 10); showAddressDetail(); }); }); container.querySelectorAll(".btn-add-address").forEach((btn) => { btn.addEventListener("click", async (e) => { e.stopPropagation(); const wi = parseInt(btn.dataset.wallet, 10); const wallet = state.wallets[wi]; const newAddr = deriveAddressFromXpub( wallet.xpub, wallet.nextIndex, ); wallet.addresses.push({ address: newAddr, balance: "0.0000", tokens: [], }); wallet.nextIndex++; await saveState(); renderWalletList(); }); }); } function showAddressDetail() { const wallet = state.wallets[state.selectedWallet]; const addr = wallet.addresses[state.selectedAddress]; $("address-title").textContent = wallet.name; $("address-full").textContent = addr.address; $("address-copied-msg").textContent = ""; $("address-eth-balance").textContent = addr.balance; const ethUsd = prices.ETH ? parseFloat(addr.balance) * prices.ETH : null; $("address-usd-value").textContent = ethUsd !== null ? formatUsd(ethUsd) : ""; const ensEl = $("address-ens"); if (addr.ensName) { ensEl.textContent = addr.ensName; ensEl.classList.remove("hidden"); } else { ensEl.classList.add("hidden"); } updateTokenList(addr); updateSendTokenSelect(addr); showView("address"); } function updateTokenList(addr) { const list = $("token-list"); if (addr.tokens.length === 0) { list.innerHTML = '
No tokens added yet. Use "+ Add" to track a token.
'; return; } list.innerHTML = addr.tokens .map( (t) => `
` + `${t.symbol}` + `${t.balance || "0"}` + `
`, ) .join(""); } function updateSendTokenSelect(addr) { const sel = $("send-token"); sel.innerHTML = ''; addr.tokens.forEach((t) => { const opt = document.createElement("option"); opt.value = t.contractAddress; opt.textContent = t.symbol; sel.appendChild(opt); }); } function currentAddress() { if (state.selectedWallet === null || state.selectedAddress === null) { return null; } return state.wallets[state.selectedWallet].addresses[state.selectedAddress]; } async function addWalletAndGoToMain(wallet) { state.wallets.push(wallet); state.hasWallet = true; await saveState(); renderWalletList(); showView("main"); } function showAddWalletView() { $("wallet-mnemonic").value = ""; $("add-wallet-phrase-warning").classList.add("hidden"); hideError("add-wallet-error"); showView("add-wallet"); } function showImportKeyView() { $("import-private-key").value = ""; hideError("import-key-error"); showView("import-key"); } function backFromWalletAdd() { if (!state.hasWallet) { showView("welcome"); } else { renderWalletList(); showView("main"); } } // -- init -- async function init() { if (DEBUG) { const banner = document.createElement("div"); banner.textContent = "DEBUG / INSECURE"; banner.style.cssText = "background:#c00;color:#fff;text-align:center;font-size:10px;padding:1px 0;font-family:monospace;position:sticky;top:0;z-index:9999;"; document.body.prepend(banner); } await loadState(); if (!state.hasWallet) { showView("welcome"); } else { renderWalletList(); showView("main"); // Fetch prices and balances in parallel, re-render as each completes refreshPrices().then(() => renderWalletList()); refreshBalances().then(() => renderWalletList()); } // -- Welcome -- $("btn-welcome-add").addEventListener("click", showAddWalletView); // -- Add wallet (unified create/import) -- $("btn-generate-phrase").addEventListener("click", () => { const phrase = generateMnemonic(); $("wallet-mnemonic").value = phrase; $("add-wallet-phrase-warning").classList.remove("hidden"); }); $("btn-add-wallet-confirm").addEventListener("click", async () => { const mnemonic = $("wallet-mnemonic").value.trim(); if (!mnemonic) { showError( "add-wallet-error", "Please enter a recovery phrase or press the die to generate one.", ); return; } const words = mnemonic.split(/\s+/); if (words.length !== 12 && words.length !== 24) { showError( "add-wallet-error", "Recovery phrase must be 12 or 24 words. You entered " + words.length + ".", ); return; } if (!Mnemonic.isValidMnemonic(mnemonic)) { showError( "add-wallet-error", "Invalid recovery phrase. Please check for typos.", ); return; } const pw = $("add-wallet-password").value; const pw2 = $("add-wallet-password-confirm").value; if (!pw) { showError("add-wallet-error", "Please choose a password."); return; } if (pw.length < 8) { showError( "add-wallet-error", "Password must be at least 8 characters.", ); return; } if (pw !== pw2) { showError("add-wallet-error", "Passwords do not match."); return; } hideError("add-wallet-error"); const encrypted = await encryptWithPassword(mnemonic, pw); const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic); const walletNum = state.wallets.length + 1; addWalletAndGoToMain({ type: "hd", name: "Wallet " + walletNum, xpub: xpub, encryptedSecret: encrypted, nextIndex: 1, addresses: [ { address: firstAddress, balance: "0.0000", tokens: [] }, ], }); }); $("btn-add-wallet-back").addEventListener("click", backFromWalletAdd); $("btn-add-wallet-import-key").addEventListener("click", showImportKeyView); // -- Import private key -- $("btn-import-key-confirm").addEventListener("click", async () => { const key = $("import-private-key").value.trim(); if (!key) { showError("import-key-error", "Please enter your private key."); return; } let addr; try { addr = addressFromPrivateKey(key); } catch (e) { showError("import-key-error", "Invalid private key."); return; } const pw = $("import-key-password").value; const pw2 = $("import-key-password-confirm").value; if (!pw) { showError("import-key-error", "Please choose a password."); return; } if (pw.length < 8) { showError( "import-key-error", "Password must be at least 8 characters.", ); return; } if (pw !== pw2) { showError("import-key-error", "Passwords do not match."); return; } hideError("import-key-error"); const encrypted = await encryptWithPassword(key, pw); const walletNum = state.wallets.length + 1; addWalletAndGoToMain({ type: "key", name: "Wallet " + walletNum, encryptedSecret: encrypted, addresses: [{ address: addr, balance: "0.0000", tokens: [] }], }); }); $("btn-import-key-back").addEventListener("click", backFromWalletAdd); // -- Main view -- $("btn-settings").addEventListener("click", () => { $("settings-rpc").value = state.rpcUrl; showView("settings"); }); $("btn-main-add-wallet").addEventListener("click", showAddWalletView); // -- Address detail -- $("address-full").addEventListener("click", () => { const addr = $("address-full").textContent; if (addr) { navigator.clipboard.writeText(addr); $("address-copied-msg").textContent = "Copied!"; setTimeout(() => { $("address-copied-msg").textContent = ""; }, 2000); } }); $("btn-address-back").addEventListener("click", () => { renderWalletList(); showView("main"); }); $("btn-send").addEventListener("click", () => { $("send-to").value = ""; $("send-amount").value = ""; $("send-password").value = ""; $("send-fee-estimate").classList.add("hidden"); $("send-status").classList.add("hidden"); showView("send"); }); $("btn-receive").addEventListener("click", () => { const addr = currentAddress(); const address = addr ? addr.address : ""; $("receive-address").textContent = address; if (address) { QRCode.toCanvas($("receive-qr"), address, { width: 200, margin: 2, color: { dark: "#000000", light: "#ffffff" }, }); } showView("receive"); }); $("btn-add-token").addEventListener("click", () => { $("add-token-address").value = ""; $("add-token-info").classList.add("hidden"); hideError("add-token-error"); showView("add-token"); }); // -- Send -- $("btn-send-confirm").addEventListener("click", async () => { const to = $("send-to").value.trim(); const amount = $("send-amount").value.trim(); if (!to) { showError("send-status", "Please enter a recipient address."); $("send-status").classList.remove("hidden"); return; } if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) { showError("send-status", "Please enter a valid amount."); $("send-status").classList.remove("hidden"); return; } // Resolve ENS name if it looks like one let resolvedTo = to; if (to.includes(".") && !to.startsWith("0x")) { const statusEl = $("send-status"); statusEl.textContent = "Resolving " + to + "..."; statusEl.classList.remove("hidden"); try { const provider = getProvider(); const resolved = await provider.resolveName(to); if (!resolved) { showError("send-status", "Could not resolve " + to); return; } resolvedTo = resolved; statusEl.textContent = to + " = " + resolvedTo; } catch (e) { showError("send-status", "Failed to resolve ENS name."); return; } } const password = $("send-password").value; if (!password) { showError("send-status", "Please enter your password."); $("send-status").classList.remove("hidden"); return; } const wallet = state.wallets[state.selectedWallet]; let decryptedSecret; const statusEl = $("send-status"); statusEl.textContent = "Decrypting..."; statusEl.classList.remove("hidden"); try { decryptedSecret = await decryptWithPassword( wallet.encryptedSecret, password, ); } catch (e) { showError("send-status", "Wrong password."); return; } statusEl.textContent = "Sending..."; try { const signer = getSignerForCurrentAddress(decryptedSecret); const provider = getProvider(); const connectedSigner = signer.connect(provider); const tx = await connectedSigner.sendTransaction({ to: resolvedTo, value: parseEther(amount), }); statusEl.textContent = "Sent. Waiting for confirmation..."; const receipt = await tx.wait(); statusEl.textContent = "Confirmed in block " + receipt.blockNumber + ". Tx: " + receipt.hash; // Refresh balance after send refreshBalances().then(() => renderWalletList()); } catch (e) { statusEl.textContent = "Failed: " + (e.shortMessage || e.message); } }); $("btn-send-back").addEventListener("click", () => { showAddressDetail(); }); // -- Receive -- $("btn-receive-copy").addEventListener("click", () => { const addr = $("receive-address").textContent; if (addr) { navigator.clipboard.writeText(addr); } }); $("btn-receive-back").addEventListener("click", () => { showAddressDetail(); }); // -- Add Token -- $("btn-add-token-confirm").addEventListener("click", async () => { const contractAddr = $("add-token-address").value.trim(); if (!contractAddr || !contractAddr.startsWith("0x")) { showError( "add-token-error", "Please enter a valid contract address starting with 0x.", ); return; } hideError("add-token-error"); // TODO: look up token name/symbol/decimals from contract via RPC const addr = currentAddress(); if (addr) { addr.tokens.push({ contractAddress: contractAddr, symbol: "TKN", decimals: 18, balance: "0", }); await saveState(); } showAddressDetail(); }); $("btn-add-token-back").addEventListener("click", () => { showAddressDetail(); }); // -- Settings -- $("btn-save-rpc").addEventListener("click", async () => { state.rpcUrl = $("settings-rpc").value.trim(); await saveState(); }); $("btn-settings-back").addEventListener("click", () => { renderWalletList(); showView("main"); }); // -- Approval -- $("btn-approve").addEventListener("click", () => { // TODO: send approval to background renderWalletList(); showView("main"); }); $("btn-reject").addEventListener("click", () => { // TODO: send rejection to background renderWalletList(); showView("main"); }); } document.addEventListener("DOMContentLoaded", init);