Aegis Wallet 0.4.4 → 0.6.1:
- Vault lifecycle from the wallet gate. The locked / not-yet-created states
now show a master-password form (with optional BIP39 mnemonic on setup)
instead of redirecting users to Settings › Passwords. New
api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by
the existing "vault-derive" capability. api.openSettings(section) also
added; settings.html honours a #section hash on open.
- Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44
path or a WIF; the cashaddr is derived in the add-on, the signer material
goes to a separate wallet-imports.enc via api.vault.imports {list, add,
remove, signer}. Argus password-vault gains createImports / unlockImports /
saveImports with its own KDF salt so the imports key is disjoint from the
passwords key. lib/chain-bch-imported.js is a single-address Electrum
adapter; spend support is deferred to M.1b.
- Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted
in add-on storage. Fiat lines under balances, in the wallet picker, and a
portfolio total when 2+ wallets are open. Settings tab is now reachable
while the vault is locked so the toggle is always available.
- WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js).
@wizardconnect/{core,wallet} are loaded dynamically via api.import to stay
on the right side of LGPL §4d. Sign requests go through approvalModal and
are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS.
- DGB adapter load is now soft-fail: when Aegis runs from userData/addons the
bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of
taking the whole add-on down.
1158 lines
62 KiB
JavaScript
1158 lines
62 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;
|
||
// 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 `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Bitcoin Cash" style="vertical-align:middle;flex:none"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" fill="#8dc351" r="16"/><path d="M21.207 10.534c-.776-1.972-2.722-2.15-4.988-1.71l-.807-2.813-1.712.491.786 2.74c-.45.128-.908.27-1.363.41l-.79-2.758-1.711.49.805 2.813c-.368.114-.73.226-1.085.328l-.003-.01-2.362.677.525 1.83s1.258-.388 1.243-.358c.694-.199 1.035.139 1.2.468l.92 3.204c.047-.013.11-.029.184-.04l-.181.052 1.287 4.49c.032.227.004.612-.48.752.027.013-1.246.356-1.246.356l.247 2.143 2.228-.64c.415-.117.825-.227 1.226-.34l.817 2.845 1.71-.49-.807-2.815a65.74 65.74 0 001.372-.38l.802 2.803 1.713-.491-.814-2.84c2.831-.991 4.638-2.294 4.113-5.07-.422-2.234-1.724-2.912-3.471-2.836.848-.79 1.213-1.858.642-3.3zm-.65 6.77c.61 2.127-3.1 2.929-4.26 3.263l-1.081-3.77c1.16-.333 4.704-1.71 5.34.508zm-2.322-5.09c.554 1.935-2.547 2.58-3.514 2.857l-.98-3.419c.966-.277 3.915-1.455 4.494.563z" fill="#fff" fill-rule="nonzero"/></g></svg>`;
|
||
}
|
||
if (logo === "trx") {
|
||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Tron" style="vertical-align:middle;flex:none"><g fill="none"><circle fill="#EF0027" cx="16" cy="16" r="16"/><path d="M21.932 9.913L7.5 7.257l7.595 19.112 10.583-12.894-3.746-3.562zm-.232 1.17l2.208 2.099-6.038 1.093 3.83-3.192zm-5.142 2.973l-6.364-5.278 10.402 1.914-4.038 3.364zm-.453.934l-1.038 8.58L9.472 9.487l6.633 5.502zm.96.455l6.687-1.21-7.67 9.343.983-8.133z" fill="#FFF"/></g></svg>`;
|
||
}
|
||
if (logo === "sc") {
|
||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Siacoin" style="vertical-align:middle;flex:none"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#20EE82"/><path fill="#FFF" d="M16 7.5a8.5 8.5 0 018.5 8.5v8.5H16a8.5 8.5 0 110-17zm5.1 13.6v-5.023c0-2.82-2.255-5.163-5.074-5.177a5.106 5.106 0 00-5.126 5.126c.014 2.819 2.358 5.074 5.177 5.074H21.1z"/></g></svg>`;
|
||
}
|
||
if (logo === "dgb") {
|
||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="DigiByte" style="vertical-align:middle;flex:none"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#006AD2"/><path fill="#FFF" d="M12.368 25l.479-1.282-.85.084-.306.81c-.024.061-.044.125-.075.183-.067.125-.17.203-.313.204-.63.001-1.258 0-1.888-.001-.015 0-.03-.009-.063-.019l.402-1.085c-.733-.02-1.446-.032-2.156-.113.012-.133 4.062-10.345 4.223-10.652.04-.003.087-.01.135-.01h3.27c.033 0 .066 0 .098.002.331.025.515.305.4.623-.153.42-.315.838-.472 1.256l-2.058 5.474c-.021.056-.039.114-.065.19.058.003.103.009.148.007 3.096-.135 5.368-1.613 6.836-4.39a6.711 6.711 0 00.67-1.935c.073-.395.096-.791-.003-1.186a1.763 1.763 0 00-.698-1.03c-.468-.337-.994-.481-1.562-.484H7.5c.024-.06.035-.1.054-.136l1.388-2.501a.754.754 0 01.706-.418h5.866l.601-1.59h1.782c.044 0 .088-.003.13.003.127.02.2.12.181.25-.008.054-.028.106-.048.158-.123.331-.249.661-.372.992-.021.056-.038.113-.06.18h.805c.02-.043.04-.087.058-.132l.496-1.317c.05-.133.052-.134.185-.134.564 0 1.129-.002 1.693 0 .238.001.323.127.238.357-.135.369-.274.735-.412 1.102-.019.051-.036.103-.06.173.055.01.1.02.145.026.785.096 1.549.274 2.274.601.551.249 1.052.574 1.464 1.03.558.615.835 1.35.879 2.18.042.805-.105 1.581-.372 2.33-.632 1.775-1.53 3.388-2.83 4.747-.896.936-1.93 1.68-3.064 2.282-1.224.65-2.518 1.105-3.858 1.427-.12.03-.183.082-.224.2-.147.41-.303.818-.457 1.226-.095.25-.19.318-.452.318h-1.868z"/></g></svg>`;
|
||
}
|
||
if (logo === "btc") {
|
||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Bitcoin" style="vertical-align:middle;flex:none"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#F7931A"/><path fill="#FFF" fill-rule="nonzero" d="M23.189 14.02c.314-2.096-1.283-3.223-3.465-3.975l.708-2.84-1.728-.43-.69 2.765c-.454-.114-.92-.22-1.385-.326l.695-2.783L15.596 6l-.708 2.839c-.376-.086-.746-.17-1.104-.26l.002-.009-2.384-.595-.46 1.846s1.283.294 1.256.312c.7.175.826.638.805 1.006l-.806 3.235c.048.012.11.03.18.057l-.183-.045-1.13 4.532c-.086.212-.303.531-.793.41.018.025-1.256-.313-1.256-.313l-.858 1.978 2.25.561c.418.105.828.215 1.231.318l-.715 2.872 1.727.43.708-2.84c.472.127.93.245 1.378.357l-.706 2.828 1.728.43.715-2.866c2.948.558 5.164.333 6.097-2.333.752-2.146-.037-3.385-1.588-4.192 1.13-.26 1.98-1.003 2.207-2.538zm-3.95 5.538c-.533 2.147-4.148.986-5.32.695l.95-3.805c1.172.293 4.929.872 4.37 3.11zm.535-5.569c-.487 1.953-3.495.96-4.47.717l.86-3.45c.975.243 4.118.696 3.61 2.733z"/></g></svg>`;
|
||
}
|
||
if (logo === "eth") {
|
||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Ethereum" style="vertical-align:middle;flex:none"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#627EEA"/><g fill="#FFF" fill-rule="nonzero"><path fill-opacity=".602" d="M16.498 4v8.87l7.497 3.35z"/><path d="M16.498 4L9 16.22l7.498-3.35z"/><path fill-opacity=".602" d="M16.498 21.968v6.027L24 17.616z"/><path d="M16.498 27.995v-6.028L9 17.616z"/><path fill-opacity=".2" d="M16.498 20.573l7.497-4.353-7.497-3.348z"/><path fill-opacity=".602" d="M9 16.22l7.498 4.353v-7.701z"/></g></g></svg>`;
|
||
}
|
||
if (logo === "sol") {
|
||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Solana" style="vertical-align:middle;flex:none"><g fill="none"><circle fill="#66F9A1" cx="16" cy="16" r="16"/><path d="M9.925 19.687a.59.59 0 01.415-.17h14.366a.29.29 0 01.207.497l-2.838 2.815a.59.59 0 01-.415.171H7.294a.291.291 0 01-.207-.498l2.838-2.815zm0-10.517A.59.59 0 0110.34 9h14.366c.261 0 .392.314.207.498l-2.838 2.815a.59.59 0 01-.415.17H7.294a.291.291 0 01-.207-.497L9.925 9.17zm12.15 5.225a.59.59 0 00-.415-.17H7.294a.291.291 0 00-.207.498l2.838 2.815c.11.109.26.17.415.17h14.366a.291.291 0 00.207-.498l-2.838-2.815z" fill="#FFF"/></g></svg>`;
|
||
}
|
||
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 `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Aegis" style="vertical-align:middle;flex:none">
|
||
<polygon points="16,2 29,9 29,23 16,30 3,23 3,9" fill="none" stroke="#d6ff3d" stroke-width="2" stroke-linejoin="round"/>
|
||
<circle cx="16" cy="16" r="4.3" fill="none" stroke="#d6ff3d" stroke-width="1.4"/>
|
||
<circle cx="16" cy="16" r="1.3" fill="#d6ff3d"/>
|
||
<path d="M16 10.5 v-2.4 M16 21.5 v2.4 M10.5 16 h-2.4 M21.5 16 h2.4" stroke="#d6ff3d" stroke-width="1.4" stroke-linecap="round"/>
|
||
</svg>`;
|
||
}
|
||
// Fallback = Aegis shield (rather than a "?"), so an unrecognised
|
||
// registry entry still looks intentional.
|
||
return logoSvg("aegis", s);
|
||
}
|
||
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();
|
||
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 ? `<div class="fs">${esc(fiat)}</div>` : "";
|
||
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"><div>${esc(bal)}</div>${fiatLine}</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}` +
|
||
`<hr><div class="addhdr">↓ Import existing (BCH)</div>
|
||
<div class="coinrow" id="picker-import-bch">
|
||
${logoSvg("bch", 22)}
|
||
<div class="m"><div class="l">Import a BCH wallet</div><div class="s">Paste a BIP39 mnemonic + derivation path, or a WIF</div></div>
|
||
<div class="caret">›</div>
|
||
</div>`;
|
||
|
||
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 = `
|
||
<div style="width:min(94vw,380px);background:var(--panel,#12161e);border:1px solid var(--line,#2a2f38);border-radius:10px;padding:14px 14px 12px;box-shadow:0 10px 40px rgba(0,0,0,.4)">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
|
||
${logoSvg("bch", 22)}
|
||
<div style="font-weight:600;flex:1">Import a BCH wallet</div>
|
||
<button class="btn sm" id="imClose" type="button">✕</button>
|
||
</div>
|
||
<div class="hint" style="margin-bottom:10px">Key material stays in Theseus's vault (wallet-imports.enc). Aegis derives only the address and shows the balance — spending support ships next.</div>
|
||
<div class="field">
|
||
<div class="lbl">Network</div>
|
||
<div style="display:flex;gap:12px;font-size:12.5px;margin-top:4px">
|
||
<label><input type="radio" name="imNet" value="chipnet" checked> Chipnet testnet</label>
|
||
<label><input type="radio" name="imNet" value="mainnet"> Mainnet</label>
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="lbl">Source</div>
|
||
<div style="display:flex;gap:12px;font-size:12.5px;margin-top:4px">
|
||
<label><input type="radio" name="imKind" value="mnemonic" checked> BIP39 mnemonic</label>
|
||
<label><input type="radio" name="imKind" value="wif"> WIF</label>
|
||
</div>
|
||
</div>
|
||
<div class="field" id="imMnemonicField">
|
||
<div class="lbl">Mnemonic (12/24 words)</div>
|
||
<textarea id="imMnemonic" spellcheck="false" rows="2" style="font-family:ui-monospace,monospace;font-size:12px" placeholder="paste the seed phrase"></textarea>
|
||
<div class="lbl" style="margin-top:6px">Derivation path</div>
|
||
<input type="text" id="imPath" spellcheck="false" placeholder="m/44'/1'/0'/0/0">
|
||
<div class="hint">Default: <span class="mono">m/44'/1'/0'/0/0</span> for chipnet, <span class="mono">m/44'/145'/0'/0/0</span> for mainnet.</div>
|
||
</div>
|
||
<div class="field" id="imWifField" hidden>
|
||
<div class="lbl">WIF private key</div>
|
||
<input type="text" id="imWif" spellcheck="false" placeholder="Kx… or Lz… (base58check)">
|
||
</div>
|
||
<div class="field">
|
||
<div class="lbl">Label</div>
|
||
<input type="text" id="imLabel" placeholder="e.g. Sirius.x · BNS name buyer">
|
||
</div>
|
||
<div class="field">
|
||
<div class="lbl">Category</div>
|
||
<select id="imCategory" style="width:100%;padding:7px 9px;border-radius:7px;background:var(--panel);border:1px solid var(--line);color:var(--ink);font-size:13px">
|
||
<option value="operational">operational</option>
|
||
<option value="bns">bns</option>
|
||
<option value="bns-infra">bns-infra</option>
|
||
<option value="chipnet-test">chipnet-test</option>
|
||
<option value="hd-general">hd-general</option>
|
||
<option value="primary">primary</option>
|
||
</select>
|
||
</div>
|
||
<div class="msg err" id="imMsg" hidden style="margin-top:8px"></div>
|
||
<div class="actions" style="justify-content:flex-end;margin-top:10px">
|
||
<button class="btn" id="imCancel">Cancel</button>
|
||
<button class="btn primary" id="imGo">Import</button>
|
||
</div>
|
||
</div>`;
|
||
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 = `<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";
|
||
// 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
|
||
// <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.", "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 = `
|
||
<div class="gateform" style="margin-top:14px;display:flex;flex-direction:column;gap:8px;text-align:left">
|
||
<input type="password" id="gateSetupPw" placeholder="Master password (4+ chars)" autocomplete="new-password">
|
||
<input type="password" id="gateSetupPw2" placeholder="Confirm master password" autocomplete="new-password">
|
||
<textarea id="gateSetupMnemonic" placeholder="BIP39 mnemonic — optional, 12 or 24 words" rows="2" spellcheck="false" style="font-family:ui-monospace,monospace;font-size:12px"></textarea>
|
||
<div class="hint">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.</div>
|
||
<div class="actions" style="justify-content:center;margin-top:6px">
|
||
<button class="btn primary" id="gateSetupBtn">Create vault</button>
|
||
</div>
|
||
<div class="msg err" id="gateSetupMsg" hidden></div>
|
||
</div>`;
|
||
} else if (phase === "locked") {
|
||
form = `
|
||
<div class="gateform" style="margin-top:14px;display:flex;flex-direction:column;gap:8px;text-align:left">
|
||
<input type="password" id="gateUnlockPw" placeholder="Master password" autocomplete="current-password" autofocus>
|
||
<div class="actions" style="justify-content:center">
|
||
<button class="btn primary" id="gateUnlockBtn">Unlock</button>
|
||
</div>
|
||
<div class="msg err" id="gateUnlockMsg" hidden></div>
|
||
</div>`;
|
||
} else if (phase === "empty") {
|
||
form = `<div class="actions" style="justify-content:center;margin-top:16px"><button class="btn primary" id="gateAddWallet">+ Add your first wallet</button></div>`;
|
||
}
|
||
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>${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 = `<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`;
|
||
// 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: <b>${esc(fiatSkeleton() || "—")}</b>`;
|
||
return;
|
||
}
|
||
const noun = wallets.length === 1 ? "wallet" : "wallets";
|
||
el.hidden = false;
|
||
el.innerHTML = `Portfolio: <b>${esc(fmtFiat(total))}</b> 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 `<div class="tx" style="grid-template-columns:1fr auto auto;cursor:default">
|
||
<div><div>${esc(t.symbol)}${t.name ? ' <span class="hint">' + esc(t.name) + '</span>' : ""}</div><div class="hint mono">${esc(t.mint.slice(0, 10))}…${esc(t.mint.slice(-6))}</div></div>
|
||
<div class="amt2 in" style="align-self:center">${esc(bal)}</div>
|
||
<button class="btn sm" data-mint="${esc(t.mint)}" data-symbol="${esc(t.symbol)}" data-decimals="${dec}" style="align-self:center">Send</button>
|
||
</div>`;
|
||
}).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 = `<option value="">SOL — native</option>` + tokens.map((t) =>
|
||
`<option value="${esc(t.mint)}" data-symbol="${esc(t.symbol)}" data-decimals="${Number(t.decimals) || 0}">${esc(t.symbol)}${t.name ? " · " + esc(t.name) : ""}</option>`
|
||
).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 =
|
||
`<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 = ({
|
||
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 = `<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(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. <a class="link" data-tx="${esc(r.txid)}">${esc(r.txid.slice(0, 16))}…</a>`;
|
||
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 = `<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 = "";
|
||
$("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 =
|
||
`<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); }
|
||
});
|
||
|
||
// Family-picker helper used by both DGB and BTC. Prefix is "Dgb" or "Btc":
|
||
// the DOM IDs are #set<Prefix>Family + #set<Prefix>Path.
|
||
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) =>
|
||
`<option value="${esc(f.id)}" data-path="${esc(f.defaultAccountPath)}" ${f.id === currentId ? "selected" : ""}>${esc(f.label)}</option>`
|
||
).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 =
|
||
`<div class="lbl">Address</div><div class="mono">${esc(sel().address || "")}</div>` +
|
||
`<div class="lbl">Public key (uncompressed hex)</div><div class="mono">${esc(r.xpub || "")}</div>` +
|
||
(r.xprv ? `<div class="lbl">Private key (hex)</div><div class="mono" style="color:var(--danger)">${esc(r.xprv)}</div>` : "");
|
||
} catch (e) { $("ethRecovery").textContent = cleanErr(e); }
|
||
});
|
||
$("showSolKey").addEventListener("click", async () => {
|
||
try {
|
||
const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true });
|
||
$("solRecovery").innerHTML =
|
||
`<div class="lbl">Address (public key, base58)</div><div class="mono">${esc(r.xpub || "")}</div>` +
|
||
(r.xprv ? `<div class="lbl">Wallet seed (hex, 32 bytes)</div><div class="mono" style="color:var(--danger)">${esc(r.xprv)}</div>` : "");
|
||
} 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 = `<div class="hint">No dapps paired yet.</div>`; return; }
|
||
el.innerHTML = conns.map((c) => {
|
||
const label = c.dappName || "(pairing…)";
|
||
const iconHtml = c.dappIcon ? `<img src="${esc(c.dappIcon)}" style="width:18px;height:18px;border-radius:4px" onerror="this.hidden=true">` : "";
|
||
return `<div class="tx" style="grid-template-columns:auto 1fr auto;cursor:default;align-items:center">
|
||
<div>${iconHtml}</div>
|
||
<div><div>${esc(label)}</div><div class="hint mono">${esc((c.uri || "").slice(0, 46))}…</div></div>
|
||
<button class="btn sm" data-wcconn="${esc(c.id)}">Disconnect</button>
|
||
</div>`;
|
||
}).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 = `<div class="big">⚠</div><div>${esc(cleanErr(e))}</div>`; }
|
||
})();
|