All checks were successful
check / check (push) Successful in 14s
Move "AutistMask by @sneak" to a global title bar that appears on every screen. Per-view headings demoted to h2 sub-headings. Settings button moved to bottom of main view alongside Add wallet. In DEBUG mode, the red banner now shows the current screen name in parentheses (e.g. "DEBUG / INSECURE (main)").
733 lines
23 KiB
JavaScript
733 lines
23 KiB
JavaScript
// AutistMask popup UI — view management and event wiring
|
|
const {
|
|
Mnemonic,
|
|
HDNodeWallet,
|
|
Wallet,
|
|
JsonRpcProvider,
|
|
formatEther,
|
|
parseEther,
|
|
} = require("ethers");
|
|
const { TOKENS, 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);
|
|
}
|
|
}
|
|
if (DEBUG) {
|
|
const banner = document.getElementById("debug-banner");
|
|
if (banner) {
|
|
banner.textContent = "DEBUG / INSECURE (" + 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 =
|
|
'<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 py-1 border-b border-border-light cursor-pointer hover:bg-hover px-1" data-wallet="${wi}" data-address="${ai}">`;
|
|
if (addr.ensName) {
|
|
html += `<div class="text-xs font-bold">${addr.ensName}</div>`;
|
|
}
|
|
html += `<div class="text-xs break-all">${addr.address}</div>`;
|
|
html += `<div class="flex justify-between items-center">`;
|
|
html += `<span class="text-xs">${addr.balance} ETH</span>`;
|
|
const ethUsd = prices.ETH
|
|
? parseFloat(addr.balance) * prices.ETH
|
|
: null;
|
|
if (ethUsd !== null) {
|
|
html += `<span class="text-xs text-muted">${formatUsd(ethUsd)}</span>`;
|
|
}
|
|
html += `</div>`;
|
|
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);
|
|
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 =
|
|
'<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;
|
|
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.id = "debug-banner";
|
|
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");
|
|
const list = $("common-token-list");
|
|
list.innerHTML = TOKENS.slice(0, 25)
|
|
.map(
|
|
(t) =>
|
|
`<button class="common-token border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs" data-address="${t.address}" data-symbol="${t.symbol}" data-decimals="${t.decimals}">${t.symbol}</button>`,
|
|
)
|
|
.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;
|
|
}
|
|
// 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);
|