feat(theseus/aegis): SVG coin logos, two-step coin/network picker, BCH Chipnet
- 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: <name> — 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.
This commit is contained in:
parent
5574641fb9
commit
a3810ef1eb
4 changed files with 248 additions and 99 deletions
|
|
@ -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[<id>]` 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,
|
||||
},
|
||||
"trx:mainnet": {
|
||||
chain: "trx", network: "mainnet",
|
||||
label: "Tron", short: "TRX", ticker: "TRX", decimals: 6,
|
||||
badge: "🔴", color: "#ff060a",
|
||||
purposePrefix: "bchwallet/trx/mainnet/",
|
||||
chipnet: {
|
||||
id: "chipnet", label: "Chipnet testnet", testnet: true,
|
||||
purposePrefix: "bchwallet/bch/chipnet/",
|
||||
startIndex: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
trx: {
|
||||
chain: "trx",
|
||||
label: "Tron",
|
||||
short: "TRX",
|
||||
ticker: "TRX",
|
||||
decimals: 6,
|
||||
color: "#ff060a",
|
||||
logo: "trx",
|
||||
supportsMessageSign: true,
|
||||
supportsPageInject: true, // window.tronWeb everywhere
|
||||
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,
|
||||
},
|
||||
},
|
||||
"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,
|
||||
},
|
||||
};
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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/<walletId>/…" 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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<body>
|
||||
<header>
|
||||
<div class="picker" id="pickerBtn">
|
||||
<div class="t"><span class="badge" id="hBadge">🛡</span><span class="lbl" id="hLabel">Aegis Wallet</span></div>
|
||||
<div class="t">
|
||||
<span class="badge" id="hBadge"></span>
|
||||
<span class="lbl" id="hLabel">Aegis Wallet</span>
|
||||
<span id="hNet"></span>
|
||||
</div>
|
||||
<div class="caret">▾</div>
|
||||
</div>
|
||||
<div id="drop" hidden></div>
|
||||
|
|
@ -196,8 +202,8 @@
|
|||
<input type="text" id="setPath" spellcheck="false" placeholder="m/44'/145'/0'">
|
||||
<div class="hint">Changing this switches to a different set of addresses under the same wallet seed.</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="lbl">Electrum servers (shared across BCH wallets, one per line)</div>
|
||||
<div class="field" id="bchServersRow">
|
||||
<div class="lbl">Electrum servers (shared across BCH mainnet wallets, one per line)</div>
|
||||
<textarea id="setServers" spellcheck="false"></textarea>
|
||||
<div class="hint" id="serverHint"></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Bitcoin Cash" style="vertical-align:middle;flex:none">
|
||||
<circle cx="16" cy="16" r="15.5" fill="#0ac18e" stroke="#0a9a72" stroke-width=".8"/>
|
||||
<text x="16" y="22.4" text-anchor="middle" font-family="Segoe UI,Arial,sans-serif" font-size="20" font-weight="800" fill="#fff">₿</text>
|
||||
</svg>`;
|
||||
}
|
||||
if (logo === "trx") {
|
||||
// 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 `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Tron" style="vertical-align:middle;flex:none">
|
||||
<circle cx="16" cy="16" r="15.5" fill="#ff060a" stroke="#c1050a" stroke-width=".8"/>
|
||||
<path d="M7.5 10.2 L24 12.5 L14.5 23.8 Z"
|
||||
fill="none" stroke="#fff" stroke-width="1.7" stroke-linejoin="round"/>
|
||||
<line x1="7.5" y1="10.2" x2="14.5" y2="23.8" stroke="#fff" stroke-width="1.7" stroke-linejoin="round"/>
|
||||
</svg>`;
|
||||
}
|
||||
// Fallback — an unknown-chain shield glyph, so a broken registry entry
|
||||
// shows up as clearly-wrong rather than blank.
|
||||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" style="vertical-align:middle;flex:none">
|
||||
<rect x="4" y="4" width="24" height="24" rx="6" fill="#7f8ba1"/>
|
||||
<text x="16" y="22" text-anchor="middle" fill="#fff" font-size="16" font-weight="700">?</text>
|
||||
</svg>`;
|
||||
}
|
||||
function testnetTag() { return `<span class="ttag">TEST</span>`; }
|
||||
|
||||
// Selected wallet convenience.
|
||||
const sel = () => state && state.selected;
|
||||
const chain = () => sel()?.chain || "";
|
||||
const decimals = () => sel()?.meta?.decimals || 8;
|
||||
const ticker = () => sel()?.meta?.ticker || "";
|
||||
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 `<div class="row ${on}" data-select="${esc(w.id)}"><div class="b">${esc(w.badge)}</div><div class="m"><div class="l">${esc(w.label)}</div><div class="s">${esc(w.short)} · ${w.phase === "ready" ? esc(w.address || "") : esc(w.phase)}</div></div><div class="v">${esc(bal)}</div></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">${esc(bal)}</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
const chainList = chains.map((c) =>
|
||||
`<div class="netchoice" data-add="${esc(c.chain + ":" + c.network)}">${esc(c.badge)} ${esc(c.label)}</div>`
|
||||
).join("");
|
||||
d.innerHTML = rows + `<hr><div class="add" id="addToggle">+ Add wallet</div><div class="netgroup" id="netgroup">${chainList}</div>`;
|
||||
// "Add wallet" is a two-step flyout: first show coins, then that coin's
|
||||
// networks. Nothing is created until the user clicks a specific network.
|
||||
const coinRows = coins.map((c) => {
|
||||
const testCount = c.networks.filter((n) => n.testnet).length;
|
||||
const sub = c.networks.length > 1
|
||||
? c.networks.map((n) => n.label).join(" · ")
|
||||
: c.networks[0].label;
|
||||
return `<div class="coinrow" data-coin="${esc(c.chain)}">
|
||||
${logoSvg(c.logo, 22)}
|
||||
<div class="m"><div class="l">${esc(c.label)}</div><div class="s">${esc(sub)}</div></div>
|
||||
<div class="caret">▸</div>
|
||||
</div>
|
||||
<div class="netgroup" id="netgroup-${esc(c.chain)}" hidden>
|
||||
${c.networks.map((n) => `<div class="netchoice" data-add="${esc(c.chain + ":" + n.id)}">
|
||||
${esc(n.label)}${n.testnet ? " " + testnetTag() : ""}
|
||||
</div>`).join("")}
|
||||
</div>`;
|
||||
}).join("");
|
||||
d.innerHTML =
|
||||
rowsHtml +
|
||||
`<hr><div class="addhdr">+ Add wallet</div>${coinRows}`;
|
||||
|
||||
d.querySelectorAll("[data-select]").forEach((r) => r.addEventListener("click", async () => {
|
||||
d.hidden = true;
|
||||
try { state = await S.invoke("selectWallet", { id: r.dataset.select }); settingsFilled = false; render(); }
|
||||
catch (e) { showErr(cleanErr(e)); }
|
||||
}));
|
||||
$("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
|
||||
// <wallet label> <coin · network + optional TEST tag>
|
||||
$("hBadge").innerHTML = s?.meta?.logo ? logoSvg(s.meta.logo, 22) : logoSvg(null, 22);
|
||||
$("hLabel").textContent = s?.label || "Aegis Wallet";
|
||||
$("hNet").innerHTML = s?.meta
|
||||
? `${esc(s.meta.coinLabel)} · ${esc(s.meta.networkLabel)}${s.meta.testnet ? " " + testnetTag() : ""}`
|
||||
: "";
|
||||
if (!ready) {
|
||||
const copy = {
|
||||
locked: ["🔒", "Unlock your password vault to open the wallet.", "Settings › Passwords. Aegis derives its keys from the vault seed, so there is nothing separate to unlock."],
|
||||
nosetup: ["🗝", "Set up a password vault to create your wallet.", "Settings › Passwords › Set up. Use a recovery phrase there and every wallet in Aegis can be recreated from it on any machine."],
|
||||
error: ["⚠", "This wallet could not start.", s?.error || ""],
|
||||
empty: ["🧩", "No wallets yet.", "Open the wallet picker at the top and pick a 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 = `<div class="big">${copy[0]}</div><div><b>${esc(copy[1])}</b></div><div class="hint" style="margin-top:8px">${esc(copy[2])}</div>`;
|
||||
}
|
||||
// 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() {
|
|||
`<button data-u="big" class="${unit === "big" ? "on" : ""}" type="button">${esc(big)}</button>` +
|
||||
`<button data-u="small" class="${unit === "small" ? "on" : ""}" type="button">${esc(small)}</button>`;
|
||||
$("unitPicker").querySelectorAll("button").forEach((b) => b.addEventListener("click", () => setUnit(b.dataset.u)));
|
||||
$("sendTo").placeholder = chain() === "bch" ? "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,14 +403,15 @@ $("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 });
|
||||
}
|
||||
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; }
|
||||
|
|
@ -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 += `<div class="lbl">Account private key (xprv)</div><div class="mono" style="color:var(--danger)">${esc(r.xprv)}</div>`;
|
||||
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 ------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue