theseus/bundled-addons/aegis/panel.js

6038 lines
312 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 };
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
// Cached security state ({ hasPin, requirePinForSending, requirePinForReveal }).
// Populated on startup and refreshed after any pin/security invoke — used by
// the lock screen (PIN vs. password), the Settings General card, and the
// reveal gate. requirePinForReveal defaults TRUE, here and in the host, so a
// failed read never lands on the permissive setting.
let securityState = { hasPin: false, requirePinForSending: false, requirePinForReveal: true };
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 ----------------------------------------------------------
// The pads below only collect six digits. The host (index.js pinUnwrap)
// holds the PIN blob, counts every guess before trying it, and enforces the
// 15-minute lockout after PIN_MAX_FAILS wrong ones. The panel never sees the
// blob, so it cannot be searched from here, and it cannot reset the counter.
// pinTry(pin) -> { ok: true, masterPassword } | { ok: false, remaining, lockedMs }
const PIN_MAX_FAILS = 5;
// A refusal that is not a verdict on the PIN (the TPM is in its lockout
// window, or its key is gone) carries `error`; it is thrown, so each pad's
// catch shows it as it is instead of as a wrong PIN.
const pinTry = async (pin) => {
const r = await S.invoke("pinUnwrap", { pin: String(pin) });
if (r && !r.ok && r.error) throw new Error(r.error);
// Theseus now has one PIN for everything. An Aegis PIN entered correctly
// while Theseus already has a different one: ask once which to keep.
if (r && r.ok && r.pinConflict) {
const replace = await aegisConfirm({
title: "Use this PIN everywhere?",
confirmLabel: "Use this PIN",
cancelLabel: "Keep the Theseus PIN",
body: "Theseus and Aegis now share one PIN. Theseus already has a PIN of its own. Use the PIN you just entered for both (Settings, Pithos and Aegis), or keep the Theseus PIN — your old Aegis PIN then stops working.",
});
try { await S.invoke("pinMigrateResolve", { replace: !!replace }); } catch {}
try { await refreshSecurityState(); } catch {}
}
return r;
};
async function pinLockoutRemainingMs() {
try { return Number((await S.invoke("pinStatus")).lockedMs) || 0; } catch { return 0; }
}
async function refreshSecurityState() {
try {
securityState = await S.invoke("securityGet");
securityLoaded = true;
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
} catch { securityState = { hasPin: false, requirePinForSending: false, requirePinForReveal: true }; 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");
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
// Start the panel over rather than re-render it. Whatever was open
// goes with the page: a revealed key, an import form holding a seed
// phrase, a half-filled send, the password remembered for PIN
// enrolment. Painting a lock screen underneath left all of that on
// top of a "locked" wallet.
try { delete window.__aegisLastPw; } catch {}
location.reload();
} 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)); }
});
// Network indicator and network switch, merged into one always-visible
// control under the balance. Before this, the indicator was a chip in the
// status row and the real selector was a chip row inside the picker's coin
// drilldown: the half that told you where you were and the half that could
// move you were in different places, and the useful half was two clicks deep.
//
// A chain with a single network still renders its one chip. That chip is the
// indicator, and dropping it would make the header height jump as the user
// moves between chains.
function paintNetSelector(s) {
const host = $("hNetSel");
if (!host) return;
const chain = s && s.chain;
if (!chain) { host.hidden = true; host.innerHTML = ""; return; }
const mine = (state?.wallets || []).filter((w) => w.chain === chain);
const nets = orderNetworks(Array.from(new Set(mine.map((w) => w.network))));
if (!nets.length) { host.hidden = true; host.innerHTML = ""; return; }
host.hidden = false;
host.innerHTML = nets.map((n) => {
const peers = mine.filter((w) => w.network === n);
const lbl = networkLabelFor(chain, n, n);
// "Chipnet testnet TEST" says it twice. Only tag a testnet whose own
// label doesn't already announce it (Nile, Sepolia, devnet, signet).
const isTestnet = !!peers.find((w) => w.testnet) && !/test/i.test(lbl);
return `<button type="button" class="netselchip ${n === s.network ? "on" : ""}" data-netsel="${esc(n)}"
title="${n === s.network ? "You are on " + esc(lbl) : "Switch to " + esc(lbl)} — ${peers.length} wallet${peers.length === 1 ? "" : "s"}">
${esc(lbl)}${isTestnet ? " " + testnetTag() : ""}<span class="cnt">${peers.length}</span>
</button>`;
}).join("");
host.querySelectorAll("[data-netsel]").forEach((b) => b.addEventListener("click", async (e) => {
e.stopPropagation();
const net = b.dataset.netsel;
if (net === s.network) return;
const peers = mine.filter((w) => w.network === net);
if (!peers.length) return;
// Keep the picker's own per-chain network memory in step, so the two
// views never disagree about which network this chain is showing.
activeNetworkByChain.set(chain, net);
persistActiveNetworks();
// The wallet list has to come along. In the addresses drilldown its
// groupKey pins "<chain>:<network>", so without this the header says
// chipnet while the list below keeps showing mainnet addresses.
const subKey = `${chain}:${net}`;
if (stripView.mode === "addresses" && String(stripView.groupKey || "").split(":")[0] === chain) {
stripView = { mode: "addresses", groupKey: subKey };
}
// Land on whichever wallet this network was last left on, so the header
// switch and the row the strip highlights are the same wallet.
const remembered = activeWalletBySubgroup.get(subKey);
const target = peers.find((w) => w.id === remembered) || peers[0];
activeWalletBySubgroup.set(subKey, target.id);
try {
state = await S.invoke("selectWallet", { id: target.id });
settingsFilled = false;
render();
} catch (err) { aegisAlert("Could not switch network: " + cleanErr(err)); }
}));
}
// + 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); }
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
else if (e.key === "Enter") {
// Enter means "the focused button". It used to mean yes even with
// focus on Cancel, so Tab → Enter removed the wallet.
e.preventDefault(); e.stopPropagation();
finish(document.activeElement?.dataset?.ac !== "no");
}
};
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");
}));
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
// A destructive question opens on Cancel, like the host's own overlay.
const yes = overlay.querySelector('[data-ac="yes"]');
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
const no = overlay.querySelector('[data-ac="no"]');
if (o.danger && no) no.focus(); else 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(aegis): 0.21.0 — you pick which wallet pays and which one dapps get A WizardConnect pairing could land on an address the user had never seen. Pairing took the selected wallet when it qualified and otherwise the first pairable one in list order, so with the selected wallet ineligible (a WIF import has no xpub) it silently fell through to whichever wallet happened to be first. The approval named that wallet by label only, which does not help when the label is one Aegis generated. Three changes, one idea: the wallet a purpose uses should be something you said, not something that fell out of list order. - Roles. A wallet can be nominated for payments and/or for WizardConnect, set from its manage modal and badged on its row. A role that points at a removed wallet reads back as null instead of being trusted, so a stale pointer can never quietly redirect a payment. - The pairing approval asks. With more than one candidate it offers a dropdown of them, each labelled with its own short address; with only one it shows that wallet's full address. The rows are static, so naming a wallet above a select the user can change would contradict itself — hence one or the other, never both. The panel's own Connect pane now follows the same precedence, because two rules for "which wallet" is how a pairing surprises someone. - Send gets a From row listing the wallets on this coin and network, with balances. It switches the panel selection rather than carrying a separate source: planSend and send resolve the wallet host-side from that, and a second notion of "current" would let the form and the approval disagree. Payments also becomes the opening selection when nothing has been picked yet, which is what nominating it is for. No Theseus release needed — approvalModal has supported a select row all along, and the pick comes back as "allow+wallet=<id>", validated against the options offered.
2026-10-01 00:59:17 +02:00
// Per-purpose defaults. WizardConnect is BCH-only and a WIF import has no
// xpub to pair with, so the row explains itself rather than offering a
// choice that must fail.
const roles = (state && state.roles) || {};
const isPay = roles.payments === w.id;
const isWc = roles.wizardconnect === w.id;
const wcOk = w.chain === "bch" && !w.wcBlocked;
const wcWhy = w.chain !== "bch"
? "WizardConnect pairs Bitcoin Cash wallets only."
: (w.wcBlocked || "");
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.21.0 — you pick which wallet pays and which one dapps get A WizardConnect pairing could land on an address the user had never seen. Pairing took the selected wallet when it qualified and otherwise the first pairable one in list order, so with the selected wallet ineligible (a WIF import has no xpub) it silently fell through to whichever wallet happened to be first. The approval named that wallet by label only, which does not help when the label is one Aegis generated. Three changes, one idea: the wallet a purpose uses should be something you said, not something that fell out of list order. - Roles. A wallet can be nominated for payments and/or for WizardConnect, set from its manage modal and badged on its row. A role that points at a removed wallet reads back as null instead of being trusted, so a stale pointer can never quietly redirect a payment. - The pairing approval asks. With more than one candidate it offers a dropdown of them, each labelled with its own short address; with only one it shows that wallet's full address. The rows are static, so naming a wallet above a select the user can change would contradict itself — hence one or the other, never both. The panel's own Connect pane now follows the same precedence, because two rules for "which wallet" is how a pairing surprises someone. - Send gets a From row listing the wallets on this coin and network, with balances. It switches the panel selection rather than carrying a separate source: planSend and send resolve the wallet host-side from that, and a second notion of "current" would let the form and the approval disagree. Payments also becomes the opening selection when nothing has been picked yet, which is what nominating it is for. No Theseus release needed — approvalModal has supported a select row all along, and the pick comes back as "allow+wallet=<id>", validated against the options offered.
2026-10-01 00:59:17 +02:00
<div style="border-top:1px solid var(--line);margin:12px 0 0;padding-top:10px">
<div class="lbl" style="margin-bottom:6px">Use this wallet for</div>
<label style="display:flex;align-items:center;gap:8px;margin-bottom:6px;cursor:pointer">
<input type="checkbox" id="mwRolePay" ${isPay ? "checked" : ""}>
<span>Payments <span class="hint" style="font-weight:normal">— opens here when nothing else is picked</span></span>
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;${wcOk ? "" : "opacity:.55;cursor:default"}">
<input type="checkbox" id="mwRoleWc" ${isWc ? "checked" : ""} ${wcOk ? "" : "disabled"}>
<span>WizardConnect <span class="hint" style="font-weight:normal">— offered first when a site asks to pair</span></span>
</label>
${wcOk ? "" : `<div class="hint" style="margin-top:4px">${esc(wcWhy)}</div>`}
</div>
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
<div style="border-top:1px solid var(--line);margin:12px 0 0;padding-top:10px">
<div class="lbl" style="margin-bottom:4px">Secret key</div>
<div class="hint" style="margin-bottom:8px">Everything needed to spend this wallet elsewhere. Aegis asks for your PIN (or master password) first.</div>
<button class="btn" id="mwReveal" type="button">Show secret key…</button>
</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 });
}
feat(aegis): 0.21.0 — you pick which wallet pays and which one dapps get A WizardConnect pairing could land on an address the user had never seen. Pairing took the selected wallet when it qualified and otherwise the first pairable one in list order, so with the selected wallet ineligible (a WIF import has no xpub) it silently fell through to whichever wallet happened to be first. The approval named that wallet by label only, which does not help when the label is one Aegis generated. Three changes, one idea: the wallet a purpose uses should be something you said, not something that fell out of list order. - Roles. A wallet can be nominated for payments and/or for WizardConnect, set from its manage modal and badged on its row. A role that points at a removed wallet reads back as null instead of being trusted, so a stale pointer can never quietly redirect a payment. - The pairing approval asks. With more than one candidate it offers a dropdown of them, each labelled with its own short address; with only one it shows that wallet's full address. The rows are static, so naming a wallet above a select the user can change would contradict itself — hence one or the other, never both. The panel's own Connect pane now follows the same precedence, because two rules for "which wallet" is how a pairing surprises someone. - Send gets a From row listing the wallets on this coin and network, with balances. It switches the panel selection rather than carrying a separate source: planSend and send resolve the wallet host-side from that, and a second notion of "current" would let the form and the approval disagree. Payments also becomes the opening selection when nothing has been picked yet, which is what nominating it is for. No Theseus release needed — approvalModal has supported a select row all along, and the pick comes back as "allow+wallet=<id>", validated against the options offered.
2026-10-01 00:59:17 +02:00
// Roles. Only send a change: unticking clears the role, but only when
// this wallet is the one currently holding it, so closing the modal on
// some other wallet can't unset someone else's default.
const wantPay = !!overlay.querySelector("#mwRolePay")?.checked;
const wantWc = !!overlay.querySelector("#mwRoleWc")?.checked;
if (wantPay !== isPay) {
state = await S.invoke("setWalletRole", { role: "payments", walletId: wantPay ? w.id : null });
}
if (wcOk && wantWc !== isWc) {
state = await S.invoke("setWalletRole", { role: "wizardconnect", walletId: wantWc ? w.id : null });
}
close();
fillPicker();
render();
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
});
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
overlay.querySelector("#mwReveal").addEventListener("click", () => openRevealSecretModal(w));
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;
// The sweep is a spend: with "PIN on every transaction" the host refuses
// it without a proof given for a transaction.
if (!await pinGate("transaction", "Confirm the sweep with your PIN.")) return;
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
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: [
// Chipnet prefills the BCH coin type, not BIP44's testnet 1'. Chipnet
// is a BCH testnet and the tooling around it (keystore exports,
// faucets, test wallets) overwhelmingly derives from 145'; prefilling
// 1' sent every chipnet seed import to an empty address. 1' is still
// one of the presets, and the scan finds whichever is real.
// NOTE: this is the IMPORT prefill only. Vault-derived chipnet wallets
// keep m/44'/1'/0' (chain-bch.js) — changing that would move the
// addresses of wallets that already exist.
{ id: "chipnet", label: "Chipnet testnet", defaultPath: "m/44'/145'/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 };
}
// jsQR is 257 KB of vendored decoder and it is only needed when someone
// actually imports a QR image. Loading it from panel.html meant every panel
// open — every hide/show — parsed all of it before panel.js even started.
// Fetch it the first time it is wanted instead.
let jsqrLoad = null;
function loadJsQr() {
if (typeof jsQR === "function") return Promise.resolve();
if (jsqrLoad) return jsqrLoad;
jsqrLoad = new Promise((resolve, reject) => {
const el = document.createElement("script");
el.src = "lib/jsqr.js";
el.onload = () => (typeof jsQR === "function" ? resolve() : reject(new Error("the QR decoder loaded but exposed nothing")));
el.onerror = () => { jsqrLoad = null; reject(new Error("the QR decoder failed to load")); };
document.head.appendChild(el);
});
return jsqrLoad;
}
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
async function decodeQrFile(file) {
await loadJsQr();
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
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>
<!-- One prefilled path was the whole bug: a seed from a wallet on a
different path imports a valid but empty address, reports 0, and
gives no way to tell a wrong path from an empty wallet. Presets
name the wallets each path belongs to; the scan below asks the
chain which one actually holds coins. -->
<select id="imPathPreset" style="width:100%;padding:7px 9px;border-radius:7px;background:var(--panel);border:1px solid var(--line);color:var(--ink);font-size:13px;margin-bottom:6px" hidden></select>
<input type="text" id="imPath" spellcheck="false" placeholder="m/…">
<div id="imScanRow" style="margin-top:6px" hidden>
<button class="btn sm" id="imScanBtn" type="button">Check which path has my funds</button>
</div>
<div id="imScanOut"></div>
<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(true);
paintPathPresets();
}));
overlay.querySelectorAll('input[name="imKind"]').forEach((r) => r.addEventListener("change", () => {
updateFormatFields();
paintPathPresets();
}));
updatePathDefault(true);
updateFormatFields();
paintPathPresets();
}
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 = "";
}
}
// Derivation-path presets + the "which path has my funds" scan. Both are
// mnemonic-only: a WIF carries its own single key and has no path.
const CUSTOM = "__custom__";
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
// Which master-key variant the chosen path belongs to (DigiByte only;
// null means BIP32's standard "Bitcoin seed").
let importHmacKey = null;
let lastCandidates = null;
async function paintPathPresets() {
const presetSel = overlay.querySelector("#imPathPreset");
const scanRow = overlay.querySelector("#imScanRow");
const out = overlay.querySelector("#imScanOut");
if (!presetSel || !scanRow) return;
out.innerHTML = "";
const netId = overlay.querySelector('input[name="imNet"]:checked')?.value || "";
const fmt = overlay.querySelector('input[name="imKind"]:checked')?.value || "mnemonic";
let cands = [];
if (fmt === "mnemonic") {
try { cands = (await S.invoke("seedPathCandidates", { chain: curChain, network: netId }))?.candidates || []; }
catch { cands = []; }
}
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
lastCandidates = cands;
if (!cands.length) { presetSel.hidden = true; scanRow.hidden = true; return; }
presetSel.hidden = false;
scanRow.hidden = false;
const cur = overlay.querySelector("#imPath").value.trim();
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
// The value is an index, not the path: DigiByte lists the same path
// twice under two different master keys, so the path alone is not a
// unique choice.
presetSel.innerHTML = cands.map((c, i) =>
`<option value="${i}" ${c.path === cur && !c.hmacKey ? "selected" : ""}>${esc(c.path)} — ${esc(c.note)}</option>`
).join("") + `<option value="${CUSTOM}" ${cands.some((c) => c.path === cur) ? "" : "selected"}>Custom…</option>`;
presetSel.onchange = () => {
if (presetSel.value === CUSTOM) { overlay.querySelector("#imPath").focus(); return; }
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
const c = cands[Number(presetSel.value)];
if (!c) return;
overlay.querySelector("#imPath").value = c.path;
importHmacKey = c.hmacKey || null;
};
// Typing by hand flips the select to Custom rather than leaving it
// pointing at a preset the field no longer matches.
overlay.querySelector("#imPath").oninput = () => {
const v = overlay.querySelector("#imPath").value.trim();
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
const i = cands.findIndex((c) => c.path === v && !c.hmacKey);
presetSel.value = i >= 0 ? String(i) : CUSTOM;
// Typing a path by hand means the standard master key unless a scan
// result sets it again.
if (i >= 0) importHmacKey = null;
};
}
async function runPathScan() {
const out = overlay.querySelector("#imScanOut");
const btn = overlay.querySelector("#imScanBtn");
const mnemonic = overlay.querySelector("#imMnemonic").value.trim();
if (!mnemonic) {
out.innerHTML = `<div class="msg err" style="margin-top:6px">Paste the seed phrase first — the scan derives addresses from it to ask the chain which path has history.</div>`;
return;
}
const netId = overlay.querySelector('input[name="imNet"]:checked')?.value || "";
btn.disabled = true;
const old = btn.textContent;
btn.textContent = "Scanning…";
out.innerHTML = `<div class="hint" style="margin-top:6px">Deriving candidates and querying the network…</div>`;
try {
const r = await S.invoke("scanSeedPaths", { chain: curChain, network: netId, mnemonic });
const any = r.results.some((x) => x.balance > 0 || x.txCount > 0);
const rows = r.results.map((x) => {
const funded = x.balance > 0;
const amt = fmtBig(x.balance || 0, r.decimals);
const tag = funded
? `<b style="color:var(--acid)">${esc(amt)} ${esc(r.ticker)}</b>`
: x.txCount > 0 ? `<span class="hint">no balance, ${x.txCount} past tx</span>`
: `<span class="hint">nothing</span>`;
return `<div class="tx" style="grid-template-columns:1fr auto;cursor:default;align-items:center;margin-top:4px">
<div>
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
<div class="mono" style="font-size:12px">${esc(x.path)}${x.hmacKey ? ` <span class="ttag">${esc(x.hmacKey)}</span>` : ""}</div>
<div class="hint">${esc(x.note)} · ${tag}</div>
</div>
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
<button class="btn sm ${funded ? "primary" : ""}" data-usepath="${esc(x.path)}" data-usehmac="${esc(x.hmacKey || "")}">Use</button>
</div>`;
}).join("");
out.innerHTML = (any
? `<div class="hint" style="margin-top:6px">Scanned ${r.results.length} paths, ${r.results[0]?.scanned || 0} addresses each.</div>`
: `<div class="msg err" style="margin-top:6px">None of these paths has any history on ${esc(r.network)}. Either this seed has never been used here, or its wallet uses a path Aegis does not list — type it in by hand and re-scan.</div>`)
+ rows;
out.querySelectorAll("[data-usepath]").forEach((b) => b.addEventListener("click", () => {
overlay.querySelector("#imPath").value = b.dataset.usepath;
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
importHmacKey = b.dataset.usehmac || null;
const ps = overlay.querySelector("#imPathPreset");
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
if (ps) {
const i = (lastCandidates || []).findIndex((c) => c.path === b.dataset.usepath && (c.hmacKey || "") === (b.dataset.usehmac || ""));
ps.value = i >= 0 ? String(i) : CUSTOM;
}
flash(b, "Set");
}));
} catch (e) {
out.innerHTML = `<div class="msg err" style="margin-top:6px">${esc(cleanErr(e))}</div>`;
} finally { btn.disabled = false; btn.textContent = old; }
}
overlay.querySelector("#imScanBtn").addEventListener("click", runPathScan);
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();
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
if (importHmacKey) payload.hmacKey = importHmacKey;
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 (!await pinGate("transaction", `Confirm consolidating ${checked.length} wallet${checked.length === 1 ? "" : "s"} with your PIN.`)) {
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 {
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
const res = await S.invoke("consolidateIntoSelected", { sourceIds: checked, destinationWalletId: preview?.destinationWalletId });
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>` : esc(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 (!await pinGate("transaction", `Confirm consolidating ${checked.length} wallet${checked.length === 1 ? "" : "s"} with your PIN.`)) {
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 {
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
const res = await S.invoke("consolidateIntoSelected", { sourceIds: checked, destinationWalletId: preview?.destinationWalletId });
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>` : esc(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.
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
const RCV_VIEWS = ["address", "assets", "nfts"];
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
let rcvView = "address";
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
try {
const saved = localStorage.getItem("aegis/rcvView");
if (RCV_VIEWS.includes(saved)) rcvView = saved;
} catch (_e) {}
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
function paintRcvView() {
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
const addr = $("rcvAddressPane"), assets = $("rcvAssetsPane"), nfts = $("rcvNftsPane");
if (!addr || !assets || !nfts) return;
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
document.querySelectorAll("[data-rcv-view]").forEach((b) =>
b.classList.toggle("on", b.dataset.rcvView === rcvView));
addr.hidden = rcvView !== "address";
assets.hidden = rcvView !== "assets";
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
nfts.hidden = rcvView !== "nfts";
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
// 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 blocked = readyBch.filter((w) => w.wcBlocked);
feat(aegis): 0.21.0 — you pick which wallet pays and which one dapps get A WizardConnect pairing could land on an address the user had never seen. Pairing took the selected wallet when it qualified and otherwise the first pairable one in list order, so with the selected wallet ineligible (a WIF import has no xpub) it silently fell through to whichever wallet happened to be first. The approval named that wallet by label only, which does not help when the label is one Aegis generated. Three changes, one idea: the wallet a purpose uses should be something you said, not something that fell out of list order. - Roles. A wallet can be nominated for payments and/or for WizardConnect, set from its manage modal and badged on its row. A role that points at a removed wallet reads back as null instead of being trusted, so a stale pointer can never quietly redirect a payment. - The pairing approval asks. With more than one candidate it offers a dropdown of them, each labelled with its own short address; with only one it shows that wallet's full address. The rows are static, so naming a wallet above a select the user can change would contradict itself — hence one or the other, never both. The panel's own Connect pane now follows the same precedence, because two rules for "which wallet" is how a pairing surprises someone. - Send gets a From row listing the wallets on this coin and network, with balances. It switches the panel selection rather than carrying a separate source: planSend and send resolve the wallet host-side from that, and a second notion of "current" would let the form and the approval disagree. Payments also becomes the opening selection when nothing has been picked yet, which is what nominating it is for. No Theseus release needed — approvalModal has supported a select row all along, and the pick comes back as "allow+wallet=<id>", validated against the options offered.
2026-10-01 00:59:17 +02:00
// Same precedence as the page-initiated pairing in index.js — the wallet
// nominated for WizardConnect, else whatever the panel is showing. Two
// different rules here and there is how a pairing surprises someone.
const wcRole = (state && state.roles && state.roles.wizardconnect) || null;
const preferId = (pairable.find((w) => w.id === wcRole) || pairable.find((w) => w.id === state.selectedWalletId) || pairable[0] || {}).id || null;
const opt = (w) => `<option value="${esc(w.id)}" ${w.id === preferId ? "selected" : ""} ${w.wcBlocked ? "disabled" : ""}>${esc(w.label)} · ${esc(w.networkLabel)}${w.id === wcRole ? " ★" : ""}${w.wcBlocked ? ` — can't pair (${esc(w.wcBlockedShort || "unsupported")})` : ""}</option>`;
// Pairable first, under a heading, whenever anything is blocked. In
// registry order a bulk keystore import puts fifteen unpairable WIF
// wallets ahead of the two chipnet ones that pair fine, and a list whose
// visible rows are all greyed-out testnet entries reads as "chipnet isn't
// supported" — which is not true and never was. Grouping keeps the
// reasons visible without letting them bury the working options.
const options = blocked.length
? `<optgroup label="Can pair">${pairable.map(opt).join("")}</optgroup>`
+ `<optgroup label="Cannot pair">${blocked.map(opt).join("")}</optgroup>`
: pairable.map(opt).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 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>
<div><div>${esc(c.dappName || c.label || "(pairing…)")} ${wcStatusTag(c.status)}</div><div class="hint">${c.pairedFrom ? `paired from <b>${esc(c.pairedFrom === "panel" ? "Aegis (pasted code)" : c.pairedFrom)}</b> · ` : ""}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>`;
feat(aegis): 0.21.0 — you pick which wallet pays and which one dapps get A WizardConnect pairing could land on an address the user had never seen. Pairing took the selected wallet when it qualified and otherwise the first pairable one in list order, so with the selected wallet ineligible (a WIF import has no xpub) it silently fell through to whichever wallet happened to be first. The approval named that wallet by label only, which does not help when the label is one Aegis generated. Three changes, one idea: the wallet a purpose uses should be something you said, not something that fell out of list order. - Roles. A wallet can be nominated for payments and/or for WizardConnect, set from its manage modal and badged on its row. A role that points at a removed wallet reads back as null instead of being trusted, so a stale pointer can never quietly redirect a payment. - The pairing approval asks. With more than one candidate it offers a dropdown of them, each labelled with its own short address; with only one it shows that wallet's full address. The rows are static, so naming a wallet above a select the user can change would contradict itself — hence one or the other, never both. The panel's own Connect pane now follows the same precedence, because two rules for "which wallet" is how a pairing surprises someone. - Send gets a From row listing the wallets on this coin and network, with balances. It switches the panel selection rather than carrying a separate source: planSend and send resolve the wallet host-side from that, and a second notion of "current" would let the form and the approval disagree. Payments also becomes the opening selection when nothing has been picked yet, which is what nominating it is for. No Theseus release needed — approvalModal has supported a select row all along, and the pick comes back as "allow+wallet=<id>", validated against the options offered.
2026-10-01 00:59:17 +02:00
// Role badges, so which wallet pays and which one dapps get is legible
// from the list instead of only from inside the manage modal. They share
// the label's grid column via a flex wrapper: the label keeps its
// ellipsis, the badge stays whole.
const roles = (state && state.roles) || {};
const badges = [
roles.payments === w.id ? `<span class="rolebadge pay" title="Default wallet for payments">pay</span>` : "",
roles.wizardconnect === w.id ? `<span class="rolebadge" title="Offered first when a site asks to pair over WizardConnect">wc</span>` : "",
].join("");
const labelCell = `<span class="walabel walabel-wide" title="${esc(w.label || "")}">${esc(w.label || shortName || "—")}</span>`;
return `<div class="warow ${on}" data-listpick="${esc(w.id)}" title="${esc(w.label || fullAddr)}">
<span class="wcell">${logoSvg(meta.logo, 14)}</span>
feat(aegis): 0.21.0 — you pick which wallet pays and which one dapps get A WizardConnect pairing could land on an address the user had never seen. Pairing took the selected wallet when it qualified and otherwise the first pairable one in list order, so with the selected wallet ineligible (a WIF import has no xpub) it silently fell through to whichever wallet happened to be first. The approval named that wallet by label only, which does not help when the label is one Aegis generated. Three changes, one idea: the wallet a purpose uses should be something you said, not something that fell out of list order. - Roles. A wallet can be nominated for payments and/or for WizardConnect, set from its manage modal and badged on its row. A role that points at a removed wallet reads back as null instead of being trusted, so a stale pointer can never quietly redirect a payment. - The pairing approval asks. With more than one candidate it offers a dropdown of them, each labelled with its own short address; with only one it shows that wallet's full address. The rows are static, so naming a wallet above a select the user can change would contradict itself — hence one or the other, never both. The panel's own Connect pane now follows the same precedence, because two rules for "which wallet" is how a pairing surprises someone. - Send gets a From row listing the wallets on this coin and network, with balances. It switches the panel selection rather than carrying a separate source: planSend and send resolve the wallet host-side from that, and a second notion of "current" would let the form and the approval disagree. Payments also becomes the opening selection when nothing has been picked yet, which is what nominating it is for. No Theseus release needed — approvalModal has supported a select row all along, and the pick comes back as "allow+wallet=<id>", validated against the options offered.
2026-10-01 00:59:17 +02:00
${badges
? `<span style="display:flex;align-items:center;gap:4px;min-width:0">${labelCell}${badges}</span>`
: labelCell}
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">
<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>
<!-- One way out, not two. "← Back" and this ✕ ran the same handler and
carried the same tooltip, so the row spent its left edge on a
duplicate of the control at its right edge. -->
<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("#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 = "…";
// Say what happened. A tooltip-only result is indistinguishable from the
// button doing nothing, which is how this one came to be reported as
// broken even when it worked: balances that were already current changed
// nothing on screen.
let mark = null;
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 on ${failed.length} of ${r.results.length}: ` + failed.map((f) => f.error).join("; ");
mark = "⚠";
} else {
refreshBtn.title = `Refreshed ${r.results.length} wallet${r.results.length === 1 ? "" : "s"}`;
mark = "✓";
}
} catch (e) {
refreshBtn.title = "Refresh failed: " + cleanErr(e);
mark = "⚠";
} finally {
delete refreshBtn.dataset.spinning;
if (mark) {
refreshBtn.textContent = mark;
setTimeout(() => { if (refreshBtn.isConnected) refreshBtn.textContent = prev; }, 1200);
} else refreshBtn.textContent = prev;
}
});
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;
// Record it as this network's current wallet, so switching away on the
// header's network chips and back returns here rather than to whichever
// wallet happens to be first.
const picked = (state?.wallets || []).find((w) => w.id === id);
if (picked) activeWalletBySubgroup.set(`${picked.chain}:${picked.network}`, id);
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";
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
if (body.dataset.mode === "setup") return;
body.dataset.mode = "setup";
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) {
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
// render() runs on every state push — balance polls fire it every couple
// of seconds — and this used to rebuild body.innerHTML each time, wiping
// the pad DOM and its digit buffer out from under someone mid-entry.
// That is the "resets after two digits" report: the reset is timed to the
// poll, not to the keypress count. Rebuild only when the mode changes.
if (body.dataset.mode === "pin") return;
body.dataset.mode = "pin";
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({
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
dots: body.querySelector("#lockPinDots"),
keys: body.querySelector("#lockPinKeys"),
err: body.querySelector("#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 r = await pinTry(pin);
if (!r.ok) {
const left = Math.max(0, r.remaining);
$("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";
}
state = await S.invoke("vaultUnlock", { masterPassword: r.masterPassword });
render();
return "ok";
} catch (e) {
$("lockPinErr").textContent = cleanErr(e);
return "reset";
}
},
});
$("lockUsePw").addEventListener("click", () => { body.dataset.forcePw = "1"; renderLockScreen("locked"); });
if ($("lockGoSettingsFromPin")) $("lockGoSettingsFromPin").addEventListener("click", () => showTab("settings"));
return;
}
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
// Master-password entry. Guarded for the same reason as the pad above:
// rebuilding on every state push erased a half-typed password.
if (body.dataset.mode === "pw") return;
body.dataset.mode = "pw";
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);
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
// Empty the field and let the form be rebuilt next time. It stayed
// filled behind the unlocked wallet, so after an idle lock or Sign out
// the password was sitting in the box for anyone to press Unlock.
try { $("gateUnlockPw").value = ""; } catch {}
body.dataset.mode = "";
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.
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
// IMPORTANT: pass `dots`/`keys`/`err` as elements scoped to the pad's own
// container, never via getElementById. Each pad's markup hard-codes its ids,
// so two pads alive at once (easy: a load-time gate plus a transaction gate)
// are duplicate ids — a global lookup then returns the FIRST one, and this
// function binds a second click handler to the wrong pad's buttons. The
// symptom is a pad that appends two digits per press and resets after three.
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);
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
// Always forget the digits. A pad that kept its six after "ok" was
// dead on the next lock and held the PIN in memory until then.
buf = ""; paint();
}
}
}));
// Keyboard fallback — some users prefer typing 6 digits fast.
const keyHandler = async (e) => {
if (!dots.isConnected) { document.removeEventListener("keydown", keyHandler); return; }
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
// Only while this pad is actually on screen, and never for keys typed
// into a field. The lock-screen pad stays in the DOM (hidden) after
// unlock, so six digits typed into the amount box counted as a PIN
// attempt — and five such amounts locked the PIN for 15 minutes.
if (dots.offsetParent === null) return;
// Two pads can be visible at once (a dapp's PIN request over a send's
// pad): only the topmost one listens, or six digits would complete both
// and a wrong PIN would cost two attempts.
const topModal = [...document.querySelectorAll(".pinmodal")].pop();
if (topModal && !topModal.contains(dots)) return;
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
const tgt = e.target;
if (tgt && (tgt.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(tgt.tagName || ""))) 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);
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
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;
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
noteSelectedWallet();
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";
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
// An owed PIN hides everything, Settings included: unlike the vault lock
// (where Settings must stay reachable to configure a PIN in the first
// place), the PIN is already configured here and letting Settings through
// would be a way around the door.
if (pinGateBlocked) { paintPinDoor(); 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
$("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.25.0: the network name moved out of this row into the selector chips
// under the balance. What is left here is connection state, which only
// earns attention when it is bad.
$("hNetRow").hidden = !s?.meta;
paintNetSelector(s);
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;
feat(aegis): 0.21.0 — you pick which wallet pays and which one dapps get A WizardConnect pairing could land on an address the user had never seen. Pairing took the selected wallet when it qualified and otherwise the first pairable one in list order, so with the selected wallet ineligible (a WIF import has no xpub) it silently fell through to whichever wallet happened to be first. The approval named that wallet by label only, which does not help when the label is one Aegis generated. Three changes, one idea: the wallet a purpose uses should be something you said, not something that fell out of list order. - Roles. A wallet can be nominated for payments and/or for WizardConnect, set from its manage modal and badged on its row. A role that points at a removed wallet reads back as null instead of being trusted, so a stale pointer can never quietly redirect a payment. - The pairing approval asks. With more than one candidate it offers a dropdown of them, each labelled with its own short address; with only one it shows that wallet's full address. The rows are static, so naming a wallet above a select the user can change would contradict itself — hence one or the other, never both. The panel's own Connect pane now follows the same precedence, because two rules for "which wallet" is how a pairing surprises someone. - Send gets a From row listing the wallets on this coin and network, with balances. It switches the panel selection rather than carrying a separate source: planSend and send resolve the wallet host-side from that, and a second notion of "current" would let the form and the approval disagree. Payments also becomes the opening selection when nothing has been picked yet, which is what nominating it is for. No Theseus release needed — approvalModal has supported a select row all along, and the pick comes back as "allow+wallet=<id>", validated against the options offered.
2026-10-01 00:59:17 +02:00
paintSendFrom(cur, others);
// 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 = "";
}
}
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
// Certificates chip. Counts individual certificates, not categories — "3"
// should mean three things you hold, which is the question the chip is
// answering. A chain with no non-fungible concept lands here with 0 and says
// so, rather than the chip vanishing on some coins and not others.
function setCertsCount(n) {
const chip = $("rcvNftsCount");
if (chip) chip.textContent = n ? String(n) : "";
const card = $("nftsCard");
if (card) card.hidden = false;
if (!n) {
const el = $("nftsList");
if (el) el.innerHTML = `<div class="hint" style="padding:4px 2px">No certificates held by this wallet.</div>`;
const cnt = $("nftsCount");
if (cnt) cnt.textContent = "";
}
}
// One row per certificate: which category it belongs to, its commitment (the
// payload that makes it specific), and its capability — minting / mutable /
// none is the difference between a certificate that can still issue others
// and one that is fixed, which is worth seeing at a glance.
function renderCerts(balances, metaMap) {
const rows = [];
for (const cat of Object.keys(balances || {})) {
const b = balances[cat];
const list = Array.isArray(b?.nfts) ? b.nfts : [];
for (const n of list) rows.push({ cat, ...n });
}
setCertsCount(rows.length);
if (!rows.length) return;
const el = $("nftsList");
if (!el) return;
const cntEl = $("nftsCount");
if (cntEl) cntEl.textContent = `· ${rows.length}`;
el.innerHTML = rows.map((r) => {
const meta = metaMap?.[r.cat] || null;
const named = meta?.name || meta?.symbol || null;
const title = named || (r.cat.slice(0, 10) + "…" + r.cat.slice(-6));
fix(aegis): 0.20.0 — the UTXO scan was 28k requests that always found nothing Went looking for a performance fix and found a correctness bug underneath it. getTx() slims each transaction on the way into the cache, and the slim shape kept only { value, scriptHex } — dropping vout.tokenData. That field is where the server reports CashTokens. It is NOT in scriptPubKey.hex, which Fulcrum returns with the token prefix already stripped. So the classify pass fetched one transaction per UTXO, looked for a prefix that was never there, and concluded "no token" every single time. Measured against a real chipnet wallet (130 UTXOs, 91 of them token-bearing): the old pass cost 54 requests for that address and found 0 tokens; sampling 12 of those transactions, exactly 0 had a scriptPubKey starting with the token prefix. On the faucet-fed address with 28,289 UTXOs it was ~28k requests, still finding nothing — which is what made the wallet look hung. So HD BCH wallets have never shown CashTokens. 0.15.0 fixed the imported adapter, which reads token_data off listunspent, and I took the HD path's silence for an empty wallet. Now: when the server negotiated protocol >= 1.5, listunspent carries token_data and a UTXO WITHOUT it is definitively not token-bearing, so the whole set is classified from the one call we already make — 1 request instead of 28,289. Below 1.5 the fallback fetches parents as before but reads vout.tokenData (present even at 1.4) and only decodes a prefix as a last resort, so it is correct now too. Both routes are reconciled onto one shape. They speak different dialects: the decoder yields a numeric capability (0/1/2) labelled immutable/mutable/minting, while Electrum sends a string and calls 0 "none". Left alone, an identical UTXO would have described itself differently depending on which server answered. The decoder's vocabulary wins, and the Certificates pane treats immutable as the quiet default so only capabilities that change what the holder can do get a tag. txCache is versioned and dropped once: entries written by the old shape carry no token information, and an absent field cannot be told apart from "no token", so a warm cache on a pre-1.5 server would have reported a token wallet as empty. Verified against the live wallet: one request, 91 token UTXOs, 46 categories, 17 assets, 51 certificates — matching what the panel reports — with capability labels normalised (5 immutable, 28 mutable, 18 minting).
2026-09-29 00:56:06 +02:00
// "immutable" is the quiet default — it describes most certificates and
// tagging every row with it would say nothing. Only the capabilities
// that change what the holder can DO get a tag. ("none" is Electrum's
// word for the same thing; lib/wallet.js normalises it to immutable, but
// accept both so an older cached snapshot still reads correctly.)
const cap = String(r.capabilityLabel || "").toLowerCase();
const capTag = cap && cap !== "none" && cap !== "immutable"
? ` <span class="ttag" title="This certificate can ${cap === "minting" ? "issue more of its category" : "be altered by its holder"}">${esc(cap.toUpperCase())}</span>`
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
: "";
// A commitment is arbitrary bytes; it is the only thing distinguishing
// two certificates of the same category, so show it rather than hide it.
const commit = String(r.commitmentHex || "");
const commitTxt = commit
? (commit.length > 24 ? commit.slice(0, 24) + "…" : commit)
: "no commitment";
return `<div class="tx" style="grid-template-columns:1fr auto;cursor:default;align-items:center">
<div>
<div>${esc(title)}${capTag}</div>
<div class="hint mono">${esc(commitTxt)}</div>
</div>
<div class="hint mono" style="align-self:center;text-align:right">${named ? esc(r.cat.slice(0, 8)) + "…" : ""}</div>
</div>`;
}).join("");
}
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
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");
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
// Certificates are BCH-only today; every other chain lands on 0 and says
// so. Overwritten below for BCH.
if (chain() !== "bch") setCertsCount(0);
// 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 || {};
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
const allCats = Object.keys(balances);
// Assets = categories carrying a fungible balance. Certificates live in
// their own pane, so a category that is purely non-fungible does not
// take up a row here saying "1 NFT".
const cats = allCats.filter((c) => String(balances[c]?.fungible || "0") !== "0");
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");
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
if (hintEl) hintEl.textContent = "Fungible CashTokens held by this wallet. Names come from BCMR — configure custom registries in Settings.";
const draw = (metaMap) => {
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
// setAssetsCount already wrote the "no assets" line when there are no
// fungible categories; joining an empty list would wipe it. Common
// case now that purely-non-fungible categories live in the other pane.
if (!cats.length) { renderCerts(balances, metaMap); return; }
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";
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
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 : "") : "";
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
const amountLine = fungibleTxt || "—";
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);
}));
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
renderCerts(balances, metaMap);
};
// 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({});
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
if (!allCats.length) return;
S.invoke("tokenMetadata", { categories: allCats }).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(aegis): 0.21.0 — you pick which wallet pays and which one dapps get A WizardConnect pairing could land on an address the user had never seen. Pairing took the selected wallet when it qualified and otherwise the first pairable one in list order, so with the selected wallet ineligible (a WIF import has no xpub) it silently fell through to whichever wallet happened to be first. The approval named that wallet by label only, which does not help when the label is one Aegis generated. Three changes, one idea: the wallet a purpose uses should be something you said, not something that fell out of list order. - Roles. A wallet can be nominated for payments and/or for WizardConnect, set from its manage modal and badged on its row. A role that points at a removed wallet reads back as null instead of being trusted, so a stale pointer can never quietly redirect a payment. - The pairing approval asks. With more than one candidate it offers a dropdown of them, each labelled with its own short address; with only one it shows that wallet's full address. The rows are static, so naming a wallet above a select the user can change would contradict itself — hence one or the other, never both. The panel's own Connect pane now follows the same precedence, because two rules for "which wallet" is how a pairing surprises someone. - Send gets a From row listing the wallets on this coin and network, with balances. It switches the panel selection rather than carrying a separate source: planSend and send resolve the wallet host-side from that, and a second notion of "current" would let the form and the approval disagree. Payments also becomes the opening selection when nothing has been picked yet, which is what nominating it is for. No Theseus release needed — approvalModal has supported a select row all along, and the pick comes back as "allow+wallet=<id>", validated against the options offered.
2026-10-01 00:59:17 +02:00
// ---- Send: which address the money leaves from -----------------------------
// Aegis models one address per wallet entry, so "send from" is the same
// question as "which of this coin's wallets". Picking one switches the panel
// selection rather than carrying a separate source: planSend and send resolve
// the wallet host-side from the selection, and a second notion of "current"
// would be two answers to one question, with the approval dialog free to
// disagree with the form.
function paintSendFrom(cur, others) {
const field = $("sendFromField"), box = $("sendFrom"), hint = $("sendFromHint");
if (!field || !box) return;
if (!cur || !others.length) { field.hidden = true; return; }
// List order, not "current first" — the order matches the wallet list the
// user already knows, so the same address sits in the same place each time.
const mine = (state?.wallets || []).filter((w) => w.chain === cur.chain && w.network === cur.network);
const roles = (state && state.roles) || {};
// Rebuild only when the options or balances actually change, so a re-render
// mid-typing doesn't reset a select under the cursor.
const key = mine.map((w) => `${w.id}:${walletBalanceUnits(w) || 0}:${roles.payments === w.id ? "p" : ""}`).join("|");
if (box.dataset.key !== key) {
box.dataset.key = key;
box.innerHTML = mine.map((w) => {
const bal = fmtBig(walletBalanceUnits(w) || 0, w.decimals);
const a = stripAddrPrefix(String(w.address || ""));
const tail = a ? " · " + (a.length > 16 ? a.slice(0, 8) + "…" + a.slice(-5) : a) : "";
const star = roles.payments === w.id ? " ★" : "";
return `<option value="${esc(w.id)}">${esc((w.label || "wallet") + tail)} — ${esc(bal)} ${esc(w.ticker)}${star}</option>`;
}).join("");
box.onchange = async () => {
const id = box.value;
if (!id || id === state.selectedWalletId) return;
try {
state = await S.invoke("selectWallet", { id });
settingsFilled = false;
// A different source means the costed plan is about other UTXOs.
lastPlan = null;
render();
schedulePlan();
} catch (e) {
const m = $("sendMsg"); m.className = "msg err"; m.textContent = cleanErr(e); m.hidden = false;
}
};
}
box.value = cur.id;
const payW = mine.find((w) => roles.payments === w.id);
if (hint) {
hint.textContent = payW
? (payW.id === cur.id ? "Your default wallet for payments." : `★ ${payW.label} is your default for payments.`)
: "Pick a default for payments in a wallet's ⋯ menu.";
}
field.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
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();
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
// Exact conversion in the decimals of what is being sent. fmtBig() caps at
// 8 places and always used the chain's decimals, so 1 wei became "0" and a
// 6-decimal token amount shrank 1000× on a round trip.
if (s) $("sendAmt").value = unit === "big" ? unitsToDecimalText(String(s), amountDecimals()) : 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
}
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
function amountDecimals() { return sendAsset ? Number(sendAsset.decimals) || 0 : decimals(); }
// <amount-parser> — pure functions, exercised by the sandbox harness.
// What the user typed → { whole, frac } digit strings. A wallet must never
// guess: the old `.replace(/,/g, "")` turned a decimal comma ("0,5") into 5,
// and Number() accepted "1e3", "0x10" and "-0.5".
function parseAmountText(text) {
let raw = String(text == null ? "" : text).trim();
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
if (!raw) return { empty: true };
// Space inside the number is refused, not deleted: "1 0" used to become 10.
if (/\s/.test(raw)) return { error: "Remove the space from the amount" };
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
if (/^\d{1,3}(,\d{3})+\.\d*$/.test(raw) || /^\d{1,3}(,\d{3}){2,}$/.test(raw)) {
raw = raw.replace(/,/g, ""); // 1,234.5 · 1,234,567
} else if (/^\d{1,3},\d{3}$/.test(raw)) {
return { error: `"${raw}" could mean ${raw.replace(",", "")} or ${raw.replace(",", ".")} — write it without the comma, or with a dot` };
} else if (/^\d*,\d+$/.test(raw)) {
raw = raw.replace(",", "."); // decimal comma: 0,5
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
}
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
if (!/^(\d+\.?\d*|\.\d+)$/.test(raw)) return { error: "Enter a plain number, like 0.5" };
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(".");
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
return { whole: (w || "0").replace(/^0+(?=\d)/, ""), frac: f };
}
// → smallest units as a decimal string, or { error }. `unit` is "big" (coins)
// or "small" (sats / wei / raw token units).
function amountToUnits(text, decimalsN, unit) {
const p = parseAmountText(text);
if (p.empty) return { units: "0", empty: true };
if (p.error) return { error: p.error };
const d = Math.max(0, Math.floor(Number(decimalsN) || 0));
if (unit === "small") {
if (/[1-9]/.test(p.frac)) return { error: "The smallest unit cannot have decimals" };
return { units: BigInt(p.whole).toString() };
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
}
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
if (/[1-9]/.test(p.frac.slice(d))) return { error: `At most ${d} decimal place${d === 1 ? "" : "s"} here` };
const frac = (p.frac + "0".repeat(d)).slice(0, d);
return { units: (BigInt(p.whole) * (10n ** BigInt(d)) + BigInt(frac || "0")).toString() };
}
// Exact inverse for filling the field: units → "1.5" with no grouping.
function unitsToDecimalText(units, decimalsN) {
const d = Math.max(0, Math.floor(Number(decimalsN) || 0));
const s = String(units).replace(/^0+(?=\d)/, "");
if (!/^\d+$/.test(s)) return "";
if (d === 0) return s;
const pad = s.padStart(d + 1, "0");
const frac = pad.slice(pad.length - d).replace(/0+$/, "");
return pad.slice(0, pad.length - d) + (frac ? "." + frac : "");
}
// </amount-parser>
// Why the amount in the field is not usable, or null. Set by amountUnits().
let amountError = null;
function amountUnits() {
amountError = null;
const d = amountDecimals();
const r = amountToUnits($("sendAmt").value, d, unit);
if (r.error) { amountError = r.error; return 0; }
if (r.empty) return 0;
// SPL tokens and 18/24-decimal coins travel as decimal strings; the 8–9
// decimal chains keep their Number contract with the host, bounded so a
// value past 2^53 is refused instead of silently rounded.
const bigDecimals = sendAsset != null || d > 15;
if (bigDecimals) return r.units;
if (BigInt(r.units) > BigInt(Number.MAX_SAFE_INTEGER)) { amountError = "That amount is too large"; return 0; }
return Number(r.units);
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): 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", () => {
Aegis: fees that follow the network, connections that come back, its own name on Solana Still 0.31.0 (unpublished batch). - ETH nonce. The pending count from a load-balanced RPC often misses a transaction this wallet sent seconds ago, so two sends in a row shared a nonce and the second failed or replaced the first. The nonce is taken at signing, from the RPC or from what the wallet itself last broadcast, whichever is higher, and broadcasts are serialised per wallet. - Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155 transaction; they rejected the type-2 envelope, so a network added by a dapp could receive but never send. The tip is clamped to the fee cap. - Bitcoin and DigiByte take their fee rate from the Electrum server's estimate instead of a constant, size each output from its real script (a taproot destination was undercounted), and round the size up before pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20 sat/vB floor and does not signal RBF, which it does not have. - Electrum: a wallet with live subscriptions went quiet for good when its server dropped. The client reconnects with backoff, pings to catch dead sockets, times out a silent connect, and hands the replayed subscription answers on as notifications so the wallet refreshes. dispose() ends it. - Max with an SPL token selected did nothing; it now fills the exact token balance. - Removing a wallet retired its derivation index for good. Add wallet now takes the lowest free index, so the same wallet comes back. - Solana: registered through the Wallet Standard as "Aegis" instead of setting isPhantom, with silent connect for already-connected sites. - "Only show the wallet to sites I enable": Aegis keeps the host's page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
// A token has no "max mode": its fee is paid in the coin, so Max is just
// the whole token balance, written into the field exactly. The toggle
// below did nothing for tokens — the plan never received it, and Send
// went out with whatever sat in the disabled field.
if (sendAsset) {
const t = (sel()?.tokens || []).find((x) => x.mint === sendAsset.mint);
sendMax = false;
$("sendMax").classList.remove("primary"); $("sendAmt").disabled = false;
if (t) $("sendAmt").value = unit === "big" ? unitsToDecimalText(String(t.balance), amountDecimals()) : String(t.balance);
updateSendFiatPreview();
schedulePlan();
return;
}
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", () => {
feat(aegis): 0.19.0 — three chips: Address, Assets, Certificates Assets was one list holding two different kinds of thing. A fungible balance answers "how much do I have"; a non-fungible one answers "which ones do I hold", and they want different rows — an amount against a symbol, versus a commitment and a capability. On a real chipnet wallet that meant 46 rows where 32 of them were only ever going to say "1 NFT". Split by what each category actually holds, so a category carrying both appears in both — which is honest, because it really does hold both. Assets counts categories; Certificates counts individual certificates, since "3" should mean three things you hold rather than three groups. Certificate rows carry the commitment, because it is the only thing that distinguishes two certificates of the same category, and a MINTING or MUTABLE tag, because "can still issue others" versus "is fixed" is worth seeing without opening anything. A `none` capability is left unlabelled rather than adding noise to every row. "Certificates", not "NFTs": these are membership, licence and record tokens, and NFT carries collectible-market baggage that misdescribes them. Ids and data keys stay `nft` — that is the CashTokens protocol field, so it is protocol name inside and product name on screen. Two things caught while building it. `draw({})` was overwriting the "no assets" empty state with an empty string, which is now the common case since purely-non-fungible categories no longer appear there. And certificate rows printed the category twice, as the title and again on the right — the right column now only carries it when the row has a real name to lead with. Metadata is looked up for every category, not just the fungible ones, so naming a token also names its certificates.
2026-09-29 00:40:46 +02:00
rcvView = RCV_VIEWS.includes(b.dataset.rcvView) ? b.dataset.rcvView : "address";
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
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();
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
if (!units || amountError || !state?.prices?.enabled) { el.hidden = true; return; }
const usd = usdOf(chain(), units, decimals());
const txt = fmtFiat(usd);
el.textContent = txt ? "≈ " + txt : "";
el.hidden = !txt;
}
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
function schedulePlan() {
// The summary on screen is stale from the first keystroke: Send stays off
// until a plan for exactly what is in the form has come back.
lastPlan = null; lastPlanReq = null;
const b = $("sendBtn"); if (b) b.disabled = true;
clearTimeout(planTimer); planTimer = setTimeout(updatePlan, 250);
}
// The request a plan was made for. Send submits THIS, never a fresh read of
// the form, so what is signed is what the summary showed.
let lastPlanReq = null;
let planSeq = 0;
function currentSendReq() {
const amount = amountUnits();
const base = { walletId: state?.selectedWalletId || null, to: $("sendTo").value.trim() };
if (sendAsset) return { ...base, mint: sendAsset.mint, amount };
return {
...base, amount,
feeRate: chain() === "bch" ? Number($("feeRate").value) : undefined,
sendMax,
memo: chain() === "bch" ? String($("sendMemo")?.value || "").trim() : "",
};
}
// Selecting another wallet invalidates everything the Send form worked out.
// The fields used to survive the switch: the button stayed live on the old
// wallet's plan, Max swept the new wallet under the old summary, and a
// number typed in sats was re-read as wei.
let sendFormWalletId = null;
function noteSelectedWallet() {
const id = state?.selectedWalletId || null;
if (id === sendFormWalletId) return;
const prev = (state?.wallets || []).find((w) => w.id === sendFormWalletId);
const cur = (state?.wallets || []).find((w) => w.id === id);
const first = sendFormWalletId === null;
sendFormWalletId = id;
if (first) return;
lastPlan = null; lastPlanReq = null; planSeq++;
clearTimeout(planTimer);
sendAsset = null;
if (sendMax) {
sendMax = false;
try { $("sendMax").classList.remove("primary"); $("sendAmt").disabled = false; $("sendAmt").value = ""; } catch {}
}
// Another coin means the address and the number mean something else.
if (!prev || !cur || prev.chain !== cur.chain || prev.network !== cur.network) {
try { $("sendTo").value = ""; $("sendAmt").value = ""; if ($("sendMemo")) $("sendMemo").value = ""; } catch {}
unit = "big";
try { applyUnitPicker(); } catch {}
}
try {
$("sendBtn").disabled = true;
$("sumAmt").textContent = $("sumFee").textContent = $("sumTotal").textContent = "—";
$("sendMsg").hidden = true;
} catch {}
if ($("sendTo")?.value && $("sendAmt")?.value) schedulePlan();
}
async function updatePlan() {
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
const seq = ++planSeq;
const to = $("sendTo").value.trim();
const msg = $("sendMsg"); msg.hidden = true;
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
lastPlan = null; lastPlanReq = null; $("sendBtn").disabled = true;
$("sumAmt").textContent = $("sumFee").textContent = $("sumTotal").textContent = "—";
$("sendToHint").textContent = "";
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
const req = currentSendReq();
if (amountError && !sendMax) { msg.className = "msg err"; msg.textContent = amountError; msg.hidden = false; return; }
if (!to || (!sendMax && !req.amount)) 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.
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
const p = await S.invoke("planTokenSend", { mint: req.mint, to, amount: req.amount });
if (seq !== planSeq) return; // a newer plan is on its way
lastPlan = { _token: true, ...p }; lastPlanReq = req;
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
$("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;
}
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
const p = await S.invoke("planSend", { to, amount: req.amount, feeRate: req.feeRate, sendMax: req.sendMax, memo: req.memo });
if (seq !== planSeq) return; // a newer plan is on its way
lastPlan = p; lastPlanReq = req;
$("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();
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
if (sendMax) $("sendAmt").value = unit === "big" ? unitsToDecimalText(String(p.recipients[0].value), amountDecimals()) : String(p.recipients[0].value);
$("sendBtn").disabled = false;
} catch (e) {
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
if (seq !== planSeq) return;
msg.className = "msg err"; msg.textContent = cleanErr(e); msg.hidden = false;
}
}
$("sendBtn").addEventListener("click", async () => {
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
if (!lastPlan || !lastPlanReq) return;
const msg = $("sendMsg"); msg.hidden = true;
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
// The form must still say what the summary was computed for. Max rewrites
// the amount field itself, so it is compared without the amount.
const now = currentSendReq();
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
const strip = (r) => (r.sendMax ? { ...r, amount: null } : r);
if (!same(strip(now), strip(lastPlanReq))) {
await updatePlan();
if (lastPlan) { msg.className = "msg err"; msg.textContent = "The form changed — check the updated summary, then press Send again."; msg.hidden = false; }
return;
}
const req = lastPlanReq;
// PIN gate. Whether a transaction needs one is the policy's call, not a
// single flag's — and if the user only asked for a PIN at startup, this
// check passes without a prompt.
if (!await pinGate("transaction", "Confirm this send with your PIN.")) {
msg.className = "msg err"; msg.textContent = "Cancelled — PIN not confirmed."; msg.hidden = false; return;
}
$("sendBtn").disabled = true; $("sendBtn").textContent = "Waiting for approval…";
try {
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
const isToken = !!(req.mint && lastPlan._token);
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", { walletId: req.walletId, mint: req.mint, to: req.to, amount: req.amount })
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
: await S.invoke("send", { walletId: req.walletId, to: req.to, amount: req.amount, feeRate: req.feeRate, sendMax: req.sendMax, memo: req.memo });
// A broadcast that returned no txid is still a broadcast: never report
// it as a failure and leave a filled form inviting a second send.
const txid = r && typeof r.txid === "string" ? r.txid : "";
msg.className = "msg ok";
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
if (txid) {
msg.innerHTML = `Sent. <a class="link" data-tx="${esc(txid)}">${esc(txid.slice(0, 16))}…</a>`;
msg.querySelector("a").addEventListener("click", () => openUrl(explorerHref(sel().explorerTx, txid)));
} else {
msg.textContent = "Sent. The network did not return a transaction id — check History before sending again.";
}
msg.hidden = false;
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
lastPlanReq = 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
$("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");
if (set) set.hidden = hasPin;
if (chg) chg.hidden = !hasPin;
if (rm) rm.hidden = !hasPin;
// Say what the PIN really protects against on this machine.
const shared = securityState.pinUnified ? " It is the Theseus PIN: the same PIN unlocks Settings › Passwords and Pithos." : "";
if (hint) hint.textContent = (hasPin
? (securityState.pinHardware === "tpm"
? "On — a 6-digit PIN unlocks Aegis. It is tied to this computer's security chip (TPM), which allows only a few wrong guesses an hour, even to malware or a copied disk."
: "On — a 6-digit PIN unlocks Aegis. This computer has no usable security chip, so the PIN only stops casual use: anything that runs as your Windows account, or a copy of this disk with your Windows password, can find the PIN in minutes and with it your master password.")
: (securityState.pinStorable === false
? "Off — this system has no protected keystore, so Aegis cannot store a PIN safely and asks for the master password every time."
: "Off — Aegis asks for the master password every time.")) + (hasPin ? shared : "");
if (set) set.disabled = !hasPin && securityState.pinStorable === false;
// A PIN stored by Aegis before 0.31 sat on disk without OS protection.
// Copies of the profile made back then still hold the master password
// behind just the PIN, so the only real fix is a new master password.
let exp = $("pinExposureNote");
if (securityState.pinLegacyExposure) {
if (!exp && hint) {
exp = document.createElement("div");
exp.id = "pinExposureNote";
exp.className = "ghint pinwarn";
hint.after(exp);
}
if (exp) {
exp.textContent = "An earlier Aegis kept your PIN on disk without OS protection. If this Theseus profile was ever backed up, synced or copied, change your Theseus master password; changing the PIN alone does not help. ";
const dismiss = document.createElement("a");
dismiss.textContent = "Dismiss";
dismiss.addEventListener("click", async () => {
try { await S.invoke("pinExposureDismiss"); } catch {}
await refreshSecurityState();
renderGeneralSecurity();
});
exp.appendChild(dismiss);
}
} else if (exp) exp.remove();
// "Ask for PIN" triggers. Meaningless without a PIN to ask for.
const onLine = $("gsPinOnLine");
if (onLine) onLine.hidden = !hasPin;
const pol = securityState.pinOn || {};
for (const [id, key] of PIN_ON_FIELDS) {
const box = $(id);
if (box) box.checked = !!pol[key];
}
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
// Both PIN policies are meaningless without a PIN to use.
const revLine = $("gsRequirePinRevealLine"), rpr = $("gsRequirePinReveal");
if (revLine) revLine.hidden = !hasPin;
if (rpr) rpr.checked = !!securityState.requirePinForReveal;
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. The master password is stored in the OS keystore under this user only, so anything that runs as your Windows account can read it; the PIN's security-chip protection does not cover it.";
}
}
}
// 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({
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
// Scoped to this overlay, not looked up by global id — see the note
// on setupPinPad about duplicate ids across simultaneous pads.
dots: wrap.querySelector("#pmDots"), keys: wrap.querySelector("#pmKeys"), err: wrap.querySelector("#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: securityState.pinUnified
? "This removes the Theseus PIN everywhere — Aegis, Settings › Passwords and Pithos. You'll have to type the master password on every unlock again."
: "You'll have to type the master password on every unlock again.",
});
if (!ok) return;
try {
await S.invoke("pinBlobClear");
// Every trigger depends on there being a PIN, so clear them all rather
// than leaving a policy armed against a credential that no longer exists.
await S.invoke("securitySet", { pinOn: { restart: false, launch: false, interval: false, transaction: false } });
await refreshSecurityState();
renderGeneralSecurity();
} catch (e) { aegisAlert("Could not remove PIN: " + cleanErr(e)); }
});
// Checkbox id → policy key, used by both the paint and the wiring below.
const PIN_ON_FIELDS = [
["gsPinOnRestart", "restart"],
["gsPinOnLaunch", "launch"],
["gsPinOnInterval", "interval"],
["gsPinOnTransaction", "transaction"],
];
for (const [id, key] of PIN_ON_FIELDS) {
const box = $(id);
if (!box) continue;
box.addEventListener("change", async () => {
const want = box.checked;
try {
securityState = await S.invoke("securitySet", { pinOn: { [key]: want } });
// Ticking a trigger should not fire it retroactively: the user is
// sitting in Settings having just proved whatever got them here.
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
// The host only records a gate against proof it verified, so this
// works when the password is still at hand (just unlocked); otherwise
// the new trigger simply asks once at its next occasion.
if (want && window.__aegisLastPw) { try { await S.invoke("pinGateSatisfied", { masterPassword: window.__aegisLastPw }); } catch {} }
renderGeneralSecurity();
} catch (e) {
box.checked = !want;
aegisAlert("Could not save setting: " + cleanErr(e));
}
});
}
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
$("gsRequirePinReveal") && $("gsRequirePinReveal").addEventListener("change", async () => {
const on = $("gsRequirePinReveal").checked;
try {
securityState = await S.invoke("securitySet", { requirePinForReveal: on });
renderGeneralSecurity();
} catch (e) {
$("gsRequirePinReveal").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 {
await S.invoke("pinSet", { pin, masterPassword: masterPw });
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 {}
});
}
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
// What each secret form actually IS, and where it can actually be restored.
// This copy matters more than it looks: none of these are a BIP39 recovery
// phrase, because Aegis never stores one. An import converts the words to a
// seed and throws the words away; a vault wallet is HKDF(vault root, purpose)
// and never had words. Someone who writes down what we show here and believes
// it is a 12-word phrase has not backed anything up, so each form says what
// it is in its own name and the note says what to do with it.
const SECRET_FORMS = {
wif: {
title: "Private key (WIF)",
note: "This single key controls this one address and nothing else. Any Bitcoin Cash wallet that accepts a WIF key can import it.",
},
privhex: {
title: "Private key (hex)",
note: "This single key controls this one account. Most ETH / TRX / SOL wallets accept a raw hex private key.",
},
seed: {
title: "Wallet seed (hex)",
note: "There is no word list to show. You imported a recovery phrase, and Aegis converted it to this seed and discarded the words — that conversion is one-way, so the phrase cannot be recovered from here or from anywhere else in Aegis. Keep the original phrase wherever you first wrote it down. This seed plus the derivation path below is a complete backup of the wallet, and Aegis can take it back under Add → Import → Seed (hex).",
},
vault: {
title: "Wallet key (hex)",
note: "This wallet has no recovery phrase of its own — it is derived from your Theseus vault, so your real backup is the vault's master password. The key below restores this one wallet via Add → Import → Seed (hex), and the account xprv, where shown, is accepted by most other HD wallets.",
},
};
// Reveal one wallet's secret. Gated by authorizeForSecret, hidden until
// asked for a second time, and never left on screen after the modal closes.
async function openRevealSecretModal(w) {
// Two shapes reach here: a wallet summary from the manage modal, which
// keys the id as `id`, and state.selected from the Settings buttons, which
// keys it as `walletId`. Accept both rather than silently doing nothing.
const walletId = w && (w.id || w.walletId);
if (!walletId) return;
const pw = await authorizeForSecret(`Reveal the secret key for "${(w && w.label) || "this wallet"}".`);
if (!pw) return;
let r;
try { r = await S.invoke("revealSecret", { walletId, masterPassword: pw }); }
catch (e) { await aegisAlert(cleanErr(e), { title: "Could not reveal", icon: "⚠️" }); return; }
const form = SECRET_FORMS[r.form] || { title: "Secret", note: "" };
const overlay = document.createElement("div");
overlay.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:flex-start;justify-content:center;z-index:99999;padding-top:16px";
const field = (label, value, danger) => value
? `<div class="lbl" style="margin-top:8px">${esc(label)}</div>
<div class="mono" style="word-break:break-all;${danger ? "color:var(--danger)" : ""}">${esc(value)}</div>`
: "";
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;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">${esc(form.title)}</div>
<button class="btn sm" id="rvClose" type="button">✕</button>
</div>
<div class="hint" style="margin-bottom:10px">${esc(r.label || "")} · ${esc(r.coinLabel || "")} · ${esc(r.networkLabel || "")}</div>
<div class="msg err" style="margin-bottom:10px">Anyone who sees this can spend everything in this wallet. Nobody legitimate will ever ask you for it — not support, not Silent Mode.</div>
<div class="hint" style="margin-bottom:10px">${esc(form.note)}</div>
<div id="rvHidden">
<div class="actions"><button class="btn danger" id="rvShow" type="button">Show the key</button></div>
</div>
<div id="rvShown" hidden>
${field(form.title, r.secret, true)}
${field("Derivation path", r.path || r.accountPath, false)}
${field("Account private key (xprv)", r.xprv, true)}
${field("Account xpub (safe to share)", r.xpub, false)}
${field("Address", r.address, false)}
<div class="actions" style="margin-top:12px;gap:6px">
<button class="btn" id="rvCopy" type="button">Copy key</button>
<button class="btn" id="rvHide" type="button">Hide</button>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
// Wipe the rendered secret out of the DOM on the way out rather than
// relying on the node being dropped — the modal is the only place it
// exists in the renderer, so clearing it is cheap and exact.
const close = () => {
try { overlay.querySelector("#rvShown").innerHTML = ""; } catch {}
try { overlay.remove(); } catch {}
};
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
overlay.querySelector("#rvClose").addEventListener("click", close);
overlay.querySelector("#rvShow").addEventListener("click", () => {
overlay.querySelector("#rvHidden").hidden = true;
overlay.querySelector("#rvShown").hidden = false;
});
overlay.querySelector("#rvHide").addEventListener("click", close);
overlay.querySelector("#rvCopy").addEventListener("click", async (e) => {
try { await navigator.clipboard.writeText(r.secret); flash(e.currentTarget, "Copied"); }
catch { await aegisAlert("Could not reach the clipboard."); }
});
}
// Ask for the PIN if the configured policy says this event needs it. The
// host decides — the panel reloads whenever it is hidden and shown, so it
// cannot be what remembers that a gate was already cleared.
//
// Returns true to proceed, false if the user cancelled or failed. A passing
// check is recorded, which is what stops Aegis asking again until one of the
// user's triggers fires. Any failure to reach the host is treated as "ask",
// since the safe direction for a lock is closed.
async function pinGate(event, subtitle) {
let st;
try { st = await S.invoke("pinGateStatus", { event }); }
catch { st = { needPin: true, reason: "unknown" }; }
if (!st.needPin) return true;
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
// verifyPinInteractively resolves to what the PIN unwrapped — the vault
// master password — and the host checks that itself before it records the
// gate or lets a transaction through. A PIN the host could not verify
// does not count, so a failure here is a failed gate.
const proof = await verifyPinInteractively(subtitle || PIN_GATE_COPY[st.reason] || "Confirm with your PIN.");
if (!proof) return false;
// `event` says what the proof is for: "transaction" clears the panel's
// next send, anything else only records the gate.
try { await S.invoke("pinGateSatisfied", { masterPassword: proof, event }); }
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
catch { return false; }
return true;
}
// A dapp transaction is waiting on the PIN ("ask on every transaction").
// The host cannot draw a PIN pad, so it asks the panel: prove the PIN here
// and the pending transaction goes through.
// Requests are answered one at a time, each with its own PIN entry, and the
// proof names the request it was entered for — so a PIN typed for one site
// never releases another site's transaction.
let pinRequestBusy = false;
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
async function answerPinRequest(req) {
if (!req || !req.id || pinGateBlocked || pinRequestBusy) return;
pinRequestBusy = true;
try {
const where = String(req.origin || "A site").slice(0, 80);
const proof = await verifyPinInteractively(`${where} is waiting — confirm the ${String(req.what || "transaction").slice(0, 60)} with your PIN.`);
if (proof) { try { await S.invoke("pinGateSatisfied", { masterPassword: proof, requestId: req.id }); } catch { /* host keeps waiting, then refuses */ } }
} finally { pinRequestBusy = false; }
// Another site may be waiting too.
try { const next = await S.invoke("pinRequestPending"); if (next && next.id !== req.id) answerPinRequest(next); } catch {}
}
const PIN_GATE_COPY = {
restart: "Theseus restarted — confirm your PIN to use this wallet.",
launch: "Confirm your PIN to open the wallet.",
interval: "It has been a while — confirm your PIN to carry on.",
transaction: "Confirm this transaction with your PIN.",
};
// Gate the panel itself on load. Runs once per panel script load, which is
// also once per hide/show, so "on each wallet launch" is simply this check
// with that trigger enabled.
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
//
// This must finish BEFORE the first render(). Asking afterwards was the
// original complaint in a new costume: the wallet painted, balances and all,
// and the pad then appeared on top of a panel the user could already read.
// `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed,
// so a cancelled or failed prompt leaves a closed door rather than an open
// wallet.
let pinGateChecked = false;
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
let pinGateBlocked = false;
async function pinGateOnLoad() {
if (pinGateChecked) return;
pinGateChecked = true;
if (!securityLoaded) await refreshSecurityState();
if (!securityState.hasPin) return;
// Nothing to protect yet if the vault is locked — the master-password gate
// is already in the way, and stacking a PIN prompt on top of it is two
// locks on one door.
if (state && (state.overallPhase === "locked" || state.overallPhase === "nosetup")) return;
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
let st;
try { st = await S.invoke("pinGateStatus", { event: "panel-load" }); }
catch { st = { needPin: true, reason: "unknown" }; }
if (!st.needPin) return;
pinGateBlocked = true;
paintPinDoor(st.reason);
// Keep asking until it is satisfied. Cancelling does not open the wallet;
// it leaves the door shut with a button to try again, which is the only
// honest outcome for "a PIN is required here".
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
let proof = null;
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
for (;;) {
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
proof = await verifyPinInteractively(PIN_GATE_COPY[st.reason] || "Confirm with your PIN.");
if (proof) break;
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
const again = await aegisConfirm({
title: "PIN required",
icon: "🔒",
confirmLabel: "Enter PIN",
cancelLabel: "Leave locked",
body: "Aegis stays locked until the PIN is entered. You can also unlock with your master password from Settings.",
});
if (!again) return; // stays blocked; door remains shut
}
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
try { await S.invoke("pinGateSatisfied", { masterPassword: proof }); } catch { /* re-asks next load */ }
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
pinGateBlocked = false;
render();
}
// The closed door shown behind the pad, so there is never a moment where the
// wallet is readable but unauthenticated.
function paintPinDoor(reason) {
const g = $("gate");
if (!g) return;
document.querySelector("header").hidden = true;
document.querySelector("nav").hidden = true;
const strip = $("walletStrip"); if (strip) strip.hidden = true;
const ls = $("lockScreen"); if (ls) ls.hidden = true;
g.hidden = false;
g.innerHTML = `<div class="big">🔒</div><div>${esc(PIN_GATE_COPY[reason] || "Confirm with your PIN.")}</div>`;
}
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
// How many wrong PINs before a sensitive reveal stops asking for the PIN and
// asks for the master password instead. Lower than PIN_MAX_FAILS on purpose:
// someone fumbling their own PIN gets a way through that does not cost them a
// 15-minute lockout, and someone guessing is pushed onto the credential that
// is actually hard to guess. The global counter is NOT reset on the way
// across, so guesses still accumulate toward the lockout.
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
const REVEAL_PIN_MAX_FAILS = 3;
// Prove entitlement to see a secret, and hand back the master password —
// which is what the host verifies before it parts with anything. The PIN blob
// wraps that same password, so both routes end at the same proof and the host
// never has to take the panel's word for it.
//
// Returns the master password, or null if the user backed out.
async function authorizeForSecret(subtitle) {
if (!securityLoaded) await refreshSecurityState();
const usePin = securityState.requirePinForReveal && securityState.hasPin;
if (!usePin) {
// No PIN configured, or the user turned the PIN prompt off. Either way
// the master password is the gate — never nothing.
return promptMasterPassword({ title: "Confirm master password", subtitle });
}
const remain = await pinLockoutRemainingMs();
if (remain > 0) {
// Locked out of the PIN, but the password is a separate credential and
// the lockout exists to stop PIN guessing, not to lock the owner out.
return promptMasterPassword({
title: "Confirm master password",
subtitle: `PIN entry is locked for ${Math.ceil(remain / 60000)} min. ${subtitle || ""}`.trim(),
});
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
}
const pin = await capturePinForSecret(subtitle);
if (pin === null) return null; // cancelled
if (pin === "__fallback__") {
return promptMasterPassword({
title: "Confirm master password",
subtitle: `${REVEAL_PIN_MAX_FAILS} wrong PIN attempts. ${subtitle || ""}`.trim(),
});
}
return pin;
}
// PIN pad that resolves with the DECRYPTED MASTER PASSWORD on success, null
// on cancel, or "__fallback__" once the user has burned REVEAL_PIN_MAX_FAILS
// attempts. Separate from verifyPinInteractively because that one only
// answers yes/no and throws the password away.
function capturePinForSecret(subtitle) {
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="rsSub">${esc(subtitle || "")}</div>
<div class="pinpad">
<div class="pindots" id="rsDots">${"<span class=\"pindot\"></span>".repeat(6)}</div>
<div class="pinkeys" id="rsKeys">
${[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="rsErr"></div>
</div>
<div class="pinactions">
<button class="btn" id="rsCancel" type="button">Cancel</button>
<button class="btn" id="rsUsePw" type="button">Use master password</button>
</div>
</div>`;
document.body.appendChild(wrap);
const done = (v) => { try { wrap.remove(); } catch {} resolve(v); };
wrap.querySelector("#rsCancel").addEventListener("click", () => done(null));
wrap.querySelector("#rsUsePw").addEventListener("click", () => done("__fallback__"));
let tries = 0;
setupPinPad({
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
dots: wrap.querySelector("#rsDots"), keys: wrap.querySelector("#rsKeys"), err: wrap.querySelector("#rsErr"),
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
onComplete: async (pin) => {
let r;
try { r = await pinTry(pin); }
catch (e) { $("rsErr").textContent = cleanErr(e); return "reset"; }
if (r.ok) { done(r.masterPassword); return "ok"; }
// Every guess here also counts toward the 15 min lockout.
tries++;
if (r.lockedMs > 0 || tries >= REVEAL_PIN_MAX_FAILS) { done("__fallback__"); return "ok"; }
const left = REVEAL_PIN_MAX_FAILS - tries;
$("rsErr").textContent = `Wrong PIN. ${left} attempt${left === 1 ? "" : "s"} left, then Aegis will ask for your master password.`;
return "reset";
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
},
});
});
}
// 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.
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
// Only one PIN prompt at a time. Two concurrent prompts are asking the same
// question, so the second joins the first instead of stacking a second pad
// over it — which is also how duplicate pad ids came to collide.
let pinPromptInFlight = null;
function verifyPinInteractively(subtitle) {
if (pinPromptInFlight) return pinPromptInFlight;
pinPromptInFlight = verifyPinInteractivelyOnce(subtitle)
.finally(() => { pinPromptInFlight = null; });
return pinPromptInFlight;
}
async function verifyPinInteractivelyOnce(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({
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
dots: wrap.querySelector("#vpDots"), keys: wrap.querySelector("#vpKeys"), err: wrap.querySelector("#vpErr"),
onComplete: async (pin) => {
let r;
try { r = await pinTry(pin); }
catch (e) { $("vpErr").textContent = cleanErr(e); return "reset"; }
// The unwrapped master password is truthy for every existing caller;
// the gate hands it to the host as proof (see pinGate).
if (r.ok) { done(r.masterPassword || true); return "ok"; }
const left = Math.max(0, r.remaining);
$("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
}
}
Aegis: fees that follow the network, connections that come back, its own name on Solana Still 0.31.0 (unpublished batch). - ETH nonce. The pending count from a load-balanced RPC often misses a transaction this wallet sent seconds ago, so two sends in a row shared a nonce and the second failed or replaced the first. The nonce is taken at signing, from the RPC or from what the wallet itself last broadcast, whichever is higher, and broadcasts are serialised per wallet. - Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155 transaction; they rejected the type-2 envelope, so a network added by a dapp could receive but never send. The tip is clamped to the fee cap. - Bitcoin and DigiByte take their fee rate from the Electrum server's estimate instead of a constant, size each output from its real script (a taproot destination was undercounted), and round the size up before pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20 sat/vB floor and does not signal RBF, which it does not have. - Electrum: a wallet with live subscriptions went quiet for good when its server dropped. The client reconnects with backoff, pings to catch dead sockets, times out a silent connect, and hands the replayed subscription answers on as notifications so the wallet refreshes. dispose() ends it. - Max with an SPL token selected did nothing; it now fills the exact token balance. - Removing a wallet retired its derivation index for good. Add wallet now takes the lowest free index, so the same wallet comes back. - Solana: registered through the Wallet Standard as "Aegis" instead of setting isPhantom, with silent connect for already-connected sites. - "Only show the wallet to sites I enable": Aegis keeps the host's page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
// "Who can see the wallet". The list is the sites the user enabled by hand;
// sites with a saved connection stay visible regardless and are listed under
// Connected sites above.
async function renderInjectPolicy() {
const box = $("injectOnlyAllowed");
if (!box) return;
let st;
try { st = await S.invoke("injectPolicyGet"); } catch { return; }
box.checked = st.mode === "allowed";
box.disabled = !st.supported;
$("injectUnsupported").hidden = !!st.supported;
$("injectAllowBox").hidden = !(st.supported && st.mode === "allowed");
const list = $("injectSites");
list.innerHTML = (st.origins || []).length
? st.origins.map((o) => `<div class="k" style="overflow-wrap:anywhere">${esc(o)}</div><div class="v"><button class="btn sm" data-injremove="${esc(o)}" type="button">Remove</button></div>`).join("")
: `<div class="hint">No sites enabled yet. Open the site in a tab, then press the button above.</div>`;
list.querySelectorAll("[data-injremove]").forEach((b) => b.addEventListener("click", async () => {
try { await S.invoke("injectRemoveSite", { origin: b.dataset.injremove }); } catch {}
renderInjectPolicy();
}));
}
if ($("injectOnlyAllowed")) {
$("injectOnlyAllowed").addEventListener("change", async (e) => {
try { await S.invoke("injectPolicySet", { mode: e.target.checked ? "allowed" : "all" }); }
catch (err) { e.target.checked = !e.target.checked; aegisAlert("Could not save setting: " + cleanErr(err)); }
renderInjectPolicy();
});
$("injectEnableCurrent").addEventListener("click", async () => {
const msg = $("injectMsg");
try {
const r = await S.invoke("injectAllowSite", {});
msg.className = "msg ok"; msg.textContent = `Enabled on ${r.origin}. Reload that page to use the wallet there.`;
} catch (err) { msg.className = "msg err"; msg.textContent = cleanErr(err); }
msg.hidden = false;
renderInjectPolicy();
});
}
async function renderSites() {
Aegis: fees that follow the network, connections that come back, its own name on Solana Still 0.31.0 (unpublished batch). - ETH nonce. The pending count from a load-balanced RPC often misses a transaction this wallet sent seconds ago, so two sends in a row shared a nonce and the second failed or replaced the first. The nonce is taken at signing, from the RPC or from what the wallet itself last broadcast, whichever is higher, and broadcasts are serialised per wallet. - Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155 transaction; they rejected the type-2 envelope, so a network added by a dapp could receive but never send. The tip is clamped to the fee cap. - Bitcoin and DigiByte take their fee rate from the Electrum server's estimate instead of a constant, size each output from its real script (a taproot destination was undercounted), and round the size up before pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20 sat/vB floor and does not signal RBF, which it does not have. - Electrum: a wallet with live subscriptions went quiet for good when its server dropped. The client reconnects with backoff, pings to catch dead sockets, times out a silent connect, and hands the replayed subscription answers on as notifications so the wallet refreshes. dispose() ends it. - Max with an SPL token selected did nothing; it now fills the exact token balance. - Removing a wallet retired its derivation index for good. Add wallet now takes the lowest free index, so the same wallet comes back. - Solana: registered through the Wallet Standard as "Aegis" instead of setting isPhantom, with silent connect for already-connected sites. - "Only show the wallet to sites I enable": Aegis keeps the host's page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
renderInjectPolicy();
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) || (p.eth && p.eth.readAddress) || (p.sol && p.sol.readAddress));
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 = $("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");
if (p.eth && p.eth.readAddress) what.push("EVM address" + (p.eth.chainId ? ` (chain ${p.eth.chainId})` : ""));
if (p.sol && p.sol.readAddress) what.push("Solana address");
// A plain "Connect" lasts until Theseus restarts; only "Always allow" is stored.
if (p.session) what.push("this session only");
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); }
});
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
// Every "show the secret" button goes through the one gated path. They used
// to call recovery({reveal:true}), which handed back the xprv behind nothing
// but an approval click and refused imported wallets outright.
$("showXprv").addEventListener("click", () => openRevealSecretModal(sel()));
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; }
});
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
$("showScSeed").addEventListener("click", () => openRevealSecretModal(sel()));
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): 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); }
});
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
$("showBtcXprv").addEventListener("click", () => openRevealSecretModal(sel()));
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; }
});
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
$("showEthKey").addEventListener("click", () => openRevealSecretModal(sel()));
$("showSolKey").addEventListener("click", () => openRevealSecretModal(sel()));
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); }
});
feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN Getting a key back out of Aegis only worked for wallets it derived itself. Every imported adapter's recovery() returned xprv:null with "Recovery lives in the source of the import", so the wallets most likely to need exporting were the ones that refused, and the vault-derived ones handed over an xprv behind nothing but an approval click. There is one gate now, and it is enforced in the host. revealSecret takes the master password and verifies it with vault.lifecycle.unlock before it reads anything; the panel obtains that password either by decrypting the PIN blob, which wraps exactly it, or by asking. Both routes end at the same proof, so the host never takes the panel's word for authorisation. The old recovery({reveal:true}) path is gone and all six Settings buttons route here. Three wrong PINs switch to the master password rather than dead-ending, and those attempts still count toward the existing 15-minute lockout, so a fumbled PIN costs nothing and a guessed one gains nothing. Being locked out of the PIN also falls through to the password: the lockout exists to stop PIN guessing, not to lock an owner out of their own key. "Use PIN to show secret keys" defaults ON, unlike the send flag — a send is already fronted by an approval overlay, whereas a revealed key is irreversible the moment it is on screen. Turning it off moves the prompt to the master password. There is deliberately no setting that reveals a key without asking for anything. It is NOT called a recovery phrase, because Aegis has none to show. An import stores mnemonicToSeedHex(words) and discards the words, vault wallets are HKDF(vault root, purpose) and never had words, and password-vault.js is explicit that the seed is never persisted. So each form names itself — WIF, private key (hex), wallet seed (hex), wallet key (hex) — and says where it can actually be restored. Someone who writes down what this shows believing it is twelve words has backed up nothing, which is the one outcome this screen has to prevent.
2026-10-02 00:04:06 +02:00
$("showDgbXprv").addEventListener("click", () => openRevealSecretModal(sel()));
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 ------------------------------------------------------------------
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
S.on("state", (s) => {
state = s;
render();
// render() returns early while a PIN is owed; don't fill a hidden Settings
// tab behind the door either.
if (tab === "settings" && !pinGateBlocked) fillSettings();
});
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
S.on("pinRequest", (req) => { answerPinRequest(req); });
(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 {}
fix(aegis): PIN asked at the door, and the pad stops erasing itself Two regressions in 0.26.0. The PIN was still asked after the wallet had painted. pinGateOnLoad() ran after render() and was not awaited, so the interface appeared — balances and all — and the pad then opened on top of a panel the user could already read. It is now awaited before the first render, and `pinGateBlocked` keeps the chrome hidden for as long as the PIN is owed: render() returns early into a closed door, state pushes cannot paint around it, and Settings is covered too (unlike the vault lock, where Settings must stay reachable to create a PIN in the first place, here it would just be a way around the gate). Cancelling leaves the door shut with a retry rather than falling through to an open wallet. The pad threw digits away mid-entry. render() fires on every state push — balance polls fire it every couple of seconds — and renderLockScreen() rebuilt body.innerHTML unconditionally, replacing the pad and its buffer under the user's fingers. The reset was timed to the poll, not to the keypress count, which is why it looked like "after two digits". All three branches are now keyed on body.dataset.mode and rebuild only when the mode actually changes; the master-password and first-run forms had the same defect and were erasing half-typed input the same way. Also: every pad was wired through getElementById, so two pads alive at once (a load gate plus a transaction gate) were duplicate ids and the second setupPinPad bound the first pad's buttons. All four are now scoped to their own container, only one PIN prompt can be open at a time, and setupPinPad carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
try { state = await S.invoke("state"); }
catch (e) {
$("gate").hidden = false;
$("gate").innerHTML = `<div class="big">⚠</div><div>${esc(cleanErr(e))}</div>`;
return;
}
// Settle the PIN BEFORE the first paint. Awaited on purpose: rendering the
// wallet first and prompting afterwards is exactly the behaviour being
// fixed, and a fire-and-forget call also let a second prompt open on top
// of this one.
await pinGateOnLoad();
render();
bindIdleAutoLock();
Aegis: the PIN is enforced by the host, and every spend is confirmed there PIN - The PIN blob wraps the vault master password under six digits and sat in plain add-on storage, so a copy of the profile reduced the master password to a million offline PBKDF2 guesses. It is sealed with the OS keystore before it is stored; a plain blob from an older build is sealed on first read. New blobs use 600k iterations. - "Ask for PIN on every transaction" was decided by the host and enforced by nobody: `send` never checked it and dapp transactions had no PIN step. A gate is now cleared only by the master password the PIN unwraps, verified against the vault, which also opens a single-use transaction clearance. Panel sends consume one; dapp transactions ask the open panel and wait. Spending and signing - Consolidate emptied wallets on the panel's confirmation alone, defaulted to every sibling when no list was sent, and swept into whatever was selected at click time. It now needs explicit sources and the previewed destination, and shows the whole batch on the host overlay. - Typed data for a chain other than the connected one is refused. Permit and Permit2 signatures name the spender, tokens, amounts and expiry, and an unlimited one gets the danger action. Previews are no longer cut at 600/400 characters without saying so. - eth.rpc relayed any eth_* call for sites that never connected. - The WizardConnect overlay lists the tokens being spent and received. Send form - "0,5" was read as 5: the parser deleted commas. Amounts are parsed exactly; a decimal comma is a decimal, ambiguous or non-numeric input is refused, extra decimals are an error instead of being dropped. - Send submits the request the summary was computed for, never a fresh read of the form, and stays off while a plan is pending or stale. - Switching wallet resets the form instead of leaving a live button on the previous wallet's plan. - The unlock field kept the master password after unlocking; the PIN pad kept its digits and kept counting keystrokes typed elsewhere as PIN attempts; an idle lock left a revealed key or seed form on screen. - Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
// Opened because a dapp transaction is waiting for the PIN? Answer it.
try { answerPinRequest(await S.invoke("pinRequestPending")); } catch {}
})();
// 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);
})();