All checks were successful
check / check (push) Successful in 11s
DEBUG mode must behave identically to normal mode except for the red banner and hardcoded mnemonic. No other DEBUG branches without explicit owner approval. Policy documented in README.
535 lines
16 KiB
JavaScript
535 lines
16 KiB
JavaScript
// AutistMask popup UI — view management and event wiring
|
|
const { Mnemonic } = require("ethers");
|
|
|
|
const DEBUG = true;
|
|
const DEBUG_MNEMONIC =
|
|
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck";
|
|
|
|
const VIEWS = [
|
|
"lock",
|
|
"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 storage =
|
|
typeof browser !== "undefined"
|
|
? browser.storage.local
|
|
: chrome.storage.local;
|
|
|
|
// A wallet is either { type: "hd", name, mnemonic, addresses: [...] }
|
|
// or { type: "key", name, privateKey, addresses: [single] }.
|
|
// Each address is { address, balance, tokens: [...] }.
|
|
const DEFAULT_STATE = {
|
|
hasWallet: false,
|
|
wallets: [],
|
|
rpcUrl: "https://eth.llamarpc.com",
|
|
isFirstSetup: true,
|
|
};
|
|
|
|
// Transient state (not persisted)
|
|
const state = {
|
|
...DEFAULT_STATE,
|
|
locked: true,
|
|
password: null,
|
|
selectedWallet: null,
|
|
selectedAddress: null,
|
|
};
|
|
|
|
async function saveState() {
|
|
const persisted = {
|
|
hasWallet: state.hasWallet,
|
|
wallets: state.wallets,
|
|
rpcUrl: state.rpcUrl,
|
|
isFirstSetup: state.isFirstSetup,
|
|
};
|
|
await storage.set({ autistmask: persisted });
|
|
}
|
|
|
|
async function loadState() {
|
|
const result = await storage.get("autistmask");
|
|
if (result.autistmask) {
|
|
const saved = result.autistmask;
|
|
state.hasWallet = saved.hasWallet;
|
|
state.wallets = saved.wallets || [];
|
|
state.rpcUrl = saved.rpcUrl || DEFAULT_STATE.rpcUrl;
|
|
state.isFirstSetup = saved.isFirstSetup;
|
|
}
|
|
}
|
|
|
|
// -- 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 truncateAddress(addr) {
|
|
if (!addr) return "";
|
|
return addr.slice(0, 6) + "\u2026" + addr.slice(-4);
|
|
}
|
|
|
|
function makeStubAddress() {
|
|
const hex = Array.from({ length: 40 }, () =>
|
|
Math.floor(Math.random() * 16).toString(16),
|
|
).join("");
|
|
return {
|
|
address: "0x" + hex,
|
|
balance: "0.0000",
|
|
tokens: [],
|
|
};
|
|
}
|
|
|
|
function generateMnemonic() {
|
|
if (DEBUG) return DEBUG_MNEMONIC;
|
|
const wallet = Mnemonic.fromEntropy(
|
|
globalThis.crypto.getRandomValues(new Uint8Array(16)),
|
|
);
|
|
return wallet.phrase;
|
|
}
|
|
|
|
// -- render wallet list on main view --
|
|
function renderWalletList() {
|
|
const container = $("wallet-list");
|
|
if (state.wallets.length === 0) {
|
|
container.innerHTML =
|
|
'<p class="text-muted py-2">No wallets yet. Add one to get started.</p>';
|
|
return;
|
|
}
|
|
|
|
let html = "";
|
|
state.wallets.forEach((wallet, wi) => {
|
|
html += `<div class="mb-3">`;
|
|
html += `<div class="flex justify-between items-center border-b border-border pb-1 mb-1">`;
|
|
html += `<span class="font-bold">${wallet.name}</span>`;
|
|
if (wallet.type === "hd") {
|
|
html += `<button class="btn-add-address border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs" data-wallet="${wi}" title="Add another address to this wallet">+</button>`;
|
|
}
|
|
html += `</div>`;
|
|
|
|
wallet.addresses.forEach((addr, ai) => {
|
|
html += `<div class="address-row flex justify-between items-center py-1 border-b border-border-light cursor-pointer hover:bg-hover px-1" data-wallet="${wi}" data-address="${ai}">`;
|
|
html += `<span class="text-xs">${truncateAddress(addr.address)}</span>`;
|
|
html += `<span class="text-xs">${addr.balance} ETH</span>`;
|
|
html += `</div>`;
|
|
});
|
|
|
|
html += `</div>`;
|
|
});
|
|
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);
|
|
// TODO: derive next address from seed via background
|
|
state.wallets[wi].addresses.push(makeStubAddress());
|
|
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;
|
|
$("address-usd-value").textContent = "";
|
|
updateTokenList(addr);
|
|
updateSendTokenSelect(addr);
|
|
showView("address");
|
|
}
|
|
|
|
function updateTokenList(addr) {
|
|
const list = $("token-list");
|
|
if (addr.tokens.length === 0) {
|
|
list.innerHTML =
|
|
'<div class="text-muted text-xs py-1">No tokens added yet. Use "+ Add" to track a token.</div>';
|
|
return;
|
|
}
|
|
list.innerHTML = addr.tokens
|
|
.map(
|
|
(t) =>
|
|
`<div class="py-1 border-b border-border-light flex justify-between">` +
|
|
`<span>${t.symbol}</span>` +
|
|
`<span>${t.balance || "0"}</span>` +
|
|
`</div>`,
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function updateSendTokenSelect(addr) {
|
|
const sel = $("send-token");
|
|
sel.innerHTML = '<option value="ETH">ETH</option>';
|
|
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;
|
|
state.isFirstSetup = false;
|
|
await saveState();
|
|
renderWalletList();
|
|
showView("main");
|
|
}
|
|
|
|
function showAddWalletView() {
|
|
$("wallet-mnemonic").value = "";
|
|
$("add-wallet-phrase-warning").classList.add("hidden");
|
|
hideError("add-wallet-error");
|
|
const needsPw = state.isFirstSetup;
|
|
$("add-wallet-password-section").classList.toggle("hidden", !needsPw);
|
|
$("add-wallet-password-confirm-section").classList.toggle(
|
|
"hidden",
|
|
!needsPw,
|
|
);
|
|
showView("add-wallet");
|
|
}
|
|
|
|
function showImportKeyView() {
|
|
$("import-private-key").value = "";
|
|
hideError("import-key-error");
|
|
const needsPw = state.isFirstSetup;
|
|
$("import-key-password-section").classList.toggle("hidden", !needsPw);
|
|
$("import-key-password-confirm-section").classList.toggle(
|
|
"hidden",
|
|
!needsPw,
|
|
);
|
|
showView("import-key");
|
|
}
|
|
|
|
function validatePasswords(pwId, pw2Id, errorId) {
|
|
if (!state.isFirstSetup) return true;
|
|
const pw = $(pwId).value;
|
|
const pw2 = $(pw2Id).value;
|
|
if (!pw) {
|
|
showError(errorId, "Please choose a password.");
|
|
return false;
|
|
}
|
|
if (pw.length < 8) {
|
|
showError(errorId, "Password must be at least 8 characters.");
|
|
return false;
|
|
}
|
|
if (pw !== pw2) {
|
|
showError(errorId, "Passwords do not match.");
|
|
return false;
|
|
}
|
|
state.password = pw;
|
|
return true;
|
|
}
|
|
|
|
function backFromWalletAdd() {
|
|
if (state.isFirstSetup) {
|
|
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 if (state.locked) {
|
|
showView("lock");
|
|
} else {
|
|
renderWalletList();
|
|
showView("main");
|
|
}
|
|
|
|
// -- Lock screen --
|
|
$("btn-unlock").addEventListener("click", () => {
|
|
const pw = $("unlock-password").value;
|
|
if (!pw) {
|
|
showError("unlock-error", "Please enter your password.");
|
|
return;
|
|
}
|
|
hideError("unlock-error");
|
|
// TODO: send unlock message to background
|
|
state.locked = false;
|
|
renderWalletList();
|
|
showView("main");
|
|
});
|
|
|
|
// -- 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", () => {
|
|
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 (
|
|
!validatePasswords(
|
|
"add-wallet-password",
|
|
"add-wallet-password-confirm",
|
|
"add-wallet-error",
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
hideError("add-wallet-error");
|
|
const walletNum = state.wallets.length + 1;
|
|
addWalletAndGoToMain({
|
|
type: "hd",
|
|
name: "Wallet " + walletNum,
|
|
mnemonic: mnemonic,
|
|
addresses: [makeStubAddress()],
|
|
});
|
|
});
|
|
|
|
$("btn-add-wallet-back").addEventListener("click", backFromWalletAdd);
|
|
$("btn-add-wallet-import-key").addEventListener("click", showImportKeyView);
|
|
|
|
// -- Import private key --
|
|
$("btn-import-key-confirm").addEventListener("click", () => {
|
|
const key = $("import-private-key").value.trim();
|
|
if (!key) {
|
|
showError("import-key-error", "Please enter your private key.");
|
|
return;
|
|
}
|
|
if (
|
|
!validatePasswords(
|
|
"import-key-password",
|
|
"import-key-password-confirm",
|
|
"import-key-error",
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
hideError("import-key-error");
|
|
const walletNum = state.wallets.length + 1;
|
|
addWalletAndGoToMain({
|
|
type: "key",
|
|
name: "Wallet " + walletNum,
|
|
privateKey: key,
|
|
addresses: [makeStubAddress()],
|
|
});
|
|
});
|
|
|
|
$("btn-import-key-back").addEventListener("click", backFromWalletAdd);
|
|
|
|
// -- Main view --
|
|
$("btn-lock").addEventListener("click", () => {
|
|
state.locked = true;
|
|
$("unlock-password").value = "";
|
|
showView("lock");
|
|
});
|
|
|
|
$("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-fee-estimate").classList.add("hidden");
|
|
$("send-status").classList.add("hidden");
|
|
showView("send");
|
|
});
|
|
|
|
$("btn-receive").addEventListener("click", () => {
|
|
const addr = currentAddress();
|
|
$("receive-address").textContent = addr ? addr.address : "";
|
|
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", () => {
|
|
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;
|
|
}
|
|
// TODO: construct and send transaction via background
|
|
const el = $("send-status");
|
|
el.textContent = "Sent! (stub)";
|
|
el.classList.remove("hidden");
|
|
});
|
|
|
|
$("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 background
|
|
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);
|