diff --git a/src/popup/index.js b/src/popup/index.js
index e928554..f11cddf 100644
--- a/src/popup/index.js
+++ b/src/popup/index.js
@@ -1,237 +1,25 @@
-// AutistMask popup UI — view switching and event wiring only.
-// All business logic lives in src/shared/*.
+// AutistMask popup entry point.
+// Loads state, initializes views, triggers first render.
-const { parseEther } = require("ethers");
-const QRCode = require("qrcode");
-const { TOKENS } = require("../shared/tokens");
-const {
- state,
- saveState,
- loadState,
- currentAddress,
-} = require("../shared/state");
-const {
- DEBUG,
- generateMnemonic,
- deriveAddressFromXpub,
- hdWalletFromMnemonic,
- addressFromPrivateKey,
- getSignerForAddress,
- isValidMnemonic,
-} = require("../shared/wallet");
-const {
- refreshPrices,
- formatUsd,
- getAddressValueUsd,
- getTotalValueUsd,
-} = require("../shared/prices");
-const {
- refreshBalances,
- lookupTokenInfo,
- invalidateBalanceCache,
- getProvider,
-} = require("../shared/balances");
-const { encryptWithPassword, decryptWithPassword } = require("../shared/vault");
+const { DEBUG } = require("../shared/wallet");
+const { state, saveState, loadState } = require("../shared/state");
+const { refreshPrices } = require("../shared/prices");
+const { refreshBalances } = require("../shared/balances");
+const { showView } = require("./views/helpers");
-const VIEWS = [
- "welcome",
- "add-wallet",
- "import-key",
- "main",
- "address",
- "send",
- "receive",
- "add-token",
- "settings",
- "approve",
-];
-
-// -- DOM 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 showView(name) {
- for (const v of VIEWS) {
- const el = document.getElementById(`view-${v}`);
- if (el) {
- el.classList.toggle("hidden", v !== name);
- }
- }
- if (DEBUG) {
- const banner = document.getElementById("debug-banner");
- if (banner) {
- banner.textContent = "DEBUG / INSECURE (" + name + ")";
- }
- }
-}
-
-// -- rendering --
-function renderTotalValue() {
- const el = $("total-value");
- if (!el) return;
- el.textContent = formatUsd(getTotalValueUsd(state.wallets));
-}
-
-function renderTokenList(addr) {
- const list = $("token-list");
- const balances = addr.tokenBalances || [];
- if (balances.length === 0 && state.trackedTokens.length === 0) {
- list.innerHTML =
- '
No tokens added yet. Use "+ Add" to track a token.
';
- return;
- }
- list.innerHTML = balances
- .map(
- (t) =>
- `` +
- `${t.symbol}` +
- `${t.balance || "0"}` +
- `
`,
- )
- .join("");
-}
-
-function renderSendTokenSelect(addr) {
- const sel = $("send-token");
- sel.innerHTML = '';
- for (const t of addr.tokenBalances || []) {
- const opt = document.createElement("option");
- opt.value = t.address;
- opt.textContent = t.symbol;
- sel.appendChild(opt);
- }
-}
-
-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;
- $("address-usd-value").textContent = formatUsd(getAddressValueUsd(addr));
- const ensEl = $("address-ens");
- if (addr.ensName) {
- ensEl.textContent = addr.ensName;
- ensEl.classList.remove("hidden");
- } else {
- ensEl.classList.add("hidden");
- }
- renderTokenList(addr);
- renderSendTokenSelect(addr);
- showView("address");
-}
+const home = require("./views/home");
+const welcome = require("./views/welcome");
+const addWallet = require("./views/addWallet");
+const importKey = require("./views/importKey");
+const addressDetail = require("./views/addressDetail");
+const send = require("./views/send");
+const receive = require("./views/receive");
+const addToken = require("./views/addToken");
+const settings = require("./views/settings");
+const approval = require("./views/approval");
function renderWalletList() {
- const container = $("wallet-list");
- if (state.wallets.length === 0) {
- container.innerHTML =
- 'No wallets yet. Add one to get started.
';
- renderTotalValue();
- 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`;
- html += `${formatUsd(getAddressValueUsd(addr))}`;
- 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",
- tokenBalances: [],
- });
- wallet.nextIndex++;
- await saveState();
- renderWalletList();
- });
- });
-
- renderTotalValue();
-}
-
-async function addWalletAndGoToMain(wallet) {
- state.wallets.push(wallet);
- state.hasWallet = true;
- await saveState();
- renderWalletList();
- showView("main");
-}
-
-function showAddWalletView() {
- $("wallet-mnemonic").value = "";
- $("add-wallet-password").value = "";
- $("add-wallet-password-confirm").value = "";
- $("add-wallet-phrase-warning").classList.add("hidden");
- hideError("add-wallet-error");
- showView("add-wallet");
-}
-
-function showImportKeyView() {
- $("import-private-key").value = "";
- $("import-key-password").value = "";
- $("import-key-password-confirm").value = "";
- hideError("import-key-error");
- showView("import-key");
-}
-
-function backFromWalletAdd() {
- if (!state.hasWallet) {
- showView("welcome");
- } else {
- renderWalletList();
- showView("main");
- }
+ home.render(ctx);
}
async function doRefreshAndRender() {
@@ -243,7 +31,15 @@ async function doRefreshAndRender() {
renderWalletList();
}
-// -- init --
+const ctx = {
+ renderWalletList,
+ doRefreshAndRender,
+ showAddWalletView: () => addWallet.show(),
+ showImportKeyView: () => importKey.show(),
+ showAddressDetail: () => addressDetail.show(),
+ showAddTokenView: () => addToken.show(),
+};
+
async function init() {
if (DEBUG) {
const banner = document.createElement("div");
@@ -256,6 +52,18 @@ async function init() {
await loadState();
+ // Initialize all view event handlers
+ welcome.init(ctx);
+ addWallet.init(ctx);
+ importKey.init(ctx);
+ home.init(ctx);
+ addressDetail.init(ctx);
+ send.init(ctx);
+ receive.init(ctx);
+ addToken.init(ctx);
+ settings.init(ctx);
+ approval.init(ctx);
+
if (!state.hasWallet) {
showView("welcome");
} else {
@@ -263,356 +71,6 @@ async function init() {
showView("main");
doRefreshAndRender();
}
-
- // -- Welcome --
- $("btn-welcome-add").addEventListener("click", showAddWalletView);
-
- // -- Add wallet --
- $("btn-generate-phrase").addEventListener("click", () => {
- $("wallet-mnemonic").value = generateMnemonic();
- $("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 (!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", tokenBalances: [] },
- ],
- });
- });
-
- $("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", tokenBalances: [] },
- ],
- });
- });
-
- $("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");
- const list = $("common-token-list");
- list.innerHTML = TOKENS.slice(0, 25)
- .map(
- (t) =>
- ``,
- )
- .join("");
- list.querySelectorAll(".common-token").forEach((btn) => {
- btn.addEventListener("click", () => {
- $("add-token-address").value = btn.dataset.address;
- });
- });
- 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;
- }
- 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(state.rpcUrl);
- const resolved = await provider.resolveName(to);
- if (!resolved) {
- showError("send-status", "Could not resolve " + to);
- return;
- }
- resolvedTo = resolved;
- } 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 = getSignerForAddress(
- wallet,
- state.selectedAddress,
- decryptedSecret,
- );
- const provider = getProvider(state.rpcUrl);
- 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;
- invalidateBalanceCache();
- doRefreshAndRender();
- } 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;
- }
- // Check if already tracked
- const already = state.trackedTokens.find(
- (t) => t.address.toLowerCase() === contractAddr.toLowerCase(),
- );
- if (already) {
- showError(
- "add-token-error",
- already.symbol + " is already being tracked.",
- );
- return;
- }
- hideError("add-token-error");
- const infoEl = $("add-token-info");
- infoEl.textContent = "Looking up token...";
- infoEl.classList.remove("hidden");
- try {
- const info = await lookupTokenInfo(contractAddr, state.rpcUrl);
- state.trackedTokens.push({
- address: contractAddr,
- symbol: info.symbol,
- decimals: info.decimals,
- name: info.name,
- });
- await saveState();
- invalidateBalanceCache();
- await refreshBalances(
- state.wallets,
- state.trackedTokens,
- state.rpcUrl,
- );
- await saveState();
- showAddressDetail();
- } catch (e) {
- showError(
- "add-token-error",
- "Could not read token contract. Check the address.",
- );
- infoEl.classList.add("hidden");
- }
- });
-
- $("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", () => {
- renderWalletList();
- showView("main");
- });
-
- $("btn-reject").addEventListener("click", () => {
- renderWalletList();
- showView("main");
- });
}
document.addEventListener("DOMContentLoaded", init);
diff --git a/src/popup/views/addToken.js b/src/popup/views/addToken.js
new file mode 100644
index 0000000..18f6f77
--- /dev/null
+++ b/src/popup/views/addToken.js
@@ -0,0 +1,82 @@
+const { $, showError, hideError, showView } = require("./helpers");
+const { TOKENS } = require("../../shared/tokens");
+const { state, saveState } = require("../../shared/state");
+const {
+ lookupTokenInfo,
+ invalidateBalanceCache,
+ refreshBalances,
+} = require("../../shared/balances");
+
+function show() {
+ $("add-token-address").value = "";
+ $("add-token-info").classList.add("hidden");
+ hideError("add-token-error");
+ const list = $("common-token-list");
+ list.innerHTML = TOKENS.slice(0, 25)
+ .map(
+ (t) =>
+ ``,
+ )
+ .join("");
+ list.querySelectorAll(".common-token").forEach((btn) => {
+ btn.addEventListener("click", () => {
+ $("add-token-address").value = btn.dataset.address;
+ });
+ });
+ showView("add-token");
+}
+
+function init(ctx) {
+ $("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;
+ }
+ const already = state.trackedTokens.find(
+ (t) => t.address.toLowerCase() === contractAddr.toLowerCase(),
+ );
+ if (already) {
+ showError(
+ "add-token-error",
+ already.symbol + " is already being tracked.",
+ );
+ return;
+ }
+ hideError("add-token-error");
+ const infoEl = $("add-token-info");
+ infoEl.textContent = "Looking up token...";
+ infoEl.classList.remove("hidden");
+ try {
+ const info = await lookupTokenInfo(contractAddr, state.rpcUrl);
+ state.trackedTokens.push({
+ address: contractAddr,
+ symbol: info.symbol,
+ decimals: info.decimals,
+ name: info.name,
+ });
+ await saveState();
+ invalidateBalanceCache();
+ await refreshBalances(
+ state.wallets,
+ state.trackedTokens,
+ state.rpcUrl,
+ );
+ await saveState();
+ ctx.showAddressDetail();
+ } catch (e) {
+ showError(
+ "add-token-error",
+ "Could not read token contract. Check the address.",
+ );
+ infoEl.classList.add("hidden");
+ }
+ });
+
+ $("btn-add-token-back").addEventListener("click", ctx.showAddressDetail);
+}
+
+module.exports = { init, show };
diff --git a/src/popup/views/addWallet.js b/src/popup/views/addWallet.js
new file mode 100644
index 0000000..6336a02
--- /dev/null
+++ b/src/popup/views/addWallet.js
@@ -0,0 +1,103 @@
+const { $, showError, hideError, showView } = require("./helpers");
+const {
+ generateMnemonic,
+ hdWalletFromMnemonic,
+ isValidMnemonic,
+} = require("../../shared/wallet");
+const { encryptWithPassword } = require("../../shared/vault");
+const { state, saveState } = require("../../shared/state");
+
+function show() {
+ $("wallet-mnemonic").value = "";
+ $("add-wallet-password").value = "";
+ $("add-wallet-password-confirm").value = "";
+ $("add-wallet-phrase-warning").classList.add("hidden");
+ hideError("add-wallet-error");
+ showView("add-wallet");
+}
+
+function init(ctx) {
+ $("btn-generate-phrase").addEventListener("click", () => {
+ $("wallet-mnemonic").value = generateMnemonic();
+ $("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 (!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;
+ state.wallets.push({
+ type: "hd",
+ name: "Wallet " + walletNum,
+ xpub: xpub,
+ encryptedSecret: encrypted,
+ nextIndex: 1,
+ addresses: [
+ { address: firstAddress, balance: "0.0000", tokenBalances: [] },
+ ],
+ });
+ state.hasWallet = true;
+ await saveState();
+ ctx.renderWalletList();
+ showView("main");
+ });
+
+ $("btn-add-wallet-back").addEventListener("click", () => {
+ if (!state.hasWallet) {
+ showView("welcome");
+ } else {
+ ctx.renderWalletList();
+ showView("main");
+ }
+ });
+
+ $("btn-add-wallet-import-key").addEventListener(
+ "click",
+ ctx.showImportKeyView,
+ );
+}
+
+module.exports = { init, show };
diff --git a/src/popup/views/addressDetail.js b/src/popup/views/addressDetail.js
new file mode 100644
index 0000000..048bba3
--- /dev/null
+++ b/src/popup/views/addressDetail.js
@@ -0,0 +1,99 @@
+const { $, showView } = require("./helpers");
+const { state, currentAddress } = require("../../shared/state");
+const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
+const QRCode = require("qrcode");
+
+function show() {
+ 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;
+ $("address-usd-value").textContent = formatUsd(getAddressValueUsd(addr));
+ const ensEl = $("address-ens");
+ if (addr.ensName) {
+ ensEl.textContent = addr.ensName;
+ ensEl.classList.remove("hidden");
+ } else {
+ ensEl.classList.add("hidden");
+ }
+ renderTokenList(addr);
+ renderSendTokenSelect(addr);
+ showView("address");
+}
+
+function renderTokenList(addr) {
+ const list = $("token-list");
+ const balances = addr.tokenBalances || [];
+ if (balances.length === 0 && state.trackedTokens.length === 0) {
+ list.innerHTML =
+ 'No tokens added yet. Use "+ Add" to track a token.
';
+ return;
+ }
+ list.innerHTML = balances
+ .map(
+ (t) =>
+ `` +
+ `${t.symbol}` +
+ `${t.balance || "0"}` +
+ `
`,
+ )
+ .join("");
+}
+
+function renderSendTokenSelect(addr) {
+ const sel = $("send-token");
+ sel.innerHTML = '';
+ for (const t of addr.tokenBalances || []) {
+ const opt = document.createElement("option");
+ opt.value = t.address;
+ opt.textContent = t.symbol;
+ sel.appendChild(opt);
+ }
+}
+
+function init(ctx) {
+ $("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", () => {
+ ctx.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", ctx.showAddTokenView);
+}
+
+module.exports = { init, show };
diff --git a/src/popup/views/approval.js b/src/popup/views/approval.js
new file mode 100644
index 0000000..fde0269
--- /dev/null
+++ b/src/popup/views/approval.js
@@ -0,0 +1,15 @@
+const { $, showView } = require("./helpers");
+
+function init(ctx) {
+ $("btn-approve").addEventListener("click", () => {
+ ctx.renderWalletList();
+ showView("main");
+ });
+
+ $("btn-reject").addEventListener("click", () => {
+ ctx.renderWalletList();
+ showView("main");
+ });
+}
+
+module.exports = { init };
diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js
new file mode 100644
index 0000000..dd4ac02
--- /dev/null
+++ b/src/popup/views/helpers.js
@@ -0,0 +1,47 @@
+// Shared DOM helpers used by all views.
+
+const { DEBUG } = require("../../shared/wallet");
+
+const VIEWS = [
+ "welcome",
+ "add-wallet",
+ "import-key",
+ "main",
+ "address",
+ "send",
+ "receive",
+ "add-token",
+ "settings",
+ "approve",
+];
+
+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 showView(name) {
+ for (const v of VIEWS) {
+ const el = document.getElementById(`view-${v}`);
+ if (el) {
+ el.classList.toggle("hidden", v !== name);
+ }
+ }
+ if (DEBUG) {
+ const banner = document.getElementById("debug-banner");
+ if (banner) {
+ banner.textContent = "DEBUG / INSECURE (" + name + ")";
+ }
+ }
+}
+
+module.exports = { $, showError, hideError, showView };
diff --git a/src/popup/views/home.js b/src/popup/views/home.js
new file mode 100644
index 0000000..ed59985
--- /dev/null
+++ b/src/popup/views/home.js
@@ -0,0 +1,88 @@
+const { $, showView } = require("./helpers");
+const { state, saveState } = require("../../shared/state");
+const { deriveAddressFromXpub } = require("../../shared/wallet");
+const {
+ formatUsd,
+ getAddressValueUsd,
+ getTotalValueUsd,
+} = require("../../shared/prices");
+
+function renderTotalValue() {
+ const el = $("total-value");
+ if (!el) return;
+ el.textContent = formatUsd(getTotalValueUsd(state.wallets));
+}
+
+function render(ctx) {
+ const container = $("wallet-list");
+ if (state.wallets.length === 0) {
+ container.innerHTML =
+ 'No wallets yet. Add one to get started.
';
+ renderTotalValue();
+ 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`;
+ html += `${formatUsd(getAddressValueUsd(addr))}`;
+ 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);
+ ctx.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];
+ wallet.addresses.push({
+ address: deriveAddressFromXpub(wallet.xpub, wallet.nextIndex),
+ balance: "0.0000",
+ tokenBalances: [],
+ });
+ wallet.nextIndex++;
+ await saveState();
+ render(ctx);
+ });
+ });
+
+ renderTotalValue();
+}
+
+function init(ctx) {
+ $("btn-settings").addEventListener("click", () => {
+ $("settings-rpc").value = state.rpcUrl;
+ showView("settings");
+ });
+
+ $("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
+}
+
+module.exports = { init, render };
diff --git a/src/popup/views/importKey.js b/src/popup/views/importKey.js
new file mode 100644
index 0000000..0794e81
--- /dev/null
+++ b/src/popup/views/importKey.js
@@ -0,0 +1,72 @@
+const { $, showError, hideError, showView } = require("./helpers");
+const { addressFromPrivateKey } = require("../../shared/wallet");
+const { encryptWithPassword } = require("../../shared/vault");
+const { state, saveState } = require("../../shared/state");
+
+function show() {
+ $("import-private-key").value = "";
+ $("import-key-password").value = "";
+ $("import-key-password-confirm").value = "";
+ hideError("import-key-error");
+ showView("import-key");
+}
+
+function init(ctx) {
+ $("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;
+ state.wallets.push({
+ type: "key",
+ name: "Wallet " + walletNum,
+ encryptedSecret: encrypted,
+ addresses: [
+ { address: addr, balance: "0.0000", tokenBalances: [] },
+ ],
+ });
+ state.hasWallet = true;
+ await saveState();
+ ctx.renderWalletList();
+ showView("main");
+ });
+
+ $("btn-import-key-back").addEventListener("click", () => {
+ if (!state.hasWallet) {
+ showView("welcome");
+ } else {
+ ctx.renderWalletList();
+ showView("main");
+ }
+ });
+}
+
+module.exports = { init, show };
diff --git a/src/popup/views/receive.js b/src/popup/views/receive.js
new file mode 100644
index 0000000..3bfe573
--- /dev/null
+++ b/src/popup/views/receive.js
@@ -0,0 +1,12 @@
+const { $ } = require("./helpers");
+
+function init(ctx) {
+ $("btn-receive-copy").addEventListener("click", () => {
+ const addr = $("receive-address").textContent;
+ if (addr) navigator.clipboard.writeText(addr);
+ });
+
+ $("btn-receive-back").addEventListener("click", ctx.showAddressDetail);
+}
+
+module.exports = { init };
diff --git a/src/popup/views/send.js b/src/popup/views/send.js
new file mode 100644
index 0000000..c24c12d
--- /dev/null
+++ b/src/popup/views/send.js
@@ -0,0 +1,93 @@
+const { parseEther } = require("ethers");
+const { $, showError } = require("./helpers");
+const { state } = require("../../shared/state");
+const { getSignerForAddress } = require("../../shared/wallet");
+const { decryptWithPassword } = require("../../shared/vault");
+const {
+ getProvider,
+ invalidateBalanceCache,
+} = require("../../shared/balances");
+
+function init(ctx) {
+ $("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;
+ }
+ 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(state.rpcUrl);
+ const resolved = await provider.resolveName(to);
+ if (!resolved) {
+ showError("send-status", "Could not resolve " + to);
+ return;
+ }
+ resolvedTo = resolved;
+ } 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 = getSignerForAddress(
+ wallet,
+ state.selectedAddress,
+ decryptedSecret,
+ );
+ const provider = getProvider(state.rpcUrl);
+ 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;
+ invalidateBalanceCache();
+ ctx.doRefreshAndRender();
+ } catch (e) {
+ statusEl.textContent = "Failed: " + (e.shortMessage || e.message);
+ }
+ });
+
+ $("btn-send-back").addEventListener("click", ctx.showAddressDetail);
+}
+
+module.exports = { init };
diff --git a/src/popup/views/settings.js b/src/popup/views/settings.js
new file mode 100644
index 0000000..a32baa6
--- /dev/null
+++ b/src/popup/views/settings.js
@@ -0,0 +1,16 @@
+const { $, showView } = require("./helpers");
+const { state, saveState } = require("../../shared/state");
+
+function init(ctx) {
+ $("btn-save-rpc").addEventListener("click", async () => {
+ state.rpcUrl = $("settings-rpc").value.trim();
+ await saveState();
+ });
+
+ $("btn-settings-back").addEventListener("click", () => {
+ ctx.renderWalletList();
+ showView("main");
+ });
+}
+
+module.exports = { init };
diff --git a/src/popup/views/welcome.js b/src/popup/views/welcome.js
new file mode 100644
index 0000000..46d27f3
--- /dev/null
+++ b/src/popup/views/welcome.js
@@ -0,0 +1,7 @@
+const { $ } = require("./helpers");
+
+function init(ctx) {
+ $("btn-welcome-add").addEventListener("click", ctx.showAddWalletView);
+}
+
+module.exports = { init };