popup/index.js reduced to ~75 lines: loads state, builds a shared context object, initializes all views, shows first screen. Each view in popup/views/: helpers.js — $(), showError, hideError, showView welcome.js — welcome screen addWallet.js — unified create/import recovery phrase importKey.js — private key import home.js — wallet list, total value, address derivation addressDetail.js — address view, token list, QR, copy send.js — send form, ENS resolution, tx broadcast receive.js — QR + copy addToken.js — token lookup, common token picker settings.js — RPC endpoint approval.js — dApp approval (stub) Views communicate via a ctx object with shared callbacks (renderWalletList, showAddressDetail, doRefreshAndRender, etc).
This commit is contained in:
@@ -1,237 +1,25 @@
|
|||||||
// AutistMask popup UI — view switching and event wiring only.
|
// AutistMask popup entry point.
|
||||||
// All business logic lives in src/shared/*.
|
// Loads state, initializes views, triggers first render.
|
||||||
|
|
||||||
const { parseEther } = require("ethers");
|
const { DEBUG } = require("../shared/wallet");
|
||||||
const QRCode = require("qrcode");
|
const { state, saveState, loadState } = require("../shared/state");
|
||||||
const { TOKENS } = require("../shared/tokens");
|
const { refreshPrices } = require("../shared/prices");
|
||||||
const {
|
const { refreshBalances } = require("../shared/balances");
|
||||||
state,
|
const { showView } = require("./views/helpers");
|
||||||
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 VIEWS = [
|
const home = require("./views/home");
|
||||||
"welcome",
|
const welcome = require("./views/welcome");
|
||||||
"add-wallet",
|
const addWallet = require("./views/addWallet");
|
||||||
"import-key",
|
const importKey = require("./views/importKey");
|
||||||
"main",
|
const addressDetail = require("./views/addressDetail");
|
||||||
"address",
|
const send = require("./views/send");
|
||||||
"send",
|
const receive = require("./views/receive");
|
||||||
"receive",
|
const addToken = require("./views/addToken");
|
||||||
"add-token",
|
const settings = require("./views/settings");
|
||||||
"settings",
|
const approval = require("./views/approval");
|
||||||
"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 =
|
|
||||||
'<div class="text-muted text-xs py-1">No tokens added yet. Use "+ Add" to track a token.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
list.innerHTML = balances
|
|
||||||
.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 renderSendTokenSelect(addr) {
|
|
||||||
const sel = $("send-token");
|
|
||||||
sel.innerHTML = '<option value="ETH">ETH</option>';
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderWalletList() {
|
function renderWalletList() {
|
||||||
const container = $("wallet-list");
|
home.render(ctx);
|
||||||
if (state.wallets.length === 0) {
|
|
||||||
container.innerHTML =
|
|
||||||
'<p class="text-muted py-2">No wallets yet. Add one to get started.</p>';
|
|
||||||
renderTotalValue();
|
|
||||||
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>`;
|
|
||||||
html += `<span class="text-xs text-muted">${formatUsd(getAddressValueUsd(addr))}</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",
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doRefreshAndRender() {
|
async function doRefreshAndRender() {
|
||||||
@@ -243,7 +31,15 @@ async function doRefreshAndRender() {
|
|||||||
renderWalletList();
|
renderWalletList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- init --
|
const ctx = {
|
||||||
|
renderWalletList,
|
||||||
|
doRefreshAndRender,
|
||||||
|
showAddWalletView: () => addWallet.show(),
|
||||||
|
showImportKeyView: () => importKey.show(),
|
||||||
|
showAddressDetail: () => addressDetail.show(),
|
||||||
|
showAddTokenView: () => addToken.show(),
|
||||||
|
};
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
const banner = document.createElement("div");
|
const banner = document.createElement("div");
|
||||||
@@ -256,6 +52,18 @@ async function init() {
|
|||||||
|
|
||||||
await loadState();
|
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) {
|
if (!state.hasWallet) {
|
||||||
showView("welcome");
|
showView("welcome");
|
||||||
} else {
|
} else {
|
||||||
@@ -263,356 +71,6 @@ async function init() {
|
|||||||
showView("main");
|
showView("main");
|
||||||
doRefreshAndRender();
|
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) =>
|
|
||||||
`<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;
|
|
||||||
}
|
|
||||||
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);
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
|||||||
82
src/popup/views/addToken.js
Normal file
82
src/popup/views/addToken.js
Normal file
@@ -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) =>
|
||||||
|
`<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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
103
src/popup/views/addWallet.js
Normal file
103
src/popup/views/addWallet.js
Normal file
@@ -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 };
|
||||||
99
src/popup/views/addressDetail.js
Normal file
99
src/popup/views/addressDetail.js
Normal file
@@ -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 =
|
||||||
|
'<div class="text-muted text-xs py-1">No tokens added yet. Use "+ Add" to track a token.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = balances
|
||||||
|
.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 renderSendTokenSelect(addr) {
|
||||||
|
const sel = $("send-token");
|
||||||
|
sel.innerHTML = '<option value="ETH">ETH</option>';
|
||||||
|
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 };
|
||||||
15
src/popup/views/approval.js
Normal file
15
src/popup/views/approval.js
Normal file
@@ -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 };
|
||||||
47
src/popup/views/helpers.js
Normal file
47
src/popup/views/helpers.js
Normal file
@@ -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 };
|
||||||
88
src/popup/views/home.js
Normal file
88
src/popup/views/home.js
Normal file
@@ -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 =
|
||||||
|
'<p class="text-muted py-2">No wallets yet. Add one to get started.</p>';
|
||||||
|
renderTotalValue();
|
||||||
|
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>`;
|
||||||
|
html += `<span class="text-xs text-muted">${formatUsd(getAddressValueUsd(addr))}</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);
|
||||||
|
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 };
|
||||||
72
src/popup/views/importKey.js
Normal file
72
src/popup/views/importKey.js
Normal file
@@ -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 };
|
||||||
12
src/popup/views/receive.js
Normal file
12
src/popup/views/receive.js
Normal file
@@ -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 };
|
||||||
93
src/popup/views/send.js
Normal file
93
src/popup/views/send.js
Normal file
@@ -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 };
|
||||||
16
src/popup/views/settings.js
Normal file
16
src/popup/views/settings.js
Normal file
@@ -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 };
|
||||||
7
src/popup/views/welcome.js
Normal file
7
src/popup/views/welcome.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const { $ } = require("./helpers");
|
||||||
|
|
||||||
|
function init(ctx) {
|
||||||
|
$("btn-welcome-add").addEventListener("click", ctx.showAddWalletView);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { init };
|
||||||
Reference in New Issue
Block a user