// Wallet panel. All state comes from the add-on's activate() context via // window.silentmode.invoke / on; this file only renders and collects input. const $ = (id) => document.getElementById(id); const S = window.silentmode; let state = null; let tab = "receive"; // 8 decimals, trailing zeros trimmed, never fewer than two: 0.00, 0.001, 1.23456789 function fmtBch(sats) { let s = (Number(sats || 0) / 1e8).toFixed(8).replace(/0+$/, ""); if (s.endsWith(".")) s += "00"; else if (/\.\d$/.test(s)) s += "0"; return s; } const fmtSats = (sats) => Number(sats || 0).toLocaleString("en-US"); const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]); const hostOf = (url) => { try { return new URL(url).host; } catch { return url; } }; const openUrl = (url) => S.invoke("openUrl", { url }).catch(() => {}); // ---- 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") fillSettings(); } // ---- render ---------------------------------------------------------------- function render() { if (!state) return; const ready = state.phase === "ready"; const gate = $("gate"); $("tabs").hidden = !ready; gate.hidden = ready; if (!ready) { const copy = { locked: ["🔒", "Unlock your password vault to open the wallet.", "Settings › Passwords. The wallet keys derive 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 this wallet can be recreated from it on any machine."], error: ["⚠", "The wallet could not start.", state.error || ""], }[state.phase] || ["…", "Starting…", ""]; gate.innerHTML = `
${copy[0]}
${esc(copy[1])}
${esc(copy[2])}
`; } // header const dot = $("dot"); dot.className = "dot " + (state.server ? (state.scanning ? "busy" : "on") : ""); $("netlbl").textContent = state.server ? hostOf(state.server) + (state.scanning ? " · syncing" : "") : (ready ? "connecting…" : "mainnet"); if (ready) { const total = (state.balance?.confirmed || 0) + (state.balance?.unconfirmed || 0); $("balBch").textContent = fmtBch(total); const parts = [fmtSats(total) + " sat"]; if (state.balance?.unconfirmed) parts.push(fmtBch(state.balance.unconfirmed) + " unconfirmed"); if (state.height) parts.push("block " + fmtSats(state.height)); $("balSub").textContent = parts.join(" · "); } else { $("balBch").textContent = "—"; $("balSub").textContent = "mainnet"; } if (!ready) return; // receive if (state.address && $("addr").textContent !== state.address) { $("addr").textContent = state.address; drawQr("bitcoincash:" + state.address.replace(/^bitcoincash:/, "")); } $("addrMeta").textContent = `· #${state.addressIndex} · ${state.addressPath || ""}`; // history renderHistory(); if (state.error) { $("balSub").textContent = state.error; } } 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); } function renderHistory() { const list = state.history || []; const el = $("txlist"); if (!list.length) { el.innerHTML = `
${state.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 what = inc ? "Received" : ("Sent" + (t.to ? " to " + esc(t.to.replace(/^bitcoincash:/, "").slice(0, 12)) + "…" : "")); const conf = t.confirmations > 0 ? (t.confirmations >= 6 ? "confirmed" : t.confirmations + " conf") : "unconfirmed"; return `
${inc ? "↓" : "↑"}
${what}
${inc ? "+" : "−"}${fmtBch(Math.abs(t.delta))}
${esc(when)}${t.fee != null ? " · fee " + fmtSats(t.fee) + " sat" : ""}
${conf}
`; }).join(""); el.querySelectorAll(".tx").forEach((row) => row.addEventListener("click", () => openUrl(state.explorerTx + row.dataset.txid))); } // ---- receive actions ------------------------------------------------------- $("copyAddr").addEventListener("click", async () => { try { await navigator.clipboard.writeText(state.address); flash($("copyAddr"), "Copied"); } catch {} }); $("nextAddr").addEventListener("click", async () => { try { state = await S.invoke("nextAddress"); render(); } catch (e) { flash($("nextAddr"), "Failed"); } }); $("viewAddr").addEventListener("click", () => openUrl(state.explorerAddr + state.address)); function flash(btn, text) { const old = btn.textContent; btn.textContent = text; setTimeout(() => { btn.textContent = old; }, 1200); } // ---- settings -------------------------------------------------------------- let settingsFilled = false; function fillSettings() { if (!state) return; if (!settingsFilled) { $("setPath").value = state.accountPath || ""; $("setServers").value = (state.servers || []).join("\n"); settingsFilled = true; } $("serverHint").textContent = (state.customServers ? "Custom list." : "Bundled defaults.") + (state.server ? " Connected to " + hostOf(state.server) + "." : " Not connected."); $("purpose").textContent = "silentmode/addons/bchwallet/mainnet/0"; } $("applySettings").addEventListener("click", async () => { const msg = $("settingsMsg"); msg.hidden = true; try { const servers = $("setServers").value.split(/\n+/).map((s) => s.trim()).filter(Boolean); state = await S.invoke("setSettings", { accountPath: $("setPath").value, servers: state.customServers || servers.join() !== (state.servers || []).join() ? servers : undefined }); 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("setSettings", { servers: [] }); settingsFilled = false; fillSettings(); render(); } catch (e) { $("settingsMsg").textContent = cleanErr(e); $("settingsMsg").hidden = false; } }); $("showXpub").addEventListener("click", async () => { try { const r = await S.invoke("recovery", {}); $("recovery").innerHTML = recoveryHtml(r); } catch (e) { $("recovery").textContent = cleanErr(e); } }); $("showXprv").addEventListener("click", async () => { try { const r = await S.invoke("recovery", { 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; } const cleanErr = (e) => String(e?.message || e).replace(/^Error invoking remote method '[^']+': Error: /, ""); // Wipe a revealed key when the user leaves the Settings tab. document.querySelectorAll("nav button").forEach((b) => b.addEventListener("click", () => { if (b.dataset.tab !== "settings") $("recovery").innerHTML = ""; })); // ---- boot ------------------------------------------------------------------ S.on("state", (s) => { state = s; render(); }); (async () => { try { state = await S.invoke("state"); render(); } catch (e) { $("gate").hidden = false; $("gate").innerHTML = `
⚠
${esc(cleanErr(e))}
`; } })();