Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
569 lines
28 KiB
JavaScript
569 lines
28 KiB
JavaScript
// 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") {
|
||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Bitcoin Cash" style="vertical-align:middle;flex:none">
|
||
<circle cx="16" cy="16" r="15.5" fill="#0ac18e" stroke="#0a9a72" stroke-width=".8"/>
|
||
<text x="16" y="22.4" text-anchor="middle" font-family="Segoe UI,Arial,sans-serif" font-size="20" font-weight="800" fill="#fff">₿</text>
|
||
</svg>`;
|
||
}
|
||
if (logo === "trx") {
|
||
// Simplified from the official geometric Tron mark: triangle + tail line.
|
||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Tron" style="vertical-align:middle;flex:none">
|
||
<circle cx="16" cy="16" r="15.5" fill="#ff060a" stroke="#c1050a" stroke-width=".8"/>
|
||
<path d="M7.5 10.2 L24 12.5 L14.5 23.8 Z"
|
||
fill="none" stroke="#fff" stroke-width="1.7" stroke-linejoin="round"/>
|
||
<line x1="7.5" y1="10.2" x2="14.5" y2="23.8" stroke="#fff" stroke-width="1.7" stroke-linejoin="round"/>
|
||
</svg>`;
|
||
}
|
||
if (logo === "sc") {
|
||
// Sia's mark is a stylized S built from two mirrored crescents. Approximated
|
||
// here with a plain S glyph on the brand green so it reads at 22px.
|
||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Siacoin" style="vertical-align:middle;flex:none">
|
||
<circle cx="16" cy="16" r="15.5" fill="#20be82" stroke="#158e5f" stroke-width=".8"/>
|
||
<text x="16" y="22.3" text-anchor="middle" font-family="Segoe UI,Arial,sans-serif" font-size="18" font-weight="800" fill="#fff">S</text>
|
||
</svg>`;
|
||
}
|
||
if (logo === "dgb") {
|
||
// DigiByte's brand shape is an octagonal ring; approximated with an
|
||
// octagon outline + a bold D inside on the DGB blue.
|
||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="DigiByte" style="vertical-align:middle;flex:none">
|
||
<polygon points="10,2 22,2 30,10 30,22 22,30 10,30 2,22 2,10" fill="#0066cc" stroke="#004a99" stroke-width=".8"/>
|
||
<text x="16" y="22.4" text-anchor="middle" font-family="Segoe UI,Arial,sans-serif" font-size="18" font-weight="800" fill="#fff">D</text>
|
||
</svg>`;
|
||
}
|
||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" style="vertical-align:middle;flex:none">
|
||
<rect x="4" y="4" width="24" height="24" rx="6" fill="#7f8ba1"/>
|
||
<text x="16" y="22" text-anchor="middle" fill="#fff" font-size="16" font-weight="700">?</text>
|
||
</svg>`;
|
||
}
|
||
function testnetTag() { return `<span class="ttag">TEST</span>`; }
|
||
|
||
// 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();
|
||
return c === "bch" || c === "dgb" ? "sat" : (c === "trx" ? "sun" : (c === "sc" ? "H" : "u"));
|
||
}
|
||
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 `<div class="row ${on}" data-select="${esc(w.id)}">
|
||
${logoSvg(w.logo, 22)}
|
||
<div class="m"><div class="l">${esc(w.label)}</div><div class="s">${sub}</div></div>
|
||
<div class="v">${esc(bal)}</div>
|
||
</div>`;
|
||
}).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 `<div class="coinrow" data-coin="${esc(c.chain)}">
|
||
${logoSvg(c.logo, 22)}
|
||
<div class="m"><div class="l">${esc(c.label)}</div><div class="s">${esc(sub)}</div></div>
|
||
<div class="caret">▸</div>
|
||
</div>
|
||
<div class="netgroup" id="netgroup-${esc(c.chain)}" hidden>
|
||
${c.networks.map((n) => `<div class="netchoice" data-add="${esc(c.chain + ":" + n.id)}">
|
||
${esc(n.label)}${n.testnet ? " " + testnetTag() : ""}
|
||
</div>`).join("")}
|
||
</div>`;
|
||
}).join("");
|
||
d.innerHTML =
|
||
rowsHtml +
|
||
`<hr><div class="addhdr">+ Add wallet</div>${coinRows}`;
|
||
|
||
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)); }
|
||
}));
|
||
}
|
||
function showErr(text) {
|
||
const box = $("gate");
|
||
box.hidden = false; box.innerHTML = `<div class="big">⚠</div><div>${esc(text)}</div>`;
|
||
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
|
||
// <wallet label> <coin · network + optional TEST tag>
|
||
$("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 = `<div class="big">${copy[0]}</div><div><b>${esc(copy[1])}</b></div><div class="hint" style="margin-top:8px">${esc(copy[2])}</div>`;
|
||
}
|
||
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 = `<div class="big">🗝</div><div><b>Point Aegis at a walletd node</b></div><div class="hint" style="margin-top:8px">Settings › Sia › walletd URL. Any public or self-hosted <span class="mono">go.sia.tech/walletd</span> in "full" index mode works.</div>`;
|
||
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`;
|
||
} 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 =
|
||
`<button data-u="big" class="${unit === "big" ? "on" : ""}" type="button">${esc(big)}</button>` +
|
||
`<button data-u="small" class="${unit === "small" ? "on" : ""}" type="button">${esc(small)}</button>`;
|
||
$("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;
|
||
const d = decimals();
|
||
const bigDecimals = d > 15; // SC (24) needs BigInt to preserve precision.
|
||
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) {
|
||
// "1.234" (24 dp) → BigInt("1000000000000000000000000") + BigInt("234000…")
|
||
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 = `<div class="empty">${s?.scanning ? "Syncing…" : "No transactions yet."}</div>`; 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 `<div class="tx" data-txid="${esc(t.txid)}" title="${esc(t.txid)}">
|
||
<div class="ic ${inc ? "in" : "out"}">${inc ? "↓" : "↑"}</div>
|
||
<div class="what">${esc(what)}</div>
|
||
<div class="amt2 ${inc ? "in" : ""}">${inc ? "+" : "−"}${delta ? fmtBig(delta) : "—"}</div>
|
||
<div class="when">${esc(when)}${t.fee != null ? " · fee " + fmtSmall(t.fee) + " " + smallUnitLabel() : ""}</div>
|
||
<div class="conf ${t.confirmations > 0 ? (t.status === "failed" ? "pending" : "") : "pending"}">${esc(conf)}</div>
|
||
</div>`;
|
||
}).join("");
|
||
el.querySelectorAll(".tx").forEach((row) => row.addEventListener("click", () => openUrl(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 --------------------------------------------------------------------
|
||
|
||
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(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 {
|
||
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 feeRate = chain() === "bch" ? Number($("feeRate").value) : undefined;
|
||
const r = await S.invoke("send", { to: $("sendTo").value.trim(), amount: amountUnits(), feeRate, sendMax });
|
||
msg.className = "msg ok";
|
||
msg.innerHTML = `Sent. <a class="link" data-tx="${esc(r.txid)}">${esc(r.txid.slice(0, 16))}…</a>`;
|
||
msg.querySelector("a").addEventListener("click", () => openUrl(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() {
|
||
const s = sel(); if (!s) return;
|
||
$("bchSettings").hidden = chain() !== "bch";
|
||
$("trxSettings").hidden = chain() !== "trx";
|
||
$("scSettings").hidden = chain() !== "sc";
|
||
$("dgbSettings").hidden = chain() !== "dgb";
|
||
$("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 || "");
|
||
} 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) {
|
||
$("setDgbPath").value = s.accountPath || "";
|
||
settingsFilled = true;
|
||
}
|
||
$("dgbPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
|
||
$("dgbRecovery").innerHTML = "";
|
||
}
|
||
renderSites();
|
||
}
|
||
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 = `<div class="hint">None yet.</div>`; 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 `<div class="tx" style="grid-template-columns:1fr auto;cursor:default"><div><div class="mono">${esc(o)}</div><div class="hint">${esc(what.join(" · "))}</div></div><button class="btn sm" data-origin="${esc(o)}">Revoke</button></div>`;
|
||
}).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 = `<div class="lbl">Account path</div><div class="mono">${esc(r.accountPath)}</div><div class="lbl">Account xpub</div><div class="mono">${esc(r.xpub)}</div>`;
|
||
if (r.xprv) h += `<div class="lbl">Account private key (xprv)</div><div class="mono" style="color:var(--danger)">${esc(r.xprv)}</div>`;
|
||
return h;
|
||
}
|
||
document.querySelectorAll("nav button").forEach((b) => b.addEventListener("click", () => {
|
||
if (b.dataset.tab !== "settings") {
|
||
$("recovery").innerHTML = "";
|
||
$("scRecovery").innerHTML = "";
|
||
$("dgbRecovery").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 =
|
||
`<div class="lbl">First address (index 0)</div><div class="mono">${esc(r.xpub || "")}</div>` +
|
||
(r.xprv ? `<div class="lbl">Wallet seed (hex)</div><div class="mono" style="color:var(--danger)">${esc(r.xprv)}</div>` : "");
|
||
} catch (e) { $("scRecovery").textContent = cleanErr(e); }
|
||
});
|
||
|
||
// DGB-specific settings.
|
||
$("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; }
|
||
});
|
||
$("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); }
|
||
});
|
||
|
||
// ---- 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 = `<div class="big">⚠</div><div>${esc(cleanErr(e))}</div>`; }
|
||
})();
|