From a3810ef1ebd5432be9cf03fd3f9b1d4c1bb04437 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Mon, 7 Sep 2026 01:07:31 +0200 Subject: [PATCH] feat(theseus/aegis): SVG coin logos, two-step coin/network picker, BCH Chipnet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Inline SVG logos for BCH (green disc + ₿) and TRX (red disc + geometric T) replace the 🟨/🔴/🔵 emoji in the sidebar header and wallet picker rows. The approval overlay stays text-only ("Wallet: — BCH · Mainnet") because that surface renders plain rows, not HTML. - Wallet picker's "Add wallet" is now two-step: click a coin to expand its networks, then click a network to create the wallet. The flat list is gone. - BCH Chipnet is a real chain option now: bchtest cashaddr prefix, m/44'/1'/0' derivation (BIP44 testnet coin type), bundled Chipnet electrum defaults, chipnet.imaginary.cash explorer, tbch.googol.cash faucet link in Receive. The shared electrum-servers setting stays mainnet-only in this rev; Chipnet uses adapter-embedded defaults. - lib/chain-bch.js gained a BCH_NETWORKS table so mainnet vs chipnet differences (prefix, path, servers, explorer, faucet) live in one place. - Registry is grouped by coin ({networks:{…}}) instead of a flat chain:network map — snapshot exposes coins[] for the panel and adds coinLabel/networkLabel/testnet fields per wallet. - Testnet wallets get a small "TEST" tag next to the network name so the user can never mistake a chipnet or Nile balance for real money. Legacy BCH mainnet index 0 derivation unchanged (bchwallet/mainnet/0 → m/44'/145'/0' → bitcoincash prefix); the network parameter defaults to "mainnet" and BCH_NETWORKS.mainnet reproduces the pre-change constants. --- bundled-addons/bchwallet/index.js | 112 ++++++++++++------ bundled-addons/bchwallet/lib/chain-bch.js | 66 ++++++++--- bundled-addons/bchwallet/panel.html | 38 ++++--- bundled-addons/bchwallet/panel.js | 131 ++++++++++++++++------ 4 files changed, 248 insertions(+), 99 deletions(-) diff --git a/bundled-addons/bchwallet/index.js b/bundled-addons/bchwallet/index.js index 6f3f791..0b67ccb 100644 --- a/bundled-addons/bchwallet/index.js +++ b/bundled-addons/bchwallet/index.js @@ -57,39 +57,75 @@ function bchServerList(api) { } // ---- chain registry -------------------------------------------------------- -// Chain-side facts that don't depend on state. Adding a chain = extending this -// map + writing an adapter that constructs a wallet from a 32-byte root. -const CHAIN_REGISTRY = { - "bch:mainnet": { - chain: "bch", network: "mainnet", - label: "Bitcoin Cash", short: "BCH", ticker: "BCH", decimals: 8, - badge: "🟨", color: "#0ac18e", - purposePrefix: "bchwallet/bch/", // legacy wallet uses the flat "bchwallet/mainnet/0" instead - startIndex: 1, // 0 is reserved for the legacy wallet +// Two-level structure so the panel can present coin-then-network as separate +// picks. `coin` fields are chain-wide; `networks[]` fields override or +// add to them per-network. `logo` is the SVG key panel.js draws from. +const COINS = { + bch: { + chain: "bch", + label: "Bitcoin Cash", + short: "BCH", + ticker: "BCH", + decimals: 8, + color: "#0ac18e", + logo: "bch", supportsMessageSign: true, supportsPageInject: true, // window.bitcoincash on *.x pages + networks: { + mainnet: { + id: "mainnet", label: "Mainnet", testnet: false, + // Legacy default wallet uses the flat "bchwallet/mainnet/0" purpose; + // new BCH mainnet sub-accounts start at index 1 under the /bch/ prefix. + purposePrefix: "bchwallet/bch/", + startIndex: 1, + }, + chipnet: { + id: "chipnet", label: "Chipnet testnet", testnet: true, + purposePrefix: "bchwallet/bch/chipnet/", + startIndex: 0, + }, + }, }, - "trx:mainnet": { - chain: "trx", network: "mainnet", - label: "Tron", short: "TRX", ticker: "TRX", decimals: 6, - badge: "🔴", color: "#ff060a", - purposePrefix: "bchwallet/trx/mainnet/", - startIndex: 0, + trx: { + chain: "trx", + label: "Tron", + short: "TRX", + ticker: "TRX", + decimals: 6, + color: "#ff060a", + logo: "trx", supportsMessageSign: true, supportsPageInject: true, // window.tronWeb everywhere - }, - "trx:nile": { - chain: "trx", network: "nile", - label: "Tron Nile testnet", short: "TRX (Nile)", ticker: "TRX", decimals: 6, - badge: "🔵", color: "#4d9dff", - purposePrefix: "bchwallet/trx/nile/", - startIndex: 0, - supportsMessageSign: true, - supportsPageInject: true, + networks: { + mainnet: { + id: "mainnet", label: "Mainnet", testnet: false, + purposePrefix: "bchwallet/trx/mainnet/", startIndex: 0, + }, + nile: { + id: "nile", label: "Nile testnet", testnet: true, + purposePrefix: "bchwallet/trx/nile/", startIndex: 0, + }, + }, }, }; function chainKey(chain, network) { return `${chain}:${network}`; } -function chainMeta(chain, network) { return CHAIN_REGISTRY[chainKey(chain, network)] || null; } +function chainMeta(chain, network) { + const c = COINS[chain]; const n = c && c.networks[network]; + if (!c || !n) return null; + return { + chain: c.chain, network: n.id, + label: c.label + " · " + n.label, short: c.short, ticker: c.ticker, decimals: c.decimals, + color: c.color, logo: c.logo, coinLabel: c.label, networkLabel: n.label, testnet: !!n.testnet, + purposePrefix: n.purposePrefix, startIndex: n.startIndex, + supportsMessageSign: !!c.supportsMessageSign, supportsPageInject: !!c.supportsPageInject, + }; +} +function coinsForPanel() { + return Object.values(COINS).map((c) => ({ + chain: c.chain, label: c.label, short: c.short, ticker: c.ticker, color: c.color, logo: c.logo, decimals: c.decimals, + networks: Object.values(c.networks).map((n) => ({ id: n.id, label: n.label, testnet: !!n.testnet })), + })); +} // ---- wallet list ------------------------------------------------------------ @@ -186,12 +222,16 @@ async function mountWallet(entry) { try { let adapter; if (entry.chain === "bch") { + // Mainnet still honors the user-set custom electrum list; chipnet uses + // adapter-embedded defaults (no per-network custom list in this rev). + const servers = entry.network === "mainnet" ? bchServerList(c.api) : undefined; adapter = new c.d.bchAdapter.BchWallet(root, { walletId: entry.id, storage: c.api.storage, log: (...a) => c.api.log(`[${entry.id}]`, ...a), onChange: () => emitStateForWallet(entry.id), - servers: bchServerList(c.api), + network: entry.network, + servers, accountPath: entry.accountPath, }); } else if (entry.chain === "trx") { @@ -255,8 +295,9 @@ function walletSummary(w) { const snap = rt && rt.adapter ? rt.adapter.snapshot() : null; return { id: w.id, label: w.label, chain: w.chain, network: w.network, isDefault: !!w.isDefault, isLegacy: !!w.isLegacy, - badge: meta?.badge || "🧩", color: meta?.color || "#888", ticker: meta?.ticker || "?", - short: meta?.short || w.chain, decimals: meta?.decimals || 8, + logo: meta?.logo || null, color: meta?.color || "#888", + coinLabel: meta?.coinLabel || w.chain, networkLabel: meta?.networkLabel || w.network, testnet: !!meta?.testnet, + ticker: meta?.ticker || "?", short: meta?.short || w.chain, decimals: meta?.decimals || 8, address: snap?.address || null, balance: snap?.balance || { confirmed: 0, unconfirmed: 0 }, phase: rt?.phase || "locked", @@ -276,7 +317,10 @@ function snapshotForSelected() { chain: entry?.chain, network: entry?.network, isLegacy: !!entry?.isLegacy, - meta: meta ? { badge: meta.badge, color: meta.color, short: meta.short, ticker: meta.ticker, decimals: meta.decimals } : null, + meta: meta ? { + logo: meta.logo, color: meta.color, short: meta.short, ticker: meta.ticker, decimals: meta.decimals, + coinLabel: meta.coinLabel, networkLabel: meta.networkLabel, testnet: meta.testnet, + } : null, supportsMessageSign: !!meta?.supportsMessageSign, phase: rt?.phase || "locked", error: rt?.error || null, @@ -295,9 +339,7 @@ function fullState() { list: bchServerList(ctx.api), custom: Array.isArray(ctx.api.storage.get("servers", null)), }, - chains: Object.entries(CHAIN_REGISTRY).map(([k, m]) => ({ - key: k, chain: m.chain, network: m.network, label: m.label, short: m.short, ticker: m.ticker, badge: m.badge, decimals: m.decimals, - })), + coins: coinsForPanel(), }; } @@ -456,7 +498,7 @@ function registerPanelMessages(api) { { label: "Amount", value: `${fmtValue(d.recipients[0].value, meta.decimals)} ${meta.ticker}`, strong: true }, { label: "Fee", value: rt.entry.chain === "bch" ? `${plan.fee} sat (${plan.feeRate} sat/B)` : `${fmtValue(plan.fee, meta.decimals)} ${meta.ticker}` }, { label: "Total", value: `${fmtValue(d.total, meta.decimals)} ${meta.ticker}` }, - { label: "Wallet", value: `${meta.badge} ${rt.entry.label}` }, + { label: "Wallet", value: `${rt.entry.label} — ${meta.coinLabel} · ${meta.networkLabel}` }, ]; const pick = await api.approvalModal({ title: `Send ${meta.ticker}?`, @@ -662,7 +704,7 @@ function registerPageMessages(api) { rows: [ { label: "Address", value: snap.address, mono: true }, { label: "Network", value: snap.network === "nile" ? "Nile testnet" : "Tron mainnet" }, - { label: "Wallet", value: `🔴 ${rt.entry.label}` }, + { label: "Wallet", value: `${rt.entry.label} — Tron · ${snap.network === "nile" ? "Nile testnet" : "Mainnet"}` }, ], actions: [{ id: "allow", label: "Connect", primary: true }], checkbox: { id: "always", label: "Always allow this site to see this address" }, @@ -706,7 +748,7 @@ function registerPageMessages(api) { rows.splice(1, 0, { label: "To", value: to, mono: true }, { label: "Amount", value: `${fmtTrx(amount)} TRX`, strong: true }); } catch {} } - rows.push({ label: "Wallet", value: `🔴 ${rt.entry.label} (${rt.adapter.snapshot().network})` }); + rows.push({ label: "Wallet", value: `${rt.entry.label} — Tron · ${rt.adapter.snapshot().network === "nile" ? "Nile testnet" : "Mainnet"}` }); const pick = await api.approvalModal({ title: "Sign a Tron transaction?", origin, diff --git a/bundled-addons/bchwallet/lib/chain-bch.js b/bundled-addons/bchwallet/lib/chain-bch.js index 89b3dae..cdddf39 100644 --- a/bundled-addons/bchwallet/lib/chain-bch.js +++ b/bundled-addons/bchwallet/lib/chain-bch.js @@ -6,15 +6,42 @@ // Deps are handed in so index.js loads @noble/* once and shares them across // every wallet, rather than each adapter dynamic-importing on its own. +const BCH_NETWORKS = { + mainnet: { + id: "mainnet", + label: "Mainnet", + prefix: "bitcoincash", + defaultAccountPath: "m/44'/145'/0'", + explorerTx: "https://blockchair.com/bitcoin-cash/transaction/", + explorerAddr: "https://blockchair.com/bitcoin-cash/address/", + defaultServers: [ + "wss://bch.imaginary.cash:50004", + "wss://cashnode.bch.ninja:50004", + "wss://electroncash.dk:50004", + "wss://fulcrum.jettscythe.xyz:50004", + ], + faucet: null, + }, + chipnet: { + id: "chipnet", + label: "Chipnet testnet", + prefix: "bchtest", + // BIP44 testnet coin type is 1 across every chain; the account is still 0. + defaultAccountPath: "m/44'/1'/0'", + explorerTx: "https://chipnet.imaginary.cash/tx/", + explorerAddr: "https://chipnet.imaginary.cash/address/", + defaultServers: [ + "wss://chipnet.imaginary.cash:50004", + "wss://chipnet.bch.ninja:50004", + ], + faucet: "https://tbch.googol.cash/", + }, +}; + module.exports = function makeBchAdapter({ HDKey, secp256k1, sha256, ripemd160, cashaddr, keysLib, tx, electrum, WebSocket, }) { - const NETWORK = "mainnet"; - const PREFIX = "bitcoincash"; - const DEFAULT_ACCOUNT_PATH = "m/44'/145'/0'"; - const EXPLORER_TX = "https://blockchair.com/bitcoin-cash/transaction/"; - const EXPLORER_ADDR = "https://blockchair.com/bitcoin-cash/address/"; // storage is the FULL api.storage. keyPrefix scopes every read/write under // "wallets//…" so multiple BCH wallets don't stomp each other. @@ -29,20 +56,27 @@ module.exports = function makeBchAdapter({ class BchWallet { constructor(root32, { walletId, storage, log = () => {}, onChange = () => {}, servers, - accountPath = DEFAULT_ACCOUNT_PATH, + network = "mainnet", accountPath, } = {}) { if (!walletId) throw new Error("chain-bch: walletId required"); + const net = BCH_NETWORKS[network]; + if (!net) throw new Error(`chain-bch: unknown network ${network}`); this.walletId = walletId; this.chain = "bch"; - this.network = NETWORK; + this.network = net.id; + this._net = net; this.log = log; this.onChange = onChange; this.storage = scopedStorage(storage, `wallets/${walletId}/`); - this._servers = Array.isArray(servers) && servers.length ? servers : []; - this._accountPath = /^m(\/\d+'?)+$/.test(accountPath) ? accountPath : DEFAULT_ACCOUNT_PATH; + // Servers: caller-provided override → user-set custom list (handled by + // index.js already, this is a fallback path) → the network's built-in + // defaults so a wallet always has somewhere to connect. + this._servers = Array.isArray(servers) && servers.length ? servers : net.defaultServers.slice(); + const wantPath = accountPath || net.defaultAccountPath; + this._accountPath = /^m(\/\d+'?)+$/.test(wantPath) ? wantPath : net.defaultAccountPath; this._client = new electrum.Client(this._servers); this._client.onServer = () => this._emit(); - this._keys = new keysLib.WalletKeys(root32, this._accountPath, PREFIX); + this._keys = new keysLib.WalletKeys(root32, this._accountPath, net.prefix); this._root = new Uint8Array(root32); const walletFactory = require("./wallet.js"); this._wallet = walletFactory({ @@ -54,7 +88,7 @@ module.exports = function makeBchAdapter({ } setServers(list) { - this._servers = Array.isArray(list) && list.length ? list : []; + this._servers = Array.isArray(list) && list.length ? list : this._net.defaultServers.slice(); this._client.setServers(this._servers); } @@ -64,7 +98,7 @@ module.exports = function makeBchAdapter({ const w = this._wallet.snapshot(); return { chain: "bch", - network: NETWORK, + network: this._net.id, ticker: "BCH", decimals: 8, address: w.address, @@ -79,9 +113,9 @@ module.exports = function makeBchAdapter({ servers: this._servers, accountPath: this._accountPath, xpub: this._keys.xpub, - explorerTx: EXPLORER_TX, - explorerAddr: EXPLORER_ADDR, - faucet: null, + explorerTx: this._net.explorerTx, + explorerAddr: this._net.explorerAddr, + faucet: this._net.faucet, }; } @@ -118,5 +152,5 @@ module.exports = function makeBchAdapter({ } } - return { BchWallet, DEFAULT_ACCOUNT_PATH, PREFIX, EXPLORER_TX, EXPLORER_ADDR }; + return { BchWallet, BCH_NETWORKS }; }; diff --git a/bundled-addons/bchwallet/panel.html b/bundled-addons/bchwallet/panel.html index cd875e6..d90b0f9 100644 --- a/bundled-addons/bchwallet/panel.html +++ b/bundled-addons/bchwallet/panel.html @@ -33,23 +33,25 @@ .bal .sub { color: var(--dim); font-size: 11.5px; display: flex; justify-content: space-between; gap: 8px; } .bal .sub .netlbl { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #drop { position: absolute; left: 10px; right: 10px; top: 100%; background: var(--panel); border: 1px solid var(--line); - border-radius: 10px; box-shadow: 0 8px 26px rgba(0,0,0,.3); z-index: 20; padding: 4px; margin-top: 4px; } + border-radius: 10px; box-shadow: 0 8px 26px rgba(0,0,0,.3); z-index: 20; padding: 4px; margin-top: 4px; max-height: 60vh; overflow-y: auto; } #drop[hidden] { display: none; } - #drop .row { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 7px; cursor: pointer; } - #drop .row:hover { background: rgba(255,255,255,.05); } - #drop .row .b { font-size: 15px; line-height: 1; } - #drop .row .m { flex: 1; min-width: 0; } - #drop .row .m .l { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - #drop .row .m .s { color: var(--dim); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + #drop .row, #drop .coinrow { display: flex; align-items: center; gap: 9px; padding: 7px 8px; border-radius: 7px; cursor: pointer; } + #drop .row:hover, #drop .coinrow:hover { background: rgba(255,255,255,.05); } + #drop .row .m, #drop .coinrow .m { flex: 1; min-width: 0; } + #drop .row .m .l, #drop .coinrow .m .l { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + #drop .row .m .s, #drop .coinrow .m .s { color: var(--dim); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #drop .row .v { color: var(--mut); font-size: 12px; font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; } #drop .row.on { background: rgba(214,255,61,.10); } - #drop hr { border: 0; border-top: 1px solid var(--line); margin: 4px 0; } - #drop .add { padding: 7px 8px; color: var(--acid); font-weight: 600; cursor: pointer; border-radius: 7px; } - #drop .add:hover { background: rgba(214,255,61,.10); } - #drop .netgroup { display: none; padding: 4px; border-radius: 7px; margin-top: 2px; } - #drop .netgroup.on { display: block; background: rgba(255,255,255,.03); } + #drop .coinrow .caret { color: var(--dim); font-size: 11px; } + #drop hr { border: 0; border-top: 1px solid var(--line); margin: 6px 0; } + #drop .addhdr { padding: 5px 8px 3px; color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; } + #drop .netgroup { padding: 2px 4px 6px 40px; } + #drop .netgroup[hidden] { display: none; } #drop .netchoice { padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 12.5px; color: var(--mut); } - #drop .netchoice:hover { background: rgba(255,255,255,.05); color: var(--ink); } + #drop .netchoice:hover { background: rgba(255,255,255,.06); color: var(--ink); } + .ttag { display: inline-block; font-size: 9.5px; letter-spacing: .06em; padding: 1px 5px; border-radius: 3px; + background: rgba(224,179,65,.18); color: #e0b341; font-weight: 700; vertical-align: middle; margin-left: 2px; } + #hNet { color: var(--dim); font-size: 11px; margin-left: 4px; font-weight: 500; } nav { display: flex; border-bottom: 1px solid var(--line); background: var(--panel); } nav button { flex: 1; padding: 9px 0 8px; border: 0; background: transparent; color: var(--mut); cursor: pointer; font: inherit; font-size: 12.5px; border-bottom: 2px solid transparent; } @@ -112,7 +114,11 @@
-
🛡Aegis Wallet
+
+ + Aegis Wallet + +
@@ -196,8 +202,8 @@
Changing this switches to a different set of addresses under the same wallet seed.
-
-
Electrum servers (shared across BCH wallets, one per line)
+
+
Electrum servers (shared across BCH mainnet wallets, one per line)
diff --git a/bundled-addons/bchwallet/panel.js b/bundled-addons/bchwallet/panel.js index 7793ed9..c618191 100644 --- a/bundled-addons/bchwallet/panel.js +++ b/bundled-addons/bchwallet/panel.js @@ -9,21 +9,53 @@ let unit = null; // "big" | "small" — chain-dependent let sendMax = false; let planTimer = null; let lastPlan = null; -let settingsFilled = false; // when true, we don't overwrite user edits +let settingsFilled = false; const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]); const hostOf = (url) => { try { return new URL(url).host || url; } catch { return url; } }; const openUrl = (url) => S.invoke("openUrl", { url }).catch(() => {}); const cleanErr = (e) => String(e?.message || e).replace(/^Error invoking remote method '[^']+': Error: /, ""); +// ---- coin logos ------------------------------------------------------------ +// Inline SVGs so the header, wallet picker and settings surface all render +// the same mark. Sized by the container via width/height attributes. +function logoSvg(logo, size) { + const s = size || 20; + if (logo === "bch") { + // Green disc with the Bitcoin sign — the mark bchcommunity + Bitcoin.com + // both use in reduced form. Ring wraps a solid disc so the mark reads + // as a coin, not a flat glyph. + return ` + + + `; + } + if (logo === "trx") { + // Red disc with the Tron mark: an angular triangle-in-a-T. Simplified + // from the official geometric wordless logo; still reads as "Tron" at + // small sizes because of the tri-line arrangement. + return ` + + + + `; + } + // Fallback — an unknown-chain shield glyph, so a broken registry entry + // shows up as clearly-wrong rather than blank. + return ` + + ? + `; +} +function testnetTag() { return `TEST`; } + // Selected wallet convenience. const sel = () => state && state.selected; const chain = () => sel()?.chain || ""; const decimals = () => sel()?.meta?.decimals || 8; const ticker = () => sel()?.meta?.ticker || ""; -const badgeOf = (chainKey) => (state?.chains || []).find((c) => `${c.chain}:${c.network}` === chainKey)?.badge || "🧩"; -// Amount formatting: n_units -> string trimmed to the coin's precision. function fmtBig(units, dec) { const d = dec != null ? dec : decimals(); const s = (Number(units || 0) / Math.pow(10, d)).toFixed(d); @@ -44,7 +76,7 @@ function showTab(name) { if (name === "send") applyUnitPicker(); } -// ---- wallet picker --------------------------------------------------------- +// ---- wallet picker (two-step add) ------------------------------------------ $("pickerBtn").addEventListener("click", () => { const d = $("drop"); @@ -57,25 +89,56 @@ document.addEventListener("click", (e) => { if (e.target.closest("#drop") || e.target.closest("#pickerBtn")) return; d.hidden = true; }); + function fillPicker() { const d = $("drop"); const wallets = state?.wallets || []; - const chains = state?.chains || []; - const rows = wallets.map((w) => { + 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 : "—"; - return `
${esc(w.badge)}
${esc(w.label)}
${esc(w.short)} · ${w.phase === "ready" ? esc(w.address || "") : esc(w.phase)}
${esc(bal)}
`; + const sub = `${esc(w.coinLabel)} · ${esc(w.networkLabel)}${w.testnet ? " " + testnetTag() : ""}`; + return `
+ ${logoSvg(w.logo, 22)} +
${esc(w.label)}
${sub}
+
${esc(bal)}
+
`; }).join(""); - const chainList = chains.map((c) => - `
${esc(c.badge)} ${esc(c.label)}
` - ).join(""); - d.innerHTML = rows + `
+ Add wallet
${chainList}
`; + // "Add wallet" is a two-step flyout: first show coins, then that coin's + // networks. Nothing is created until the user clicks a specific network. + const coinRows = coins.map((c) => { + const testCount = c.networks.filter((n) => n.testnet).length; + const sub = c.networks.length > 1 + ? c.networks.map((n) => n.label).join(" · ") + : c.networks[0].label; + return `
+ ${logoSvg(c.logo, 22)} +
${esc(c.label)}
${esc(sub)}
+
+
+ `; + }).join(""); + d.innerHTML = + rowsHtml + + `
+ Add wallet
${coinRows}`; + 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)); } })); - $("addToggle").addEventListener("click", () => $("netgroup").classList.toggle("on")); + 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; @@ -98,19 +161,22 @@ function render() { $("tabs").hidden = !ready; const gate = $("gate"); gate.hidden = ready; - // Header - $("hBadge").textContent = s?.meta?.badge || "🛡"; + // Header: replace the badge slot with the coin's SVG and show + // + $("hBadge").innerHTML = s?.meta?.logo ? logoSvg(s.meta.logo, 22) : logoSvg(null, 22); $("hLabel").textContent = s?.label || "Aegis Wallet"; + $("hNet").innerHTML = s?.meta + ? `${esc(s.meta.coinLabel)} · ${esc(s.meta.networkLabel)}${s.meta.testnet ? " " + testnetTag() : ""}` + : ""; if (!ready) { const copy = { locked: ["🔒", "Unlock your password vault to open the wallet.", "Settings › Passwords. Aegis derives its keys from the vault seed, so there is nothing separate to unlock."], nosetup: ["🗝", "Set up a password vault to create your wallet.", "Settings › Passwords › Set up. Use a recovery phrase there and every wallet in Aegis can be recreated from it on any machine."], error: ["⚠", "This wallet could not start.", s?.error || ""], - empty: ["🧩", "No wallets yet.", "Open the wallet picker at the top and pick a chain to create one."], + empty: ["🧩", "No wallets yet.", "Open the wallet picker at the top and pick a coin, then a network to create one."], }[s?.phase || "locked"] || ["…", "Starting…", ""]; gate.innerHTML = `
${copy[0]}
${esc(copy[1])}
${esc(copy[2])}
`; } - // Balance line 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 || "")); @@ -123,22 +189,16 @@ function render() { $("balMain").textContent = "—"; $("balTicker").textContent = ""; } if (!ready) return; - // Chain-specific header adjustments - document.querySelector("nav [data-tab='settings']").hidden = false; - // Receive tab const addr = s.address || ""; if ($("addr").textContent !== addr) { $("addr").textContent = addr; - drawQr(chain() === "bch" ? "bitcoincash:" + addr.replace(/^bitcoincash:/, "") : "tron:" + 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; - // Send tab: input placeholder + unit picker applyUnitPicker(); - // Fee slider only meaningful for BCH $("feeField").hidden = chain() !== "bch"; - // History renderHistory(); } @@ -150,7 +210,9 @@ function applyUnitPicker() { `` + ``; $("unitPicker").querySelectorAll("button").forEach((b) => b.addEventListener("click", () => setUnit(b.dataset.u))); - $("sendTo").placeholder = chain() === "bch" ? "bitcoincash:q… or legacy 1…" : "T… (base58check, 34 chars)"; + $("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) { @@ -196,7 +258,7 @@ function renderHistory() { } function shortAddr(a) { if (!a) return ""; - const s = String(a).replace(/^bitcoincash:/, ""); + const s = String(a).replace(/^bitcoincash:|^bchtest:/, ""); return esc(s.slice(0, 10)) + "…" + esc(s.slice(-4)); } @@ -303,7 +365,12 @@ function fillSettings() { $("setServers").value = (state.bchServers?.list || []).join("\n"); settingsFilled = true; } - $("serverHint").textContent = (state.bchServers?.custom ? "Custom list." : "Bundled defaults.") + (s.server ? " Connected to " + hostOf(s.server) + "." : " Not connected."); + // Chipnet uses its own bundled defaults; the shared electrum-servers + // list only applies to mainnet wallets. + $("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 || ""); @@ -336,13 +403,14 @@ $("applySettings").addEventListener("click", async () => { try { const path = $("setPath").value.trim(); const servers = $("setServers").value.split(/\n+/).map((s) => s.trim()).filter(Boolean); - // Apply path (per-wallet) and servers (shared) separately. if (path && path !== (sel().accountPath || "")) { state = await S.invoke("setAccountPath", { id: state.selectedWalletId, accountPath: path }); } - const currentJoined = (state.bchServers?.list || []).join(); - if (state.bchServers?.custom || servers.join() !== currentJoined) { - state = await S.invoke("setBchServers", { servers }); + 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"); @@ -360,7 +428,7 @@ $("renameBtn").addEventListener("click", async () => { }); $("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 chain.`)) 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; } }); @@ -377,7 +445,6 @@ function recoveryHtml(r) { if (r.xprv) h += `
Account private key (xprv)
${esc(r.xprv)}
`; return h; } -// Wipe a revealed key when the user leaves the Settings tab. document.querySelectorAll("nav button").forEach((b) => b.addEventListener("click", () => { if (b.dataset.tab !== "settings") $("recovery").innerHTML = ""; })); // ---- boot ------------------------------------------------------------------