// 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; // Selected asset for the Send tab. `null` = native coin. Otherwise a // { mint, symbol, decimals } picked from the SOL wallet's SPL token list. let sendAsset = null; 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") { // All coin marks below are the canonical SVGs from // github.com/spothq/cryptocurrency-icons — the permissive-licensed // set most wallets, exchanges, and explorers standardised on, so // Aegis's logos match what users see everywhere else. Inline so // panel load doesn't fetch anything. return ``; } if (logo === "trx") { return ``; } if (logo === "sc") { return ``; } if (logo === "dgb") { return ``; } if (logo === "btc") { return ``; } if (logo === "eth") { return ``; } if (logo === "sol") { return ``; } if (logo === "aegis") { // Athena's aspis — hexagonal shield with a boss at center + four // spoke marks. Same silhouette as the aegis.x hero SVG so the wallet // and the marketing page read as one identity. return ` `; } // Fallback = Aegis shield (rather than a "?"), so an unrecognised // registry entry still looks intentional. return logoSvg("aegis", s); } 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 || ""; // Numbers past ~9e15 lose precision as JS `Number`, and Sia amounts live at // 10^24-scale routinely. Use BigInt for anything that arrives as a string. function fmtBig(units, dec) { const d = dec != null ? dec : decimals(); if (typeof units === "string" && /^-?\d+$/.test(units)) { const neg = units.startsWith("-"); const raw = neg ? units.slice(1) : units; const bi = BigInt(raw || "0"); const base = 10n ** BigInt(d); const whole = (bi / base).toString(); let frac = (bi % base).toString().padStart(d, "0").replace(/0+$/, ""); // Show 8-digit precision at most for very small units; keep 2 dp minimum. const cap = Math.min(d, 8); if (frac.length > cap) frac = frac.slice(0, cap); if (!frac) frac = ""; return (neg ? "-" : "") + whole + (frac ? "." + frac : ""); } const s = (Number(units || 0) / Math.pow(10, d)).toFixed(d); return s.replace(/(\.\d*?[1-9])0+$|\.0+$/, "$1"); } function fmtSmall(units) { if (typeof units === "string" && /^-?\d+$/.test(units)) return units.replace(/\B(?=(\d{3})+(?!\d))/g, ","); return Number(units || 0).toLocaleString("en-US"); } function smallUnitLabel() { const c = chain(); if (c === "bch" || c === "dgb" || c === "btc") return "sat"; if (c === "trx") return "sun"; if (c === "sc") return "H"; if (c === "eth") return "wei"; if (c === "sol") return "lamports"; return "u"; } // Some chains (SOL) suffix explorer URLs to tell devnet from mainnet. function explorerHref(base, id) { const s = sel(); return base + id + (s?.explorerSuffix || ""); } function bigUnitLabel() { return ticker(); } // ---- fiat helpers ---------------------------------------------------------- // Prices live in state.prices.{enabled, prices, fetchedAt}. When disabled // or missing, fiat helpers return null and the caller renders nothing. function priceFor(chain) { if (!state?.prices?.enabled) return null; return state.prices.prices?.[chain] ?? null; } // Convert native units (sats/lamports/wei/…) to a USD number, BigInt-safe // for wide-decimals coins (SC=24, ETH=18) that overflow Number. function usdOf(chain, units, decimals) { const price = priceFor(chain); if (price == null || !units) return null; const d = Number(decimals) || 0; if (typeof units === "string" && /^-?\d+$/.test(units)) { // BigInt-safe: divide the units by 10^d first via BigInt, then use // the fractional remainder as a Number multiplier for the last dp. const neg = units.startsWith("-"); const abs = neg ? units.slice(1) : units; const base = 10n ** BigInt(d); const bi = BigInt(abs); const whole = Number(bi / base); const frac = Number(bi % base) / Number(base); return (neg ? -1 : 1) * (whole + frac) * price; } const n = Number(units) / Math.pow(10, d); return n * price; } // Format a USD value for the UI. < $0.01 → "< $0.01", < $10 → 2dp, else // grouped whole dollars with ".xx" fine detail. Skeleton "≈ $—" when the // feed is enabled but hasn't returned yet. function fmtFiat(usd) { if (usd == null) return null; if (usd === 0) return "$0.00"; if (Math.abs(usd) < 0.01) return "< $0.01"; if (Math.abs(usd) < 10) return "$" + usd.toFixed(2); const int = Math.floor(usd); const frac = Math.abs(usd - int).toFixed(2).slice(1); return "$" + int.toLocaleString("en-US") + frac; } function fiatSkeleton() { return state?.prices?.enabled ? "≈ $—" : null; } // ---- 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(); // Settings tab is always usable (fiat prices are a global setting); every // other tab is gated by the wallet-ready state. Re-run gate visibility so // switching TO or AWAY FROM Settings while locked does the right thing. const s = sel(); const ready = s && s.phase === "ready"; const onSettings = name === "settings"; $("tabs").hidden = !(ready || onSettings); $("gate").hidden = ready || onSettings; } // ---- wallet picker (two-step add) ------------------------------------------ $("pickerBtn").addEventListener("click", (e) => { // The "+" chip inside the picker header opens the dropdown with the // Add-wallet section pre-expanded — same UX as the empty-state gate // button but always available. const isAddChip = e.target && (e.target.id === "hAddWallet" || e.target.closest("#hAddWallet")); const d = $("drop"); if (isAddChip) { d.hidden = false; fillPicker(); setTimeout(() => { const first = d.querySelector(".coinrow"); if (first) first.click(); }, 0); e.stopPropagation(); return; } 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 totalUnits = w.balance ? (typeof w.balance.confirmed === "string" ? (BigInt(w.balance.confirmed || "0") + BigInt(w.balance.unconfirmed || "0")).toString() : (w.balance.confirmed || 0) + (w.balance.unconfirmed || 0)) : 0; const bal = w.balance ? fmtBig(totalUnits, w.decimals) + " " + w.ticker : "—"; // Fiat sits on a second line under the native balance, right-aligned. // Testnet coins mirror mainnet prices, so we don't dim them. const usd = usdOf(w.chain, totalUnits, w.decimals); const fiat = fmtFiat(usd); const fiatLine = fiat ? `
${esc(fiat)}
` : ""; const sub = `${esc(w.coinLabel)} · ${esc(w.networkLabel)}${w.testnet ? " " + testnetTag() : ""}`; return `
${logoSvg(w.logo, 22)}
${esc(w.label)}
${sub}
${esc(bal)}
${fiatLine}
`; }).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 `
${logoSvg(c.logo, 22)}
${esc(c.label)}
${esc(sub)}
`; }).join(""); d.innerHTML = rowsHtml + `
+ Add wallet
${coinRows}` + `
↓ Import existing (BCH)
${logoSvg("bch", 22)}
Import a BCH wallet
Paste a BIP39 mnemonic + derivation path, or a WIF
`; d.querySelectorAll("[data-select]").forEach((r) => r.addEventListener("click", async () => { d.hidden = true; try { state = await S.invoke("selectWallet", { id: r.dataset.select }); settingsFilled = false; render(); } catch (e) { showErr(cleanErr(e)); } })); d.querySelectorAll(".coinrow").forEach((r) => r.addEventListener("click", () => { // Collapse other coins' network groups; toggle this one. d.querySelectorAll(".netgroup").forEach((g) => { if (g.id !== "netgroup-" + r.dataset.coin) g.hidden = true; }); d.querySelectorAll(".coinrow .caret").forEach((c) => { c.textContent = "▸"; }); const group = d.querySelector("#netgroup-" + r.dataset.coin); group.hidden = !group.hidden; r.querySelector(".caret").textContent = group.hidden ? "▸" : "▾"; })); d.querySelectorAll("[data-add]").forEach((r) => r.addEventListener("click", async () => { const [c, n] = r.dataset.add.split(":"); d.hidden = true; try { state = await S.invoke("addWallet", { chain: c, network: n }); settingsFilled = false; render(); } catch (e) { showErr(cleanErr(e)); } })); const impBtn = $("picker-import-bch"); if (impBtn) impBtn.addEventListener("click", () => { d.hidden = true; openImportModal("bch"); }); } // Import modal — M.1 UX. Paste mnemonic + path OR WIF, choose network + label // + category. Backend derives cashaddr and stores signer material in // wallet-imports.enc (design §3.2). Modal is a plain overlay div injected // into the panel body so it works over any tab. function openImportModal(chain) { const overlay = document.createElement("div"); overlay.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.55);display:flex;align-items:flex-start;justify-content:center;z-index:9999;padding-top:24px"; overlay.innerHTML = `
${logoSvg("bch", 22)}
Import a BCH wallet
Key material stays in Theseus's vault (wallet-imports.enc). Aegis derives only the address and shows the balance — spending support ships next.
Network
Source
Mnemonic (12/24 words)
Derivation path
Default: m/44'/1'/0'/0/0 for chipnet, m/44'/145'/0'/0/0 for mainnet.
Label
Category
`; document.body.appendChild(overlay); const close = () => { try { overlay.remove(); } catch {} }; overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); }); overlay.querySelector("#imClose").addEventListener("click", close); overlay.querySelector("#imCancel").addEventListener("click", close); // Toggle mnemonic vs WIF fields. overlay.querySelectorAll('input[name="imKind"]').forEach((r) => r.addEventListener("change", () => { const kind = overlay.querySelector('input[name="imKind"]:checked').value; overlay.querySelector("#imMnemonicField").hidden = kind !== "mnemonic"; overlay.querySelector("#imWifField").hidden = kind !== "wif"; })); // Update default path on network switch. const setDefaultPath = () => { const net = overlay.querySelector('input[name="imNet"]:checked').value; const path = overlay.querySelector("#imPath"); if (!path.value.trim()) path.value = net === "mainnet" ? "m/44'/145'/0'/0/0" : "m/44'/1'/0'/0/0"; }; overlay.querySelectorAll('input[name="imNet"]').forEach((r) => r.addEventListener("change", setDefaultPath)); setDefaultPath(); overlay.querySelector("#imGo").addEventListener("click", async () => { const msg = overlay.querySelector("#imMsg"); msg.hidden = true; const network = overlay.querySelector('input[name="imNet"]:checked').value; const kind = overlay.querySelector('input[name="imKind"]:checked').value; const label = overlay.querySelector("#imLabel").value.trim(); const category = overlay.querySelector("#imCategory").value; if (!label) { msg.textContent = "Label required."; msg.hidden = false; return; } const payload = { chain: "bch", network, label, category }; if (kind === "mnemonic") { payload.mnemonic = overlay.querySelector("#imMnemonic").value.trim(); payload.path = overlay.querySelector("#imPath").value.trim(); if (!payload.mnemonic) { msg.textContent = "Mnemonic required."; msg.hidden = false; return; } } else { payload.wif = overlay.querySelector("#imWif").value.trim(); if (!payload.wif) { msg.textContent = "WIF required."; msg.hidden = false; return; } } try { state = await S.invoke("importWallet", payload); close(); render(); } catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; } }); } function showErr(text) { const box = $("gate"); box.hidden = false; box.innerHTML = `
${esc(text)}
`; 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"; // Settings is the only always-usable tab (fiat prices, connected sites // — nothing needs a live wallet). Every other tab is gated. const onSettings = tab === "settings"; $("tabs").hidden = !(ready || onSettings); const gate = $("gate"); gate.hidden = ready || onSettings; if (onSettings) fillSettings(); // 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.", "Aegis derives its keys from the vault seed, so there is nothing separate to unlock — the vault is your wallet."], nosetup: ["🗝", "Set up a password vault to create your wallet.", "Pick a master password on the next screen. Every Aegis wallet is derived from it — the same master password on another machine recreates the same addresses."], error: ["⚠", "This wallet could not start.", s?.error || ""], empty: ["🛡", "No wallets yet.", "Aegis derives every wallet from your Theseus password vault — there's no separate seed to import. Pick a coin below to create your first one."], }[s?.phase || "locked"] || ["…", "Starting…", ""]; const phase = s?.phase; let form = ""; if (phase === "nosetup") { form = `
Optional. Paste a mnemonic to derive your vault from an existing seed (Ariadne mobile, another Theseus profile, etc.). Leave empty for a fresh independent seed.
`; } else if (phase === "locked") { form = `
`; } else if (phase === "empty") { form = `
`; } gate.innerHTML = `
${copy[0]}
${esc(copy[1])}
${esc(copy[2])}
${form}`; if (phase === "empty") { const btn = $("gateAddWallet"); if (btn) btn.addEventListener("click", () => { const d = $("drop"); d.hidden = false; fillPicker(); setTimeout(() => { const first = d.querySelector(".coinrow"); if (first) first.click(); }, 0); }); } if (phase === "locked") { const doUnlock = async () => { const pw = $("gateUnlockPw").value; const msg = $("gateUnlockMsg"); msg.hidden = true; if (!pw) return; try { state = await S.invoke("vaultUnlock", { masterPassword: pw }); render(); } catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; } }; $("gateUnlockBtn").addEventListener("click", doUnlock); $("gateUnlockPw").addEventListener("keydown", (e) => { if (e.key === "Enter") doUnlock(); }); try { $("gateUnlockPw").focus(); } catch {} } if (phase === "nosetup") { const doSetup = async () => { const pw = $("gateSetupPw").value; const pw2 = $("gateSetupPw2").value; const mnemonic = $("gateSetupMnemonic").value.trim(); const msg = $("gateSetupMsg"); msg.hidden = true; if (!pw || pw.length < 4) { msg.textContent = "Master password must be 4+ characters."; msg.hidden = false; return; } if (pw !== pw2) { msg.textContent = "Master passwords don't match."; msg.hidden = false; return; } const seedSource = mnemonic ? { kind: "mnemonic", mnemonic } : { kind: "random" }; try { state = await S.invoke("vaultSetup", { masterPassword: pw, seedSource }); render(); } catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; } }; $("gateSetupBtn").addEventListener("click", doSetup); } } 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) { // Sia-specific gate: adapter is up, keys are derived, but no walletd URL // means no balance / history until the user configures one in Settings. if (chain() === "sc" && s.needsWalletdUrl) { $("balMain").textContent = "—"; $("balTicker").textContent = s.meta.ticker; $("netlbl").textContent = "point Aegis at a walletd node in Settings"; $("tabs").hidden = true; gate.hidden = false; gate.innerHTML = `
🗝
Point Aegis at a walletd node
Settings › Sia › walletd URL. Any public or self-hosted go.sia.tech/walletd in "full" index mode works.
`; return; } const total = balanceSum(s.balance); $("balMain").textContent = fmtBig(total); $("balTicker").textContent = s.meta.ticker; const uc = s.balance?.unconfirmed; if (uc && uc !== "0" && uc !== 0) $("netlbl").textContent += ` · ${fmtBig(uc)} unconfirmed`; // Fiat under the native amount (opt-in, might be null while loading). const usd = usdOf(chain(), total, decimals()); const fiat = fmtFiat(usd) || fiatSkeleton(); $("balFiat").textContent = fiat || ""; $("balFiat").hidden = !fiat; } else { $("balMain").textContent = "—"; $("balTicker").textContent = ""; $("balFiat").hidden = true; } renderPortfolio(); if (!ready) return; const addr = s.address || ""; if ($("addr").textContent !== addr) { $("addr").textContent = addr; drawQr(qrPayload(chain(), addr, sel()?.network)); } $("addrMeta").textContent = s.addressPath ? "· " + s.addressPath : ""; $("nextAddr").hidden = chain() !== "bch"; $("openFaucet").hidden = !s.faucet; // Render SPL tokens list (SOL wallets only). Sending a token clicks // through to the Send tab with that asset pre-picked. renderTokens(); applyUnitPicker(); $("feeField").hidden = chain() !== "bch"; renderHistory(); } // Sum every wallet's confirmed+unconfirmed × price and show "≈ $X across N // wallets" under the header. Only rendered when prices are on AND there are // two or more wallets (a single wallet's fiat already sits in #balFiat). function renderPortfolio() { const el = $("portfolio"); const wallets = state?.wallets || []; if (!state?.prices?.enabled || wallets.length < 2) { el.hidden = true; return; } let total = 0, priced = 0; for (const w of wallets) { const b = w.balance; if (!b) continue; const units = (typeof b.confirmed === "string") ? (BigInt(b.confirmed || "0") + BigInt(b.unconfirmed || "0")).toString() : (b.confirmed || 0) + (b.unconfirmed || 0); const usd = usdOf(w.chain, units, w.decimals); if (usd != null) { total += usd; priced++; } } if (!priced) { el.hidden = false; el.innerHTML = `Portfolio: ${esc(fiatSkeleton() || "—")}`; return; } const noun = wallets.length === 1 ? "wallet" : "wallets"; el.hidden = false; el.innerHTML = `Portfolio: ${esc(fmtFiat(total))} across ${wallets.length} ${noun}`; } function renderTokens() { const s = sel(); const tokens = (chain() === "sol" && s?.tokens) || []; const card = $("tokensCard"); card.hidden = tokens.length === 0; if (!tokens.length) return; const el = $("tokensList"); el.innerHTML = tokens.map((t) => { const dec = Number(t.decimals) || 0; const bal = fmtTokenAmount(t.balance, dec); return `
${esc(t.symbol)}${t.name ? ' ' + esc(t.name) + '' : ""}
${esc(t.mint.slice(0, 10))}…${esc(t.mint.slice(-6))}
${esc(bal)}
`; }).join(""); el.querySelectorAll("button[data-mint]").forEach((b) => b.addEventListener("click", () => { sendAsset = { mint: b.dataset.mint, symbol: b.dataset.symbol, decimals: Number(b.dataset.decimals) }; showTab("send"); })); } // Same shape as index.js's fmtTokenAmount — string-safe for u64 SPL amounts. function fmtTokenAmount(rawStr, decimals) { const s = String(rawStr || "0"); const neg = s.startsWith("-"); const abs = neg ? s.slice(1) : s; const d = Number(decimals) || 0; if (d === 0) return (neg ? "-" : "") + abs; const pad = abs.padStart(d + 1, "0"); const whole = pad.slice(0, pad.length - d); const frac = pad.slice(pad.length - d).replace(/0+$/, ""); return (neg ? "-" : "") + whole + (frac ? "." + frac : ""); } function applyUnitPicker() { const s = sel(); if (!s) return; if (!unit) unit = "big"; // ---- SPL asset picker (SOL wallets with tokens) --------------------- const tokens = (chain() === "sol" && s.tokens) || []; const assetField = $("sendAssetField"); if (tokens.length) { assetField.hidden = false; const sel_ = $("sendAsset"); // Rebuild whenever the asset set changes so a new token appears. const key = tokens.map((t) => t.mint).join("|"); if (sel_.dataset.key !== key) { sel_.dataset.key = key; sel_.innerHTML = `` + tokens.map((t) => `` ).join(""); sel_.onchange = () => { const opt = sel_.options[sel_.selectedIndex]; sendAsset = opt && opt.value ? { mint: opt.value, symbol: opt.dataset.symbol, decimals: Number(opt.dataset.decimals) } : null; applyUnitPicker(); schedulePlan(); }; } // Reflect the current sendAsset back into the select. sel_.value = sendAsset ? sendAsset.mint : ""; } else { assetField.hidden = true; sendAsset = null; } const isToken = sendAsset != null; const big = isToken ? sendAsset.symbol : bigUnitLabel(); const small = isToken ? "raw" : smallUnitLabel(); $("unitPicker").innerHTML = `` + ``; $("unitPicker").querySelectorAll("button").forEach((b) => b.addEventListener("click", () => setUnit(b.dataset.u))); $("sendTo").placeholder = ({ bch: s.network === "chipnet" ? "bchtest:q… or legacy m…" : "bitcoincash:q… or legacy 1…", btc: s.network === "testnet" ? "tb1q… (or 2… / m…, n…)" : "bc1q… (or bc1p…, 3…, 1…)", trx: "T… (base58check, 34 chars)", sc: "addr1… (76-hex + checksum)", dgb: "dgb1q… (or D… / S… depending on family)", eth: "0x… (40 hex chars, EIP-55)", sol: "base58 public key (32 bytes)", })[chain()] || "recipient address"; $("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; // For SPL tokens the amount is a raw u64 string in the token's own // smallest unit — same BigInt-safe path SC uses. const d = sendAsset ? Number(sendAsset.decimals) || 0 : decimals(); const bigDecimals = sendAsset != null || d > 15; if (unit === "small") { if (bigDecimals) return raw.replace(/\D+/g, "") || "0"; return Math.round(Number(raw)); } const [w, f = ""] = raw.split("."); const frac = (f + "0".repeat(d)).slice(0, d); if (bigDecimals) { const total = (BigInt(w || "0") * (10n ** BigInt(d))) + BigInt(frac || "0"); return total.toString(); } return Number(w || 0) * Math.pow(10, d) + Number(frac || 0); } // Sum "confirmed + unconfirmed" BigInt-safely (strings for SC, numbers elsewhere). function balanceSum(b) { if (!b) return 0; if (typeof b.confirmed === "string" || typeof b.unconfirmed === "string") { return (BigInt(b.confirmed || "0") + BigInt(b.unconfirmed || "0")).toString(); } return (b.confirmed || 0) + (b.unconfirmed || 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."}
`; return; } el.innerHTML = list.map((t) => { const inc = t.delta >= 0; const when = t.time ? new Date(t.time * 1000).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }) : "pending"; const who = inc ? (t.from ? "from " + shortAddr(t.from) : "") : (t.to ? "to " + shortAddr(t.to) : ""); const what = (inc ? "Received" : "Sent") + (who ? " " + who : ""); const conf = t.confirmations > 0 ? (t.confirmations >= 6 ? "confirmed" : t.confirmations + " conf") : (t.status === "failed" ? "failed" : "unconfirmed"); const delta = Math.abs(t.delta || 0); return `
${inc ? "↓" : "↑"}
${esc(what)}
${inc ? "+" : "−"}${delta ? fmtBig(delta) : "—"}
${esc(when)}${t.fee != null ? " · fee " + fmtSmall(t.fee) + " " + smallUnitLabel() : ""}
${esc(conf)}
`; }).join(""); el.querySelectorAll(".tx").forEach((row) => row.addEventListener("click", () => openUrl(explorerHref(sel().explorerTx, row.dataset.txid)))); } function shortAddr(a) { if (!a) return ""; const s = String(a).replace(/^bitcoincash:|^bchtest:/, ""); return esc(s.slice(0, 10)) + "…" + esc(s.slice(-4)); } // ---- QR -------------------------------------------------------------------- // Coin-scheme URI so wallet apps that scan know which chain the payment is // for. Follows each chain's own convention (BIP21 for BTC-family, EIP-681 // for ETH, Solana Pay for SOL, bare address for SC where no widely-agreed // URI scheme exists). function qrPayload(chain, address, network) { if (chain === "bch") return (network === "chipnet" ? "bchtest:" : "bitcoincash:") + String(address).replace(/^bitcoincash:|^bchtest:/, ""); if (chain === "btc") return "bitcoin:" + address; // BIP21 if (chain === "dgb") return "digibyte:" + address; if (chain === "eth") return "ethereum:" + address; if (chain === "sol") return "solana:" + address; if (chain === "trx") return "tron:" + address; return String(address); } function drawQr(text) { const cv = $("qr"); const g = cv.getContext("2d"); let q; try { q = window.QR.build(text); } catch { g.clearRect(0, 0, cv.width, cv.height); return; } const scale = Math.max(2, Math.floor(200 / (q.size + 2))); const px = (q.size + 2) * scale; cv.width = cv.height = px; cv.style.width = cv.style.height = px + "px"; g.fillStyle = "#fff"; g.fillRect(0, 0, px, px); g.fillStyle = "#000"; for (let r = 0; r < q.size; r++) for (let c = 0; c < q.size; c++) if (q.modules[r][c]) g.fillRect((c + 1) * scale, (r + 1) * scale, scale, scale); } // ---- receive actions ------------------------------------------------------- $("copyAddr").addEventListener("click", async () => { try { await navigator.clipboard.writeText(sel().address); flash($("copyAddr"), "Copied"); } catch {} }); $("nextAddr").addEventListener("click", async () => { try { const s = await S.invoke("nextAddress"); state.selected = { ...state.selected, ...s }; render(); } catch (e) { flash($("nextAddr"), "Failed"); } }); $("viewAddr").addEventListener("click", () => openUrl(explorerHref(sel().explorerAddr, sel().address))); $("openFaucet").addEventListener("click", () => sel().faucet && openUrl(sel().faucet)); function flash(btn, text) { const old = btn.textContent; btn.textContent = text; setTimeout(() => { btn.textContent = old; }, 1200); } // ---- send ------------------------------------------------------------------ $("sendMax").addEventListener("click", () => { sendMax = !sendMax; $("sendMax").classList.toggle("primary", sendMax); $("sendAmt").disabled = sendMax; if (!sendMax) $("sendAmt").value = ""; schedulePlan(); }); $("feeRate").addEventListener("input", () => { $("feeLbl").textContent = $("feeRate").value + " sat/B"; schedulePlan(); }); ["sendTo", "sendAmt"].forEach((id) => $(id).addEventListener("input", () => { if (id === "sendAmt" && sendMax) return; schedulePlan(); })); function schedulePlan() { clearTimeout(planTimer); planTimer = setTimeout(updatePlan, 250); } async function updatePlan() { const to = $("sendTo").value.trim(); const msg = $("sendMsg"); msg.hidden = true; lastPlan = null; $("sendBtn").disabled = true; $("sumAmt").textContent = $("sumFee").textContent = $("sumTotal").textContent = "—"; $("sendToHint").textContent = ""; if (!to || (!sendMax && !amountUnits())) return; try { if (sendAsset) { // SPL token flow — amount is raw units of the token's decimals. const p = await S.invoke("planTokenSend", { mint: sendAsset.mint, to, amount: amountUnits() }); lastPlan = { _token: true, ...p }; $("sumAmt").textContent = fmtTokenAmount(p.recipients[0].value, sendAsset.decimals) + " " + sendAsset.symbol; $("sumFee").textContent = fmtBig(p.fee, decimals()) + " SOL"; $("sumTotal").textContent = fmtTokenAmount(p.total, sendAsset.decimals) + " " + sendAsset.symbol; $("sendBtn").disabled = false; return; } const feeRate = chain() === "bch" ? Number($("feeRate").value) : undefined; const p = await S.invoke("planSend", { to, amount: amountUnits(), feeRate, sendMax }); lastPlan = p; $("sendToHint").textContent = p.recipients[0].to !== to ? "→ " + p.recipients[0].to : ""; $("sumAmt").textContent = fmtBig(p.recipients[0].value) + " " + ticker(); $("sumFee").textContent = chain() === "bch" ? fmtSmall(p.fee) + " " + smallUnitLabel() : fmtBig(p.fee) + " " + ticker(); $("sumTotal").textContent = fmtBig(p.total) + " " + ticker(); if (sendMax) $("sendAmt").value = unit === "big" ? fmtBig(p.recipients[0].value) : String(p.recipients[0].value); $("sendBtn").disabled = false; } catch (e) { msg.className = "msg err"; msg.textContent = cleanErr(e); msg.hidden = false; } } $("sendBtn").addEventListener("click", async () => { if (!lastPlan) return; const msg = $("sendMsg"); msg.hidden = true; $("sendBtn").disabled = true; $("sendBtn").textContent = "Waiting for approval…"; try { const isToken = sendAsset && lastPlan._token; const feeRate = chain() === "bch" ? Number($("feeRate").value) : undefined; const r = isToken ? await S.invoke("sendToken", { mint: sendAsset.mint, to: $("sendTo").value.trim(), amount: amountUnits() }) : await S.invoke("send", { to: $("sendTo").value.trim(), amount: amountUnits(), feeRate, sendMax }); msg.className = "msg ok"; msg.innerHTML = `Sent. ${esc(r.txid.slice(0, 16))}…`; msg.querySelector("a").addEventListener("click", () => openUrl(explorerHref(sel().explorerTx, r.txid))); msg.hidden = false; $("sendTo").value = ""; $("sendAmt").value = ""; sendMax = false; $("sendMax").classList.remove("primary"); $("sendAmt").disabled = false; lastPlan = null; } catch (e) { const t = cleanErr(e); if (t !== "cancelled") { msg.className = "msg err"; msg.textContent = t; msg.hidden = false; } $("sendBtn").disabled = !lastPlan; } finally { $("sendBtn").textContent = "Send"; } }); // ---- settings -------------------------------------------------------------- function fillSettings() { // Global settings (fiat prices, connected sites) render even when there // is no active wallet / the vault is locked. renderPricesSetting(); renderSites(); const s = sel(); const chainSetup = !!s && s.phase === "ready"; $("walletManage").hidden = !chainSetup; if (!chainSetup) { $("bchSettings").hidden = true; $("trxSettings").hidden = true; $("scSettings").hidden = true; $("dgbSettings").hidden = true; $("btcSettings").hidden = true; $("ethSettings").hidden = true; $("solSettings").hidden = true; return; } $("bchSettings").hidden = chain() !== "bch"; $("trxSettings").hidden = chain() !== "trx"; $("scSettings").hidden = chain() !== "sc"; $("dgbSettings").hidden = chain() !== "dgb"; $("btcSettings").hidden = chain() !== "btc"; $("ethSettings").hidden = chain() !== "eth"; $("solSettings").hidden = chain() !== "sol"; $("removeBtn").disabled = !!s.isLegacy && s.chain === "bch"; $("removeHint").textContent = (s.isLegacy && s.chain === "bch") ? "The default BCH wallet cannot be removed (it protects legacy funds)." : (s.isLegacy && s.chain === "sc" ? "Removing this wallet unlinks it from Aegis. Funds stay on-chain and reappear if you add a Siacoin wallet again with the legacy seed slot." : ""); $("renameLabel").value = s.label || ""; if (chain() === "bch") { if (!settingsFilled) { $("setPath").value = s.accountPath || ""; $("setServers").value = (state.bchServers?.list || []).join("\n"); settingsFilled = true; } $("bchServersRow").hidden = s.network !== "mainnet"; $("serverHint").textContent = s.network !== "mainnet" ? "Chipnet uses bundled defaults in this build." : (state.bchServers?.custom ? "Custom list." : "Bundled defaults.") + (s.server ? " Connected to " + hostOf(s.server) + "." : " Not connected."); $("purpose").textContent = "silentmode/addons/" + (s.purpose || ""); renderWcSites(); } else if (chain() === "trx") { $("trxPurpose").textContent = "silentmode/addons/" + (s.purpose || ""); } else if (chain() === "sc") { if (!settingsFilled) { $("setWalletdUrl").value = s.walletdUrl || ""; settingsFilled = true; } $("scPurpose").textContent = "silentmode/addons/" + (s.purpose || ""); $("scRecovery").innerHTML = ""; } else if (chain() === "dgb") { if (!settingsFilled) { fillFamilyPicker("Dgb", s); settingsFilled = true; } $("dgbPurpose").textContent = "silentmode/addons/" + (s.purpose || ""); $("dgbRecovery").innerHTML = ""; } else if (chain() === "btc") { if (!settingsFilled) { fillFamilyPicker("Btc", s); settingsFilled = true; } $("btcPurpose").textContent = "silentmode/addons/" + (s.purpose || ""); $("btcRecovery").innerHTML = ""; } else if (chain() === "eth") { if (!settingsFilled) { $("setEthRpcUrl").value = s.rpcUrl || ""; settingsFilled = true; } $("ethPurpose").textContent = "silentmode/addons/" + (s.purpose || ""); $("ethRecovery").innerHTML = ""; } else if (chain() === "sol") { if (!settingsFilled) { $("setSolRpcUrl").value = s.rpcUrl || ""; settingsFilled = true; } $("solPurpose").textContent = "silentmode/addons/" + (s.purpose || ""); $("solRecovery").innerHTML = ""; } } // Reflect the current price feed state into the Settings toggle + status // line. Called from fillSettings() and whenever fresh state arrives. function renderPricesSetting() { const p = state?.prices; const toggle = $("pricesToggle"); if (!toggle) return; toggle.checked = !!p?.enabled; $("refreshPrices").hidden = !p?.enabled; const st = $("pricesStatus"); if (!p?.enabled) { st.textContent = "Disabled — no requests made."; return; } if (p.loading) { st.textContent = "Fetching…"; return; } if (p.error) { st.textContent = "Error: " + p.error; return; } if (p.fetchedAt) { const secs = Math.round((Date.now() - p.fetchedAt) / 1000); const when = secs < 60 ? `${secs}s ago` : `${Math.round(secs / 60)}m ago`; st.textContent = `Updated ${when} · ${Object.keys(p.prices || {}).length} coins.`; return; } st.textContent = "Enabled — first fetch pending."; } async function renderSites() { let perms = {}; try { perms = await S.invoke("permissions"); } catch {} const origins = Object.keys(perms).filter((o) => { const p = perms[o]; return p && (p.readAddress || p.sendTx || (p.trx && p.trx.readAddress)); }); const el = $("sites"); if (!origins.length) { el.innerHTML = `
None yet.
`; return; } el.innerHTML = origins.map((o) => { const p = perms[o]; const what = []; if (p.readAddress) what.push("BCH address"); if (p.sendTx) what.push(`BCH payments: ${fmtBig(Math.max(0, p.sendTx.capSats - (p.sendTx.usedSats || 0)), 8)} of ${fmtBig(p.sendTx.capSats, 8)} BCH left`); if (p.trx && p.trx.readAddress) what.push("Tron " + (p.trx.network === "nile" ? "Nile testnet" : "mainnet") + " address"); return `
${esc(o)}
${esc(what.join(" · "))}
`; }).join(""); el.querySelectorAll("button[data-origin]").forEach((b) => b.addEventListener("click", async () => { try { await S.invoke("revoke", { origin: b.dataset.origin }); renderSites(); } catch {} })); } $("applySettings").addEventListener("click", async () => { const msg = $("settingsMsg"); msg.hidden = true; try { const path = $("setPath").value.trim(); const servers = $("setServers").value.split(/\n+/).map((s) => s.trim()).filter(Boolean); if (path && path !== (sel().accountPath || "")) { state = await S.invoke("setAccountPath", { id: state.selectedWalletId, accountPath: path }); } if (sel().network === "mainnet") { const currentJoined = (state.bchServers?.list || []).join(); if (state.bchServers?.custom || servers.join() !== currentJoined) { state = await S.invoke("setBchServers", { servers }); } } settingsFilled = false; fillSettings(); render(); flash($("applySettings"), "Applied"); } catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; } }); $("resetServers").addEventListener("click", async () => { try { state = await S.invoke("setBchServers", { servers: [] }); settingsFilled = false; fillSettings(); render(); } catch (e) { $("settingsMsg").textContent = cleanErr(e); $("settingsMsg").hidden = false; } }); $("renameBtn").addEventListener("click", async () => { const label = $("renameLabel").value.trim(); if (!label) return; try { state = await S.invoke("renameWallet", { id: state.selectedWalletId, label }); render(); flash($("renameBtn"), "Renamed"); } catch (e) { $("settingsMsg").textContent = cleanErr(e); $("settingsMsg").hidden = false; } }); $("removeBtn").addEventListener("click", async () => { const s = sel(); if (!s || s.isLegacy) return; if (!confirm(`Remove the wallet "${s.label}"?\n\nThe on-chain address stays; the wallet is unlinked from Aegis. You can add it back later by creating a new wallet on the same coin + network.`)) return; try { state = await S.invoke("removeWallet", { id: state.selectedWalletId }); settingsFilled = false; render(); } catch (e) { $("settingsMsg").textContent = cleanErr(e); $("settingsMsg").hidden = false; } }); $("showXpub").addEventListener("click", async () => { try { const r = await S.invoke("recovery", { id: state.selectedWalletId }); $("recovery").innerHTML = recoveryHtml(r); } catch (e) { $("recovery").textContent = cleanErr(e); } }); $("showXprv").addEventListener("click", async () => { try { const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true }); $("recovery").innerHTML = recoveryHtml(r); } catch (e) { $("recovery").textContent = cleanErr(e); } }); function recoveryHtml(r) { let h = `
Account path
${esc(r.accountPath)}
Account xpub
${esc(r.xpub)}
`; if (r.xprv) h += `
Account private key (xprv)
${esc(r.xprv)}
`; return h; } document.querySelectorAll("nav button").forEach((b) => b.addEventListener("click", () => { if (b.dataset.tab !== "settings") { $("recovery").innerHTML = ""; $("scRecovery").innerHTML = ""; $("dgbRecovery").innerHTML = ""; $("btcRecovery").innerHTML = ""; $("ethRecovery").innerHTML = ""; $("solRecovery").innerHTML = ""; } })); // Sia-specific settings. $("applyWalletdUrl").addEventListener("click", async () => { const msg = $("settingsMsg"); msg.hidden = true; try { state = await S.invoke("setWalletdUrl", { id: state.selectedWalletId, walletdUrl: $("setWalletdUrl").value.trim() }); settingsFilled = false; fillSettings(); render(); flash($("applyWalletdUrl"), "Applied"); } catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; } }); $("showScSeed").addEventListener("click", async () => { try { const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true }); $("scRecovery").innerHTML = `
First address (index 0)
${esc(r.xpub || "")}
` + (r.xprv ? `
Wallet seed (hex)
${esc(r.xprv)}
` : ""); } catch (e) { $("scRecovery").textContent = cleanErr(e); } }); // Family-picker helper used by both DGB and BTC. Prefix is "Dgb" or "Btc": // the DOM IDs are #setFamily + #setPath. function fillFamilyPicker(prefix, s) { const families = s.meta?.addressFamilies || []; const current = String(s.accountPath || ""); let currentId = families.find((f) => f.defaultAccountPath === current)?.id; if (!currentId) { const m = /^m\/(\d+)'/.exec(current); const purpose = m ? Number(m[1]) : null; currentId = families.find((f) => f.purpose === purpose)?.id || families[0]?.id; } $(`set${prefix}Path`).value = current || families[0]?.defaultAccountPath || ""; $(`set${prefix}Family`).innerHTML = families.map((f) => `` ).join(""); } // Any family select → auto-fill the sibling path input. document.addEventListener("change", (e) => { const t = e.target; if (!t) return; const m = /^set(Dgb|Btc)Family$/.exec(t.id || ""); if (!m) return; const opt = t.options[t.selectedIndex]; if (opt && opt.dataset.path) $(`set${m[1]}Path`).value = opt.dataset.path; }); $("applyDgbPath").addEventListener("click", async () => { const msg = $("settingsMsg"); msg.hidden = true; try { state = await S.invoke("setAccountPath", { id: state.selectedWalletId, accountPath: $("setDgbPath").value.trim() }); settingsFilled = false; fillSettings(); render(); flash($("applyDgbPath"), "Applied"); } catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; } }); $("applyBtcPath").addEventListener("click", async () => { const msg = $("settingsMsg"); msg.hidden = true; try { state = await S.invoke("setAccountPath", { id: state.selectedWalletId, accountPath: $("setBtcPath").value.trim() }); settingsFilled = false; fillSettings(); render(); flash($("applyBtcPath"), "Applied"); } catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; } }); $("showBtcXpub").addEventListener("click", async () => { try { const r = await S.invoke("recovery", { id: state.selectedWalletId }); $("btcRecovery").innerHTML = recoveryHtml(r); } catch (e) { $("btcRecovery").textContent = cleanErr(e); } }); $("showBtcXprv").addEventListener("click", async () => { try { const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true }); $("btcRecovery").innerHTML = recoveryHtml(r); } catch (e) { $("btcRecovery").textContent = cleanErr(e); } }); // ETH / SOL: RPC URL. $("applyEthRpc").addEventListener("click", async () => { const msg = $("settingsMsg"); msg.hidden = true; try { state = await S.invoke("setRpcUrl", { id: state.selectedWalletId, rpcUrl: $("setEthRpcUrl").value.trim() }); settingsFilled = false; fillSettings(); render(); flash($("applyEthRpc"), "Applied"); } catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; } }); $("applySolRpc").addEventListener("click", async () => { const msg = $("settingsMsg"); msg.hidden = true; try { state = await S.invoke("setRpcUrl", { id: state.selectedWalletId, rpcUrl: $("setSolRpcUrl").value.trim() }); settingsFilled = false; fillSettings(); render(); flash($("applySolRpc"), "Applied"); } catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; } }); $("showEthKey").addEventListener("click", async () => { try { const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true }); $("ethRecovery").innerHTML = `
Address
${esc(sel().address || "")}
` + `
Public key (uncompressed hex)
${esc(r.xpub || "")}
` + (r.xprv ? `
Private key (hex)
${esc(r.xprv)}
` : ""); } catch (e) { $("ethRecovery").textContent = cleanErr(e); } }); $("showSolKey").addEventListener("click", async () => { try { const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true }); $("solRecovery").innerHTML = `
Address (public key, base58)
${esc(r.xpub || "")}
` + (r.xprv ? `
Wallet seed (hex, 32 bytes)
${esc(r.xprv)}
` : ""); } catch (e) { $("solRecovery").textContent = cleanErr(e); } }); $("showDgbXpub").addEventListener("click", async () => { try { const r = await S.invoke("recovery", { id: state.selectedWalletId }); $("dgbRecovery").innerHTML = recoveryHtml(r); } catch (e) { $("dgbRecovery").textContent = cleanErr(e); } }); $("showDgbXprv").addEventListener("click", async () => { try { const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true }); $("dgbRecovery").innerHTML = recoveryHtml(r); } catch (e) { $("dgbRecovery").textContent = cleanErr(e); } }); // ---- WizardConnect (BCH only) --------------------------------------------- function renderWcSites() { const el = $("wcSites"); if (!el) return; const walletId = state?.selectedWalletId; const conns = (state?.wc && state.wc[walletId]) || []; if (!conns.length) { el.innerHTML = `
No dapps paired yet.
`; return; } el.innerHTML = conns.map((c) => { const label = c.dappName || "(pairing…)"; const iconHtml = c.dappIcon ? `` : ""; return `
${iconHtml}
${esc(label)}
${esc((c.uri || "").slice(0, 46))}…
`; }).join(""); el.querySelectorAll("button[data-wcconn]").forEach((b) => b.addEventListener("click", async () => { try { state = await S.invoke("wcDisconnect", { walletId, connId: b.dataset.wcconn }); render(); } catch (e) { const m = $("wcMsg"); m.className = "msg err"; m.textContent = cleanErr(e); m.hidden = false; } })); } $("wcConnectBtn").addEventListener("click", async () => { const walletId = state?.selectedWalletId; const uri = $("wcUri").value.trim(); const m = $("wcMsg"); m.hidden = true; if (!uri) return; try { state = await S.invoke("wcConnect", { walletId, uri }); $("wcUri").value = ""; m.className = "msg ok"; m.textContent = "Pairing…"; m.hidden = false; render(); } catch (e) { m.className = "msg err"; m.textContent = cleanErr(e); m.hidden = false; } }); // ---- prices toggle --------------------------------------------------------- $("pricesToggle").addEventListener("change", async () => { const on = $("pricesToggle").checked; try { state = await S.invoke("setPricesEnabled", { enabled: on }); render(); if (tab === "settings") renderPricesSetting(); } catch (e) { // Roll the checkbox back if the host rejected the change. $("pricesToggle").checked = !on; $("settingsMsg").textContent = cleanErr(e); $("settingsMsg").hidden = false; } }); $("refreshPrices").addEventListener("click", async () => { try { await S.invoke("refreshPrices"); // The host emits a state event on completion; the render will pick it up. renderPricesSetting(); } catch (e) { $("settingsMsg").textContent = cleanErr(e); $("settingsMsg").hidden = false; } }); // ---- boot ------------------------------------------------------------------ S.on("state", (s) => { state = s; render(); if (tab === "settings") fillSettings(); }); (async () => { try { state = await S.invoke("state"); render(); } catch (e) { $("gate").hidden = false; $("gate").innerHTML = `
${esc(cleanErr(e))}
`; } })();