theseus/bundled-addons/aegis/panel.js

5048 lines
259 KiB
JavaScript
Raw Normal View History

feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
// 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;
feat(aegis): 0.12.0 — open the wallet full screen, sidebar becomes the rail The 380px panel is the wrong shape for anything that needs room. This opens the wallet as a full Theseus tab, with the sidebar's own furniture — identity, network, balance, section nav, wallet list — laid out as a left rail and the tab body given to the selected section. It is the SAME panel.html, loaded with ?surface=web. No second wallet, no second copy of 4,600 lines to drift apart. That works because an add-on's own tab is handed a window.silentmode with the same invoke/on surface as the sidebar, and addon-msg dispatches it as from:"panel" with the add-on identity derived from the file:// sender — so every existing handler, including the panel-only ones, works there untouched. The whole change is a CSS grid behind one attribute plus a chip to open it. Details that needed care: - The QR is drag-sized against a 380px panel and the size is remembered. Given a 720px column it filled the page, so it is capped on this surface only; the stored sidebar preference is left exactly as the user set it. - #drop (the coin picker sheet) is fixed-position and sized for the panel; it is pinned to the rail instead of covering the window. - Content columns are capped at 720px so forms and lists keep the measure the sidebar already tuned, rather than stretching across a monitor. - Under 900px the grid falls back to the stacked layout, so a narrow window degrades to what the sidebar already does. - The "open full screen" chip hides itself on the full-screen surface, so it cannot open a second copy of itself. Everything is scoped to [data-surface="web"], so the sidebar is byte-for-byte unchanged. Verified both surfaces, the narrow fallback (by exercising the real media rule) and zero horizontal overflow. The aegis.x/app URL still needs a main.js route in Theseus; this ships the destination over the add-on channel first.
2026-09-27 20:17:08 +02:00
// Which surface is this? The sidebar panel and the full-screen tab are the
// SAME file — an add-on's own tab is handed a window.silentmode with the
// same invoke/on surface, and main dispatches it as from:"panel", so
// everything below works identically in both. The flag only drives layout.
const SURFACE = (function () {
try { return new URL(location.href).searchParams.get("surface") || "panel"; }
catch (e) { return "panel"; }
})();
if (SURFACE === "web") document.documentElement.dataset.surface = "web";
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
let state = null; // full state (all wallets + selected)
let tab = "receive";
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
let unit = null; // "big" | "small" — chain-dependent
let sendMax = false;
let planTimer = null;
// 0.8.0: mode toggles for the Send + Receive tabs. "send"/"receive" is
// the normal flow; "consolidate" swaps the tab body for the inline
// batch-consolidate picker. Session-scoped — resets to normal on reload.
let sendMode = "send";
let rcvMode = "receive";
// Cached inline consolidate render tokens — rebuilt on demand, reused
// across paints while the mode is active.
let consolidateInlineHost = null;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
let lastPlan = null;
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.
2026-09-07 01:07:31 +02:00
let settingsFilled = false;
feat(theseus/aegis): SPL token support (view balances + send) SPL tokens now show up in the Solana wallet — balances on the Receive card, an asset picker on Send that flips the amount input into the token's own units. Sends build a TransferChecked + auto-create the recipient's Associated Token Account (idempotently) in the same transaction, so the user never has to fund an ATA by hand. - lib/sol-spl.js: SPL primitives that don't need @solana/web3.js. TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, findProgramAddress (PDA loop backed by an ed25519 is-on-curve check via @noble Point.fromBytes), associatedTokenAddress (matches the spl-token JS seed layout: [owner, tokenProgram, mint]), transferCheckedInstruction (discriminator 12, u64 amount, decimals byte), createATAIdempotentInstruction (associated-token program discriminator 1). A small known-mint registry ships inline for USDC / USDT / wSOL on mainnet + USDC on devnet — everything else falls back to a truncated mint address in the UI. - Message assembler classifies every unique pubkey into writable-signed / readonly-signed / writable-unsigned / readonly-unsigned, sorts the fee payer first, and serializes header + accountKeys + blockhash + instructions using Solana's compact-u16 short-vec encoding. Same wire shape @solana/web3.js produces from Transaction.serializeMessage. - lib/chain-sol.js: snapshot() now carries a tokens[] array of {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram, isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against both the classic Token program and Token-2022. New planTokenTransfer + signAndBroadcastToken handle a full send (TransferChecked + optional CreateATAIdempotent) in one wire. - Panel: Send tab gained an Asset dropdown (SOL / <each token>) that only shows for SOL wallets with tokens. Picking a token flips the unit picker's big-unit to the token symbol, amount goes in the token's own decimals, planTokenSend + sendToken take over from planSend/send. Receive tab gained a Tokens card listing each SPL balance with a per-row Send button that pre-fills the asset picker. - Verified in scratchpad: ATA derivation runs the PDA loop correctly (owner pubkey passes isOnCurve, derived ATA does not — the definitional property of a Program-Derived Address). Cross-check the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS and the value matches. Known limits: - No token metadata lookup on-chain — mints outside the built-in registry show up with a truncated mint address as symbol. Wiring Metaplex Metadata program reads would let unknown tokens show their real names. - Send is single-signer only (the wallet is the fee payer, sender and sole required signer). Multi-sig SPL transfers work via the dapp bridge (window.solana.signAndSendTransaction, which already handles partial signatures).
2026-09-07 23:55:09 +02:00
// 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;
// Wallet strip's view mode. "coins" is the six-column ticker/chain summary;
// "addresses" replaces it inline with the per-address list under one coin
// group. Toggled via the group row click / the back arrow in the inline
// header. Cleared whenever a fresh render is triggered by a wallet change
// so the strip snaps back to the summary.
let stripView = { mode: "coins", groupKey: null };
// Cached security state ({ hasPin, requirePinForSending }). Populated on
// startup and refreshed after any pin/security invoke — used both by the
// lock screen (PIN vs. password) and the Settings General card.
let securityState = { hasPin: false, requirePinForSending: false };
let securityLoaded = false;
// Cached session config: whether the vault stays unlocked across Theseus
// restarts (safeStorage-backed) and how many idle minutes trigger an
// auto-lock. Populated on boot; refreshed after each Settings edit.
let sessionState = { lockOnClose: true, idleMinutes: 15, hasSession: false, safeStorageAvailable: true };
let sessionLoaded = false;
let idleTimer = null;
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
// Truncate a label to at most `n` visible chars, appending an ellipsis
// when clipped. Used by the inline coin list so long user labels don't
// blow out the row width; the full name stays available via title="".
const shortLabel = (s, n) => {
const t = String(s ?? "").trim();
const cap = Math.max(1, n || 7);
return t.length > cap ? t.slice(0, cap) + "…" : t;
};
// CashAddr / BIP-173 / Cashtokens all prefix the mainnet or testnet name
// before the payload ("bitcoincash:qz…", "bchtest:qp…", "bchreg:qr…").
// The prefix is the same on every row of a coin drilldown so showing it
// there is redundant noise — strip for display, keep in the tooltip and
// the clipboard so the full canonical form is one hover / one click away.
// Non-BCH addresses (ETH 0x…, TRX T…, base58 SOL) pass through unchanged.
const stripAddrPrefix = (addr) => {
if (!addr) return "";
const s = String(addr);
const i = s.indexOf(":");
if (i < 0) return s;
const p = s.slice(0, i).toLowerCase();
return (p === "bitcoincash" || p === "bchtest" || p === "bchreg") ? s.slice(i + 1) : s;
};
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const hostOf = (url) => { try { return new URL(url).host || url; } catch { return url; } };
const openUrl = (url) => S.invoke("openUrl", { url }).catch(() => {});
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const cleanErr = (e) => String(e?.message || e).replace(/^Error invoking remote method '[^']+': Error: /, "");
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.
2026-09-07 01:07:31 +02:00
// ---- 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>`;
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.
2026-09-07 01:07:31 +02:00
}
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>`;
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.
2026-09-07 01:07:31 +02:00
}
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
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>`;
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
}
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>`;
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
}
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
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>`;
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
}
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
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>`;
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
}
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>`;
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
}
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);
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.
2026-09-07 01:07:31 +02:00
}
function testnetTag() { return `<span class="ttag">TEST</span>`; }
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
// Selected wallet convenience.
const sel = () => state && state.selected;
const chain = () => sel()?.chain || "";
const decimals = () => sel()?.meta?.decimals || 8;
const ticker = () => sel()?.meta?.ticker || "";
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
// 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.
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
function fmtBig(units, dec) {
const d = dec != null ? dec : decimals();
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
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 : "");
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const s = (Number(units || 0) / Math.pow(10, d)).toFixed(d);
return s.replace(/(\.\d*?[1-9])0+$|\.0+$/, "$1");
}
2026-09-27 10:17:15 +02:00
// Header balance. Groups the integer part and dims the fraction — the
// "98,230.02" treatment the reference wallets lead with — and by default
// shortens a long tail, because eight decimals of noise sit exactly where
// the eye lands.
//
// The shortening NEVER rounds and never collapses a non-zero balance to
// zeros, which is the way this idea normally goes wrong: a flat "2 decimal
// places" rule renders 0.00042 BCH as "0.00" and the wallet reads as empty.
// Instead it keeps two decimals minimum and then extends past any leading
// zeros so roughly four significant digits always survive, capped at what
// the amount actually has:
//
// 1204.23234145 -> 1,204.23 0.23234145 -> 0.2323
// 0.00012345 -> 0.0001234 0.00000001 -> 0.00000001 (1 sat, intact)
//
// It truncates rather than rounds, so the figure shown is never more than
// the wallet holds and sending the displayed amount always clears.
// An ellipsis marks the cut, hover shows the exact figure, and clicking
// pins full precision (remembered). Only this hero is abbreviated: Send,
// MAX, history and the wallet strip keep calling fmtBig directly, so every
// number you act on is exact.
const BAL_MIN_DP = 2, BAL_SIG_DP = 4;
let balFullText = "—";
let balFullPrecision = false;
try { balFullPrecision = localStorage.getItem("aegis/fullPrecision") === "1"; } catch (_e) {}
function setBalMain(text) {
2026-09-27 10:17:15 +02:00
balFullText = String(text == null ? "—" : text);
paintBalMain();
}
function paintBalMain() {
const el = $("balMain");
if (!el) return;
2026-09-27 10:17:15 +02:00
const m = /^(-?)(\d+)(\.\d+)?$/.exec(balFullText);
if (!m) {
el.textContent = balFullText;
el.removeAttribute("title");
el.classList.remove("balx");
return;
}
const group = (d) => d.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const head = m[1] + group(m[2]);
const fullFrac = m[3] ? m[3].slice(1) : "";
let frac = fullFrac, cut = false;
if (!balFullPrecision && fullFrac) {
// Whole units already carry the significant digits, so anything with a
// non-zero integer part only needs the two decimals. It is the amounts
// under 1 that have to dig past their leading zeros to keep any meaning.
const whole = m[2] !== "0";
const zeros = (/^0*/.exec(fullFrac) || [""])[0].length;
const want = whole ? BAL_MIN_DP : zeros + BAL_SIG_DP;
const keep = Math.max(BAL_MIN_DP, Math.min(fullFrac.length, want));
if (keep < fullFrac.length) { frac = fullFrac.slice(0, keep); cut = true; }
}
// Every interpolated piece is digits, a sign, a dot or a comma the regex
// vouched for, so innerHTML here cannot inject markup.
el.innerHTML = head + (frac
? '<span class="dec">.' + frac + (cut ? '<span class="more">' + String.fromCharCode(0x2026) + "</span>" : "") + "</span>"
: "");
const exact = head + (fullFrac ? "." + fullFrac : "");
const toggleable = cut || balFullPrecision;
el.title = cut ? exact + " — click for every decimal"
: (balFullPrecision ? exact + " — click to shorten" : exact);
el.classList.toggle("balx", toggleable);
}
2026-09-27 10:17:15 +02:00
$("balMain") && $("balMain").addEventListener("click", () => {
balFullPrecision = !balFullPrecision;
try { localStorage.setItem("aegis/fullPrecision", balFullPrecision ? "1" : "0"); } catch (_e) {}
paintBalMain();
});
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
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();
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
if (c === "bch" || c === "dgb" || c === "btc") return "sat";
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
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 || "");
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
function bigUnitLabel() { return ticker(); }
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
// ---- 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";
const abs = Math.abs(usd);
// Sub-cent coins (SC ~ $0.0007, DGB ~ $0.005) get 3 significant digits so
// users see meaningful movement without the row screaming "< $0.01" at
// every wallet. Keeps trailing zeros trimmed: $0.000756, not $0.0007560.
if (abs < 0.01) {
const sig = usd.toPrecision(3);
const num = Number(sig);
if (num === 0) return "$0";
// Node.js's toPrecision returns e.g. "0.000756" for tiny numbers, "5.60e-4"
// for extreme. Normalise to a plain fixed string.
const s = /e/i.test(sig) ? num.toFixed(Math.max(0, -Math.floor(Math.log10(abs)) + 2)) : sig;
return "$" + s;
}
if (abs < 10) return "$" + usd.toFixed(2);
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
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;
}
// ---- security: PIN encryption + verification (WebCrypto) -------------------
// The PIN blob wraps the master password: PBKDF2-SHA256(pin, salt, iters)
// derives an AES-GCM key; the master password is encrypted with a fresh
// per-blob IV. The addon (main process) only handles the opaque blob; the
// panel never sends the raw PIN or the master password to it. The rate
// limiter is stored addon-side so reloading the panel cannot reset it.
const PIN_ITERS = 200000;
const PIN_MAX_FAILS = 5;
const PIN_LOCKOUT_MS = 15 * 60 * 1000;
const b2h = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
const h2b = (h) => { const b = new Uint8Array(h.length / 2); for (let i = 0; i < b.length; i++) b[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16); return b; };
async function pinDeriveKey(pin, saltBytes, iters) {
const enc = new TextEncoder();
const material = await crypto.subtle.importKey("raw", enc.encode(pin), "PBKDF2", false, ["deriveKey"]);
return crypto.subtle.deriveKey(
{ name: "PBKDF2", salt: saltBytes, iterations: iters, hash: "SHA-256" },
material,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"],
);
}
async function pinEncryptMaster(pin, masterPassword) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await pinDeriveKey(pin, salt, PIN_ITERS);
const ct = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(masterPassword)));
return { salt: b2h(salt), iv: b2h(iv), ct: b2h(ct), iters: PIN_ITERS };
}
async function pinDecryptMaster(pin, blob) {
const key = await pinDeriveKey(pin, h2b(blob.salt), blob.iters || PIN_ITERS);
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: h2b(blob.iv) }, key, h2b(blob.ct));
return new TextDecoder().decode(pt);
}
async function pinLockoutRemainingMs() {
try {
const s = await S.invoke("pinFailStatus");
if (!s || !s.count || s.count < PIN_MAX_FAILS) return 0;
const since = Date.now() - (s.last || 0);
return since >= PIN_LOCKOUT_MS ? 0 : (PIN_LOCKOUT_MS - since);
} catch { return 0; }
}
async function refreshSecurityState() {
try {
securityState = await S.invoke("securityGet");
securityLoaded = true;
} catch { securityState = { hasPin: false, requirePinForSending: false }; securityLoaded = true; }
return securityState;
}
async function refreshSessionState() {
try {
sessionState = await S.invoke("sessionStatus");
sessionLoaded = true;
} catch {
sessionState = { lockOnClose: true, idleMinutes: 15, hasSession: false, safeStorageAvailable: true };
sessionLoaded = true;
}
return sessionState;
}
// Idle auto-lock. Any user gesture in the panel resets the timer; if the
// user stays quiet for `sessionState.idleMinutes`, Aegis invokes vaultLock
// so a walked-away laptop doesn't leave the wallet unlocked. Wired at
// boot; each config change bounces it via bindIdleAutoLock().
function bindIdleAutoLock() {
if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
const mins = Number(sessionState.idleMinutes) || 0;
if (mins <= 0) return;
const reset = () => {
if (idleTimer) clearTimeout(idleTimer);
idleTimer = setTimeout(async () => {
// Only lock if the vault is actually open — no point calling lock
// while we're already on the unlock screen.
const s = sel();
if (!s || s.phase !== "ready") return;
try {
state = await S.invoke("vaultLock");
stripView = { mode: "coins", groupKey: null };
render();
} catch (e) { /* silent — user activity will retry */ }
}, mins * 60 * 1000);
};
reset();
// Reset on any deliberate gesture. Passive listeners so scrolling long
// wallet lists doesn't fight the idle timer.
const opts = { passive: true, capture: true };
const listener = () => reset();
["mousedown", "keydown", "touchstart", "focus", "click"].forEach((ev) => document.addEventListener(ev, listener, opts));
// Store the listener so a later bindIdleAutoLock doesn't stack duplicates.
if (bindIdleAutoLock._prev) {
for (const ev of ["mousedown", "keydown", "touchstart", "focus", "click"]) {
document.removeEventListener(ev, bindIdleAutoLock._prev, opts);
}
}
bindIdleAutoLock._prev = listener;
}
// ---- tabs ------------------------------------------------------------------
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
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(); applySetSec(activeSetSec); }
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
if (name === "send") applyUnitPicker();
// Settings is the only tab that can be reached while the vault is
// locked. Re-run the full render() so the lock-screen overlay + chrome
// visibility stay in sync with whichever tab the user just picked.
render();
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
}
// ---- settings section nav (0.8.2) -----------------------------------------
// Settings grew tall enough (Security, Session, Master password, MultiSig,
// Wallet manage + 6 per-chain cards, Prices, Sites) that scrolling to any
// one was awkward. Segment the tab with a chip row: only one section is
// visible at a time, choice persists in localStorage so users land back
// where they left off.
let activeSetSec = "security";
try {
const saved = localStorage.getItem("aegis/setSec");
if (saved) activeSetSec = saved;
} catch (_e) {}
function applySetSec(name) {
activeSetSec = name || "security";
try { localStorage.setItem("aegis/setSec", activeSetSec); } catch (_e) {}
document.querySelectorAll("#setsecnav [data-setsec]").forEach((b) => {
b.classList.toggle("on", b.dataset.setsec === activeSetSec);
});
// A section can span more than one card (Security holds both the top
// Security card and the MultiSig card lower down), so toggle every
// matching body — hide non-matches.
document.querySelectorAll("[data-setsec-body]").forEach((el) => {
el.hidden = el.dataset.setsecBody !== activeSetSec;
});
}
document.querySelectorAll("#setsecnav [data-setsec]").forEach((b) => {
b.addEventListener("click", () => applySetSec(b.dataset.setsec));
});
applySetSec(activeSetSec);
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.
2026-09-07 01:07:31 +02:00
// ---- wallet picker (two-step add) ------------------------------------------
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("pickerBtn").addEventListener("click", (e) => {
// 0.8.0: header is a CURRENCY PICKER. Clicking the wallet name/badge
// opens the browse pane in #drop (coin list → wallet list per coin
// → click a wallet to select it). The ✎ chip on the right opens the
// per-wallet manage modal (rename, path, remove), which is the
// dedicated "edit THIS wallet" affordance — different intent.
if (e.target && e.target.closest("#hAdd")) return;
if (e.target && e.target.closest("#hManage")) {
e.stopPropagation();
const w = (state?.wallets || []).find((x) => x.id === state?.selectedWalletId);
if (w) openWalletManageModal(w);
return;
}
e.stopPropagation();
pickerTab = "browse";
const d = $("drop");
positionDropBelowTabs(d);
d.hidden = false;
fillPicker();
});
// 0.8.4: separate netchip row. Click jumps straight to the browse:chain
// view for the current wallet's chain — the network-chip strip at the
// top of that view is what actually switches networks.
feat(aegis): 0.12.0 — open the wallet full screen, sidebar becomes the rail The 380px panel is the wrong shape for anything that needs room. This opens the wallet as a full Theseus tab, with the sidebar's own furniture — identity, network, balance, section nav, wallet list — laid out as a left rail and the tab body given to the selected section. It is the SAME panel.html, loaded with ?surface=web. No second wallet, no second copy of 4,600 lines to drift apart. That works because an add-on's own tab is handed a window.silentmode with the same invoke/on surface as the sidebar, and addon-msg dispatches it as from:"panel" with the add-on identity derived from the file:// sender — so every existing handler, including the panel-only ones, works there untouched. The whole change is a CSS grid behind one attribute plus a chip to open it. Details that needed care: - The QR is drag-sized against a 380px panel and the size is remembered. Given a 720px column it filled the page, so it is capped on this surface only; the stored sidebar preference is left exactly as the user set it. - #drop (the coin picker sheet) is fixed-position and sized for the panel; it is pinned to the rail instead of covering the window. - Content columns are capped at 720px so forms and lists keep the measure the sidebar already tuned, rather than stretching across a monitor. - Under 900px the grid falls back to the stacked layout, so a narrow window degrades to what the sidebar already does. - The "open full screen" chip hides itself on the full-screen surface, so it cannot open a second copy of itself. Everything is scoped to [data-surface="web"], so the sidebar is byte-for-byte unchanged. Verified both surfaces, the narrow fallback (by exercising the real media rule) and zero horizontal overflow. The aegis.x/app URL still needs a main.js route in Theseus; this ships the destination over the add-on channel first.
2026-09-27 20:17:08 +02:00
// Open the same wallet as a full Theseus tab. CSS hides this chip on the
// full-screen surface, so it never offers to open a second copy of itself.
$("hFull") && $("hFull").addEventListener("click", async (e) => {
e.stopPropagation();
try { await S.invoke("openFullScreen"); }
catch (err) { showErr(cleanErr(err)); }
});
$("hNet").addEventListener("click", (e) => {
e.stopPropagation();
const w = (state?.wallets || []).find((x) => x.id === state?.selectedWalletId);
if (!w) return;
pickerTab = "browse:" + w.chain;
const d = $("drop");
positionDropBelowTabs(d);
d.hidden = false;
fillPicker();
});
// + Add and ⋯ More chips moved from the wallet strip into the header
// (0.6.31). Same handlers as before — fillPicker for the Add-only picker,
// openMoreMenu for Import/Connect/About. Each stopsPropagation so the
// outer pickerBtn click doesn't also fire "manage this wallet".
$("hAdd").addEventListener("click", (e) => {
e.stopPropagation();
// 0.7.1: header + opens the method chooser first (New / Import /
// Connect), then routes into the coin picker for the chosen method.
// Users who want to skip straight to "Add new" from another entry
// point (e.g. openMoreMenu) still set pickerTab = "add" directly.
pickerTab = "method";
const d = $("drop");
// 0.7.3: anchor the drop to the BOTTOM of the tabs bar (nav) so it
// opens over the wallet list + main content but leaves the wallet
// header (balance + selected wallet) AND the Receive/Send/History/
// Settings tabs visible above it. Measured at open time because
// header height varies with content (portfolio line, error banner).
positionDropBelowTabs(d);
d.hidden = false;
fillPicker();
});
function positionDropBelowTabs(dropEl) {
try {
const nav = document.querySelector("nav");
if (!nav) return;
const y = Math.round(nav.getBoundingClientRect().bottom);
if (y > 0) dropEl.style.top = y + "px";
} catch {}
}
// hMore chip removed in 0.7.2 — the compact method chooser under hAdd
// carries Create / Import / Connect / About, so a second right-corner
// button was redundant.
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
document.addEventListener("click", (e) => {
const d = $("drop");
if (d.hidden) return;
// Also whitelist the always-visible wallet strip so its + / ⋯ buttons —
// which run fillPicker() and detach themselves during the render — don't
// trigger the outer "click outside → close" logic. Before this whitelist
// the Add button appeared broken because the picker opened and immediately
// closed in the same event tick.
if (e.target.closest("#drop") || e.target.closest("#pickerBtn") || e.target.closest("#walletStrip")) return;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
d.hidden = true;
});
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.
2026-09-07 01:07:31 +02:00
// Which picker tab is showing. Persisted in the picker instance state so a
// user who opens the picker → picks Import → cancels → reopens returns to
// Wallets (the sane default).
let pickerTab = "wallets";
// 0.8.4: coin catalogue search query + one-shot preselected chain for the
// Add pane (set when the browse view routes into "add" for a specific
// unowned coin).
let browseQuery = "";
let pickerAddChain = null;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
function fillPicker() {
const d = $("drop");
const wallets = state?.wallets || [];
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.
2026-09-07 01:07:31 +02:00
const coins = state?.coins || [];
const rowsHtml = wallets.map((w) => {
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const on = w.id === state.selectedWalletId ? "on" : "";
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
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 : "—";
const usd = usdOf(w.chain, totalUnits, w.decimals);
const fiat = fmtFiat(usd);
const fiatLine = fiat ? `<div class="fs">${esc(fiat)}</div>` : "";
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.
2026-09-07 01:07:31 +02:00
const sub = `${esc(w.coinLabel)} · ${esc(w.networkLabel)}${w.testnet ? " " + testnetTag() : ""}`;
const importedTag = w.kind === "imported" ? ` <span class="ttag" style="background:rgba(214,255,61,.16);color:var(--acid,#d6ff3d)">IMPORTED</span>` : "";
// Derivation path under the balance — one of the most requested pieces of
// info for anyone verifying an address against another wallet. Legacy /
// isDefault wallets can't be removed (they gate legacy funds).
const pathLine = w.accountPath ? `<div class="s mono" style="font-size:10.5px;opacity:.7">${esc(w.accountPath)}</div>` : "";
const menu = w.isLegacy || w.isDefault
? `<span title="Default wallet — protects legacy funds; cannot be removed" style="padding:4px 8px;font-size:14px;color:var(--dim);cursor:not-allowed">🔒</span>`
: `<button class="btn sm" data-walletmenu="${esc(w.id)}" title="Manage wallet" style="padding:4px 8px;font-size:14px">⋯</button>`;
return `<div class="row ${on}" style="position:relative">
<div style="display:flex;align-items:center;gap:9px;flex:1;min-width:0;cursor:pointer" data-select="${esc(w.id)}">
${logoSvg(w.logo, 22)}
<div class="m"><div class="l">${esc(w.label)}${importedTag}</div><div class="s">${sub}</div>${pathLine}</div>
<div class="v"><div>${esc(bal)}</div>${fiatLine}</div>
</div>
${menu}
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.
2026-09-07 01:07:31 +02:00
</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>`;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
}).join("");
// Three-tab layout: Add (create new) / Import (external) / Connect
// (WizardConnect pairing). The old Wallets tab is gone — the always-visible
// strip above the header owns switching, so the picker no longer needs to
// duplicate that list. Add-only when the picker opens from [+].
const bchWallets = wallets.filter((w) => w.chain === "bch");
const wcCount = Object.values(state?.wc || {}).reduce((n, arr) => n + (arr?.length || 0), 0);
// 0.7.1: picker is now a stepped wizard. First "screen" ("method") asks
// HOW the user wants to add a wallet — three cards — before picking a
// coin. Once a method is chosen, the coin picker (or import options)
// appear with a "← Back" chevron so users can revise the method
// without closing the picker.
if (pickerTab === "wallets") pickerTab = "method"; // migrate any stale default
// 0.8.0: browse mode — coin picker → wallet list. Entered by clicking
// the header. Two sub-states:
// pickerTab = "browse" → coin list (all chains user
// owns wallets for)
// pickerTab = "browse:<chain>" → wallet list for that chain,
// with a network-chip row at
// the top for switching.
if (pickerTab === "browse" || pickerTab.startsWith("browse:")) {
const focused = pickerTab.startsWith("browse:") ? pickerTab.slice(7) : null;
// Group user's wallets by chain — the browse view mirrors the strip's
// per-chain grouping but is triggered explicitly by clicking the
// header, and shows richer per-chain summaries (wallet count, total
// native balance, remembered active-network name).
const walletsByChain = new Map();
for (const w of wallets) {
if (!walletsByChain.has(w.chain)) walletsByChain.set(w.chain, []);
walletsByChain.get(w.chain).push(w);
}
if (!focused) {
// 0.8.4: full catalogue with search. Every supported chain is
// listed, whether the user already has a wallet on it or not.
// Rows for owned coins jump to that coin's wallet list; rows for
// unowned coins jump straight into the Add-wallet flow for that
// coin, so the header picker doubles as a fast on-ramp.
const q = String(browseQuery || "").trim().toLowerCase();
const matches = (c) => !q
|| String(c.chain || "").toLowerCase().includes(q)
|| String(c.label || "").toLowerCase().includes(q)
|| String(c.ticker || "").toLowerCase().includes(q)
|| String(c.short || "").toLowerCase().includes(q);
const ownedRows = [];
const otherRows = [];
for (const c of coins) {
if (!matches(c)) continue;
const ws = walletsByChain.get(c.chain) || [];
if (ws.length > 0) {
const first = ws[0];
const nets = Array.from(new Set(ws.map((w) => w.network)));
let active = activeNetworkByChain.get(c.chain);
if (!active || !nets.includes(active)) active = nets.includes("mainnet") ? "mainnet" : nets[0];
const netLbl = networkLabelFor(c.chain, active, active);
const totalUnits = sumGroupUnits(ws.filter((w) => w.network === active));
const dec = first.decimals || c.decimals || 8;
const bal = fmtBig(totalUnits || 0, dec) + " " + esc(first.ticker || c.ticker || c.chain.toUpperCase());
const usd = usdOf(c.chain, totalUnits || 0, dec);
const fiat = usd != null ? `<div class="fs">${esc(fmtFiat(usd))}</div>` : "";
const activeMark = ws.some((w) => w.id === state?.selectedWalletId) ? "on" : "";
ownedRows.push(`<div class="row ${activeMark}" data-browse-chain="${esc(c.chain)}" style="cursor:pointer">
${logoSvg(c.logo, 22)}
<div class="m">
<div class="l">${esc(c.label)} <span class="hint" style="font-weight:400">· ${ws.length} wallet${ws.length === 1 ? "" : "s"}${nets.length > 1 ? ` · ${esc(netLbl)}` : ""}</span></div>
<div class="s mono">${esc(c.ticker || c.chain)}</div>
</div>
<div class="v"><div>${bal}</div>${fiat}</div>
</div>`);
} else {
otherRows.push(`<div class="row unowned" data-browse-add="${esc(c.chain)}" style="cursor:pointer">
${logoSvg(c.logo, 22)}
<div class="m">
<div class="l">${esc(c.label)}</div>
<div class="s mono">${esc(c.ticker || c.chain)}</div>
</div>
<div class="v">+ Add</div>
</div>`);
}
}
2026-09-23 02:49:05 +02:00
// 0.9.3: the "Pick a coin" title row is gone — the search field's
// own placeholder already says what this pane is, so the title was
// a line of chrome restating it and pushing the list down.
d.innerHTML = `
2026-09-23 02:49:05 +02:00
<div class="pickersearch pickersearch-top">
<input id="pickerSearch" type="search" autocomplete="off" spellcheck="false"
placeholder="Search coins by name or ticker…" value="${esc(q)}">
<button class="closex" id="pickerClose" title="Close">✕</button>
</div>
<div class="droppane" style="padding:6px">
${ownedRows.length ? `<div class="catgroup">Your wallets</div>${ownedRows.join("")}` : ""}
${otherRows.length ? `<div class="catgroup" style="margin-top:6px">Add a new wallet</div>${otherRows.join("")}` : ""}
${(!ownedRows.length && !otherRows.length) ? `<div class="hint" style="padding:14px;text-align:center">No coins match "${esc(q)}".</div>` : ""}
</div>`;
d.querySelectorAll("[data-browse-chain]").forEach((row) => row.addEventListener("click", (e) => {
e.stopPropagation();
pickerTab = "browse:" + row.dataset.browseChain;
fillPicker();
}));
d.querySelectorAll("[data-browse-add]").forEach((row) => row.addEventListener("click", (e) => {
e.stopPropagation();
const chain = row.dataset.browseAdd;
// Offer the method chooser (New / Import / Connect) rather than
// dropping straight into create — "add a BCH wallet" is just as
// often "bring in the one I already have". pickerAddChain is
// module-level, so the coin stays preselected through whichever
// branch the user picks.
pickerTab = "method";
pickerAddChain = chain;
fillPicker();
}));
const searchEl = d.querySelector("#pickerSearch");
if (searchEl) {
searchEl.addEventListener("input", () => {
browseQuery = searchEl.value;
// Preserve caret across re-render.
const caret = searchEl.selectionStart;
fillPicker();
const s2 = d.querySelector("#pickerSearch");
if (s2) { s2.focus(); try { s2.setSelectionRange(caret, caret); } catch (_e) {} }
});
// Autofocus on first render, but not on every re-render — the
// re-focus above handles that. Guard with a data flag.
if (!searchEl.dataset.autof) { searchEl.dataset.autof = "1"; searchEl.focus(); }
}
const closeBtn = d.querySelector("#pickerClose");
if (closeBtn) closeBtn.addEventListener("click", (e) => { e.stopPropagation(); d.hidden = true; });
return;
}
// WALLETS: list under one chain, with a network-chip row on top.
const ws = walletsByChain.get(focused) || [];
const nets = Array.from(new Set(ws.map((w) => w.network)));
const ordered = orderNetworks(nets);
let active = activeNetworkByChain.get(focused);
if (!active || !ordered.includes(active)) active = ordered.includes("mainnet") ? "mainnet" : ordered[0];
const first = ws[0] || {};
const filtered = ws.filter((w) => w.network === active);
const dec = first.decimals || 8;
const ticker = first.ticker || focused.toUpperCase();
const netChips = ordered.length > 1
? `<div class="brnetrow">${ordered.map((n) => {
const on = n === active ? "on" : "";
const cnt = ws.filter((w) => w.network === n).length;
const lbl = networkLabelFor(focused, n, n);
return `<button class="brnet ${on}" data-browse-net="${esc(n)}">${esc(lbl)} <span class="hint">${cnt}</span></button>`;
}).join("")}</div>`
: "";
const rows = filtered.map((w) => {
const on = w.id === state?.selectedWalletId ? "on" : "";
const units = walletBalanceUnits(w);
const bal = fmtBig(units || 0, dec) + " " + esc(ticker);
const usd = usdOf(w.chain, units || 0, dec);
const fiat = usd != null ? `<div class="fs">${esc(fmtFiat(usd))}</div>` : "";
const importedTag = w.kind === "imported" ? ` <span class="ttag" style="background:rgba(214,255,61,.16);color:var(--acid,#d6ff3d)">IMPORTED</span>` : "";
const addrShort = w.address ? `${String(w.address).slice(0, 10)}…${String(w.address).slice(-6)}` : "";
return `<div class="row ${on}" data-browse-wid="${esc(w.id)}" style="cursor:pointer">
${logoSvg(w.logo, 22)}
<div class="m">
<div class="l">${esc(w.label)}${importedTag}</div>
<div class="s mono" title="${esc(w.address || "")}">${esc(addrShort)}</div>
</div>
<div class="v"><div>${bal}</div>${fiat}</div>
</div>`;
}).join("");
d.innerHTML = `
<div class="droptabs">
<button data-browse-back style="padding-left:6px;padding-right:10px">← ${esc(first.coinLabel || focused.toUpperCase())}</button>
<span style="flex:1"></span>
<button class="closex" id="pickerClose" title="Close">✕</button>
</div>
${netChips}
<div class="droppane" style="padding:6px">
${filtered.length ? rows : `<div class="hint" style="padding:14px;text-align:center">No ${esc(ticker)} wallet on this network yet.</div>`}
</div>`;
d.querySelectorAll("[data-browse-back]").forEach((b) => b.addEventListener("click", (e) => { e.stopPropagation(); pickerTab = "browse"; fillPicker(); }));
d.querySelectorAll("[data-browse-net]").forEach((b) => b.addEventListener("click", (e) => {
e.stopPropagation();
activeNetworkByChain.set(focused, b.dataset.browseNet);
persistActiveNetworks();
fillPicker();
}));
d.querySelectorAll("[data-browse-wid]").forEach((row) => row.addEventListener("click", async (e) => {
e.stopPropagation();
const id = row.dataset.browseWid;
d.hidden = true;
pickerTab = "method"; // Reset so next + opens the add flow, not the wallet list.
try {
if (id !== state?.selectedWalletId) {
state = await S.invoke("selectWallet", { id });
settingsFilled = false;
render();
}
} catch (er) { showErr(cleanErr(er)); }
}));
const closeBtn = d.querySelector("#pickerClose");
if (closeBtn) closeBtn.addEventListener("click", (e) => { e.stopPropagation(); d.hidden = true; });
return;
}
if (pickerTab === "method") {
// Compact method chooser. Three "how" options + About sit as short
// one-line rows, so #drop keeps to about a third of the panel and the
// footer (aegis.x brand + version) stays visible below it. Was three
// large cards in 0.7.1; the tall layout was covering the footer.
d.innerHTML = `
<div class="droptabs">
<span style="flex:1;font-size:12.5px;color:var(--dim);padding-left:10px">How would you like to add a wallet?</span>
<button class="closex" id="pickerClose" title="Close">✕</button>
</div>
<div class="droppane" style="padding:4px">
<div class="coinrow" data-method="add" style="cursor:pointer">
<span style="font-size:18px;line-height:1;width:22px;text-align:center">+</span>
<div class="m"><div class="l">Create a new wallet</div><div class="s">Derived from your Theseus vault</div></div>
<div class="caret">›</div>
</div>
<div class="coinrow" data-method="import" style="cursor:pointer">
<span style="font-size:18px;line-height:1;width:22px;text-align:center">↓</span>
<div class="m"><div class="l">Import an existing wallet</div><div class="s">BIP39 mnemonic, WIF, or encrypted keystore</div></div>
<div class="caret">›</div>
</div>
<div class="coinrow" data-method="connect" style="cursor:pointer">
<span style="font-size:18px;line-height:1;width:22px;text-align:center">⚡</span>
<div class="m"><div class="l">Connect via WizardConnect${wcCount ? ` <span class="ttag">${wcCount} paired</span>` : ""}</div><div class="s">Pair a hardware / desktop signer over WC</div></div>
<div class="caret">›</div>
</div>
<hr>
<div class="coinrow" data-method="about" style="cursor:pointer">
<span style="font-size:18px;line-height:1;width:22px;text-align:center">🛡</span>
<div class="m"><div class="l">About Aegis · aegis.x</div><div class="s">Open the wallet's front-door site</div></div>
<div class="caret">›</div>
</div>
</div>`;
d.querySelectorAll("[data-method]").forEach((row) => row.addEventListener("click", (e) => {
e.stopPropagation();
const m = row.dataset.method;
if (m === "about") { d.hidden = true; openUrl("https://aegis.x/"); return; }
pickerTab = m;
fillPicker();
}));
const closeBtn = d.querySelector("#pickerClose");
if (closeBtn) closeBtn.addEventListener("click", (e) => { e.stopPropagation(); d.hidden = true; });
return;
}
const modeLabel = pickerTab === "add" ? "Create a new wallet"
: pickerTab === "import" ? "Import an existing wallet"
: "Connect via WizardConnect";
d.innerHTML = `
<div class="droptabs">
<button data-ptab="method" title="Back to method chooser" style="padding-left:6px;padding-right:10px">← ${esc(modeLabel)}</button>
<span style="flex:1"></span>
<button class="closex" id="pickerClose" title="Close">✕</button>
</div>
<div class="droppane" id="ppane-add" ${pickerTab === "add" ? "" : "hidden"}>
<div class="hint" style="padding:6px 8px 10px">Pick a coin, then a network. The wallet is derived from your Theseus vault — nothing to write down.</div>
${coinRows}
</div>
<div class="droppane" id="ppane-import" ${pickerTab === "import" ? "" : "hidden"}>
<div class="hint" style="padding:6px 8px 10px">Load an <b>existing</b> wallet by pasting its BIP39 mnemonic + derivation path, or a WIF private key. Key material is stored encrypted in Theseus's wallet-imports.enc.</div>
<div class="coinrow" id="picker-import-keystore" style="background:rgb(from var(--acid, #d6ff3d) r g b / .06);border:1px solid rgb(from var(--acid, #d6ff3d) r g b / .25);border-radius:8px">
${logoSvg("aegis", 22)}
<div class="m">
<div class="l">Bulk-import from encrypted keystore</div>
<div class="s">Deviant chipnet-keystore.json (or any <span class="mono">chipnet-keystore/2-encrypted</span> file) — master password unlocks all wallets in one go</div>
</div>
<div class="caret">›</div>
</div>
<div class="coinrow" id="picker-import-single">
${logoSvg("aegis", 22)}
<div class="m"><div class="l">Import a single wallet (any coin)</div><div class="s">BIP39 mnemonic + path, or a chain-native private key (WIF / hex / base58)</div></div>
<div class="caret">›</div>
</div>
</div>
<div class="droppane" id="ppane-connect" ${pickerTab === "connect" ? "" : "hidden"}>
${renderConnectPane(bchWallets)}
</div>`;
// Back-chevron routes to the method screen. stopPropagation is critical:
// the click re-renders innerHTML, so the tab element becomes detached,
// and the outer document handler (which hides the picker when a click
// lands outside #drop) then sees a disconnected target and dismisses
// the whole panel. Same reason the import row needs it below.
d.querySelectorAll("[data-ptab]").forEach((b) => b.addEventListener("click", (e) => {
e.stopPropagation();
pickerTab = b.dataset.ptab;
fillPicker();
}));
const closeBtn = d.querySelector("#pickerClose");
if (closeBtn) closeBtn.addEventListener("click", (e) => { e.stopPropagation(); d.hidden = true; });
if (pickerTab === "connect") wireConnectPane();
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.
2026-09-07 01:07:31 +02:00
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
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)); }
}));
// Per-row "⋯" menu — rename + remove. Removes call the same handler the
// Settings tab uses; a hard confirm gates any accidental click since the
// action is unrecoverable for the wallet's local metadata (funds stay
// on-chain; the pointer is what disappears).
d.querySelectorAll("[data-walletmenu]").forEach((b) => b.addEventListener("click", async (e) => {
e.stopPropagation();
const id = b.dataset.walletmenu;
const w = (state?.wallets || []).find((x) => x.id === id);
if (!w) return;
openWalletManageModal(w);
}));
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.
2026-09-07 01:07:31 +02:00
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 ? "▸" : "▾";
}));
// 0.8.4: if the browse view routed here with a preselected chain (user
// clicked "+ Add" on an unowned coin), expand that coin's network
// group AND scroll it into view so the user sees which networks
// exist without a second click.
if (pickerTab === "add" && pickerAddChain) {
const target = pickerAddChain;
pickerAddChain = null;
const row = d.querySelector(`.coinrow[data-coin="${target.replace(/["\\]/g, "")}"]`);
const group = d.querySelector(`#netgroup-${target.replace(/["\\]/g, "")}`);
if (row && group) {
group.hidden = false;
const caret = row.querySelector(".caret"); if (caret) caret.textContent = "▾";
row.scrollIntoView({ block: "nearest" });
}
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
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-single");
if (impBtn) impBtn.addEventListener("click", (e) => { e.stopPropagation(); d.hidden = true; openImportModal(null); });
const impKs = $("picker-import-keystore");
if (impKs) impKs.addEventListener("click", (e) => { e.stopPropagation(); d.hidden = true; openKeystoreImportModal(); });
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
}
// 0.8.6: in-panel replacement for window.confirm(). The native dialog is
// chrome-owned, so it renders as a Theseus-branded OS box outside the
// sidebar — jarring next to the wallet's own UI, and it can't carry an
// icon or a danger-styled button. Resolves true/false like confirm(),
// so callers just `await` it.
// opts: { title, body, confirmLabel, cancelLabel, danger, icon }
function aegisConfirm(opts) {
const o = opts || {};
return new Promise((resolve) => {
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:100000;padding-top:24px";
overlay.innerHTML = `
<div style="width:min(94vw,360px);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:8px">
<span style="font-size:17px;line-height:1">${o.icon || (o.danger ? "⚠" : "🛡")}</span>
<div style="font-weight:600;flex:1">${esc(o.title || "Are you sure?")}</div>
</div>
${o.body ? `<div class="hint" style="margin-bottom:12px;line-height:1.5">${o.body}</div>` : ""}
<div class="actions" style="justify-content:flex-end;gap:6px">
${o.alertOnly ? "" : `<button class="btn" data-ac="no">${esc(o.cancelLabel || "Cancel")}</button>`}
<button class="btn ${o.danger ? "danger" : "primary"}" data-ac="yes">${esc(o.confirmLabel || "Confirm")}</button>
</div>
</div>`;
document.body.appendChild(overlay);
let done = false;
const finish = (val) => {
if (done) return;
done = true;
document.removeEventListener("keydown", onKey, true);
try { overlay.remove(); } catch {}
resolve(val);
};
const onKey = (e) => {
if (e.key === "Escape") { e.stopPropagation(); finish(false); }
else if (e.key === "Enter") { e.stopPropagation(); finish(true); }
};
document.addEventListener("keydown", onKey, true);
overlay.addEventListener("click", (e) => { if (e.target === overlay) finish(false); });
overlay.querySelectorAll("[data-ac]").forEach((b) => b.addEventListener("click", (e) => {
e.stopPropagation();
finish(b.dataset.ac === "yes");
}));
const yes = overlay.querySelector('[data-ac="yes"]');
if (yes) yes.focus();
});
}
// Pick-one-of-several sibling of aegisConfirm, for branches where the
// honest answer is a question rather than a yes/no. Resolves the chosen
// option's `id`, or null if dismissed.
// opts: { title, body, icon, options: [{id, label, hint, primary}] }
function aegisChoose(opts) {
const o = opts || {};
const options = Array.isArray(o.options) ? o.options : [];
return new Promise((resolve) => {
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:100000;padding-top:24px";
overlay.innerHTML = `
<div style="width:min(94vw,360px);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:8px">
<span style="font-size:17px;line-height:1">${o.icon || "+"}</span>
<div style="font-weight:600;flex:1">${esc(o.title || "Choose")}</div>
</div>
${o.body ? `<div class="hint" style="margin-bottom:12px;line-height:1.5">${o.body}</div>` : ""}
<div style="display:flex;flex-direction:column;gap:6px">
${options.map((op) => `
<button class="btn ${op.primary ? "primary" : ""}" data-ac="${esc(op.id)}"
style="text-align:left;padding:9px 11px;display:block;width:100%">
<div style="font-weight:600">${esc(op.label)}</div>
${op.hint ? `<div class="hint" style="margin-top:2px;font-weight:400">${esc(op.hint)}</div>` : ""}
</button>`).join("")}
</div>
<div class="actions" style="justify-content:flex-end;margin-top:10px">
<button class="btn" data-ac="">${esc(o.cancelLabel || "Cancel")}</button>
</div>
</div>`;
document.body.appendChild(overlay);
let done = false;
const finish = (val) => {
if (done) return;
done = true;
document.removeEventListener("keydown", onKey, true);
try { overlay.remove(); } catch {}
resolve(val);
};
const onKey = (e) => { if (e.key === "Escape") { e.stopPropagation(); finish(null); } };
document.addEventListener("keydown", onKey, true);
overlay.addEventListener("click", (e) => { if (e.target === overlay) finish(null); });
overlay.querySelectorAll("[data-ac]").forEach((b) => b.addEventListener("click", (e) => {
e.stopPropagation();
finish(b.dataset.ac || null);
}));
const first = overlay.querySelector('[data-ac]:not([data-ac=""])');
if (first) first.focus();
});
}
// Single-button sibling of aegisConfirm, for the error notices that used
// window.alert(). Fire-and-forget: callers don't need the result, so it
// works from sync handlers too.
function aegisAlert(message, opts) {
const o = opts || {};
return aegisConfirm({
title: o.title || "Something went wrong",
icon: o.icon || "⚠",
body: esc(String(message == null ? "" : message)),
confirmLabel: o.confirmLabel || "OK",
cancelLabel: null,
alertOnly: true,
});
}
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
// 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.
// Manage-wallet modal: rename + derivation path + hard remove. Backend
// handlers already exist (renameWallet, setAccountPath, removeWallet); this
// just gives them a UI in the picker so users don't dive into per-wallet
// Settings for something they view as a top-level action.
function openWalletManageModal(w) {
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
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:99999;padding-top:24px";
const canRemove = !(w.isDefault || w.isLegacy);
const canSetPath = ["bch", "btc", "dgb"].includes(w.chain);
feat(aegis): 0.11.0 — promote a WIF import to an HD wallet A wallet imported from a single private key cannot do WizardConnect, and no amount of work inside Aegis changes that: the handshake ships BIP32 xpubs so the dapp derives addresses without further round-trips, and a lone key has no chain code to build one from. Manufacturing a parent whose child equals a given key means inverting HMAC-SHA512, and even solved for index 0 the dapp's next index lands elsewhere. The way out is to stop being a single-key wallet. Promote derives a fresh wallet from the vault on the import's own network and sweeps the key into it, after which WizardConnect works — and so does every other thing that assumes a key tree. It reuses plan() + signAndBroadcast(), the same pair the consolidate flow already spends through, rather than growing a second money path. Three things it deliberately does not do: - The preview costs the sweep by planning a send-max to the wallet's OWN address, so cancelling leaves nothing behind. Same inputs, same single P2PKH output, so the fee is identical to the real sweep. - The imported key is kept, not deleted. The sweep is unconfirmed when the call returns and anyone holding the old address can still pay into it; removing the key there would strand those coins. Removal stays a separate step the user takes once the balance reads zero. - A promote that fails in plan() takes the just-created wallet back out, since nothing was broadcast. Past that point the wallet is kept even on error, because a transaction may already be on the wire and its destination has to stay visible. BCH only: the BTC/DGB/ETH/TRX/SOL imported adapters still throw "read-only" from plan(), and the refusal now names the chain instead of failing vaguely. Wallet creation is lifted out of the addWallet handler into createVaultWallet so promote builds its destination through exactly the same purpose allocation, legacy-purpose carry-over and id numbering as the Add flow, instead of a near-copy sitting next to a transfer.
2026-09-27 16:19:24 +02:00
// Only BCH imports can be promoted: the other imported adapters still
// throw "read-only" from plan(), so there is no sweep to run.
const canPromote = w.kind === "imported" && w.chain === "bch";
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
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(w.logo, 22)}
<div style="font-weight:600;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">Manage: ${esc(w.label)}</div>
<button class="btn sm" id="mwClose" type="button">✕</button>
</div>
<div class="hint" style="margin-bottom:10px">${esc(w.coinLabel)} · ${esc(w.networkLabel)}${w.testnet ? " · testnet" : ""}</div>
<div class="field">
<div class="lbl">Label</div>
<input type="text" id="mwLabel" value="${esc(w.label || "")}" placeholder="Wallet name">
</div>
${canSetPath ? `<div class="field">
<div class="lbl">Derivation path (account)</div>
<input type="text" id="mwPath" value="${esc(w.accountPath || "")}" spellcheck="false" placeholder="m/44'/…">
<div class="hint">Advanced. Changing this switches to a different set of addresses under the same wallet seed.</div>
</div>` : ""}
feat(aegis): 0.11.0 — promote a WIF import to an HD wallet A wallet imported from a single private key cannot do WizardConnect, and no amount of work inside Aegis changes that: the handshake ships BIP32 xpubs so the dapp derives addresses without further round-trips, and a lone key has no chain code to build one from. Manufacturing a parent whose child equals a given key means inverting HMAC-SHA512, and even solved for index 0 the dapp's next index lands elsewhere. The way out is to stop being a single-key wallet. Promote derives a fresh wallet from the vault on the import's own network and sweeps the key into it, after which WizardConnect works — and so does every other thing that assumes a key tree. It reuses plan() + signAndBroadcast(), the same pair the consolidate flow already spends through, rather than growing a second money path. Three things it deliberately does not do: - The preview costs the sweep by planning a send-max to the wallet's OWN address, so cancelling leaves nothing behind. Same inputs, same single P2PKH output, so the fee is identical to the real sweep. - The imported key is kept, not deleted. The sweep is unconfirmed when the call returns and anyone holding the old address can still pay into it; removing the key there would strand those coins. Removal stays a separate step the user takes once the balance reads zero. - A promote that fails in plan() takes the just-created wallet back out, since nothing was broadcast. Past that point the wallet is kept even on error, because a transaction may already be on the wire and its destination has to stay visible. BCH only: the BTC/DGB/ETH/TRX/SOL imported adapters still throw "read-only" from plan(), and the refusal now names the chain instead of failing vaguely. Wallet creation is lifted out of the addWallet handler into createVaultWallet so promote builds its destination through exactly the same purpose allocation, legacy-purpose carry-over and id numbering as the Add flow, instead of a near-copy sitting next to a transfer.
2026-09-27 16:19:24 +02:00
${canPromote ? `<div style="border-top:1px solid var(--line);margin:12px 0 0;padding-top:10px">
<div class="lbl" style="margin-bottom:4px">WizardConnect</div>
<div class="hint" style="margin-bottom:8px">This wallet came from a single private key, so it can't pair with dapps — WizardConnect hands them an xpub to derive addresses from, and one key is not a key tree. Promoting derives a proper wallet from your vault and sweeps this one into it.</div>
<button class="btn" id="mwPromote" type="button">Promote to HD wallet…</button>
</div>` : ""}
<div class="msg err" id="mwMsg" hidden></div>
<div class="actions" style="justify-content:space-between;margin-top:12px">
${canRemove ? `<button class="btn danger" id="mwRemove">Remove wallet</button>` : `<span class="hint">Default wallet — cannot be removed.</span>`}
<div style="display:flex;gap:6px">
<button class="btn" id="mwCancel">Cancel</button>
<button class="btn primary" id="mwSave">Save</button>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
const close = () => { try { overlay.remove(); } catch {} };
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
overlay.querySelector("#mwClose").addEventListener("click", close);
overlay.querySelector("#mwCancel").addEventListener("click", close);
overlay.querySelector("#mwSave").addEventListener("click", async () => {
const msg = overlay.querySelector("#mwMsg"); msg.hidden = true;
const nextLabel = overlay.querySelector("#mwLabel").value.trim();
const nextPath = overlay.querySelector("#mwPath")?.value?.trim();
try {
if (nextLabel && nextLabel !== w.label) {
state = await S.invoke("renameWallet", { id: w.id, label: nextLabel });
}
if (canSetPath && nextPath && nextPath !== (w.accountPath || "")) {
state = await S.invoke("setAccountPath", { id: w.id, accountPath: nextPath });
}
close();
fillPicker();
render();
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
});
feat(aegis): 0.11.0 — promote a WIF import to an HD wallet A wallet imported from a single private key cannot do WizardConnect, and no amount of work inside Aegis changes that: the handshake ships BIP32 xpubs so the dapp derives addresses without further round-trips, and a lone key has no chain code to build one from. Manufacturing a parent whose child equals a given key means inverting HMAC-SHA512, and even solved for index 0 the dapp's next index lands elsewhere. The way out is to stop being a single-key wallet. Promote derives a fresh wallet from the vault on the import's own network and sweeps the key into it, after which WizardConnect works — and so does every other thing that assumes a key tree. It reuses plan() + signAndBroadcast(), the same pair the consolidate flow already spends through, rather than growing a second money path. Three things it deliberately does not do: - The preview costs the sweep by planning a send-max to the wallet's OWN address, so cancelling leaves nothing behind. Same inputs, same single P2PKH output, so the fee is identical to the real sweep. - The imported key is kept, not deleted. The sweep is unconfirmed when the call returns and anyone holding the old address can still pay into it; removing the key there would strand those coins. Removal stays a separate step the user takes once the balance reads zero. - A promote that fails in plan() takes the just-created wallet back out, since nothing was broadcast. Past that point the wallet is kept even on error, because a transaction may already be on the wire and its destination has to stay visible. BCH only: the BTC/DGB/ETH/TRX/SOL imported adapters still throw "read-only" from plan(), and the refusal now names the chain instead of failing vaguely. Wallet creation is lifted out of the addWallet handler into createVaultWallet so promote builds its destination through exactly the same purpose allocation, legacy-purpose carry-over and id numbering as the Add flow, instead of a near-copy sitting next to a transfer.
2026-09-27 16:19:24 +02:00
// Promote: preview the sweep (costed without creating anything), confirm
// with the real numbers, then create + sweep in one host call. The confirm
// carries the amounts because this moves the wallet's entire balance.
if (canPromote) overlay.querySelector("#mwPromote").addEventListener("click", async () => {
const msg = overlay.querySelector("#mwMsg"); msg.hidden = true;
let pv;
try { pv = await S.invoke("promotePreview", { walletId: w.id }); }
catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; return; }
if (pv.error) { msg.textContent = pv.error; msg.hidden = false; return; }
const amt = (u) => fmtBig(u, pv.decimals);
const ok = await aegisConfirm({
title: "Promote to HD wallet?",
confirmLabel: "Create and sweep",
body: `Aegis will derive <b>${esc(pv.suggestedLabel)}</b> from your vault on ${esc(pv.networkLabel)}, then send this wallet's whole balance to it.`
+ `<br><br><b>${esc(amt(pv.net))} ${esc(pv.ticker)}</b> arrives · <b>${esc(amt(pv.fee))} ${esc(pv.ticker)}</b> miner fee.`
+ `<br><br>The imported key is kept rather than deleted: the sweep still has to confirm, and anyone holding the old address can still pay into it. Remove it yourself once its balance reads zero.`,
});
if (!ok) return;
try {
const r = await S.invoke("promoteToHd", { walletId: w.id, label: pv.suggestedLabel });
close();
fillPicker();
render();
await aegisAlert(
`Sent ${amt(r.sent)} ${r.ticker} to "${r.newWalletLabel}". It can pair with WizardConnect once the sweep confirms.`
+ (r.txid ? ` Txid ${r.txid}` : ""),
{ title: "Promoted to HD wallet", icon: "✅", confirmLabel: "Done" });
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
});
if (canRemove) overlay.querySelector("#mwRemove").addEventListener("click", async () => {
const msg = overlay.querySelector("#mwMsg"); msg.hidden = true;
const ok = await aegisConfirm({
title: `Remove "${w.label}"?`,
danger: true,
confirmLabel: "Remove wallet",
body: `On-chain funds stay exactly where they are — this only unlinks the wallet from Aegis.<br><br>You can add it back later on the same coin + network to derive the same addresses ${w.kind === "imported" ? "(or re-import it, since this one was imported)" : "from your vault seed"}.`,
});
if (!ok) return;
try {
state = await S.invoke("removeWallet", { id: w.id });
close();
fillPicker();
render();
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
});
}
// Master-key bulk import (MASTER-KEY-INTEGRATION.md §7.1).
// The user picks a chipnet-keystore/2-encrypted JSON file + types the master
// password. Decrypt runs entirely in the panel iframe via SubtleCrypto; the
// password never crosses IPC or the network. Preview shows cashaddr + label
// + category for each entry; user picks with checkboxes and hits Import.
// Rate limit: 5 fails / 60 s → 30 s lockout (§8.6). Chipnet-only (§8.7 —
// rejects `bitcoincash:` prefixes silently).
let keystoreUnlockFails = { count: 0, firstAt: 0, lockedUntil: 0 };
function hexToBytesU8(h) {
const s = String(h || "");
const out = new Uint8Array(s.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
return out;
}
async function unlockKeystoreV2(encryptedJson, passphrase) {
if (encryptedJson.spec !== "chipnet-keystore/2-encrypted") {
throw new Error("wrong password"); // opaque — actual reason is bad file
}
const enc = new TextEncoder();
const salt = hexToBytesU8(encryptedJson.kdf.salt);
const iv = hexToBytesU8(encryptedJson.encryption.iv);
const cipherAll = hexToBytesU8(encryptedJson.ciphertext);
const passKey = await crypto.subtle.importKey("raw", enc.encode(passphrase), { name: "PBKDF2" }, false, ["deriveKey"]);
const aesKey = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: encryptedJson.kdf.iterations, hash: "SHA-256" },
passKey, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
const ptBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, aesKey, cipherAll);
return JSON.parse(new TextDecoder().decode(ptBuf));
}
function openKeystoreImportModal() {
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:99999;padding-top:16px";
overlay.innerHTML = `
<div style="width:min(94vw,420px);max-height:92vh;overflow-y:auto;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:8px">
${logoSvg("aegis", 22)}
<div style="font-weight:600;flex:1">Bulk-import from encrypted keystore</div>
<button class="btn sm" id="ksClose" type="button">✕</button>
</div>
<div class="hint" style="margin-bottom:10px">
Chipnet only. Master password never leaves this panel — it decrypts the file locally via WebCrypto. Every imported wallet lands in Theseus's <span class="mono">wallet-imports.enc</span>, no plaintext on disk.
</div>
<div class="field" id="ksFileField">
<div class="lbl">Keystore file</div>
<input type="file" id="ksFile" accept="application/json,.json" style="padding:6px 0">
<div class="hint">Typically <span class="mono">Deviant/Keys/chipnet-keystore.json</span>. Any <span class="mono">chipnet-keystore/2-encrypted</span> file works.</div>
</div>
<div class="field" id="ksPassField">
<div class="lbl">Master password</div>
<input type="password" id="ksPass" spellcheck="false" autocomplete="off">
</div>
<div id="ksPreview" hidden>
<div class="lbl" style="margin-top:6px">Select wallets to import</div>
<div class="hint" id="ksPreviewMeta" style="margin-bottom:6px"></div>
<div class="actions" style="margin:4px 0 8px 0">
<button class="btn sm" id="ksSelAll" type="button">Select all</button>
<button class="btn sm" id="ksSelNone" type="button">Clear</button>
<button class="btn sm" id="ksSelBns" type="button">Only bns</button>
<button class="btn sm" id="ksSelOps" type="button">Only operational</button>
</div>
<div id="ksList" class="serverlist" style="max-height:38vh;overflow-y:auto"></div>
</div>
<div class="msg err" id="ksMsg" hidden style="margin-top:8px"></div>
<div class="actions" style="justify-content:space-between;margin-top:10px">
<button class="btn" id="ksCancel" type="button">Cancel</button>
<div style="display:flex;gap:6px">
<button class="btn" id="ksUnlock" type="button">Unlock →</button>
<button class="btn primary" id="ksImport" type="button" hidden>Import selected</button>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
const close = () => { try { overlay.remove(); } catch {} };
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
overlay.querySelector("#ksClose").addEventListener("click", close);
overlay.querySelector("#ksCancel").addEventListener("click", close);
// Loaded keystore file (parsed JSON) and the decrypted plaintext once
// the user unlocks it. Kept in this closure so nothing hits IPC.
let loadedFile = null;
let decrypted = null;
const setMsg = (t, cls = "err") => {
const el = overlay.querySelector("#ksMsg");
if (!t) { el.hidden = true; return; }
el.className = "msg " + cls; el.textContent = t; el.hidden = false;
};
overlay.querySelector("#ksFile").addEventListener("change", async (e) => {
setMsg("");
const file = e.target.files?.[0]; if (!file) { loadedFile = null; return; }
if (file.size > 512 * 1024) { setMsg("File is too large for a keystore (>512 KB)."); loadedFile = null; return; }
try {
const text = await file.text();
loadedFile = JSON.parse(text);
if (loadedFile?.spec !== "chipnet-keystore/2-encrypted") {
setMsg("File is not a chipnet-keystore/2-encrypted."); loadedFile = null; return;
}
} catch (er) { setMsg("File is not valid JSON."); loadedFile = null; }
});
overlay.querySelector("#ksUnlock").addEventListener("click", async () => {
setMsg("");
// Rate-limit check first (§8.6).
const now = Date.now();
if (keystoreUnlockFails.lockedUntil && now < keystoreUnlockFails.lockedUntil) {
const secs = Math.ceil((keystoreUnlockFails.lockedUntil - now) / 1000);
setMsg(`Too many failed attempts — try again in ${secs}s.`); return;
}
if (!loadedFile) { setMsg("Pick a keystore file first."); return; }
const pass = overlay.querySelector("#ksPass").value;
if (!pass) { setMsg("Enter the master password."); return; }
const btn = overlay.querySelector("#ksUnlock");
btn.disabled = true; const orig = btn.textContent; btn.textContent = "Decrypting…";
try {
decrypted = await unlockKeystoreV2(loadedFile, pass);
// Reset failure counter on success (§8.6).
keystoreUnlockFails = { count: 0, firstAt: 0, lockedUntil: 0 };
renderKeystorePreview(overlay, decrypted);
} catch (err) {
// Opaque error (§8.5). Track failure for rate-limit.
if (!keystoreUnlockFails.firstAt || now - keystoreUnlockFails.firstAt > 60_000) {
keystoreUnlockFails = { count: 1, firstAt: now, lockedUntil: 0 };
} else {
keystoreUnlockFails.count++;
if (keystoreUnlockFails.count >= 5) {
keystoreUnlockFails.lockedUntil = now + 30_000;
setMsg("5 failed attempts. Locked for 30 seconds.");
}
}
if (!keystoreUnlockFails.lockedUntil) setMsg("Wrong password.");
} finally { btn.disabled = false; btn.textContent = orig; }
});
overlay.querySelector("#ksImport").addEventListener("click", async () => {
setMsg("");
const rows = [...overlay.querySelectorAll("[data-ksrow]")].filter((r) => r.querySelector("input[type=checkbox]").checked);
if (!rows.length) { setMsg("Select at least one wallet to import."); return; }
const btn = overlay.querySelector("#ksImport");
btn.disabled = true; const orig = btn.textContent; btn.textContent = "Importing…";
let ok = 0, skipped = 0, errors = [];
for (const row of rows) {
const slug = row.dataset.ksrow;
const entry = decrypted?.wallets?.[slug];
if (!entry) { errors.push(`${slug}: missing in decrypted payload`); continue; }
// Chipnet-only guard (§8.7). Refuse mainnet.
if (String(entry.cashaddr || "").startsWith("bitcoincash:")) { skipped++; continue; }
if (!String(entry.cashaddr || "").startsWith("bchtest:")) { skipped++; continue; }
const spec = { chain: "bch", network: "chipnet", label: entry.label || slug,
category: entry.category || "operational",
source: entry.source || `keystore-bulk-import#${slug}` };
if (entry.wif) {
spec.wif = entry.wif;
} else if (entry.seed) {
// Deviant's keystore stores `seed` as either raw hex (fromMasterSeed
// path) or a BIP39 mnemonic (word list). Route based on shape.
const s = String(entry.seed).trim();
if (/^[0-9a-f]{64,128}$/i.test(s)) { spec.seedHex = s; spec.path = entry.path; }
else { spec.mnemonic = s; spec.path = entry.path; }
} else { errors.push(`${slug}: no wif or seed`); continue; }
try {
await S.invoke("importWallet", spec);
ok++;
} catch (er) {
const msg = cleanErr(er);
// Duplicate imports are non-errors: user re-ran on the same file.
if (/duplicate/i.test(msg)) { skipped++; continue; }
errors.push(`${slug}: ${msg}`);
}
}
btn.textContent = orig; btn.disabled = false;
if (errors.length) { setMsg(`Imported ${ok}, ${skipped} skipped (mainnet). ${errors.length} error(s): ${errors.slice(0, 3).join("; ")}${errors.length > 3 ? "…" : ""}`); }
else if (ok) {
// Session pw is dropped when the overlay closes; we don't hold it.
close();
// Refresh panel state so the wallet strip shows the new imports.
try { state = await S.invoke("state"); render(); } catch {}
} else { setMsg(`Nothing imported${skipped ? ` — ${skipped} mainnet entries skipped (chipnet-only)` : ""}.`); }
});
}
function renderKeystorePreview(overlay, plain) {
const wallets = plain?.wallets || {};
const entries = Object.entries(wallets).map(([slug, w]) => ({
slug, cashaddr: String(w.cashaddr || ""), label: w.label || slug,
category: w.category || "operational", kind: w.wif ? "wif" : (w.seed ? "seed" : "?"),
}));
const chipnet = entries.filter((e) => e.cashaddr.startsWith("bchtest:"));
const mainnet = entries.filter((e) => e.cashaddr.startsWith("bitcoincash:"));
const el = overlay.querySelector("#ksList");
el.innerHTML = chipnet.map((e) => `<label data-ksrow="${esc(e.slug)}">
<input type="checkbox" checked>
<span class="surl">
<div style="font-size:12px;color:var(--ink)">${esc(e.label)}
<span class="ttag" style="background:rgba(214,255,61,.16);color:var(--acid,#d6ff3d);text-transform:none">${esc(e.category)}</span>
<span class="hint" style="font-size:10.5px">· ${esc(e.kind.toUpperCase())}</span>
</div>
<div class="mono" style="font-size:10.5px;color:var(--dim)">${esc(e.cashaddr.slice(0, 32))}…${esc(e.cashaddr.slice(-6))}</div>
</span>
</label>`).join("");
const meta = `${chipnet.length} chipnet wallets available.` + (mainnet.length ? ` ${mainnet.length} mainnet entries hidden (chipnet-only import).` : "");
overlay.querySelector("#ksPreviewMeta").textContent = meta;
overlay.querySelector("#ksPreview").hidden = false;
overlay.querySelector("#ksUnlock").hidden = true;
overlay.querySelector("#ksImport").hidden = false;
overlay.querySelector("#ksFileField").style.display = "none";
overlay.querySelector("#ksPassField").style.display = "none";
overlay.querySelector("#ksSelAll").addEventListener("click", () => el.querySelectorAll("input[type=checkbox]").forEach((c) => c.checked = true));
overlay.querySelector("#ksSelNone").addEventListener("click", () => el.querySelectorAll("input[type=checkbox]").forEach((c) => c.checked = false));
overlay.querySelector("#ksSelBns").addEventListener("click", () => el.querySelectorAll("[data-ksrow]").forEach((r) => {
const cat = r.querySelector(".ttag")?.textContent || "";
r.querySelector("input[type=checkbox]").checked = cat === "bns" || cat === "bns-infra";
}));
overlay.querySelector("#ksSelOps").addEventListener("click", () => el.querySelectorAll("[data-ksrow]").forEach((r) => {
const cat = r.querySelector(".ttag")?.textContent || "";
r.querySelector("input[type=checkbox]").checked = cat === "operational";
}));
}
// Multi-chain import config — drives the form shape per coin. Every entry
// declares: label / logo / networks (with default derivation path) /
// key-material formats accepted / placeholder for the raw-key input.
const IMPORT_COIN_CONFIG = {
bch: {
label: "Bitcoin Cash", logo: "bch",
networks: [
{ id: "chipnet", label: "Chipnet testnet", defaultPath: "m/44'/1'/0'/0/0", testnet: true },
{ id: "mainnet", label: "Mainnet", defaultPath: "m/44'/145'/0'/0/0" },
],
formats: [
{ id: "mnemonic", label: "BIP39 mnemonic + path" },
{ id: "wif", label: "WIF private key", placeholder: "Kx… / Lz… / cN… (base58check)" },
],
},
btc: {
label: "Bitcoin", logo: "btc",
// Testnet3 is de facto abandoned (blocks stall for weeks, faucets
// dried up); Signet is Bitcoin's living testnet now. Only Signet is
// exposed to new imports. The testnet3 adapter is kept in
// lib/chain-btc.js so any wallet created on an earlier version still
// loads — it just no longer appears in the picker.
networks: [
{ id: "mainnet", label: "Mainnet", defaultPath: "m/84'/0'/0'/0/0" },
{ id: "signet", label: "Signet", defaultPath: "m/84'/1'/0'/0/0", testnet: true },
],
formats: [
{ id: "mnemonic", label: "BIP39 mnemonic + path" },
{ id: "wif", label: "WIF private key", placeholder: "Kx… / Lz… / cN… (base58check)" },
],
},
dgb: {
label: "DigiByte", logo: "dgb",
networks: [
{ id: "mainnet", label: "Mainnet", defaultPath: "m/84'/20'/0'/0/0" },
],
formats: [
{ id: "mnemonic", label: "BIP39 mnemonic + path" },
{ id: "wif", label: "WIF private key", placeholder: "L… / K… (base58check)" },
],
},
eth: {
label: "Ethereum", logo: "eth",
networks: [
{ id: "mainnet", label: "Mainnet", defaultPath: "m/44'/60'/0'/0/0" },
{ id: "sepolia", label: "Sepolia", defaultPath: "m/44'/60'/0'/0/0", testnet: true },
],
formats: [
{ id: "mnemonic", label: "BIP39 mnemonic + path" },
{ id: "privHex", label: "Private key (32-byte hex)", placeholder: "0x…" },
],
},
trx: {
label: "Tron", logo: "trx",
networks: [
{ id: "mainnet", label: "Mainnet", defaultPath: "m/44'/195'/0'/0/0" },
{ id: "nile", label: "Nile testnet", defaultPath: "m/44'/195'/0'/0/0", testnet: true },
],
formats: [
{ id: "mnemonic", label: "BIP39 mnemonic + path" },
{ id: "privHex", label: "Private key (32-byte hex)", placeholder: "0x…" },
],
},
sol: {
label: "Solana", logo: "sol",
networks: [
{ id: "mainnet", label: "Mainnet-beta", defaultPath: "m/44'/501'/0'/0'" },
{ id: "devnet", label: "Devnet", defaultPath: "m/44'/501'/0'/0'", testnet: true },
],
formats: [
{ id: "mnemonic", label: "BIP39 mnemonic + path" },
{ id: "privHex", label: "Private key (hex)", placeholder: "32 or 64 bytes hex" },
{ id: "privB58", label: "Private key (base58)", placeholder: "Phantom / Solflare export" },
],
},
sc: {
label: "Siacoin", logo: "sc",
// Sia's walletd (v2) uses a 32-byte root seed and an integer index
// (KeyFromSeed layout) — no BIP44 path. Sia Central Lite / walletd
// both accept a 12-word BIP39 mnemonic that PBKDF2's down to the
// root; the raw 32-byte hex is the alternative that walletd's API
// itself takes. defaultPath doubles as the address index the import
// starts on (0 is standard for a fresh import).
networks: [
{ id: "mainnet", label: "Mainnet", defaultPath: "0" },
],
formats: [
{ id: "mnemonic", label: "BIP39 mnemonic (12 words)" },
{ id: "seedHex", label: "Seed hex (64 chars)", placeholder: "32-byte root, walletd-compatible" },
],
},
};
feat(aegis): 0.13.0 — read a seed phrase from a QR image Import accepts a picture of a QR code. The file is decoded in the panel by the vendored jsQR, drawn to a canvas: no upload, no network, no camera. Image, not camera, on purpose. Theseus sets blockCamera + hideMediaDevices by default and hides device labels from fingerprinting; a camera scanner would fail silently for everyone until they turned that off globally, and a wallet should not be the reason a privacy browser gives up the camera. A photo or screenshot of the code needs no permission at all. What comes back is classified, never trusted. A QR is opaque to the person holding it, and "scan this to restore your wallet" is a working phish, so the decode only chooses which field to fill: BIP39-shaped (12/15/18/21/24 lowercase words) -> mnemonic field WIF or 32-byte hex -> private key field anything else -> nothing is filled; the decoded text is shown so the user can see it was a URL, an address, or junk Nothing auto-submits. The user reads what landed in the box and presses Import, and the host handler still does the real validation. jsQR 1.4.0 is vendored at lib/jsqr.js under Apache-2.0 with its LICENSE beside it, unmodified and unminified — code that touches seed phrases should be auditable in the shipped add-on, not an opaque blob. It is 57 KB gzipped. Verified by round-tripping through the shipped path: Aegis's own encoder builds the QR, it is rasterised to a real PNG File, and decodeQrFile() reads it back byte-exact; an image with no code returns null rather than throwing; and driving the actual file input fills the mnemonic for a seed, fills the key field for a WIF, and leaves every field untouched for a phishing URL.
2026-09-28 00:05:04 +02:00
// ---- QR import (image file, never the camera) -------------------------------
//
// Reads a seed phrase or a private key out of a picture of a QR code. The
// file never leaves the panel: it is drawn to a canvas here and decoded by
// the vendored jsQR, with no upload and no camera permission.
//
// What comes back is CLASSIFIED, not trusted. It picks the field to fill and
// nothing else — no auto-submit, no auto-import. A QR is an opaque blob to
// the person holding it, and "scan this to restore your wallet" is a working
// phish; the user still reads what landed in the box and presses Import, and
// the real validation happens in the host handler either way.
const BIP39_LENGTHS = new Set([12, 15, 18, 21, 24]);
// A BIP39 English phrase is only lowercase a-z words, 3-8 letters each, in
// one of the defined lengths. Word membership is not checked here — the host
// handler does that when it derives; this only decides which box to fill.
function mnemonicIn(str) {
const words = String(str || "").toLowerCase().split(/\s+/).filter(Boolean);
if (!BIP39_LENGTHS.has(words.length)) return null;
if (!words.every((w) => /^[a-z]{3,8}$/.test(w))) return null;
return { value: words.join(" "), words: words.length };
}
feat(aegis): 0.13.0 — read a seed phrase from a QR image Import accepts a picture of a QR code. The file is decoded in the panel by the vendored jsQR, drawn to a canvas: no upload, no network, no camera. Image, not camera, on purpose. Theseus sets blockCamera + hideMediaDevices by default and hides device labels from fingerprinting; a camera scanner would fail silently for everyone until they turned that off globally, and a wallet should not be the reason a privacy browser gives up the camera. A photo or screenshot of the code needs no permission at all. What comes back is classified, never trusted. A QR is opaque to the person holding it, and "scan this to restore your wallet" is a working phish, so the decode only chooses which field to fill: BIP39-shaped (12/15/18/21/24 lowercase words) -> mnemonic field WIF or 32-byte hex -> private key field anything else -> nothing is filled; the decoded text is shown so the user can see it was a URL, an address, or junk Nothing auto-submits. The user reads what landed in the box and presses Import, and the host handler still does the real validation. jsQR 1.4.0 is vendored at lib/jsqr.js under Apache-2.0 with its LICENSE beside it, unmodified and unminified — code that touches seed phrases should be auditable in the shipped add-on, not an opaque blob. It is 57 KB gzipped. Verified by round-tripping through the shipped path: Aegis's own encoder builds the QR, it is rasterised to a real PNG File, and decodeQrFile() reads it back byte-exact; an image with no code returns null rather than throwing; and driving the actual file input fills the mnemonic for a seed, fills the key field for a WIF, and leaves every field untouched for a phishing URL.
2026-09-28 00:05:04 +02:00
function classifyScannedSecret(text) {
const s = String(text || "").trim();
if (!s) return { kind: "empty" };
const bare = mnemonicIn(s);
if (bare) return { kind: "mnemonic", value: bare.value, words: bare.words };
feat(aegis): 0.13.0 — read a seed phrase from a QR image Import accepts a picture of a QR code. The file is decoded in the panel by the vendored jsQR, drawn to a canvas: no upload, no network, no camera. Image, not camera, on purpose. Theseus sets blockCamera + hideMediaDevices by default and hides device labels from fingerprinting; a camera scanner would fail silently for everyone until they turned that off globally, and a wallet should not be the reason a privacy browser gives up the camera. A photo or screenshot of the code needs no permission at all. What comes back is classified, never trusted. A QR is opaque to the person holding it, and "scan this to restore your wallet" is a working phish, so the decode only chooses which field to fill: BIP39-shaped (12/15/18/21/24 lowercase words) -> mnemonic field WIF or 32-byte hex -> private key field anything else -> nothing is filled; the decoded text is shown so the user can see it was a URL, an address, or junk Nothing auto-submits. The user reads what landed in the box and presses Import, and the host handler still does the real validation. jsQR 1.4.0 is vendored at lib/jsqr.js under Apache-2.0 with its LICENSE beside it, unmodified and unminified — code that touches seed phrases should be auditable in the shipped add-on, not an opaque blob. It is 57 KB gzipped. Verified by round-tripping through the shipped path: Aegis's own encoder builds the QR, it is rasterised to a real PNG File, and decodeQrFile() reads it back byte-exact; an image with no code returns null rather than throwing; and driving the actual file input fills the mnemonic for a seed, fills the key field for a WIF, and leaves every field untouched for a phishing URL.
2026-09-28 00:05:04 +02:00
// WIF: base58, 51-52 chars. 5/K/L = BTC-family mainnet, 9/c = testnet.
if (/^[5KL9c][1-9A-HJ-NP-Za-km-z]{50,51}$/.test(s)) return { kind: "wif", value: s };
// Raw 32-byte hex, with or without 0x.
if (/^(0x)?[0-9a-fA-F]{64}$/.test(s)) return { kind: "hex", value: s };
// Wrapped phrases. Several wallets export the seed inside an envelope —
// a version marker, the words, sometimes a derivation path, pipe- or
// comma-separated ("1|<words>|m/44'/145'/0'"). Split on every run of
// non-letters (keeping spaces, which separate the words) and look for a
// BIP39-shaped run in any piece. This is tolerant of the wrapper without
// being loose about what counts as a phrase: a URL or an address still
// breaks into single-word pieces and matches nothing.
for (const piece of s.split(/[^A-Za-z ]+/)) {
const hit = mnemonicIn(piece);
if (!hit) continue;
// A path anywhere in the payload is worth carrying over — pulled from
// the ORIGINAL string, since splitting on non-letters shreds it.
const path = (s.match(/m(?:\/\d+'?)+/) || [])[0] || null;
return { kind: "mnemonic", value: hit.value, words: hit.words, path, wrapped: true };
}
feat(aegis): 0.13.0 — read a seed phrase from a QR image Import accepts a picture of a QR code. The file is decoded in the panel by the vendored jsQR, drawn to a canvas: no upload, no network, no camera. Image, not camera, on purpose. Theseus sets blockCamera + hideMediaDevices by default and hides device labels from fingerprinting; a camera scanner would fail silently for everyone until they turned that off globally, and a wallet should not be the reason a privacy browser gives up the camera. A photo or screenshot of the code needs no permission at all. What comes back is classified, never trusted. A QR is opaque to the person holding it, and "scan this to restore your wallet" is a working phish, so the decode only chooses which field to fill: BIP39-shaped (12/15/18/21/24 lowercase words) -> mnemonic field WIF or 32-byte hex -> private key field anything else -> nothing is filled; the decoded text is shown so the user can see it was a URL, an address, or junk Nothing auto-submits. The user reads what landed in the box and presses Import, and the host handler still does the real validation. jsQR 1.4.0 is vendored at lib/jsqr.js under Apache-2.0 with its LICENSE beside it, unmodified and unminified — code that touches seed phrases should be auditable in the shipped add-on, not an opaque blob. It is 57 KB gzipped. Verified by round-tripping through the shipped path: Aegis's own encoder builds the QR, it is rasterised to a real PNG File, and decodeQrFile() reads it back byte-exact; an image with no code returns null rather than throwing; and driving the actual file input fills the mnemonic for a seed, fills the key field for a WIF, and leaves every field untouched for a phishing URL.
2026-09-28 00:05:04 +02:00
return { kind: "unknown", value: s };
}
async function decodeQrFile(file) {
if (typeof jsQR !== "function") throw new Error("the QR decoder failed to load");
const bitmap = await createImageBitmap(file);
try {
// Very large photos cost time and memory for no accuracy gain; jsQR wants
// pixels, not megapixels. Cap the long edge and let the browser scale.
const MAX = 1600;
const scale = Math.min(1, MAX / Math.max(bitmap.width, bitmap.height));
const w = Math.max(1, Math.round(bitmap.width * scale));
const h = Math.max(1, Math.round(bitmap.height * scale));
const cv = document.createElement("canvas");
cv.width = w; cv.height = h;
const ctx = cv.getContext("2d", { willReadFrequently: true });
ctx.drawImage(bitmap, 0, 0, w, h);
const img = ctx.getImageData(0, 0, w, h);
// Photographs often invert or sit on a dark background; try both.
const hit = jsQR(img.data, w, h, { inversionAttempts: "attemptBoth" });
return hit && hit.data ? hit.data : null;
} finally {
try { bitmap.close(); } catch (_e) {}
}
}
function wireQrImport(overlay) {
const btn = overlay.querySelector("#imQrBtn");
const input = overlay.querySelector("#imQrFile");
if (!btn || !input) return;
const msg = () => overlay.querySelector("#imMsg");
const say = (text, isErr) => {
const m = msg(); if (!m) return;
m.textContent = text;
m.className = isErr ? "msg err" : "msg";
m.hidden = false;
};
btn.addEventListener("click", (e) => { e.preventDefault(); input.value = ""; input.click(); });
input.addEventListener("change", async () => {
const file = input.files && input.files[0];
if (!file) return;
const prev = btn.textContent;
btn.textContent = "Reading…"; btn.disabled = true;
try {
const text = await decodeQrFile(file);
if (!text) { say("No QR code found in that image. A flat, well-lit crop of just the code works best.", true); return; }
const found = classifyScannedSecret(text);
if (found.kind === "mnemonic") {
const ta = overlay.querySelector("#imMnemonic");
if (ta) { ta.value = found.value; ta.dispatchEvent(new Event("input", { bubbles: true })); }
// A derivation path in the QR is worth surfacing, but it does not get
// to silently replace a path already in the box — that box is
// prefilled with this coin's default and may have been edited, and
// quietly changing which addresses get derived is the kind of thing
// that looks like lost funds. Fill it only when empty; otherwise say
// what the QR carried and let the user decide.
let pathNote = "";
if (found.path) {
const pf = overlay.querySelector("#imPath");
if (pf && !pf.value.trim()) {
pf.value = found.path;
pf.dispatchEvent(new Event("input", { bubbles: true }));
pathNote = ` Its derivation path ${found.path} went into the path box.`;
} else if (pf && pf.value.trim() !== found.path) {
pathNote = ` It also carried the path ${found.path} — the box says ${pf.value.trim()}, change it if that is wrong.`;
}
}
const unwrapped = found.wrapped ? " (unwrapped from the QR's own format)" : "";
say(`Read a ${found.words}-word phrase${unwrapped}.${pathNote} Check it, pick the coin and network, then press Import.`, false);
feat(aegis): 0.13.0 — read a seed phrase from a QR image Import accepts a picture of a QR code. The file is decoded in the panel by the vendored jsQR, drawn to a canvas: no upload, no network, no camera. Image, not camera, on purpose. Theseus sets blockCamera + hideMediaDevices by default and hides device labels from fingerprinting; a camera scanner would fail silently for everyone until they turned that off globally, and a wallet should not be the reason a privacy browser gives up the camera. A photo or screenshot of the code needs no permission at all. What comes back is classified, never trusted. A QR is opaque to the person holding it, and "scan this to restore your wallet" is a working phish, so the decode only chooses which field to fill: BIP39-shaped (12/15/18/21/24 lowercase words) -> mnemonic field WIF or 32-byte hex -> private key field anything else -> nothing is filled; the decoded text is shown so the user can see it was a URL, an address, or junk Nothing auto-submits. The user reads what landed in the box and presses Import, and the host handler still does the real validation. jsQR 1.4.0 is vendored at lib/jsqr.js under Apache-2.0 with its LICENSE beside it, unmodified and unminified — code that touches seed phrases should be auditable in the shipped add-on, not an opaque blob. It is 57 KB gzipped. Verified by round-tripping through the shipped path: Aegis's own encoder builds the QR, it is rasterised to a real PNG File, and decodeQrFile() reads it back byte-exact; an image with no code returns null rather than throwing; and driving the actual file input fills the mnemonic for a seed, fills the key field for a WIF, and leaves every field untouched for a phishing URL.
2026-09-28 00:05:04 +02:00
} else if (found.kind === "wif" || found.kind === "hex") {
const raw = overlay.querySelector("#imRaw");
if (raw) { raw.value = found.value; raw.dispatchEvent(new Event("input", { bubbles: true })); }
say(`Read a ${found.kind === "wif" ? "WIF private key" : "32-byte key"}. Switch Source to the matching option if it is not already, then press Import.`, false);
} else {
// Don't paste unrecognised payloads into a key field — show a short
// preview so the user can see it was, say, a URL, and stop there.
const peek = found.value.length > 90 ? found.value.slice(0, 90) + "…" : found.value;
feat(aegis): 0.13.0 — read a seed phrase from a QR image Import accepts a picture of a QR code. The file is decoded in the panel by the vendored jsQR, drawn to a canvas: no upload, no network, no camera. Image, not camera, on purpose. Theseus sets blockCamera + hideMediaDevices by default and hides device labels from fingerprinting; a camera scanner would fail silently for everyone until they turned that off globally, and a wallet should not be the reason a privacy browser gives up the camera. A photo or screenshot of the code needs no permission at all. What comes back is classified, never trusted. A QR is opaque to the person holding it, and "scan this to restore your wallet" is a working phish, so the decode only chooses which field to fill: BIP39-shaped (12/15/18/21/24 lowercase words) -> mnemonic field WIF or 32-byte hex -> private key field anything else -> nothing is filled; the decoded text is shown so the user can see it was a URL, an address, or junk Nothing auto-submits. The user reads what landed in the box and presses Import, and the host handler still does the real validation. jsQR 1.4.0 is vendored at lib/jsqr.js under Apache-2.0 with its LICENSE beside it, unmodified and unminified — code that touches seed phrases should be auditable in the shipped add-on, not an opaque blob. It is 57 KB gzipped. Verified by round-tripping through the shipped path: Aegis's own encoder builds the QR, it is rasterised to a real PNG File, and decodeQrFile() reads it back byte-exact; an image with no code returns null rather than throwing; and driving the actual file input fills the mnemonic for a seed, fills the key field for a WIF, and leaves every field untouched for a phishing URL.
2026-09-28 00:05:04 +02:00
say(`That QR decoded to something that is not a seed phrase or key: "${peek}"`, true);
}
} catch (err) {
say(cleanErr(err), true);
} finally {
btn.textContent = prev; btn.disabled = false;
input.value = "";
}
});
}
function openImportModal(initialChain) {
const chains = Object.keys(IMPORT_COIN_CONFIG);
let curChain = chains.includes(initialChain) ? initialChain : "bch";
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:16px";
// Check vault lock state up front. Imported wallets that mounted at
// startup can leave overallPhase === "ready" even when the vault is
// still locked (imports skip vault.derive) — that's what makes the
// main panel look unlocked while a fresh "wallet-imports-add" IPC
// fails with "password vault is locked". So we test the vault
// directly here and, if it's locked, surface an unlock form inside
// the modal rather than blindly submitting and showing red text.
const paintUnlockGate = (errText) => {
overlay.innerHTML = `
<div style="width:min(94vw,420px);max-height:92vh;overflow-y:auto;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">
<div style="font-weight:600;flex:1">🔒 Unlock the vault to import</div>
<button class="btn sm" id="imClose" type="button">✕</button>
</div>
<div class="hint" style="margin-bottom:10px">Aegis stores imported key material in Theseus's encrypted vault (<span class="mono">wallet-imports.enc</span>). Enter your master password once to unlock it, then Aegis will remember the import form you were filling in.</div>
<div class="field">
<div class="lbl">Master password</div>
<input type="password" id="imUnlockPw" autocomplete="current-password" placeholder="…">
</div>
<div class="msg err" id="imUnlockMsg" ${errText ? "" : "hidden"} style="margin-top:8px">${esc(errText || "")}</div>
<div class="actions" style="justify-content:flex-end;margin-top:10px">
<button class="btn" id="imCancel">Cancel</button>
<button class="btn primary" id="imUnlockBtn">Unlock and continue</button>
</div>
</div>`;
overlay.querySelector("#imClose").addEventListener("click", close);
overlay.querySelector("#imCancel").addEventListener("click", close);
const pwInput = overlay.querySelector("#imUnlockPw");
pwInput.focus();
const doUnlock = async () => {
const pw = pwInput.value;
const err = overlay.querySelector("#imUnlockMsg");
if (!pw) { err.textContent = "Password required."; err.hidden = false; return; }
err.hidden = true;
try {
state = await S.invoke("vaultUnlock", { masterPassword: pw });
// Success — re-paint the modal as the actual import form.
paintImportForm();
} catch (e) {
err.textContent = cleanErr(e); err.hidden = false;
}
};
overlay.querySelector("#imUnlockBtn").addEventListener("click", doUnlock);
pwInput.addEventListener("keydown", (e) => { if (e.key === "Enter") doUnlock(); });
};
const paintImportForm = () => {
overlay.innerHTML = `
<div style="width:min(94vw,420px);max-height:92vh;overflow-y:auto;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">
<span id="imHeaderLogo"></span>
<div style="font-weight:600;flex:1">Import a wallet</div>
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
<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">Coin</div>
<select id="imCoin" style="width:100%;padding:7px 9px;border-radius:7px;background:var(--panel);border:1px solid var(--line);color:var(--ink);font-size:13px">
${chains.map((c) => `<option value="${esc(c)}" ${c === curChain ? "selected" : ""}>${esc(IMPORT_COIN_CONFIG[c].label)}</option>`).join("")}
</select>
</div>
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
<div class="field">
<div class="lbl">Network</div>
<div id="imNetworkGroup" style="display:flex;gap:12px;flex-wrap:wrap;font-size:12.5px;margin-top:4px"></div>
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
</div>
<div class="field">
<div class="lbl">Source</div>
<div id="imFormatGroup" style="display:flex;gap:12px;flex-wrap:wrap;font-size:12.5px;margin-top:4px"></div>
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
</div>
<div class="field" id="imMnemonicField">
feat(aegis): 0.13.0 — read a seed phrase from a QR image Import accepts a picture of a QR code. The file is decoded in the panel by the vendored jsQR, drawn to a canvas: no upload, no network, no camera. Image, not camera, on purpose. Theseus sets blockCamera + hideMediaDevices by default and hides device labels from fingerprinting; a camera scanner would fail silently for everyone until they turned that off globally, and a wallet should not be the reason a privacy browser gives up the camera. A photo or screenshot of the code needs no permission at all. What comes back is classified, never trusted. A QR is opaque to the person holding it, and "scan this to restore your wallet" is a working phish, so the decode only chooses which field to fill: BIP39-shaped (12/15/18/21/24 lowercase words) -> mnemonic field WIF or 32-byte hex -> private key field anything else -> nothing is filled; the decoded text is shown so the user can see it was a URL, an address, or junk Nothing auto-submits. The user reads what landed in the box and presses Import, and the host handler still does the real validation. jsQR 1.4.0 is vendored at lib/jsqr.js under Apache-2.0 with its LICENSE beside it, unmodified and unminified — code that touches seed phrases should be auditable in the shipped add-on, not an opaque blob. It is 57 KB gzipped. Verified by round-tripping through the shipped path: Aegis's own encoder builds the QR, it is rasterised to a real PNG File, and decodeQrFile() reads it back byte-exact; an image with no code returns null rather than throwing; and driving the actual file input fills the mnemonic for a seed, fills the key field for a WIF, and leaves every field untouched for a phishing URL.
2026-09-28 00:05:04 +02:00
<div class="lbl" style="display:flex;align-items:center;gap:8px">
<span style="flex:1">Mnemonic (12/24 words)</span>
<!-- Image only, no camera. Theseus denies the camera by default
(blockCamera) and blanks device labels; a wallet should not be
the reason a privacy browser turns that off. A picture of the
QR needs no permission at all. -->
<button type="button" class="btn sm" id="imQrBtn" title="Read a seed phrase or key from a QR code image">Scan QR image</button>
<input type="file" id="imQrFile" accept="image/*" hidden>
</div>
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
<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" id="imPathLabel" style="margin-top:6px">Derivation path</div>
<input type="text" id="imPath" spellcheck="false" placeholder="m/…">
<div class="hint" id="imPathHint"></div>
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
</div>
<div class="field" id="imRawField" hidden>
<div class="lbl" id="imRawLabel">Private key</div>
<input type="text" id="imRaw" spellcheck="false" placeholder="">
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
</div>
<div class="field">
<div class="lbl">Label</div>
<input type="text" id="imLabel" placeholder="e.g. Trading wallet">
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
</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>`;
overlay.querySelector("#imClose").addEventListener("click", close);
overlay.querySelector("#imCancel").addEventListener("click", close);
feat(aegis): 0.13.0 — read a seed phrase from a QR image Import accepts a picture of a QR code. The file is decoded in the panel by the vendored jsQR, drawn to a canvas: no upload, no network, no camera. Image, not camera, on purpose. Theseus sets blockCamera + hideMediaDevices by default and hides device labels from fingerprinting; a camera scanner would fail silently for everyone until they turned that off globally, and a wallet should not be the reason a privacy browser gives up the camera. A photo or screenshot of the code needs no permission at all. What comes back is classified, never trusted. A QR is opaque to the person holding it, and "scan this to restore your wallet" is a working phish, so the decode only chooses which field to fill: BIP39-shaped (12/15/18/21/24 lowercase words) -> mnemonic field WIF or 32-byte hex -> private key field anything else -> nothing is filled; the decoded text is shown so the user can see it was a URL, an address, or junk Nothing auto-submits. The user reads what landed in the box and presses Import, and the host handler still does the real validation. jsQR 1.4.0 is vendored at lib/jsqr.js under Apache-2.0 with its LICENSE beside it, unmodified and unminified — code that touches seed phrases should be auditable in the shipped add-on, not an opaque blob. It is 57 KB gzipped. Verified by round-tripping through the shipped path: Aegis's own encoder builds the QR, it is rasterised to a real PNG File, and decodeQrFile() reads it back byte-exact; an image with no code returns null rather than throwing; and driving the actual file input fills the mnemonic for a seed, fills the key field for a WIF, and leaves every field untouched for a phishing URL.
2026-09-28 00:05:04 +02:00
wireQrImport(overlay);
const netGroup = overlay.querySelector("#imNetworkGroup");
const fmtGroup = overlay.querySelector("#imFormatGroup");
const rawField = overlay.querySelector("#imRawField");
const mnField = overlay.querySelector("#imMnemonicField");
function paintChain() {
const cfg = IMPORT_COIN_CONFIG[curChain];
overlay.querySelector("#imHeaderLogo").innerHTML = logoSvg(cfg.logo, 22);
netGroup.innerHTML = cfg.networks.map((n, i) => `<label><input type="radio" name="imNet" value="${esc(n.id)}" ${i === 0 ? "checked" : ""}> ${esc(n.label)}${n.testnet ? " " + testnetTag() : ""}</label>`).join("");
fmtGroup.innerHTML = cfg.formats.map((f, i) => `<label><input type="radio" name="imKind" value="${esc(f.id)}" ${i === 0 ? "checked" : ""}> ${esc(f.label)}</label>`).join("");
overlay.querySelectorAll('input[name="imNet"]').forEach((r) => r.addEventListener("change", updatePathDefault));
overlay.querySelectorAll('input[name="imKind"]').forEach((r) => r.addEventListener("change", updateFormatFields));
updatePathDefault(true);
updateFormatFields();
}
function updatePathDefault(force) {
const cfg = IMPORT_COIN_CONFIG[curChain];
const netId = overlay.querySelector('input[name="imNet"]:checked')?.value;
const net = cfg.networks.find((n) => n.id === netId) || cfg.networks[0];
const path = overlay.querySelector("#imPath");
if (force || !path.value.trim()) path.value = net.defaultPath;
// Sia doesn't have a BIP44 path — the same field carries the u64
// address index KeyFromSeed derives from. Rename the label + hint
// so users don't paste a bogus m/44'/… into the SC form.
const pathLbl = overlay.querySelector("#imPathLabel");
const isSia = curChain === "sc";
if (pathLbl) pathLbl.textContent = isSia ? "Address index" : "Derivation path";
overlay.querySelector("#imPathHint").textContent = isSia
? `Sia uses KeyFromSeed(seed, index) — default is ${net.defaultPath} for a fresh import.`
: `Default for ${net.label}: ${net.defaultPath}`;
}
function updateFormatFields() {
const cfg = IMPORT_COIN_CONFIG[curChain];
const fmt = overlay.querySelector('input[name="imKind"]:checked')?.value || "mnemonic";
const f = cfg.formats.find((x) => x.id === fmt) || cfg.formats[0];
mnField.hidden = fmt !== "mnemonic";
rawField.hidden = fmt === "mnemonic";
if (fmt !== "mnemonic") {
overlay.querySelector("#imRawLabel").textContent = f.label;
overlay.querySelector("#imRaw").placeholder = f.placeholder || "";
overlay.querySelector("#imRaw").value = "";
}
}
overlay.querySelector("#imCoin").addEventListener("change", (e) => { curChain = e.target.value; paintChain(); });
paintChain();
overlay.querySelector("#imGo").addEventListener("click", async () => {
const msg = overlay.querySelector("#imMsg"); msg.hidden = true;
const chain = curChain;
const network = overlay.querySelector('input[name="imNet"]:checked')?.value;
const kind = overlay.querySelector('input[name="imKind"]:checked')?.value || "mnemonic";
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, 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; }
// Sia has no BIP44 path — the "path" field carries a u64 address
// index instead. Rename before sending so the addon reads it via
// p.index (mnemonic path field still populates for other chains).
if (chain === "sc") {
payload.index = payload.path || "0";
delete payload.path;
}
} else if (kind === "wif") {
payload.wif = overlay.querySelector("#imRaw").value.trim();
if (!payload.wif) { msg.textContent = "WIF required."; msg.hidden = false; return; }
} else if (kind === "privHex") {
payload.privHex = overlay.querySelector("#imRaw").value.trim();
if (!payload.privHex) { msg.textContent = "Private key hex required."; msg.hidden = false; return; }
} else if (kind === "privB58") {
payload.privB58 = overlay.querySelector("#imRaw").value.trim();
if (!payload.privB58) { msg.textContent = "Private key base58 required."; msg.hidden = false; return; }
} else if (kind === "seedHex") {
payload.seedHex = overlay.querySelector("#imRaw").value.trim();
if (!payload.seedHex) { msg.textContent = "Seed hex required."; msg.hidden = false; return; }
// Same rename as the mnemonic branch — SC's "path" input carries
// the address index. The path field defaults to "0" per config.
if (chain === "sc") {
payload.index = "0"; // Raw-hex form has no path input; use 0.
}
}
try {
state = await S.invoke("importWallet", payload);
close();
render();
} catch (e) {
const errText = cleanErr(e);
// Race case: vault got locked between the modal opening and the
// submit hitting Theseus (idle-lock, or user unlocked but the
// imports subsystem didn't get its own credential). Route the
// user through the unlock gate instead of the raw error text.
if (/vault is locked|password vault is locked/i.test(errText)) {
paintUnlockGate("Vault locked while you were filling in the form. Unlock again to save this import.");
return;
}
msg.textContent = errText; msg.hidden = false;
}
});
};
// Bootstrap: attach the overlay first, then pick which face to show.
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
document.body.appendChild(overlay);
const close = () => { try { overlay.remove(); } catch {} };
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
S.invoke("vaultStatus").then((st) => {
if (st && st.unlocked) paintImportForm();
else paintUnlockGate(null);
}).catch(() => {
// If we can't even reach vaultStatus, assume unlocked and let the
// downstream submit surface the real error.
paintImportForm();
});
}
// ---- Consolidation modal ---------------------------------------------------
// Opens a batch send-max flow from every same-chain/same-network sibling
// into the currently-selected wallet. Preview first, per-source checkboxes,
// then a single PIN gate (if enabled) before the batch fires.
function openConsolidateModal() {
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:16px";
overlay.innerHTML = `
<div style="width:min(94vw,460px);max-height:92vh;overflow-y:auto;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:8px">
<div style="font-weight:600;flex:1">⇢ Consolidate balances</div>
<button class="btn sm" id="conClose" type="button">✕</button>
</div>
<div class="hint" id="conIntro" style="margin-bottom:10px">Every wallet you tick is swept via send-max into the currently-selected wallet. Same chain + same network only — nothing crosses networks or coins.</div>
<div class="field" style="margin-bottom:6px">
<div class="lbl">Destination</div>
<div id="conDest" class="mono" style="padding:6px 8px;background:rgba(255,255,255,.04);border-radius:6px;font-size:12.5px;overflow-wrap:anywhere">Loading…</div>
</div>
<div id="conBody" style="margin-top:10px"></div>
<div class="msg err" id="conMsg" hidden style="margin-top:8px"></div>
<div class="actions" style="justify-content:space-between;margin-top:10px;align-items:center">
<label style="font-size:12px;display:inline-flex;align-items:center;gap:6px;color:var(--dim)">
<input type="checkbox" id="conSelectAll" checked> select all eligible
</label>
<div style="display:flex;gap:8px">
<button class="btn" id="conCancel">Close</button>
<button class="btn primary" id="conGo" disabled>Consolidate</button>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
const close = () => { try { overlay.remove(); } catch {} };
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
overlay.querySelector("#conClose").addEventListener("click", close);
overlay.querySelector("#conCancel").addEventListener("click", close);
let preview = null;
const setBusy = (on, t) => {
const btn = overlay.querySelector("#conGo");
btn.disabled = !!on || !eligibleCount();
btn.textContent = t || "Consolidate";
};
const eligibleCount = () => {
if (!preview) return 0;
return overlay.querySelectorAll('input[type="checkbox"][data-conwid]:checked').length;
};
const paintPreview = () => {
const dest = overlay.querySelector("#conDest");
dest.textContent = preview.destinationLabel + " — " + shortenAddress(preview.destinationAddress);
const body = overlay.querySelector("#conBody");
if (!preview.sources.length) {
body.innerHTML = `<div class="hint" style="padding:12px;text-align:center">No other ${esc(preview.ticker || preview.chain.toUpperCase())} wallets on ${esc(preview.network)}. Add or import one first, then come back.</div>`;
overlay.querySelector("#conSelectAll").disabled = true;
return;
}
const dec = preview.decimals;
const rows = preview.sources.map((s) => {
const bal = fmtBig(s.balance, dec) + " " + esc(preview.ticker);
const net = s.net != null ? fmtBig(s.net, dec) + " " + esc(preview.ticker) : "—";
const fee = s.fee != null ? (preview.chain === "bch" ? `${s.fee} sat` : fmtBig(s.fee, dec) + " " + esc(preview.ticker)) : "—";
const err = s.error ? `<div class="hint" style="color:#f6768a;margin-top:2px;overflow-wrap:anywhere">${esc(s.error)}</div>` : "";
const check = s.eligible
? `<input type="checkbox" data-conwid="${esc(s.walletId)}" checked>`
: `<input type="checkbox" disabled>`;
const opacity = s.eligible ? "" : "opacity:.55";
return `<div class="tx" style="grid-template-columns:auto 1fr auto;align-items:start;gap:10px;padding:8px;border-radius:6px;background:rgba(255,255,255,.02);${opacity}">
<label style="display:flex;align-items:center;height:100%">${check}</label>
<div style="min-width:0">
<div style="font-weight:600">${esc(s.label)}</div>
<div class="hint mono" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(s.address || "—")}</div>
<div class="hint" style="margin-top:2px">Balance <b>${bal}</b> · Fee ${fee}</div>
${err}
</div>
<div style="text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap">
<div>${net}</div>
<div class="hint">will land</div>
</div>
</div>`;
}).join("");
// Summary line at the bottom.
body.innerHTML = `<div style="display:flex;flex-direction:column;gap:6px">${rows}</div>
<div id="conSummary" class="hint" style="margin-top:10px;padding-top:8px;border-top:1px solid var(--line)"></div>`;
const refreshSummary = () => {
const eligible = preview.sources.filter((s) => s.eligible);
const checked = new Set(Array.from(overlay.querySelectorAll('input[type="checkbox"][data-conwid]:checked')).map((c) => c.dataset.conwid));
const sum = (getter) => eligible.filter((s) => checked.has(s.walletId)).reduce((a, s) => a + BigInt(getter(s) || "0"), 0n);
const totalNet = sum((s) => s.net);
const totalFee = sum((s) => s.fee);
const totalBal = sum((s) => s.balance);
overlay.querySelector("#conSummary").innerHTML = `
<b>${checked.size}</b> wallet${checked.size === 1 ? "" : "s"} selected · Moving <b>${esc(fmtBig(totalBal.toString(), dec))}</b> ${esc(preview.ticker)}
(net <b>${esc(fmtBig(totalNet.toString(), dec))}</b> ${esc(preview.ticker)} after ${esc(fmtBig(totalFee.toString(), dec))} in fees)
`;
overlay.querySelector("#conGo").disabled = checked.size === 0;
};
overlay.querySelectorAll('input[type="checkbox"][data-conwid]').forEach((cb) => cb.addEventListener("change", () => {
// Uncheck master when any individual comes off.
const master = overlay.querySelector("#conSelectAll");
const boxes = Array.from(overlay.querySelectorAll('input[type="checkbox"][data-conwid]:not(:disabled)'));
master.checked = boxes.length > 0 && boxes.every((b) => b.checked);
refreshSummary();
}));
overlay.querySelector("#conSelectAll").addEventListener("change", (e) => {
const on = !!e.target.checked;
overlay.querySelectorAll('input[type="checkbox"][data-conwid]:not(:disabled)').forEach((cb) => { cb.checked = on; });
refreshSummary();
});
refreshSummary();
};
const loadPreview = async () => {
try {
preview = await S.invoke("consolidatePreview");
paintPreview();
} catch (e) {
overlay.querySelector("#conBody").innerHTML = `<div class="hint" style="color:#f6768a;padding:12px">Preview failed: ${esc(cleanErr(e))}</div>`;
}
};
overlay.querySelector("#conGo").addEventListener("click", async () => {
const checked = Array.from(overlay.querySelectorAll('input[type="checkbox"][data-conwid]:checked')).map((c) => c.dataset.conwid);
if (!checked.length) return;
const msg = overlay.querySelector("#conMsg"); msg.hidden = true;
// PIN gate fires ONCE for the whole batch — a batch send-max operation
// is a single user intent.
if (!securityLoaded) await refreshSecurityState();
if (securityState.requirePinForSending && securityState.hasPin) {
const ok = await verifyPinInteractively(`Confirm consolidating ${checked.length} wallet${checked.length === 1 ? "" : "s"} with your PIN.`);
if (!ok) { msg.className = "msg err"; msg.textContent = "Cancelled — PIN not confirmed."; msg.hidden = false; return; }
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
}
setBusy(true, "Sending…");
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
try {
const res = await S.invoke("consolidateIntoSelected", { sourceIds: checked });
renderResult(res);
} catch (e) {
msg.className = "msg err"; msg.textContent = cleanErr(e); msg.hidden = false;
setBusy(false);
}
});
const renderResult = (res) => {
const ok = res.results.filter((r) => r.ok);
const bad = res.results.filter((r) => !r.ok);
const explorer = sel()?.explorerTx || "";
const okRows = ok.map((r) => {
const link = explorer && r.txid ? `<a class="link" data-conurl="${esc(explorerHref(explorer, r.txid))}">${esc(r.txid.slice(0, 16))}…</a>` : (r.txid || "");
return `<div class="tx" style="grid-template-columns:1fr auto;padding:6px 8px;background:rgba(214,255,61,.06);border-radius:6px">
<div><div style="font-weight:600">${esc(r.label)}</div><div class="hint">Sent — ${link}</div></div>
<div style="color:var(--acid,#d6ff3d);font-size:16px">✓</div>
</div>`;
}).join("");
const badRows = bad.map((r) => `<div class="tx" style="grid-template-columns:1fr auto;padding:6px 8px;background:rgba(246,118,138,.06);border-radius:6px">
<div><div style="font-weight:600">${esc(r.label)}</div><div class="hint" style="color:#f6768a;overflow-wrap:anywhere">${esc(r.error || "unknown error")}</div></div>
<div style="color:#f6768a;font-size:16px">⚠</div>
</div>`).join("");
overlay.querySelector("#conIntro").textContent = ok.length
? `${ok.length} broadcast · ${bad.length} skipped/failed. Balances update as the network confirms.`
: "Nothing broadcast — see per-source errors below.";
overlay.querySelector("#conBody").innerHTML = `<div style="display:flex;flex-direction:column;gap:6px">${okRows}${badRows}</div>`;
overlay.querySelector("#conSelectAll").disabled = true;
overlay.querySelector("#conGo").hidden = true;
overlay.querySelector("#conCancel").textContent = "Done";
overlay.querySelectorAll("[data-conurl]").forEach((a) => a.addEventListener("click", (e) => {
e.preventDefault(); openUrl(a.dataset.conurl);
}));
};
loadPreview();
}
function shortenAddress(a) {
const s = String(a || "");
if (s.length <= 20) return s;
return s.slice(0, 12) + "…" + s.slice(-6);
}
// 0.8.0: inline consolidate view rendered into the Send/Receive tab body
// when the user flips the mode toggle to Consolidate. Same preview + batch
// mechanics as openConsolidateModal, but without the outer overlay so the
// tab feels like a native alternate mode rather than an interrupting modal.
async function renderConsolidateInline(hostEl) {
if (!hostEl) return;
hostEl.innerHTML = `<div class="hint" style="padding:12px;text-align:center">Loading…</div>`;
let preview;
try { preview = await S.invoke("consolidatePreview"); }
catch (e) { hostEl.innerHTML = `<div class="hint" style="color:#f6768a;padding:12px">Preview failed: ${esc(cleanErr(e))}</div>`; return; }
const dec = preview.decimals;
const rows = preview.sources.map((s) => {
const bal = fmtBig(s.balance, dec) + " " + esc(preview.ticker);
const net = s.net != null ? fmtBig(s.net, dec) + " " + esc(preview.ticker) : "—";
const fee = s.fee != null ? (preview.chain === "bch" ? `${s.fee} sat` : fmtBig(s.fee, dec) + " " + esc(preview.ticker)) : "—";
const err = s.error ? `<div class="hint" style="color:#f6768a;margin-top:2px;overflow-wrap:anywhere">${esc(s.error)}</div>` : "";
const check = s.eligible ? `<input type="checkbox" data-inconwid="${esc(s.walletId)}" checked>` : `<input type="checkbox" disabled>`;
const opacity = s.eligible ? "" : "opacity:.55";
return `<div class="tx" style="grid-template-columns:auto 1fr auto;align-items:start;gap:10px;padding:8px;border-radius:6px;background:rgba(255,255,255,.02);${opacity}">
<label style="display:flex;align-items:center;height:100%">${check}</label>
<div style="min-width:0">
<div style="font-weight:600">${esc(s.label)}</div>
<div class="hint mono" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(s.address || "—")}</div>
<div class="hint" style="margin-top:2px">Balance <b>${bal}</b> · Fee ${fee}</div>
${err}
</div>
<div style="text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap">
<div>${net}</div>
<div class="hint">will land</div>
</div>
</div>`;
}).join("");
hostEl.innerHTML = `
<div class="hint" style="margin-bottom:10px">Sweep same-network siblings into <b>${esc(preview.destinationLabel)}</b> (${esc(shortenAddress(preview.destinationAddress))}).</div>
<div id="inconBody" style="display:flex;flex-direction:column;gap:6px">${preview.sources.length ? rows : `<div class="hint" style="padding:12px;text-align:center">No other wallets to sweep.</div>`}</div>
<div id="inconSummary" class="hint" style="margin-top:10px;padding-top:8px;border-top:1px solid var(--line)"></div>
<div class="msg err" id="inconMsg" hidden style="margin-top:8px"></div>
<div class="actions" style="justify-content:space-between;margin-top:10px;align-items:center">
<label style="font-size:12px;display:inline-flex;align-items:center;gap:6px;color:var(--dim)">
<input type="checkbox" id="inconSelectAll" checked> select all eligible
</label>
<button class="btn primary" id="inconGo">Consolidate</button>
</div>`;
const eligible = preview.sources.filter((s) => s.eligible);
const refreshSummary = () => {
const checked = new Set(Array.from(hostEl.querySelectorAll('input[type="checkbox"][data-inconwid]:checked')).map((c) => c.dataset.inconwid));
const sum = (getter) => eligible.filter((s) => checked.has(s.walletId)).reduce((a, s) => a + BigInt(getter(s) || "0"), 0n);
const totalNet = sum((s) => s.net);
const totalFee = sum((s) => s.fee);
const totalBal = sum((s) => s.balance);
hostEl.querySelector("#inconSummary").innerHTML = `<b>${checked.size}</b> selected · Moving <b>${esc(fmtBig(totalBal.toString(), dec))}</b> ${esc(preview.ticker)} (net <b>${esc(fmtBig(totalNet.toString(), dec))}</b> after ${esc(fmtBig(totalFee.toString(), dec))} fees)`;
hostEl.querySelector("#inconGo").disabled = checked.size === 0;
};
hostEl.querySelectorAll('input[type="checkbox"][data-inconwid]').forEach((cb) => cb.addEventListener("change", () => {
const master = hostEl.querySelector("#inconSelectAll");
const boxes = Array.from(hostEl.querySelectorAll('input[type="checkbox"][data-inconwid]:not(:disabled)'));
master.checked = boxes.length > 0 && boxes.every((b) => b.checked);
refreshSummary();
}));
hostEl.querySelector("#inconSelectAll").addEventListener("change", (e) => {
const on = !!e.target.checked;
hostEl.querySelectorAll('input[type="checkbox"][data-inconwid]:not(:disabled)').forEach((cb) => { cb.checked = on; });
refreshSummary();
});
refreshSummary();
hostEl.querySelector("#inconGo").addEventListener("click", async () => {
const checked = Array.from(hostEl.querySelectorAll('input[type="checkbox"][data-inconwid]:checked')).map((c) => c.dataset.inconwid);
if (!checked.length) return;
const msg = hostEl.querySelector("#inconMsg"); msg.hidden = true;
if (!securityLoaded) await refreshSecurityState();
if (securityState.requirePinForSending && securityState.hasPin) {
const ok = await verifyPinInteractively(`Confirm consolidating ${checked.length} wallet${checked.length === 1 ? "" : "s"} with your PIN.`);
if (!ok) { msg.className = "msg err"; msg.textContent = "Cancelled — PIN not confirmed."; msg.hidden = false; return; }
}
const go = hostEl.querySelector("#inconGo");
go.disabled = true; go.textContent = "Sending…";
try {
const res = await S.invoke("consolidateIntoSelected", { sourceIds: checked });
const okCount = res.results.filter((r) => r.ok).length;
const badCount = res.results.length - okCount;
const explorer = sel()?.explorerTx || "";
const rowsResult = res.results.map((r) => {
if (r.ok) {
const link = explorer && r.txid ? `<a class="link" data-inconurl="${esc(explorerHref(explorer, r.txid))}">${esc(r.txid.slice(0, 16))}…</a>` : (r.txid || "");
return `<div class="tx" style="grid-template-columns:1fr auto;padding:6px 8px;background:rgba(214,255,61,.06);border-radius:6px">
<div><div style="font-weight:600">${esc(r.label)}</div><div class="hint">Sent — ${link}</div></div>
<div style="color:var(--acid,#d6ff3d);font-size:16px">✓</div>
</div>`;
}
return `<div class="tx" style="grid-template-columns:1fr auto;padding:6px 8px;background:rgba(246,118,138,.06);border-radius:6px">
<div><div style="font-weight:600">${esc(r.label)}</div><div class="hint" style="color:#f6768a;overflow-wrap:anywhere">${esc(r.error || "unknown error")}</div></div>
<div style="color:#f6768a;font-size:16px">⚠</div>
</div>`;
}).join("");
hostEl.innerHTML = `<div class="hint" style="margin-bottom:10px">${okCount} broadcast · ${badCount} skipped/failed.</div>
<div style="display:flex;flex-direction:column;gap:6px">${rowsResult}</div>`;
hostEl.querySelectorAll("[data-inconurl]").forEach((a) => a.addEventListener("click", (e) => { e.preventDefault(); openUrl(a.dataset.inconurl); }));
} catch (e) {
msg.className = "msg err"; msg.textContent = cleanErr(e); msg.hidden = false;
go.disabled = false; go.textContent = "Consolidate";
}
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
});
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
}
function paintSendMode() {
const normal = $("sendNormal"); const cons = $("sendConsolidate");
if (!normal || !cons) return;
const buttons = document.querySelectorAll("[data-send-mode]");
buttons.forEach((b) => b.classList.toggle("on", b.dataset.sendMode === sendMode));
normal.hidden = sendMode !== "send";
cons.hidden = sendMode !== "consolidate";
if (sendMode === "consolidate") {
const host = $("sendConsolidateInline");
if (host && consolidateInlineHost !== host) { consolidateInlineHost = host; renderConsolidateInline(host); }
}
}
feat(aegis): 0.16.0 — Address & QR and Assets are peer chips, and the coin row loses its dropdown Two changes to the same idea: one way to do each thing, and the same shape for every coin. **Assets is no longer buried under the QR.** The Receive body stacked address → QR → assets in one column, so in a 380px panel the token list sat permanently below the fold behind 200px of QR — and a coin holding no tokens simply lost a section, which made every coin look structurally different. Address & QR and Assets are now peers behind a persistent chip row. The count rides on the chip, and a wallet with nothing says "No assets held by this wallet" instead of the section vanishing. That mattered more than it sounds: a chipnet wallet with 46 CashToken categories was indistinguishable from an empty one until 0.15.0, because the only signal was an absent card. The choice persists across re-renders and wallet switches — comparing token balances between wallets should not throw you back to the QR on every click. Switching back to Address redraws the QR, since a canvas sized while hidden comes out blank and renderReceive only repaints when the address changes. **The coin row's ▾ network dropdown is gone.** The coin view already carries real network chips (data-browse-net: they filter the wallet list and show a per-network count), so the popover was a second control for one job — and the only one that hid its options behind a click. Chips are the single network control now and the ticker is plain text again. Verified in the panel: chips switch panes and track their own state, the count shows, the empty state reads correctly, the QR survives the round trip, and no .wchev / .wcname.wswitchable remain in the DOM.
2026-09-28 23:05:48 +02:00
// Which half of the Receive pane is showing. Persistent: the choice sticks
// across re-renders and across wallet switches, because someone comparing
// token balances between wallets should not be knocked back to the QR on
// every click.
let rcvView = "address";
try { rcvView = localStorage.getItem("aegis/rcvView") === "assets" ? "assets" : "address"; } catch (_e) {}
function paintRcvView() {
const addr = $("rcvAddressPane"), assets = $("rcvAssetsPane");
if (!addr || !assets) return;
document.querySelectorAll("[data-rcv-view]").forEach((b) =>
b.classList.toggle("on", b.dataset.rcvView === rcvView));
addr.hidden = rcvView !== "address";
assets.hidden = rcvView !== "assets";
// The QR's backing store is sized from its rendered box, so one drawn
// while its pane was hidden comes out blank. Redraw on reveal — and note
// renderReceive only redraws when the address CHANGES, so switching back
// to an unchanged address would otherwise never repaint it.
if (rcvView === "address") {
const a = sel()?.address || "";
if (a) { try { drawQr(qrPayload(chain(), a, sel()?.network)); } catch (_e) {} }
}
}
function paintRcvMode() {
const normal = $("rcvNormal"); const cons = $("rcvConsolidate");
if (!normal || !cons) return;
normal.hidden = rcvMode !== "receive";
cons.hidden = rcvMode !== "consolidate";
if (rcvMode === "consolidate") {
const host = $("rcvConsolidateInline");
if (host && consolidateInlineHost !== host) { consolidateInlineHost = host; renderConsolidateInline(host); }
}
}
fix(aegis): 0.9.5 — WizardConnect signing actually works Pairing already worked; signing would have thrown on the first request a dapp ever sent. Found by testing against the real relay and the real @wizardconnect/wallet library rather than reading the code. Two bugs in wc-sign.js, both fatal: - The WC message nests the whole WcSignTransactionRequest under `.transaction`, so the tx is at request.transaction.transaction and the spent outputs at request.transaction.sourceOutputs. We read request.transaction as the tx and request.sourceOutputs as the outputs, so tx.inputs was undefined. index.js already read the nested request.transaction.userPrompt for the approval dialog, so only the signer had it wrong. The flat shape is still accepted. - generateSigningSerializationBCH takes TWO positional arguments, (compilationContext, {coveredBytecode, signingSerializationType}). We passed one merged object, leaving coveredBytecode undefined and throwing inside libauth. For P2PKH the covered bytecode is the spent output's locking script. Now verified end to end: a two-input transaction spending from two different derivation paths signs, decodes, and passes createVirtualMachineBCH().verify() — consensus-valid, with SIGHASH_ALL|FORKID|UTXOS (0x61) on every input as the protocol requires. Also: RelayStatus is an object ({status: "connected" | "reconnecting" | "disconnected" | "session_deleted"}), and the snapshot read a non-existent `.kind`, so every connection reported the literal "[object Object]". Reads `.status` now, uses the documented getConnections() accessor instead of the private connections Map, and carries the library's own `label` ("dapp name once known, otherwise Connecting…"). The panel shows a tag for anything other than connected — "reconnecting" is the difference between a pairing that will see the next signature and one that is dead, which was invisible before.
2026-09-23 03:23:41 +02:00
// A paired dapp's relay status, from RelayStatus.status. Worth showing:
// "reconnecting" is the difference between "the dapp will see my next
// signature" and "this pairing is dead and I should re-pair", and that is
// invisible otherwise. "connected" is the normal case, so it stays quiet.
function wcStatusTag(status) {
const s = String(status || "");
if (!s || s === "connected") return "";
const label = s === "reconnecting" ? "RECONNECTING"
: s === "session_deleted" ? "SESSION GONE"
: s === "disconnected" ? "OFFLINE" : s.toUpperCase();
return `<span class="ttag" style="background:rgba(224,179,65,.18);color:#e0b341">${esc(label)}</span>`;
}
// Content of the Connect pane in the picker — WizardConnect pairing lives
// here so users can paste a wiz:// URI without diving into per-wallet
// Settings. If no BCH wallet is ready, we show a gate instead of the form.
function renderConnectPane(bchWallets) {
const readyBch = bchWallets.filter((w) => w.phase === "ready");
if (!readyBch.length) {
chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page Wallet strip: - Clicking a coin opens that coin's page (addresses, price, totals, back and close) instead of only flipping the selection and leaving the list sitting there. The page already existed but was reachable only via the small count chip. - The per-coin second action was a gear that selected the wallet and opened the global Settings tab — the same destination for every coin, so it read as a per-coin control that wasn't one. It is now Remove, behind a confirm, with the default/legacy wallet showing a lock instead since it gates legacy funds. - Each address in the drilldown can expand to show what THAT address holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via tokenBalances. walletSummary now carries both per wallet, so the view no longer has to borrow the selected wallet's assets. WizardConnect — the Connect pane was effectively unusable: - The locked-vault branch told the user to unlock and gave them nothing to click. It is reachable without the lock screen ever appearing, because a mounted imported wallet makes overallPhase read "ready". It now carries the same unlock form the lock screen uses. - Imported BCH wallets were never registered with the WC manager — startForWallet ran only in the vault-derived mount branch. They mount as ready, so they appeared in the "Sign with" picker and then failed on pair. They now register from their stored seed. WC derives a child key tree, so single-key (WIF) imports genuinely cannot pair; those are disabled in the picker with the reason, rather than failing on click. - Adds window.wizardconnect so a dapp can hand over the wiz:// URI it already generated instead of making the user copy it between tabs. The protocol is Nostr-relay pairing designed for phone-scans-QR, and the SDK has no in-page discovery at all, so this is our own surface: connect() + isReady(), plus a wizardconnect:announceProvider event shaped like EIP-6963 so several WC wallets can coexist. Pairing always goes through the approval modal; the URI is validated before any UI shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00
// 0.8.8: the locked branch used to be a dead end — it told the user to
// unlock the vault but gave them nothing to click, and the panel chrome
// stays visible whenever an imported wallet is mounted (those skip the
// vault), so this is reachable without the lock screen ever showing.
// Inline the same unlock form the lock screen uses.
const locked = bchWallets.length > 0;
return `<div class="hint" style="padding:14px">
<div style="margin-bottom:6px"><b>WizardConnect</b> pairs Aegis with a BCH dapp (Cauldron, Moria, or any site built on the SDK).</div>
chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page Wallet strip: - Clicking a coin opens that coin's page (addresses, price, totals, back and close) instead of only flipping the selection and leaving the list sitting there. The page already existed but was reachable only via the small count chip. - The per-coin second action was a gear that selected the wallet and opened the global Settings tab — the same destination for every coin, so it read as a per-coin control that wasn't one. It is now Remove, behind a confirm, with the default/legacy wallet showing a lock instead since it gates legacy funds. - Each address in the drilldown can expand to show what THAT address holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via tokenBalances. walletSummary now carries both per wallet, so the view no longer has to borrow the selected wallet's assets. WizardConnect — the Connect pane was effectively unusable: - The locked-vault branch told the user to unlock and gave them nothing to click. It is reachable without the lock screen ever appearing, because a mounted imported wallet makes overallPhase read "ready". It now carries the same unlock form the lock screen uses. - Imported BCH wallets were never registered with the WC manager — startForWallet ran only in the vault-derived mount branch. They mount as ready, so they appeared in the "Sign with" picker and then failed on pair. They now register from their stored seed. WC derives a child key tree, so single-key (WIF) imports genuinely cannot pair; those are disabled in the picker with the reason, rather than failing on click. - Adds window.wizardconnect so a dapp can hand over the wiz:// URI it already generated instead of making the user copy it between tabs. The protocol is Nostr-relay pairing designed for phone-scans-QR, and the SDK has no in-page discovery at all, so this is our own surface: connect() + isReady(), plus a wizardconnect:announceProvider event shaped like EIP-6963 so several WC wallets can coexist. Pairing always goes through the approval modal; the URI is validated before any UI shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00
${locked ? `
<div style="margin-bottom:10px">WizardConnect signs with your BCH keys, so the password vault has to be unlocked first.</div>
<div class="field">
<input type="password" id="pkConnectUnlockPw" placeholder="Master password" autocomplete="current-password">
</div>
<div class="actions"><button class="btn primary" id="pkConnectUnlockBtn">Unlock vault</button></div>
<div class="msg err" id="pkConnectUnlockMsg" hidden></div>
` : `<div>Add a BCH wallet first via the Add tab, then come back.</div>`}
</div>`;
}
chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page Wallet strip: - Clicking a coin opens that coin's page (addresses, price, totals, back and close) instead of only flipping the selection and leaving the list sitting there. The page already existed but was reachable only via the small count chip. - The per-coin second action was a gear that selected the wallet and opened the global Settings tab — the same destination for every coin, so it read as a per-coin control that wasn't one. It is now Remove, behind a confirm, with the default/legacy wallet showing a lock instead since it gates legacy funds. - Each address in the drilldown can expand to show what THAT address holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via tokenBalances. walletSummary now carries both per wallet, so the view no longer has to borrow the selected wallet's assets. WizardConnect — the Connect pane was effectively unusable: - The locked-vault branch told the user to unlock and gave them nothing to click. It is reachable without the lock screen ever appearing, because a mounted imported wallet makes overallPhase read "ready". It now carries the same unlock form the lock screen uses. - Imported BCH wallets were never registered with the WC manager — startForWallet ran only in the vault-derived mount branch. They mount as ready, so they appeared in the "Sign with" picker and then failed on pair. They now register from their stored seed. WC derives a child key tree, so single-key (WIF) imports genuinely cannot pair; those are disabled in the picker with the reason, rather than failing on click. - Adds window.wizardconnect so a dapp can hand over the wiz:// URI it already generated instead of making the user copy it between tabs. The protocol is Nostr-relay pairing designed for phone-scans-QR, and the SDK has no in-page discovery at all, so this is our own surface: connect() + isReady(), plus a wizardconnect:announceProvider event shaped like EIP-6963 so several WC wallets can coexist. Pairing always goes through the approval modal; the URI is validated before any UI shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00
// Wallets with no derivable seed (WIF single-key imports) can't pair at
// all, so they're disabled rather than silently failing on Connect.
const pairable = readyBch.filter((w) => !w.wcBlocked);
const options = readyBch.map((w) => `<option value="${esc(w.id)}" ${!w.wcBlocked && w.id === state.selectedWalletId ? "selected" : ""} ${w.wcBlocked ? "disabled" : ""}>${esc(w.label)} · ${esc(w.networkLabel)}${w.wcBlocked ? ` — can't pair (${esc(w.wcBlockedShort || "unsupported")})` : ""}</option>`).join("");
// Greying an option out with "can't pair" and giving no reason anywhere on
// the page reads as a broken wallet rather than a property of how it was
// added. Show the reasons whenever ANY wallet is blocked — the old note
// only appeared when nothing at all could pair, so a user with one good
// mainnet wallet and ten WIF-imported chipnet ones saw no explanation.
const blocked = readyBch.filter((w) => w.wcBlocked);
const reasons = [...new Set(blocked.map((w) => w.wcBlocked))].map(esc).join(" ");
const blockedNote = !blocked.length
? ""
: !pairable.length
? `<div class="msg err" style="margin-bottom:8px">${reasons}</div>`
: `<div class="hint" style="margin-bottom:8px">${blocked.length} of ${readyBch.length} BCH wallets can't pair. ${reasons}</div>`;
// Flatten all connected dapps (across BCH wallets) into one list — the
// user thinks "my dapps", not "dapps per wallet".
const rows = [];
for (const w of readyBch) {
const conns = state?.wc?.[w.id] || [];
for (const c of conns) rows.push({ ...c, walletId: w.id, walletLabel: w.label });
}
const rowsHtml = rows.length
? rows.map((c) => `<div class="tx" style="grid-template-columns:auto 1fr auto;cursor:default;align-items:center;margin-top:6px">
<div>${c.dappIcon ? `<img src="${esc(c.dappIcon)}" style="width:18px;height:18px;border-radius:4px" onerror="this.hidden=true">` : ""}</div>
fix(aegis): 0.9.5 — WizardConnect signing actually works Pairing already worked; signing would have thrown on the first request a dapp ever sent. Found by testing against the real relay and the real @wizardconnect/wallet library rather than reading the code. Two bugs in wc-sign.js, both fatal: - The WC message nests the whole WcSignTransactionRequest under `.transaction`, so the tx is at request.transaction.transaction and the spent outputs at request.transaction.sourceOutputs. We read request.transaction as the tx and request.sourceOutputs as the outputs, so tx.inputs was undefined. index.js already read the nested request.transaction.userPrompt for the approval dialog, so only the signer had it wrong. The flat shape is still accepted. - generateSigningSerializationBCH takes TWO positional arguments, (compilationContext, {coveredBytecode, signingSerializationType}). We passed one merged object, leaving coveredBytecode undefined and throwing inside libauth. For P2PKH the covered bytecode is the spent output's locking script. Now verified end to end: a two-input transaction spending from two different derivation paths signs, decodes, and passes createVirtualMachineBCH().verify() — consensus-valid, with SIGHASH_ALL|FORKID|UTXOS (0x61) on every input as the protocol requires. Also: RelayStatus is an object ({status: "connected" | "reconnecting" | "disconnected" | "session_deleted"}), and the snapshot read a non-existent `.kind`, so every connection reported the literal "[object Object]". Reads `.status` now, uses the documented getConnections() accessor instead of the private connections Map, and carries the library's own `label` ("dapp name once known, otherwise Connecting…"). The panel shows a tag for anything other than connected — "reconnecting" is the difference between a pairing that will see the next signature and one that is dead, which was invisible before.
2026-09-23 03:23:41 +02:00
<div><div>${esc(c.dappName || c.label || "(pairing…)")} ${wcStatusTag(c.status)}</div><div class="hint">on <b>${esc(c.walletLabel)}</b> · <span class="mono">${esc((c.uri || "").slice(0, 40))}…</span></div></div>
<button class="btn sm" data-wcpick="${esc(c.walletId)}|${esc(c.id)}">Disconnect</button>
</div>`).join("")
: `<div class="hint" style="padding:6px 8px">No dapps paired yet.</div>`;
return `<div style="padding:6px">
feat(theseus+aegis): WizardConnect auto-detection — wiz:// links + page scan Completes the three detection paths. The injected provider shipped in 0.8.8; these two needed host support, because nothing in the add-on API could reach the active tab's content (captureTab is pixels, not DOM). wiz:// links (main.js) A click on a wiz:// anchor is intercepted in will-navigate and in the window-open handler (target="_blank" lands there instead), and routed to the wallet with the offering page's origin attached, so the approval names the real site. The tab never navigates. This needs nothing from the dapp beyond rendering the URI as a link, so it works for third-party dapps that will never adopt a Silent Mode API. scan-page capability (addons-host.js + main.js) New capability backing api.scanActiveTabForUris({scheme, limit}). Deliberately NOT a "read the page" API: the host runs the match and returns only the URIs found, so an add-on holding this still cannot see page text, markup or form values. It sits well below page-inject on the trust ladder — it learns that a page offers a wiz:// code and nothing else. Scheme is validated against [a-z][a-z0-9+.-]* and the result count is capped. The matcher also accepts WizardConnect's QR-alphanumeric spelling (WIZ://%3FP%3D…), which is frequently the only form present when a dapp renders its pairing code as a QR, and decodes it. Verified against the SDK: decodeKeyExchangeURI accepts standard, QR-raw and QR-decoded alike. Regex sources are built host-side and passed as JSON rather than assembled inside the injected string — hand-escaping backslashes and quotes through two levels of literal was both wrong on the first attempt and unreviewable. Aegis Declares scan-page, adds the wcScanPage handler and a "Scan page" button next to Connect. A scan fills the URI field and stops there rather than pairing outright: the user still chooses which wallet signs and still presses Connect, because a scan that silently paired would carry far more consequence than the button implies. Older hosts without the capability get a clear "update Theseus" message instead of a dead button.
2026-09-23 00:16:49 +02:00
<div class="hint" style="margin-bottom:8px">Dapps that support Aegis hand the pairing over with one click. Otherwise open the dapp's Connect dialog and press <b>Scan page</b>, or paste its <span class="mono">wiz://</span> code below. Aegis signs every request after your approval.</div>
chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page Wallet strip: - Clicking a coin opens that coin's page (addresses, price, totals, back and close) instead of only flipping the selection and leaving the list sitting there. The page already existed but was reachable only via the small count chip. - The per-coin second action was a gear that selected the wallet and opened the global Settings tab — the same destination for every coin, so it read as a per-coin control that wasn't one. It is now Remove, behind a confirm, with the default/legacy wallet showing a lock instead since it gates legacy funds. - Each address in the drilldown can expand to show what THAT address holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via tokenBalances. walletSummary now carries both per wallet, so the view no longer has to borrow the selected wallet's assets. WizardConnect — the Connect pane was effectively unusable: - The locked-vault branch told the user to unlock and gave them nothing to click. It is reachable without the lock screen ever appearing, because a mounted imported wallet makes overallPhase read "ready". It now carries the same unlock form the lock screen uses. - Imported BCH wallets were never registered with the WC manager — startForWallet ran only in the vault-derived mount branch. They mount as ready, so they appeared in the "Sign with" picker and then failed on pair. They now register from their stored seed. WC derives a child key tree, so single-key (WIF) imports genuinely cannot pair; those are disabled in the picker with the reason, rather than failing on click. - Adds window.wizardconnect so a dapp can hand over the wiz:// URI it already generated instead of making the user copy it between tabs. The protocol is Nostr-relay pairing designed for phone-scans-QR, and the SDK has no in-page discovery at all, so this is our own surface: connect() + isReady(), plus a wizardconnect:announceProvider event shaped like EIP-6963 so several WC wallets can coexist. Pairing always goes through the approval modal; the URI is validated before any UI shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00
${blockedNote}
<div class="field">
<div class="lbl">Sign with</div>
<select id="pkConnectWallet" style="width:100%;padding:7px 9px;border-radius:7px;background:var(--panel);border:1px solid var(--line);color:var(--ink);font-size:13px">${options}</select>
</div>
<div class="field">
<input type="text" id="pkConnectUri" spellcheck="false" placeholder="wiz://?p=…&s=…">
</div>
feat(theseus+aegis): WizardConnect auto-detection — wiz:// links + page scan Completes the three detection paths. The injected provider shipped in 0.8.8; these two needed host support, because nothing in the add-on API could reach the active tab's content (captureTab is pixels, not DOM). wiz:// links (main.js) A click on a wiz:// anchor is intercepted in will-navigate and in the window-open handler (target="_blank" lands there instead), and routed to the wallet with the offering page's origin attached, so the approval names the real site. The tab never navigates. This needs nothing from the dapp beyond rendering the URI as a link, so it works for third-party dapps that will never adopt a Silent Mode API. scan-page capability (addons-host.js + main.js) New capability backing api.scanActiveTabForUris({scheme, limit}). Deliberately NOT a "read the page" API: the host runs the match and returns only the URIs found, so an add-on holding this still cannot see page text, markup or form values. It sits well below page-inject on the trust ladder — it learns that a page offers a wiz:// code and nothing else. Scheme is validated against [a-z][a-z0-9+.-]* and the result count is capped. The matcher also accepts WizardConnect's QR-alphanumeric spelling (WIZ://%3FP%3D…), which is frequently the only form present when a dapp renders its pairing code as a QR, and decodes it. Verified against the SDK: decodeKeyExchangeURI accepts standard, QR-raw and QR-decoded alike. Regex sources are built host-side and passed as JSON rather than assembled inside the injected string — hand-escaping backslashes and quotes through two levels of literal was both wrong on the first attempt and unreviewable. Aegis Declares scan-page, adds the wcScanPage handler and a "Scan page" button next to Connect. A scan fills the URI field and stops there rather than pairing outright: the user still chooses which wallet signs and still presses Connect, because a scan that silently paired would carry far more consequence than the button implies. Older hosts without the capability get a clear "update Theseus" message instead of a dead button.
2026-09-23 00:16:49 +02:00
<div class="actions" style="gap:6px">
<button class="btn primary" id="pkConnectBtn">Connect</button>
feat(theseus+aegis): WizardConnect auto-detection — wiz:// links + page scan Completes the three detection paths. The injected provider shipped in 0.8.8; these two needed host support, because nothing in the add-on API could reach the active tab's content (captureTab is pixels, not DOM). wiz:// links (main.js) A click on a wiz:// anchor is intercepted in will-navigate and in the window-open handler (target="_blank" lands there instead), and routed to the wallet with the offering page's origin attached, so the approval names the real site. The tab never navigates. This needs nothing from the dapp beyond rendering the URI as a link, so it works for third-party dapps that will never adopt a Silent Mode API. scan-page capability (addons-host.js + main.js) New capability backing api.scanActiveTabForUris({scheme, limit}). Deliberately NOT a "read the page" API: the host runs the match and returns only the URIs found, so an add-on holding this still cannot see page text, markup or form values. It sits well below page-inject on the trust ladder — it learns that a page offers a wiz:// code and nothing else. Scheme is validated against [a-z][a-z0-9+.-]* and the result count is capped. The matcher also accepts WizardConnect's QR-alphanumeric spelling (WIZ://%3FP%3D…), which is frequently the only form present when a dapp renders its pairing code as a QR, and decodes it. Verified against the SDK: decodeKeyExchangeURI accepts standard, QR-raw and QR-decoded alike. Regex sources are built host-side and passed as JSON rather than assembled inside the injected string — hand-escaping backslashes and quotes through two levels of literal was both wrong on the first attempt and unreviewable. Aegis Declares scan-page, adds the wcScanPage handler and a "Scan page" button next to Connect. A scan fills the URI field and stops there rather than pairing outright: the user still chooses which wallet signs and still presses Connect, because a scan that silently paired would carry far more consequence than the button implies. Older hosts without the capability get a clear "update Theseus" message instead of a dead button.
2026-09-23 00:16:49 +02:00
<button class="btn" id="pkConnectScanBtn" title="Look for a wiz:// pairing code on the dapp tab you have open">Scan page</button>
</div>
<div class="msg err" id="pkConnectMsg" hidden></div>
<div class="lbl" style="margin-top:14px">Paired dapps</div>
${rowsHtml}
</div>`;
}
function wireConnectPane() {
chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page Wallet strip: - Clicking a coin opens that coin's page (addresses, price, totals, back and close) instead of only flipping the selection and leaving the list sitting there. The page already existed but was reachable only via the small count chip. - The per-coin second action was a gear that selected the wallet and opened the global Settings tab — the same destination for every coin, so it read as a per-coin control that wasn't one. It is now Remove, behind a confirm, with the default/legacy wallet showing a lock instead since it gates legacy funds. - Each address in the drilldown can expand to show what THAT address holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via tokenBalances. walletSummary now carries both per wallet, so the view no longer has to borrow the selected wallet's assets. WizardConnect — the Connect pane was effectively unusable: - The locked-vault branch told the user to unlock and gave them nothing to click. It is reachable without the lock screen ever appearing, because a mounted imported wallet makes overallPhase read "ready". It now carries the same unlock form the lock screen uses. - Imported BCH wallets were never registered with the WC manager — startForWallet ran only in the vault-derived mount branch. They mount as ready, so they appeared in the "Sign with" picker and then failed on pair. They now register from their stored seed. WC derives a child key tree, so single-key (WIF) imports genuinely cannot pair; those are disabled in the picker with the reason, rather than failing on click. - Adds window.wizardconnect so a dapp can hand over the wiz:// URI it already generated instead of making the user copy it between tabs. The protocol is Nostr-relay pairing designed for phone-scans-QR, and the SDK has no in-page discovery at all, so this is our own surface: connect() + isReady(), plus a wizardconnect:announceProvider event shaped like EIP-6963 so several WC wallets can coexist. Pairing always goes through the approval modal; the URI is validated before any UI shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00
// Locked-vault branch: unlock in place, then re-render the pane so the
// pairing form replaces the gate without the user reopening the picker.
const unlockBtn = document.getElementById("pkConnectUnlockBtn");
if (unlockBtn) {
const doUnlock = async () => {
const pwEl = document.getElementById("pkConnectUnlockPw");
const msg = document.getElementById("pkConnectUnlockMsg");
msg.hidden = true;
const pw = pwEl.value;
if (!pw) return;
try {
state = await S.invoke("vaultUnlock", { masterPassword: pw });
pwEl.value = "";
render();
fillPicker();
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
};
unlockBtn.addEventListener("click", (e) => { e.stopPropagation(); doUnlock(); });
const pwEl = document.getElementById("pkConnectUnlockPw");
if (pwEl) {
pwEl.addEventListener("keydown", (e) => { e.stopPropagation(); if (e.key === "Enter") doUnlock(); });
try { pwEl.focus(); } catch {}
}
return;
}
const btn = document.getElementById("pkConnectBtn"); if (!btn) return;
btn.addEventListener("click", async () => {
const walletId = document.getElementById("pkConnectWallet").value;
const uri = document.getElementById("pkConnectUri").value.trim();
const msg = document.getElementById("pkConnectMsg"); msg.hidden = true;
if (!uri) return;
try {
state = await S.invoke("wcConnect", { walletId, uri });
document.getElementById("pkConnectUri").value = "";
fillPicker();
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
});
feat(theseus+aegis): WizardConnect auto-detection — wiz:// links + page scan Completes the three detection paths. The injected provider shipped in 0.8.8; these two needed host support, because nothing in the add-on API could reach the active tab's content (captureTab is pixels, not DOM). wiz:// links (main.js) A click on a wiz:// anchor is intercepted in will-navigate and in the window-open handler (target="_blank" lands there instead), and routed to the wallet with the offering page's origin attached, so the approval names the real site. The tab never navigates. This needs nothing from the dapp beyond rendering the URI as a link, so it works for third-party dapps that will never adopt a Silent Mode API. scan-page capability (addons-host.js + main.js) New capability backing api.scanActiveTabForUris({scheme, limit}). Deliberately NOT a "read the page" API: the host runs the match and returns only the URIs found, so an add-on holding this still cannot see page text, markup or form values. It sits well below page-inject on the trust ladder — it learns that a page offers a wiz:// code and nothing else. Scheme is validated against [a-z][a-z0-9+.-]* and the result count is capped. The matcher also accepts WizardConnect's QR-alphanumeric spelling (WIZ://%3FP%3D…), which is frequently the only form present when a dapp renders its pairing code as a QR, and decodes it. Verified against the SDK: decodeKeyExchangeURI accepts standard, QR-raw and QR-decoded alike. Regex sources are built host-side and passed as JSON rather than assembled inside the injected string — hand-escaping backslashes and quotes through two levels of literal was both wrong on the first attempt and unreviewable. Aegis Declares scan-page, adds the wcScanPage handler and a "Scan page" button next to Connect. A scan fills the URI field and stops there rather than pairing outright: the user still chooses which wallet signs and still presses Connect, because a scan that silently paired would carry far more consequence than the button implies. Older hosts without the capability get a clear "update Theseus" message instead of a dead button.
2026-09-23 00:16:49 +02:00
const scanBtn = document.getElementById("pkConnectScanBtn");
if (scanBtn) scanBtn.addEventListener("click", async (e) => {
e.stopPropagation();
const msg = document.getElementById("pkConnectMsg"); msg.hidden = true;
const field = document.getElementById("pkConnectUri");
const prev = scanBtn.textContent;
scanBtn.textContent = "Scanning…"; scanBtn.disabled = true;
try {
const res = await S.invoke("wcScanPage");
const uris = res?.uris || [];
if (!uris.length) {
// A dapp drops its pairing code from the DOM once it is connected,
// so the commonest reason a scan comes up empty is that the page is
// ALREADY paired. Telling that user to "open the Connect dialog"
// sends them looking for a dialog the dapp will not show again.
const alreadyPaired = Object.entries(state?.wc || {})
.flatMap(([wid, conns]) => (conns || []).map((c) => ({ ...c, walletId: wid })));
const nameOf = (c) => c.dappName || c.label || "a dapp";
const walletLabel = (wid) => (state?.wallets || []).find((w) => w.id === wid)?.label || wid;
const where = res?.origin ? ` on ${res.origin}` : " on the open tab";
msg.textContent = alreadyPaired.length
? `No wiz:// pairing code found${where}. Aegis already has ${nameOf(alreadyPaired[0])} paired on ${walletLabel(alreadyPaired[0].walletId)} — a dapp removes its code once it is connected, so if this page shows itself as connected there is nothing left to scan. To pair a different wallet, disconnect on both sides first.`
: `No wiz:// pairing code found${where}. Open the dapp's Connect dialog first, then scan again.`;
feat(theseus+aegis): WizardConnect auto-detection — wiz:// links + page scan Completes the three detection paths. The injected provider shipped in 0.8.8; these two needed host support, because nothing in the add-on API could reach the active tab's content (captureTab is pixels, not DOM). wiz:// links (main.js) A click on a wiz:// anchor is intercepted in will-navigate and in the window-open handler (target="_blank" lands there instead), and routed to the wallet with the offering page's origin attached, so the approval names the real site. The tab never navigates. This needs nothing from the dapp beyond rendering the URI as a link, so it works for third-party dapps that will never adopt a Silent Mode API. scan-page capability (addons-host.js + main.js) New capability backing api.scanActiveTabForUris({scheme, limit}). Deliberately NOT a "read the page" API: the host runs the match and returns only the URIs found, so an add-on holding this still cannot see page text, markup or form values. It sits well below page-inject on the trust ladder — it learns that a page offers a wiz:// code and nothing else. Scheme is validated against [a-z][a-z0-9+.-]* and the result count is capped. The matcher also accepts WizardConnect's QR-alphanumeric spelling (WIZ://%3FP%3D…), which is frequently the only form present when a dapp renders its pairing code as a QR, and decodes it. Verified against the SDK: decodeKeyExchangeURI accepts standard, QR-raw and QR-decoded alike. Regex sources are built host-side and passed as JSON rather than assembled inside the injected string — hand-escaping backslashes and quotes through two levels of literal was both wrong on the first attempt and unreviewable. Aegis Declares scan-page, adds the wcScanPage handler and a "Scan page" button next to Connect. A scan fills the URI field and stops there rather than pairing outright: the user still chooses which wallet signs and still presses Connect, because a scan that silently paired would carry far more consequence than the button implies. Older hosts without the capability get a clear "update Theseus" message instead of a dead button.
2026-09-23 00:16:49 +02:00
msg.hidden = false;
return;
}
// Fill the field rather than pairing outright: the user still picks
// which wallet signs, and still presses Connect. A scan that silently
// paired would be a click with a much larger consequence than the
// button implies.
field.value = uris[0];
msg.textContent = uris.length > 1
? `Found ${uris.length} codes on ${res.origin || "the page"} — filled the first. Press Connect to pair.`
: `Found a pairing code on ${res.origin || "the page"}. Press Connect to pair.`;
msg.classList.remove("err");
msg.hidden = false;
} catch (err) {
msg.textContent = cleanErr(err);
msg.classList.add("err");
msg.hidden = false;
} finally {
scanBtn.textContent = prev; scanBtn.disabled = false;
}
});
document.querySelectorAll("[data-wcpick]").forEach((b) => b.addEventListener("click", async () => {
const [walletId, connId] = b.dataset.wcpick.split("|");
try { state = await S.invoke("wcDisconnect", { walletId, connId }); fillPicker(); }
catch (e) { const m = document.getElementById("pkConnectMsg"); m.textContent = cleanErr(e); m.hidden = false; }
}));
}
// Always-visible wallet strip at the top of the panel. Each existing wallet
// is a chip (click to switch). Trailing [+] opens the Add-only picker;
// trailing [⋯] opens Import / Connect / Manage. Existing wallets are NEVER
// duplicated inside the picker — the picker is for creation flows only now.
// Which coin groups are collapsed in the wallet strip. Persisted per-user in
// panel-scoped session state; not durable across restarts because the picker
// already opens on the currently-selected wallet's group (auto-expand below).
const collapsedGroups = new Set();
// Per-chain "which network is showing" pointer. Rows grouped by chain
// alone (BCH, BTC, ETH, …); this map picks which subnetwork's wallets
// the row surfaces. Missing entry → pickDefaultNetwork() below prefers
// mainnet when present, falls back to the first wallet's network.
// 0.8.0: persisted to localStorage under aegis/activeNetworks so a user
// who prefers Sepolia on ETH stays on Sepolia across reloads. Fresh
// installs (no persisted entry) still fall back to mainnet — a
// last-selected-when-known, mainnet-otherwise policy.
const activeNetworkByChain = new Map(
(function () {
try {
const raw = localStorage.getItem("aegis/activeNetworks");
const j = raw ? JSON.parse(raw) : null;
return j && typeof j === "object" ? Object.entries(j) : [];
} catch { return []; }
})(),
);
function persistActiveNetworks() {
try {
const obj = {}; for (const [k, v] of activeNetworkByChain) obj[k] = v;
localStorage.setItem("aegis/activeNetworks", JSON.stringify(obj));
} catch {}
}
// 0.7.5: analogous pointer for "which specific wallet is active" within
// a chain+network bucket. Row click uses this to select the right wallet
// instead of always drilling into the address list, and the count pill
// (▾) is what opens the picker to change it. Keyed as "<chain>:<network>".
const activeWalletBySubgroup = new Map();
// Legacy per-chain+network key, kept because stripView.groupKey (inline
// address view) still uses it, and reorderWallets writes wallet order
// grouped by it below.
function subgroupKeyFor(w) { return `${w.chain}:${w.network}`; }
// Short network suffix. Used both as the pill next to the ticker (when
// the active network is not mainnet) and inside the network-picker
// dropdown. Empty string means "call this network Mainnet in the menu"
// and skip the pill on the row itself.
const NETWORK_SHORT = {
"bch:mainnet": "", "bch:chipnet": "Chipnet",
"btc:mainnet": "", "btc:testnet3": "Testnet", "btc:signet": "Signet",
"eth:mainnet": "", "eth:sepolia": "Sepolia",
"trx:mainnet": "", "trx:nile": "Nile",
"sol:mainnet": "", "sol:devnet": "Devnet",
"dgb:mainnet": "",
"sc:mainnet": "",
};
function isTestnetNetwork(chain, network) {
return network !== "mainnet";
}
function networkLabelFor(chain, network, fallback) {
const key = `${chain}:${network}`;
const short = NETWORK_SHORT[key];
if (short !== undefined) return short || "Mainnet";
return fallback || network || "Mainnet";
}
// Sort order inside the network dropdown: mainnet first, then the rest
// in the order they appeared in the wallet list. Keeps the natural
// primary-first reading while never surprising the user with alpha sort.
function orderNetworks(nets) {
const out = [];
if (nets.includes("mainnet")) out.push("mainnet");
for (const n of nets) if (n !== "mainnet" && !out.includes(n)) out.push(n);
return out;
}
function pickDefaultNetwork(nets) {
if (nets.includes("mainnet")) return "mainnet";
return nets[0];
}
// Meta for a chain-level group. Depends on which subnetwork is active,
// so pass that in explicitly (renderWalletStrip has already resolved it).
function chainMetaFor(sampleWallet, activeNet) {
const w = sampleWallet;
const short = networkLabelFor(w.chain, activeNet, w.networkLabel);
const isMainnet = activeNet === "mainnet";
const isChipnet = w.chain === "bch" && activeNet === "chipnet";
return {
coinName: isMainnet ? w.ticker : `${w.ticker} ${short}`,
ticker: w.ticker,
networkShort: isMainnet ? "" : short,
networkLabel: short,
logo: w.logo,
testnet: !isMainnet,
chipnet: isChipnet,
chain: w.chain,
activeNetwork: activeNet,
};
}
function walletBalanceUnits(w) {
if (!w.balance) return 0;
if (typeof w.balance.confirmed === "string") {
return (BigInt(w.balance.confirmed || "0") + BigInt(w.balance.unconfirmed || "0")).toString();
}
return (w.balance.confirmed || 0) + (w.balance.unconfirmed || 0);
}
// Sum a group's balances into a single native-unit amount + fiat. BigInt-
// safe for SC/ETH-scale decimals (24, 18) via string paths.
function sumGroupUnits(gw) {
let bigTotal = null;
let numTotal = 0;
for (const w of gw) {
if (!w.balance) continue;
if (typeof w.balance.confirmed === "string" || typeof w.balance.unconfirmed === "string") {
const u = BigInt(w.balance.confirmed || "0") + BigInt(w.balance.unconfirmed || "0");
bigTotal = (bigTotal == null ? u : bigTotal + u);
} else {
numTotal += (w.balance.confirmed || 0) + (w.balance.unconfirmed || 0);
}
}
if (bigTotal != null) return bigTotal.toString();
return numTotal;
}
function renderWalletStrip() {
const el = $("walletStrip"); if (!el) return;
const wallets = state?.wallets || [];
const selId = state?.selectedWalletId;
// Bucket wallets by chain, then by network inside each chain. Order
// within a chain follows first-seen wallet, but the strip renders the
// active subnetwork's slice — see activeNetworkByChain above.
const chainGroups = new Map();
for (const w of wallets) {
if (!chainGroups.has(w.chain)) {
chainGroups.set(w.chain, { chain: w.chain, byNet: new Map(), all: [] });
}
const g = chainGroups.get(w.chain);
if (!g.byNet.has(w.network)) g.byNet.set(w.network, []);
g.byNet.get(w.network).push(w);
g.all.push(w);
}
// Inline addresses view still keys off `chain:network` — it lists the
// wallets under one specific subnetwork, not the whole chain — so its
// logic below stays subgroup-scoped.
if (stripView.mode === "addresses" && stripView.groupKey) {
const [subChain, subNet] = stripView.groupKey.split(":");
const cg = chainGroups.get(subChain);
const gw = cg?.byNet.get(subNet) || null;
if (!gw || !gw.length) { stripView = { mode: "coins", groupKey: null }; }
else {
const meta = chainMetaFor(gw[0], subNet);
return renderInlineCoinList(el, stripView.groupKey, { meta, wallets: gw });
}
}
const rows = [];
for (const [chain, cg] of chainGroups) {
const nets = orderNetworks(Array.from(cg.byNet.keys()));
let active = activeNetworkByChain.get(chain);
if (!active || !nets.includes(active)) active = pickDefaultNetwork(nets);
const gw = cg.byNet.get(active) || [];
const meta = chainMetaFor(gw[0], active);
const subKey = `${chain}:${active}`;
const groupHasSel = gw.some((w) => w.id === selId);
const unitPrice = priceFor(chain);
const priceTxt = unitPrice != null ? fmtFiat(unitPrice) : "—";
const totalUnits = sumGroupUnits(gw);
const decimals = gw[0].decimals || 8;
// Always render a number — 0 balances read as "0", not "—". Users
// seeing a dash next to a coin they just added assume Aegis failed
// to fetch; a clean "0" makes the "adapter connected, wallet just
// empty" state obvious. The em-dash still shows before the first
// fetch resolves, when the adapter hasn't emitted at all.
const totalNative = fmtBig(totalUnits || 0, decimals);
const totalUsd = usdOf(chain, totalUnits || 0, decimals);
const totalFiat = totalUsd != null ? fmtFiat(totalUsd) : "";
// Network pill sits inline with the ticker when the active network
// isn't mainnet. Chipnet gets the acid tint (BCH's friendly
// testnet); every other testnet uses amber. Mainnet renders no pill
// so the row stays visually quiet.
let pill = "";
if (meta.testnet) {
const cls = meta.chipnet ? "wchipnet" : "wtestnet";
const tip = `${meta.networkLabel} — testnet, coins have no market value`;
pill = `<span class="wnetpill ${cls}" title="${esc(tip)}">${esc(meta.networkLabel)}</span>`;
}
feat(aegis): 0.16.0 — Address & QR and Assets are peer chips, and the coin row loses its dropdown Two changes to the same idea: one way to do each thing, and the same shape for every coin. **Assets is no longer buried under the QR.** The Receive body stacked address → QR → assets in one column, so in a 380px panel the token list sat permanently below the fold behind 200px of QR — and a coin holding no tokens simply lost a section, which made every coin look structurally different. Address & QR and Assets are now peers behind a persistent chip row. The count rides on the chip, and a wallet with nothing says "No assets held by this wallet" instead of the section vanishing. That mattered more than it sounds: a chipnet wallet with 46 CashToken categories was indistinguishable from an empty one until 0.15.0, because the only signal was an absent card. The choice persists across re-renders and wallet switches — comparing token balances between wallets should not throw you back to the QR on every click. Switching back to Address redraws the QR, since a canvas sized while hidden comes out blank and renderReceive only repaints when the address changes. **The coin row's ▾ network dropdown is gone.** The coin view already carries real network chips (data-browse-net: they filter the wallet list and show a per-network count), so the popover was a second control for one job — and the only one that hid its options behind a click. Chips are the single network control now and the ticker is plain text again. Verified in the panel: chips switch panes and track their own state, the count shows, the empty state reads correctly, the QR survives the round trip, and no .wchev / .wcname.wswitchable remain in the DOM.
2026-09-28 23:05:48 +02:00
// 0.16.0: no ▾ network dropdown on the coin row. The coin view already
// carries real network chips (data-browse-net — they filter the list and
// show a per-network count), so a popover menu doing the same job was a
// second way to do one thing, and the only one that hid its options
// behind a click. Chips are the single network control now; the ticker
// is plain text again.
const multiNet = false;
const chevron = "";
const nameCls = "wcname";
const nameTitle = meta.coinName;
// Single wallet under the active network → click selects it. Multi-
// wallet: click selects the "active" wallet for this bucket (defaults
// to the currently-selected one if it's in the group, otherwise the
// first). The wgcount chip becomes a ▾ dropdown trigger that opens
// the inline addresses list — that's how you swap the active wallet.
const single = gw.length === 1;
// Pick the wallet the row will select on click. Precedence: previously
// selected in this bucket → globally selected wallet (if it's in gw)
// → first wallet in the group. Result is what the "active" pointer
// stores AND what the row's data-wstripid points at, so click always
// lands on a valid entry.
let activeWalletId = activeWalletBySubgroup.get(subKey) || null;
if (activeWalletId && !gw.some((w) => w.id === activeWalletId)) activeWalletId = null;
if (!activeWalletId && gw.some((w) => w.id === selId)) activeWalletId = selId;
if (!activeWalletId) activeWalletId = gw[0].id;
activeWalletBySubgroup.set(subKey, activeWalletId);
const walletId = activeWalletId;
const clickAction = `data-wstripid="${esc(walletId)}"`;
// Count chip carries the ▾ to signal the picker; single-wallet rows
// still get no chip.
const walletsChip = single ? "" : `<span class="wgcount wgpick" data-openlist="${esc(subKey)}" title="Choose a different ${esc(meta.ticker)} wallet">${gw.length} <span class="wgchev">▾</span></span>`;
chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page Wallet strip: - Clicking a coin opens that coin's page (addresses, price, totals, back and close) instead of only flipping the selection and leaving the list sitting there. The page already existed but was reachable only via the small count chip. - The per-coin second action was a gear that selected the wallet and opened the global Settings tab — the same destination for every coin, so it read as a per-coin control that wasn't one. It is now Remove, behind a confirm, with the default/legacy wallet showing a lock instead since it gates legacy funds. - Each address in the drilldown can expand to show what THAT address holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via tokenBalances. walletSummary now carries both per wallet, so the view no longer has to borrow the selected wallet's assets. WizardConnect — the Connect pane was effectively unusable: - The locked-vault branch told the user to unlock and gave them nothing to click. It is reachable without the lock screen ever appearing, because a mounted imported wallet makes overallPhase read "ready". It now carries the same unlock form the lock screen uses. - Imported BCH wallets were never registered with the WC manager — startForWallet ran only in the vault-derived mount branch. They mount as ready, so they appeared in the "Sign with" picker and then failed on pair. They now register from their stored seed. WC derives a child key tree, so single-key (WIF) imports genuinely cannot pair; those are disabled in the picker with the reason, rather than failing on click. - Adds window.wizardconnect so a dapp can hand over the wiz:// URI it already generated instead of making the user copy it between tabs. The protocol is Nostr-relay pairing designed for phone-scans-QR, and the SDK has no in-page discovery at all, so this is our own surface: connect() + isReady(), plus a wizardconnect:announceProvider event shaped like EIP-6963 so several WC wallets can coexist. Pairing always goes through the approval modal; the URI is validated before any UI shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00
// Actions row: on multi-wallet rows the ✎ / 🗑 target the ACTIVE
// wallet (not "the group") so the buttons still do something specific
// without needing a second click.
chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page Wallet strip: - Clicking a coin opens that coin's page (addresses, price, totals, back and close) instead of only flipping the selection and leaving the list sitting there. The page already existed but was reachable only via the small count chip. - The per-coin second action was a gear that selected the wallet and opened the global Settings tab — the same destination for every coin, so it read as a per-coin control that wasn't one. It is now Remove, behind a confirm, with the default/legacy wallet showing a lock instead since it gates legacy funds. - Each address in the drilldown can expand to show what THAT address holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via tokenBalances. walletSummary now carries both per wallet, so the view no longer has to borrow the selected wallet's assets. WizardConnect — the Connect pane was effectively unusable: - The locked-vault branch told the user to unlock and gave them nothing to click. It is reachable without the lock screen ever appearing, because a mounted imported wallet makes overallPhase read "ready". It now carries the same unlock form the lock screen uses. - Imported BCH wallets were never registered with the WC manager — startForWallet ran only in the vault-derived mount branch. They mount as ready, so they appeared in the "Sign with" picker and then failed on pair. They now register from their stored seed. WC derives a child key tree, so single-key (WIF) imports genuinely cannot pair; those are disabled in the picker with the reason, rather than failing on click. - Adds window.wizardconnect so a dapp can hand over the wiz:// URI it already generated instead of making the user copy it between tabs. The protocol is Nostr-relay pairing designed for phone-scans-QR, and the SDK has no in-page discovery at all, so this is our own surface: connect() + isReady(), plus a wizardconnect:announceProvider event shaped like EIP-6963 so several WC wallets can coexist. Pairing always goes through the approval modal; the URI is validated before any UI shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00
//
// 0.8.8: the second button used to be ⚙, which just selected the wallet
// and opened the global Settings tab — the same destination for every
// coin, so it read as a per-coin control that wasn't one. Removing a
// wallet is the action people actually wanted there.
// Same single-⋯ shape as the drilldown row: one control for "change
// this wallet", opening the manage modal that already holds rename,
// derivation path and remove. 0.9.3 merged the drilldown but left
// this row with the old ✎ + 🗑 pair, so the two views disagreed.
chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page Wallet strip: - Clicking a coin opens that coin's page (addresses, price, totals, back and close) instead of only flipping the selection and leaving the list sitting there. The page already existed but was reachable only via the small count chip. - The per-coin second action was a gear that selected the wallet and opened the global Settings tab — the same destination for every coin, so it read as a per-coin control that wasn't one. It is now Remove, behind a confirm, with the default/legacy wallet showing a lock instead since it gates legacy funds. - Each address in the drilldown can expand to show what THAT address holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via tokenBalances. walletSummary now carries both per wallet, so the view no longer has to borrow the selected wallet's assets. WizardConnect — the Connect pane was effectively unusable: - The locked-vault branch told the user to unlock and gave them nothing to click. It is reachable without the lock screen ever appearing, because a mounted imported wallet makes overallPhase read "ready". It now carries the same unlock form the lock screen uses. - Imported BCH wallets were never registered with the WC manager — startForWallet ran only in the vault-derived mount branch. They mount as ready, so they appeared in the "Sign with" picker and then failed on pair. They now register from their stored seed. WC derives a child key tree, so single-key (WIF) imports genuinely cannot pair; those are disabled in the picker with the reason, rather than failing on click. - Adds window.wizardconnect so a dapp can hand over the wiz:// URI it already generated instead of making the user copy it between tabs. The protocol is Nostr-relay pairing designed for phone-scans-QR, and the SDK has no in-page discovery at all, so this is our own surface: connect() + isReady(), plus a wizardconnect:announceProvider event shaped like EIP-6963 so several WC wallets can coexist. Pairing always goes through the approval modal; the URI is validated before any UI shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00
const activeW = gw.find((w) => w.id === walletId);
const canRemove = !(activeW?.isDefault || activeW?.isLegacy);
const rowMenu = canRemove
? `<button class="wact" data-wedit="${esc(walletId)}" title="Rename, derivation path, or remove this ${esc(meta.ticker)} wallet">⋯</button>`
: `<span class="wact" title="Default wallet — protects legacy funds; cannot be removed" style="opacity:.35;cursor:not-allowed">🔒</span>`;
chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page Wallet strip: - Clicking a coin opens that coin's page (addresses, price, totals, back and close) instead of only flipping the selection and leaving the list sitting there. The page already existed but was reachable only via the small count chip. - The per-coin second action was a gear that selected the wallet and opened the global Settings tab — the same destination for every coin, so it read as a per-coin control that wasn't one. It is now Remove, behind a confirm, with the default/legacy wallet showing a lock instead since it gates legacy funds. - Each address in the drilldown can expand to show what THAT address holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via tokenBalances. walletSummary now carries both per wallet, so the view no longer has to borrow the selected wallet's assets. WizardConnect — the Connect pane was effectively unusable: - The locked-vault branch told the user to unlock and gave them nothing to click. It is reachable without the lock screen ever appearing, because a mounted imported wallet makes overallPhase read "ready". It now carries the same unlock form the lock screen uses. - Imported BCH wallets were never registered with the WC manager — startForWallet ran only in the vault-derived mount branch. They mount as ready, so they appeared in the "Sign with" picker and then failed on pair. They now register from their stored seed. WC derives a child key tree, so single-key (WIF) imports genuinely cannot pair; those are disabled in the picker with the reason, rather than failing on click. - Adds window.wizardconnect so a dapp can hand over the wiz:// URI it already generated instead of making the user copy it between tabs. The protocol is Nostr-relay pairing designed for phone-scans-QR, and the SDK has no in-page discovery at all, so this is our own surface: connect() + isReady(), plus a wizardconnect:announceProvider event shaped like EIP-6963 so several WC wallets can coexist. Pairing always goes through the approval modal; the URI is validated before any UI shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00
rows.push(`<div class="wrow ${groupHasSel ? "on" : ""}" ${clickAction} data-groupkey="${esc(subKey)}" title="${esc(meta.coinName)} — open addresses" draggable="true" data-chain="${esc(chain)}">
<span class="wcell wclogo">${logoSvg(meta.logo, 16)}</span>
<span class="wcell ${nameCls}" data-netpicker="${esc(chain)}" title="${esc(nameTitle)}">
<span class="wtline">
<span class="wtck">${esc(meta.ticker)}</span>
${chevron}
${pill}
${walletsChip}
</span>
<span class="wcprice">${esc(priceTxt)}</span>
</span>
<span class="wcell"></span>
<span class="wcell wcamt">
<span class="wnative">${esc(totalNative)}</span>
${totalFiat ? `<span class="wfiat">${esc(totalFiat)}</span>` : ""}
</span>
<span class="wcell wcact">
${rowMenu}
</span>
</div>`);
}
// + Add / ⋯ More moved to the header's picker-actions in 0.6.31 — the
// strip now starts directly with coin rows, no waddwrap taking up space
// for buttons the user was already reaching for at the top of the panel.
el.innerHTML = rows.join("");
// Row body / ticker cell click → select single wallet or open list modal.
// We use event delegation via .wrow: check target inside for wact
// buttons AND the network picker first (they have their own handling)
// before doing the row action, so pressing ✎ or ⚙ or the ▾ chevron
// never accidentally re-selects the wallet.
el.querySelectorAll(".wrow").forEach((row) => row.addEventListener("click", async (e) => {
if (e.target.closest(".wact")) return;
if (e.target.closest(".wcname.wswitchable")) return;
// The count chip is now a picker trigger — clicks there open the
// address list without also firing the row-select.
const pickChip = e.target.closest(".wgpick[data-openlist]");
if (pickChip) {
e.stopPropagation();
stripView = { mode: "addresses", groupKey: pickChip.dataset.openlist };
renderWalletStrip();
return;
}
chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page Wallet strip: - Clicking a coin opens that coin's page (addresses, price, totals, back and close) instead of only flipping the selection and leaving the list sitting there. The page already existed but was reachable only via the small count chip. - The per-coin second action was a gear that selected the wallet and opened the global Settings tab — the same destination for every coin, so it read as a per-coin control that wasn't one. It is now Remove, behind a confirm, with the default/legacy wallet showing a lock instead since it gates legacy funds. - Each address in the drilldown can expand to show what THAT address holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via tokenBalances. walletSummary now carries both per wallet, so the view no longer has to borrow the selected wallet's assets. WizardConnect — the Connect pane was effectively unusable: - The locked-vault branch told the user to unlock and gave them nothing to click. It is reachable without the lock screen ever appearing, because a mounted imported wallet makes overallPhase read "ready". It now carries the same unlock form the lock screen uses. - Imported BCH wallets were never registered with the WC manager — startForWallet ran only in the vault-derived mount branch. They mount as ready, so they appeared in the "Sign with" picker and then failed on pair. They now register from their stored seed. WC derives a child key tree, so single-key (WIF) imports genuinely cannot pair; those are disabled in the picker with the reason, rather than failing on click. - Adds window.wizardconnect so a dapp can hand over the wiz:// URI it already generated instead of making the user copy it between tabs. The protocol is Nostr-relay pairing designed for phone-scans-QR, and the SDK has no in-page discovery at all, so this is our own surface: connect() + isReady(), plus a wizardconnect:announceProvider event shaped like EIP-6963 so several WC wallets can coexist. Pairing always goes through the approval modal; the URI is validated before any UI shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00
// 0.8.8: clicking a coin opens that coin's page (addresses + assets)
// instead of only flipping the selection and leaving the list in place.
// Selecting still happens, so Send/Receive/History follow the coin the
// user just opened — but the strip now navigates, which is what a row
// with a balance and a chevron looks like it should do.
if (row.dataset.openlist) {
stripView = { mode: "addresses", groupKey: row.dataset.openlist };
renderWalletStrip();
chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page Wallet strip: - Clicking a coin opens that coin's page (addresses, price, totals, back and close) instead of only flipping the selection and leaving the list sitting there. The page already existed but was reachable only via the small count chip. - The per-coin second action was a gear that selected the wallet and opened the global Settings tab — the same destination for every coin, so it read as a per-coin control that wasn't one. It is now Remove, behind a confirm, with the default/legacy wallet showing a lock instead since it gates legacy funds. - Each address in the drilldown can expand to show what THAT address holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via tokenBalances. walletSummary now carries both per wallet, so the view no longer has to borrow the selected wallet's assets. WizardConnect — the Connect pane was effectively unusable: - The locked-vault branch told the user to unlock and gave them nothing to click. It is reachable without the lock screen ever appearing, because a mounted imported wallet makes overallPhase read "ready". It now carries the same unlock form the lock screen uses. - Imported BCH wallets were never registered with the WC manager — startForWallet ran only in the vault-derived mount branch. They mount as ready, so they appeared in the "Sign with" picker and then failed on pair. They now register from their stored seed. WC derives a child key tree, so single-key (WIF) imports genuinely cannot pair; those are disabled in the picker with the reason, rather than failing on click. - Adds window.wizardconnect so a dapp can hand over the wiz:// URI it already generated instead of making the user copy it between tabs. The protocol is Nostr-relay pairing designed for phone-scans-QR, and the SDK has no in-page discovery at all, so this is our own surface: connect() + isReady(), plus a wizardconnect:announceProvider event shaped like EIP-6963 so several WC wallets can coexist. Pairing always goes through the approval modal; the URI is validated before any UI shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00
return;
}
if (row.dataset.wstripid) {
const id = row.dataset.wstripid;
const key = row.dataset.groupkey || null;
try {
if (id !== selId) { state = await S.invoke("selectWallet", { id }); settingsFilled = false; }
if (key) stripView = { mode: "addresses", groupKey: key };
render();
} catch (er) { showErr(cleanErr(er)); }
}
}));
// Ticker click on a multi-network chain → pop the network dropdown.
el.querySelectorAll(".wcname.wswitchable").forEach((cell) => cell.addEventListener("click", (e) => {
e.stopPropagation();
const chain = cell.dataset.netpicker;
const cg = chainGroups.get(chain); if (!cg) return;
openNetworkPicker(cell, chain, cg);
}));
el.querySelectorAll(".wact[data-wedit]").forEach((b) => b.addEventListener("click", (e) => {
e.stopPropagation();
const w = (state?.wallets || []).find((x) => x.id === b.dataset.wedit);
if (w) openWalletManageModal(w);
}));
el.querySelectorAll(".wact[data-openlist]").forEach((b) => b.addEventListener("click", (e) => {
e.stopPropagation();
stripView = { mode: "addresses", groupKey: b.dataset.openlist };
renderWalletStrip();
}));
// Drag & drop to reorder chains. Wallets sharing a chain stay contiguous
// regardless of subnetwork — every wallet under BCH moves as one block,
// mainnet + chipnet together — because the strip now presents one row
// per chain. The reorder is optimistic-persistent: we call reorderWallets,
// the addon writes storage, and the returned state re-renders the strip
// in the new order.
wireStripDragDrop(el, chainGroups);
}
// Anchored dropdown letting the user switch which subnetwork of a chain
// is showing on that chain's row. Every network under the chain gets a
// menu row with its own summed total, so the user can see all the
// balances before flipping. Only one menu can be open at a time.
let netMenuEl = null;
let netMenuDismiss = null;
function closeNetworkPicker() {
if (netMenuEl && netMenuEl.parentNode) netMenuEl.parentNode.removeChild(netMenuEl);
netMenuEl = null;
if (netMenuDismiss) {
document.removeEventListener("mousedown", netMenuDismiss, true);
document.removeEventListener("keydown", netMenuDismiss, true);
netMenuDismiss = null;
}
}
function openNetworkPicker(anchorEl, chain, chainGroup) {
closeNetworkPicker();
const nets = orderNetworks(Array.from(chainGroup.byNet.keys()));
let active = activeNetworkByChain.get(chain);
if (!active || !nets.includes(active)) active = pickDefaultNetwork(nets);
const items = nets.map((n) => {
const gw = chainGroup.byNet.get(n) || [];
const decimals = gw[0]?.decimals || 8;
const units = sumGroupUnits(gw);
const native = fmtBig(units || 0, decimals);
const isTest = n !== "mainnet";
// Mainnet is the chain itself — labelling it "Mainnet" reads as
// redundant next to the ticker. Show the plain ticker instead
// (e.g. "BCH"), and reserve the specific-network name for the
// testnets that need disambiguation (Chipnet / Sepolia / Nile / …).
const ticker = gw[0]?.ticker || chain.toUpperCase();
const label = isTest ? networkLabelFor(chain, n, n) : ticker;
const cls = isTest ? (chain === "bch" && n === "chipnet" ? "wchipnet" : "wtestnet") : "";
const on = n === active ? "on" : "";
return `<div class="nmitem ${on}" data-net="${esc(n)}">
<span class="nmname">
<span class="nmnet ${cls}">${esc(label)}</span>
<span class="nmcount">${gw.length}</span>
</span>
<span class="nmamt">${esc(native)} ${esc(ticker)}</span>
</div>`;
}).join("");
netMenuEl = document.createElement("div");
netMenuEl.className = "netmenu";
netMenuEl.innerHTML = items;
document.body.appendChild(netMenuEl);
const r = anchorEl.getBoundingClientRect();
const mr = netMenuEl.getBoundingClientRect();
const maxLeft = window.innerWidth - mr.width - 8;
const left = Math.max(8, Math.min(maxLeft, r.left));
const top = r.bottom + 4;
netMenuEl.style.left = left + "px";
netMenuEl.style.top = top + "px";
netMenuEl.querySelectorAll(".nmitem").forEach((it) => it.addEventListener("click", (e) => {
e.stopPropagation();
const n = it.dataset.net;
closeNetworkPicker();
if (!n || n === active) return;
activeNetworkByChain.set(chain, n);
persistActiveNetworks();
renderWalletStrip();
}));
netMenuDismiss = (e) => {
if (e.type === "keydown" && e.key !== "Escape") return;
if (e.type === "mousedown" && netMenuEl && netMenuEl.contains(e.target)) return;
closeNetworkPicker();
};
// Defer wiring so the click that opened the menu doesn't immediately close it.
setTimeout(() => {
document.addEventListener("mousedown", netMenuDismiss, true);
document.addEventListener("keydown", netMenuDismiss, true);
}, 0);
}
function wireStripDragDrop(el, chainGroups) {
const chainKeys = Array.from(chainGroups.keys());
let dragChain = null;
el.querySelectorAll(".wrow[draggable=true]").forEach((row) => {
row.addEventListener("dragstart", (e) => {
dragChain = row.dataset.chain || null;
if (!dragChain) return;
row.classList.add("dragging");
try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", dragChain); } catch {}
});
row.addEventListener("dragend", () => {
row.classList.remove("dragging");
el.querySelectorAll(".wrow.drop-before, .wrow.drop-after").forEach((r) => r.classList.remove("drop-before", "drop-after"));
dragChain = null;
});
row.addEventListener("dragover", (e) => {
if (!dragChain || row.dataset.chain === dragChain) return;
e.preventDefault();
try { e.dataTransfer.dropEffect = "move"; } catch {}
const rect = row.getBoundingClientRect();
const before = (e.clientY - rect.top) < rect.height / 2;
el.querySelectorAll(".wrow.drop-before, .wrow.drop-after").forEach((r) => r.classList.remove("drop-before", "drop-after"));
row.classList.add(before ? "drop-before" : "drop-after");
});
row.addEventListener("dragleave", () => {
row.classList.remove("drop-before", "drop-after");
});
row.addEventListener("drop", async (e) => {
e.preventDefault();
const targetChain = row.dataset.chain;
const before = row.classList.contains("drop-before");
row.classList.remove("drop-before", "drop-after");
if (!dragChain || !targetChain || dragChain === targetChain) return;
const next = chainKeys.filter((k) => k !== dragChain);
const at = next.indexOf(targetChain);
next.splice(before ? at : at + 1, 0, dragChain);
// Flatten chain order to a wallet ID list. Inside each chain,
// mainnet wallets come first followed by testnets, matching the
// dropdown's own order. Individual wallets keep their existing
// relative order inside each subnetwork.
const walletOrder = [];
for (const k of next) {
const cg = chainGroups.get(k); if (!cg) continue;
const nets = orderNetworks(Array.from(cg.byNet.keys()));
for (const n of nets) for (const w of cg.byNet.get(n)) walletOrder.push(w.id);
}
try { state = await S.invoke("reorderWallets", { order: walletOrder }); render(); }
catch (er) { showErr(cleanErr(er)); }
});
});
}
fix(aegis): 0.9.8 — address row columns, resizable QR, inline copy The row's grid template still described the pre-0.9.2 layout — six columns including an address cell that no longer exists — while the row renders five. Every cell therefore sat one column left of where it belonged: copy landed in the label's space, the amount in the old label column, and the ⋯ in the amount column instead of the edge. That is the whole cause of "the copy button collides with the amount" and "the last edit button is not on the edge". Now five columns for five cells: icon · label · copy · amount · menu, 8px gaps. The label is the flexible one, left-aligned, so it takes the slack and truncates rather than squeezing the number. Verified at 520, 400 and 300px: no cell overlap at any width, no row overflow, and the amount never clips — only long labels give way. The copy glyph was the 📋 emoji, which has no glyph in this platform's font stack and rendered as a tofu box that read like a stray character stuck to the balance. Replaced with an inline SVG in both the row and the address card. Both confirmation flashes now swap innerHTML rather than textContent, which would have deleted the SVG and left a blank square. Address card: the address and its copy button share one row, so the button sits at the end of the value it copies. The address clamps to two lines and truncates beyond that instead of growing the card in a narrow sidebar. The QR is always visible and the panel is drag-resizable from a grip under it (pointer events, arrow keys as a non-mouse path, clamped 90–420px, capped against the panel's own width so a size set on a wide sidebar cannot overflow a narrow one, persisted). That replaces the 0.9.2 show/hide toggle — a size set once beats a binary, and it frees the row Copy was sharing with a QR button. drawQr was setting cv.style.width/height, which would have reset the panel to its intrinsic size on every redraw — i.e. every time the address changed. It now sets only the backing store and re-asserts the user's size after drawing. "Next unused address" is correct at the adapter level on both mainnet and chipnet (tested: the index advances and the address changes), so the reported failure is elsewhere. The handler was discarding the error and flashing a bare "Failed", which is why there was nothing to diagnose; it now surfaces the real message.
2026-09-24 22:30:10 +02:00
// Copy glyph as inline SVG. The 📋 emoji has no glyph in this platform's
// font stack, so it rendered as a tofu box that read like a stray
// character stuck to the balance beside it.
const COPY_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h8"/></svg>`;
const CHECK_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>`;
// Inline replacement for the modal address list. Rendered directly into
// the wallet strip element when stripView.mode === "addresses". Header
// row has a back arrow (returns to the coins summary) and the coin's
2026-09-23 02:49:05 +02:00
// name/logo; each body row is icon · label · copy · amount · one ⋯ that
// opens the manage modal (rename / path / remove).
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
// Assets live in the Receive tab's Assets card, not here — see 0.9.2.
function renderInlineCoinList(el, groupKey, group) {
const { meta, wallets: gw } = group;
const selId = state?.selectedWalletId;
const chain = gw[0]?.chain;
const decimals = gw[0]?.decimals || 8;
const unitPrice = priceFor(chain);
const priceTxt = unitPrice != null ? fmtFiat(unitPrice) : "—";
const totalUnits = sumGroupUnits(gw);
const totalNative = fmtBig(totalUnits || 0, decimals);
const totalUsd = usdOf(chain, totalUnits || 0, decimals);
const totalFiat = totalUsd != null ? fmtFiat(totalUsd) : "";
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
const multiAddr = gw.length > 1;
const rows = gw.map((w) => {
const on = w.id === selId ? "on" : "";
const units = walletBalanceUnits(w);
// Always render a numeric balance — see the same rationale in the
// coins summary render. 0 reads as "0", not "—".
const bal = fmtBig(units || 0, w.decimals);
const usd = usdOf(w.chain, units || 0, w.decimals);
const fiat = usd != null ? fmtFiat(usd) : "";
// Address is the row's primary identifier — mono, ellipsised in
// whatever flex space is left after the fixed cells. The BCH
// "bitcoincash:" / "bchtest:" / "bchreg:" prefix is stripped for the
// in-row display (it's identical on every row of a drilldown and
// burns 8-9 chars of a fixed column), but the tooltip and clipboard
// carry the full canonical form so the truncation is display-only.
const fullAddr = w.address ? String(w.address) : "";
const displayAddr = stripAddrPrefix(fullAddr);
// Label preview capped at ~8 visible chars in JS; the CSS pill's
// fixed 68px column handles final ellipsis for oddball wide glyphs.
const shortName = shortLabel(w.label || "", 8);
// Fiat is dropped from the row to keep everything on one line; the
// aggregate coin fiat still shows in the drilldown header above.
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
// 0.9.2: the per-address asset list added in 0.8.8 duplicated the
// Receive tab's Assets card, so it's gone — assets live in one place.
// The truncated address is gone too: the full one sits at the top of
// Receive, and a shortened copy here was a second, less useful
// rendering of the same string. The row now carries identity (name)
// and, only when there is more than one address to compare, balance.
2026-09-23 02:49:05 +02:00
// 0.9.3: one row, one shape — icon · label · amount · one button.
// ✎ and 🗑 were two controls for what is really one idea ("change
// this wallet"), and the manage modal already holds rename,
// derivation path AND remove. A single ⋯ opens it, which also stops
// Remove sitting one stray click away from Rename.
const locked = w.isDefault || w.isLegacy;
const rowMenu = locked
? `<span class="wact" title="Default wallet — protects legacy funds; cannot be removed" style="opacity:.35;cursor:not-allowed">🔒</span>`
: `<button class="wact" data-lpedit="${esc(w.id)}" title="Rename, derivation path, or remove this wallet">⋯</button>`;
return `<div class="warow ${on}" data-listpick="${esc(w.id)}" title="${esc(w.label || fullAddr)}">
<span class="wcell">${logoSvg(meta.logo, 14)}</span>
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
<span class="walabel walabel-wide" title="${esc(w.label || "")}">${esc(w.label || shortName || "—")}</span>
fix(aegis): 0.9.8 — address row columns, resizable QR, inline copy The row's grid template still described the pre-0.9.2 layout — six columns including an address cell that no longer exists — while the row renders five. Every cell therefore sat one column left of where it belonged: copy landed in the label's space, the amount in the old label column, and the ⋯ in the amount column instead of the edge. That is the whole cause of "the copy button collides with the amount" and "the last edit button is not on the edge". Now five columns for five cells: icon · label · copy · amount · menu, 8px gaps. The label is the flexible one, left-aligned, so it takes the slack and truncates rather than squeezing the number. Verified at 520, 400 and 300px: no cell overlap at any width, no row overflow, and the amount never clips — only long labels give way. The copy glyph was the 📋 emoji, which has no glyph in this platform's font stack and rendered as a tofu box that read like a stray character stuck to the balance. Replaced with an inline SVG in both the row and the address card. Both confirmation flashes now swap innerHTML rather than textContent, which would have deleted the SVG and left a blank square. Address card: the address and its copy button share one row, so the button sits at the end of the value it copies. The address clamps to two lines and truncates beyond that instead of growing the card in a narrow sidebar. The QR is always visible and the panel is drag-resizable from a grip under it (pointer events, arrow keys as a non-mouse path, clamped 90–420px, capped against the panel's own width so a size set on a wide sidebar cannot overflow a narrow one, persisted). That replaces the 0.9.2 show/hide toggle — a size set once beats a binary, and it frees the row Copy was sharing with a QR button. drawQr was setting cv.style.width/height, which would have reset the panel to its intrinsic size on every redraw — i.e. every time the address changed. It now sets only the backing store and re-asserts the user's size after drawing. "Next unused address" is correct at the adapter level on both mainnet and chipnet (tested: the index advances and the address changes), so the reported failure is elsewhere. The handler was discarding the error and flashing a bare "Failed", which is why there was nothing to diagnose; it now surfaces the real message.
2026-09-24 22:30:10 +02:00
${fullAddr ? `<button class="wacopy" data-lpcopy="${esc(fullAddr)}" title="Copy full address (${esc(fullAddr)})">${COPY_ICON}</button>` : `<span></span>`}
2026-09-23 02:49:05 +02:00
<span class="waamt"><span>${esc(bal)}</span><span class="watkr">${esc(meta.ticker)}</span></span>
<span style="display:inline-flex;gap:2px;justify-self:end;align-items:center">${rowMenu}</span>
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
</div>`;
}).join("");
// Surface any adapter errors from the wallets in this group. If a fetch
// is failing (RPC unreachable, CORS block, rate limit) the display would
// silently show 0 without this — which is exactly what "why does my
// funded wallet still say 0" feels like from the user side.
const errs = gw.filter((w) => w.error).map((w) => `${shortLabel(w.label || w.address || "?", 6)}: ${w.error}`);
const errLine = errs.length ? `<div class="wcoinerr">⚠ ${esc(errs.join(" · "))}</div>` : "";
el.innerHTML = `
<div class="wcoinhead">
<button class="wback" id="stripBack" title="Back to coin list">← Back</button>
<span class="wctitle">${logoSvg(meta.logo, 16)} ${esc(meta.coinName)}<span class="wcount">· ${gw.length} address${gw.length === 1 ? "" : "es"}</span></span>
<button class="wback" id="stripRefresh" title="Refresh balances now">↻</button>
<button class="wback" id="stripBackX" title="Back to coin list">✕</button>
</div>
<div class="wcoinsub">
<span class="wprice">${esc(priceTxt)}<span class="wsep" style="margin-left:4px">/ ${esc(meta.ticker)}</span></span>
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
${multiAddr ? `<span class="wsep">·</span>
<span class="wtot">${esc(totalNative)} ${esc(meta.ticker)}</span>
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
${totalFiat ? `<span class="wtotfiat">(${esc(totalFiat)})</span>` : ""}` : ""}
</div>
${errLine}
<div class="walist">${rows}</div>
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
<div class="waddwrap">
<button id="stripAddMore" title="Add another ${esc(meta.coinName)} wallet">+ Add another ${esc(meta.ticker)}</button>
</div>`;
const back = () => { stripView = { mode: "coins", groupKey: null }; renderWalletStrip(); };
el.querySelector("#stripBack").addEventListener("click", back);
el.querySelector("#stripBackX").addEventListener("click", back);
// Manual refresh: force every wallet under this coin+network to
// re-poll now. Handy when a testnet faucet just delivered or a
// mainnet transfer is expected to have landed.
const refreshBtn = el.querySelector("#stripRefresh");
if (refreshBtn) refreshBtn.addEventListener("click", async () => {
if (refreshBtn.dataset.spinning === "1") return;
refreshBtn.dataset.spinning = "1";
const prev = refreshBtn.textContent;
refreshBtn.textContent = "…";
try {
const r = await S.invoke("refreshChain", { chain: gw[0]?.chain, network: gw[0]?.network });
const failed = (r?.results || []).filter((x) => !x.ok);
if (failed.length) refreshBtn.title = "Refresh failed: " + failed.map((f) => f.error).join("; ");
else refreshBtn.title = "Refresh balances now";
} catch (e) {
refreshBtn.title = "Refresh failed: " + (e?.message || e);
} finally {
refreshBtn.textContent = prev;
delete refreshBtn.dataset.spinning;
}
});
el.querySelectorAll("[data-listpick]").forEach((row) => row.addEventListener("click", async (e) => {
if (e.target.closest(".wact")) return;
if (e.target.closest(".wacopy")) return;
const id = row.dataset.listpick;
try { state = await S.invoke("selectWallet", { id }); settingsFilled = false; render(); }
catch (er) { showErr(cleanErr(er)); }
}));
// Address copy chip. Uses navigator.clipboard when available (the addon
// panel runs under file:// but Electron gives it clipboard access), and
// falls back to a textarea+execCommand for older stacks. Visual "copied"
// flash lasts ~1s so the user sees the click landed.
el.querySelectorAll(".wacopy[data-lpcopy]").forEach((b) => b.addEventListener("click", async (e) => {
e.stopPropagation();
const addr = b.dataset.lpcopy || "";
if (!addr) return;
try {
if (navigator.clipboard && navigator.clipboard.writeText) await navigator.clipboard.writeText(addr);
else {
const ta = document.createElement("textarea");
ta.value = addr; ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta); ta.select();
try { document.execCommand("copy"); } finally { ta.remove(); }
}
fix(aegis): 0.9.8 — address row columns, resizable QR, inline copy The row's grid template still described the pre-0.9.2 layout — six columns including an address cell that no longer exists — while the row renders five. Every cell therefore sat one column left of where it belonged: copy landed in the label's space, the amount in the old label column, and the ⋯ in the amount column instead of the edge. That is the whole cause of "the copy button collides with the amount" and "the last edit button is not on the edge". Now five columns for five cells: icon · label · copy · amount · menu, 8px gaps. The label is the flexible one, left-aligned, so it takes the slack and truncates rather than squeezing the number. Verified at 520, 400 and 300px: no cell overlap at any width, no row overflow, and the amount never clips — only long labels give way. The copy glyph was the 📋 emoji, which has no glyph in this platform's font stack and rendered as a tofu box that read like a stray character stuck to the balance. Replaced with an inline SVG in both the row and the address card. Both confirmation flashes now swap innerHTML rather than textContent, which would have deleted the SVG and left a blank square. Address card: the address and its copy button share one row, so the button sits at the end of the value it copies. The address clamps to two lines and truncates beyond that instead of growing the card in a narrow sidebar. The QR is always visible and the panel is drag-resizable from a grip under it (pointer events, arrow keys as a non-mouse path, clamped 90–420px, capped against the panel's own width so a size set on a wide sidebar cannot overflow a narrow one, persisted). That replaces the 0.9.2 show/hide toggle — a size set once beats a binary, and it frees the row Copy was sharing with a QR button. drawQr was setting cv.style.width/height, which would have reset the panel to its intrinsic size on every redraw — i.e. every time the address changed. It now sets only the backing store and re-asserts the user's size after drawing. "Next unused address" is correct at the adapter level on both mainnet and chipnet (tested: the index advances and the address changes), so the reported failure is elsewhere. The handler was discarding the error and flashing a bare "Failed", which is why there was nothing to diagnose; it now surfaces the real message.
2026-09-24 22:30:10 +02:00
// innerHTML, not textContent — the button holds an inline SVG now,
// and assigning textContent would destroy it and leave a blank
// square once the confirmation timed out.
b.classList.add("copied"); b.innerHTML = CHECK_ICON;
setTimeout(() => { b.classList.remove("copied"); b.innerHTML = COPY_ICON; }, 1000);
} catch {}
}));
el.querySelectorAll("[data-lpedit]").forEach((b) => b.addEventListener("click", (e) => {
e.stopPropagation();
const w = (state?.wallets || []).find((x) => x.id === b.dataset.lpedit);
if (w) openWalletManageModal(w);
}));
2026-09-23 02:49:05 +02:00
// Removing a wallet now happens inside the manage modal (the row's ⋯),
// so the row no longer emits its own remove button. The modal calls
// render() itself, which redraws the strip — including falling back to
// the coin list when the last address under a coin goes away.
el.querySelector("#stripAddMore").addEventListener("click", async () => {
// This used to create a fresh wallet immediately, on the reasoning
// that being inside a coin's address list made the intent
// unambiguous. It doesn't: "add another BCH wallet" is just as often
// "bring in the one I already have somewhere else". Creating instead
// of importing is not a harmless guess either — the user ends up with
// an empty new address and has to work out why their funds aren't
// there. So ask.
const first = gw[0];
const pick = await aegisChoose({
title: `Add another ${meta.ticker} wallet`,
// meta.coinName already carries the network ("TRX Nile"), so naming
// networkLabel again read "TRX Nile · Nile testnet".
body: `On <b>${esc(meta.coinName)}</b>.`,
options: [
{ id: "create", label: "Create a new wallet", hint: "Derived from your Theseus vault — nothing to write down", primary: true },
{ id: "import", label: "Import an existing wallet", hint: "BIP39 mnemonic, or a chain-native private key" },
],
});
if (!pick) return;
if (pick === "import") { openImportModal(first.chain); return; }
try {
state = await S.invoke("addWallet", { chain: first.chain, network: first.network });
settingsFilled = false; render();
} catch (er) { showErr(cleanErr(er)); }
});
}
// Small popover for the "⋯" chip on the strip. Lists Import / Connect /
// Manage / About without cluttering the strip itself.
function openMoreMenu() {
const overlay = document.createElement("div");
overlay.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:flex-start;justify-content:center;z-index:99998;padding-top:60px";
// "Manage current wallet" removed in 0.6.31 — the header's dedicated ✎
// chip already opens the same modal, and the header row itself remains
// a click target for the same thing. Two identical entry points were
// fine when they were the only path; three would just be clutter.
overlay.innerHTML = `
<div style="width:min(94vw,300px);background:var(--panel,#12161e);border:1px solid var(--line,#2a2f38);border-radius:10px;padding:6px;box-shadow:0 10px 40px rgba(0,0,0,.4)">
<button class="row" data-mm="import" style="width:100%;display:flex;align-items:center;gap:10px;padding:9px 10px;background:transparent;border:0;color:var(--ink);cursor:pointer;font:inherit;text-align:left">↓ Import an existing wallet</button>
<button class="row" data-mm="connect" style="width:100%;display:flex;align-items:center;gap:10px;padding:9px 10px;background:transparent;border:0;color:var(--ink);cursor:pointer;font:inherit;text-align:left">⚡ Connect via WizardConnect</button>
<hr style="border:0;border-top:1px solid var(--line);margin:4px 0">
<button class="row" data-mm="about" style="width:100%;display:flex;align-items:center;gap:10px;padding:9px 10px;background:transparent;border:0;color:var(--dim);cursor:pointer;font:inherit;text-align:left;font-size:12px">About Aegis · aegis.x</button>
</div>`;
document.body.appendChild(overlay);
const close = () => { try { overlay.remove(); } catch {} };
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
overlay.querySelectorAll("[data-mm]").forEach((b) => b.addEventListener("click", () => {
const action = b.dataset.mm;
close();
if (action === "import") openImportModal(null);
if (action === "connect") { pickerTab = "connect"; const d=$("drop"); d.hidden=false; fillPicker(); }
if (action === "about") openUrl("https://aegis.x/");
}));
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
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 ----------------------------------------------------------------
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
// Populate the full-panel lock screen with either a master-password form
// (nosetup / no-PIN locked) or a PIN pad (locked with a PIN configured).
// PIN mode falls back to master-password via a link at the bottom so a
// forgotten PIN never locks the user out of their own vault.
function renderLockScreen(phase) {
const title = $("lockTitle");
const sub = $("lockSub");
const body = $("lockBody");
if (phase === "nosetup") {
title.textContent = "Set up Aegis";
sub.textContent = "Pick a master password — every Aegis wallet is derived from it. The same master password on another machine recreates the same addresses.";
body.innerHTML = `
<div class="lockform">
<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). 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>`;
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);
return;
}
// Locked phase. Two shapes:
// 1) PIN configured → 6-digit pad. Falls back to master-password
// entry if the user clicks "Use master password".
// 2) No PIN → master-password entry directly.
const hasPin = !!securityState?.hasPin;
const forcePw = body.dataset.forcePw === "1";
title.textContent = "Unlock Aegis";
sub.textContent = "Aegis derives its keys from your Theseus vault. There's nothing separate to unlock — the vault is your wallet.";
if (hasPin && !forcePw) {
body.innerHTML = `
<div class="pinpad" id="lockPinPad">
<div class="pindots" id="lockPinDots">${"<span class=\"pindot\"></span>".repeat(6)}</div>
<div class="pinkeys" id="lockPinKeys">
${[1,2,3,4,5,6,7,8,9].map((n) => `<button data-k="${n}">${n}</button>`).join("")}
<button class="util" data-k="clear">Clear</button>
<button data-k="0">0</button>
<button class="util" data-k="back">⌫</button>
</div>
<div class="pinerr" id="lockPinErr"></div>
</div>
<div class="altline"><a id="lockUsePw">Use master password instead</a> · <a id="lockGoSettingsFromPin">Settings</a></div>`;
setupPinPad({
dots: $("lockPinDots"),
keys: $("lockPinKeys"),
err: $("lockPinErr"),
onComplete: async (pin) => {
const remain = await pinLockoutRemainingMs();
if (remain > 0) {
$("lockPinErr").textContent = `Too many failed attempts. Try again in ${Math.ceil(remain / 60000)} min or use the master password.`;
return "reset";
}
try {
const blob = await S.invoke("pinBlobGet");
if (!blob) throw new Error("PIN not set");
const pw = await pinDecryptMaster(pin, blob);
state = await S.invoke("vaultUnlock", { masterPassword: pw });
await S.invoke("pinFailReset");
render();
return "ok";
} catch (e) {
const fs = await S.invoke("pinFailInc").catch(() => ({ count: 0 }));
const left = Math.max(0, PIN_MAX_FAILS - (fs?.count || 0));
$("lockPinErr").textContent = left > 0
? `Wrong PIN. ${left} attempt${left === 1 ? "" : "s"} left before a 15 min lockout.`
: `Locked for 15 min — use the master password instead.`;
return "reset";
}
},
});
$("lockUsePw").addEventListener("click", () => { body.dataset.forcePw = "1"; renderLockScreen("locked"); });
if ($("lockGoSettingsFromPin")) $("lockGoSettingsFromPin").addEventListener("click", () => showTab("settings"));
return;
}
// Master-password entry.
body.innerHTML = `
<div class="lockform">
<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>
<div class="altline">
${hasPin ? `<a id="lockUsePin">Use PIN instead</a> · ` : ""}<a id="lockGoSettings">Settings</a>
</div>`;
const doUnlock = async () => {
const pw = $("gateUnlockPw").value;
const msg = $("gateUnlockMsg"); msg.hidden = true;
if (!pw) return;
try {
state = await S.invoke("vaultUnlock", { masterPassword: pw });
// Remember the master password for a moment so the user can, right
// after unlock, enroll a PIN without re-typing it. Cleared as soon
// as the panel navigates or reloads.
window.__aegisLastPw = pw;
setTimeout(() => { try { delete window.__aegisLastPw; } catch {} }, 60_000);
body.dataset.forcePw = "";
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 (hasPin && $("lockUsePin")) $("lockUsePin").addEventListener("click", () => { body.dataset.forcePw = ""; renderLockScreen("locked"); });
if ($("lockGoSettings")) $("lockGoSettings").addEventListener("click", () => showTab("settings"));
}
// Wire up a PIN pad instance. `onComplete(pin)` runs when 6 digits are
// typed and must return "ok" (leave state) or "reset" (clear back to
// empty). Rendered by renderLockScreen for the unlock flow and by the
// PIN modal helper for set / verify flows.
function setupPinPad({ dots, keys, err, onComplete }) {
let buf = "";
const paint = () => {
const nodes = dots.querySelectorAll(".pindot");
nodes.forEach((n, i) => n.classList.toggle("on", i < buf.length));
};
keys.querySelectorAll("button[data-k]").forEach((b) => b.addEventListener("click", async () => {
const k = b.dataset.k;
if (err) err.textContent = "";
if (k === "clear") { buf = ""; paint(); return; }
if (k === "back") { buf = buf.slice(0, -1); paint(); return; }
if (buf.length >= 6) return;
buf += k;
paint();
if (buf.length === 6) {
keys.querySelectorAll("button").forEach((x) => x.disabled = true);
let res = "reset";
try { res = await onComplete(buf); }
finally {
keys.querySelectorAll("button").forEach((x) => x.disabled = false);
if (res !== "ok") { buf = ""; paint(); }
}
}
}));
// Keyboard fallback — some users prefer typing 6 digits fast.
const keyHandler = async (e) => {
if (!dots.isConnected) { document.removeEventListener("keydown", keyHandler); return; }
if (err) err.textContent = "";
if (/^[0-9]$/.test(e.key)) {
if (buf.length >= 6) return;
buf += e.key; paint();
if (buf.length === 6) {
keys.querySelectorAll("button").forEach((x) => x.disabled = true);
let res = "reset";
try { res = await onComplete(buf); }
finally {
keys.querySelectorAll("button").forEach((x) => x.disabled = false);
if (res !== "ok") { buf = ""; paint(); }
}
}
} else if (e.key === "Backspace") { buf = buf.slice(0, -1); paint(); }
else if (e.key === "Escape") { buf = ""; paint(); }
};
document.addEventListener("keydown", keyHandler);
}
function render() {
if (!state) return;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const s = sel();
const ready = s && s.phase === "ready";
const phase = s?.phase;
// Locked / no-setup take over the whole panel — the wallet strip, tab
// bar and per-wallet views would show either nothing or partial data,
// so we hide them behind an opaque overlay until the vault is open.
const fullLock = phase === "locked" || phase === "nosetup";
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
// 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);
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const gate = $("gate");
gate.hidden = ready || onSettings || fullLock;
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
if (onSettings) fillSettings();
const lockScreen = $("lockScreen");
const showLock = fullLock && !onSettings;
lockScreen.hidden = !showLock;
// Hide the rest of the panel behind the lock overlay. Settings stays
// open even while locked (users can set up PIN policy without unlocking
// first), so a lock override does NOT hide the tab bar when the user
// has clicked into Settings.
const hideChrome = fullLock && !onSettings;
document.querySelector("header").hidden = hideChrome;
document.querySelector("nav").hidden = hideChrome;
$("walletStrip").hidden = hideChrome;
// Re-drawing the strip after unhiding keeps the coins/addresses view
// in sync with the current wallet set.
if (!hideChrome) renderWalletStrip();
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.
2026-09-07 01:07:31 +02:00
// 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);
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("hLabel").textContent = s?.label || "Aegis Wallet";
// 0.8.4: hNet is a chip on its own row. Show only the network name
// (coin name is already in hLabel above); hide the row when we don't
// have any wallet-context to describe yet.
if (s?.meta) {
$("hNet").innerHTML = `${esc(s.meta.networkLabel)}${s.meta.testnet ? " " + testnetTag() : ""}`;
$("hNetRow").hidden = false;
} else {
$("hNet").innerHTML = "";
$("hNetRow").hidden = true;
}
if (showLock) {
renderLockScreen(phase);
}
if (!ready && !fullLock) {
const copy = {
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
error: ["⚠", "This wallet could not start.", s?.error || ""],
empty: ["🛡", "No wallets yet.", "Aegis can derive a fresh wallet from your Theseus password vault — nothing extra to write down — or import one you already have from its seed phrase or private key."],
}[phase] || ["…", "Starting…", ""];
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
let form = "";
if (phase === "empty") {
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
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");
// First-run is the MOST important place to offer import: someone
// arriving with an existing seed who is handed a create-only flow
// ends up staring at an empty wallet wondering where their coins
// went. Method chooser, not straight to create.
pickerTab = "method";
d.hidden = false;
fillPicker();
});
}
}
const dot = $("dot");
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
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) {
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
// 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) {
setBalMain("—"); $("balTicker").textContent = s.meta.ticker;
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
$("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);
setBalMain(fmtBig(total));
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("balTicker").textContent = s.meta.ticker;
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
const uc = s.balance?.unconfirmed;
if (uc && uc !== "0" && uc !== 0) $("netlbl").textContent += ` · ${fmtBig(uc)} unconfirmed`;
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
// 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;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
} else {
setBalMain("—"); $("balTicker").textContent = "";
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
$("balFiat").hidden = true;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
}
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
renderPortfolio();
if (!ready) return;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const addr = s.address || "";
if ($("addr").textContent !== addr) {
$("addr").textContent = addr;
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
drawQr(qrPayload(chain(), addr, sel()?.network));
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("addrMeta").textContent = s.addressPath ? "· " + s.addressPath : "";
$("nextAddr").hidden = chain() !== "bch";
$("openFaucet").hidden = !s.faucet;
feat(theseus/aegis): SPL token support (view balances + send) SPL tokens now show up in the Solana wallet — balances on the Receive card, an asset picker on Send that flips the amount input into the token's own units. Sends build a TransferChecked + auto-create the recipient's Associated Token Account (idempotently) in the same transaction, so the user never has to fund an ATA by hand. - lib/sol-spl.js: SPL primitives that don't need @solana/web3.js. TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, findProgramAddress (PDA loop backed by an ed25519 is-on-curve check via @noble Point.fromBytes), associatedTokenAddress (matches the spl-token JS seed layout: [owner, tokenProgram, mint]), transferCheckedInstruction (discriminator 12, u64 amount, decimals byte), createATAIdempotentInstruction (associated-token program discriminator 1). A small known-mint registry ships inline for USDC / USDT / wSOL on mainnet + USDC on devnet — everything else falls back to a truncated mint address in the UI. - Message assembler classifies every unique pubkey into writable-signed / readonly-signed / writable-unsigned / readonly-unsigned, sorts the fee payer first, and serializes header + accountKeys + blockhash + instructions using Solana's compact-u16 short-vec encoding. Same wire shape @solana/web3.js produces from Transaction.serializeMessage. - lib/chain-sol.js: snapshot() now carries a tokens[] array of {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram, isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against both the classic Token program and Token-2022. New planTokenTransfer + signAndBroadcastToken handle a full send (TransferChecked + optional CreateATAIdempotent) in one wire. - Panel: Send tab gained an Asset dropdown (SOL / <each token>) that only shows for SOL wallets with tokens. Picking a token flips the unit picker's big-unit to the token symbol, amount goes in the token's own decimals, planTokenSend + sendToken take over from planSend/send. Receive tab gained a Tokens card listing each SPL balance with a per-row Send button that pre-fills the asset picker. - Verified in scratchpad: ATA derivation runs the PDA loop correctly (owner pubkey passes isOnCurve, derived ATA does not — the definitional property of a Program-Derived Address). Cross-check the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS and the value matches. Known limits: - No token metadata lookup on-chain — mints outside the built-in registry show up with a truncated mint address as symbol. Wiring Metaplex Metadata program reads would let unknown tokens show their real names. - Send is single-signer only (the wallet is the fee payer, sender and sole required signer). Multi-sig SPL transfers work via the dapp bridge (window.solana.signAndSendTransaction, which already handles partial signatures).
2026-09-07 23:55:09 +02:00
// Render SPL tokens list (SOL wallets only). Sending a token clicks
// through to the Send tab with that asset pre-picked.
renderTokens();
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
applyUnitPicker();
$("feeField").hidden = chain() !== "bch";
// OP_RETURN memo is BCH-only (added 0.6.36). Every other chain hides
// the field completely so the Send tab stays consistent.
const memoEl = $("memoField"); if (memoEl) memoEl.hidden = chain() !== "bch";
renderHistory();
// 0.8.0: consolidate is a MODE TOGGLE on Send + Receive, not a chip.
// Show the toggle when there's at least one same-network sibling; the
// count sits inside the button label. paintSendMode() / paintRcvMode()
// swap the body between normal and consolidate views.
const cur = sel();
const others = (state?.wallets || []).filter((w) =>
cur && w.chain === cur.chain && w.network === cur.network && w.id !== state.selectedWalletId,
);
const showToggle = others.length > 0;
// Send keeps its toggle. Receive's became a button in the address actions
// (0.18.0), so it is shown/counted the same way but is not a toggle.
for (const [wrapId, cntId] of [["sendModeToggle", "sendConsolidateCount"], ["rcvConsolidateBtn", "rcvConsolidateCount"]]) {
const wrap = $(wrapId); if (!wrap) continue;
wrap.hidden = !showToggle;
if (showToggle) {
const c = $(cntId); if (c) c.textContent = `${others.length}`;
}
}
// Reset to normal mode when the toggle disappears (no siblings left).
if (!showToggle) { sendMode = "send"; rcvMode = "receive"; }
paintSendMode();
paintRcvMode();
feat(aegis): 0.16.0 — Address & QR and Assets are peer chips, and the coin row loses its dropdown Two changes to the same idea: one way to do each thing, and the same shape for every coin. **Assets is no longer buried under the QR.** The Receive body stacked address → QR → assets in one column, so in a 380px panel the token list sat permanently below the fold behind 200px of QR — and a coin holding no tokens simply lost a section, which made every coin look structurally different. Address & QR and Assets are now peers behind a persistent chip row. The count rides on the chip, and a wallet with nothing says "No assets held by this wallet" instead of the section vanishing. That mattered more than it sounds: a chipnet wallet with 46 CashToken categories was indistinguishable from an empty one until 0.15.0, because the only signal was an absent card. The choice persists across re-renders and wallet switches — comparing token balances between wallets should not throw you back to the QR on every click. Switching back to Address redraws the QR, since a canvas sized while hidden comes out blank and renderReceive only repaints when the address changes. **The coin row's ▾ network dropdown is gone.** The coin view already carries real network chips (data-browse-net: they filter the wallet list and show a per-network count), so the popover was a second control for one job — and the only one that hid its options behind a click. Chips are the single network control now and the ticker is plain text again. Verified in the panel: chips switch panes and track their own state, the count shows, the empty state reads correctly, the QR survives the round trip, and no .wchev / .wcname.wswitchable remain in the DOM.
2026-09-28 23:05:48 +02:00
paintRcvView();
}
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
// 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 || [];
// Show whenever prices are on and at least one wallet exists — the single-
// wallet case still benefits from a portfolio row when the balance-line
// fiat is elided (e.g. header hidden during pane switches).
if (!state?.prices?.enabled || !wallets.length) { el.hidden = true; return; }
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
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}`;
}
feat(aegis): 0.16.0 — Address & QR and Assets are peer chips, and the coin row loses its dropdown Two changes to the same idea: one way to do each thing, and the same shape for every coin. **Assets is no longer buried under the QR.** The Receive body stacked address → QR → assets in one column, so in a 380px panel the token list sat permanently below the fold behind 200px of QR — and a coin holding no tokens simply lost a section, which made every coin look structurally different. Address & QR and Assets are now peers behind a persistent chip row. The count rides on the chip, and a wallet with nothing says "No assets held by this wallet" instead of the section vanishing. That mattered more than it sounds: a chipnet wallet with 46 CashToken categories was indistinguishable from an empty one until 0.15.0, because the only signal was an absent card. The choice persists across re-renders and wallet switches — comparing token balances between wallets should not throw you back to the QR on every click. Switching back to Address redraws the QR, since a canvas sized while hidden comes out blank and renderReceive only repaints when the address changes. **The coin row's ▾ network dropdown is gone.** The coin view already carries real network chips (data-browse-net: they filter the wallet list and show a per-network count), so the popover was a second control for one job — and the only one that hid its options behind a click. Chips are the single network control now and the ticker is plain text again. Verified in the panel: chips switch panes and track their own state, the count shows, the empty state reads correctly, the QR survives the round trip, and no .wchev / .wcname.wswitchable remain in the DOM.
2026-09-28 23:05:48 +02:00
// Assets is a chip now, not a card that disappears when the count is zero.
// The count rides on the chip and an empty wallet says so in the pane, so
// "this wallet holds nothing" is a visible answer rather than a missing
// section the user has to infer — which is exactly how 46 CashTokens managed
// to look like a working, empty wallet before 0.15.0.
function setAssetsCount(n) {
const chip = $("rcvAssetsCount");
if (chip) chip.textContent = n ? String(n) : "";
const card = $("tokensCard");
if (card) card.hidden = false;
if (!n) {
const el = $("tokensList");
if (el) el.innerHTML = `<div class="hint" style="padding:4px 2px">No assets held by this wallet.</div>`;
const cnt = $("tokensCount");
if (cnt) cnt.textContent = "";
}
}
fix(aegis): 0.17.0 — both BCMR registries were dead, and self-minted tokens can now be named Assets listed but every row read as a hex string. Three separate reasons. **Both default registries were dead.** Checked 2026-09-29: raw.githubusercontent.com/cashonize/registry/main/bcmr.json returns 404, and bcmr.salemkode.com does not resolve. So no token resolved a name on any chain, mainnet included — and the panel's `.catch(() => {})` meant the failure was completely silent, indistinguishable from a token nobody has registered. Replaced with OpenTokenRegistry, which answers and carries chipnet identities. A dead registry is now reported instead of swallowed: the reply says whether any registry answered, and the hint says so. **The tokens in question publish no metadata at all.** Not a wallet problem and not fixable by any registry list: their genesis transactions carry no OP_RETURN whatsoever, so there is no BCMR authchain to follow and no registry entry to find. Their names exist only inside the app that minted them. So a token can now be named locally, per category, stored under bcmr/local/<cat> and taking precedence over any registry — with a YOURS tag so a self-assigned name is never mistaken for a published one. Clearing both fields removes it and lets a registry entry show through again. Optional decimals, because a raw fungible amount with no scale is its own kind of wrong: 15000 became "150 GMX" once told there were two. **The unnamed row printed the category twice**, once as the name fallback and once as the sub-line. Unnamed rows now show the category as the identity and "unnamed token · N UTXOs" beneath it. The header comment also promised a bundled static registry fallback for well-known tokens. There is no such directory and no load path for one; the comment is gone rather than left to mislead. Verified against the real module: local names beat registry entries, a category with only a local name resolves instead of returning null, clearing restores the registry value, out-of-range decimals are dropped, and the new default registry resolves a real chipnet identity (OTRC, 6 decimals). In the panel: naming a token repaints it with the tag and rescales the amount, and a non-numeric decimals entry is refused with the modal left open.
2026-09-29 00:13:30 +02:00
// Name a token yourself. Self-minted categories publish nothing — no
// registry entry, and usually not even an OP_RETURN in the genesis — so
// without this they can only ever read as a hex string. Clearing both fields
// deletes the local name and lets any registry entry show through again.
function openTokenLabelModal(category, meta) {
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:100000;padding-top:24px";
const isLocal = !!meta?.local;
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:8px">
<div style="font-weight:600;flex:1">Name this token</div>
<button class="btn sm" id="tlClose" type="button">✕</button>
</div>
<div class="hint mono" style="margin-bottom:10px;word-break:break-all">${esc(category)}</div>
<div class="field">
<div class="lbl">Name</div>
<input type="text" id="tlName" maxlength="40" placeholder="e.g. Game.X credit" value="${esc(isLocal ? (meta?.name || "") : "")}">
</div>
<div class="field">
<div class="lbl">Ticker</div>
<input type="text" id="tlSymbol" maxlength="12" placeholder="e.g. GMX" value="${esc(isLocal ? (meta?.symbol || "") : "")}">
</div>
<div class="field">
<div class="lbl">Decimals</div>
<input type="text" id="tlDecimals" inputmode="numeric" placeholder="0" value="${esc(isLocal && Number.isFinite(meta?.decimals) ? String(meta.decimals) : "")}">
<div class="hint">How many decimal places the amount should be shown with. Leave blank for whole units.</div>
</div>
<div class="hint" style="margin-top:8px">Stored on this device only. It takes precedence over any BCMR registry; clear both fields to remove it.</div>
<div class="msg err" id="tlMsg" hidden style="margin-top:8px"></div>
<div class="actions" style="justify-content:space-between;margin-top:12px">
${isLocal ? `<button class="btn danger" id="tlClear">Remove name</button>` : `<span></span>`}
<div style="display:flex;gap:6px">
<button class="btn" id="tlCancel">Cancel</button>
<button class="btn primary" id="tlSave">Save</button>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
const close = () => { try { overlay.remove(); } catch (_e) {} };
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
overlay.querySelector("#tlClose").addEventListener("click", close);
overlay.querySelector("#tlCancel").addEventListener("click", close);
const save = async (clear) => {
const msg = overlay.querySelector("#tlMsg"); msg.hidden = true;
const dRaw = overlay.querySelector("#tlDecimals").value.trim();
if (!clear && dRaw && !/^\d{1,2}$/.test(dRaw)) {
msg.textContent = "Decimals must be a whole number between 0 and 18."; msg.hidden = false; return;
}
try {
await S.invoke("setTokenLabel", {
category,
name: clear ? "" : overlay.querySelector("#tlName").value,
symbol: clear ? "" : overlay.querySelector("#tlSymbol").value,
decimals: clear || !dRaw ? undefined : Number(dRaw),
});
close();
renderTokens();
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
};
overlay.querySelector("#tlSave").addEventListener("click", () => save(false));
if (isLocal) overlay.querySelector("#tlClear").addEventListener("click", () => save(true));
overlay.querySelector("#tlName").focus();
}
feat(theseus/aegis): SPL token support (view balances + send) SPL tokens now show up in the Solana wallet — balances on the Receive card, an asset picker on Send that flips the amount input into the token's own units. Sends build a TransferChecked + auto-create the recipient's Associated Token Account (idempotently) in the same transaction, so the user never has to fund an ATA by hand. - lib/sol-spl.js: SPL primitives that don't need @solana/web3.js. TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, findProgramAddress (PDA loop backed by an ed25519 is-on-curve check via @noble Point.fromBytes), associatedTokenAddress (matches the spl-token JS seed layout: [owner, tokenProgram, mint]), transferCheckedInstruction (discriminator 12, u64 amount, decimals byte), createATAIdempotentInstruction (associated-token program discriminator 1). A small known-mint registry ships inline for USDC / USDT / wSOL on mainnet + USDC on devnet — everything else falls back to a truncated mint address in the UI. - Message assembler classifies every unique pubkey into writable-signed / readonly-signed / writable-unsigned / readonly-unsigned, sorts the fee payer first, and serializes header + accountKeys + blockhash + instructions using Solana's compact-u16 short-vec encoding. Same wire shape @solana/web3.js produces from Transaction.serializeMessage. - lib/chain-sol.js: snapshot() now carries a tokens[] array of {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram, isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against both the classic Token program and Token-2022. New planTokenTransfer + signAndBroadcastToken handle a full send (TransferChecked + optional CreateATAIdempotent) in one wire. - Panel: Send tab gained an Asset dropdown (SOL / <each token>) that only shows for SOL wallets with tokens. Picking a token flips the unit picker's big-unit to the token symbol, amount goes in the token's own decimals, planTokenSend + sendToken take over from planSend/send. Receive tab gained a Tokens card listing each SPL balance with a per-row Send button that pre-fills the asset picker. - Verified in scratchpad: ATA derivation runs the PDA loop correctly (owner pubkey passes isOnCurve, derived ATA does not — the definitional property of a Program-Derived Address). Cross-check the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS and the value matches. Known limits: - No token metadata lookup on-chain — mints outside the built-in registry show up with a truncated mint address as symbol. Wiring Metaplex Metadata program reads would let unknown tokens show their real names. - Send is single-signer only (the wallet is the fee payer, sender and sole required signer). Multi-sig SPL transfers work via the dapp bridge (window.solana.signAndSendTransaction, which already handles partial signatures).
2026-09-07 23:55:09 +02:00
function renderTokens() {
const s = sel();
const card = $("tokensCard");
const el = $("tokensList");
// SOL wallets: SPL tokens with a Send button (existing flow).
if (chain() === "sol") {
const tokens = s?.tokens || [];
feat(aegis): 0.16.0 — Address & QR and Assets are peer chips, and the coin row loses its dropdown Two changes to the same idea: one way to do each thing, and the same shape for every coin. **Assets is no longer buried under the QR.** The Receive body stacked address → QR → assets in one column, so in a 380px panel the token list sat permanently below the fold behind 200px of QR — and a coin holding no tokens simply lost a section, which made every coin look structurally different. Address & QR and Assets are now peers behind a persistent chip row. The count rides on the chip, and a wallet with nothing says "No assets held by this wallet" instead of the section vanishing. That mattered more than it sounds: a chipnet wallet with 46 CashToken categories was indistinguishable from an empty one until 0.15.0, because the only signal was an absent card. The choice persists across re-renders and wallet switches — comparing token balances between wallets should not throw you back to the QR on every click. Switching back to Address redraws the QR, since a canvas sized while hidden comes out blank and renderReceive only repaints when the address changes. **The coin row's ▾ network dropdown is gone.** The coin view already carries real network chips (data-browse-net: they filter the wallet list and show a per-network count), so the popover was a second control for one job — and the only one that hid its options behind a click. Chips are the single network control now and the ticker is plain text again. Verified in the panel: chips switch panes and track their own state, the count shows, the empty state reads correctly, the QR survives the round trip, and no .wchev / .wcname.wswitchable remain in the DOM.
2026-09-28 23:05:48 +02:00
setAssetsCount(tokens.length);
const hintEl = $("tokensHint");
if (hintEl) hintEl.textContent = "SPL tokens held by this wallet. Send by picking one under the Send tab's Asset dropdown.";
if (!tokens.length) return;
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");
}));
return;
}
// BCH wallets: CashTokens. Read-only display in 0.7.0 (spend ships in
// 0.7.1). Categories come pre-serialised from the wallet (fungible is
// a decimal string; NFTs are per-UTXO). Names/symbols/decimals/icons
// come from BCMR, fetched async; the first paint uses raw category hex.
if (chain() === "bch") {
const balances = s?.tokenBalances || {};
const cats = Object.keys(balances);
feat(aegis): 0.16.0 — Address & QR and Assets are peer chips, and the coin row loses its dropdown Two changes to the same idea: one way to do each thing, and the same shape for every coin. **Assets is no longer buried under the QR.** The Receive body stacked address → QR → assets in one column, so in a 380px panel the token list sat permanently below the fold behind 200px of QR — and a coin holding no tokens simply lost a section, which made every coin look structurally different. Address & QR and Assets are now peers behind a persistent chip row. The count rides on the chip, and a wallet with nothing says "No assets held by this wallet" instead of the section vanishing. That mattered more than it sounds: a chipnet wallet with 46 CashToken categories was indistinguishable from an empty one until 0.15.0, because the only signal was an absent card. The choice persists across re-renders and wallet switches — comparing token balances between wallets should not throw you back to the QR on every click. Switching back to Address redraws the QR, since a canvas sized while hidden comes out blank and renderReceive only repaints when the address changes. **The coin row's ▾ network dropdown is gone.** The coin view already carries real network chips (data-browse-net: they filter the wallet list and show a per-network count), so the popover was a second control for one job — and the only one that hid its options behind a click. Chips are the single network control now and the ticker is plain text again. Verified in the panel: chips switch panes and track their own state, the count shows, the empty state reads correctly, the QR survives the round trip, and no .wchev / .wcname.wswitchable remain in the DOM.
2026-09-28 23:05:48 +02:00
setAssetsCount(cats.length);
const hintEl = $("tokensHint");
if (hintEl) hintEl.textContent = "CashTokens held by this wallet. Names come from BCMR — configure custom registries in Settings.";
if (!cats.length) return;
const draw = (metaMap) => {
el.innerHTML = cats.map((cat) => {
const b = balances[cat];
const meta = metaMap?.[cat] || null;
const decimals = meta && Number.isFinite(meta.decimals) ? meta.decimals : 0;
const fungibleRaw = String(b.fungible || "0");
const showFungible = fungibleRaw !== "0";
const nftCount = Array.isArray(b.nfts) ? b.nfts.length : 0;
const name = meta?.name || meta?.symbol || null;
const symbol = meta?.symbol || "";
const icon = meta?.iconUri || "";
const iconHtml = icon ? `<img src="${esc(icon)}" alt="" style="width:18px;height:18px;border-radius:4px;vertical-align:middle;margin-right:6px" onerror="this.style.display='none'">` : "";
const catShort = cat.slice(0, 10) + "…" + cat.slice(-6);
const fungibleTxt = showFungible ? fmtTokenAmount(fungibleRaw, decimals) + (symbol ? " " + symbol : "") : "";
const nftTxt = nftCount ? `${nftCount} NFT${nftCount === 1 ? "" : "s"}` : "";
const amountLine = [fungibleTxt, nftTxt].filter(Boolean).join(" · ") || "—";
fix(aegis): 0.17.0 — both BCMR registries were dead, and self-minted tokens can now be named Assets listed but every row read as a hex string. Three separate reasons. **Both default registries were dead.** Checked 2026-09-29: raw.githubusercontent.com/cashonize/registry/main/bcmr.json returns 404, and bcmr.salemkode.com does not resolve. So no token resolved a name on any chain, mainnet included — and the panel's `.catch(() => {})` meant the failure was completely silent, indistinguishable from a token nobody has registered. Replaced with OpenTokenRegistry, which answers and carries chipnet identities. A dead registry is now reported instead of swallowed: the reply says whether any registry answered, and the hint says so. **The tokens in question publish no metadata at all.** Not a wallet problem and not fixable by any registry list: their genesis transactions carry no OP_RETURN whatsoever, so there is no BCMR authchain to follow and no registry entry to find. Their names exist only inside the app that minted them. So a token can now be named locally, per category, stored under bcmr/local/<cat> and taking precedence over any registry — with a YOURS tag so a self-assigned name is never mistaken for a published one. Clearing both fields removes it and lets a registry entry show through again. Optional decimals, because a raw fungible amount with no scale is its own kind of wrong: 15000 became "150 GMX" once told there were two. **The unnamed row printed the category twice**, once as the name fallback and once as the sub-line. Unnamed rows now show the category as the identity and "unnamed token · N UTXOs" beneath it. The header comment also promised a bundled static registry fallback for well-known tokens. There is no such directory and no load path for one; the comment is gone rather than left to mislead. Verified against the real module: local names beat registry entries, a category with only a local name resolves instead of returning null, clearing restores the registry value, out-of-range decimals are dropped, and the new default registry resolves a real chipnet identity (OTRC, 6 decimals). In the panel: naming a token repaints it with the tag and rescales the amount, and a non-numeric decimals entry is refused with the modal left open.
2026-09-29 00:13:30 +02:00
const localTag = meta?.local ? ` <span class="ttag" title="A name you set — no registry knows this token">YOURS</span>` : "";
// Named: name on top, category beneath. Unnamed: the category IS the
// only identity, so it goes on top and the sub-line says what it is
// rather than repeating the same hex twice.
const utxoTxt = b.utxoCount ? `${b.utxoCount} UTXO${b.utxoCount === 1 ? "" : "s"}` : "";
const nameLine = name
? `${iconHtml}<b>${esc(name)}</b>${symbol && name !== symbol ? ` <span class="hint">${esc(symbol)}</span>` : ""}${localTag}`
: `${iconHtml}<span class="mono">${esc(catShort)}</span>`;
const subLine = name
? `<span class="mono">${esc(catShort)}</span>`
: ["unnamed token", utxoTxt].filter(Boolean).join(" · ");
// ✎ per row: these tokens often publish no metadata at all, so
// naming one locally is the only way it will ever read as anything
// but a hex string.
return `<div class="tx" style="grid-template-columns:1fr auto auto;cursor:default;align-items:center">
<div><div>${nameLine}</div><div class="hint mono">${subLine}</div></div>
<div class="amt2 in" style="align-self:center;text-align:right">${esc(amountLine)}</div>
fix(aegis): 0.17.0 — both BCMR registries were dead, and self-minted tokens can now be named Assets listed but every row read as a hex string. Three separate reasons. **Both default registries were dead.** Checked 2026-09-29: raw.githubusercontent.com/cashonize/registry/main/bcmr.json returns 404, and bcmr.salemkode.com does not resolve. So no token resolved a name on any chain, mainnet included — and the panel's `.catch(() => {})` meant the failure was completely silent, indistinguishable from a token nobody has registered. Replaced with OpenTokenRegistry, which answers and carries chipnet identities. A dead registry is now reported instead of swallowed: the reply says whether any registry answered, and the hint says so. **The tokens in question publish no metadata at all.** Not a wallet problem and not fixable by any registry list: their genesis transactions carry no OP_RETURN whatsoever, so there is no BCMR authchain to follow and no registry entry to find. Their names exist only inside the app that minted them. So a token can now be named locally, per category, stored under bcmr/local/<cat> and taking precedence over any registry — with a YOURS tag so a self-assigned name is never mistaken for a published one. Clearing both fields removes it and lets a registry entry show through again. Optional decimals, because a raw fungible amount with no scale is its own kind of wrong: 15000 became "150 GMX" once told there were two. **The unnamed row printed the category twice**, once as the name fallback and once as the sub-line. Unnamed rows now show the category as the identity and "unnamed token · N UTXOs" beneath it. The header comment also promised a bundled static registry fallback for well-known tokens. There is no such directory and no load path for one; the comment is gone rather than left to mislead. Verified against the real module: local names beat registry entries, a category with only a local name resolves instead of returning null, clearing restores the registry value, out-of-range decimals are dropped, and the new default registry resolves a real chipnet identity (OTRC, 6 decimals). In the panel: naming a token repaints it with the tag and rescales the amount, and a non-numeric decimals entry is refused with the modal left open.
2026-09-29 00:13:30 +02:00
<button class="wact" data-tokname="${esc(cat)}" title="Name this token locally" style="align-self:center">✎</button>
</div>`;
}).join("");
fix(aegis): 0.17.0 — both BCMR registries were dead, and self-minted tokens can now be named Assets listed but every row read as a hex string. Three separate reasons. **Both default registries were dead.** Checked 2026-09-29: raw.githubusercontent.com/cashonize/registry/main/bcmr.json returns 404, and bcmr.salemkode.com does not resolve. So no token resolved a name on any chain, mainnet included — and the panel's `.catch(() => {})` meant the failure was completely silent, indistinguishable from a token nobody has registered. Replaced with OpenTokenRegistry, which answers and carries chipnet identities. A dead registry is now reported instead of swallowed: the reply says whether any registry answered, and the hint says so. **The tokens in question publish no metadata at all.** Not a wallet problem and not fixable by any registry list: their genesis transactions carry no OP_RETURN whatsoever, so there is no BCMR authchain to follow and no registry entry to find. Their names exist only inside the app that minted them. So a token can now be named locally, per category, stored under bcmr/local/<cat> and taking precedence over any registry — with a YOURS tag so a self-assigned name is never mistaken for a published one. Clearing both fields removes it and lets a registry entry show through again. Optional decimals, because a raw fungible amount with no scale is its own kind of wrong: 15000 became "150 GMX" once told there were two. **The unnamed row printed the category twice**, once as the name fallback and once as the sub-line. Unnamed rows now show the category as the identity and "unnamed token · N UTXOs" beneath it. The header comment also promised a bundled static registry fallback for well-known tokens. There is no such directory and no load path for one; the comment is gone rather than left to mislead. Verified against the real module: local names beat registry entries, a category with only a local name resolves instead of returning null, clearing restores the registry value, out-of-range decimals are dropped, and the new default registry resolves a real chipnet identity (OTRC, 6 decimals). In the panel: naming a token repaints it with the tag and rescales the amount, and a non-numeric decimals entry is refused with the modal left open.
2026-09-29 00:13:30 +02:00
el.querySelectorAll("[data-tokname]").forEach((b) => b.addEventListener("click", () => {
openTokenLabelModal(b.dataset.tokname, metaMap?.[b.dataset.tokname] || null);
}));
};
// Paint immediately with whatever we know synchronously so the row
// set doesn't wait for the network. Then run the async lookup and
// redraw with names + icons.
draw({});
S.invoke("tokenMetadata", { categories: cats }).then((res) => {
if (chain() !== "bch") return; // user switched away mid-fetch
fix(aegis): 0.17.0 — both BCMR registries were dead, and self-minted tokens can now be named Assets listed but every row read as a hex string. Three separate reasons. **Both default registries were dead.** Checked 2026-09-29: raw.githubusercontent.com/cashonize/registry/main/bcmr.json returns 404, and bcmr.salemkode.com does not resolve. So no token resolved a name on any chain, mainnet included — and the panel's `.catch(() => {})` meant the failure was completely silent, indistinguishable from a token nobody has registered. Replaced with OpenTokenRegistry, which answers and carries chipnet identities. A dead registry is now reported instead of swallowed: the reply says whether any registry answered, and the hint says so. **The tokens in question publish no metadata at all.** Not a wallet problem and not fixable by any registry list: their genesis transactions carry no OP_RETURN whatsoever, so there is no BCMR authchain to follow and no registry entry to find. Their names exist only inside the app that minted them. So a token can now be named locally, per category, stored under bcmr/local/<cat> and taking precedence over any registry — with a YOURS tag so a self-assigned name is never mistaken for a published one. Clearing both fields removes it and lets a registry entry show through again. Optional decimals, because a raw fungible amount with no scale is its own kind of wrong: 15000 became "150 GMX" once told there were two. **The unnamed row printed the category twice**, once as the name fallback and once as the sub-line. Unnamed rows now show the category as the identity and "unnamed token · N UTXOs" beneath it. The header comment also promised a bundled static registry fallback for well-known tokens. There is no such directory and no load path for one; the comment is gone rather than left to mislead. Verified against the real module: local names beat registry entries, a category with only a local name resolves instead of returning null, clearing restores the registry value, out-of-range decimals are dropped, and the new default registry resolves a real chipnet identity (OTRC, 6 decimals). In the panel: naming a token repaints it with the tag and rescales the amount, and a non-numeric decimals entry is refused with the modal left open.
2026-09-29 00:13:30 +02:00
// 0.17.0: the reply carries the map under .meta plus whether any
// registry actually answered. The old shape was the bare map and the
// failure path was .catch(() => {}), so two dead default registries
// went unnoticed — every token looked simply unregistered.
const map = res && res.meta ? res.meta : (res || {});
draw(map);
if (hintEl && res && res.registriesAnswered === false) {
hintEl.textContent = res.registries
? `No configured BCMR registry answered, so names are unavailable — check the registry list in Settings, or name a token yourself with ✎.`
: `No BCMR registry configured, so names are unavailable — add one in Settings, or name a token yourself with ✎.`;
}
}).catch((e) => {
if (hintEl) hintEl.textContent = `Token names unavailable: ${cleanErr(e)}. You can still name a token yourself with ✎.`;
});
return;
}
2026-09-23 02:49:05 +02:00
// Every other account-model chain (TRX, ETH, and anything added later)
// renders from the same `tokens` shape the adapters already return, so
// this is keyed on the DATA rather than on a list of chain ids — a new
// chain gets the asset list for free instead of silently falling
// through to a hidden card the way ETH did until 0.9.3.
{
const tokens = s?.tokens || [];
feat(aegis): 0.16.0 — Address & QR and Assets are peer chips, and the coin row loses its dropdown Two changes to the same idea: one way to do each thing, and the same shape for every coin. **Assets is no longer buried under the QR.** The Receive body stacked address → QR → assets in one column, so in a 380px panel the token list sat permanently below the fold behind 200px of QR — and a coin holding no tokens simply lost a section, which made every coin look structurally different. Address & QR and Assets are now peers behind a persistent chip row. The count rides on the chip, and a wallet with nothing says "No assets held by this wallet" instead of the section vanishing. That mattered more than it sounds: a chipnet wallet with 46 CashToken categories was indistinguishable from an empty one until 0.15.0, because the only signal was an absent card. The choice persists across re-renders and wallet switches — comparing token balances between wallets should not throw you back to the QR on every click. Switching back to Address redraws the QR, since a canvas sized while hidden comes out blank and renderReceive only repaints when the address changes. **The coin row's ▾ network dropdown is gone.** The coin view already carries real network chips (data-browse-net: they filter the wallet list and show a per-network count), so the popover was a second control for one job — and the only one that hid its options behind a click. Chips are the single network control now and the ticker is plain text again. Verified in the panel: chips switch panes and track their own state, the count shows, the empty state reads correctly, the QR survives the round trip, and no .wchev / .wcname.wswitchable remain in the DOM.
2026-09-28 23:05:48 +02:00
if (!tokens.length) { setAssetsCount(0); return; }
setAssetsCount(tokens.length);
2026-09-23 02:49:05 +02:00
const hintEl = $("tokensHint");
const kindLabel = chain() === "trx" ? "TRC20" : chain() === "eth" ? "ERC20" : "Tokens";
if (hintEl) hintEl.textContent = `${kindLabel} tokens held by this wallet. Read-only in this build — open the explorer from the header to move them.`;
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
// A symbol claimed by more than one contract is the lookalike pattern:
// spam mints borrow a trusted ticker so a careless send lands on the
// wrong contract. We can't tell which is genuine, so we don't guess —
// we mark every member of the clash and let the user check the address.
const symCount = new Map();
for (const t of tokens) {
const k = (t.symbol || "").toLowerCase();
if (t.known && k) symCount.set(k, (symCount.get(k) || 0) + 1);
}
const nativeUnits = walletBalanceUnits(sel()) || 0;
const nativeRow = `<div class="tx asset native" style="grid-template-columns:1fr auto;cursor:default;align-items:center">
<div><div><b>${esc(s.meta?.ticker || chain().toUpperCase())}</b> <span class="hint">${esc(s.meta?.coinLabel || "")}</span></div>
<div class="hint">native</div></div>
<div class="amt2 in" style="align-self:center;text-align:right">${esc(fmtBig(nativeUnits, s.decimals))}</div>
</div>`;
const cnt = $("tokensCount");
if (cnt) cnt.textContent = `· ${tokens.length + 1}`;
el.innerHTML = nativeRow + tokens.map((t) => {
const dec = Number(t.decimals) || 0;
const short = String(t.mint || "");
const addrLine = `${esc(short.slice(0, 10))}…${esc(short.slice(-6))}`;
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
const clash = t.known && symCount.get((t.symbol || "").toLowerCase()) > 1;
// Unknown contracts have no decimals, so a raw integer would be a
// misleading "balance". Say so instead of inventing a number.
const title = t.known
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
? `${esc(t.symbol)}${t.name ? ' <span class="hint">' + esc(t.name) + "</span>" : ""}${clash ? ' <span class="ttag tdupe" title="More than one contract here uses this symbol — check the address before sending">LOOK-ALIKE</span>' : ""}`
: `<span class="hint">Unknown token</span>`;
const amount = t.known
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
? `<span title="${esc(fmtTokenAmountExact(t.balance, dec))} ${esc(t.symbol)}">${esc(fmtTokenAmount(t.balance, dec))}</span>`
: `<span class="hint mono" title="No registry entry for this contract — raw on-chain amount, decimals unknown">${esc(t.balance)} raw</span>`;
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
return `<div class="tx asset" style="grid-template-columns:1fr auto;cursor:default;align-items:center">
<div><div>${title}</div><div class="hint mono" title="${esc(short)}">${addrLine}</div></div>
<div class="amt2 in" style="align-self:center;text-align:right">${amount}</div>
</div>`;
}).join("");
}
feat(theseus/aegis): SPL token support (view balances + send) SPL tokens now show up in the Solana wallet — balances on the Receive card, an asset picker on Send that flips the amount input into the token's own units. Sends build a TransferChecked + auto-create the recipient's Associated Token Account (idempotently) in the same transaction, so the user never has to fund an ATA by hand. - lib/sol-spl.js: SPL primitives that don't need @solana/web3.js. TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, findProgramAddress (PDA loop backed by an ed25519 is-on-curve check via @noble Point.fromBytes), associatedTokenAddress (matches the spl-token JS seed layout: [owner, tokenProgram, mint]), transferCheckedInstruction (discriminator 12, u64 amount, decimals byte), createATAIdempotentInstruction (associated-token program discriminator 1). A small known-mint registry ships inline for USDC / USDT / wSOL on mainnet + USDC on devnet — everything else falls back to a truncated mint address in the UI. - Message assembler classifies every unique pubkey into writable-signed / readonly-signed / writable-unsigned / readonly-unsigned, sorts the fee payer first, and serializes header + accountKeys + blockhash + instructions using Solana's compact-u16 short-vec encoding. Same wire shape @solana/web3.js produces from Transaction.serializeMessage. - lib/chain-sol.js: snapshot() now carries a tokens[] array of {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram, isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against both the classic Token program and Token-2022. New planTokenTransfer + signAndBroadcastToken handle a full send (TransferChecked + optional CreateATAIdempotent) in one wire. - Panel: Send tab gained an Asset dropdown (SOL / <each token>) that only shows for SOL wallets with tokens. Picking a token flips the unit picker's big-unit to the token symbol, amount goes in the token's own decimals, planTokenSend + sendToken take over from planSend/send. Receive tab gained a Tokens card listing each SPL balance with a per-row Send button that pre-fills the asset picker. - Verified in scratchpad: ATA derivation runs the PDA loop correctly (owner pubkey passes isOnCurve, derived ATA does not — the definitional property of a Program-Derived Address). Cross-check the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS and the value matches. Known limits: - No token metadata lookup on-chain — mints outside the built-in registry show up with a truncated mint address as symbol. Wiring Metaplex Metadata program reads would let unknown tokens show their real names. - Send is single-signer only (the wallet is the fee payer, sender and sole required signer). Multi-sig SPL transfers work via the dapp bridge (window.solana.signAndSendTransaction, which already handles partial signatures).
2026-09-07 23:55:09 +02:00
}
// 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;
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
const group = (w) => w.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
if (d === 0) return (neg ? "-" : "") + group(abs);
feat(theseus/aegis): SPL token support (view balances + send) SPL tokens now show up in the Solana wallet — balances on the Receive card, an asset picker on Send that flips the amount input into the token's own units. Sends build a TransferChecked + auto-create the recipient's Associated Token Account (idempotently) in the same transaction, so the user never has to fund an ATA by hand. - lib/sol-spl.js: SPL primitives that don't need @solana/web3.js. TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, findProgramAddress (PDA loop backed by an ed25519 is-on-curve check via @noble Point.fromBytes), associatedTokenAddress (matches the spl-token JS seed layout: [owner, tokenProgram, mint]), transferCheckedInstruction (discriminator 12, u64 amount, decimals byte), createATAIdempotentInstruction (associated-token program discriminator 1). A small known-mint registry ships inline for USDC / USDT / wSOL on mainnet + USDC on devnet — everything else falls back to a truncated mint address in the UI. - Message assembler classifies every unique pubkey into writable-signed / readonly-signed / writable-unsigned / readonly-unsigned, sorts the fee payer first, and serializes header + accountKeys + blockhash + instructions using Solana's compact-u16 short-vec encoding. Same wire shape @solana/web3.js produces from Transaction.serializeMessage. - lib/chain-sol.js: snapshot() now carries a tokens[] array of {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram, isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against both the classic Token program and Token-2022. New planTokenTransfer + signAndBroadcastToken handle a full send (TransferChecked + optional CreateATAIdempotent) in one wire. - Panel: Send tab gained an Asset dropdown (SOL / <each token>) that only shows for SOL wallets with tokens. Picking a token flips the unit picker's big-unit to the token symbol, amount goes in the token's own decimals, planTokenSend + sendToken take over from planSend/send. Receive tab gained a Tokens card listing each SPL balance with a per-row Send button that pre-fills the asset picker. - Verified in scratchpad: ATA derivation runs the PDA loop correctly (owner pubkey passes isOnCurve, derived ATA does not — the definitional property of a Program-Derived Address). Cross-check the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS and the value matches. Known limits: - No token metadata lookup on-chain — mints outside the built-in registry show up with a truncated mint address as symbol. Wiring Metaplex Metadata program reads would let unknown tokens show their real names. - Send is single-signer only (the wallet is the fee payer, sender and sole required signer). Multi-sig SPL transfers work via the dapp bridge (window.solana.signAndSendTransaction, which already handles partial signatures).
2026-09-07 23:55:09 +02:00
const pad = abs.padStart(d + 1, "0");
const whole = pad.slice(0, pad.length - d);
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
let frac = pad.slice(pad.length - d).replace(/0+$/, "");
// An 18-decimal token renders as 60000000.000000005435817984 without a
// cap — 26 digits of noise for a balance whose meaningful part is the
// first few. Keep enough to distinguish small amounts, drop the rest;
// the exact value stays available in the row's title attribute.
const CAP = 8;
let truncated = false;
if (frac.length > CAP) { frac = frac.slice(0, CAP).replace(/0+$/, ""); truncated = true; }
return (neg ? "-" : "") + group(whole) + (frac ? "." + frac : "") + (truncated && !frac ? "…" : "");
}
// Exact, ungrouped rendering for tooltips — what fmtTokenAmount hides.
function fmtTokenAmountExact(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");
feat(theseus/aegis): SPL token support (view balances + send) SPL tokens now show up in the Solana wallet — balances on the Receive card, an asset picker on Send that flips the amount input into the token's own units. Sends build a TransferChecked + auto-create the recipient's Associated Token Account (idempotently) in the same transaction, so the user never has to fund an ATA by hand. - lib/sol-spl.js: SPL primitives that don't need @solana/web3.js. TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, findProgramAddress (PDA loop backed by an ed25519 is-on-curve check via @noble Point.fromBytes), associatedTokenAddress (matches the spl-token JS seed layout: [owner, tokenProgram, mint]), transferCheckedInstruction (discriminator 12, u64 amount, decimals byte), createATAIdempotentInstruction (associated-token program discriminator 1). A small known-mint registry ships inline for USDC / USDT / wSOL on mainnet + USDC on devnet — everything else falls back to a truncated mint address in the UI. - Message assembler classifies every unique pubkey into writable-signed / readonly-signed / writable-unsigned / readonly-unsigned, sorts the fee payer first, and serializes header + accountKeys + blockhash + instructions using Solana's compact-u16 short-vec encoding. Same wire shape @solana/web3.js produces from Transaction.serializeMessage. - lib/chain-sol.js: snapshot() now carries a tokens[] array of {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram, isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against both the classic Token program and Token-2022. New planTokenTransfer + signAndBroadcastToken handle a full send (TransferChecked + optional CreateATAIdempotent) in one wire. - Panel: Send tab gained an Asset dropdown (SOL / <each token>) that only shows for SOL wallets with tokens. Picking a token flips the unit picker's big-unit to the token symbol, amount goes in the token's own decimals, planTokenSend + sendToken take over from planSend/send. Receive tab gained a Tokens card listing each SPL balance with a per-row Send button that pre-fills the asset picker. - Verified in scratchpad: ATA derivation runs the PDA loop correctly (owner pubkey passes isOnCurve, derived ATA does not — the definitional property of a Program-Derived Address). Cross-check the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS and the value matches. Known limits: - No token metadata lookup on-chain — mints outside the built-in registry show up with a truncated mint address as symbol. Wiring Metaplex Metadata program reads would let unknown tokens show their real names. - Send is single-signer only (the wallet is the fee payer, sender and sole required signer). Multi-sig SPL transfers work via the dapp bridge (window.solana.signAndSendTransaction, which already handles partial signatures).
2026-09-07 23:55:09 +02:00
const frac = pad.slice(pad.length - d).replace(/0+$/, "");
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
return (neg ? "-" : "") + pad.slice(0, pad.length - d) + (frac ? "." + frac : "");
feat(theseus/aegis): SPL token support (view balances + send) SPL tokens now show up in the Solana wallet — balances on the Receive card, an asset picker on Send that flips the amount input into the token's own units. Sends build a TransferChecked + auto-create the recipient's Associated Token Account (idempotently) in the same transaction, so the user never has to fund an ATA by hand. - lib/sol-spl.js: SPL primitives that don't need @solana/web3.js. TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, findProgramAddress (PDA loop backed by an ed25519 is-on-curve check via @noble Point.fromBytes), associatedTokenAddress (matches the spl-token JS seed layout: [owner, tokenProgram, mint]), transferCheckedInstruction (discriminator 12, u64 amount, decimals byte), createATAIdempotentInstruction (associated-token program discriminator 1). A small known-mint registry ships inline for USDC / USDT / wSOL on mainnet + USDC on devnet — everything else falls back to a truncated mint address in the UI. - Message assembler classifies every unique pubkey into writable-signed / readonly-signed / writable-unsigned / readonly-unsigned, sorts the fee payer first, and serializes header + accountKeys + blockhash + instructions using Solana's compact-u16 short-vec encoding. Same wire shape @solana/web3.js produces from Transaction.serializeMessage. - lib/chain-sol.js: snapshot() now carries a tokens[] array of {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram, isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against both the classic Token program and Token-2022. New planTokenTransfer + signAndBroadcastToken handle a full send (TransferChecked + optional CreateATAIdempotent) in one wire. - Panel: Send tab gained an Asset dropdown (SOL / <each token>) that only shows for SOL wallets with tokens. Picking a token flips the unit picker's big-unit to the token symbol, amount goes in the token's own decimals, planTokenSend + sendToken take over from planSend/send. Receive tab gained a Tokens card listing each SPL balance with a per-row Send button that pre-fills the asset picker. - Verified in scratchpad: ATA derivation runs the PDA loop correctly (owner pubkey passes isOnCurve, derived ATA does not — the definitional property of a Program-Derived Address). Cross-check the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS and the value matches. Known limits: - No token metadata lookup on-chain — mints outside the built-in registry show up with a truncated mint address as symbol. Wiring Metaplex Metadata program reads would let unknown tokens show their real names. - Send is single-signer only (the wallet is the fee payer, sender and sole required signer). Multi-sig SPL transfers work via the dapp bridge (window.solana.signAndSendTransaction, which already handles partial signatures).
2026-09-07 23:55:09 +02:00
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
function applyUnitPicker() {
const s = sel(); if (!s) return;
if (!unit) unit = "big";
feat(theseus/aegis): SPL token support (view balances + send) SPL tokens now show up in the Solana wallet — balances on the Receive card, an asset picker on Send that flips the amount input into the token's own units. Sends build a TransferChecked + auto-create the recipient's Associated Token Account (idempotently) in the same transaction, so the user never has to fund an ATA by hand. - lib/sol-spl.js: SPL primitives that don't need @solana/web3.js. TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, findProgramAddress (PDA loop backed by an ed25519 is-on-curve check via @noble Point.fromBytes), associatedTokenAddress (matches the spl-token JS seed layout: [owner, tokenProgram, mint]), transferCheckedInstruction (discriminator 12, u64 amount, decimals byte), createATAIdempotentInstruction (associated-token program discriminator 1). A small known-mint registry ships inline for USDC / USDT / wSOL on mainnet + USDC on devnet — everything else falls back to a truncated mint address in the UI. - Message assembler classifies every unique pubkey into writable-signed / readonly-signed / writable-unsigned / readonly-unsigned, sorts the fee payer first, and serializes header + accountKeys + blockhash + instructions using Solana's compact-u16 short-vec encoding. Same wire shape @solana/web3.js produces from Transaction.serializeMessage. - lib/chain-sol.js: snapshot() now carries a tokens[] array of {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram, isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against both the classic Token program and Token-2022. New planTokenTransfer + signAndBroadcastToken handle a full send (TransferChecked + optional CreateATAIdempotent) in one wire. - Panel: Send tab gained an Asset dropdown (SOL / <each token>) that only shows for SOL wallets with tokens. Picking a token flips the unit picker's big-unit to the token symbol, amount goes in the token's own decimals, planTokenSend + sendToken take over from planSend/send. Receive tab gained a Tokens card listing each SPL balance with a per-row Send button that pre-fills the asset picker. - Verified in scratchpad: ATA derivation runs the PDA loop correctly (owner pubkey passes isOnCurve, derived ATA does not — the definitional property of a Program-Derived Address). Cross-check the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS and the value matches. Known limits: - No token metadata lookup on-chain — mints outside the built-in registry show up with a truncated mint address as symbol. Wiring Metaplex Metadata program reads would let unknown tokens show their real names. - Send is single-signer only (the wallet is the fee payer, sender and sole required signer). Multi-sig SPL transfers work via the dapp bridge (window.solana.signAndSendTransaction, which already handles partial signatures).
2026-09-07 23:55:09 +02:00
// ---- 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();
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("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)));
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
$("sendTo").placeholder = ({
bch: s.network === "chipnet" ? "bchtest:q… or legacy m…" : "bitcoincash:q… or legacy 1…",
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
btc: s.network === "testnet" ? "tb1q… (or 2… / m…, n…)" : "bc1q… (or bc1p…, 3…, 1…)",
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
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";
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("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);
updateSendFiatPreview();
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
}
function amountUnits() {
const raw = $("sendAmt").value.trim().replace(/,/g, "");
if (!raw) return 0;
feat(theseus/aegis): SPL token support (view balances + send) SPL tokens now show up in the Solana wallet — balances on the Receive card, an asset picker on Send that flips the amount input into the token's own units. Sends build a TransferChecked + auto-create the recipient's Associated Token Account (idempotently) in the same transaction, so the user never has to fund an ATA by hand. - lib/sol-spl.js: SPL primitives that don't need @solana/web3.js. TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, findProgramAddress (PDA loop backed by an ed25519 is-on-curve check via @noble Point.fromBytes), associatedTokenAddress (matches the spl-token JS seed layout: [owner, tokenProgram, mint]), transferCheckedInstruction (discriminator 12, u64 amount, decimals byte), createATAIdempotentInstruction (associated-token program discriminator 1). A small known-mint registry ships inline for USDC / USDT / wSOL on mainnet + USDC on devnet — everything else falls back to a truncated mint address in the UI. - Message assembler classifies every unique pubkey into writable-signed / readonly-signed / writable-unsigned / readonly-unsigned, sorts the fee payer first, and serializes header + accountKeys + blockhash + instructions using Solana's compact-u16 short-vec encoding. Same wire shape @solana/web3.js produces from Transaction.serializeMessage. - lib/chain-sol.js: snapshot() now carries a tokens[] array of {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram, isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against both the classic Token program and Token-2022. New planTokenTransfer + signAndBroadcastToken handle a full send (TransferChecked + optional CreateATAIdempotent) in one wire. - Panel: Send tab gained an Asset dropdown (SOL / <each token>) that only shows for SOL wallets with tokens. Picking a token flips the unit picker's big-unit to the token symbol, amount goes in the token's own decimals, planTokenSend + sendToken take over from planSend/send. Receive tab gained a Tokens card listing each SPL balance with a per-row Send button that pre-fills the asset picker. - Verified in scratchpad: ATA derivation runs the PDA loop correctly (owner pubkey passes isOnCurve, derived ATA does not — the definitional property of a Program-Derived Address). Cross-check the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS and the value matches. Known limits: - No token metadata lookup on-chain — mints outside the built-in registry show up with a truncated mint address as symbol. Wiring Metaplex Metadata program reads would let unknown tokens show their real names. - Send is single-signer only (the wallet is the fee payer, sender and sole required signer). Multi-sig SPL transfers work via the dapp bridge (window.solana.signAndSendTransaction, which already handles partial signatures).
2026-09-07 23:55:09 +02:00
// 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;
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
if (unit === "small") {
if (bigDecimals) return raw.replace(/\D+/g, "") || "0";
return Math.round(Number(raw));
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const [w, f = ""] = raw.split(".");
const frac = (f + "0".repeat(d)).slice(0, d);
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
if (bigDecimals) {
const total = (BigInt(w || "0") * (10n ** BigInt(d))) + BigInt(frac || "0");
return total.toString();
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
return Number(w || 0) * Math.pow(10, d) + Number(frac || 0);
}
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
// 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);
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
// ---- history ---------------------------------------------------------------
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
// History asset filter (0.9.2). Both on by default — these narrow a
// combined feed, they don't opt into one.
let histShowNative = true;
let histShowTokens = true;
let histFilterWired = false;
function wireHistFilter() {
if (histFilterWired) return;
const n = $("histNative"), t = $("histTokens");
if (!n || !t) return;
histFilterWired = true;
// Unchecking both would leave an empty list with no hint why, so the
// last one standing stays checked.
const onChange = () => {
if (!n.checked && !t.checked) { n.checked = true; }
histShowNative = n.checked;
histShowTokens = t.checked;
renderHistory();
};
n.addEventListener("change", onChange);
t.addEventListener("change", onChange);
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
function renderHistory() {
const s = sel();
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
const all = s?.history || [];
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const el = $("txlist");
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
// Only offer the filter when there is actually something to filter.
const hasTokens = all.some((t) => t.asset);
const filterEl = $("histFilter");
if (filterEl) {
filterEl.hidden = !hasTokens;
const nativeLbl = $("histNativeLbl");
if (nativeLbl) nativeLbl.textContent = s?.meta?.ticker || "Coin";
}
const list = hasTokens
? all.filter((t) => (t.asset ? histShowTokens : histShowNative))
: all;
const cntEl = $("histCount");
if (cntEl) cntEl.textContent = hasTokens ? `${list.length} of ${all.length}` : "";
if (!list.length) {
el.innerHTML = `<div class="empty">${s?.scanning ? "Syncing…" : (all.length ? "Nothing matches this filter." : "No transactions yet.")}</div>`;
return;
}
chore(aegis): 0.8.9 — ETH/SOL imported history + tokens, centred setup screen Completes the parity work 0.8.7 started for Tron. Imported ETH and SOL wallets showed a native balance and nothing else, because the JSON-RPC endpoints they poll have no history or token concept at all. - ETH history + ERC-20 balances via Blockscout, which needs no API key (Etherscan V2 does). Mainnet RPC moves off eth.llamarpc.com, which was answering 525 with an HTML error page — that parsed as a JSON error and showed as a 0 balance. - SOL history via getSignaturesForAddress and SPL balances via getTokenAccountsByOwner, both keyless on the public RPC. Three things the live testing turned up: - A Blockscout mempool entry is {result:"pending", status:null}. Reading that as "not ok, therefore failed" showed pending sends as failures. Now carries a distinct pending state through to the row. - History `delta` is now a number, a decimal string, or null. ETH wei needs the string (18 decimals overflows a JS number, and Math.abs was silently rounding it); Solana's signature feed carries no amount at all, and null >= 0 is true, so unknown amounts were rendering as a "+" that claimed a receive we cannot verify. Unknown now renders as a neutral row instead. - A real address came back with 855 ERC-20s and 3078 SPL mints, nearly all airdrop spam, some with blank, zero-width or bidi-override symbols that render as an empty row borrowing trust from its neighbours. Token text is sanitised and lists are capped at 50, sorted so named tokens survive the cap. Also: the first-run setup screen forced text-align:left on the form, so its helper copy ran ragged under a centred mark, title and description. The form now inherits the centred alignment; the mnemonic box stays left-aligned on purpose, since centring wrapped seed words makes them harder to check.
2026-09-23 00:05:14 +02:00
const dec = s?.decimals ?? 8;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
el.innerHTML = list.map((t) => {
chore(aegis): 0.8.9 — ETH/SOL imported history + tokens, centred setup screen Completes the parity work 0.8.7 started for Tron. Imported ETH and SOL wallets showed a native balance and nothing else, because the JSON-RPC endpoints they poll have no history or token concept at all. - ETH history + ERC-20 balances via Blockscout, which needs no API key (Etherscan V2 does). Mainnet RPC moves off eth.llamarpc.com, which was answering 525 with an HTML error page — that parsed as a JSON error and showed as a 0 balance. - SOL history via getSignaturesForAddress and SPL balances via getTokenAccountsByOwner, both keyless on the public RPC. Three things the live testing turned up: - A Blockscout mempool entry is {result:"pending", status:null}. Reading that as "not ok, therefore failed" showed pending sends as failures. Now carries a distinct pending state through to the row. - History `delta` is now a number, a decimal string, or null. ETH wei needs the string (18 decimals overflows a JS number, and Math.abs was silently rounding it); Solana's signature feed carries no amount at all, and null >= 0 is true, so unknown amounts were rendering as a "+" that claimed a receive we cannot verify. Unknown now renders as a neutral row instead. - A real address came back with 855 ERC-20s and 3078 SPL mints, nearly all airdrop spam, some with blank, zero-width or bidi-override symbols that render as an empty row borrowing trust from its neighbours. Token text is sanitised and lists are capped at 50, sorted so named tokens survive the cap. Also: the first-run setup screen forced text-align:left on the form, so its helper copy ran ragged under a centred mark, title and description. The form now inherits the centred alignment; the mnemonic box stays left-aligned on purpose, since centring wrapped seed words makes them harder to check.
2026-09-23 00:05:14 +02:00
// `delta` arrives in three shapes now: a number (UTXO chains), a decimal
// STRING (ETH — 18 decimals of wei overflows a JS number, so it must
// stay exact), or null (Solana, where the signature feed carries no
// amount and fetching one per tx would be 25 extra round trips a poll).
// Treating null as 0 would render "+ —", claiming a receive we cannot
// actually verify, so unknown amounts get their own neutral branch.
const known = t.delta != null;
const neg = known && String(t.delta).trim().startsWith("-");
const inc = known ? !neg : null;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const when = t.time ? new Date(t.time * 1000).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }) : "pending";
chore(aegis): 0.8.9 — ETH/SOL imported history + tokens, centred setup screen Completes the parity work 0.8.7 started for Tron. Imported ETH and SOL wallets showed a native balance and nothing else, because the JSON-RPC endpoints they poll have no history or token concept at all. - ETH history + ERC-20 balances via Blockscout, which needs no API key (Etherscan V2 does). Mainnet RPC moves off eth.llamarpc.com, which was answering 525 with an HTML error page — that parsed as a JSON error and showed as a 0 balance. - SOL history via getSignaturesForAddress and SPL balances via getTokenAccountsByOwner, both keyless on the public RPC. Three things the live testing turned up: - A Blockscout mempool entry is {result:"pending", status:null}. Reading that as "not ok, therefore failed" showed pending sends as failures. Now carries a distinct pending state through to the row. - History `delta` is now a number, a decimal string, or null. ETH wei needs the string (18 decimals overflows a JS number, and Math.abs was silently rounding it); Solana's signature feed carries no amount at all, and null >= 0 is true, so unknown amounts were rendering as a "+" that claimed a receive we cannot verify. Unknown now renders as a neutral row instead. - A real address came back with 855 ERC-20s and 3078 SPL mints, nearly all airdrop spam, some with blank, zero-width or bidi-override symbols that render as an empty row borrowing trust from its neighbours. Token text is sanitised and lists are capped at 50, sorted so named tokens survive the cap. Also: the first-run setup screen forced text-align:left on the form, so its helper copy ran ragged under a centred mark, title and description. The form now inherits the centred alignment; the mnemonic box stays left-aligned on purpose, since centring wrapped seed words makes them harder to check.
2026-09-23 00:05:14 +02:00
const who = inc === null ? "" : inc ? (t.from ? "from " + shortAddr(t.from) : "") : (t.to ? "to " + shortAddr(t.to) : "");
const what = (inc === null ? (t.kind || "Transaction") : inc ? "Received" : "Sent") + (who ? " " + who : "");
const conf = t.confirmations > 0 ? (t.confirmations >= 6 ? "confirmed" : t.confirmations + " conf")
: (t.status === "failed" ? "failed" : t.status === "pending" ? "pending" : "unconfirmed");
// Strip the sign as text rather than via Math.abs so a big-decimal
// string keeps every digit.
const magnitude = known ? String(t.delta).trim().replace(/^[-+]/, "") : "";
const isZero = known && /^0*$/.test(magnitude);
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
// A token row carries its OWN decimals and ticker — rendering a USDT
// amount against the chain's 6-decimal TRX scale (or an 18-decimal
// ERC-20 against 18 for ETH) would be off by orders of magnitude.
const rowDec = t.asset ? (t.assetDecimals ?? 0) : dec;
const rowTicker = t.asset ? ` ${t.asset}` : "";
chore(aegis): 0.8.9 — ETH/SOL imported history + tokens, centred setup screen Completes the parity work 0.8.7 started for Tron. Imported ETH and SOL wallets showed a native balance and nothing else, because the JSON-RPC endpoints they poll have no history or token concept at all. - ETH history + ERC-20 balances via Blockscout, which needs no API key (Etherscan V2 does). Mainnet RPC moves off eth.llamarpc.com, which was answering 525 with an HTML error page — that parsed as a JSON error and showed as a 0 balance. - SOL history via getSignaturesForAddress and SPL balances via getTokenAccountsByOwner, both keyless on the public RPC. Three things the live testing turned up: - A Blockscout mempool entry is {result:"pending", status:null}. Reading that as "not ok, therefore failed" showed pending sends as failures. Now carries a distinct pending state through to the row. - History `delta` is now a number, a decimal string, or null. ETH wei needs the string (18 decimals overflows a JS number, and Math.abs was silently rounding it); Solana's signature feed carries no amount at all, and null >= 0 is true, so unknown amounts were rendering as a "+" that claimed a receive we cannot verify. Unknown now renders as a neutral row instead. - A real address came back with 855 ERC-20s and 3078 SPL mints, nearly all airdrop spam, some with blank, zero-width or bidi-override symbols that render as an empty row borrowing trust from its neighbours. Token text is sanitised and lists are capped at 50, sorted so named tokens survive the cap. Also: the first-run setup screen forced text-align:left on the form, so its helper copy ran ragged under a centred mark, title and description. The form now inherits the centred alignment; the mnemonic box stays left-aligned on purpose, since centring wrapped seed words makes them harder to check.
2026-09-23 00:05:14 +02:00
const amountHtml = !known ? "—"
: isZero ? "—"
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
: `${inc ? "+" : "−"}${esc(fmtTokenAmount(magnitude, rowDec))}${esc(rowTicker)}`;
chore(aegis): 0.8.9 — ETH/SOL imported history + tokens, centred setup screen Completes the parity work 0.8.7 started for Tron. Imported ETH and SOL wallets showed a native balance and nothing else, because the JSON-RPC endpoints they poll have no history or token concept at all. - ETH history + ERC-20 balances via Blockscout, which needs no API key (Etherscan V2 does). Mainnet RPC moves off eth.llamarpc.com, which was answering 525 with an HTML error page — that parsed as a JSON error and showed as a 0 balance. - SOL history via getSignaturesForAddress and SPL balances via getTokenAccountsByOwner, both keyless on the public RPC. Three things the live testing turned up: - A Blockscout mempool entry is {result:"pending", status:null}. Reading that as "not ok, therefore failed" showed pending sends as failures. Now carries a distinct pending state through to the row. - History `delta` is now a number, a decimal string, or null. ETH wei needs the string (18 decimals overflows a JS number, and Math.abs was silently rounding it); Solana's signature feed carries no amount at all, and null >= 0 is true, so unknown amounts were rendering as a "+" that claimed a receive we cannot verify. Unknown now renders as a neutral row instead. - A real address came back with 855 ERC-20s and 3078 SPL mints, nearly all airdrop spam, some with blank, zero-width or bidi-override symbols that render as an empty row borrowing trust from its neighbours. Token text is sanitised and lists are capped at 50, sorted so named tokens survive the cap. Also: the first-run setup screen forced text-align:left on the form, so its helper copy ran ragged under a centred mark, title and description. The form now inherits the centred alignment; the mnemonic box stays left-aligned on purpose, since centring wrapped seed words makes them harder to check.
2026-09-23 00:05:14 +02:00
const icon = inc === null ? "·" : inc ? "↓" : "↑";
const iconCls = inc === null ? "" : inc ? "in" : "out";
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
return `<div class="tx" data-txid="${esc(t.txid)}" title="${esc(t.txid)}">
chore(aegis): 0.8.9 — ETH/SOL imported history + tokens, centred setup screen Completes the parity work 0.8.7 started for Tron. Imported ETH and SOL wallets showed a native balance and nothing else, because the JSON-RPC endpoints they poll have no history or token concept at all. - ETH history + ERC-20 balances via Blockscout, which needs no API key (Etherscan V2 does). Mainnet RPC moves off eth.llamarpc.com, which was answering 525 with an HTML error page — that parsed as a JSON error and showed as a 0 balance. - SOL history via getSignaturesForAddress and SPL balances via getTokenAccountsByOwner, both keyless on the public RPC. Three things the live testing turned up: - A Blockscout mempool entry is {result:"pending", status:null}. Reading that as "not ok, therefore failed" showed pending sends as failures. Now carries a distinct pending state through to the row. - History `delta` is now a number, a decimal string, or null. ETH wei needs the string (18 decimals overflows a JS number, and Math.abs was silently rounding it); Solana's signature feed carries no amount at all, and null >= 0 is true, so unknown amounts were rendering as a "+" that claimed a receive we cannot verify. Unknown now renders as a neutral row instead. - A real address came back with 855 ERC-20s and 3078 SPL mints, nearly all airdrop spam, some with blank, zero-width or bidi-override symbols that render as an empty row borrowing trust from its neighbours. Token text is sanitised and lists are capped at 50, sorted so named tokens survive the cap. Also: the first-run setup screen forced text-align:left on the form, so its helper copy ran ragged under a centred mark, title and description. The form now inherits the centred alignment; the mnemonic box stays left-aligned on purpose, since centring wrapped seed words makes them harder to check.
2026-09-23 00:05:14 +02:00
<div class="ic ${iconCls}">${icon}</div>
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
<div class="what">${esc(what)}</div>
chore(aegis): 0.8.9 — ETH/SOL imported history + tokens, centred setup screen Completes the parity work 0.8.7 started for Tron. Imported ETH and SOL wallets showed a native balance and nothing else, because the JSON-RPC endpoints they poll have no history or token concept at all. - ETH history + ERC-20 balances via Blockscout, which needs no API key (Etherscan V2 does). Mainnet RPC moves off eth.llamarpc.com, which was answering 525 with an HTML error page — that parsed as a JSON error and showed as a 0 balance. - SOL history via getSignaturesForAddress and SPL balances via getTokenAccountsByOwner, both keyless on the public RPC. Three things the live testing turned up: - A Blockscout mempool entry is {result:"pending", status:null}. Reading that as "not ok, therefore failed" showed pending sends as failures. Now carries a distinct pending state through to the row. - History `delta` is now a number, a decimal string, or null. ETH wei needs the string (18 decimals overflows a JS number, and Math.abs was silently rounding it); Solana's signature feed carries no amount at all, and null >= 0 is true, so unknown amounts were rendering as a "+" that claimed a receive we cannot verify. Unknown now renders as a neutral row instead. - A real address came back with 855 ERC-20s and 3078 SPL mints, nearly all airdrop spam, some with blank, zero-width or bidi-override symbols that render as an empty row borrowing trust from its neighbours. Token text is sanitised and lists are capped at 50, sorted so named tokens survive the cap. Also: the first-run setup screen forced text-align:left on the form, so its helper copy ran ragged under a centred mark, title and description. The form now inherits the centred alignment; the mnemonic box stays left-aligned on purpose, since centring wrapped seed words makes them harder to check.
2026-09-23 00:05:14 +02:00
<div class="amt2 ${inc ? "in" : ""}">${amountHtml}</div>
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
<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("");
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
el.querySelectorAll(".tx").forEach((row) => row.addEventListener("click", () => openUrl(explorerHref(sel().explorerTx, row.dataset.txid))));
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
wireHistFilter();
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
}
function shortAddr(a) {
if (!a) return "";
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.
2026-09-07 01:07:31 +02:00
const s = String(a).replace(/^bitcoincash:|^bchtest:/, "");
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
return esc(s.slice(0, 10)) + "…" + esc(s.slice(-4));
}
// ---- QR --------------------------------------------------------------------
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
// 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:/, "");
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
if (chain === "btc") return "bitcoin:" + address; // BIP21
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
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;
fix(aegis): 0.9.8 — address row columns, resizable QR, inline copy The row's grid template still described the pre-0.9.2 layout — six columns including an address cell that no longer exists — while the row renders five. Every cell therefore sat one column left of where it belonged: copy landed in the label's space, the amount in the old label column, and the ⋯ in the amount column instead of the edge. That is the whole cause of "the copy button collides with the amount" and "the last edit button is not on the edge". Now five columns for five cells: icon · label · copy · amount · menu, 8px gaps. The label is the flexible one, left-aligned, so it takes the slack and truncates rather than squeezing the number. Verified at 520, 400 and 300px: no cell overlap at any width, no row overflow, and the amount never clips — only long labels give way. The copy glyph was the 📋 emoji, which has no glyph in this platform's font stack and rendered as a tofu box that read like a stray character stuck to the balance. Replaced with an inline SVG in both the row and the address card. Both confirmation flashes now swap innerHTML rather than textContent, which would have deleted the SVG and left a blank square. Address card: the address and its copy button share one row, so the button sits at the end of the value it copies. The address clamps to two lines and truncates beyond that instead of growing the card in a narrow sidebar. The QR is always visible and the panel is drag-resizable from a grip under it (pointer events, arrow keys as a non-mouse path, clamped 90–420px, capped against the panel's own width so a size set on a wide sidebar cannot overflow a narrow one, persisted). That replaces the 0.9.2 show/hide toggle — a size set once beats a binary, and it frees the row Copy was sharing with a QR button. drawQr was setting cv.style.width/height, which would have reset the panel to its intrinsic size on every redraw — i.e. every time the address changed. It now sets only the backing store and re-asserts the user's size after drawing. "Next unused address" is correct at the adapter level on both mainnet and chipnet (tested: the index advances and the address changes), so the reported failure is elsewhere. The handler was discarding the error and flashing a bare "Failed", which is why there was nothing to diagnose; it now surfaces the real message.
2026-09-24 22:30:10 +02:00
// Backing store only. The DISPLAY size belongs to the user's dragged
// preference — setting cv.style here would reset the panel to its
// intrinsic size on every redraw (i.e. whenever the address changed).
cv.width = cv.height = 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);
fix(aegis): 0.9.8 — address row columns, resizable QR, inline copy The row's grid template still described the pre-0.9.2 layout — six columns including an address cell that no longer exists — while the row renders five. Every cell therefore sat one column left of where it belonged: copy landed in the label's space, the amount in the old label column, and the ⋯ in the amount column instead of the edge. That is the whole cause of "the copy button collides with the amount" and "the last edit button is not on the edge". Now five columns for five cells: icon · label · copy · amount · menu, 8px gaps. The label is the flexible one, left-aligned, so it takes the slack and truncates rather than squeezing the number. Verified at 520, 400 and 300px: no cell overlap at any width, no row overflow, and the amount never clips — only long labels give way. The copy glyph was the 📋 emoji, which has no glyph in this platform's font stack and rendered as a tofu box that read like a stray character stuck to the balance. Replaced with an inline SVG in both the row and the address card. Both confirmation flashes now swap innerHTML rather than textContent, which would have deleted the SVG and left a blank square. Address card: the address and its copy button share one row, so the button sits at the end of the value it copies. The address clamps to two lines and truncates beyond that instead of growing the card in a narrow sidebar. The QR is always visible and the panel is drag-resizable from a grip under it (pointer events, arrow keys as a non-mouse path, clamped 90–420px, capped against the panel's own width so a size set on a wide sidebar cannot overflow a narrow one, persisted). That replaces the 0.9.2 show/hide toggle — a size set once beats a binary, and it frees the row Copy was sharing with a QR button. drawQr was setting cv.style.width/height, which would have reset the panel to its intrinsic size on every redraw — i.e. every time the address changed. It now sets only the backing store and re-asserts the user's size after drawing. "Next unused address" is correct at the adapter level on both mainnet and chipnet (tested: the index advances and the address changes), so the reported failure is elsewhere. The handler was discarding the error and flashing a bare "Failed", which is why there was nothing to diagnose; it now surfaces the real message.
2026-09-24 22:30:10 +02:00
applyQrSize();
}
// ---- receive actions -------------------------------------------------------
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
fix(aegis): 0.9.8 — address row columns, resizable QR, inline copy The row's grid template still described the pre-0.9.2 layout — six columns including an address cell that no longer exists — while the row renders five. Every cell therefore sat one column left of where it belonged: copy landed in the label's space, the amount in the old label column, and the ⋯ in the amount column instead of the edge. That is the whole cause of "the copy button collides with the amount" and "the last edit button is not on the edge". Now five columns for five cells: icon · label · copy · amount · menu, 8px gaps. The label is the flexible one, left-aligned, so it takes the slack and truncates rather than squeezing the number. Verified at 520, 400 and 300px: no cell overlap at any width, no row overflow, and the amount never clips — only long labels give way. The copy glyph was the 📋 emoji, which has no glyph in this platform's font stack and rendered as a tofu box that read like a stray character stuck to the balance. Replaced with an inline SVG in both the row and the address card. Both confirmation flashes now swap innerHTML rather than textContent, which would have deleted the SVG and left a blank square. Address card: the address and its copy button share one row, so the button sits at the end of the value it copies. The address clamps to two lines and truncates beyond that instead of growing the card in a narrow sidebar. The QR is always visible and the panel is drag-resizable from a grip under it (pointer events, arrow keys as a non-mouse path, clamped 90–420px, capped against the panel's own width so a size set on a wide sidebar cannot overflow a narrow one, persisted). That replaces the 0.9.2 show/hide toggle — a size set once beats a binary, and it frees the row Copy was sharing with a QR button. drawQr was setting cv.style.width/height, which would have reset the panel to its intrinsic size on every redraw — i.e. every time the address changed. It now sets only the backing store and re-asserts the user's size after drawing. "Next unused address" is correct at the adapter level on both mainnet and chipnet (tested: the index advances and the address changes), so the reported failure is elsewhere. The handler was discarding the error and flashing a bare "Failed", which is why there was nothing to diagnose; it now surfaces the real message.
2026-09-24 22:30:10 +02:00
// 0.9.8: the QR is always visible and the panel is drag-resizable, which
// replaces the 0.9.2 show/hide toggle — a size the user sets once is a
// better answer than a binary, and it means the QR button no longer
// competes with Copy for the one row that gets used.
let applyQrSize = () => {};
(function wireQrResize() {
const grip = $("qrGrip"), wrap = $("qrWrap");
if (!grip || !wrap) return;
const MIN = 90, MAX = 420;
const clamp = (v) => Math.max(MIN, Math.min(MAX, Math.round(v)));
let size = 200;
try {
const saved = Number(localStorage.getItem("aegis/qrSize"));
if (Number.isFinite(saved) && saved > 0) size = clamp(saved);
} catch {}
const apply = () => {
// Cap against the actual panel width too — a size dragged wide on a
// roomy sidebar must not overflow when the sidebar is narrowed later.
const room = Math.max(MIN, (wrap.clientWidth || MAX) - 24);
const px = Math.min(size, room);
const cv = $("qr");
if (cv) { cv.style.width = px + "px"; cv.style.height = px + "px"; }
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
};
fix(aegis): 0.9.8 — address row columns, resizable QR, inline copy The row's grid template still described the pre-0.9.2 layout — six columns including an address cell that no longer exists — while the row renders five. Every cell therefore sat one column left of where it belonged: copy landed in the label's space, the amount in the old label column, and the ⋯ in the amount column instead of the edge. That is the whole cause of "the copy button collides with the amount" and "the last edit button is not on the edge". Now five columns for five cells: icon · label · copy · amount · menu, 8px gaps. The label is the flexible one, left-aligned, so it takes the slack and truncates rather than squeezing the number. Verified at 520, 400 and 300px: no cell overlap at any width, no row overflow, and the amount never clips — only long labels give way. The copy glyph was the 📋 emoji, which has no glyph in this platform's font stack and rendered as a tofu box that read like a stray character stuck to the balance. Replaced with an inline SVG in both the row and the address card. Both confirmation flashes now swap innerHTML rather than textContent, which would have deleted the SVG and left a blank square. Address card: the address and its copy button share one row, so the button sits at the end of the value it copies. The address clamps to two lines and truncates beyond that instead of growing the card in a narrow sidebar. The QR is always visible and the panel is drag-resizable from a grip under it (pointer events, arrow keys as a non-mouse path, clamped 90–420px, capped against the panel's own width so a size set on a wide sidebar cannot overflow a narrow one, persisted). That replaces the 0.9.2 show/hide toggle — a size set once beats a binary, and it frees the row Copy was sharing with a QR button. drawQr was setting cv.style.width/height, which would have reset the panel to its intrinsic size on every redraw — i.e. every time the address changed. It now sets only the backing store and re-asserts the user's size after drawing. "Next unused address" is correct at the adapter level on both mainnet and chipnet (tested: the index advances and the address changes), so the reported failure is elsewhere. The handler was discarding the error and flashing a bare "Failed", which is why there was nothing to diagnose; it now surfaces the real message.
2026-09-24 22:30:10 +02:00
applyQrSize = apply;
apply();
window.addEventListener("resize", apply);
let startY = 0, startSize = 0, active = false;
const onMove = (e) => {
if (!active) return;
e.preventDefault();
size = clamp(startSize + (e.clientY - startY));
apply();
};
const onUp = () => {
if (!active) return;
active = false;
grip.classList.remove("dragging");
try { grip.releasePointerCapture?.(grip._pid); } catch {}
try { localStorage.setItem("aegis/qrSize", String(size)); } catch {}
window.removeEventListener("pointermove", onMove);
window.removeEventListener("pointerup", onUp);
};
grip.addEventListener("pointerdown", (e) => {
active = true; startY = e.clientY;
const cv = $("qr");
startSize = cv ? (parseInt(cv.style.width, 10) || 200) : 200;
grip._pid = e.pointerId;
grip.classList.add("dragging");
try { grip.setPointerCapture(e.pointerId); } catch {}
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp);
e.preventDefault();
});
// Keyboard affordance — a drag-only control is unusable without a mouse.
grip.tabIndex = 0;
grip.addEventListener("keydown", (e) => {
const step = e.shiftKey ? 24 : 8;
if (e.key === "ArrowUp") { size = clamp(size - step); }
else if (e.key === "ArrowDown") { size = clamp(size + step); }
else return;
e.preventDefault(); apply();
try { localStorage.setItem("aegis/qrSize", String(size)); } catch {}
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
});
fix(aegis): 0.9.8 — address row columns, resizable QR, inline copy The row's grid template still described the pre-0.9.2 layout — six columns including an address cell that no longer exists — while the row renders five. Every cell therefore sat one column left of where it belonged: copy landed in the label's space, the amount in the old label column, and the ⋯ in the amount column instead of the edge. That is the whole cause of "the copy button collides with the amount" and "the last edit button is not on the edge". Now five columns for five cells: icon · label · copy · amount · menu, 8px gaps. The label is the flexible one, left-aligned, so it takes the slack and truncates rather than squeezing the number. Verified at 520, 400 and 300px: no cell overlap at any width, no row overflow, and the amount never clips — only long labels give way. The copy glyph was the 📋 emoji, which has no glyph in this platform's font stack and rendered as a tofu box that read like a stray character stuck to the balance. Replaced with an inline SVG in both the row and the address card. Both confirmation flashes now swap innerHTML rather than textContent, which would have deleted the SVG and left a blank square. Address card: the address and its copy button share one row, so the button sits at the end of the value it copies. The address clamps to two lines and truncates beyond that instead of growing the card in a narrow sidebar. The QR is always visible and the panel is drag-resizable from a grip under it (pointer events, arrow keys as a non-mouse path, clamped 90–420px, capped against the panel's own width so a size set on a wide sidebar cannot overflow a narrow one, persisted). That replaces the 0.9.2 show/hide toggle — a size set once beats a binary, and it frees the row Copy was sharing with a QR button. drawQr was setting cv.style.width/height, which would have reset the panel to its intrinsic size on every redraw — i.e. every time the address changed. It now sets only the backing store and re-asserts the user's size after drawing. "Next unused address" is correct at the adapter level on both mainnet and chipnet (tested: the index advances and the address changes), so the reported failure is elsewhere. The handler was discarding the error and flashing a bare "Failed", which is why there was nothing to diagnose; it now surfaces the real message.
2026-09-24 22:30:10 +02:00
})();
// The inline copy button holds an SVG; set it once at load.
(function paintCopyIcon() {
const b = $("copyAddr");
if (b && !b.firstElementChild) b.innerHTML = COPY_ICON;
chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history Receive was ordered QR → address → tokens, so 200px of always-on QR sat above the thing people came for and pushed the asset list off screen. - Address first, with Copy and a QR button; the QR expands inline and the choice sticks, because someone who receives by QR wants it every time and someone who copies never does. - Explorer and Faucet moved up to the header status row. They act on the selected wallet, not on the act of receiving, and down there they competed with Copy for the one row that gets used. - Assets card renamed from Tokens and now leads with the native coin, so "what does this wallet hold" is one list rather than two places. - "+ Add another <TICKER>" moved below the address list. The balance appeared three times — header, drilldown subtitle, address row. Now once in the header; the subtitle keeps only the per-unit price, and the per-address amount returns when a coin actually has more than one address to compare. The address row drops its truncated address (the full one is at the top of Receive) and keeps the wallet name. The per-address asset list added in 0.8.8 duplicated the Assets card and is removed — assets live in one place. Token amounts were unreadable: an 18-decimal balance rendered as 60000000.000000005435817984. Capped to 8 decimals with thousands separators, exact value on hover. A symbol claimed by more than one contract now carries a LOOK-ALIKE tag. The test wallet holds four different contracts all calling themselves "Test USDT" — spam mints borrowing a trusted ticker so a careless send lands on the wrong one. We can't tell which is genuine, so we mark every member of the clash rather than guessing. History showed native transfers only, so a wallet that had only ever moved USDT looked empty. TRC20 transfers are merged in newest-first, each carrying its own decimals and ticker (rendering a token against the chain's scale would be off by orders of magnitude). Both feeds are on by default; the checkboxes narrow rather than opt in, and the last one checked can't be unchecked into an empty list.
2026-09-23 01:17:33 +02:00
})();
$("copyAddr").addEventListener("click", async () => {
fix(aegis): 0.9.8 — address row columns, resizable QR, inline copy The row's grid template still described the pre-0.9.2 layout — six columns including an address cell that no longer exists — while the row renders five. Every cell therefore sat one column left of where it belonged: copy landed in the label's space, the amount in the old label column, and the ⋯ in the amount column instead of the edge. That is the whole cause of "the copy button collides with the amount" and "the last edit button is not on the edge". Now five columns for five cells: icon · label · copy · amount · menu, 8px gaps. The label is the flexible one, left-aligned, so it takes the slack and truncates rather than squeezing the number. Verified at 520, 400 and 300px: no cell overlap at any width, no row overflow, and the amount never clips — only long labels give way. The copy glyph was the 📋 emoji, which has no glyph in this platform's font stack and rendered as a tofu box that read like a stray character stuck to the balance. Replaced with an inline SVG in both the row and the address card. Both confirmation flashes now swap innerHTML rather than textContent, which would have deleted the SVG and left a blank square. Address card: the address and its copy button share one row, so the button sits at the end of the value it copies. The address clamps to two lines and truncates beyond that instead of growing the card in a narrow sidebar. The QR is always visible and the panel is drag-resizable from a grip under it (pointer events, arrow keys as a non-mouse path, clamped 90–420px, capped against the panel's own width so a size set on a wide sidebar cannot overflow a narrow one, persisted). That replaces the 0.9.2 show/hide toggle — a size set once beats a binary, and it frees the row Copy was sharing with a QR button. drawQr was setting cv.style.width/height, which would have reset the panel to its intrinsic size on every redraw — i.e. every time the address changed. It now sets only the backing store and re-asserts the user's size after drawing. "Next unused address" is correct at the adapter level on both mainnet and chipnet (tested: the index advances and the address changes), so the reported failure is elsewhere. The handler was discarding the error and flashing a bare "Failed", which is why there was nothing to diagnose; it now surfaces the real message.
2026-09-24 22:30:10 +02:00
const b = $("copyAddr");
try {
await navigator.clipboard.writeText(sel().address);
// innerHTML, not flash() — flash() swaps textContent, which would
// delete the inline SVG and leave an empty square behind.
b.classList.add("copied"); b.innerHTML = CHECK_ICON;
setTimeout(() => { b.classList.remove("copied"); b.innerHTML = COPY_ICON; }, 1000);
} catch {}
});
$("nextAddr").addEventListener("click", async () => {
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
try { const s = await S.invoke("nextAddress"); state.selected = { ...state.selected, ...s }; render(); }
fix(aegis): 0.9.8 — address row columns, resizable QR, inline copy The row's grid template still described the pre-0.9.2 layout — six columns including an address cell that no longer exists — while the row renders five. Every cell therefore sat one column left of where it belonged: copy landed in the label's space, the amount in the old label column, and the ⋯ in the amount column instead of the edge. That is the whole cause of "the copy button collides with the amount" and "the last edit button is not on the edge". Now five columns for five cells: icon · label · copy · amount · menu, 8px gaps. The label is the flexible one, left-aligned, so it takes the slack and truncates rather than squeezing the number. Verified at 520, 400 and 300px: no cell overlap at any width, no row overflow, and the amount never clips — only long labels give way. The copy glyph was the 📋 emoji, which has no glyph in this platform's font stack and rendered as a tofu box that read like a stray character stuck to the balance. Replaced with an inline SVG in both the row and the address card. Both confirmation flashes now swap innerHTML rather than textContent, which would have deleted the SVG and left a blank square. Address card: the address and its copy button share one row, so the button sits at the end of the value it copies. The address clamps to two lines and truncates beyond that instead of growing the card in a narrow sidebar. The QR is always visible and the panel is drag-resizable from a grip under it (pointer events, arrow keys as a non-mouse path, clamped 90–420px, capped against the panel's own width so a size set on a wide sidebar cannot overflow a narrow one, persisted). That replaces the 0.9.2 show/hide toggle — a size set once beats a binary, and it frees the row Copy was sharing with a QR button. drawQr was setting cv.style.width/height, which would have reset the panel to its intrinsic size on every redraw — i.e. every time the address changed. It now sets only the backing store and re-asserts the user's size after drawing. "Next unused address" is correct at the adapter level on both mainnet and chipnet (tested: the index advances and the address changes), so the reported failure is elsewhere. The handler was discarding the error and flashing a bare "Failed", which is why there was nothing to diagnose; it now surfaces the real message.
2026-09-24 22:30:10 +02:00
// The old handler discarded the error and flashed a bare "Failed", so
// neither the user nor a maintainer could tell WHY — which is why
// "next unused address doesn't work" had no diagnosable cause. Show it.
catch (e) { showErr("Next address: " + cleanErr(e)); }
});
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
$("viewAddr").addEventListener("click", () => openUrl(explorerHref(sel().explorerAddr, sel().address)));
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("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 ------------------------------------------------------------------
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("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(); });
if ($("sendMemo")) $("sendMemo").addEventListener("input", () => schedulePlan());
// 0.7.7 legacy chips — hidden in 0.8.0 but kept for graceful transition;
// clicking still opens the standalone modal for anyone with muscle memory.
if ($("consolidateChip")) $("consolidateChip").addEventListener("click", () => openConsolidateModal());
if ($("consolidateChipRcv")) $("consolidateChipRcv").addEventListener("click", () => openConsolidateModal());
// 0.8.0 mode-toggle wiring. One class="modetoggle" element per tab —
// clicking a segment flips the mode variable and repaints the body via
// paintSendMode / paintRcvMode. Freshly-rendered inline hosts get their
// consolidate view populated on first switch.
document.querySelectorAll("[data-send-mode]").forEach((b) => b.addEventListener("click", () => {
sendMode = b.dataset.sendMode;
consolidateInlineHost = null; // force re-render on next switch
paintSendMode();
}));
// Consolidate, now an address action rather than a view toggle. Entering the
// view forces a re-render of the inline host so the preview is costed fresh
// each time, the same thing the old toggle did.
if ($("rcvConsolidateBtn")) $("rcvConsolidateBtn").addEventListener("click", () => {
rcvMode = "consolidate";
consolidateInlineHost = null;
paintRcvMode();
});
if ($("rcvConsolidateBack")) $("rcvConsolidateBack").addEventListener("click", () => {
rcvMode = "receive";
consolidateInlineHost = null;
paintRcvMode();
paintRcvView();
});
feat(aegis): 0.16.0 — Address & QR and Assets are peer chips, and the coin row loses its dropdown Two changes to the same idea: one way to do each thing, and the same shape for every coin. **Assets is no longer buried under the QR.** The Receive body stacked address → QR → assets in one column, so in a 380px panel the token list sat permanently below the fold behind 200px of QR — and a coin holding no tokens simply lost a section, which made every coin look structurally different. Address & QR and Assets are now peers behind a persistent chip row. The count rides on the chip, and a wallet with nothing says "No assets held by this wallet" instead of the section vanishing. That mattered more than it sounds: a chipnet wallet with 46 CashToken categories was indistinguishable from an empty one until 0.15.0, because the only signal was an absent card. The choice persists across re-renders and wallet switches — comparing token balances between wallets should not throw you back to the QR on every click. Switching back to Address redraws the QR, since a canvas sized while hidden comes out blank and renderReceive only repaints when the address changes. **The coin row's ▾ network dropdown is gone.** The coin view already carries real network chips (data-browse-net: they filter the wallet list and show a per-network count), so the popover was a second control for one job — and the only one that hid its options behind a click. Chips are the single network control now and the ticker is plain text again. Verified in the panel: chips switch panes and track their own state, the count shows, the empty state reads correctly, the QR survives the round trip, and no .wchev / .wcname.wswitchable remain in the DOM.
2026-09-28 23:05:48 +02:00
// Address & QR / Assets — the persistent pair inside the Receive body.
document.querySelectorAll("[data-rcv-view]").forEach((b) => b.addEventListener("click", () => {
rcvView = b.dataset.rcvView === "assets" ? "assets" : "address";
try { localStorage.setItem("aegis/rcvView", rcvView); } catch (_e) {}
paintRcvView();
}));
["sendTo", "sendAmt"].forEach((id) => $(id).addEventListener("input", () => {
if (id === "sendAmt" && sendMax) return;
if (id === "sendAmt") updateSendFiatPreview();
schedulePlan();
}));
// Live ≈$ preview beside the Amount label, updated on every keystroke. Off
// when prices are disabled or the input is empty, so a quiet form stays quiet.
function updateSendFiatPreview() {
const el = $("sendAmtFiat"); if (!el) return;
const s = sel(); if (!s || sendAsset) { el.hidden = true; return; }
const units = amountUnits();
if (!units || !state?.prices?.enabled) { el.hidden = true; return; }
const usd = usdOf(chain(), units, decimals());
const txt = fmtFiat(usd);
el.textContent = txt ? "≈ " + txt : "";
el.hidden = !txt;
}
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 = "";
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
if (!to || (!sendMax && !amountUnits())) return;
try {
feat(theseus/aegis): SPL token support (view balances + send) SPL tokens now show up in the Solana wallet — balances on the Receive card, an asset picker on Send that flips the amount input into the token's own units. Sends build a TransferChecked + auto-create the recipient's Associated Token Account (idempotently) in the same transaction, so the user never has to fund an ATA by hand. - lib/sol-spl.js: SPL primitives that don't need @solana/web3.js. TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, findProgramAddress (PDA loop backed by an ed25519 is-on-curve check via @noble Point.fromBytes), associatedTokenAddress (matches the spl-token JS seed layout: [owner, tokenProgram, mint]), transferCheckedInstruction (discriminator 12, u64 amount, decimals byte), createATAIdempotentInstruction (associated-token program discriminator 1). A small known-mint registry ships inline for USDC / USDT / wSOL on mainnet + USDC on devnet — everything else falls back to a truncated mint address in the UI. - Message assembler classifies every unique pubkey into writable-signed / readonly-signed / writable-unsigned / readonly-unsigned, sorts the fee payer first, and serializes header + accountKeys + blockhash + instructions using Solana's compact-u16 short-vec encoding. Same wire shape @solana/web3.js produces from Transaction.serializeMessage. - lib/chain-sol.js: snapshot() now carries a tokens[] array of {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram, isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against both the classic Token program and Token-2022. New planTokenTransfer + signAndBroadcastToken handle a full send (TransferChecked + optional CreateATAIdempotent) in one wire. - Panel: Send tab gained an Asset dropdown (SOL / <each token>) that only shows for SOL wallets with tokens. Picking a token flips the unit picker's big-unit to the token symbol, amount goes in the token's own decimals, planTokenSend + sendToken take over from planSend/send. Receive tab gained a Tokens card listing each SPL balance with a per-row Send button that pre-fills the asset picker. - Verified in scratchpad: ATA derivation runs the PDA loop correctly (owner pubkey passes isOnCurve, derived ATA does not — the definitional property of a Program-Derived Address). Cross-check the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS and the value matches. Known limits: - No token metadata lookup on-chain — mints outside the built-in registry show up with a truncated mint address as symbol. Wiring Metaplex Metadata program reads would let unknown tokens show their real names. - Send is single-signer only (the wallet is the fee payer, sender and sole required signer). Multi-sig SPL transfers work via the dapp bridge (window.solana.signAndSendTransaction, which already handles partial signatures).
2026-09-07 23:55:09 +02:00
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;
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const feeRate = chain() === "bch" ? Number($("feeRate").value) : undefined;
const memo = chain() === "bch" ? String($("sendMemo")?.value || "").trim() : "";
const p = await S.invoke("planSend", { to, amount: amountUnits(), feeRate, sendMax, memo });
lastPlan = p;
$("sendToHint").textContent = p.recipients[0].to !== to ? "→ " + p.recipients[0].to : "";
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("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;
// PIN approval gate: when the user has opted into "Require PIN for
// sending", panel-initiated sends must clear a PIN check before the
// approval overlay even shows. Cancel if PIN check fails.
if (!securityLoaded) await refreshSecurityState();
if (securityState.requirePinForSending && securityState.hasPin) {
const ok = await verifyPinInteractively("Confirm this send with your PIN.");
if (!ok) { msg.className = "msg err"; msg.textContent = "Cancelled — PIN not confirmed."; msg.hidden = false; return; }
}
$("sendBtn").disabled = true; $("sendBtn").textContent = "Waiting for approval…";
try {
feat(theseus/aegis): SPL token support (view balances + send) SPL tokens now show up in the Solana wallet — balances on the Receive card, an asset picker on Send that flips the amount input into the token's own units. Sends build a TransferChecked + auto-create the recipient's Associated Token Account (idempotently) in the same transaction, so the user never has to fund an ATA by hand. - lib/sol-spl.js: SPL primitives that don't need @solana/web3.js. TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, findProgramAddress (PDA loop backed by an ed25519 is-on-curve check via @noble Point.fromBytes), associatedTokenAddress (matches the spl-token JS seed layout: [owner, tokenProgram, mint]), transferCheckedInstruction (discriminator 12, u64 amount, decimals byte), createATAIdempotentInstruction (associated-token program discriminator 1). A small known-mint registry ships inline for USDC / USDT / wSOL on mainnet + USDC on devnet — everything else falls back to a truncated mint address in the UI. - Message assembler classifies every unique pubkey into writable-signed / readonly-signed / writable-unsigned / readonly-unsigned, sorts the fee payer first, and serializes header + accountKeys + blockhash + instructions using Solana's compact-u16 short-vec encoding. Same wire shape @solana/web3.js produces from Transaction.serializeMessage. - lib/chain-sol.js: snapshot() now carries a tokens[] array of {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram, isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against both the classic Token program and Token-2022. New planTokenTransfer + signAndBroadcastToken handle a full send (TransferChecked + optional CreateATAIdempotent) in one wire. - Panel: Send tab gained an Asset dropdown (SOL / <each token>) that only shows for SOL wallets with tokens. Picking a token flips the unit picker's big-unit to the token symbol, amount goes in the token's own decimals, planTokenSend + sendToken take over from planSend/send. Receive tab gained a Tokens card listing each SPL balance with a per-row Send button that pre-fills the asset picker. - Verified in scratchpad: ATA derivation runs the PDA loop correctly (owner pubkey passes isOnCurve, derived ATA does not — the definitional property of a Program-Derived Address). Cross-check the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS and the value matches. Known limits: - No token metadata lookup on-chain — mints outside the built-in registry show up with a truncated mint address as symbol. Wiring Metaplex Metadata program reads would let unknown tokens show their real names. - Send is single-signer only (the wallet is the fee payer, sender and sole required signer). Multi-sig SPL transfers work via the dapp bridge (window.solana.signAndSendTransaction, which already handles partial signatures).
2026-09-07 23:55:09 +02:00
const isToken = sendAsset && lastPlan._token;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const feeRate = chain() === "bch" ? Number($("feeRate").value) : undefined;
const memo = chain() === "bch" ? String($("sendMemo")?.value || "").trim() : "";
feat(theseus/aegis): SPL token support (view balances + send) SPL tokens now show up in the Solana wallet — balances on the Receive card, an asset picker on Send that flips the amount input into the token's own units. Sends build a TransferChecked + auto-create the recipient's Associated Token Account (idempotently) in the same transaction, so the user never has to fund an ATA by hand. - lib/sol-spl.js: SPL primitives that don't need @solana/web3.js. TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, findProgramAddress (PDA loop backed by an ed25519 is-on-curve check via @noble Point.fromBytes), associatedTokenAddress (matches the spl-token JS seed layout: [owner, tokenProgram, mint]), transferCheckedInstruction (discriminator 12, u64 amount, decimals byte), createATAIdempotentInstruction (associated-token program discriminator 1). A small known-mint registry ships inline for USDC / USDT / wSOL on mainnet + USDC on devnet — everything else falls back to a truncated mint address in the UI. - Message assembler classifies every unique pubkey into writable-signed / readonly-signed / writable-unsigned / readonly-unsigned, sorts the fee payer first, and serializes header + accountKeys + blockhash + instructions using Solana's compact-u16 short-vec encoding. Same wire shape @solana/web3.js produces from Transaction.serializeMessage. - lib/chain-sol.js: snapshot() now carries a tokens[] array of {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram, isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against both the classic Token program and Token-2022. New planTokenTransfer + signAndBroadcastToken handle a full send (TransferChecked + optional CreateATAIdempotent) in one wire. - Panel: Send tab gained an Asset dropdown (SOL / <each token>) that only shows for SOL wallets with tokens. Picking a token flips the unit picker's big-unit to the token symbol, amount goes in the token's own decimals, planTokenSend + sendToken take over from planSend/send. Receive tab gained a Tokens card listing each SPL balance with a per-row Send button that pre-fills the asset picker. - Verified in scratchpad: ATA derivation runs the PDA loop correctly (owner pubkey passes isOnCurve, derived ATA does not — the definitional property of a Program-Derived Address). Cross-check the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS and the value matches. Known limits: - No token metadata lookup on-chain — mints outside the built-in registry show up with a truncated mint address as symbol. Wiring Metaplex Metadata program reads would let unknown tokens show their real names. - Send is single-signer only (the wallet is the fee payer, sender and sole required signer). Multi-sig SPL transfers work via the dapp bridge (window.solana.signAndSendTransaction, which already handles partial signatures).
2026-09-07 23:55:09 +02:00
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, memo });
msg.className = "msg ok";
msg.innerHTML = `Sent. <a class="link" data-tx="${esc(r.txid)}">${esc(r.txid.slice(0, 16))}…</a>`;
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
msg.querySelector("a").addEventListener("click", () => openUrl(explorerHref(sel().explorerTx, r.txid)));
msg.hidden = false;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("sendTo").value = ""; $("sendAmt").value = ""; sendMax = false;
if ($("sendMemo")) $("sendMemo").value = "";
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("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 --------------------------------------------------------------
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
function fillSettings() {
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
// Global settings (fiat prices, connected sites) render even when there
// is no active wallet / the vault is locked.
renderPricesSetting();
renderSites();
renderGeneralSecurity();
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
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;
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("bchSettings").hidden = chain() !== "bch";
$("trxSettings").hidden = chain() !== "trx";
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
$("scSettings").hidden = chain() !== "sc";
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
$("dgbSettings").hidden = chain() !== "dgb";
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
$("btcSettings").hidden = chain() !== "btc";
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
$("ethSettings").hidden = chain() !== "eth";
$("solSettings").hidden = chain() !== "sol";
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
$("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." : "");
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("renameLabel").value = s.label || "";
if (chain() === "bch") {
if (!settingsFilled) {
$("setPath").value = s.accountPath || "";
renderServerCheckboxes();
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
settingsFilled = true;
}
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.
2026-09-07 01:07:31 +02:00
$("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.");
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("purpose").textContent = "silentmode/addons/" + (s.purpose || "");
// WC pairing needs the vault-derived signer path. Imported wallets
// don't have one yet (M.1b), so we hide the paste field + surface a
// clear explanation in its place — otherwise the user hits an opaque
// "wc: wallet not ready" error from the addon.
const wcImported = s.kind === "imported";
if ($("wcImportedNotice")) $("wcImportedNotice").hidden = !wcImported;
if ($("wcInputs")) $("wcInputs").hidden = wcImported;
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
renderWcSites();
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
} else if (chain() === "trx") {
$("trxPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
} 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) {
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
fillFamilyPicker("Dgb", s);
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
settingsFilled = true;
}
$("dgbPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
$("dgbRecovery").innerHTML = "";
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
} else if (chain() === "btc") {
if (!settingsFilled) {
fillFamilyPicker("Btc", s);
settingsFilled = true;
}
$("btcPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
$("btcRecovery").innerHTML = "";
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
} 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 = "";
}
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
}
// Reflect the current price feed state into the Settings toggle + status
// line. Called from fillSettings() and whenever fresh state arrives.
// General security card — PIN state + "Require PIN for sending" toggle.
// Loads (or refreshes) securityState on demand. Shown even when the vault
// is locked so users on the Settings tab can flip the require-pin policy
// before unlocking.
async function renderGeneralSecurity() {
if (!securityLoaded) await refreshSecurityState();
if (!sessionLoaded) await refreshSessionState();
const hasPin = !!securityState.hasPin;
const set = $("gsPinSet"), chg = $("gsPinChange"), rm = $("gsPinRemove");
const hint = $("pinStatusHint");
const line = $("gsRequirePinLine"), rp = $("gsRequirePin");
if (set) set.hidden = hasPin;
if (chg) chg.hidden = !hasPin;
if (rm) rm.hidden = !hasPin;
if (hint) hint.textContent = hasPin
? "On — Aegis accepts a 6-digit PIN as an alias for your master password."
: "Off — Aegis asks for the master password every time.";
if (line) line.hidden = !hasPin;
if (rp) rp.checked = !!securityState.requirePinForSending;
renderSessionSettings();
}
// Session card: reflects lockOnClose + idleMinutes + safeStorage
// availability into the toggles. When the OS keystore isn't available
// (rare — mainly stripped Linux setups), lock-on-close is forced on and
// the toggle is disabled with a clear hint.
function renderSessionSettings() {
const lc = $("gsLockOnClose");
const im = $("gsIdleMinutes");
const hint = $("gsSessionHint");
if (!lc || !im) return;
const canRemember = !!sessionState.safeStorageAvailable;
lc.checked = !!sessionState.lockOnClose;
lc.disabled = !canRemember;
im.value = String(sessionState.idleMinutes || 0);
if (hint) {
if (!canRemember) {
hint.textContent = "OS keystore unavailable on this machine — Aegis can't remember the unlock across restarts. Master-password entry on every launch.";
} else if (sessionState.lockOnClose) {
hint.textContent = "On — Aegis asks for the master password (or PIN) every time Theseus starts.";
} else {
hint.textContent = "Off — Aegis stays signed in across Theseus restarts. Master password is stored in the OS keystore under this user only.";
}
}
}
// Modal helper that captures a PIN via the same 6-digit pad used on the
// lock screen. Returns the entered PIN (string of 6 digits) or null if
// the user closes without confirming. `confirm` mode double-prompts and
// only resolves when both entries match.
function openPinModal({ title, subtitle, mode }) {
return new Promise((resolve) => {
const first = { pin: null };
const wrap = document.createElement("div");
wrap.className = "pinmodal";
wrap.innerHTML = `
<div class="pincard">
<h2 id="pmTitle">${esc(title)}</h2>
<div class="pinsub" id="pmSub">${esc(subtitle || "")}</div>
<div class="pinpad">
<div class="pindots" id="pmDots">${"<span class=\"pindot\"></span>".repeat(6)}</div>
<div class="pinkeys" id="pmKeys">
${[1,2,3,4,5,6,7,8,9].map((n) => `<button data-k="${n}">${n}</button>`).join("")}
<button class="util" data-k="clear">Clear</button>
<button data-k="0">0</button>
<button class="util" data-k="back">⌫</button>
</div>
<div class="pinerr" id="pmErr"></div>
</div>
<div class="pinactions">
<button class="btn" id="pmCancel" type="button">Cancel</button>
</div>
</div>`;
document.body.appendChild(wrap);
const close = (val) => { try { wrap.remove(); } catch {} resolve(val); };
wrap.addEventListener("click", (e) => { if (e.target === wrap) close(null); });
wrap.querySelector("#pmCancel").addEventListener("click", () => close(null));
setupPinPad({
dots: $("pmDots"), keys: $("pmKeys"), err: $("pmErr"),
onComplete: async (pin) => {
if (mode === "confirm" && first.pin == null) {
first.pin = pin;
$("pmSub").textContent = "Re-enter to confirm";
return "reset";
}
if (mode === "confirm" && first.pin !== pin) {
$("pmErr").textContent = "PINs don't match. Start again.";
first.pin = null;
$("pmSub").textContent = subtitle || "";
return "reset";
}
close(pin);
return "ok";
},
});
});
}
$("gsPinSet") && $("gsPinSet").addEventListener("click", async () => {
await handlePinSet();
});
$("gsPinChange") && $("gsPinChange").addEventListener("click", async () => {
await handlePinSet(true);
});
$("gsPinRemove") && $("gsPinRemove").addEventListener("click", async () => {
const ok = await aegisConfirm({
title: "Remove the quick-access PIN?",
danger: true,
confirmLabel: "Remove PIN",
body: "You'll have to type the master password on every unlock again.",
});
if (!ok) return;
try {
await S.invoke("pinBlobClear");
// Also disable the send-time PIN policy — it depends on having a PIN.
await S.invoke("securitySet", { requirePinForSending: false });
await refreshSecurityState();
renderGeneralSecurity();
} catch (e) { aegisAlert("Could not remove PIN: " + cleanErr(e)); }
});
$("gsRequirePin") && $("gsRequirePin").addEventListener("change", async () => {
const on = $("gsRequirePin").checked;
try {
securityState = await S.invoke("securitySet", { requirePinForSending: on });
renderGeneralSecurity();
} catch (e) {
$("gsRequirePin").checked = !on;
aegisAlert("Could not save setting: " + cleanErr(e));
}
});
$("gsOpenPasswords") && $("gsOpenPasswords").addEventListener("click", () => {
// Route through the addon so it can pass the section slug back to
// Theseus (main-process gates section-hint validation).
S.invoke("openSettings", { section: "passwords" }).catch(() => {});
});
// Session controls: Lock-on-close toggle + Idle-lock dropdown + Sign out.
// Turning "Lock on close" OFF is the "stay signed in" opt-in — we need
// the master password once to seed the OS keystore. Turning it back ON
// wipes the stored blob and reverts to the classic every-launch prompt.
$("gsLockOnClose") && $("gsLockOnClose").addEventListener("change", async () => {
const on = $("gsLockOnClose").checked;
try {
if (!on) {
const pw = window.__aegisLastPw || await promptMasterPassword({
title: "Stay signed in",
subtitle: "Aegis needs your master password once to encrypt it into the OS keystore. It never touches disk in plaintext.",
});
if (!pw) { $("gsLockOnClose").checked = true; return; }
sessionState = await S.invoke("sessionEnable", { masterPassword: pw });
// Drop the buffered password immediately — safeStorage now holds it.
try { delete window.__aegisLastPw; } catch {}
} else {
sessionState = await S.invoke("sessionDisable");
}
renderSessionSettings();
bindIdleAutoLock();
} catch (e) {
$("gsLockOnClose").checked = !on;
aegisAlert("Could not save setting: " + cleanErr(e));
}
});
$("gsIdleMinutes") && $("gsIdleMinutes").addEventListener("change", async () => {
const mins = Number($("gsIdleMinutes").value) || 0;
try {
sessionState = await S.invoke("sessionConfigSet", { idleMinutes: mins });
renderSessionSettings();
bindIdleAutoLock();
} catch (e) { aegisAlert("Could not save idle timeout: " + cleanErr(e)); }
});
$("gsSignOut") && $("gsSignOut").addEventListener("click", async () => {
const ok = await aegisConfirm({
title: "Sign out of Aegis?",
icon: "🔒",
confirmLabel: "Sign out",
body: "The vault will re-lock and you'll need the master password (or PIN) to open it again.",
});
if (!ok) return;
try {
state = await S.invoke("vaultLock");
stripView = { mode: "coins", groupKey: null };
render();
// Session blob was cleared server-side; refresh our cached view.
sessionState = await S.invoke("sessionStatus");
renderSessionSettings();
} catch (e) { aegisAlert("Could not sign out: " + cleanErr(e)); }
});
// Setting or changing a PIN needs the master password to encrypt against.
// If the panel has one buffered from a recent unlock (window.__aegisLastPw)
// we use it silently; otherwise we ask, verify via a fresh vaultUnlock, and
// then proceed with the PIN capture flow.
async function handlePinSet(replacing) {
let masterPw = window.__aegisLastPw || null;
if (!masterPw) {
masterPw = await promptMasterPassword({
title: replacing ? "Confirm master password" : "Set up quick-access PIN",
subtitle: replacing
? "We need the master password once to re-encrypt the PIN under a fresh key."
: "The PIN is an alias for your master password. Enter the master password once to bind them.",
});
if (!masterPw) return;
}
const pin = await openPinModal({
title: replacing ? "Choose a new PIN" : "Choose a PIN",
subtitle: "Six digits",
mode: "confirm",
});
if (!pin) return;
try {
const blob = await pinEncryptMaster(pin, masterPw);
await S.invoke("pinBlobSet", { blob });
await S.invoke("pinFailReset").catch(() => {});
await refreshSecurityState();
renderGeneralSecurity();
} catch (e) {
aegisAlert("Could not save PIN: " + cleanErr(e));
} finally {
// Drop the buffered password sooner rather than later — we only kept
// it around to enroll a PIN without a re-prompt.
try { delete window.__aegisLastPw; } catch {}
}
}
// Small modal that captures the master password + verifies it via a
// vaultUnlock roundtrip. Resolves with the password string on success or
// null on cancel / failure. Used both by PIN enrollment (from Settings)
// and by the PIN approval gate when the user chose to fall back.
function promptMasterPassword({ title, subtitle }) {
return new Promise((resolve) => {
const wrap = document.createElement("div");
wrap.className = "pinmodal";
wrap.innerHTML = `
<div class="pincard">
<h2>${esc(title || "Confirm master password")}</h2>
<div class="pinsub">${esc(subtitle || "")}</div>
<div style="display:flex;flex-direction:column;gap:8px">
<input type="password" id="pmpPw" placeholder="Master password" autocomplete="current-password" autofocus>
<div class="msg err" id="pmpErr" hidden></div>
</div>
<div class="pinactions">
<button class="btn" id="pmpCancel" type="button">Cancel</button>
<button class="btn primary" id="pmpOk" type="button">Confirm</button>
</div>
</div>`;
document.body.appendChild(wrap);
const done = (v) => { try { wrap.remove(); } catch {} resolve(v); };
wrap.querySelector("#pmpCancel").addEventListener("click", () => done(null));
const submit = async () => {
const pw = $("pmpPw").value;
const err = $("pmpErr"); err.hidden = true;
if (!pw) return;
try {
// Re-unlock the vault to confirm the password is correct. Idempotent —
// if the vault is already open, calling unlock again is a no-op.
state = await S.invoke("vaultUnlock", { masterPassword: pw });
done(pw);
} catch (e) { err.textContent = cleanErr(e); err.hidden = false; }
};
wrap.querySelector("#pmpOk").addEventListener("click", submit);
$("pmpPw").addEventListener("keydown", (e) => { if (e.key === "Enter") submit(); });
try { $("pmpPw").focus(); } catch {}
});
}
// Ask the user to prove they know the PIN. Uses the same lockout counter
// as the unlock flow so an attacker can't drain guesses via a spammed
// Send button. Returns true on match, false on cancel / lockout / bad PIN.
async function verifyPinInteractively(subtitle) {
const remain = await pinLockoutRemainingMs();
if (remain > 0) {
aegisAlert(`PIN entry is locked for ${Math.ceil(remain / 60000)} min. Use "Remove" in Settings or wait it out.`);
return false;
}
return new Promise((resolve) => {
const wrap = document.createElement("div");
wrap.className = "pinmodal";
wrap.innerHTML = `
<div class="pincard">
<h2>Confirm with PIN</h2>
<div class="pinsub" id="vpSub">${esc(subtitle || "")}</div>
<div class="pinpad">
<div class="pindots" id="vpDots">${"<span class=\"pindot\"></span>".repeat(6)}</div>
<div class="pinkeys" id="vpKeys">
${[1,2,3,4,5,6,7,8,9].map((n) => `<button data-k="${n}">${n}</button>`).join("")}
<button class="util" data-k="clear">Clear</button>
<button data-k="0">0</button>
<button class="util" data-k="back">⌫</button>
</div>
<div class="pinerr" id="vpErr"></div>
</div>
<div class="pinactions">
<button class="btn" id="vpCancel" type="button">Cancel</button>
</div>
</div>`;
document.body.appendChild(wrap);
const done = (v) => { try { wrap.remove(); } catch {} resolve(v); };
wrap.querySelector("#vpCancel").addEventListener("click", () => done(false));
setupPinPad({
dots: $("vpDots"), keys: $("vpKeys"), err: $("vpErr"),
onComplete: async (pin) => {
try {
const blob = await S.invoke("pinBlobGet");
if (!blob) throw new Error("no PIN configured");
await pinDecryptMaster(pin, blob);
await S.invoke("pinFailReset").catch(() => {});
done(true);
return "ok";
} catch (e) {
const fs = await S.invoke("pinFailInc").catch(() => ({ count: 0 }));
const left = Math.max(0, PIN_MAX_FAILS - (fs?.count || 0));
$("vpErr").textContent = left > 0
? `Wrong PIN. ${left} attempt${left === 1 ? "" : "s"} left before a 15 min lockout.`
: `Locked for 15 min.`;
if (left === 0) { done(false); return "ok"; }
return "reset";
}
},
});
});
}
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
function renderPricesSetting() {
const p = state?.prices;
const toggle = $("pricesToggle");
if (!toggle) return;
toggle.checked = !!p?.enabled;
$("refreshPrices").hidden = !p?.enabled;
const st = $("pricesStatus");
const sourcesEl = $("pricesSources");
const paintStatus = () => {
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.";
};
paintStatus();
// Per-source status: name → up/down + last fetch age. Renders even when
// disabled so users can see WHICH oracles will be polled once they flip
// the switch. On a down source we surface the error text.
if (sourcesEl) {
const sources = Array.isArray(p?.sources) ? p.sources : [];
const status = p?.sourceStatus || {};
if (!sources.length) { sourcesEl.innerHTML = ""; }
else {
const rows = sources.map((s) => {
const st = status[s.id];
let tag = `<span style="color:var(--dim)">idle</span>`;
if (st) {
if (st.ok) {
const covers = Object.keys(st.prices || {}).length;
const age = Math.round((Date.now() - (st.at || Date.now())) / 1000);
tag = `<span style="color:var(--acid, #d6ff3d)">✓ ${covers} coin${covers === 1 ? "" : "s"}${age > 5 ? ` · ${age < 60 ? age + "s" : Math.round(age / 60) + "m"}` : ""}</span>`;
} else {
tag = `<span style="color:#f6768a" title="${esc(st.error || "")}">⚠ down</span>`;
}
}
return `<div style="display:flex;justify-content:space-between;gap:8px;padding:2px 0"><span>${esc(s.label)} <span style="color:var(--dim)">· ${esc(s.origin)}</span></span>${tag}</div>`;
});
sourcesEl.innerHTML = rows.join("");
}
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
}
}
async function renderSites() {
let perms = {};
try { perms = await S.invoke("permissions"); } catch {}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
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 = [];
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
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 {}
}));
}
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
$("applySettings").addEventListener("click", async () => {
const msg = $("settingsMsg"); msg.hidden = true;
try {
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const path = $("setPath").value.trim();
if (path && path !== (sel().accountPath || "")) {
state = await S.invoke("setAccountPath", { id: state.selectedWalletId, accountPath: path });
}
// Server list is now saved on-checkbox-tick via saveServerCheckboxes(),
// so Apply doesn't need to re-collect. Still refresh the pane so any
// path change reflects immediately.
settingsFilled = false; fillSettings(); render();
flash($("applySettings"), "Applied");
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
});
// Preset BCH mainnet Electrum servers users are likely to have heard of.
// Kept in sync with chain-bch.js's defaultServers so a fresh install with no
// custom pick behaves like this list. Order = suggested-priority.
const BCH_KNOWN_SERVERS = [
"wss://bch.imaginary.cash:50004",
"wss://cashnode.bch.ninja:50004",
"wss://electroncash.dk:50004",
"wss://fulcrum.jettscythe.xyz:50004",
];
function renderServerCheckboxes() {
const el = $("setServersList"); if (!el) return;
// The current list is the union of user-picked + presets; distinguish so we
// can render "custom" rows with a remove button while presets stay stable.
const current = new Set((state?.bchServers?.list || []).map(String));
const rows = [];
for (const url of BCH_KNOWN_SERVERS) {
rows.push({ url, checked: current.has(url), custom: false });
}
// Any picked URL that isn't in the presets list is treated as user-added.
for (const url of current) {
if (!BCH_KNOWN_SERVERS.includes(url)) rows.push({ url, checked: true, custom: true });
}
el.innerHTML = rows.map((r) => `<label>
<input type="checkbox" data-server="${esc(r.url)}" ${r.checked ? "checked" : ""}>
<span class="surl">${esc(r.url)}</span>
${r.custom ? `<button class="sremove" data-remove="${esc(r.url)}" title="Remove custom server">✕</button>` : ""}
</label>`).join("");
el.querySelectorAll("input[type=checkbox]").forEach((cb) => cb.addEventListener("change", saveServerCheckboxes));
el.querySelectorAll("[data-remove]").forEach((b) => b.addEventListener("click", async (e) => {
e.preventDefault();
const list = collectServerCheckboxes().filter((u) => u !== b.dataset.remove);
try { state = await S.invoke("setBchServers", { servers: list }); settingsFilled = false; fillSettings(); render(); }
catch (er) { $("settingsMsg").textContent = cleanErr(er); $("settingsMsg").hidden = false; }
}));
}
function collectServerCheckboxes() {
const el = $("setServersList");
if (!el) return [];
return [...el.querySelectorAll("input[type=checkbox]")]
.filter((cb) => cb.checked)
.map((cb) => cb.dataset.server);
}
async function saveServerCheckboxes() {
const list = collectServerCheckboxes();
try {
state = await S.invoke("setBchServers", { servers: list });
// Don't rebuild the whole settings pane on every checkbox tick — just
// refresh the hint line so the "connected to …" text stays current.
if (state?.selected?.chain === "bch") {
const s = state.selected;
$("serverHint").textContent = (state.bchServers?.custom ? "Custom list." : "Bundled defaults.") + (s.server ? " Connected to " + hostOf(s.server) + "." : " Not connected.");
}
} catch (e) { $("settingsMsg").textContent = cleanErr(e); $("settingsMsg").hidden = false; }
}
$("addCustomServer").addEventListener("click", async () => {
const input = $("setServersCustom");
const url = input.value.trim();
if (!/^wss?:\/\/[^/\s]+$/i.test(url)) {
$("settingsMsg").textContent = "Server must look like wss://host:port"; $("settingsMsg").hidden = false; return;
}
const list = [...new Set([...collectServerCheckboxes(), url])];
try {
state = await S.invoke("setBchServers", { servers: list });
input.value = "";
settingsFilled = false; fillSettings(); render();
} catch (e) { $("settingsMsg").textContent = cleanErr(e); $("settingsMsg").hidden = false; }
});
$("resetServers").addEventListener("click", async () => {
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
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;
const ok = await aegisConfirm({
title: `Remove "${s.label}"?`,
danger: true,
confirmLabel: "Remove wallet",
body: "The on-chain address stays exactly where it is; the wallet is only unlinked from Aegis.<br><br>You can add it back later by creating a new wallet on the same coin + network.",
});
if (!ok) return;
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
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 () => {
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
try { const r = await S.invoke("recovery", { id: state.selectedWalletId }); $("recovery").innerHTML = recoveryHtml(r); }
catch (e) { $("recovery").textContent = cleanErr(e); }
});
$("showXprv").addEventListener("click", async () => {
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
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;
}
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
document.querySelectorAll("nav button").forEach((b) => b.addEventListener("click", () => {
if (b.dataset.tab !== "settings") {
$("recovery").innerHTML = "";
$("scRecovery").innerHTML = "";
$("dgbRecovery").innerHTML = "";
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
$("btcRecovery").innerHTML = "";
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
$("ethRecovery").innerHTML = "";
$("solRecovery").innerHTML = "";
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
}
}));
// 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); }
});
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
// 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;
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
}
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
$(`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;
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
});
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
$("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; }
});
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
$("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); }
});
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield Multi-currency coverage matches what aegis.x has been advertising: BCH, TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for each. Panel logos, favicon and fallback all read as Aegis. - Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0 → secp256k1 → EIP-55 checksummed hex address (verified against MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet override). EIP-1559 send with an inline RLP encoder + secp256k1 recoverable sign; broadcast via eth_sendRawTransaction. personal_sign message signing follows the \x19Ethereum Signed Message:\n prefix. - Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519 derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble ed25519). SLIP-0010 layer verified against spec Test Vector 1 in scratchpad/verify-slip10.mjs. Native SOL transfer via the system program with compact-u16 message serialization + ed25519 sign + sendTransaction. Devnet gets a faucet.solana.com link in Receive; the panel appends ?cluster=devnet when opening the explorer. - DGB address family selector (lib/chain-dgb.js already carried the paths): the Settings block now shows a Native SegWit / Taproot / Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills the derivation-path input with that family's default; Apply rebuilds the wallet against the new path. Address families exposed via chainMeta.addressFamilies so the panel can render them from data. - Panel branding: inline SVG shield (hexagonal aspis, same silhouette as the aegis.x hero) replaces the "?" fallback in logoSvg() and is what the header shows before a wallet is selected. Data-URI favicon wired into panel.html so the Theseus sidebar tab icon reads as Aegis rather than a chain-specific coin mark. - QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB, EIP-681 for ETH, Solana Pay for SOL) so external scanners route the scan to the right wallet. Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter protocol (window.solana). The signing paths exist; only the page-inject bridge glue is missing. History for ETH/SOL is also empty in this rev — both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress + getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
// 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); }
});
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
$("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); }
});
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
// ---- 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) => {
fix(aegis): 0.9.5 — WizardConnect signing actually works Pairing already worked; signing would have thrown on the first request a dapp ever sent. Found by testing against the real relay and the real @wizardconnect/wallet library rather than reading the code. Two bugs in wc-sign.js, both fatal: - The WC message nests the whole WcSignTransactionRequest under `.transaction`, so the tx is at request.transaction.transaction and the spent outputs at request.transaction.sourceOutputs. We read request.transaction as the tx and request.sourceOutputs as the outputs, so tx.inputs was undefined. index.js already read the nested request.transaction.userPrompt for the approval dialog, so only the signer had it wrong. The flat shape is still accepted. - generateSigningSerializationBCH takes TWO positional arguments, (compilationContext, {coveredBytecode, signingSerializationType}). We passed one merged object, leaving coveredBytecode undefined and throwing inside libauth. For P2PKH the covered bytecode is the spent output's locking script. Now verified end to end: a two-input transaction spending from two different derivation paths signs, decodes, and passes createVirtualMachineBCH().verify() — consensus-valid, with SIGHASH_ALL|FORKID|UTXOS (0x61) on every input as the protocol requires. Also: RelayStatus is an object ({status: "connected" | "reconnecting" | "disconnected" | "session_deleted"}), and the snapshot read a non-existent `.kind`, so every connection reported the literal "[object Object]". Reads `.status` now, uses the documented getConnections() accessor instead of the private connections Map, and carries the library's own `label` ("dapp name once known, otherwise Connecting…"). The panel shows a tag for anything other than connected — "reconnecting" is the difference between a pairing that will see the next signature and one that is dead, which was invisible before.
2026-09-23 03:23:41 +02:00
const label = c.dappName || c.label || "(pairing…)";
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
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>
fix(aegis): 0.9.5 — WizardConnect signing actually works Pairing already worked; signing would have thrown on the first request a dapp ever sent. Found by testing against the real relay and the real @wizardconnect/wallet library rather than reading the code. Two bugs in wc-sign.js, both fatal: - The WC message nests the whole WcSignTransactionRequest under `.transaction`, so the tx is at request.transaction.transaction and the spent outputs at request.transaction.sourceOutputs. We read request.transaction as the tx and request.sourceOutputs as the outputs, so tx.inputs was undefined. index.js already read the nested request.transaction.userPrompt for the approval dialog, so only the signer had it wrong. The flat shape is still accepted. - generateSigningSerializationBCH takes TWO positional arguments, (compilationContext, {coveredBytecode, signingSerializationType}). We passed one merged object, leaving coveredBytecode undefined and throwing inside libauth. For P2PKH the covered bytecode is the spent output's locking script. Now verified end to end: a two-input transaction spending from two different derivation paths signs, decodes, and passes createVirtualMachineBCH().verify() — consensus-valid, with SIGHASH_ALL|FORKID|UTXOS (0x61) on every input as the protocol requires. Also: RelayStatus is an object ({status: "connected" | "reconnecting" | "disconnected" | "session_deleted"}), and the snapshot read a non-existent `.kind`, so every connection reported the literal "[object Object]". Reads `.status` now, uses the documented getConnections() accessor instead of the private connections Map, and carries the library's own `label` ("dapp name once known, otherwise Connecting…"). The panel shows a tag for anything other than connected — "reconnecting" is the difference between a pairing that will see the next signature and one that is dead, which was invisible before.
2026-09-23 03:23:41 +02:00
<div><div>${esc(label)} ${wcStatusTag(c.status)}</div><div class="hint mono">${esc((c.uri || "").slice(0, 46))}…</div></div>
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
<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;
}
});
// pricesSource select is a hidden legacy element in 0.6.36+ — the picker
// was removed when pricing moved to multi-source majority-rule. No change
// handler needed; kept the DOM node so panel.js code that reads .value
// doesn't NPE mid-migration.
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect 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.
2026-09-09 10:33:21 +02:00
$("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 ------------------------------------------------------------------
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
S.on("state", (s) => { state = s; render(); if (tab === "settings") fillSettings(); });
(async () => {
// Load security + session state first so the very first render() knows
// whether to paint the PIN pad on the lock screen and what idle-lock
// timer to arm once the vault is open.
try { await refreshSecurityState(); } catch {}
try { await refreshSessionState(); } catch {}
try { state = await S.invoke("state"); render(); }
catch (e) { $("gate").hidden = false; $("gate").innerHTML = `<div class="big">⚠</div><div>${esc(cleanErr(e))}</div>`; }
bindIdleAutoLock();
})();
// Persistent footer: aegis.x brand link + version marker + update check.
// The check button hits the OTA manifest and compares versions client-side;
// when a newer one is advertised, the pill turns into an "Update to vX.Y.Z"
// chip. Clicking that chip fires the addon-message "requestUpdate" which
// runs the same check + apply flow used by Settings > Extensions > Aegis
// (falls back to opening Settings for pre-0.3.47 Theseus that lacks the
// panel-facing apply path).
const OTA_URL = "https://navigate.st/bns/theseus.x/extensions/aegis/updates.json";
let footerCurrentVer = null;
let footerLatestKnown = null;
function cmpSemver(a, b) {
const pa = String(a || "0").split(".").map((n) => Number(n) || 0);
const pb = String(b || "0").split(".").map((n) => Number(n) || 0);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const d = (pa[i] || 0) - (pb[i] || 0);
if (d) return d < 0 ? -1 : 1;
}
return 0;
}
// Track whether the last check was manual. Auto-checks stay silent when
// nothing new is available; manual clicks always get a visible reply so
// the ↻ button never feels dead when the user is already current.
let footerLastCheckManual = false;
function paintFooterUpdate() {
const el = $("brandUpdate");
const verEl = $("brandVer");
if (!el) return;
// 0.8.1: the update chip and version marker share the same slot at the
// bottom-right. When a newer build is available the chip takes over the
// slot and the raw "v0.8.0" marker hides; when the check flashes "up to
// date" the chip briefly steals it back; otherwise the version marker
// is the resting state and the chip stays hidden. showVer/hideVer are
// no-ops when brandVer isn't in the DOM so the render is idempotent.
const showVer = () => { if (verEl) verEl.hidden = false; };
const hideVer = () => { if (verEl) verEl.hidden = true; };
if (!footerCurrentVer || !footerLatestKnown) { el.hidden = true; showVer(); return; }
if (cmpSemver(footerLatestKnown, footerCurrentVer) > 0) {
el.hidden = false;
el.className = "brandupd";
el.textContent = "↑ v" + footerLatestKnown;
el.title = "Aegis v" + footerLatestKnown + " is available — click to install";
el.style.cursor = "pointer";
el.onclick = () => triggerFooterUpdate();
hideVer();
return;
}
// At-or-past latest: silent on auto-check (unobtrusive), transient
// "up to date" flash on manual so the ↻ click has visible feedback.
if (footerLastCheckManual) {
el.hidden = false;
el.className = "brandupd brandok";
el.textContent = "✓ Up to date";
el.title = "Aegis v" + footerCurrentVer + " is the latest";
el.style.cursor = "default";
el.onclick = null;
hideVer();
setTimeout(() => {
if (el.classList.contains("brandok")) { el.hidden = true; showVer(); }
}, 2200);
} else {
el.hidden = true;
showVer();
}
}
async function checkFooterUpdate(opts = {}) {
const btn = $("brandCheck");
if (btn) btn.classList.add("spin");
footerLastCheckManual = !!opts.manual;
try {
// The addon frame runs under a file:// origin — fetch to https is fine,
// no CORS block since no server headers are involved for a same-origin
// request... actually addon frames CAN cross-fetch. Cache-bust with a
// per-minute query so a fresh check reflects a just-published manifest.
const bust = Math.floor(Date.now() / 60_000);
const r = await fetch(OTA_URL + "?t=" + bust, { cache: "no-store" });
if (!r.ok) throw new Error("HTTP " + r.status);
const j = await r.json();
const entries = Array.isArray(j?.addons) ? j.addons : [];
let best = null;
for (const e of entries) if (!best || cmpSemver(e.version, best.version) > 0) best = e;
footerLatestKnown = best?.version || null;
paintFooterUpdate();
} catch (e) {
console.warn("footer update check failed:", e?.message || e);
if (footerLastCheckManual) {
const el = $("brandUpdate");
if (el) {
el.hidden = false;
el.className = "brandupd branderr";
el.textContent = "⚠ Check failed";
el.title = String(e?.message || e);
el.style.cursor = "default";
el.onclick = null;
setTimeout(() => { if (el.classList.contains("branderr")) el.hidden = true; }, 2500);
}
}
} finally {
if (btn) btn.classList.remove("spin");
}
}
// Two-step chip flow. First click → stage the newer signed build; the
// chip's message and click handler swap to "Restart Theseus to apply".
// Second click → app.relaunch(). Both steps go through the same
// requestUpdate handler so a single Theseus IPC round-trip covers each
// leg. Falls back to opening Settings › Extensions when running under
// an older Theseus that lacks the panel-driven update hooks.
async function triggerFooterUpdate() {
const el = $("brandUpdate"); if (!el) return;
const verEl = $("brandVer");
// While the chip is doing something, the raw version marker stays
// hidden — the chip owns the slot end-to-end for the whole transaction
// so the user never sees "v0.8.0 ↑ v0.8.1" simultaneously in the
// corner. Returned to the version marker only after the flash timers
// clear (below).
if (verEl) verEl.hidden = true;
const setChip = (text, klass, title, handler) => {
el.hidden = false;
el.className = "brandupd" + (klass ? " " + klass : "");
el.textContent = text;
el.title = title || "";
el.style.cursor = handler ? "pointer" : "default";
el.onclick = handler || null;
};
const restoreVer = () => { el.hidden = true; if (verEl) verEl.hidden = false; };
try {
setChip("Staging update…", "brandwait", "Downloading + verifying the signed payload", null);
const r = await S.invoke("requestUpdate", { step: "stage" });
if (r?.fallback === "settings") {
setChip("Open Settings to update", null, "This Theseus lacks the in-panel updater — opening Settings › Extensions", () => S.invoke("openSettings", { section: "addons" }).catch(() => {}));
return;
}
if (r?.staged) {
const nextVer = r.next ? " v" + r.next : "";
setChip("↻ Restart to apply" + nextVer, null, "Aegis" + nextVer + " is staged — click to relaunch Theseus", async () => {
setChip("Restarting…", "brandwait", "", null);
try { await S.invoke("requestUpdate", { step: "apply" }); }
catch (e) { setChip("⚠ Restart failed", "branderr", String(e?.message || e), null); }
});
return;
}
const msg = r?.status === "up-to-date" ? "✓ Already up to date"
: r?.status ? "⚠ " + r.status : "⚠ Update failed";
setChip(msg, r?.status === "up-to-date" ? "brandok" : "branderr", r?.detail || "", null);
setTimeout(() => { if (el.classList.contains("brandok") || el.classList.contains("branderr")) restoreVer(); }, 2500);
} catch (e) {
console.warn("update trigger failed:", e?.message || e);
setChip("⚠ Update failed", "branderr", String(e?.message || e), null);
setTimeout(() => { if (el.classList.contains("branderr")) restoreVer(); }, 2500);
}
}
(function wireFooter() {
const link = $("brandLink"); if (!link) return;
link.addEventListener("click", (e) => { e.preventDefault(); openUrl("https://aegis.x/"); });
const a1 = $("aboutOpenAegisSite");
if (a1) a1.addEventListener("click", (e) => { e.preventDefault(); openUrl("https://aegis.x/"); });
const a2 = $("aboutOpenSilentmodeSite");
if (a2) a2.addEventListener("click", (e) => { e.preventDefault(); openUrl("https://silentmode.st/"); });
// Version comes from the addon manifest; if the state message carries it
// we surface it, otherwise the slot stays empty.
S.invoke("aegisVersion").then((v) => {
const el = $("brandVer");
if (el && v) el.textContent = "v" + String(v);
footerCurrentVer = v || null;
paintFooterUpdate();
}).catch(() => {});
const check = $("brandCheck");
if (check) check.addEventListener("click", () => checkFooterUpdate({ manual: true }));
// First check on panel open — non-blocking; failures stay quiet. A user
// who never opens Settings still gets a clear update signal here.
setTimeout(() => checkFooterUpdate({ manual: false }), 500);
})();