// Aegis wallet panel. All state comes from activate() via window.silentmode.
// This file renders the multi-wallet picker, per-chain views, and collects
// input; it never touches keys or the vault.
const $ = (id) => document.getElementById(id);
const S = window.silentmode;
let state = null; // full state (all wallets + selected)
let tab = "receive";
let unit = null; // "big" | "small" — chain-dependent
let sendMax = false;
let planTimer = null;
let lastPlan = null;
let settingsFilled = false;
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
const hostOf = (url) => { try { return new URL(url).host || url; } catch { return url; } };
const openUrl = (url) => S.invoke("openUrl", { url }).catch(() => {});
const cleanErr = (e) => String(e?.message || e).replace(/^Error invoking remote method '[^']+': Error: /, "");
// ---- coin logos ------------------------------------------------------------
// Inline SVGs so the header, wallet picker and settings surface all render
// the same mark. Sized by the container via width/height attributes.
function logoSvg(logo, size) {
const s = size || 20;
if (logo === "bch") {
// Green disc with the Bitcoin sign — the mark bchcommunity + Bitcoin.com
// both use in reduced form. Ring wraps a solid disc so the mark reads
// as a coin, not a flat glyph.
return ``;
}
if (logo === "trx") {
// Red disc with the Tron mark: an angular triangle-in-a-T. Simplified
// from the official geometric wordless logo; still reads as "Tron" at
// small sizes because of the tri-line arrangement.
return ``;
}
// Fallback — an unknown-chain shield glyph, so a broken registry entry
// shows up as clearly-wrong rather than blank.
return ``;
}
function testnetTag() { return `TEST`; }
// Selected wallet convenience.
const sel = () => state && state.selected;
const chain = () => sel()?.chain || "";
const decimals = () => sel()?.meta?.decimals || 8;
const ticker = () => sel()?.meta?.ticker || "";
function fmtBig(units, dec) {
const d = dec != null ? dec : decimals();
const s = (Number(units || 0) / Math.pow(10, d)).toFixed(d);
return s.replace(/(\.\d*?[1-9])0+$|\.0+$/, "$1");
}
function fmtSmall(units) { return Number(units || 0).toLocaleString("en-US"); }
function smallUnitLabel() { return chain() === "bch" ? "sat" : "sun"; }
function bigUnitLabel() { return ticker(); }
// ---- tabs ------------------------------------------------------------------
document.querySelectorAll("nav button").forEach((b) => b.addEventListener("click", () => showTab(b.dataset.tab)));
function showTab(name) {
tab = name;
document.querySelectorAll("nav button").forEach((b) => b.classList.toggle("on", b.dataset.tab === name));
document.querySelectorAll("main section").forEach((s) => { s.hidden = s.id !== "tab-" + name; });
if (name === "settings") { settingsFilled = false; fillSettings(); }
if (name === "send") applyUnitPicker();
}
// ---- wallet picker (two-step add) ------------------------------------------
$("pickerBtn").addEventListener("click", () => {
const d = $("drop");
d.hidden = !d.hidden;
if (!d.hidden) fillPicker();
});
document.addEventListener("click", (e) => {
const d = $("drop");
if (d.hidden) return;
if (e.target.closest("#drop") || e.target.closest("#pickerBtn")) return;
d.hidden = true;
});
function fillPicker() {
const d = $("drop");
const wallets = state?.wallets || [];
const coins = state?.coins || [];
const rowsHtml = wallets.map((w) => {
const on = w.id === state.selectedWalletId ? "on" : "";
const bal = w.balance ? fmtBig(w.balance.confirmed || 0, w.decimals) + " " + w.ticker : "—";
const sub = `${esc(w.coinLabel)} · ${esc(w.networkLabel)}${w.testnet ? " " + testnetTag() : ""}`;
return `
${logoSvg(w.logo, 22)}
${esc(w.label)}
${sub}
${esc(bal)}
`;
}).join("");
// "Add wallet" is a two-step flyout: first show coins, then that coin's
// networks. Nothing is created until the user clicks a specific network.
const coinRows = coins.map((c) => {
const testCount = c.networks.filter((n) => n.testnet).length;
const sub = c.networks.length > 1
? c.networks.map((n) => n.label).join(" · ")
: c.networks[0].label;
return `
`;
setTimeout(() => { if (state?.selected?.phase === "ready") { box.hidden = true; } }, 3500);
}
// ---- render ----------------------------------------------------------------
function render() {
if (!state) return;
const s = sel();
const ready = s && s.phase === "ready";
$("tabs").hidden = !ready;
const gate = $("gate");
gate.hidden = ready;
// Header: replace the badge slot with the coin's SVG and show
//
$("hBadge").innerHTML = s?.meta?.logo ? logoSvg(s.meta.logo, 22) : logoSvg(null, 22);
$("hLabel").textContent = s?.label || "Aegis Wallet";
$("hNet").innerHTML = s?.meta
? `${esc(s.meta.coinLabel)} · ${esc(s.meta.networkLabel)}${s.meta.testnet ? " " + testnetTag() : ""}`
: "";
if (!ready) {
const copy = {
locked: ["🔒", "Unlock your password vault to open the wallet.", "Settings › Passwords. Aegis derives its keys from the vault seed, so there is nothing separate to unlock."],
nosetup: ["🗝", "Set up a password vault to create your wallet.", "Settings › Passwords › Set up. Use a recovery phrase there and every wallet in Aegis can be recreated from it on any machine."],
error: ["⚠", "This wallet could not start.", s?.error || ""],
empty: ["🧩", "No wallets yet.", "Open the wallet picker at the top and pick a coin, then a network to create one."],
}[s?.phase || "locked"] || ["…", "Starting…", ""];
gate.innerHTML = `
${copy[0]}
${esc(copy[1])}
${esc(copy[2])}
`;
}
const dot = $("dot");
dot.className = "dot " + (s?.server ? (s?.scanning ? "busy" : "on") : "");
$("netlbl").textContent = s?.server ? hostOf(s.server) + (s?.scanning ? " · syncing" : "") : (ready ? "connecting…" : (s?.network || ""));
if (ready) {
const total = (s.balance?.confirmed || 0) + (s.balance?.unconfirmed || 0);
$("balMain").textContent = fmtBig(total);
$("balTicker").textContent = s.meta.ticker;
if (s.balance?.unconfirmed) $("netlbl").textContent += ` · ${fmtBig(s.balance.unconfirmed)} unconfirmed`;
} else {
$("balMain").textContent = "—"; $("balTicker").textContent = "";
}
if (!ready) return;
const addr = s.address || "";
if ($("addr").textContent !== addr) {
$("addr").textContent = addr;
drawQr(chain() === "bch" ? "bitcoincash:" + addr.replace(/^bitcoincash:|^bchtest:/, "") : "tron:" + addr);
}
$("addrMeta").textContent = s.addressPath ? "· " + s.addressPath : "";
$("nextAddr").hidden = chain() !== "bch";
$("openFaucet").hidden = !s.faucet;
applyUnitPicker();
$("feeField").hidden = chain() !== "bch";
renderHistory();
}
function applyUnitPicker() {
const s = sel(); if (!s) return;
if (!unit) unit = "big";
const big = bigUnitLabel(), small = smallUnitLabel();
$("unitPicker").innerHTML =
`` +
``;
$("unitPicker").querySelectorAll("button").forEach((b) => b.addEventListener("click", () => setUnit(b.dataset.u)));
$("sendTo").placeholder = chain() === "bch"
? (s.network === "chipnet" ? "bchtest:q… or legacy m…" : "bitcoincash:q… or legacy 1…")
: "T… (base58check, 34 chars)";
$("sendAmt").placeholder = unit === "big" ? "0.00" : "0";
}
function setUnit(u) {
if (u === unit) return;
const s = amountUnits();
unit = u;
applyUnitPicker();
if (s) $("sendAmt").value = unit === "big" ? fmtBig(s) : String(s);
}
function amountUnits() {
const raw = $("sendAmt").value.trim().replace(/,/g, "");
if (!raw) return 0;
if (unit === "small") return Math.round(Number(raw));
const d = decimals();
const [w, f = ""] = raw.split(".");
const frac = (f + "0".repeat(d)).slice(0, d);
return Number(w || 0) * Math.pow(10, d) + Number(frac || 0);
}
// ---- history ---------------------------------------------------------------
function renderHistory() {
const s = sel();
const list = s?.history || [];
const el = $("txlist");
if (!list.length) { el.innerHTML = `
${s?.scanning ? "Syncing…" : "No transactions yet."}