theseus/bundled-addons/aegis/index.js

4660 lines
236 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 — multi-chain wallet bundled with Theseus. activate() runs in the
// main process; every wallet's key material lives here, in memory, and is
// re-derived from the password vault on every launch. Nothing secret is ever
// written to disk or logged.
//
// A single addon can hold many wallets — one per {chain, network}, or several
// sub-accounts of the same chain — and each wallet is backed by its own
// vault-derived 32-byte HKDF root. Chains today: BCH, Tron mainnet, Tron
// Nile testnet. Adding a fourth chain is a new adapter file under lib/ and
// an entry in the CHAIN_REGISTRY below.
//
// Back-compat: the addon id stays "bchwallet" (the manifest label became
// Aegis, but the id gates the vault-derive namespace and older vaults have
// funds against it). The legacy BCH default wallet uses purpose
// "bchwallet/mainnet/0" — byte-identical to the pre-multi-wallet build —
// so on-disk funds are untouched. See memory bchwallet-vault-root-derivation.
const path = require("node:path");
const fs = require("node:fs");
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 LEGACY_BCH_PURPOSE = "bchwallet/mainnet/0";
// New each time the add-on's main process starts, i.e. once per Theseus
// launch. Comparing it against the id stored the last time a PIN was
// accepted is how "ask again after a browser restart" is detected — a
// timestamp cannot tell a restart from a long idle, and the panel cannot be
// trusted to report its own restarts.
const BOOT_ID = require("node:crypto").randomBytes(8).toString("hex");
const PIN_INTERVAL_MS = 6 * 60 * 60 * 1000;
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 LEGACY_BCH_WALLET_ID = "bch-default";
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 ctx = 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
// ---- deps -------------------------------------------------------------------
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
async function loadDeps(api) {
const { secp256k1 } = await api.import("@noble/curves/secp256k1.js");
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 { ed25519 } = await api.import("@noble/curves/ed25519.js");
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 { sha256, sha512 } = await api.import("@noble/hashes/sha2.js");
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 { hkdf } = await api.import("@noble/hashes/hkdf.js");
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
// For DigiByte's legacy master-key derivation — see HMAC_DIGIBYTE_SEED in
// lib/dgb/core/hd.js.
const { hmac } = await api.import("@noble/hashes/hmac.js");
const { ripemd160 } = await api.import("@noble/hashes/legacy.js");
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 { keccak_256 } = await api.import("@noble/hashes/sha3.js");
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 { blake2b } = await api.import("@noble/hashes/blake2.js");
const { HDKey } = await api.import("@scure/bip32");
const WebSocket = api.require("ws");
const cashaddr = require("./lib/cashaddr.js");
const keysLib = require("./lib/keys.js")({ HDKey, secp256k1, sha256, ripemd160, cashaddr });
const tx = require("./lib/tx.js")({ sha256 });
const electrum = require("./lib/electrum.js")({ WebSocket, log: (...a) => api.log("electrum", ...a) });
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 base58check = require("./lib/base58check.js")({ sha256 });
const bchAdapter = require("./lib/chain-bch.js")({
HDKey, secp256k1, sha256, ripemd160, cashaddr, keysLib, tx, electrum, WebSocket,
});
const tronAdapter = require("./lib/chain-tron.js")({
HDKey, secp256k1, sha256, keccak_256, base58check,
});
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 siaAdapter = require("./lib/chain-sia.js")({ ed25519, blake2b });
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
const ethAdapter = require("./lib/chain-eth.js")({ HDKey, secp256k1, keccak_256 });
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
const eip712 = require("./lib/eip712.js")({ keccak_256 });
// Tron raw_data_hex decoder — the approval overlay for dapp-built Tron
// transactions is built from these bytes, never from the dapp's JSON.
const tronDecode = require("./lib/tron-decode.js")({ sha256, base58check });
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
// Solana uses the raw base58 alphabet (no checksum), which lives inside
// base58check as encodeBase58 / decodeBase58 — expose them under a
// `{encode, decode}` shape the SOL adapter reads from.
const solBase58 = { encode: base58check.encodeBase58, decode: base58check.decodeBase58 };
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 solAdapter = require("./lib/chain-sol.js")({ ed25519, base58: solBase58, sha256 });
feat(theseus/aegis): DGB adapter on @dgb-wallet/{core,psbt} vendored packages Aegis now shares its DGB code with the standalone DigiByte web-wallet at D:\Dev\SilentCode\Digibyte. Address derivation and PSBT construction come from that project's @dgb-wallet/core and @dgb-wallet/psbt packages instead of Aegis-local reimplementations. Any bugfix upstream flows in via a re-vendor of dist/*. - lib/dgb/{core,psbt}/ — vendored dist/ output of the two packages plus a tiny package.json shim marking them as ESM. @dgb-wallet/core's own import specifier "@dgb-wallet/core" inside psbt/*.js is rewritten to "../core/index.js" so the sibling module resolves without a workspace. - New Theseus deps: bitcoinjs-lib, bip32, bip39, @bitcoinerlab/secp256k1, ecpair — the peer deps the vendored packages need. Loaded via api.require in index.js's loadDeps(). - chain-dgb.js is a thin adapter now: BIP32 tree via bip32 + DGB Network object, addresses via core.p2wpkhAddress, tx via psbt.buildPsbt + PSBT.signInput (per-input, since each UTXO's key differs) + psbt.finalizeAndExtract. Runtime backend stays the same — Theseus's lib/electrum.js against the DGB ElectrumX pool. - Verified end-to-end in scratchpad: abandon×11 mnemonic derives dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8 (matches iancoleman.io/bip39 and the previous inline implementation, so no on-chain address change for anyone who was already using Aegis's DGB slot). PSBT build+sign+ finalize on a mock UTXO produces a valid 223-byte witness tx. BIP44 (D…) and BIP49 (S…) address families are implemented in the vendored core but not yet exposed in Aegis's picker — the panel needs an "address family" selector inside the DGB settings block first. Left for a follow-up; today's DGB pick uses BIP84 native SegWit only.
2026-09-07 02:19:44 +02:00
// DGB delegates address derivation + PSBT to the vendored @dgb-wallet/*
// packages under lib/dgb/. Those are ESM; the peer deps (bitcoinjs-lib,
// bip32, ecpair, @bitcoinerlab/secp256k1) are CommonJS and reachable via
// api.require from the Theseus dependency tree.
const { pathToFileURL } = require("node:url");
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
// DGB's bundled ESM packages import their peer deps by bare specifier
// ("bip32", "bitcoinjs-lib", …). When aegis is loaded from a copy in
// userData/addons/, Node's ESM resolver can't reach Theseus's node_modules
// from that path — so the import throws. Wrap it: DGB just becomes
// unavailable, the rest of Aegis keeps working.
fix(aegis): DigiByte import failed on every OTA install Reported as "the Aegis DigiByte import error". The vendored @dgb-wallet/core modules under lib/dgb/ imported "bitcoinjs-lib", "bip32", "ecpair", "bip39" and "@bitcoinerlab/secp256k1" as bare specifiers. Node's ESM resolver walks up from the importing file, so that resolves in a dev checkout — where Theseus's node_modules sits above the add-on — and resolves nowhere once Aegis is running from <userData>/extensions/aegis/, which is every OTA install. The import threw, index.js caught it and set dgbCore = null, and any DigiByte import then died in digibyteNetwork() with "DGB adapter not available". So DigiByte worked on a fresh install and disappeared after the first update. Confirmed on this machine: the only node_modules reachable from the installed extension carries bip39 and none of the other four. lib/dgb/deps.js now holds the dependencies, injected by index.js from api.require (which resolves against the app tree) before anything under lib/dgb/ is imported — the same pattern every other lib/*.js in Aegis already uses, and the reason that pattern exists. Consumers read them through accessors rather than capturing them at module scope, so import order is no longer load-bearing: hd.js builds its bip32 on first use and the two initEccLib callers go through a once-only ensureEcc(). Using a DGB module without injection now throws a named error instead of a resolver failure swallowed into a null. Verified by loading the modules from a directory with no reachable node_modules and deriving real addresses: BIP44 D…, BIP49 S… with its legacy 3… pair, BIP84 dgb1q…, BIP86 dgb1p… taproot, plus a WIF round-trip and the non-DGB WIF rejection.
2026-10-03 13:59:01 +02:00
const bitcoinjs = api.require("bitcoinjs-lib");
const { BIP32Factory } = api.require("bip32");
const { ECPairFactory } = api.require("ecpair");
const ecc = api.require("@bitcoinerlab/secp256k1");
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 dgbCore = null, dgbPsbt = null, dgbAdapter = null;
try {
fix(aegis): DigiByte import failed on every OTA install Reported as "the Aegis DigiByte import error". The vendored @dgb-wallet/core modules under lib/dgb/ imported "bitcoinjs-lib", "bip32", "ecpair", "bip39" and "@bitcoinerlab/secp256k1" as bare specifiers. Node's ESM resolver walks up from the importing file, so that resolves in a dev checkout — where Theseus's node_modules sits above the add-on — and resolves nowhere once Aegis is running from <userData>/extensions/aegis/, which is every OTA install. The import threw, index.js caught it and set dgbCore = null, and any DigiByte import then died in digibyteNetwork() with "DGB adapter not available". So DigiByte worked on a fresh install and disappeared after the first update. Confirmed on this machine: the only node_modules reachable from the installed extension carries bip39 and none of the other four. lib/dgb/deps.js now holds the dependencies, injected by index.js from api.require (which resolves against the app tree) before anything under lib/dgb/ is imported — the same pattern every other lib/*.js in Aegis already uses, and the reason that pattern exists. Consumers read them through accessors rather than capturing them at module scope, so import order is no longer load-bearing: hd.js builds its bip32 on first use and the two initEccLib callers go through a once-only ensureEcc(). Using a DGB module without injection now throws a named error instead of a resolver failure swallowed into a null. Verified by loading the modules from a directory with no reachable node_modules and deriving real addresses: BIP44 D…, BIP49 S… with its legacy 3… pair, BIP84 dgb1q…, BIP86 dgb1p… taproot, plus a WIF round-trip and the non-DGB WIF rejection.
2026-10-03 13:59:01 +02:00
// Inject before importing anything under lib/dgb/. Those modules used to
// import "bitcoinjs-lib" and friends as bare specifiers, which only
// resolves when the add-on sits inside the app tree — so DigiByte worked
// on a fresh install and vanished after the first OTA update, where Aegis
// runs from <userData>/extensions/aegis/. See lib/dgb/deps.js.
const dgbDeps = await import(pathToFileURL(path.join(api.folder, "lib/dgb/deps.js")).href);
dgbDeps.setDgbDeps({
bitcoinjs, ecc,
bip32Factory: BIP32Factory,
ecpairFactory: ECPairFactory,
bip39: api.require("bip39"),
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
hmac, sha512,
fix(aegis): DigiByte import failed on every OTA install Reported as "the Aegis DigiByte import error". The vendored @dgb-wallet/core modules under lib/dgb/ imported "bitcoinjs-lib", "bip32", "ecpair", "bip39" and "@bitcoinerlab/secp256k1" as bare specifiers. Node's ESM resolver walks up from the importing file, so that resolves in a dev checkout — where Theseus's node_modules sits above the add-on — and resolves nowhere once Aegis is running from <userData>/extensions/aegis/, which is every OTA install. The import threw, index.js caught it and set dgbCore = null, and any DigiByte import then died in digibyteNetwork() with "DGB adapter not available". So DigiByte worked on a fresh install and disappeared after the first update. Confirmed on this machine: the only node_modules reachable from the installed extension carries bip39 and none of the other four. lib/dgb/deps.js now holds the dependencies, injected by index.js from api.require (which resolves against the app tree) before anything under lib/dgb/ is imported — the same pattern every other lib/*.js in Aegis already uses, and the reason that pattern exists. Consumers read them through accessors rather than capturing them at module scope, so import order is no longer load-bearing: hd.js builds its bip32 on first use and the two initEccLib callers go through a once-only ensureEcc(). Using a DGB module without injection now throws a named error instead of a resolver failure swallowed into a null. Verified by loading the modules from a directory with no reachable node_modules and deriving real addresses: BIP44 D…, BIP49 S… with its legacy 3… pair, BIP84 dgb1q…, BIP86 dgb1p… taproot, plus a WIF round-trip and the non-DGB WIF rejection.
2026-10-03 13:59:01 +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
dgbCore = await import(pathToFileURL(path.join(api.folder, "lib/dgb/core/index.js")).href);
dgbPsbt = await import(pathToFileURL(path.join(api.folder, "lib/dgb/psbt/index.js")).href);
} catch (e) {
api.log("dgb unavailable:", e?.message || e);
}
if (dgbCore && dgbPsbt) {
dgbAdapter = require("./lib/chain-dgb.js")({
dgbCore, dgbPsbt, bitcoinjs,
bip32Factory: BIP32Factory, ecpairFactory: ECPairFactory, ecc,
sha256, electrum,
});
}
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
const btcAdapter = require("./lib/chain-btc.js")({
bitcoinjs, bip32Factory: BIP32Factory, ecpairFactory: ECPairFactory, ecc,
sha256, electrum,
});
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 bip39 = api.require("bip39");
// Imported BCH — a lean read-only adapter for wallets whose key material
// lives in Theseus's wallet-imports.enc. Same electrum/cashaddr surface as
// the primary BCH adapter but a single fixed address per wallet.
const importedBchAdapter = require("./lib/chain-bch-imported.js")({
sha256, ripemd160, cashaddr, electrum, WebSocket, tx,
HDKey, secp256k1, base58check, vaultImports: api.vault && api.vault.imports,
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
});
// Multi-chain imported adapters. UTXO chains (BTC, DGB) share an electrum-
// based reader; account-model chains (ETH, TRX, SOL) share a JSON-RPC
// reader. Every runtime is read-only in M.1b, matching chain-bch-imported.
const utxoImportedAdapter = require("./lib/chain-utxo-imported.js")({
sha256, bitcoinjs, dgbCore, electrum, WebSocket,
});
const genericImportedAdapter = require("./lib/chain-generic-imported.js")();
// Per-chain address derivation from raw material (mnemonic + path or
// chain-native private key). Used by the importWallet handler to compute
// the address client-side before wallet-imports.enc stores the material.
const derive = require("./lib/import-derive.js")({
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
HDKey, secp256k1, ed25519, sha256, sha512, hmac, ripemd160, keccak_256, blake2b,
cashaddr, base58check, bitcoinjs, bip32Factory: BIP32Factory,
ecpairFactory: ECPairFactory, ecc, bip39, dgbCore,
});
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: LGPL-3.0-or-later. Dynamic-linked via api.import so the
// §4d combined-work requirement (dynamic linkage + license notice + source
// availability) is met — package sources ship with npm.
const wcCore = await api.import("@wizardconnect/core");
const wcWallet = await api.import("@wizardconnect/wallet");
const libauth = await api.import("@bitauth/libauth");
return { HDKey, secp256k1, ed25519, sha256, hkdf, ripemd160, keccak_256, blake2b,
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
cashaddr, keysLib, tx, electrum, base58check,
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
bchAdapter, tronAdapter, siaAdapter, dgbAdapter, ethAdapter, solAdapter, btcAdapter,
importedBchAdapter, utxoImportedAdapter, genericImportedAdapter,
derive, bip39,
dgbCore, dgbPsbt, bitcoinjs, ecc, eip712, tronDecode,
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
wcCore, wcWallet, libauth };
}
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
// ---- servers ---------------------------------------------------------------
function bchDefaultServers(api) {
try { return JSON.parse(fs.readFileSync(path.join(api.folder, "electrum-servers.json"), "utf8")); }
catch { return []; }
}
function bchServerList(api) {
const custom = api.storage.get("servers", null);
return Array.isArray(custom) && custom.length ? custom : bchDefaultServers(api);
}
// ---- chain registry --------------------------------------------------------
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
// Two-level structure so the panel can present coin-then-network as separate
// picks. `coin` fields are chain-wide; `networks[<id>]` fields override or
// add to them per-network. `logo` is the SVG key panel.js draws from.
const COINS = {
bch: {
chain: "bch",
label: "Bitcoin Cash",
short: "BCH",
ticker: "BCH",
decimals: 8,
color: "#0ac18e",
logo: "bch",
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
supportsMessageSign: true,
supportsPageInject: true, // window.bitcoincash on *.x pages
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
networks: {
mainnet: {
id: "mainnet", label: "Mainnet", testnet: false,
// Legacy default wallet uses the flat "bchwallet/mainnet/0" purpose;
// new BCH mainnet sub-accounts start at index 1 under the /bch/ prefix.
purposePrefix: "bchwallet/bch/",
startIndex: 1,
},
chipnet: {
id: "chipnet", label: "Chipnet testnet", testnet: true,
purposePrefix: "bchwallet/bch/chipnet/",
startIndex: 0,
},
},
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): 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
trx: {
chain: "trx",
label: "Tron",
short: "TRX",
ticker: "TRX",
decimals: 6,
color: "#ff060a",
logo: "trx",
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
supportsMessageSign: true,
supportsPageInject: true, // window.tronWeb everywhere
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
networks: {
mainnet: {
id: "mainnet", label: "Mainnet", testnet: false,
purposePrefix: "bchwallet/trx/mainnet/", startIndex: 0,
},
nile: {
id: "nile", label: "Nile testnet", testnet: true,
purposePrefix: "bchwallet/trx/nile/", startIndex: 0,
},
},
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
sc: {
chain: "sc",
label: "Siacoin",
short: "SC",
ticker: "SC",
decimals: 24,
color: "#20be82",
logo: "sc",
supportsMessageSign: true,
supportsPageInject: false,
// First SC wallet in Aegis reuses the legacy standalone-siawallet purpose
// so pre-Aegis funds carry over automatically (addon.json declares
// `absorbs: ["siawallet"]` to allow the derivation). Second+ use the new
// Aegis-namespaced prefix.
networks: {
mainnet: {
id: "mainnet", label: "Mainnet", testnet: false,
purposePrefix: "bchwallet/sc/mainnet/", startIndex: 1,
legacyFirstPurpose: "siawallet/mainnet/0",
},
},
},
dgb: {
chain: "dgb",
label: "DigiByte",
short: "DGB",
ticker: "DGB",
decimals: 8,
color: "#0066cc",
logo: "dgb",
supportsMessageSign: true,
supportsPageInject: false,
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
// BIP44/49/84/86 address families — the picker lives in the DGB
// settings block. Default is BIP84 (dgb1q…), which matches modern
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
// DGB Core, DigiByte-Go, and the SilentCode web-wallet. `coinType`
// per chain feeds the path builder below.
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
addressFamilies: [
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
{ id: "bip84", purpose: 84, label: "Native SegWit (dgb1q…)" },
{ id: "bip86", purpose: 86, label: "Taproot (dgb1p…)" },
{ id: "bip49", purpose: 49, label: "Wrapped SegWit (S…)" },
{ id: "bip44", purpose: 44, label: "Legacy P2PKH (D…)" },
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
coinType: 20,
defaultPurpose: 84,
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
networks: {
mainnet: {
id: "mainnet", label: "Mainnet", testnet: false,
purposePrefix: "bchwallet/dgb/mainnet/", startIndex: 0,
},
},
},
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: {
chain: "eth",
label: "Ethereum",
short: "ETH",
ticker: "ETH",
decimals: 18,
color: "#627eea",
logo: "eth",
supportsMessageSign: true,
supportsPageInject: false, // EIP-1193 provider is a follow-up
networks: {
mainnet: {
id: "mainnet", label: "Mainnet", testnet: false,
purposePrefix: "bchwallet/eth/mainnet/", startIndex: 0,
},
sepolia: {
id: "sepolia", label: "Sepolia testnet", testnet: true,
purposePrefix: "bchwallet/eth/sepolia/", startIndex: 0,
},
},
},
sol: {
chain: "sol",
label: "Solana",
short: "SOL",
ticker: "SOL",
decimals: 9,
color: "#9945ff",
logo: "sol",
supportsMessageSign: true,
supportsPageInject: false,
networks: {
mainnet: {
id: "mainnet", label: "Mainnet-beta", testnet: false,
purposePrefix: "bchwallet/sol/mainnet/", startIndex: 0,
},
devnet: {
id: "devnet", label: "Devnet", testnet: true,
purposePrefix: "bchwallet/sol/devnet/", startIndex: 0,
},
},
},
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: {
chain: "btc",
label: "Bitcoin",
short: "BTC",
ticker: "BTC",
decimals: 8,
color: "#f7931a",
logo: "btc",
supportsMessageSign: true,
supportsPageInject: 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
// BIP44/49/84/86 across bc1q… / bc1p… / 3… / 1… on mainnet and
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
// tb1q… / tb1p… / 2… / m/n… on testnet3 + signet. Coin type shifts
// per network (0 for mainnet, 1 for both testnet3 and signet — SLIP-44
// treats every Bitcoin testnet as coin type 1).
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
addressFamilies: [
{ id: "bip84", purpose: 84, label: "Native SegWit (bc1q… / tb1q…)" },
{ id: "bip86", purpose: 86, label: "Taproot (bc1p… / tb1p…)" },
{ id: "bip49", purpose: 49, label: "Wrapped SegWit (3… / 2…)" },
{ id: "bip44", purpose: 44, label: "Legacy P2PKH (1… / m…, n…)" },
],
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
coinType: { mainnet: 0, testnet: 1, signet: 1 },
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
defaultPurpose: 84,
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
networks: {
mainnet: {
id: "mainnet", label: "Mainnet", testnet: false,
purposePrefix: "bchwallet/btc/mainnet/", startIndex: 0,
},
testnet: {
id: "testnet", label: "Testnet3", testnet: true,
purposePrefix: "bchwallet/btc/testnet/", startIndex: 0,
},
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
signet: {
id: "signet", label: "Signet", testnet: true,
purposePrefix: "bchwallet/btc/signet/", startIndex: 0,
},
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): 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 chainKey(chain, network) { return `${chain}:${network}`; }
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
// Coin type per (chain, network). A number literal on the COINS entry
// (DGB uses a single 20) or a per-network object ({mainnet: 0, testnet: 1}
// for BTC). Returns null when the chain doesn't declare a family picker.
function coinTypeFor(c, network) {
if (!c || c.coinType == null) return null;
return typeof c.coinType === "number" ? c.coinType : (c.coinType[network] ?? null);
}
// Full derivation paths per family for a given (chain, network) — expands
// the family list on the fly so each picker knows exactly which path a
// pick would produce.
function addressFamiliesFor(c, network) {
if (!c || !c.addressFamilies) return null;
const ct = coinTypeFor(c, network);
if (ct == null) return c.addressFamilies;
return c.addressFamilies.map((f) => ({
...f,
defaultAccountPath: `m/${f.purpose}'/${ct}'/0'`,
}));
}
function defaultAccountPathFor(c, network) {
const ct = coinTypeFor(c, network);
if (ct == null || c.defaultPurpose == null) return null;
return `m/${c.defaultPurpose}'/${ct}'/0'`;
}
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
// Custom EVM chains (EIP-3085) live in api.storage under `customEthChains`.
// Structured as { [chainId]: {chainId, chainName, rpcUrl, explorerTx,
// explorerAddr, ticker, addedAt, addedByOrigin} }. They're not in COINS at
// module-load time — we synthesize a chainMeta / mount entry from storage
// so wallet_addEthereumChain can register new networks at runtime without
// a Theseus restart.
const CUSTOM_ETH_PREFIX = "custom-";
function customEthChains(api) {
const raw = api.storage.get("customEthChains", {});
return raw && typeof raw === "object" ? raw : {};
}
function customEthNetworkEntry(api, network) {
if (!network || !network.startsWith(CUSTOM_ETH_PREFIX)) return null;
const chainId = Number(network.slice(CUSTOM_ETH_PREFIX.length));
if (!Number.isFinite(chainId)) return null;
const all = customEthChains(api);
const cfg = all[String(chainId)];
if (!cfg) return null;
return {
id: network,
label: cfg.chainName || `EVM #${chainId}`,
chainId,
defaultRpc: cfg.rpcUrl,
explorerTx: cfg.explorerTx,
explorerAddr: cfg.explorerAddr,
ticker: cfg.ticker || "ETH",
faucet: 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
function chainMeta(chain, network) {
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
// ETH custom-network fallback for EIP-3085 chains.
if (chain === "eth" && String(network || "").startsWith(CUSTOM_ETH_PREFIX)) {
const cfg = customEthNetworkEntry(ctx?.api, network);
if (!cfg) return null;
return {
chain: "eth", network: cfg.id,
label: "Ethereum · " + cfg.label,
short: cfg.ticker, ticker: cfg.ticker, decimals: 18,
color: COINS.eth.color, logo: COINS.eth.logo,
coinLabel: cfg.label, networkLabel: `chainId ${cfg.chainId}`,
testnet: false,
purposePrefix: `bchwallet/eth/${cfg.id}/`, startIndex: 0,
supportsMessageSign: true, supportsPageInject: false,
addressFamilies: null, defaultAccountPath: null,
isCustom: true, chainId: cfg.chainId,
};
}
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 c = COINS[chain]; const n = c && c.networks[network];
if (!c || !n) return null;
return {
chain: c.chain, network: n.id,
label: c.label + " · " + n.label, short: c.short, ticker: c.ticker, decimals: c.decimals,
color: c.color, logo: c.logo, coinLabel: c.label, networkLabel: n.label, testnet: !!n.testnet,
purposePrefix: n.purposePrefix, startIndex: n.startIndex,
supportsMessageSign: !!c.supportsMessageSign, supportsPageInject: !!c.supportsPageInject,
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
addressFamilies: addressFamiliesFor(c, network),
defaultAccountPath: defaultAccountPathFor(c, network),
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 coinsForPanel() {
return Object.values(COINS).map((c) => ({
chain: c.chain, label: c.label, short: c.short, ticker: c.ticker, color: c.color, logo: c.logo, decimals: c.decimals,
networks: Object.values(c.networks).map((n) => ({ id: n.id, label: n.label, testnet: !!n.testnet })),
}));
}
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
// ---- wallet list ------------------------------------------------------------
perf(aegis): stop freezing Theseus — a 7.4 MB store parsed on every read Reported as "opening Aegis makes Theseus get stuck / not responding", and it was Aegis's fault. The host's add-on store is ONE JSON file per add-on, and storage.get() does a readFileSync plus a JSON.parse of the whole thing on every call — synchronously, on the Electron main thread, the thread that drives the entire browser. storage.set() additionally stringifies and writes all of it. That store had grown to 7.4 MB, 99.9% of it one wallet's txCache: getTx() kept every transaction it ever fetched, with full vin/vout arrays, and a busy chipnet test wallet had thousands. Measured on the real file: parse 59 ms, stringify 39 ms. So one storage.get blocked the UI for ~60 ms, one set for ~99 ms, and fullState() — which reads the store seven times over selectedWalletId, walletRoles, walletEntries, snapshotForSelected and the server list — cost ~420 ms. emitState() runs on every adapter change, across nine wallets, so the main thread was never given back. Three changes: - txCache is capped at 400 entries, pruned newest-first by block time (unconfirmed entries sort as newest — they are the current ones). Worst case ~0.3 MB per wallet instead of unbounded. - TX_CACHE_VERSION 3, so existing oversized caches are discarded on first load rather than needing a manual clear. - loadHistory() only persists when something actually changed; an idle wallet was rewriting the whole file on every poll for nothing. - api.storage gains a write-through read cache, so repeated gets cost one parse per process instead of one per call. Writes still go to the host unchanged. Safe because this process is the only writer — Aegis's panel talks over addon messages and never touches addon storage; if that changes, the cache has to go.
2026-10-03 10:56:59 +02:00
// Replace api.storage with a write-through read cache. Writes still go to the
// host so nothing changes about durability or who owns the file; only the
// repeated parsing of it disappears. Falls back to the host's storage
// untouched if the api object will not accept the substitution.
function installStorageCache(api, onSet) {
perf(aegis): stop freezing Theseus — a 7.4 MB store parsed on every read Reported as "opening Aegis makes Theseus get stuck / not responding", and it was Aegis's fault. The host's add-on store is ONE JSON file per add-on, and storage.get() does a readFileSync plus a JSON.parse of the whole thing on every call — synchronously, on the Electron main thread, the thread that drives the entire browser. storage.set() additionally stringifies and writes all of it. That store had grown to 7.4 MB, 99.9% of it one wallet's txCache: getTx() kept every transaction it ever fetched, with full vin/vout arrays, and a busy chipnet test wallet had thousands. Measured on the real file: parse 59 ms, stringify 39 ms. So one storage.get blocked the UI for ~60 ms, one set for ~99 ms, and fullState() — which reads the store seven times over selectedWalletId, walletRoles, walletEntries, snapshotForSelected and the server list — cost ~420 ms. emitState() runs on every adapter change, across nine wallets, so the main thread was never given back. Three changes: - txCache is capped at 400 entries, pruned newest-first by block time (unconfirmed entries sort as newest — they are the current ones). Worst case ~0.3 MB per wallet instead of unbounded. - TX_CACHE_VERSION 3, so existing oversized caches are discarded on first load rather than needing a manual clear. - loadHistory() only persists when something actually changed; an idle wallet was rewriting the whole file on every poll for nothing. - api.storage gains a write-through read cache, so repeated gets cost one parse per process instead of one per call. Writes still go to the host unchanged. Safe because this process is the only writer — Aegis's panel talks over addon messages and never touches addon storage; if that changes, the cache has to go.
2026-10-03 10:56:59 +02:00
const raw = api.storage;
if (!raw || typeof raw.all !== "function") return false;
let mem = null;
const load = () => {
if (!mem) { try { mem = raw.all() || {}; } catch { mem = {}; } }
return mem;
};
const cached = {
get: (key, fallback = null) => {
const s = load();
return Object.prototype.hasOwnProperty.call(s, key) ? s[key] : fallback;
},
set: (key, value) => {
load()[key] = value;
raw.set(key, value);
if (onSet) try { onSet(key); } catch {}
perf(aegis): stop freezing Theseus — a 7.4 MB store parsed on every read Reported as "opening Aegis makes Theseus get stuck / not responding", and it was Aegis's fault. The host's add-on store is ONE JSON file per add-on, and storage.get() does a readFileSync plus a JSON.parse of the whole thing on every call — synchronously, on the Electron main thread, the thread that drives the entire browser. storage.set() additionally stringifies and writes all of it. That store had grown to 7.4 MB, 99.9% of it one wallet's txCache: getTx() kept every transaction it ever fetched, with full vin/vout arrays, and a busy chipnet test wallet had thousands. Measured on the real file: parse 59 ms, stringify 39 ms. So one storage.get blocked the UI for ~60 ms, one set for ~99 ms, and fullState() — which reads the store seven times over selectedWalletId, walletRoles, walletEntries, snapshotForSelected and the server list — cost ~420 ms. emitState() runs on every adapter change, across nine wallets, so the main thread was never given back. Three changes: - txCache is capped at 400 entries, pruned newest-first by block time (unconfirmed entries sort as newest — they are the current ones). Worst case ~0.3 MB per wallet instead of unbounded. - TX_CACHE_VERSION 3, so existing oversized caches are discarded on first load rather than needing a manual clear. - loadHistory() only persists when something actually changed; an idle wallet was rewriting the whole file on every poll for nothing. - api.storage gains a write-through read cache, so repeated gets cost one parse per process instead of one per call. Writes still go to the host unchanged. Safe because this process is the only writer — Aegis's panel talks over addon messages and never touches addon storage; if that changes, the cache has to go.
2026-10-03 10:56:59 +02:00
},
all: () => ({ ...load() }),
};
try {
api.storage = cached;
return api.storage === cached;
} catch { return false; }
}
// Aegis starts on first use (addon.json "activation": "on-demand"). Two
// things only work while it runs, so while either is on it asks the host to
// start it at launch: a live WizardConnect pairing (nobody else listens on
// the relays for the dapp's sign requests) and "stay unlocked" (that
// auto-unlock also opens Settings › Passwords). Pairings of a removed wallet
// stay in storage, so only wallets still in the list count.
function needsLaunchStart(api) {
const ids = new Set((readWallets(api) || []).map((w) => w.id));
const all = api.storage.all ? api.storage.all() : {};
for (const [k, v] of Object.entries(all)) {
const m = /^wc\/(.+)\/uris$/.exec(k);
if (m && ids.has(m[1]) && Array.isArray(v) && v.length) return true;
}
const cfg = api.storage.get("aegis/session/cfg", null);
const blob = api.storage.get("aegis/session/enc", null);
return !!(cfg && cfg.lockOnClose === false && blob && blob.encPwB64);
}
let launchStartAsked = null;
function syncLaunchStart(api) {
if (typeof api.startAtLaunch !== "function") return; // host predates on-demand
const on = needsLaunchStart(api);
if (on === launchStartAsked) return;
launchStartAsked = on;
try { api.startAtLaunch(on); } catch (e) { api.log("startAtLaunch:", e?.message || e); }
}
const LAUNCH_START_KEYS = /^(wallets$|wc\/|aegis\/session\/)/;
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 readWallets(api) {
const raw = api.storage.get("wallets", null);
return Array.isArray(raw) ? raw : null;
}
function writeWallets(api, list) { api.storage.set("wallets", list); }
// Bring pre-multi-wallet storage forward: create the legacy BCH default entry
// and rehome its receiveCursor / txCache under the new per-wallet subkey.
function migrateLegacyStorage(api) {
if (readWallets(api)) return; // already multi-wallet
const legacyAccountPath = String(api.storage.get("accountPath", "") || "").trim() || "m/44'/145'/0'";
const wallets = [{
id: LEGACY_BCH_WALLET_ID,
label: "BCH — main",
chain: "bch",
network: "mainnet",
purpose: LEGACY_BCH_PURPOSE,
accountPath: legacyAccountPath,
isDefault: true,
isLegacy: true,
createdAt: 0,
}];
writeWallets(api, wallets);
api.storage.set("selectedWalletId", LEGACY_BCH_WALLET_ID);
// Move per-wallet state under the scoped prefix used by chain-bch.js.
const prefix = `wallets/${LEGACY_BCH_WALLET_ID}/`;
for (const legacyKey of ["receiveCursor", "txCache"]) {
const v = api.storage.get(legacyKey, null);
if (v !== null && api.storage.get(prefix + legacyKey, null) === null) {
api.storage.set(prefix + legacyKey, v);
}
}
api.log("migrated legacy BCH wallet into multi-wallet layout");
}
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
// The lowest index not in use. It used to be highest + 1, so removing a
// wallet retired its index for good: the keys were still derivable from the
// vault, but nothing in the UI could ever produce that wallet again, and
// whatever was on it was out of reach. Now "Add wallet" after a removal
// brings the same wallet (same keys, same address) back.
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 nextIndex(wallets, meta) {
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
const used = new Set();
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
for (const w of wallets) {
if (chainKey(w.chain, w.network) !== chainKey(meta.chain, meta.network)) continue;
if (w.isLegacy) continue;
const m = /\/(\d+)$/.exec(w.purpose || "");
const n = m ? Number(m[1]) : NaN;
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
if (Number.isFinite(n)) used.add(n);
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: 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
let i = meta.startIndex;
while (used.has(i)) i++;
return i;
}
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 makeWalletId(meta, index) {
const n = String(meta.network).replace(/[^a-z0-9]/gi, "");
return `${meta.chain}-${n}-${index}`;
}
function autoLabel(meta, wallets) {
const same = wallets.filter((w) => chainKey(w.chain, w.network) === chainKey(meta.chain, meta.network));
if (!same.length) return meta.short;
return `${meta.short} #${same.length + 1}`;
}
// ---- runtime wallet map -----------------------------------------------------
// A "runtime" is a mounted wallet: its adapter instance plus phase + error.
// activate() derives all of them in parallel once the vault unlocks.
async function mountAllWallets() {
const c = ctx;
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 walletList = readWallets(c.api) || [];
for (const w of walletList) {
if (!c.runtimes.has(w.id)) c.runtimes.set(w.id, { entry: w, phase: "locked", error: null, adapter: null });
}
emitState();
await Promise.all(walletList.map((w) => mountWallet(w)));
}
// An import stores the FULL leaf path the user typed (m/44'/1'/0'/0/0) in
// entry.accountPath, because that is what derived the single address the
// strip shows. WizardConnect wants the BIP44 *account* node instead — it
// appends the branch (receive/change/defi) and the address index itself.
// Handing it the leaf made it derive m/44'/1'/0'/0/0/<branch>/<i>: a tree
// the user holds no keys in, so pairing looked healthy and then every sign
// request failed with "no path for input". The account level is the last
// hardened element of the path.
function wcAccountPath(path) {
const p = String(path || "").trim();
if (!/^m(\/\d+'?)+$/.test(p)) return null;
const parts = p.split("/");
let last = -1;
for (let i = 1; i < parts.length; i++) if (parts[i].endsWith("'")) last = i;
if (last < 1) return null;
return parts.slice(0, last + 1).join("/");
}
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
async function mountWallet(entry) {
const c = ctx;
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 rt = c.runtimes.get(entry.id) || { entry, phase: "locked", error: null, adapter: null };
rt.entry = entry;
rt.phase = "locked"; rt.error = null;
c.runtimes.set(entry.id, rt);
emitState();
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
// Imported wallets skip the vault-derive/HKDF path entirely: their signer
// material is in wallet-imports.enc, and the runtime here is read-only so
// it doesn't need the material until spend support ships (M.1b).
if (entry.kind === "imported") {
try {
let adapter;
const commonOpts = {
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
walletId: entry.id, storage: c.api.storage,
log: (...a) => c.api.log(`[${entry.id}]`, ...a),
onChange: () => emitStateForWallet(entry.id),
network: entry.network,
};
if (entry.chain === "bch") {
fix(aegis): 0.14.0 — a seed import is an HD wallet, not one address Importing a Bitcoin.com seed showed a balance of zero. Two causes, one mine. Mine: a Copay / Bitcoin.com backup QR is "1|<words>|<network>|<ACCOUNT path>|…", and 0.13.2 dropped that path straight into a box whose contents are derived as a LEAF. Deriving the account node itself yields an address the wallet has never used — for the standard BIP39 vector, qr96x72dpwrjmg8gtmfemmdhn6u8aqdgnvn4fp2906 instead of qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6. An address with no history, so: zero. An account path now extends to /0/0 instead of being derived in place. The deeper one: a seed import mounted on the single-address adapter, which watches exactly one scripthash. Bitcoin.com, Electron Cash and the rest spread funds across a whole BIP44 account, so even with the right leaf the balance only shows if it all happens to sit on the first receive address. A seed import is an HD wallet and now mounts as one — the same BchWallet the vault-derived wallets use, whose WalletKeys walks receive AND change to a gap limit of 20. That is what actually finds the money, and it brings real spend support to seed imports as a side effect. WIF imports are unchanged: one key is one address, nothing to scan. Existing seed imports are picked up without re-importing. accountPath is stored in either shape — older imports kept the full leaf, the QR carries the account — and the mount trims both to the last hardened element before handing it to WalletKeys. The stale display address stored at import time is irrelevant, since the panel reads the address off the adapter's snapshot. Mounting now reads the signer up front to decide which adapter to use. A locked vault still mounts watch-only from the stored address rather than failing, and the WC manager gets its own copy of the root because it keeps a live reference for the per-URI relay-identity HKDF.
2026-09-28 22:29:19 +02:00
// A seed import IS an HD wallet. Bitcoin.com, Electron Cash and the
// rest spread funds across a whole BIP44 account, so watching the one
// address a leaf path happens to derive reports 0 for a wallet that
// plainly has money in it. Mount the seed on the same adapter the
// vault-derived wallets use — WalletKeys walks receive AND change to
// a gap limit of 20, which is what actually finds the balance, and it
// brings real spend support with it.
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
//
fix(aegis): 0.14.0 — a seed import is an HD wallet, not one address Importing a Bitcoin.com seed showed a balance of zero. Two causes, one mine. Mine: a Copay / Bitcoin.com backup QR is "1|<words>|<network>|<ACCOUNT path>|…", and 0.13.2 dropped that path straight into a box whose contents are derived as a LEAF. Deriving the account node itself yields an address the wallet has never used — for the standard BIP39 vector, qr96x72dpwrjmg8gtmfemmdhn6u8aqdgnvn4fp2906 instead of qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6. An address with no history, so: zero. An account path now extends to /0/0 instead of being derived in place. The deeper one: a seed import mounted on the single-address adapter, which watches exactly one scripthash. Bitcoin.com, Electron Cash and the rest spread funds across a whole BIP44 account, so even with the right leaf the balance only shows if it all happens to sit on the first receive address. A seed import is an HD wallet and now mounts as one — the same BchWallet the vault-derived wallets use, whose WalletKeys walks receive AND change to a gap limit of 20. That is what actually finds the money, and it brings real spend support to seed imports as a side effect. WIF imports are unchanged: one key is one address, nothing to scan. Existing seed imports are picked up without re-importing. accountPath is stored in either shape — older imports kept the full leaf, the QR carries the account — and the mount trims both to the last hardened element before handing it to WalletKeys. The stale display address stored at import time is irrelevant, since the panel reads the address off the adapter's snapshot. Mounting now reads the signer up front to decide which adapter to use. A locked vault still mounts watch-only from the stored address rather than failing, and the WC manager gets its own copy of the root because it keeps a live reference for the per-URI relay-identity HKDF.
2026-09-28 22:29:19 +02:00
// WIF imports stay on the single-address adapter: one key is one
// address, and there is nothing to scan.
//
// Reading the signer needs the vault unlocked. When it is locked we
// fall back to the watch-only adapter so balances still render from
// the stored address instead of the wallet failing to mount at all.
let seedRoot = null;
try {
const blob = await c.api.vault.imports.signer(entry.importId);
if (ctx !== c) return;
if (blob && blob.kind === "seed" && blob.seed) {
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 seedHex = String(blob.seed).trim();
fix(aegis): 0.14.0 — a seed import is an HD wallet, not one address Importing a Bitcoin.com seed showed a balance of zero. Two causes, one mine. Mine: a Copay / Bitcoin.com backup QR is "1|<words>|<network>|<ACCOUNT path>|…", and 0.13.2 dropped that path straight into a box whose contents are derived as a LEAF. Deriving the account node itself yields an address the wallet has never used — for the standard BIP39 vector, qr96x72dpwrjmg8gtmfemmdhn6u8aqdgnvn4fp2906 instead of qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6. An address with no history, so: zero. An account path now extends to /0/0 instead of being derived in place. The deeper one: a seed import mounted on the single-address adapter, which watches exactly one scripthash. Bitcoin.com, Electron Cash and the rest spread funds across a whole BIP44 account, so even with the right leaf the balance only shows if it all happens to sit on the first receive address. A seed import is an HD wallet and now mounts as one — the same BchWallet the vault-derived wallets use, whose WalletKeys walks receive AND change to a gap limit of 20. That is what actually finds the money, and it brings real spend support to seed imports as a side effect. WIF imports are unchanged: one key is one address, nothing to scan. Existing seed imports are picked up without re-importing. accountPath is stored in either shape — older imports kept the full leaf, the QR carries the account — and the mount trims both to the last hardened element before handing it to WalletKeys. The stale display address stored at import time is irrelevant, since the panel reads the address off the adapter's snapshot. Mounting now reads the signer up front to decide which adapter to use. A locked vault still mounts watch-only from the stored address rather than failing, and the WC manager gets its own copy of the root because it keeps a live reference for the per-URI relay-identity HKDF.
2026-09-28 22:29:19 +02:00
if (/^[0-9a-f]+$/i.test(seedHex) && seedHex.length >= 32) {
seedRoot = new Uint8Array(seedHex.match(/../g).map((x) => parseInt(x, 16)));
} else {
wcIneligible.set(entry.id, {
short: "unsupported key",
detail: "Imported seed material is not in a form WizardConnect can derive from.",
});
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
}
fix(aegis): 0.14.0 — a seed import is an HD wallet, not one address Importing a Bitcoin.com seed showed a balance of zero. Two causes, one mine. Mine: a Copay / Bitcoin.com backup QR is "1|<words>|<network>|<ACCOUNT path>|…", and 0.13.2 dropped that path straight into a box whose contents are derived as a LEAF. Deriving the account node itself yields an address the wallet has never used — for the standard BIP39 vector, qr96x72dpwrjmg8gtmfemmdhn6u8aqdgnvn4fp2906 instead of qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6. An address with no history, so: zero. An account path now extends to /0/0 instead of being derived in place. The deeper one: a seed import mounted on the single-address adapter, which watches exactly one scripthash. Bitcoin.com, Electron Cash and the rest spread funds across a whole BIP44 account, so even with the right leaf the balance only shows if it all happens to sit on the first receive address. A seed import is an HD wallet and now mounts as one — the same BchWallet the vault-derived wallets use, whose WalletKeys walks receive AND change to a gap limit of 20. That is what actually finds the money, and it brings real spend support to seed imports as a side effect. WIF imports are unchanged: one key is one address, nothing to scan. Existing seed imports are picked up without re-importing. accountPath is stored in either shape — older imports kept the full leaf, the QR carries the account — and the mount trims both to the last hardened element before handing it to WalletKeys. The stale display address stored at import time is irrelevant, since the panel reads the address off the adapter's snapshot. Mounting now reads the signer up front to decide which adapter to use. A locked vault still mounts watch-only from the stored address rather than failing, and the WC manager gets its own copy of the root because it keeps a live reference for the per-URI relay-identity HKDF.
2026-09-28 22:29:19 +02:00
} else {
wcIneligible.set(entry.id, {
short: "WIF import",
detail: "Wallets imported from a single private key (WIF) can't pair. WizardConnect hands the dapp an xpub so it can derive addresses on its own, and a lone private key carries no chain code to build one from. Open this wallet's ⋯ menu and choose \"Promote to HD wallet\" — Aegis derives a proper wallet from your vault and sweeps this key into it.",
});
}
} catch (e) {
c.api.log(`[${entry.id}] signer unavailable:`, e?.message || e);
wcIneligible.set(entry.id, wcErrReason(e));
}
const bchServers = entry.network === "mainnet" ? bchServerList(c.api) : undefined;
if (seedRoot) {
// entry.accountPath arrives in either shape: imports used to store
// the full leaf the displayed address came from
// (m/44'/145'/0'/0/0), while a Copay / Bitcoin.com backup QR
// carries the ACCOUNT path. Trim both to the account — the last
// hardened element — so WalletKeys derives the branches the wallet
// actually used rather than a tree hanging off a leaf.
adapter = new c.d.bchAdapter.BchWallet(seedRoot, {
...commonOpts,
servers: bchServers,
accountPath: wcAccountPath(entry.accountPath) || undefined,
});
} else {
adapter = new c.d.importedBchAdapter.ImportedBchWallet({
...commonOpts,
cashaddr: entry.importedCashaddr || entry.importedAddress,
servers: bchServers,
importId: entry.importId,
});
adapter.schedulePoll(20_000);
}
// Imported BCH wallets were never registered with WizardConnect —
// startForWallet only ran in the vault-derived branch, so they showed
// up in the "Sign with" picker and then failed on pair with "no
// manager". Register the seed-backed ones here too.
if (c.wc && seedRoot) {
c.wc.startForWallet({
walletId: entry.id, label: entry.label,
// Its own copy: the WC adapter keeps a live reference for the
// per-URI relay-identity HKDF, so it must not share the buffer
// we are about to wipe.
root32: new Uint8Array(seedRoot),
accountPath: wcAccountPath(entry.accountPath) || adapter.snapshot?.()?.accountPath || "m/44'/145'/0'",
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
}).catch((e) => {
c.api.log(`[${entry.id}] wc start (imported):`, e?.message || e);
wcIneligible.set(entry.id, wcErrReason(e));
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
emitStateForWallet(entry.id);
});
}
fix(aegis): 0.14.0 — a seed import is an HD wallet, not one address Importing a Bitcoin.com seed showed a balance of zero. Two causes, one mine. Mine: a Copay / Bitcoin.com backup QR is "1|<words>|<network>|<ACCOUNT path>|…", and 0.13.2 dropped that path straight into a box whose contents are derived as a LEAF. Deriving the account node itself yields an address the wallet has never used — for the standard BIP39 vector, qr96x72dpwrjmg8gtmfemmdhn6u8aqdgnvn4fp2906 instead of qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6. An address with no history, so: zero. An account path now extends to /0/0 instead of being derived in place. The deeper one: a seed import mounted on the single-address adapter, which watches exactly one scripthash. Bitcoin.com, Electron Cash and the rest spread funds across a whole BIP44 account, so even with the right leaf the balance only shows if it all happens to sit on the first receive address. A seed import is an HD wallet and now mounts as one — the same BchWallet the vault-derived wallets use, whose WalletKeys walks receive AND change to a gap limit of 20. That is what actually finds the money, and it brings real spend support to seed imports as a side effect. WIF imports are unchanged: one key is one address, nothing to scan. Existing seed imports are picked up without re-importing. accountPath is stored in either shape — older imports kept the full leaf, the QR carries the account — and the mount trims both to the last hardened element before handing it to WalletKeys. The stale display address stored at import time is irrelevant, since the panel reads the address off the adapter's snapshot. Mounting now reads the signer up front to decide which adapter to use. A locked vault still mounts watch-only from the stored address rather than failing, and the WC manager gets its own copy of the root because it keeps a live reference for the per-URI relay-identity HKDF.
2026-09-28 22:29:19 +02:00
// BchWallet copied the root and WalletKeys already derived from it,
// so the local buffer has no further use.
if (seedRoot) { try { seedRoot.fill(0); } catch {} }
} else if (entry.chain === "btc" || entry.chain === "dgb") {
adapter = new c.d.utxoImportedAdapter.UtxoImportedWallet({
...commonOpts, chain: entry.chain, address: entry.importedAddress,
});
adapter.schedulePoll(20_000);
} else if (entry.chain === "eth" || entry.chain === "trx" || entry.chain === "sol") {
adapter = new c.d.genericImportedAdapter.GenericImportedWallet({
...commonOpts, chain: entry.chain, address: entry.importedAddress,
// Only a real custom URL overrides the network default. The old
// String(x || undefined) turned an unset override into the text
// "undefined", which is truthy — so every imported TRX/ETH/SOL wallet
// without a custom RPC fetched "undefined/v1/accounts/…" and showed
// "undefined" as its server.
rpcUrl: String(c.api.storage.get(`wallets/${entry.id}/rpcUrl`, "") || "").trim() || undefined,
});
adapter.schedulePoll(20_000);
} else if (entry.chain === "sc") {
// Sia's SiaWallet needs the 32-byte root at mount time — its key
// tree derives eagerly. Fetch the signer material from the vault
// (this branch runs only while the vault is unlocked; a locked
// vault would surface at import-time and gate the flow there).
// Falls back to a read-only stub if the fetch fails so a stray
// locked mount doesn't break panel rendering.
if (!c.api.vault?.imports || typeof c.api.vault.imports.signer !== "function") {
throw new Error("vault.imports.signer unavailable — cannot mount Sia import");
}
const signerBlob = await c.api.vault.imports.signer(entry.importId);
if (!signerBlob || signerBlob.kind !== "seed" || !signerBlob.seed) {
throw new Error("Sia signer material missing or malformed");
}
const seedHex = String(signerBlob.seed).trim();
if (!/^[0-9a-f]{64}$/i.test(seedHex)) throw new Error("Sia seed must be 32 bytes");
const rootBytes = new Uint8Array(seedHex.match(/../g).map((x) => parseInt(x, 16)));
const walletdUrl = String(c.api.storage.get(`wallets/${entry.id}/walletdUrl`, "") || "");
adapter = new c.d.siaAdapter.SiaWallet(rootBytes, {
walletId: entry.id,
storage: c.api.storage,
log: (...a) => c.api.log(`[${entry.id}]`, ...a),
onChange: () => emitStateForWallet(entry.id),
walletdUrl,
});
if (walletdUrl && typeof adapter.startPolling === "function") adapter.startPolling();
} else {
throw new Error(`no imported adapter for chain "${entry.chain}"`);
}
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
rt.adapter = adapter; rt.phase = "ready";
adapter.refresh(true).catch((e) => c.api.log(`[${entry.id}] initial refresh:`, e?.message || e));
emitStateForWallet(entry.id);
} catch (e) {
rt.phase = "error"; rt.error = e?.message || String(e);
emitState();
}
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
let root;
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
root = await c.api.vault.derive(entry.purpose);
} catch (e) {
const msg = e?.message || String(e);
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
rt.phase = /not set up/i.test(msg) ? "nosetup" : "error";
rt.error = msg;
emitState();
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
if (ctx !== c) return;
try {
let adapter;
if (entry.chain === "bch") {
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
// Mainnet still honors the user-set custom electrum list; chipnet uses
// adapter-embedded defaults (no per-network custom list in this rev).
const servers = entry.network === "mainnet" ? bchServerList(c.api) : undefined;
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
adapter = new c.d.bchAdapter.BchWallet(root, {
walletId: entry.id,
storage: c.api.storage,
log: (...a) => c.api.log(`[${entry.id}]`, ...a),
onChange: () => emitStateForWallet(entry.id),
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
network: entry.network,
servers,
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
accountPath: entry.accountPath,
});
} else if (entry.chain === "trx") {
adapter = new c.d.tronAdapter.TronWallet(root, entry.network, {
storage: c.api.storage,
log: (...a) => c.api.log(`[${entry.id}]`, ...a),
onChange: () => emitStateForWallet(entry.id),
});
adapter.schedulePoll(20_000);
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 (entry.chain === "sc") {
// walletdUrl is per-Sia-wallet (each sub-account may point at a
// different node) and stored under the scoped wallets/<id>/walletdUrl
// key. Empty = the panel shows a "point me at walletd" gate.
const walletdUrl = String(c.api.storage.get(`wallets/${entry.id}/walletdUrl`, "") || "");
adapter = new c.d.siaAdapter.SiaWallet(root, {
walletId: entry.id,
storage: c.api.storage,
log: (...a) => c.api.log(`[${entry.id}]`, ...a),
onChange: () => emitStateForWallet(entry.id),
walletdUrl,
});
if (walletdUrl) adapter.startPolling();
} else if (entry.chain === "dgb") {
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 (!c.d.dgbAdapter) throw new Error("DGB unavailable (bundled ESM couldn't resolve peer deps)");
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
adapter = new c.d.dgbAdapter.DgbWallet(root, {
walletId: entry.id,
storage: c.api.storage,
log: (...a) => c.api.log(`[${entry.id}]`, ...a),
onChange: () => emitStateForWallet(entry.id),
accountPath: entry.accountPath,
});
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 (entry.chain === "eth") {
const rpcUrl = String(c.api.storage.get(`wallets/${entry.id}/rpcUrl`, "") || "");
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
// Custom EIP-3085 chains resolve their config from storage rather than
// the built-in NETWORKS map.
const customNetwork = customEthNetworkEntry(c.api, entry.network);
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
adapter = new c.d.ethAdapter.EthWallet(root, entry.network, {
walletId: entry.id,
storage: c.api.storage,
log: (...a) => c.api.log(`[${entry.id}]`, ...a),
onChange: () => emitStateForWallet(entry.id),
rpcUrl,
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
customNetwork,
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
});
adapter.schedulePoll(20_000);
} else if (entry.chain === "sol") {
const rpcUrl = String(c.api.storage.get(`wallets/${entry.id}/rpcUrl`, "") || "");
adapter = new c.d.solAdapter.SolWallet(root, entry.network, {
walletId: entry.id,
storage: c.api.storage,
log: (...a) => c.api.log(`[${entry.id}]`, ...a),
onChange: () => emitStateForWallet(entry.id),
rpcUrl,
});
adapter.schedulePoll(20_000);
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
} else if (entry.chain === "btc") {
adapter = new c.d.btcAdapter.BtcWallet(root, entry.network, {
walletId: entry.id,
storage: c.api.storage,
log: (...a) => c.api.log(`[${entry.id}]`, ...a),
onChange: () => emitStateForWallet(entry.id),
accountPath: entry.accountPath,
});
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 {
throw new Error(`unknown chain ${entry.chain}`);
}
rt.adapter = adapter;
rt.phase = "ready";
// Kick a first fetch. Errors here don't fail the mount — the panel shows
// them per-wallet via snapshot.error.
adapter.refresh(true).catch((e) => c.api.log(`[${entry.id}] initial refresh:`, e?.message || e));
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
if (entry.chain === "bch" && c.wc) {
c.wc.startForWallet({
walletId: entry.id, label: entry.label,
fix(aegis): 0.9.7 — chipnet WizardConnect advertised the wrong key tree A chipnet BCH wallet derives from m/44'/1'/0', but the WizardConnect registration fell back to a hardcoded m/44'/145'/0' whenever the entry had no explicit accountPath — which is the normal case for a wallet created through the UI. Mainnet's default happens to be that same literal, so only chipnet was affected. The consequence was worse than a failed pairing. Pairing SUCCEEDED, the handshake carried xpubs for an unrelated key tree, and the dapp then derived addresses this wallet does not own: wallet's real chipnet address : bchtest:qpezx8qkwpjd4e6pd5aang0ve6fctpjvg5ckp2lwu7 address from the WC xpub : bchtest:qzvpe3w9rqnszk6mntnef6zv6v94zmfvpc49qkxml4 So the dapp saw an empty stranger's wallet, and anything it built spent inputs the wallet could not match — signing would fail with "no path for input". Silent, and only reachable on testnet. Both registration sites (vault-derived and imported) now take the path from adapter.snapshot().accountPath, which is by construction the tree the wallet actually derives its addresses from. Two wrong turns worth recording. defaultAccountPathFor() takes the coin CONFIG object, not a chain string, so passing entry.chain returned null and would have stopped WizardConnect registering at all — strictly worse than the bug being fixed. chainMeta().defaultAccountPath was no better: BCH has no coinType in COINS, so it is null for every BCH network. The adapter is the only component that resolves this correctly, which is why it is now the source.
2026-09-24 04:56:12 +02:00
// Resolve the default from the wallet's OWN network. This used to
// fall back to a hardcoded m/44'/145'/0' (mainnet), so a chipnet
// wallet — which derives from m/44'/1'/0' — advertised xpubs for a
// completely different key tree. Pairing succeeded and then the
// dapp saw an unrelated, empty wallet; anything it built spent
// from addresses this wallet does not own, so signing failed with
// "no path for input". Silent, and only visible on testnet.
// A copy: `root` is zeroed in the finally below, and the adapter
// reads its root again for every new pairing's relay key. Sharing
// the buffer gave every pairing made after mount a relay key derived
// from 32 zero bytes, i.e. from the pairing URI alone.
root32: new Uint8Array(root), accountPath: entry.accountPath || adapter.snapshot?.()?.accountPath || "m/44'/145'/0'",
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
}).catch((e) => c.api.log(`[${entry.id}] wc start:`, e?.message || e));
}
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
emitStateForWallet(entry.id);
} catch (e) {
rt.phase = "error";
rt.error = e?.message || String(e);
emitState();
} finally {
// Wipe the root buffer — the adapter has already turned it into keys.
if (root) try { root.fill(0); } catch {}
}
}
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
// Create a vault-derived wallet for a coin+network and mount it. Lifted out
// of the addWallet handler so "promote to HD" can build its destination
// through exactly the same path the Add flow uses — purpose allocation, the
// legacy-purpose carry-over and id numbering all stay in one place rather
// than being reimplemented slightly differently next to a money transfer.
async function createVaultWallet({ chain, network, label }) {
const meta = chainMeta(chain, network);
if (!meta) throw new Error("unknown chain/network");
const list = walletEntries().slice();
const netMeta = COINS[chain]?.networks?.[network] || {};
const existingForCoin = list.filter((w) => w.chain === chain && w.network === network && !w.isLegacy);
let purpose, isLegacy = false;
if (netMeta.legacyFirstPurpose && existingForCoin.length === 0
&& !list.some((w) => w.purpose === netMeta.legacyFirstPurpose)) {
purpose = netMeta.legacyFirstPurpose;
isLegacy = true;
} else {
purpose = meta.purposePrefix + nextIndex(list, meta);
}
const id = makeWalletId(meta, nextIndex(list, meta));
if (list.some((w) => w.id === id || w.purpose === purpose)) throw new Error("duplicate wallet");
const entry = {
id, chain, network, purpose, isLegacy,
label: String(label || "").trim() || autoLabel(meta, list),
createdAt: Date.now(),
};
list.push(entry);
writeWallets(ctx.api, list);
ctx.api.storage.set("selectedWalletId", id);
ctx.runtimes.set(id, { entry, phase: "locked", error: null, adapter: null });
emitState();
await mountWallet(entry);
return entry;
}
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 unmountWallet(walletId) {
const rt = ctx.runtimes.get(walletId);
if (rt && rt.adapter) { try { rt.adapter.dispose(); } catch {} }
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 (ctx.wc) { try { ctx.wc.stopForWallet(walletId); } catch {} }
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
wcIneligible.delete(walletId);
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
ctx.runtimes.delete(walletId);
}
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
// ---- import derive helpers (client-side, cashaddr only) --------------------
// The pasted material never leaves this process — main-side stores it once
// via api.vault.imports.add. These helpers only turn (seed+path) or WIF into
// a P2PKH cashaddr, which is safe to send back to the panel.
fix(aegis): 0.14.0 — a seed import is an HD wallet, not one address Importing a Bitcoin.com seed showed a balance of zero. Two causes, one mine. Mine: a Copay / Bitcoin.com backup QR is "1|<words>|<network>|<ACCOUNT path>|…", and 0.13.2 dropped that path straight into a box whose contents are derived as a LEAF. Deriving the account node itself yields an address the wallet has never used — for the standard BIP39 vector, qr96x72dpwrjmg8gtmfemmdhn6u8aqdgnvn4fp2906 instead of qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6. An address with no history, so: zero. An account path now extends to /0/0 instead of being derived in place. The deeper one: a seed import mounted on the single-address adapter, which watches exactly one scripthash. Bitcoin.com, Electron Cash and the rest spread funds across a whole BIP44 account, so even with the right leaf the balance only shows if it all happens to sit on the first receive address. A seed import is an HD wallet and now mounts as one — the same BchWallet the vault-derived wallets use, whose WalletKeys walks receive AND change to a gap limit of 20. That is what actually finds the money, and it brings real spend support to seed imports as a side effect. WIF imports are unchanged: one key is one address, nothing to scan. Existing seed imports are picked up without re-importing. accountPath is stored in either shape — older imports kept the full leaf, the QR carries the account — and the mount trims both to the last hardened element before handing it to WalletKeys. The stale display address stored at import time is irrelevant, since the panel reads the address off the adapter's snapshot. Mounting now reads the signer up front to decide which adapter to use. A locked vault still mounts watch-only from the stored address rather than failing, and the WC manager gets its own copy of the root because it keeps a live reference for the per-URI relay-identity HKDF.
2026-09-28 22:29:19 +02:00
// The import form's path box can hold either shape: a full leaf
// (m/44'/145'/0'/0/0) or a BIP44 ACCOUNT path, which is what a Copay /
// Bitcoin.com backup QR carries. The address we display should be the
// wallet's first receive address either way, so an account path gets
// extended rather than derived as if it were a leaf — deriving the account
// node itself yields an address the wallet has never used, which is exactly
// how an imported wallet ends up showing a balance of zero.
function firstReceivePath(path) {
const p = String(path || "").trim();
return wcAccountPath(p) === p ? p + "/0/0" : p;
}
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 deriveCashaddrFromSeed(seedHex, path, prefix) {
const d = ctx.d;
const seed = new Uint8Array(seedHex.length / 2);
for (let i = 0; i < seed.length; i++) seed[i] = parseInt(seedHex.substr(i * 2, 2), 16);
const root = d.HDKey.fromMasterSeed(seed);
const node = root.derive(path);
const h160 = d.ripemd160(d.sha256(node.publicKey));
return d.cashaddr.encode(prefix, 0, h160);
}
function deriveCashaddrFromWif(wif, prefix) {
const d = ctx.d;
// WIF layout: base58check(networkByte || privkey32 || [compressionByte 0x01])
// Wrap decodeCheck so a malformed WIF (bad chars, bad checksum, or an
// internal library shape change) surfaces as a user-facing "invalid WIF"
// instead of leaking "TypeError: base58check.decode is not a function".
let raw;
try {
raw = d.base58check.decodeCheck(wif);
} catch (e) {
throw new Error("invalid WIF format (base58check decode failed)");
}
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 (raw.length !== 33 && raw.length !== 34) throw new Error(`bad WIF length ${raw.length}`);
// First byte is version (network); we allow any — BCH mainnet uses 0x80,
// testnet 0xEF. Both round-trip through the same address derivation below.
const priv = raw.slice(1, 33);
const compressed = raw.length === 34; // trailing 0x01 marker
const pub = d.secp256k1.getPublicKey(priv, compressed);
const h160 = d.ripemd160(d.sha256(pub));
return d.cashaddr.encode(prefix, 0, h160);
}
// Plain-text body + rows for the host overlay (it escapes everything, so
// markup would show up literally). Outputs are decoded best-effort from the
// libauth transaction the dapp sent: P2PKH / P2SH locking bytecode → cashaddr.
// The WC message nests the WcSignTransactionRequest under .transaction, so
// the libauth tx itself is request.transaction.transaction (see wc-sign.js).
function buildWcApproval(payload) {
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 req = payload?.request || {};
const tx = req.transaction || {};
// Who is asking is what Aegis recorded when the pairing was made: the page
// origin the host verified, or the panel when the user pasted the code.
// The dapp's userPrompt is its own free text, shown only as a quote — it
// used to be the overlay's origin and the PIN request's name, so a paired
// dapp could present itself as any site.
const paired = payload?.pairing && typeof payload.pairing.origin === "string" ? payload.pairing.origin : null;
const dappName = paired === "panel" ? "a dapp you paired in Aegis by pasting its code"
: paired ? paired
: "a dapp paired before Aegis recorded where pairings came from";
const says = plainLabel(tx.userPrompt || "", 160);
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 walletName = payload?.label || payload?.walletId || "";
const network = ctx.runtimes.get(payload?.walletId)?.entry?.network;
const prefix = network === "chipnet" ? "bchtest" : "bitcoincash";
let inner = tx.transaction;
if (typeof inner === "string") {
try {
const lib = ctx.d.libauth;
const dec = (lib.decodeTransactionCommon || lib.decodeTransaction)(ctx.d.tx.fromHex(inner));
inner = typeof dec === "string" ? null : dec;
} catch { inner = null; }
}
// Refuse what cannot be shown. The signer only takes this shape (see
// wc-sign.js), and an overlay without outputs or spent inputs is no
// approval at all.
if (!inner || !Array.isArray(inner.inputs) || !Array.isArray(inner.outputs) || !inner.outputs.length
|| !Array.isArray(tx.sourceOutputs) || tx.sourceOutputs.length !== inner.inputs.length) {
return { unreadable: true, dappName };
}
const inputCount = Array.isArray(req.inputPaths) ? req.inputPaths.length : (Array.isArray(inner?.inputs) ? inner.inputs.length : "?");
const rows = [{ label: "Wallet", value: walletName }, { label: "Inputs", value: String(inputCount) }];
if (says) rows.unshift({ label: "The dapp says", value: `“${says}”` });
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
// What the wallet is putting IN. The outputs alone never showed that a
// transaction spends the user's tokens — and with the token prefix now
// signed correctly (wc-sign.js) such a transaction is valid, so the
// tokens leaving must be on the overlay.
const hexOf = (v) => (typeof v === "string" ? v.toLowerCase() : Buffer.from(v || []).toString("hex"));
const tokenText = (t) => {
if (!t) return "";
const cat = hexOf(t.category);
let amt = "0";
try { amt = BigInt(t.amount ?? 0).toString(); } catch {}
const nft = t.nft ? ` + NFT (${String(t.nft.capability || "none")})` : "";
return ` + token ${cat.slice(0, 8)}…${cat.slice(-4)}: ${amt} units${nft}`;
};
try {
const srcs = Array.isArray(tx.sourceOutputs) ? tx.sourceOutputs : [];
if (srcs.length) {
let inTotal = 0n;
const tokenLines = [];
srcs.forEach((o, i) => {
try { inTotal += BigInt(o.valueSatoshis ?? 0); } catch {}
if (o.token) tokenLines.push(`input #${i + 1}${tokenText(o.token)}`);
});
rows.push({ label: "Spending", value: `${fmtBch(Number(inTotal))} BCH from ${srcs.length} input${srcs.length === 1 ? "" : "s"}` });
if (tokenLines.length) rows.push({ label: "Tokens spent", value: tokenLines.slice(0, 8).join("\n") + (tokenLines.length > 8 ? `\n… and ${tokenLines.length - 8} more` : ""), mono: true, strong: true });
}
} catch {}
try {
const outs = inner?.outputs || [];
let total = 0n;
for (const o of outs) { try { total += BigInt(o.valueSatoshis ?? 0); } catch {} }
outs.slice(0, 8).forEach((o, i) => {
const lb = o.lockingBytecode;
const hexScript = typeof lb === "string" ? lb.toLowerCase() : Buffer.from(lb || []).toString("hex");
let addr = null;
if (/^76a914[0-9a-f]{40}88ac$/.test(hexScript)) addr = ctx.d.cashaddr.encode(prefix, 0, ctx.d.tx.fromHex(hexScript.slice(6, 46)));
else if (/^a914[0-9a-f]{40}87$/.test(hexScript)) addr = ctx.d.cashaddr.encode(prefix, 1, ctx.d.tx.fromHex(hexScript.slice(4, 44)));
const v = BigInt(o.valueSatoshis ?? 0);
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 token = tokenText(o.token);
rows.push({ label: `Output #${i + 1}`, value: `${fmtBch(Number(v))} BCH${token} → ${addr || (hexScript.startsWith("6a") ? "OP_RETURN data" : "script " + hexScript.slice(0, 24) + "…")}`, mono: true });
});
if (outs.length > 8) rows.push({ label: "Outputs", value: `… and ${outs.length - 8} more` });
if (outs.length) rows.push({ label: "Total out", value: `${fmtBch(Number(total))} BCH`, strong: true });
} catch {}
rows.push({ label: "After signing", value: tx.broadcast ? "the dapp broadcasts it" : "signed hex is returned to the dapp" });
rows.push({ label: "Sighash", value: "ALL | FORKID | UTXOS" });
return { body: `A site paired over WizardConnect asks you to sign a Bitcoin Cash transaction. Paired from: ${dappName}.`, rows, dappName, origin: paired && paired !== "panel" ? paired : "WizardConnect" };
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(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 default wallets -------------------------------------------
// The wallet you pay from and the wallet you hand to a dapp are not
// necessarily the same one, and until now both fell out of whatever happened
// to be selected in the panel. That is why a WizardConnect pairing could land
// on an address the user did not recognise: with the selected wallet
// ineligible, pairing silently took the first one in list order.
//
// A role points at a wallet id. An id whose wallet has since been removed
// reads back as null rather than being trusted, so a stale pointer can never
// quietly redirect a payment.
const WALLET_ROLES = ["payments", "wizardconnect"];
function walletRoles() {
const raw = ctx.api.storage.get("aegis/roles/v1", null);
const list = readWallets(ctx.api) || [];
const out = {};
for (const role of WALLET_ROLES) {
const id = raw && typeof raw === "object" ? String(raw[role] || "") : "";
out[role] = id && list.some((w) => w.id === id) ? id : null;
}
return out;
}
function setWalletRole(role, walletId) {
if (!WALLET_ROLES.includes(role)) throw new Error("unknown role: " + role);
const next = walletRoles();
if (walletId == null || walletId === "") next[role] = null;
else {
const list = readWallets(ctx.api) || [];
if (!list.some((w) => w.id === walletId)) throw new Error("no such wallet");
next[role] = String(walletId);
}
ctx.api.storage.set("aegis/roles/v1", next);
return next;
}
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
// ---- state / snapshot -------------------------------------------------------
function selectedWalletId() {
const list = readWallets(ctx.api) || [];
if (!list.length) return null;
const saved = String(ctx.api.storage.get("selectedWalletId", "") || "");
if (saved && list.some((w) => w.id === saved)) return saved;
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
// Nothing picked yet this session. The wallet the user nominated for
// payments is a better opening guess than list order.
const pay = walletRoles().payments;
if (pay) return pay;
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 dflt = list.find((w) => w.isDefault) || list[0];
return dflt.id;
}
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 walletEntries() { return readWallets(ctx.api) || []; }
function overallPhase() {
// If ANY wallet is nosetup, treat the whole addon as nosetup — the user
// hasn't unlocked / set up the vault, so no wallet can work.
const runtimes = [...ctx.runtimes.values()];
if (!runtimes.length) return "locked";
if (runtimes.some((r) => r.phase === "nosetup")) return "nosetup";
if (runtimes.some((r) => r.phase === "locked")) return "locked";
return "ready";
}
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
// Per-wallet reason a BCH wallet can't do WizardConnect even though it's
// mounted and ready — today that's single-key (WIF) imports, which have no
// seed to derive a per-dapp key tree from. Surfaced in walletSummary so the
// picker can grey them out instead of offering a pairing that must fail.
const wcIneligible = new Map();
function wcErrReason(e) {
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 m = e?.message || String(e);
return /locked|vault/i.test(m)
? { short: "vault locked", detail: "Unlock the password vault to use WizardConnect with this wallet." }
: { short: "unavailable", detail: m };
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
}
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 walletSummary(w) {
const meta = chainMeta(w.chain, w.network);
const rt = ctx.runtimes.get(w.id);
const snap = rt && rt.adapter ? rt.adapter.snapshot() : null;
return {
id: w.id, label: w.label, chain: w.chain, network: w.network, isDefault: !!w.isDefault, isLegacy: !!w.isLegacy,
kind: w.kind || 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
logo: meta?.logo || null, color: meta?.color || "#888",
coinLabel: meta?.coinLabel || w.chain, networkLabel: meta?.networkLabel || w.network, testnet: !!meta?.testnet,
ticker: meta?.ticker || "?", short: meta?.short || w.chain, decimals: meta?.decimals || 8,
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
address: snap?.address || null,
// Prefer the wallet-registry's stored accountPath; fall back to whatever
// the runtime derived (default when the user hasn't overridden). Nulls
// stay null so the picker knows whether to render the mono path line.
accountPath: w.accountPath || snap?.accountPath || 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
balance: snap?.balance || { confirmed: 0, unconfirmed: 0 },
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
// Per-wallet assets, so the coin drilldown can show what each ADDRESS
// holds instead of only the selected wallet's. `tokens` is the account-
// model shape (SPL / TRC20); `tokenBalances` is BCH CashTokens, keyed
// by category. Both stay null/empty for chains that have neither.
tokens: Array.isArray(snap?.tokens) ? snap.tokens : [],
tokenBalances: snap?.tokenBalances || 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
phase: rt?.phase || "locked",
error: rt?.error || null,
wcBlocked: w.chain === "bch" ? (wcIneligible.get(w.id)?.detail || null) : null,
wcBlockedShort: w.chain === "bch" ? (wcIneligible.get(w.id)?.short || null) : 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 snapshotForSelected() {
const id = selectedWalletId();
if (!id) return { phase: "empty" };
const rt = ctx.runtimes.get(id);
const entry = walletEntries().find((w) => w.id === id);
const meta = entry ? chainMeta(entry.chain, entry.network) : null;
const base = {
walletId: id,
label: entry?.label,
chain: entry?.chain,
network: entry?.network,
isLegacy: !!entry?.isLegacy,
kind: entry?.kind || 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
meta: meta ? {
logo: meta.logo, color: meta.color, short: meta.short, ticker: meta.ticker, decimals: meta.decimals,
coinLabel: meta.coinLabel, networkLabel: meta.networkLabel, testnet: meta.testnet,
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
addressFamilies: meta.addressFamilies, defaultAccountPath: meta.defaultAccountPath,
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
} : 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
supportsMessageSign: !!meta?.supportsMessageSign,
phase: rt?.phase || "locked",
error: rt?.error || null,
};
if (rt && rt.adapter) Object.assign(base, rt.adapter.snapshot());
return base;
}
function fullState() {
return {
overallPhase: overallPhase(),
selectedWalletId: selectedWalletId(),
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: walletRoles(),
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
wallets: walletEntries().map(walletSummary),
selected: snapshotForSelected(),
bchServers: {
list: bchServerList(ctx.api),
custom: Array.isArray(ctx.api.storage.get("servers", 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
coins: coinsForPanel(),
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
prices: ctx.priceFeed ? ctx.priceFeed.snapshot() : { enabled: false, prices: {} },
wc: ctx.wc ? ctx.wc.snapshot() : {},
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 emitState() { try { ctx.api.emit("state", fullState()); } catch {} }
function emitStateForWallet(id) {
// Any wallet change fans out to the panel with the full state so the
// wallet list balances update in the header too.
if (!ctx || !ctx.runtimes.has(id)) return;
emitState();
}
// ---- panel messages ---------------------------------------------------------
function requireWallet(id) {
const rt = ctx.runtimes.get(id);
if (!rt || rt.phase !== "ready" || !rt.adapter) throw new Error("wallet is not ready (vault locked?)");
return rt;
}
function requireSelected() { return requireWallet(selectedWalletId()); }
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
// ---- PIN: sealed blob + host-verified transaction clearance ---------------
// The PIN blob is the vault master password wrapped under a 6-digit PIN. In
// plain add-on storage that made the master password exactly as strong as a
// million PBKDF2 guesses to anyone holding a copy of the profile (a backup,
// another machine, a disk image) — the panel's lockout counter never sees an
// offline guess. It is therefore sealed with the OS keystore (DPAPI /
// Keychain / libsecret) before it touches disk, the same as Theseus's own
// vault PIN: a copied file is useless without this OS account.
//
// There is no unsealed fallback any more: without a real OS keystore a PIN is
// not stored at all (pinSet refuses) and the master password is asked for
// instead. On Linux, safeStorage reports "available" even on its basic_text
// backend, whose key is a constant compiled into Chromium — that counts as
// unsealed. A plain blob from an older build is sealed the first time it is
// read, and the fact that it once sat on disk in the clear is remembered so
// the panel can tell the user to change the master password (see
// PIN_EXPOSED_KEY); where it cannot be sealed it is deleted.
//
// The OS seal does not stop someone running as this OS user, nor a disk
// image plus the Windows password. Where a TPM is available the PIN is
// therefore also the authorization value of a TPM key (lib/tpm-pin.js), and
// the chip's own lockout limits guesses to ~144 a day however the blob is
// obtained. See pinWrap.
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 PIN_BLOB_KEY = "aegis/pin/v1";
const PIN_EXPOSED_KEY = "aegis/pin/legacyExposure";
const tpmPin = require("./lib/tpm-pin.js");
function osSealUsable(ss) {
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 {
if (!ss || !ss.isEncryptionAvailable()) return false;
if (process.platform === "linux") {
const backend = typeof ss.getSelectedStorageBackend === "function" ? ss.getSelectedStorageBackend() : "unknown";
if (backend === "basic_text" || backend === "unknown") return 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
}
return true;
} catch { return false; }
}
// → the sealed record, or null when this system has no keystore worth the name.
function sealPinBlob(api, blob) {
const ss = safeStorageOr(api);
if (!osSealUsable(ss)) return null;
try { return { v: 2, sealed: ss.encryptString(JSON.stringify(blob)).toString("base64") }; }
catch { return null; }
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 openPinBlob(api) {
const b = api.storage.get(PIN_BLOB_KEY, null);
if (!b || typeof b !== "object") return null;
if (b.sealed) {
const ss = safeStorageOr(api);
if (!ss) return null;
try {
const plain = JSON.parse(ss.decryptString(Buffer.from(String(b.sealed), "base64")));
return (plain && typeof plain === "object") ? plain : null;
} catch { return null; } // sealed under another OS account: the PIN is simply gone
}
// A plain blob from before 0.31: the master password sat on disk behind
// only a 6-digit PIN. Seal it (or drop it), and remember that it was ever
// there — copies of the profile made before now still hold it.
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 plain = { salt: b.salt, iv: b.iv, ct: b.ct, iters: b.iters };
if (!api.storage.get(PIN_EXPOSED_KEY, null)) api.storage.set(PIN_EXPOSED_KEY, { at: Date.now(), iters: Number(b.iters) || 0 });
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 sealed = sealPinBlob(api, plain);
if (sealed) { api.storage.set(PIN_BLOB_KEY, sealed); return plain; }
api.storage.set(PIN_BLOB_KEY, null);
return null;
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 PIN is checked here, never in the panel. The panel used to fetch the
// blob and decrypt it itself, then report its own failures — so the lockout
// counted only the guesses a well-behaved panel chose to report, and anything
// that could run in the panel could take the blob and search all million
// PINs offline. Now the blob stays in this process and every guess is
// counted before it is tried. PIN_MAX_FAILS wrong guesses lock the PIN for
// PIN_LOCKOUT_MS (the panel shows the same 15-minute lockout as before);
// every further wrong guess locks it again. A correct PIN or a master
// password the vault accepts clears the count.
// The blob format is unchanged: PBKDF2-SHA256(pin) -> AES-256-GCM, hex, tag
// appended to the ciphertext, as WebCrypto wrote it.
const PIN_ITERS = 600_000;
const PIN_MAX_FAILS = 5;
const PIN_LOCKOUT_MS = 15 * 60 * 1000;
const PIN_RE = /^\d{6}$/;
const nodeCrypto = require("node:crypto");
const pinKey = (pin, saltHex, iters) => new Promise((resolve, reject) =>
nodeCrypto.pbkdf2(String(pin), Buffer.from(saltHex, "hex"), iters, 32, "sha256", (e, k) => (e ? reject(e) : resolve(k))));
// With a TPM, `hw` is { keyName, wrapped, secret } from tpmPin.create and the
// AES key needs both the chip's secret and PBKDF2(pin) (tpmPin.mixKey); the
// blob records which TPM key to ask. Without one the blob is PBKDF2 only.
async function pinWrap(pin, masterPassword, hw = null) {
const salt = nodeCrypto.randomBytes(16).toString("hex");
const iv = nodeCrypto.randomBytes(12);
let key = await pinKey(pin, salt, PIN_ITERS);
if (hw) key = tpmPin.mixKey(hw.secret, key);
const c = nodeCrypto.createCipheriv("aes-256-gcm", key, iv);
const ct = Buffer.concat([c.update(String(masterPassword), "utf8"), c.final(), c.getAuthTag()]);
const out = { salt, iv: iv.toString("hex"), ct: ct.toString("hex"), iters: PIN_ITERS };
if (hw) out.hw = { kind: "tpm", key: hw.keyName, wrapped: hw.wrapped };
return out;
}
// `tpmSecret` is what the TPM released for this PIN (required when blob.hw).
async function pinUnwrapBlob(pin, blob, tpmSecret = null) {
const ct = Buffer.from(String(blob.ct), "hex");
let key = await pinKey(pin, blob.salt, Number(blob.iters) || PIN_ITERS);
if (blob.hw) {
if (!tpmSecret) throw new Error("TPM secret required");
key = tpmPin.mixKey(tpmSecret, key);
}
const d = nodeCrypto.createDecipheriv("aes-256-gcm", key, Buffer.from(String(blob.iv), "hex"));
d.setAuthTag(ct.subarray(ct.length - 16));
return Buffer.concat([d.update(ct.subarray(0, ct.length - 16)), d.final()]).toString("utf8");
}
// A TPM key for a new PIN, or null where there is none (not Windows, no TPM,
// PowerShell blocked by policy). Never throws: the PIN then works as before,
// and pinStatus says honestly that it is software-only.
let tpmUnavailableThisBoot = false;
async function tryTpmKey(api, pin) {
if (!tpmPin.supported() || tpmUnavailableThisBoot) return null;
try { return await tpmPin.create(pin); }
catch (e) { tpmUnavailableThisBoot = true; api.log("PIN: no TPM key:", e?.message || e); return null; }
}
function pinFails(api) {
const n = Number(api.storage.get("aegis/pin/failCount", 0)) || 0;
const last = Number(api.storage.get("aegis/pin/failLast", 0)) || 0;
const lockedMs = n >= PIN_MAX_FAILS ? Math.max(0, PIN_LOCKOUT_MS - (Date.now() - last)) : 0;
return { fails: n, last, lockedMs };
}
// A master password the vault accepted. Clears the PIN strikes, as a
// correct PIN does.
function noteMasterVerified(api) {
api.storage.set("aegis/pin/failCount", 0);
api.storage.set("aegis/pin/failLast", 0);
}
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
// "Ask for PIN on every transaction" used to be decided by the host and
// enforced by nobody: `send` never checked it, and a dapp-initiated
// transaction had no PIN step at all. A clearance is now a short-lived,
// single-use fact recorded only after the host itself has verified the
// master password the PIN unwraps (pinGateSatisfied). Panel sends consume
// one; dapp transactions ask the open panel for one and wait.
//
// A clearance belongs to the thing the PIN was entered for. It used to be
// one global window: any PIN proof (opening the wallet, a settings toggle)
// let the next dapp transaction from any site through, a waiting dapp could
// take the clearance the user had just made for their own send, and with
// two sites waiting the second one's request was lost. Now:
// - each waiting dapp transaction has its own id, and only a proof that
// names that id releases it;
// - a proof given for "transaction" in the panel clears the panel's next
// send only;
// - any other proof clears nothing.
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 TX_CLEARANCE_MS = 90_000;
const DAPP_PIN_WAIT_MS = 120_000;
let panelClearanceUntil = 0;
const pendingPinRequests = new Map(); // id -> { id, origin, what, at, cleared }
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 txPinOwed() {
try { return !!(ctx && ctx.pinNeeded && ctx.pinNeeded("transaction").needPin); }
catch { return true; } // the safe direction for a lock is closed
}
function requirePanelTxPin() {
if (!txPinOwed()) return;
if (Date.now() < panelClearanceUntil) { panelClearanceUntil = 0; return; }
throw new Error("PIN required — confirm your PIN to continue");
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 requireDappTxPin(origin, what) {
if (!txPinOwed()) return;
const req = { id: nodeCrypto.randomBytes(8).toString("hex"), origin: String(origin || ""), what: String(what || "transaction"), at: Date.now(), cleared: false };
pendingPinRequests.set(req.id, req);
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 {
try { ctx.api.emit("pinRequest", { id: req.id, origin: req.origin, what: req.what, at: req.at }); } catch {}
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 deadline = Date.now() + DAPP_PIN_WAIT_MS;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 250));
if (!ctx) break;
if (req.cleared) 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
}
} finally { pendingPinRequests.delete(req.id); }
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
throw new Error("Aegis asks for your PIN on every transaction. Open the Aegis panel, confirm your PIN there, then try again.");
}
// Signed text shown on an approval overlay. Long messages are cut for the
// overlay's sake, but never silently: the cut says how much is missing, so a
// benign-looking opening cannot hide what the rest commits the user to.
function previewText(text, max = 1500) {
const s = String(text);
const cut = s.length > max ? `${s.slice(0, max)}\n… [${s.length - max} more characters are NOT shown but will be signed]` : s;
return showInvisibles(cut);
}
// Characters that change how text LOOKS without being visible themselves:
// C0/C1 controls (newline and tab excepted), zero-width characters, and the
// bidi overrides/isolates that can make "0x…dead" render as "0x…daed" or
// reorder an overlay line. Built from code points so no editor or tool can
// silently turn the escapes into the characters themselves.
const INVISIBLE_RE = (() => {
const r = [[0x00, 0x08], [0x0b, 0x0c], [0x0e, 0x1f], [0x7f, 0x9f], [0x061c, 0x061c], [0x180e, 0x180e],
[0x200b, 0x200f], [0x2028, 0x202e], [0x2060, 0x2064], [0x2066, 0x206f], [0xfeff, 0xfeff]];
const esc = (c) => String.fromCharCode(92) + "u" + c.toString(16).padStart(4, "0");
return new RegExp("[" + r.map(([a, b]) => (a === b ? esc(a) : esc(a) + "-" + esc(b))).join("") + "]", "g");
})();
// Signed text keeps every character, so the overlay names the invisible ones.
function showInvisibles(s) {
return String(s).replace(INVISIBLE_RE, (c) => `⟨U+${c.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}⟩`);
}
// A name or label that someone else chose (a dapp, a token registry): one
// line, nothing invisible, bounded.
function plainLabel(v, max = 80) {
return String(v ?? "").replace(INVISIBLE_RE, "").replace(/\s+/g, " ").trim().slice(0, max);
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
}
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 fromPanel(m) { if (!m || m.from !== "panel") throw new Error("panel-only message"); }
function fromPage(m) {
if (!m || m.from !== "page" || !m.origin) throw new Error("page-only message");
return m.origin;
}
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
// Head and tail of an address, with any "bitcoincash:"/"bchtest:" prefix
// dropped — the prefix is the same on every row, so it costs width without
// helping anyone tell two addresses apart.
function shortAddr(addr) {
const s = String(addr || "");
const body = s.includes(":") ? s.slice(s.indexOf(":") + 1) : s;
return body.length <= 20 ? body : body.slice(0, 10) + "…" + body.slice(-6);
}
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 fmtBch = (sats) => (Number(sats) / 1e8).toFixed(8).replace(/(\.\d*?[1-9])0+$|\.0+$/, "$1");
const fmtTrx = (sun) => (Number(sun) / 1e6).toFixed(6).replace(/(\.\d*?[1-9])0+$|\.0+$/, "$1");
function fmtValue(units, decimals) {
const n = Number(units) / Math.pow(10, decimals);
return n.toFixed(decimals).replace(/(\.\d*?[1-9])0+$|\.0+$/, "$1");
}
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
// BigInt-safe display for SPL token amounts (raw units in u64 strings).
function fmtTokenAmount(rawStr, decimals) {
const s = String(rawStr || "0");
const neg = s.startsWith("-");
const abs = neg ? s.slice(1) : s;
const d = Number(decimals) || 0;
if (d === 0) return (neg ? "-" : "") + abs;
const pad = abs.padStart(d + 1, "0");
const whole = pad.slice(0, pad.length - d);
const frac = pad.slice(pad.length - d).replace(/0+$/, "");
return (neg ? "-" : "") + whole + (frac ? "." + frac : "");
}
function registerPanelMessages(api) {
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
api.onMessage("state", (_p, m) => { fromPanel(m); return fullState(); });
api.onMessage("selectWallet", (p, m) => {
fromPanel(m);
const id = String(p && p.id || "");
if (!walletEntries().some((w) => w.id === id)) throw new Error("unknown wallet");
api.storage.set("selectedWalletId", id);
emitState();
return fullState();
});
api.onMessage("addWallet", async (p, m) => {
fromPanel(m);
const chain = String(p && p.chain || "");
const network = String(p && p.network || "");
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
// Purpose allocation and mounting live in createVaultWallet — see there
// for why (legacyFirstPurpose carry-over, id numbering).
await createVaultWallet({ chain, network, label: String(p && p.label || "") });
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 fullState();
});
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
// Vault lifecycle from inside the wallet panel — no more redirecting the
// user to Settings > Passwords. After a successful unlock/setup we remount
// every wallet: the vault-derive route is now available.
api.onMessage("vaultSetup", async (p, m) => {
fromPanel(m);
const pw = String(p && p.masterPassword || "");
const seedSource = p && p.seedSource;
await api.vault.lifecycle.setup(pw, seedSource);
await mountAllWallets();
return fullState();
});
api.onMessage("vaultUnlock", async (p, m) => {
fromPanel(m);
const pw = String(p && p.masterPassword || "");
await api.vault.lifecycle.unlock(pw);
noteMasterVerified(api);
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
await mountAllWallets();
return fullState();
});
api.onMessage("vaultStatus", async (_p, m) => {
fromPanel(m);
return api.vault.lifecycle.status();
});
// ---- wallet import (M.1 of DESIGN-wallet-multi-account-amendment.md) ----
// Accepts either a BIP39 mnemonic (12/24 words) + BIP44 path, OR a raw WIF.
// Derives the P2PKH cashaddr client-side, stores the signer material via
// api.vault.imports.add (main-process holds it), then adds a slim Aegis
// wallet entry with kind=imported pointing at the returned importId.
api.onMessage("importWallet", async (p, m) => {
fromPanel(m);
const chain = String(p && p.chain || "bch");
const network = String(p && p.network || "").trim();
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 label = String(p && p.label || "").trim();
if (!label) throw new Error("label required");
const category = String(p && p.category || "operational").trim();
const source = String(p && p.source || "manual-paste");
// Chain-specific address derivation. Every branch has to produce an
// `address` string + fill spec.{seed,path} or spec.wif/privkey. The
// spec is what lands in wallet-imports.enc; the address gets stored on
// the Aegis wallet entry so the picker/strip can show it without
// touching the imports file.
const spec = { kind: null, label, category, source };
let address = null;
const der = ctx.d.derive;
if (chain === "bch") {
const net = network || "chipnet";
if (net !== "mainnet" && net !== "chipnet") throw new Error(`BCH network must be mainnet or chipnet (got ${net})`);
const prefix = net === "mainnet" ? "bitcoincash" : "bchtest";
if (p && p.wif) {
spec.kind = "wif"; spec.wif = String(p.wif).trim();
address = deriveCashaddrFromWif(spec.wif, prefix);
} else if (p && p.mnemonic) {
spec.kind = "seed"; spec.seed = der.mnemonicToSeedHex(String(p.mnemonic).trim());
spec.path = String(p.path || (net === "mainnet" ? "m/44'/145'/0'/0/0" : "m/44'/1'/0'/0/0"));
fix(aegis): 0.14.0 — a seed import is an HD wallet, not one address Importing a Bitcoin.com seed showed a balance of zero. Two causes, one mine. Mine: a Copay / Bitcoin.com backup QR is "1|<words>|<network>|<ACCOUNT path>|…", and 0.13.2 dropped that path straight into a box whose contents are derived as a LEAF. Deriving the account node itself yields an address the wallet has never used — for the standard BIP39 vector, qr96x72dpwrjmg8gtmfemmdhn6u8aqdgnvn4fp2906 instead of qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6. An address with no history, so: zero. An account path now extends to /0/0 instead of being derived in place. The deeper one: a seed import mounted on the single-address adapter, which watches exactly one scripthash. Bitcoin.com, Electron Cash and the rest spread funds across a whole BIP44 account, so even with the right leaf the balance only shows if it all happens to sit on the first receive address. A seed import is an HD wallet and now mounts as one — the same BchWallet the vault-derived wallets use, whose WalletKeys walks receive AND change to a gap limit of 20. That is what actually finds the money, and it brings real spend support to seed imports as a side effect. WIF imports are unchanged: one key is one address, nothing to scan. Existing seed imports are picked up without re-importing. accountPath is stored in either shape — older imports kept the full leaf, the QR carries the account — and the mount trims both to the last hardened element before handing it to WalletKeys. The stale display address stored at import time is irrelevant, since the panel reads the address off the adapter's snapshot. Mounting now reads the signer up front to decide which adapter to use. A locked vault still mounts watch-only from the stored address rather than failing, and the WC manager gets its own copy of the root because it keeps a live reference for the per-URI relay-identity HKDF.
2026-09-28 22:29:19 +02:00
address = deriveCashaddrFromSeed(spec.seed, firstReceivePath(spec.path), prefix);
} else if (p && p.seedHex) {
spec.kind = "seed"; spec.seed = String(p.seedHex).trim().toLowerCase().replace(/^0x/, "");
if (!/^[0-9a-f]{64,128}$/.test(spec.seed)) throw new Error("seedHex must be 32-64 bytes of hex");
spec.path = String(p.path || (net === "mainnet" ? "m/44'/145'/0'/0/0" : "m/44'/1'/0'/0/0"));
fix(aegis): 0.14.0 — a seed import is an HD wallet, not one address Importing a Bitcoin.com seed showed a balance of zero. Two causes, one mine. Mine: a Copay / Bitcoin.com backup QR is "1|<words>|<network>|<ACCOUNT path>|…", and 0.13.2 dropped that path straight into a box whose contents are derived as a LEAF. Deriving the account node itself yields an address the wallet has never used — for the standard BIP39 vector, qr96x72dpwrjmg8gtmfemmdhn6u8aqdgnvn4fp2906 instead of qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6. An address with no history, so: zero. An account path now extends to /0/0 instead of being derived in place. The deeper one: a seed import mounted on the single-address adapter, which watches exactly one scripthash. Bitcoin.com, Electron Cash and the rest spread funds across a whole BIP44 account, so even with the right leaf the balance only shows if it all happens to sit on the first receive address. A seed import is an HD wallet and now mounts as one — the same BchWallet the vault-derived wallets use, whose WalletKeys walks receive AND change to a gap limit of 20. That is what actually finds the money, and it brings real spend support to seed imports as a side effect. WIF imports are unchanged: one key is one address, nothing to scan. Existing seed imports are picked up without re-importing. accountPath is stored in either shape — older imports kept the full leaf, the QR carries the account — and the mount trims both to the last hardened element before handing it to WalletKeys. The stale display address stored at import time is irrelevant, since the panel reads the address off the adapter's snapshot. Mounting now reads the signer up front to decide which adapter to use. A locked vault still mounts watch-only from the stored address rather than failing, and the WC manager gets its own copy of the root because it keeps a live reference for the per-URI relay-identity HKDF.
2026-09-28 22:29:19 +02:00
address = deriveCashaddrFromSeed(spec.seed, firstReceivePath(spec.path), prefix);
} else { throw new Error("supply mnemonic, seedHex, or wif"); }
spec.cashaddr = address;
} else if (chain === "btc" || chain === "dgb") {
const defaults = { btc: { network: "mainnet", path: "m/84'/0'/0'/0/0" }, dgb: { network: "mainnet", path: "m/84'/20'/0'/0/0" } };
const net = network || defaults[chain].network;
const purposeHint = Number(p && p.purpose || 84);
if (p && p.wif) {
spec.kind = "wif"; spec.wif = String(p.wif).trim();
address = chain === "btc" ? der.btc.fromWif(spec.wif, net, purposeHint) : der.dgb.fromWif(spec.wif, purposeHint);
} else if (p && p.mnemonic) {
spec.kind = "seed"; spec.seed = der.mnemonicToSeedHex(String(p.mnemonic).trim());
spec.path = String(p.path || defaults[chain].path);
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 (chain === "btc") address = der.btc.fromSeed(spec.seed, spec.path, net);
else {
// A DigiByte seed can belong to the 2018-19 official mobile
// wallets, which built the master node with HMAC key
// "DigiByte seed". Carry the choice onto the spec so the entry
// records which tree the stored address came from.
const hk = String((p && p.hmacKey) || der.dgb.HMAC_BITCOIN_SEED);
if (hk !== der.dgb.HMAC_BITCOIN_SEED && hk !== der.dgb.HMAC_DIGIBYTE_SEED) {
throw new Error("unknown DigiByte master-key variant");
}
if (hk !== der.dgb.HMAC_BITCOIN_SEED) spec.hmacKey = hk;
address = der.dgb.fromSeed(spec.seed, spec.path, hk);
}
} else { throw new Error("supply mnemonic or wif"); }
// Theseus's api.vault.imports.add validates a `cashaddr` field (from
// when only BCH imports existed). Reuse the same field name for
// every chain — Aegis reads it back by importId and knows the shape
// via entry.chain. Doesn't have to be a real cashaddr.
spec.cashaddr = address;
} else if (chain === "eth" || chain === "trx" || chain === "sol") {
const defaults = {
eth: { network: "mainnet", path: "m/44'/60'/0'/0/0" },
trx: { network: "mainnet", path: "m/44'/195'/0'/0/0" },
sol: { network: "mainnet", path: "m/44'/501'/0'/0'" },
};
const net = network || defaults[chain].network;
if (p && p.mnemonic) {
spec.kind = "seed"; spec.seed = der.mnemonicToSeedHex(String(p.mnemonic).trim());
spec.path = String(p.path || defaults[chain].path);
if (chain === "eth") address = der.eth.fromSeed(spec.seed, spec.path);
else if (chain === "trx") address = der.trx.fromSeed(spec.seed, spec.path);
else address = der.sol.fromSeed(spec.seed, spec.path);
} else if (p && p.privHex) {
// Theseus's vault.imports.add only recognises kind "seed" (BIP39
// + path) and "wif" (a base58check Bitcoin key). Raw hex keys
// for ETH/TRX/SOL don't fit either shape, so we pack them into
// the wif slot with a scheme prefix (`aegis-privhex:<hex>`) —
// the vault doesn't inspect the value, just stores it. Aegis
// reads its own prefix back when spending ships. Panel state
// + address are computed here, so read-only balance / receive
// work today without touching the vault field.
spec.kind = "wif";
// Normalise raw hex the user pasted. Tolerate every common mangle
// path so the panel error surface is a clear "expected 32-byte
// hex" instead of the raw noble/hashes error string:
// - leading / trailing whitespace, mixed case
// - "0x" or "0X" prefix
// - internal whitespace, tabs, newlines, commas, colons, dashes
// - accidental quotes wrapping the paste
// - a preamble like "private key: <hex>" (e.g. from an AI-agent
// transcript) — pick the longest hex-shaped substring.
let raw = String(p.privHex).trim();
raw = raw.replace(/^['"`]+|['"`]+$/g, "");
// If the user pasted a multi-line block, extract the longest
// run of hex characters and treat that as the key.
const hexRuns = raw.match(/[0-9a-fA-F]{16,}/g);
if (hexRuns && hexRuns.length) {
hexRuns.sort((a, b) => b.length - a.length);
raw = hexRuns[0];
}
raw = raw.toLowerCase().replace(/^0x/, "").replace(/[\s,:_\-]/g, "");
if (!/^[0-9a-f]+$/.test(raw)) {
// Give the user something concrete to act on. Tron-specific
// hints: base58-shaped strings that start with T (34 chars) are
// addresses, not private keys; whitespace-separated words look
// like a mnemonic.
const original = String(p.privHex).trim();
if (/^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(original)) {
throw new Error("That looks like a Tron address (T…), not a private key. Paste the 64-hex-character private key instead.");
}
if (/^([a-z]+\s+){11,}[a-z]+$/i.test(original)) {
throw new Error("That looks like a BIP39 mnemonic. Switch the import format to 'Mnemonic + path'.");
}
throw new Error("Private key must be hex (with or without 0x). Whitespace, dashes and colons are ignored, but non-hex characters aren't accepted.");
}
if (raw.length !== 64) {
throw new Error(`Private key must be 32 bytes (64 hex characters). Got ${raw.length} hex character${raw.length === 1 ? "" : "s"} after normalising the paste.`);
}
// Derive first so any bad key surfaces before we write to disk.
if (chain === "eth") address = der.eth.fromPrivHex(raw);
else if (chain === "trx") address = der.trx.fromPrivHex(raw);
else address = der.sol.fromPrivHex(raw);
spec.wif = `aegis-privhex:${raw}`;
} else if (p && p.privB58 && chain === "sol") {
// Same repacking trick as privhex above — Solana's Phantom-style
// base58 key gets packed into wif with an `aegis-privb58:` tag.
const raw = String(p.privB58).trim();
address = der.sol.fromBase58(raw);
spec.kind = "wif";
spec.wif = `aegis-privb58:${raw}`;
} else { throw new Error("supply mnemonic, privHex" + (chain === "sol" ? ", or privB58" : "")); }
spec.cashaddr = address; // storage-key reuse — see BTC/DGB comment above
} else if (chain === "sc") {
// Siacoin. Uses a 32-byte root seed + u64 index (KeyFromSeed layout);
// no BIP44 path. Accepts a BIP39 12-word mnemonic (matches Sia
// Central Lite / walletd, PBKDF2 → first 32 bytes) or raw 32-byte
// seed hex. Address at index 0 is what we surface at import time;
// the mounted SiaWallet lets users advance through additional
// indices via the "Next unused address" affordance.
const net = network || "mainnet";
if (net !== "mainnet") throw new Error(`SC only supports mainnet (got ${net})`);
const index = Number(p && p.index != null ? p.index : 0);
if (!Number.isInteger(index) || index < 0) throw new Error("SC index must be a non-negative integer");
let seedHex;
if (p && p.mnemonic) {
const r = der.sc.fromMnemonic(String(p.mnemonic).trim(), index);
seedHex = r.seedHex; address = r.address;
} else if (p && p.seedHex) {
const r = der.sc.fromSeedHex(String(p.seedHex).trim(), index);
seedHex = r.seedHex; address = r.address;
} else { throw new Error("supply mnemonic or seedHex"); }
spec.kind = "seed";
spec.seed = seedHex;
// path field carries the Sia address index as an integer string,
// opaque to the vault. Mount reads it back as Number(spec.path).
spec.path = String(index);
spec.cashaddr = address;
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
} else {
throw new Error(`import not supported for chain "${chain}"`);
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 { id: importId } = await api.vault.imports.add(spec);
const netForId = network || "mainnet";
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 list = walletEntries().slice();
const walletId = `${chain}-imported-${importId}`;
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 (list.some((w) => w.id === walletId)) throw new Error("duplicate import id");
const entry = {
id: walletId, label, chain, network: netForId,
kind: "imported", importId,
importedAddress: address, importedCategory: category,
accountPath: spec.path || null,
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
createdAt: Date.now(),
};
list.push(entry);
writeWallets(api, list);
api.storage.set("selectedWalletId", walletId);
ctx.runtimes.set(walletId, { entry, phase: "locked", error: null, adapter: null });
emitState();
await mountWallet(entry);
return fullState();
});
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
api.onMessage("removeWallet", (p, m) => {
fromPanel(m);
const id = String(p && p.id || "");
const list = walletEntries();
const entry = list.find((w) => w.id === id);
if (!entry) throw new Error("unknown wallet");
if (entry.isDefault) throw new Error("the default wallet cannot be removed");
const next = list.filter((w) => w.id !== id);
writeWallets(api, next);
if (selectedWalletId() === id) api.storage.set("selectedWalletId", next[0]?.id || "");
unmountWallet(id);
// Drop per-wallet storage subtree.
const all = api.storage.all ? api.storage.all() : {};
const prefix = `wallets/${id}/`;
for (const k of Object.keys(all)) if (k.startsWith(prefix)) api.storage.set(k, null);
emitState();
return fullState();
});
api.onMessage("renameWallet", (p, m) => {
fromPanel(m);
const id = String(p && p.id || "");
const label = String(p && p.label || "").trim().slice(0, 60);
if (!label) throw new Error("label required");
const list = walletEntries();
const entry = list.find((w) => w.id === id);
if (!entry) throw new Error("unknown wallet");
entry.label = label;
writeWallets(api, list);
emitState();
return fullState();
});
api.onMessage("refresh", async (_p, m) => { fromPanel(m); const rt = requireSelected(); await rt.adapter.refresh(true); return snapshotForSelected(); });
// Panel drilldown → refresh every wallet under a chain (optionally scoped
// to one subnetwork). Fires each adapter's refresh in parallel; individual
// failures set the adapter's own error field (surfaced back to the panel
// via emitStateForWallet) rather than aborting the batch. Returns the
// list of {id, ok, error} so the panel can flash a summary.
api.onMessage("refreshChain", async (p, m) => {
fromPanel(m);
const chain = String(p?.chain || "");
const network = p?.network ? String(p.network) : null;
if (!chain) throw new Error("chain is required");
const targets = walletEntries().filter((w) => w.chain === chain && (!network || w.network === network));
const out = [];
await Promise.all(targets.map(async (w) => {
const rt = ctx.runtimes.get(w.id);
if (!rt || !rt.adapter) { out.push({ id: w.id, ok: false, error: "adapter not mounted" }); return; }
try {
await rt.adapter.refresh(true);
// The adapters catch their own fetch failures onto state.error rather
// than rejecting, so awaiting refresh() proves nothing. Read the
// snapshot back, or a dead server reports a successful refresh.
let snapErr = null;
try { snapErr = rt.adapter.snapshot()?.error || null; } catch { snapErr = null; }
out.push(snapErr ? { id: w.id, ok: false, error: snapErr } : { id: w.id, ok: true });
} catch (e) { out.push({ id: w.id, ok: false, error: e?.message || String(e) }); }
}));
return { chain, network, results: 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
api.onMessage("nextAddress", (_p, m) => {
fromPanel(m);
const rt = requireSelected();
if (rt.entry.chain !== "bch") throw new Error("only BCH wallets have multiple receive addresses");
rt.adapter.nextAddress();
return snapshotForSelected();
});
api.onMessage("openUrl", (p, m) => { fromPanel(m); api.openTab(String(p && p.url || "")); return true; });
api.onMessage("aegisVersion", (_p, m) => {
fromPanel(m);
try { return require("./addon.json").version; } catch { 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
// Panel gate uses this to jump to Settings › Passwords when the vault is
// locked / not yet created — one-click bridge to Theseus's built-in UI.
api.onMessage("openSettings", (p, m) => { fromPanel(m); api.openSettings(String(p && p.section || "")); return true; });
// Panel-initiated update flow. Preferred path: Theseus exposes
// checkAndStageSelfUpdate + restartApp (added 0.3.48). The panel calls
// "requestUpdate" for the two-step chip flow:
// step "stage" — verify + stage the newest signed build; the reply
// carries { status, current, next } so the panel can
// show "Update to vX.Y.Z ready — restart to apply".
// step "apply" — cleanly relaunches Theseus, which runs
// promoteStagedUpdates() before activating add-ons.
// Falls back to opening Settings › Extensions when running under an
// older Theseus that lacks either hook.
api.onMessage("requestUpdate", async (p, m) => {
fromPanel(m);
const step = String(p?.step || "stage");
if (step === "apply") {
if (typeof api.restartApp !== "function") return { restarted: false, fallback: "settings" };
try { api.restartApp(); return { restarted: true }; }
catch (e) { return { restarted: false, err: e?.message || String(e) }; }
}
if (typeof api.checkAndStageSelfUpdate !== "function") return { staged: false, fallback: "settings" };
try {
const r = await api.checkAndStageSelfUpdate();
// "staged" and "already-staged" both mean a newer signed build is
// waiting for the next launch — surface it to the panel identically.
const ok = r?.status === "staged" || r?.status === "already-staged";
return { staged: ok, status: r?.status || "unknown", detail: r?.detail || null, current: r?.current || null, next: r?.next || null };
} catch (e) {
return { staged: false, err: e?.message || String(e) };
}
});
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
api.onMessage("setBchServers", (p, m) => {
fromPanel(m);
const patch = p || {};
if ("servers" in patch) {
const list = Array.isArray(patch.servers) ? patch.servers.map((s) => String(s).trim()).filter(Boolean) : [];
for (const s of list) if (!/^wss?:\/\/[^/\s]+$/i.test(s)) throw new Error(`server must be ws(s)://host:port — got ${s}`);
api.storage.set("servers", list.length ? list : 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
// Push the new server list into every mounted BCH wallet.
for (const rt of ctx.runtimes.values()) {
if (rt.entry.chain === "bch" && rt.adapter) rt.adapter.setServers(bchServerList(api));
}
}
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 fullState();
});
api.onMessage("setAccountPath", (p, m) => {
fromPanel(m);
const patch = p || {};
const id = String(patch.id || selectedWalletId());
const entry = walletEntries().find((w) => w.id === id);
if (!entry) throw new Error("unknown wallet");
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 (!["bch", "dgb", "btc"].includes(entry.chain)) throw new Error("account path is a BCH/BTC/DGB-only setting");
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 v = String(patch.accountPath || "").trim();
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 (v && !/^m(\/\d+'?)+$/.test(v)) throw new Error("derivation path must look like m/84'/0'/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
const list = walletEntries();
const idx = list.findIndex((w) => w.id === id);
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
// Per-network default: BTC/DGB come from the registry helper, BCH keeps
// its historical m/44'/145'/0'.
const dflt = entry.chain === "bch"
? "m/44'/145'/0'"
: (defaultAccountPathFor(COINS[entry.chain], entry.network) || "m/84'/0'/0'");
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit) Aegis now covers four coins across two-step coin+network picks: BCH (mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet). - Sia (SC): pulled the standalone siawallet's lib into bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing the common adapter shape. The very first SC wallet the user adds in Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry over automatically; subsequent SC sub-accounts start at "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL shows a "Point Aegis at a walletd node" gate in the panel. - Vault-derive gate now honors a manifest-declared `absorbs` list, so Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive() guard accepts paths under either the current id or the absorbed one — the mechanism a superseding add-on uses to inherit an older add-on's keyspace without orphaning funds. - DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32. ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool). BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address for "abandon×11 about, m/84'/20'/0'/0/0" is dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39. - Panel: SVG coin logos for SC (green disc with S) and DGB (blue octagon with D) alongside the BCH/TRX marks. Chain-specific settings block per coin (walletd URL for SC; derivation path for DGB). Balance render uses BigInt-safe arithmetic so 24-decimal SC amounts don't lose precision on the way through the panel; amount input on SC returns a hastings string. - Every chain adapter's snapshot fits the panel's shared shape (address/balance/history/etc.), so future chains only need a new chain-<x>.js file, a COINS registry entry, a matching case in mountWallet, and an SVG logo. Standalone siawallet addon stays as-is on disk; users can delete it once they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
list[idx] = { ...list[idx], accountPath: v || dflt };
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
writeWallets(api, list);
const rt = ctx.runtimes.get(id);
if (rt && rt.adapter) { try { rt.adapter.dispose(); } catch {} rt.adapter = null; rt.phase = "locked"; }
mountWallet(list[idx]);
return fullState();
});
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: per-wallet RPC URL, live-swap without key rebuild.
api.onMessage("setRpcUrl", (p, m) => {
fromPanel(m);
const patch = p || {};
const id = String(patch.id || selectedWalletId());
const entry = walletEntries().find((w) => w.id === id);
if (!entry) throw new Error("unknown wallet");
if (entry.chain !== "eth" && entry.chain !== "sol") throw new Error("RPC URL is an ETH/SOL setting");
const v = String(patch.rpcUrl || "").trim();
if (v && !/^https?:\/\/[^\s]+$/i.test(v)) throw new Error("RPC URL must start with http:// or https://");
api.storage.set(`wallets/${id}/rpcUrl`, v);
const rt = ctx.runtimes.get(id);
if (rt && rt.adapter && typeof rt.adapter.setRpcUrl === "function") {
rt.adapter.setRpcUrl(v);
rt.adapter.refresh().catch((e) => api.log(`[${id}] refresh:`, e?.message || e));
}
emitState();
return fullState();
});
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-only: per-wallet walletd URL. Live: pushes the URL into the adapter
// without rebuilding the keys (the seed stays derived from the same
// vault path — only the node the wallet talks to changes).
api.onMessage("setWalletdUrl", (p, m) => {
fromPanel(m);
const patch = p || {};
const id = String(patch.id || selectedWalletId());
const entry = walletEntries().find((w) => w.id === id);
if (!entry) throw new Error("unknown wallet");
if (entry.chain !== "sc") throw new Error("walletd URL is a Sia-only setting");
const v = String(patch.walletdUrl || "").trim();
if (v && !/^https?:\/\/[^\s]+$/i.test(v)) throw new Error("walletd URL must start with http:// or https://");
api.storage.set(`wallets/${id}/walletdUrl`, v);
const rt = ctx.runtimes.get(id);
if (rt && rt.adapter) {
rt.adapter.setWalletdUrl(v);
if (v) rt.adapter.startPolling();
rt.adapter.refresh(true).catch((e) => api.log(`[${id}] refresh:`, e?.message || e));
}
emitState();
return fullState();
});
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
// Live plan preview for the selected wallet.
api.onMessage("planSend", async (p, m) => {
fromPanel(m);
const rt = requireSelected();
const plan = await Promise.resolve(rt.adapter.plan(p || {}));
return describePlan(plan, rt.entry.chain, rt.entry.network);
});
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 token plan/send — only meaningful when the selected wallet is SOL.
api.onMessage("planTokenSend", async (p, m) => {
fromPanel(m);
const rt = requireSelected();
if (rt.entry.chain !== "sol" || typeof rt.adapter.planTokenTransfer !== "function") {
throw new Error("token send is a Solana-only flow");
}
const plan = await rt.adapter.planTokenTransfer(p || {});
return {
recipients: plan.recipients, fee: String(plan.fee), feeRate: String(plan.feeRate),
inputs: 0, change: "0", total: plan.total, mint: plan.mint, decimals: plan.decimals,
};
});
api.onMessage("sendToken", async (p, m) => {
fromPanel(m);
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
requirePanelTxPin();
// Same rule as send: the token send names the wallet it was reviewed for.
if (!p || !p.walletId || String(p.walletId) !== selectedWalletId()) {
throw new Error("the selected wallet changed — review the send and try again");
}
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 rt = requireSelected();
if (rt.entry.chain !== "sol") throw new Error("token send is Solana-only");
const plan = await rt.adapter.planTokenTransfer(p || {});
const meta = chainMeta("sol", rt.entry.network);
const tokenInfo = (rt.adapter.snapshot().tokens || []).find((t) => t.mint === plan.mint) || {};
const rows = [
{ label: "To", value: plan.recipients[0].to, mono: true },
{ label: "Amount", value: `${fmtTokenAmount(plan.recipients[0].value, plan.decimals)} ${tokenInfo.symbol || "token"}`, strong: true },
{ label: "Mint", value: plan.mint, mono: true },
{ label: "Network fee", value: `~${fmtValue(Number(plan.fee), meta.decimals)} SOL${(plan._spl && plan._spl.destExists === false) ? " (includes new token account rent)" : ""}` },
{ label: "Wallet", value: `${rt.entry.label} — Solana · ${rt.entry.network}` },
];
const pick = await api.approvalModal({
title: `Send ${tokenInfo.symbol || "SPL token"}?`,
origin: "Aegis wallet panel",
rows,
actions: [{ id: "send", label: "Send", primary: true }],
});
if (pick !== "send") throw new Error("cancelled");
return rt.adapter.signAndBroadcastToken(plan);
});
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
// Execute a send with approval overlay.
api.onMessage("send", async (p, m) => {
fromPanel(m);
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
requirePanelTxPin();
// The panel names the wallet its form was built for. If the selection
// moved since (another surface, a late state push), refuse rather than
// send this amount from a different wallet.
// Required, not optional: a send that does not say which wallet it was
// reviewed for cannot be checked against the selection.
if (!p || !p.walletId) throw new Error("send names no wallet — review the send and try again");
if (String(p.walletId) !== selectedWalletId()) {
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
throw new Error("the selected wallet changed — review the send and try again");
}
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 rt = requireSelected();
const plan = await Promise.resolve(rt.adapter.plan(p || {}));
const d = describePlan(plan, rt.entry.chain, rt.entry.network);
const meta = chainMeta(rt.entry.chain, rt.entry.network);
const rows = [
{ label: "To", value: d.recipients[0].to, mono: true },
{ label: "Amount", value: `${fmtValue(d.recipients[0].value, meta.decimals)} ${meta.ticker}`, strong: true },
{ label: "Fee", value: rt.entry.chain === "bch" ? `${plan.fee} sat (${plan.feeRate} sat/B)` : `${fmtValue(plan.fee, meta.decimals)} ${meta.ticker}` },
{ label: "Total", value: `${fmtValue(d.total, meta.decimals)} ${meta.ticker}` },
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
{ label: "Wallet", value: `${rt.entry.label} — ${meta.coinLabel} · ${meta.networkLabel}` },
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 pick = await api.approvalModal({
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
title: `Send ${meta.ticker}?`,
origin: "Aegis wallet panel",
rows,
actions: [{ id: "send", label: "Send", primary: true }],
});
if (pick !== "send") throw new Error("cancelled");
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 rt.adapter.signAndBroadcast(plan);
});
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
// ---- Balance consolidation ------------------------------------------------
// Batch send-max from every same-chain/same-network wallet (or a subset the
// user picked with checkboxes) into the currently-selected wallet. Runs in
// two phases:
// consolidatePreview — dry-run plan() per source; returns balance/fee/
// net/error so the panel renders a preview list
// with checkboxes without asking the user to
// approve anything yet.
// consolidateIntoSelected — signs + broadcasts one send per chosen source.
// The panel shows a single upfront confirmation
// (with the total to move and total fees); Theseus's
// per-tx approval overlay is skipped because the
// batch itself is the user's explicit intent.
api.onMessage("consolidatePreview", async (_p, m) => {
fromPanel(m);
const destId = selectedWalletId();
if (!destId) throw new Error("no wallet selected");
const destEntry = walletEntries().find((w) => w.id === destId);
if (!destEntry) throw new Error("selected wallet not found");
const destRt = ctx.runtimes.get(destId);
if (!destRt?.adapter) throw new Error("destination wallet not ready");
const destSnap = destRt.adapter.snapshot();
const destAddr = destSnap.address;
if (!destAddr) throw new Error("destination wallet has no receive address");
const sources = walletEntries().filter((w) =>
w.chain === destEntry.chain &&
w.network === destEntry.network &&
w.id !== destId,
);
const items = [];
for (const src of sources) {
const rt = ctx.runtimes.get(src.id);
const snap = rt?.adapter?.snapshot?.() || {};
const bal = snap.balance || {};
const totalUnits = typeof bal.confirmed === "string"
? (BigInt(bal.confirmed || "0") + BigInt(bal.unconfirmed || "0")).toString()
: String((bal.confirmed || 0) + (bal.unconfirmed || 0));
const base = {
walletId: src.id, label: src.label,
address: snap.address || null,
balance: totalUnits,
fee: null, net: null, error: null, eligible: false,
};
if (!rt?.adapter) { items.push({ ...base, error: "adapter not mounted" }); continue; }
if (typeof rt.adapter.plan !== "function") { items.push({ ...base, error: "adapter has no plan()" }); continue; }
// Dry-run send-max to the destination. plan() throws on empty /
// dust-only wallets — that's the "nothing to sweep" case and it
// reads as an error string per source in the preview.
try {
const plan = await Promise.resolve(rt.adapter.plan({ to: destAddr, sendMax: true }));
const fee = String(plan.fee ?? 0);
const net = String(plan.recipients?.[0]?.value ?? 0);
items.push({ ...base, fee, net, eligible: true });
} catch (e) {
items.push({ ...base, error: e?.message || String(e) });
}
}
const meta = chainMeta(destEntry.chain, destEntry.network) || null;
return {
destinationWalletId: destId,
destinationLabel: destEntry.label,
destinationAddress: destAddr,
chain: destEntry.chain,
network: destEntry.network,
ticker: meta?.ticker || "",
decimals: meta?.decimals || 8,
sources: items,
};
});
api.onMessage("consolidateIntoSelected", async (p, m) => {
fromPanel(m);
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
requirePanelTxPin();
const destId = selectedWalletId();
if (!destId) throw new Error("no wallet selected");
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 destination is the wallet the PREVIEW was drawn for, not whatever
// is selected by the time the button is pressed: a preview painted for A
// followed by a switch to B used to sweep everything into B.
if (!p || !p.destinationWalletId || String(p.destinationWalletId) !== destId) {
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
throw new Error("the selected wallet changed since this preview — reopen Consolidate");
}
const destEntry = walletEntries().find((w) => w.id === destId);
if (!destEntry) throw new Error("selected wallet not found");
const destRt = ctx.runtimes.get(destId);
if (!destRt?.adapter) throw new Error("destination wallet not ready");
const destAddr = destRt.adapter.snapshot().address;
if (!destAddr) throw new Error("destination wallet has no receive address");
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
// sourceIds are the wallets the user CHECKED. They are required: an
// omitted list used to mean "every sibling wallet".
if (!Array.isArray(p?.sourceIds) || !p.sourceIds.length) throw new Error("choose at least one wallet to consolidate");
const requested = new Set(p.sourceIds.map(String));
const sources = walletEntries().filter((w) =>
w.chain === destEntry.chain &&
w.network === destEntry.network &&
w.id !== destId &&
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
requested.has(w.id),
);
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 (!sources.length) throw new Error("none of the chosen wallets can be consolidated into this one");
// Plan every sweep first, then put the WHOLE batch on the host overlay.
// This emptied wallets on the panel's say-so alone; every other spend
// in Aegis is confirmed on a surface the panel cannot draw.
const meta = chainMeta(destEntry.chain, destEntry.network);
const dec = meta?.decimals ?? 8;
const planned = [];
const results = [];
for (const src of sources) {
const rt = ctx.runtimes.get(src.id);
if (!rt?.adapter || typeof rt.adapter.plan !== "function") {
results.push({ walletId: src.id, label: src.label, ok: false, error: "adapter not mounted" });
continue;
}
try {
const plan = await Promise.resolve(rt.adapter.plan({ to: destAddr, sendMax: 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
planned.push({ src, rt, plan });
} catch (e) {
results.push({ walletId: src.id, label: src.label, ok: false, error: e?.message || String(e) });
}
}
if (planned.length) {
let totalNet = 0n, totalFee = 0n;
const rows = planned.slice(0, 12).map(({ src, plan }) => {
const net = BigInt(String(plan.recipients?.[0]?.value ?? 0));
const fee = BigInt(String(plan.fee ?? 0));
totalNet += net; totalFee += fee;
return { label: "Empty", value: `${src.label} — ${fmtValue(net.toString(), dec)} ${meta?.ticker || ""}`, mono: true };
});
for (const { plan } of planned.slice(12)) {
totalNet += BigInt(String(plan.recipients?.[0]?.value ?? 0));
totalFee += BigInt(String(plan.fee ?? 0));
}
if (planned.length > 12) rows.push({ label: "…", value: `and ${planned.length - 12} more wallets` });
rows.push({ label: "Into", value: `${destEntry.label} — ${destAddr}`, mono: true });
rows.push({ label: "Arrives", value: `${fmtValue(totalNet.toString(), dec)} ${meta?.ticker || ""}`, strong: true });
rows.push({ label: "Fees", value: `${fmtValue(totalFee.toString(), dec)} ${meta?.ticker || ""} across ${planned.length} transaction${planned.length === 1 ? "" : "s"}` });
const pick = await api.approvalModal({
title: `Consolidate ${planned.length} wallet${planned.length === 1 ? "" : "s"}?`,
origin: "Aegis wallet panel",
body: "Each wallet listed is emptied into the destination in its own transaction. This cannot be undone.",
rows,
actions: [{ id: "send", label: "Consolidate", primary: true }],
});
if (pick !== "send") throw new Error("cancelled");
}
for (const { src, rt, plan } of planned) {
try {
const r = await rt.adapter.signAndBroadcast(plan);
results.push({
walletId: src.id, label: src.label, ok: true,
txid: r?.txid || null,
sent: String(plan.recipients?.[0]?.value ?? 0),
fee: String(plan.fee ?? 0),
});
} catch (e) {
results.push({ walletId: src.id, label: src.label, ok: false, error: e?.message || String(e) });
}
}
// Force a refresh on the destination so its balance jumps once the txs
// reach the network's mempool. Silent-fail — panel will pick up state
// on the next state emit anyway.
try { if (typeof destRt.adapter.refresh === "function") destRt.adapter.refresh(false); } catch {}
return {
destinationWalletId: destId,
destinationAddress: destAddr,
chain: destEntry.chain,
network: destEntry.network,
results,
};
});
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 an import to an HD wallet ----------------------------------
//
// A wallet imported from a single private key cannot do WizardConnect: the
// handshake ships BIP32 xpubs so the dapp can derive addresses on its own,
// and a lone key has no chain code to build one from. Nothing Aegis can do
// locally fixes that — manufacturing a parent whose child equals a given
// key means inverting HMAC-SHA512. The way out is to stop being a
// single-key wallet: derive a proper HD wallet from the vault and sweep the
// imported key into it.
//
// Only BCH imports can do this. The BTC/DGB/ETH/TRX/SOL imported adapters
// still throw "read-only" from plan(), so there is no sweep to run.
function promotableImport(walletId) {
const entry = walletEntries().find((w) => w.id === String(walletId || ""));
if (!entry) throw new Error("unknown wallet");
if (entry.kind !== "imported") throw new Error("this wallet is already derived from your vault");
if (entry.chain !== "bch") {
throw new Error(`Imported ${String(entry.chain).toUpperCase()} wallets are still read-only, so there is nothing to sweep with yet. Only BCH imports can be promoted today.`);
}
const rt = ctx.runtimes.get(entry.id);
if (!rt?.adapter || rt.phase !== "ready") throw new Error("wallet is still loading — try again in a moment");
return { entry, rt };
}
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 wallet as a full Theseus tab. It loads panel.html — the very
// same file the sidebar uses — with ?surface=web, because an add-on's own
// tab is handed the same window.silentmode surface and main dispatches it
// as from:"panel". So this is a re-layout, not a second wallet: one set of
// handlers, one UI, no drift between the two.
api.onMessage("openFullScreen", (_p, m) => {
fromPanel(m);
if (typeof api.openTab !== "function") {
throw new Error("This Theseus build can't open add-on tabs — update Theseus.");
}
api.openTab("panel.html", { query: { surface: "web" } });
return true;
});
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
api.onMessage("promotePreview", async (p, m) => {
fromPanel(m);
const { entry, rt } = promotableImport(p && p.walletId);
const snap = rt.adapter.snapshot();
const meta = chainMeta(entry.chain, entry.network);
// Cost the sweep WITHOUT creating the destination wallet first, so
// cancelling the preview leaves nothing behind. A send-max to our own
// address spends the same UTXOs into the same single P2PKH output, so
// the fee is identical to the real sweep — only the output's 20-byte
// hash differs, and that does not change the transaction's size.
let fee = null, net = null, error = null;
try {
const plan = await Promise.resolve(rt.adapter.plan({ to: snap.address, sendMax: true }));
fee = String(plan.fee ?? 0);
net = String(plan.recipients?.[0]?.value ?? 0);
} catch (e) { error = e?.message || String(e); }
return {
walletId: entry.id, label: entry.label,
chain: entry.chain, network: entry.network,
networkLabel: meta?.networkLabel || entry.network,
ticker: meta?.ticker || "", decimals: meta?.decimals || 8,
address: snap.address || null,
balance: snap.balance || null,
fee, net, error,
suggestedLabel: `${entry.label} (HD)`,
};
});
api.onMessage("promoteToHd", async (p, m) => {
fromPanel(m);
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
requirePanelTxPin();
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
const { entry, rt } = promotableImport(p && p.walletId);
const dest = await createVaultWallet({
chain: entry.chain, network: entry.network,
label: String(p && p.label || "") || `${entry.label} (HD)`,
});
const destRt = ctx.runtimes.get(dest.id);
const destAddr = destRt?.adapter?.snapshot?.()?.address;
if (!destAddr) throw new Error("the new wallet came up without a receive address — nothing was moved");
let plan;
try {
plan = await Promise.resolve(rt.adapter.plan({ to: destAddr, sendMax: true }));
} catch (e) {
// Nothing was broadcast, so the empty wallet we just made is pure
// litter — take it back out. Past this point we keep it even on
// failure, because a transaction may already be on the wire and its
// destination must stay visible.
try { unmountWallet(dest.id); writeWallets(ctx.api, walletEntries().filter((w) => w.id !== dest.id)); } catch {}
ctx.api.storage.set("selectedWalletId", entry.id);
emitState();
throw e;
}
const sent = String(plan.recipients?.[0]?.value ?? 0);
const fee = String(plan.fee ?? 0);
const r = await rt.adapter.signAndBroadcast(plan);
// The import keeps its key on purpose. The sweep is unconfirmed for now,
// and anyone who still has the old address can pay into it — removing
// the key here would strand those coins. The panel offers removal as a
// separate step once the balance has actually gone to zero.
try { rt.adapter.refresh(false); } catch {}
try { destRt.adapter.refresh(false); } catch {}
emitState();
return {
ok: true, txid: r?.txid || null, sent, fee,
fromWalletId: entry.id, fromLabel: entry.label,
newWalletId: dest.id, newWalletLabel: dest.label, newAddress: destAddr,
ticker: chainMeta(entry.chain, entry.network)?.ticker || "",
decimals: chainMeta(entry.chain, entry.network)?.decimals || 8,
};
});
api.onMessage("recovery", async (p, m) => {
fromPanel(m);
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 id = String(p && p.id || selectedWalletId());
const rt = requireWallet(id);
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 rt.adapter.recovery !== "function") throw new Error("this chain does not expose recovery details");
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 r = rt.adapter.recovery();
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
// Public material only. Revealing a secret goes through revealSecret,
// which proves the master password first — this used to hand back the
// xprv behind nothing but an approval click.
return { accountPath: r.accountPath, xpub: r.xpub, purpose: rt.entry.purpose };
});
// ---- reveal a wallet's secret ------------------------------------------
// Authorisation is the master password, and it is verified HERE rather
// than taken on trust from the panel: the PIN route never touches
// vaultUnlock, so without this check the only thing between a secret and
// the screen would be panel-side logic. The panel gets the password either
// by decrypting the PIN blob (which wraps exactly this) or by asking.
//
// What comes back is NOT a recovery phrase, because no BIP39 mnemonic is
// stored anywhere in Aegis or the Theseus vault. An import keeps
// mnemonicToSeedHex(words) and discards the words (PBKDF2, one-way), and a
// vault wallet is HKDF(vault root, purpose) and never had words of its
// own — password-vault.js is explicit that the seed is never persisted.
// Calling any of this a "recovery phrase" in the UI would be a lie that
// costs someone their backup, so every form names itself and says where it
// can actually be restored.
api.onMessage("revealSecret", async (p, m) => {
fromPanel(m);
const pw = String((p && p.masterPassword) || "");
if (!pw) throw new Error("master password required");
try { await api.vault.lifecycle.unlock(pw); }
catch (e) {
// A missing or unset-up vault is a structural failure, not a bad
// password — don't accuse the user of mistyping something that was
// never going to work.
const msg = e?.message || String(e);
if (/no vault|not set up/i.test(msg)) throw new Error(msg);
throw new Error("wrong master password");
}
noteMasterVerified(api);
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 id = String((p && p.walletId) || selectedWalletId() || "");
const entry = walletEntries().find((w) => w.id === id);
if (!entry) throw new Error("no such wallet");
const meta = chainMeta(entry.chain, entry.network);
const base = {
walletId: entry.id, label: entry.label, chain: entry.chain,
coinLabel: meta?.coinLabel || entry.chain,
networkLabel: meta?.networkLabel || entry.network,
address: ctx.runtimes.get(entry.id)?.adapter?.snapshot()?.address || null,
};
if (entry.kind === "imported") {
if (!api.vault?.imports || typeof api.vault.imports.signer !== "function") {
throw new Error("this build cannot read imported keys");
}
const blob = await api.vault.imports.signer(entry.importId);
if (blob.kind === "wif") {
// ETH/TRX/SOL raw hex keys ride in the wif slot behind a scheme
// prefix (see importWallet) — unwrap so the user sees the key.
const w = String(blob.wif || "");
const PRIVHEX = "aegis-privhex:";
return w.startsWith(PRIVHEX)
? { ...base, form: "privhex", secret: w.slice(PRIVHEX.length) }
: { ...base, form: "wif", secret: w };
}
if (blob.kind === "seed") {
return { ...base, form: "seed", secret: String(blob.seed || ""), path: blob.path || null };
}
throw new Error("unknown import kind: " + blob.kind);
}
// Vault-derived. Two genuinely useful forms: the account xprv, which
// other HD wallets accept, and the 32-byte purpose root Aegis derives
// from. Sia's recovery() calls its seed "xprv" and it is the same bytes
// as the root, so don't report it twice.
let xprv = null, xpub = null, accountPath = null;
const rt = ctx.runtimes.get(entry.id);
if (rt && rt.adapter && typeof rt.adapter.recovery === "function") {
try {
const r = rt.adapter.recovery();
xpub = r.xpub || null;
accountPath = r.accountPath || null;
if (entry.chain !== "sc") xprv = r.xprv || null;
} catch { /* public material is a bonus, not the point */ }
}
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 root = await api.vault.derive(entry.purpose);
let rootHex = "";
try { rootHex = Array.from(root, (b) => b.toString(16).padStart(2, "0")).join(""); }
finally { try { root.fill(0); } catch {} }
return { ...base, form: "vault", secret: rootHex, purpose: entry.purpose, xprv, xpub, accountPath };
});
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
// Opt-in USD prices. Persist the choice so restart doesn't silently
// disable it, and kick a fetch immediately when switched on.
api.onMessage("setPricesEnabled", async (p, m) => {
fromPanel(m);
const on = !!(p && p.enabled);
api.storage.set("pricesEnabled", on);
if (ctx.priceFeed) await ctx.priceFeed.setEnabled(on);
return fullState();
});
api.onMessage("refreshPrices", async (_p, m) => {
fromPanel(m);
if (ctx.priceFeed) await ctx.priceFeed.refresh();
return fullState();
});
api.onMessage("setPricesSource", async (p, m) => {
fromPanel(m);
const id = String(p && p.source || "").trim();
if (!id) throw new Error("source required");
api.storage.set("pricesSource", id);
if (ctx.priceFeed) await ctx.priceFeed.setSource(id);
return fullState();
});
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: pair a wiz:// URI with a specific BCH wallet.
api.onMessage("wcConnect", async (p, m) => {
fromPanel(m);
if (!ctx.wc) throw new Error("WizardConnect not ready");
const walletId = String(p && p.walletId || "");
const uri = String(p && p.uri || "");
await ctx.wc.connectUri(walletId, uri, "panel");
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
return fullState();
});
api.onMessage("wcDisconnect", async (p, m) => {
fromPanel(m);
if (!ctx.wc) throw new Error("WizardConnect not ready");
await ctx.wc.disconnect(String(p && p.walletId || ""), String(p && p.connId || ""));
return fullState();
});
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
// Scan the open dapp tab for a wiz:// pairing code, for dapps that render
// one but haven't adopted window.wizardconnect. Strictly user-initiated —
// it runs when someone presses "Scan page", never on a timer and never in
// the background. The host does the matching and returns only the URIs, so
// Aegis never receives page content.
api.onMessage("wcScanPage", async (_p, m) => {
fromPanel(m);
if (typeof api.scanActiveTabForUris !== "function") {
throw new Error("This Theseus build can't scan pages yet — update Theseus, or paste the wiz:// code manually.");
}
const { origin, uris } = await api.scanActiveTabForUris({ scheme: "wiz", limit: 10 });
return { origin: origin || null, uris: Array.isArray(uris) ? uris : [] };
});
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
// ---- WizardConnect from the page (0.8.8) --------------------------------
//
// WC was built for cross-device pairing: the dapp renders a QR, a phone
// scans it. Same-device that means copying a wiz:// string out of one
// tab and into the wallet by hand. These two handlers back the
// window.wizardconnect bridge so a dapp can hand Aegis the URI it has
// already generated, and the user just approves.
// Which BCH wallets can actually pair right now. Used by the page bridge
// AND by isReady() so a dapp can decide between "hand it to Aegis" and
// "render the QR" before it commits to either.
function wcPairableWallets() {
if (!ctx.wc) return [];
return walletEntries()
.filter((w) => w.chain === "bch")
.map((w) => ({ entry: w, rt: ctx.runtimes.get(w.id) }))
.filter(({ entry, rt }) => rt && rt.phase === "ready" && !wcIneligible.has(entry.id))
.map(({ entry }) => entry);
}
api.onMessage("wcPageReady", async (_p, m) => {
fromPage(m);
// Deliberately coarse: a page learns only whether pairing is possible,
// never how many wallets exist or what they are.
return { available: !!ctx.wc, pairable: wcPairableWallets().length > 0 };
});
api.onMessage("wcConnectFromPage", async (p, m) => {
const origin = fromPage(m);
if (!ctx.wc) throw new Error("WizardConnect is still starting up — try again in a moment");
const uri = String(p && p.uri || "").trim();
// Validate before showing any UI so a malformed or hostile value can't
// put a confusing approval in front of the user.
if (!/^wiz:\/\//i.test(uri)) throw new Error("not a WizardConnect URI");
if (uri.length > 4096) throw new Error("WizardConnect URI is implausibly long");
const candidates = wcPairableWallets();
if (!candidates.length) {
throw new Error("No Bitcoin Cash wallet is ready to pair. Unlock the Aegis vault (or add a BCH wallet) and try again.");
}
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
// Which wallet to offer first: the one nominated for WizardConnect, else
// the one the panel is showing, else list order. Whatever wins is only a
// default — with more than one candidate the approval lets the user say.
const wcRole = walletRoles().wizardconnect;
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 selId = selectedWalletId();
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
const preferred = candidates.find((w) => w.id === wcRole)
|| candidates.find((w) => w.id === selId)
|| candidates[0];
// Default first: approval.html renders options in order, so the browser
// selects the head of the list.
const ordered = [preferred, ...candidates.filter((w) => w.id !== preferred.id)];
const addrOf = (w) => {
try { return ctx.runtimes.get(w.id)?.adapter?.snapshot()?.address || ""; }
catch { return ""; }
};
// The address, not just the label — "I don't recognise the connected
// wallet" is the failure this dialog has to prevent, and a label the user
// never chose ("BCH wallet 2") does not prevent it.
const describe = (w) => {
const a = addrOf(w);
return a ? `${w.label} · ${shortAddr(a)}` : w.label;
};
const choose = ordered.length > 1;
// With a choice on offer the dropdown is the only honest statement of
// which wallet pairs: the rows are static, so a "Wallet: X" line above a
// select the user just changed to Y would contradict itself. Each option
// therefore carries its own short address, and the full-address row shows
// only when there is nothing to choose.
const rows = choose
? [{ label: "Pairing code", value: uri.slice(0, 48) + (uri.length > 48 ? "…" : ""), mono: true }]
: [
{ label: "Wallet", value: preferred.label, strong: true },
{ label: "Address", value: addrOf(preferred) || "—", mono: true },
{ label: "Pairing code", value: uri.slice(0, 48) + (uri.length > 48 ? "…" : ""), mono: true },
];
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 withOriginLock(origin, async () => {
const pick = await api.approvalModal({
title: "Pair this site with your wallet?",
origin,
body: "The site will be able to ask Aegis to sign Bitcoin Cash transactions over WizardConnect. Every signature still needs your approval — pairing on its own moves no funds.",
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
rows,
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: [{ id: "allow", label: "Pair", primary: true }],
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
select: choose ? {
id: "wallet", label: "Pair with",
options: ordered.map((w) => ({ value: w.id, label: describe(w) })),
} : null,
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
});
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
const [action, ...flags] = String(pick || "").split("+");
if (action !== "allow") throw new Error("pairing declined");
const picked = flags.find((f) => f.startsWith("wallet="));
const pickedId = picked ? picked.slice(7) : "";
// Re-check against the candidate list: main validates the value came
// from the options we offered, but the wallet could have gone away
// while the dialog was open.
const chosen = candidates.find((w) => w.id === pickedId) || preferred;
await ctx.wc.connectUri(chosen.id, uri, origin);
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 { paired: true, wallet: chosen.label };
});
});
// Reorder wallets by an explicit ID list. Silently drops IDs that are
// not in the current wallet set (removed since the panel last read);
// appends any wallets missing from `order` to the end of the list so a
// stale panel reorder cannot make a wallet vanish from the strip.
api.onMessage("reorderWallets", (p, m) => {
fromPanel(m);
const order = Array.isArray(p && p.order) ? p.order.map(String) : [];
const current = walletEntries();
const byId = new Map(current.map((w) => [w.id, w]));
const next = [];
const seen = new Set();
for (const id of order) {
if (byId.has(id) && !seen.has(id)) { next.push(byId.get(id)); seen.add(id); }
}
for (const w of current) if (!seen.has(w.id)) next.push(w);
writeWallets(api, next);
emitState();
return fullState();
});
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
// Which wallet each purpose reaches for. Panel-only: a page must never be
// able to read the whole wallet set, let alone re-point a role at one.
api.onMessage("walletRoles", (_p, m) => { fromPanel(m); return walletRoles(); });
api.onMessage("setWalletRole", (p, m) => {
fromPanel(m);
const role = String(p && p.role || "");
const id = p && p.walletId ? String(p.walletId) : null;
setWalletRole(role, id);
emitState();
// Full state, not just the role map — the panel assigns this straight
// onto `state`, and the badges it drives live on the wallet rows.
return fullState();
});
// ---- seed import: which derivation path actually holds the funds? -------
// The import form prefilled exactly one path, so a seed from a wallet that
// used a different one imported a valid, empty address and reported 0 with
// no way to tell "wrong path" from "empty wallet". Both real cases hit it:
// Bitcoin.com derives mainnet BCH from coin type 0' (its Copay lineage
// predates the 145' split), and chipnet tooling generally uses 145' rather
// than BIP44's testnet 1' — which is what Aegis defaulted chipnet to.
//
// So ask the chain instead of guessing. Each candidate is scanned for
// history and balance, and the panel reports what it found.
const SEED_PATH_CANDIDATES = {
"bch/mainnet": [
{ path: "m/44'/145'/0'/0/0", note: "BCH standard — Electron Cash, Zapit, newer Bitcoin.com" },
{ path: "m/44'/0'/0'/0/0", note: "BTC coin type — Bitcoin.com, Copay, BitPay" },
{ path: "m/44'/145'/0'", note: "account node — some QR exports" },
{ path: "m/0'/0/0", note: "pre-BIP44" },
],
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
// DigiByte. Both master-key variants are covered, because the 2018-19
// official mobile wallets (BreadWallet forks) used HMAC "DigiByte seed"
// instead of BIP32's "Bitcoin seed" — the same words then produce a
// completely different key tree and a standard scan finds nothing.
// Finding from Digibyte.X/dgb-wallet @ 989a2696.
"dgb/mainnet": [
{ path: "m/84'/20'/0'/0/0", note: "BIP84 native SegWit — dgb1q…, the modern default" },
{ path: "m/44'/20'/0'/0/0", note: "BIP44 legacy — D…" },
{ path: "m/49'/20'/0'/0/0", note: "BIP49 wrapped SegWit — S…" },
{ path: "m/86'/20'/0'/0/0", note: "BIP86 Taproot — dgb1p…" },
{ path: "m/0'/0/0", note: "official 2018-19 mobile wallet", hmacKey: "DigiByte seed" },
{ path: "m/44'/20'/0'/0/0", note: "BIP44 with the 2018-19 mobile master key", hmacKey: "DigiByte seed" },
],
"bch/chipnet": [
{ path: "m/44'/145'/0'/0/0", note: "BCH coin type on chipnet — most chipnet tooling" },
{ path: "m/44'/1'/0'/0/0", note: "BIP44 testnet coin type" },
{ path: "m/44'/145'/0'", note: "account node" },
],
};
api.onMessage("seedPathCandidates", (p, m) => {
fromPanel(m);
const key = `${String(p && p.chain || "")}/${String(p && p.network || "")}`;
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
return { key, candidates: (SEED_PATH_CANDIDATES[key] || []).map((c) => ({ ...c })) };
});
api.onMessage("scanSeedPaths", async (p, m) => {
fromPanel(m);
const chain = String(p && p.chain || "bch");
const network = String(p && p.network || "");
const cands = SEED_PATH_CANDIDATES[`${chain}/${network}`];
if (!cands) throw new Error(`Path scanning is not available for ${chain} ${network} yet.`);
const mnemonic = String(p && p.mnemonic || "").trim();
if (!mnemonic) throw new Error("seed phrase required");
// Same conversion importWallet does. Never stored here — this handler
// derives, queries and returns counts, nothing else.
let seedHex;
try { seedHex = ctx.d.derive.mnemonicToSeedHex(mnemonic); }
catch (e) { throw new Error("That does not look like a BIP39 seed phrase: " + (e?.message || e)); }
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
// Per-chain: where to ask, how to turn a path into an address, and how to
// key a script for Electrum. Everything below is chain-agnostic.
let servers, addressAt, scripthashOf;
if (chain === "bch") {
const prefix = network === "mainnet" ? "bitcoincash" : "bchtest";
servers = network === "mainnet"
? bchServerList(api)
: ["wss://chipnet.imaginary.cash:50004", "wss://chipnet.bch.ninja:50004"];
addressAt = (pth) => deriveCashaddrFromSeed(seedHex, pth, prefix);
scripthashOf = (addr) => {
const d = ctx.d.cashaddr.decode(addr);
const h = Buffer.from(d.hash);
const spk = d.type === 1
? Buffer.concat([Buffer.from([0xa9, 0x14]), h, Buffer.from([0x87])])
: Buffer.concat([Buffer.from([0x76, 0xa9, 0x14]), h, Buffer.from([0x88, 0xac])]);
return Buffer.from(ctx.d.sha256(new Uint8Array(spk))).reverse().toString("hex");
};
} else if (chain === "dgb") {
if (!ctx.d.dgbCore) throw new Error("the DigiByte modules did not load, so paths cannot be scanned");
servers = ["wss://electrum1.cipig.net:20063", "wss://electrum2.cipig.net:20063"];
// hmacKey rides on the candidate, so both master-key variants are
// scanned in the same pass.
addressAt = (pth, hmacKey) => ctx.d.derive.dgb.fromSeed(seedHex, pth, hmacKey);
scripthashOf = (addr) => {
// bitcoinjs knows every DGB output type (D…, S…, dgb1q…, dgb1p…),
// so let it build the scriptPubKey rather than hand-rolling four.
const spk = ctx.d.bitcoinjs.address.toOutputScript(addr, ctx.d.dgbCore.digibyte);
return Buffer.from(ctx.d.sha256(new Uint8Array(spk))).reverse().toString("hex");
};
} else {
throw new Error(`Path scanning is not available for ${chain} yet.`);
}
const client = new ctx.d.electrum.Client(servers);
// Look a few addresses deep per candidate: a wallet whose first receive
// address is spent clean still has history, and funds often sit further
// along the branch.
const DEPTH = 5;
const out = [];
try {
for (const c of cands) {
const acct = wcAccountPath(c.path) || c.path;
const probes = [];
for (let i = 0; i < DEPTH; i++) probes.push(`${acct}/0/${i}`);
// The exact path the user would be importing, in case it is a leaf
// outside the account/0/i shape we probe.
if (!probes.includes(c.path) && /\/\d+$/.test(c.path)) probes.unshift(c.path);
let balance = 0, txCount = 0, firstAddress = null, fundedAddress = null;
for (const [idx, pth] of probes.entries()) {
let addr;
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
try { addr = addressAt(pth, c.hmacKey); }
catch { continue; }
if (idx === 0 || firstAddress === null) firstAddress = firstAddress || addr;
const sh = scripthashOf(addr);
try {
const bal = await client.call("blockchain.scripthash.get_balance", [sh]);
const got = Number(bal?.confirmed || 0) + Number(bal?.unconfirmed || 0);
if (got > 0 && !fundedAddress) fundedAddress = addr;
balance += got;
const hist = await client.call("blockchain.scripthash.get_history", [sh]);
txCount += Array.isArray(hist) ? hist.length : 0;
} catch (e) { api.log("scanSeedPaths:", pth, e?.message || e); }
}
out.push({
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
path: c.path, note: c.note, accountPath: acct, hmacKey: c.hmacKey || null,
firstAddress, fundedAddress, balance, txCount, scanned: probes.length,
});
}
} finally { try { client.disconnect(); } catch {} }
const meta = chainMeta(chain, network);
return {
chain, network, decimals: meta?.decimals || 8, ticker: meta?.ticker || "BCH",
results: out,
// Rank for the panel: funds first, then any history at all.
best: out.slice().sort((a, b) => (b.balance - a.balance) || (b.txCount - a.txCount))[0]?.path || null,
};
});
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 --------------------------------------------
const injectState = () => ({
supported: !!(api.features && api.features.pageInjectPolicy),
mode: injectMode(api),
origins: injectAllowList(api),
});
api.onMessage("injectPolicyGet", (_p, m) => { fromPanel(m); return injectState(); });
api.onMessage("injectPolicySet", (p, m) => {
fromPanel(m);
if (!(api.features && api.features.pageInjectPolicy)) throw new Error("this Theseus cannot limit which sites see the wallet — update Theseus");
api.storage.set(INJECT_MODE_KEY, p && p.mode === "allowed" ? "allowed" : "all");
syncInjectPolicy(api);
return injectState();
});
// Enable one site. With no origin given it is the site in the active tab,
// which the host reports — the panel never has to type or guess a URL.
api.onMessage("injectAllowSite", async (p, m) => {
fromPanel(m);
let origin = normalizeOrigin(p && p.origin);
if (!origin && typeof api.scanActiveTabForUris === "function") {
try { origin = normalizeOrigin((await api.scanActiveTabForUris({ scheme: "wiz", limit: 1 })).origin); } catch {}
}
if (!origin) throw new Error("Open the site in a tab first — there is no web page in the active tab.");
const list = injectAllowList(api);
if (!list.includes(origin)) list.push(origin);
api.storage.set(INJECT_ALLOW_KEY, list);
syncInjectPolicy(api);
return { origin, ...injectState() };
});
api.onMessage("injectRemoveSite", (p, m) => {
fromPanel(m);
const origin = String(p && p.origin || "");
api.storage.set(INJECT_ALLOW_KEY, injectAllowList(api).filter((o) => o !== origin));
syncInjectPolicy(api);
return injectState();
});
try { syncInjectPolicy(api); } catch {}
api.onMessage("permissions", (_p, m) => { fromPanel(m); return grantsForPanel(api); });
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
api.onMessage("revoke", (p, m) => {
fromPanel(m);
revokeOrigin(api, String(p && p.origin || ""));
emitState();
return grantsForPanel(api);
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
});
// ---- security: quick-access PIN + policy flags ------------------------
// The PIN blob is a WebCrypto AES-GCM ciphertext of the master password,
// derived from PBKDF2(pin, salt). Panel handles the actual encryption /
// decryption inside its iframe — the master password never crosses the
// process boundary except via vaultUnlock. These handlers only shuttle
// the opaque blob + a small policy object in and out of api.storage.
// Set or replace the PIN. The master password is checked against the vault
// first, and the blob is built here, so the panel never holds one.
api.onMessage("pinSet", async (p, m) => {
fromPanel(m);
const pin = String((p && p.pin) || "");
const pw = String((p && p.masterPassword) || "");
if (!PIN_RE.test(pin)) throw new Error("the PIN must be 6 digits");
if (!pw) throw new Error("master password required");
try { await api.vault.lifecycle.unlock(pw); }
catch { throw new Error("wrong master password"); }
noteMasterVerified(api);
if (!osSealUsable(safeStorageOr(api))) {
throw new Error("this system has no protected keystore, so a PIN cannot be stored safely; Aegis will ask for the master password instead");
}
const old = openPinBlob(api);
const hw = await tryTpmKey(api, pin);
const sealed = sealPinBlob(api, await pinWrap(pin, pw, hw));
if (!sealed) { if (hw) tpmPin.remove(hw.keyName).catch(() => {}); throw new Error("the PIN could not be sealed by the OS keystore"); }
api.storage.set(PIN_BLOB_KEY, sealed);
if (old?.hw?.key && old.hw.key !== hw?.keyName) tpmPin.remove(old.hw.key).catch(() => {});
return { ok: true, hardware: hw ? "tpm" : "none" };
});
// Try a PIN. Counts the guess before trying it, so a crash or a closed
// panel mid-check still costs an attempt. Answers
// { ok: true, masterPassword } | { ok: false, remaining, lockedMs }
api.onMessage("pinUnwrap", async (p, m) => {
fromPanel(m);
const pin = String((p && p.pin) || "");
const blob = openPinBlob(api);
if (!blob) throw new Error("no PIN is set");
const st = pinFails(api);
if (st.lockedMs > 0) return { ok: false, remaining: 0, lockedMs: st.lockedMs };
api.storage.set("aegis/pin/failCount", st.fails + 1);
api.storage.set("aegis/pin/failLast", Date.now());
let pw = null;
if (PIN_RE.test(pin)) {
let secret = null;
if (blob.hw) {
// The chip decides first. Its own counter is the limit that holds
// even against someone who resets ours (it lives in this same file).
const r = await tpmPin.open(blob.hw.key, blob.hw.wrapped, pin);
if (r.ok) secret = r.secret;
else if (r.code === "locked" || r.code === "error") {
// Not a verdict on the PIN: give the attempt back.
api.storage.set("aegis/pin/failCount", st.fails);
api.storage.set("aegis/pin/failLast", st.last);
return { ok: false, remaining: Math.max(0, PIN_MAX_FAILS - st.fails), lockedMs: 0, hwLocked: r.code === "locked",
error: r.code === "locked" ? "The security chip is refusing PINs for a few minutes after too many wrong ones. Use the master password, or wait." : "The security chip did not answer. Use the master password." };
} else if (r.code === "missing") {
// The TPM key is gone (profile copied to another machine, TPM
// cleared): this PIN can never open again.
api.storage.set(PIN_BLOB_KEY, null);
api.storage.set("aegis/pin/failCount", 0);
api.storage.set("aegis/pin/failLast", 0);
return { ok: false, remaining: 0, lockedMs: 0, pinGone: true, error: "This PIN was tied to a security chip that no longer has its key. Enter the master password and set the PIN again." };
}
}
if (!blob.hw || secret) { try { pw = await pinUnwrapBlob(pin, blob, secret); } catch { pw = null; } }
}
if (pw == null) {
const now = pinFails(api);
return { ok: false, remaining: Math.max(0, PIN_MAX_FAILS - now.fails), lockedMs: now.lockedMs };
}
api.storage.set("aegis/pin/failCount", 0);
api.storage.set("aegis/pin/failLast", 0);
// A blob from an older build (200k iterations, from before sealing, or
// without a TPM key on a machine that has one) is re-made now, under a
// fresh salt, while the PIN is at hand.
if (!blob.hw && ((Number(blob.iters) || 0) < PIN_ITERS || (tpmPin.supported() && !tpmUnavailableThisBoot))) {
try {
const hw = await tryTpmKey(api, pin);
if ((Number(blob.iters) || 0) < PIN_ITERS || hw) {
const sealed = sealPinBlob(api, await pinWrap(pin, pw, hw));
if (sealed) api.storage.set(PIN_BLOB_KEY, sealed);
else if (hw) tpmPin.remove(hw.keyName).catch(() => {});
}
} catch (e) { api.log("PIN re-wrap:", e?.message || e); }
}
return { ok: true, masterPassword: pw };
});
api.onMessage("pinStatus", (_p, m) => {
fromPanel(m);
const f = pinFails(api);
return { hasPin: !!api.storage.get(PIN_BLOB_KEY, null), fails: f.fails, last: f.last, maxFails: PIN_MAX_FAILS, lockedMs: f.lockedMs, ...pinProtection() };
});
api.onMessage("pinBlobClear", (_p, m) => {
fromPanel(m);
const old = openPinBlob(api);
if (old?.hw?.key) tpmPin.remove(old.hw.key).catch(() => {});
api.storage.set("aegis/pin/v1", null);
api.storage.set("aegis/pin/failCount", 0);
api.storage.set("aegis/pin/failLast", 0);
// Drop the gate record as well, so enrolling a new PIN later starts from
// "not yet satisfied" rather than inheriting the old PIN's clearance.
api.storage.set("aegis/pin/gate", null);
return true;
});
// When to ask for the PIN. These are independent triggers, not a single
// mode: wanting one at startup and one per transaction is a normal
// combination. Previously the only control was requirePinForSending, which
// produced the behaviour the user reported — Aegis opens unlocked after a
// restart (safeStorage remembered the password) and then demands a PIN the
// moment you touch something. Asking at the door or not at all is
// coherent; asking only once you are inside is not.
//
// `restart` defaults ON for a wallet that otherwise reopens fully unlocked.
// `transaction` inherits the old requirePinForSending so nobody silently
// loses a gate they had chosen.
function pinPolicy() {
const cfg = api.storage.get("aegis/security/v1", {}) || {};
const on = (cfg.pinOn && typeof cfg.pinOn === "object") ? cfg.pinOn : null;
return {
restart: on ? !!on.restart : true,
launch: on ? !!on.launch : false,
interval: on ? !!on.interval : false,
transaction: on ? !!on.transaction : !!cfg.requirePinForSending,
};
}
// What actually protects the PIN on this machine, for honest wording in the
// panel: "tpm" = the chip limits guesses; "none" = only PBKDF2 + the OS
// keystore, which falls to anyone who can run as this user.
function pinProtection() {
const blob = api.storage.get(PIN_BLOB_KEY, null) ? openPinBlob(api) : null;
const exposed = api.storage.get(PIN_EXPOSED_KEY, null);
return {
pinHardware: blob ? (blob.hw ? "tpm" : "none") : null,
pinStorable: osSealUsable(safeStorageOr(api)),
pinLegacyExposure: exposed && !exposed.dismissed ? { at: exposed.at } : null,
};
}
api.onMessage("pinExposureDismiss", (_p, m) => {
fromPanel(m);
const e = api.storage.get(PIN_EXPOSED_KEY, null);
if (e) api.storage.set(PIN_EXPOSED_KEY, { ...e, dismissed: Date.now() });
return true;
});
const pinGate = () => {
const g = api.storage.get("aegis/pin/gate", null);
return (g && typeof g === "object") ? g : {};
};
// Does the user have to prove the PIN right now? Decided host-side: the
// panel reloads freely and must not be the thing that remembers whether a
// gate was already satisfied.
function pinNeeded(event) {
if (!api.storage.get("aegis/pin/v1", null)) return { needPin: false, reason: "no-pin" };
const on = pinPolicy();
const g = pinGate();
if (on.interval) {
// Never satisfied, or satisfied too long ago. Checked for every event
// so a six-hour expiry also lands on the next transaction.
if (!g.lastOkAt || (Date.now() - Number(g.lastOkAt)) > PIN_INTERVAL_MS) {
return { needPin: true, reason: "interval" };
}
}
if (event === "transaction" && on.transaction) return { needPin: true, reason: "transaction" };
if (event === "panel-load") {
if (on.launch) return { needPin: true, reason: "launch" };
if (on.restart && g.bootId !== BOOT_ID) return { needPin: true, reason: "restart" };
}
return { needPin: false, reason: "satisfied" };
}
api.onMessage("pinGateStatus", (p, m) => {
fromPanel(m);
const event = String(p && p.event || "panel-load");
return { ...pinNeeded(event), event, policy: pinPolicy() };
});
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 panel decrypts the PIN blob and hands over what was inside it. That
// is the vault master password, and the host checks it against the vault
// itself — so a gate is cleared by proof the host verified, not by the
// panel saying so. The same proof opens a single-use transaction
// clearance (see requirePanelTxPin / requireDappTxPin).
api.onMessage("pinGateSatisfied", async (p, m) => {
fromPanel(m);
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 pw = String((p && p.masterPassword) || "");
if (!pw) throw new Error("PIN proof required");
if (!api.vault?.lifecycle || typeof api.vault.lifecycle.unlock !== "function") throw new Error("this build cannot verify a PIN");
try { await api.vault.lifecycle.unlock(pw); }
catch { throw new Error("PIN proof rejected"); }
noteMasterVerified(api);
api.storage.set("aegis/pin/gate", { lastOkAt: Date.now(), bootId: BOOT_ID });
// What this proof clears (see requireDappTxPin).
const forRequest = String((p && p.requestId) || "");
if (forRequest) {
const req = pendingPinRequests.get(forRequest);
if (!req) throw new Error("that request is no longer waiting");
req.cleared = true;
} else if (String((p && p.event) || "") === "transaction") {
panelClearanceUntil = Date.now() + TX_CLEARANCE_MS;
}
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
// A panel opened while a dapp transaction is waiting for the PIN picks the
// request up here; one already open gets the "pinRequest" event instead.
api.onMessage("pinRequestPending", (_p, m) => {
fromPanel(m);
// The oldest waiting request that is not answered yet.
for (const r of pendingPinRequests.values()) if (!r.cleared) return { id: r.id, origin: r.origin, what: r.what, at: r.at };
return null;
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
});
ctx.pinNeeded = pinNeeded;
// Seal a plain blob left by an older build now, not when the panel next
// happens to open.
try { openPinBlob(api); } catch {}
api.onMessage("securityGet", (_p, m) => {
fromPanel(m);
const cfg = api.storage.get("aegis/security/v1", {}) || {};
return {
hasPin: !!api.storage.get("aegis/pin/v1", null),
pinOn: pinPolicy(),
pinIntervalHours: PIN_INTERVAL_MS / 3600000,
requirePinForSending: !!cfg.requirePinForSending,
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
// Defaults ON (note the !== false), unlike the send flag: a send is
// already fronted by an approval overlay, whereas revealing a key is
// irreversible the moment it is on screen. Turning this off does not
// make a secret free to read — it moves the prompt to the master
// password, which is the stronger credential, not a weaker one.
requirePinForReveal: cfg.requirePinForReveal !== false,
...pinProtection(),
};
});
api.onMessage("securitySet", (p, m) => {
fromPanel(m);
const cur = api.storage.get("aegis/security/v1", {}) || {};
const next = { ...cur };
if (p && typeof p.requirePinForSending === "boolean") next.requirePinForSending = p.requirePinForSending;
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
if (p && typeof p.requirePinForReveal === "boolean") next.requirePinForReveal = p.requirePinForReveal;
if (p && p.pinOn && typeof p.pinOn === "object") {
// Write the whole set from the current policy plus the keys given, so
// the first edit materialises the migrated defaults instead of leaving
// three triggers undefined and one set.
const cur = pinPolicy();
const merged = { ...cur };
for (const k of ["restart", "launch", "interval", "transaction"]) {
if (typeof p.pinOn[k] === "boolean") merged[k] = p.pinOn[k];
}
next.pinOn = merged;
// The legacy flag now lives in pinOn.transaction; keep them in step so
// an older build reading this store still gates sends the same way.
next.requirePinForSending = merged.transaction;
}
api.storage.set("aegis/security/v1", next);
return {
hasPin: !!api.storage.get("aegis/pin/v1", null),
pinOn: pinPolicy(),
pinIntervalHours: PIN_INTERVAL_MS / 3600000,
requirePinForSending: !!next.requirePinForSending,
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
requirePinForReveal: next.requirePinForReveal !== false,
...pinProtection(),
};
});
// ---- session: stay-signed-in + idle-lock + manual sign out ------------
// "Stay signed in" persists the master password across Theseus restarts
// using electron.safeStorage — an OS-level protected keystore (Windows
// DPAPI, macOS Keychain, libsecret on Linux). The encrypted blob only
// decrypts under the same OS user account, so filesystem-only access
// (SSH from another user, a lost backup) cannot use it.
//
// Storage:
// aegis/session/enc — { encPwB64, savedAt } — safeStorage blob
// aegis/session/cfg — { lockOnClose: bool, idleMinutes: number }
//
// Defaults: lockOnClose=true, idleMinutes=15. The user opts in to
// remember-me by turning "Lock on Navigator close" off in Settings.
api.onMessage("sessionStatus", (_p, m) => {
fromPanel(m);
return sessionStatusFor(api);
});
api.onMessage("sessionConfigSet", (p, m) => {
fromPanel(m);
const cur = api.storage.get("aegis/session/cfg", null) || { lockOnClose: true, idleMinutes: 15 };
const next = { ...cur };
if (p && typeof p.lockOnClose === "boolean") next.lockOnClose = p.lockOnClose;
if (p && typeof p.idleMinutes === "number") {
const im = Math.max(0, Math.min(180, Math.floor(p.idleMinutes)));
next.idleMinutes = im;
}
api.storage.set("aegis/session/cfg", next);
// Turning "Lock on close" on invalidates any stored remember-me blob.
if (next.lockOnClose) api.storage.set("aegis/session/enc", null);
return sessionStatusFor(api);
});
api.onMessage("sessionEnable", async (p, m) => {
fromPanel(m);
const pw = String(p && p.masterPassword || "");
if (!pw) throw new Error("master password required");
const ss = safeStorageOr(api);
// Same bar as the PIN: Linux's basic_text "keystore" is a constant key,
// i.e. the master password in the clear.
if (!osSealUsable(ss)) throw new Error("OS keystore unavailable — remember-me needs Windows DPAPI / macOS Keychain / libsecret");
// Store only a password the vault accepts.
try { await api.vault.lifecycle.unlock(pw); }
catch { throw new Error("wrong master password"); }
noteMasterVerified(api);
const enc = ss.encryptString(pw).toString("base64");
api.storage.set("aegis/session/enc", { encPwB64: enc, savedAt: Date.now() });
// Force lockOnClose = false alongside — semantically they're the same
// switch as far as the user's UI expects.
const cur = api.storage.get("aegis/session/cfg", {}) || {};
api.storage.set("aegis/session/cfg", { ...cur, lockOnClose: false });
return sessionStatusFor(api);
});
api.onMessage("sessionDisable", (_p, m) => {
fromPanel(m);
api.storage.set("aegis/session/enc", null);
const cur = api.storage.get("aegis/session/cfg", {}) || {};
api.storage.set("aegis/session/cfg", { ...cur, lockOnClose: true });
return sessionStatusFor(api);
});
// Manual sign-out: locks the vault (main-process re-locks it in memory)
// and drops the runtime cache. Also wipes any remember-me blob so the
// NEXT Theseus launch will require the master password again — the user
// just said "sign me out", not "sign me out just for this restart".
api.onMessage("vaultLock", async (_p, m) => {
fromPanel(m);
api.storage.set("aegis/session/enc", null);
for (const walletId of Array.from(ctx.runtimes.keys())) unmountWallet(walletId);
try { await api.vault.lifecycle.lock(); } catch (e) { api.log("vault lock:", e?.message || e); }
emitState();
return fullState();
});
}
// Read session status. Kept as a plain helper so both the message handler
// and the startup auto-unlock path can call it without duplicating shape.
function sessionStatusFor(api) {
const cfg = api.storage.get("aegis/session/cfg", null) || { lockOnClose: true, idleMinutes: 15 };
const blob = api.storage.get("aegis/session/enc", null);
const ss = safeStorageOr(api);
return {
lockOnClose: !!cfg.lockOnClose,
idleMinutes: Number(cfg.idleMinutes) || 0,
hasSession: !!(blob && blob.encPwB64),
safeStorageAvailable: osSealUsable(ss),
};
}
// Best-effort access to electron.safeStorage from inside the addon. The
// addon runs in the main process, so require("electron") gives us the
// full main-process API; on hosts that shadow this (tests, older builds)
// we degrade to "unavailable" instead of throwing.
function safeStorageOr(api) {
try {
const e = api.require ? api.require("electron") : require("electron");
return e && e.safeStorage ? e.safeStorage : null;
} catch { return null; }
}
// Called from activate() after deps + WC init, BEFORE mountAllWallets.
// If the user opted into stay-signed-in AND we have a stored blob AND
// safeStorage can decrypt it under this OS user → auto-unlock the vault.
// Any failure is silent (log-only) — mountAllWallets will fall back to
// the panel's lock screen exactly as before.
async function tryAutoUnlock(api) {
try {
const status = await api.vault.lifecycle.status();
if (status && status.unlocked) return;
} catch {}
const cfg = api.storage.get("aegis/session/cfg", null) || { lockOnClose: true, idleMinutes: 15 };
if (cfg.lockOnClose) return;
const blob = api.storage.get("aegis/session/enc", null);
if (!blob || !blob.encPwB64) return;
const ss = safeStorageOr(api);
if (!osSealUsable(ss)) {
// Sealed under no real keystore: it is the master password in the
// clear on disk. Never use it; remove it.
api.storage.set("aegis/session/enc", null);
return;
}
try {
const pw = ss.decryptString(Buffer.from(blob.encPwB64, "base64"));
await api.vault.lifecycle.unlock(pw);
api.log("auto-unlocked via safeStorage session");
} catch (e) {
api.log("auto-unlock failed:", e?.message || e);
// Drop the stale blob so we don't retry every launch.
api.storage.set("aegis/session/enc", 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
// One "describePlan" is enough for both chains because plan() returns a
// common shape: {recipients:[{to,value}], fee, feeRate, total, inputs:[]…}.
function describePlan(plan) {
const sent = plan.recipients.reduce((a, r) => a + r.value, 0);
return {
recipients: plan.recipients, fee: plan.fee, feeRate: plan.feeRate,
inputs: (plan.inputs || []).length, change: plan.change || null,
total: plan.total != null ? plan.total : (sent + plan.fee),
};
}
// ---- dapp bridges (page → activate()) --------------------------------------
// Permission model stays the same as the single-wallet build for BCH:
// { [origin]: { readAddress:true, sendTx:{capSats,usedSats,grantedAt},
// trx: { readAddress:true, network } } }
// The BCH bridge always talks to the LEGACY default BCH wallet (the .x pages
// pre-date multi-wallet and cannot pick between them). The Tron bridge talks
// to the currently-SELECTED Tron wallet; if none is selected, requests fail.
const BCH_ALLOWANCES = [100000, 1000000, 10000000]; // 0.001, 0.01, 0.1 BCH
const pendingByOrigin = new Set();
function permissions(api) { const p = api.storage.get("permissions", {}); return p && typeof p === "object" ? p : {}; }
// ---- grants: session + persisted ------------------------------------------
// A plain "Connect" grants the origin for this browser session (memory only);
// ticking "Always allow" persists it under api.storage "permissions". Both
// look identical to the handlers via grantFor(). Scopes: "bch" (the
// window.bitcoincash bridge keeps its flat readAddress), "eth", "sol", "trx".
// ETH grants also carry the origin's chainId — chain is per origin, never a
// global the sidebar or another site can flip under a connected dapp.
// Before this, a plain "Connect" stored nothing, so the very next call from
// the site failed with "not connected".
const sessionGrants = new Map(); // origin -> { readAddress?, eth?, sol?, trx? }
function grantFor(api, origin, scope) {
const persisted = permissions(api)[origin] || {};
const session = sessionGrants.get(origin) || {};
if (scope === "bch") {
if (persisted.readAddress) return { readAddress: true, persisted: true };
if (session.readAddress) return { readAddress: true, persisted: false };
return null;
}
const p = persisted[scope], s = session[scope];
if (p && p.readAddress) return { ...p, persisted: true };
if (s && s.readAddress) return { ...s, persisted: false };
return null;
}
function setGrant(api, origin, scope, value, persist) {
const wrap = scope === "bch" ? value : { [scope]: value };
if (persist) {
const perms = permissions(api);
perms[origin] = { ...(perms[origin] || {}), ...wrap };
api.storage.set("permissions", perms);
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
syncInjectPolicy(api);
} else {
sessionGrants.set(origin, { ...(sessionGrants.get(origin) || {}), ...wrap });
}
emitState();
}
// Patch fields (e.g. chainId) on whichever grant the origin currently holds.
function patchGrant(api, origin, scope, patch) {
const g = grantFor(api, origin, scope);
if (!g) return false;
const { persisted, ...rest } = g;
setGrant(api, origin, scope, { ...rest, ...patch }, persisted);
return true;
}
// A connected site talks to the wallet the user approved for it — recorded
// as walletId + address in the grant — and to no other. The bridges used to
// resolve "the first ready wallet on this chain" or the sidebar selection on
// every call, so a site the user connected to wallet B was silently served
// wallet A, and selecting another wallet in the sidebar re-pointed every
// connected Solana/Tron site to it (and told it the new address).
// Grants made before this carry no walletId; they are bound to whatever
// they resolve to the first time they are used.
function boundRuntime(api, origin, scope, chain) {
const g = origin ? grantFor(api, origin, scope) : null;
if (!g || !g.walletId) return null;
const rt = ctx.runtimes.get(g.walletId);
if (!rt || rt.entry.chain !== chain || rt.phase !== "ready") {
throw new Error("the wallet this site was connected to is not available (removed or locked) — reconnect the site");
}
return rt;
}
function bindGrant(api, origin, scope, rt) {
const g = origin ? grantFor(api, origin, scope) : null;
if (g && !g.walletId) patchGrant(api, origin, scope, { walletId: rt.entry.id, address: rt.adapter.snapshot().address });
return rt;
}
// An unconnected site that asked for a chain (addChain / switch before
// connect) gets it remembered for its eventual eth_requestAccounts — no
// address is disclosed by this.
function rememberPendingChain(origin, chainId) {
const cur = sessionGrants.get(origin) || {};
sessionGrants.set(origin, { ...cur, eth: { ...(cur.eth || {}), chainId } });
}
function revokeOrigin(api, origin) {
const perms = permissions(api);
delete perms[origin];
api.storage.set("permissions", perms);
sessionGrants.delete(origin);
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
syncInjectPolicy(api);
// A revoked site keeps no WizardConnect pairing either.
if (ctx?.wc?.disconnectOrigin) ctx.wc.disconnectOrigin(origin).catch(() => {});
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 bridges used to go into every https page, which tells every page that
// this browser carries a wallet (and which one) before the user has done
// anything. In "allowed" mode Theseus injects only into origins listed in
// the reserved storage key "__pageInjectPolicy" — it reads that key itself,
// at document start, even while Aegis has not been started yet. The list is
// the sites the user enabled plus every site with a saved connection, so
// turning the mode on never breaks a dapp already in use.
const INJECT_MODE_KEY = "aegis/inject/mode";
const INJECT_ALLOW_KEY = "aegis/inject/allow";
function injectAllowList(api) {
const v = api.storage.get(INJECT_ALLOW_KEY, []);
return Array.isArray(v) ? v.filter((o) => typeof o === "string") : [];
}
function injectMode(api) { return api.storage.get(INJECT_MODE_KEY, "all") === "allowed" ? "allowed" : "all"; }
function syncInjectPolicy(api) {
const origins = Array.from(new Set([...injectAllowList(api), ...Object.keys(permissions(api))]));
api.storage.set("__pageInjectPolicy", { mode: injectMode(api), origins });
}
function normalizeOrigin(raw) {
try {
const u = new URL(String(raw || "").replace(/^bns:\/\//i, "https://"));
if (u.protocol !== "https:" && u.protocol !== "http:") return null;
return `${u.protocol}//${u.host}`;
} catch { return null; }
}
// Panel view: persisted grants plus session-only ones. An origin with no
// persisted entry is flagged `session`; one with both gets the session
// scopes merged in under the persisted ones.
function grantsForPanel(api) {
const out = JSON.parse(JSON.stringify(permissions(api)));
for (const [origin, g] of sessionGrants) {
const hasGrant = g.readAddress || g.eth?.readAddress || g.sol?.readAddress || g.trx?.readAddress;
if (!hasGrant) continue;
out[origin] = out[origin] ? { ...g, ...out[origin] } : { ...g, session: true };
}
return out;
}
// Tron: only transactions Aegis itself signed may be broadcast through the
// bridge — a dapp can't use the wallet as a relay for foreign signatures.
const signedTronTxids = new Set();
function rememberSignedTron(txid) {
signedTronTxids.add(txid);
if (signedTronTxids.size > 200) signedTronTxids.delete(signedTronTxids.values().next().value);
}
// Dapp message bytes: personal_sign carries hex-encoded bytes (ethers, viem,
// wagmi); a plain string is UTF-8 (older dapps, our own panel).
function bytesFromDappMessage(raw) {
const s = raw == null ? "" : String(raw);
if (/^0x([0-9a-f]{2})*$/i.test(s)) return new Uint8Array(Buffer.from(s.slice(2), "hex"));
return new TextEncoder().encode(s);
}
// Overlay preview: the text if it is clean UTF-8, else a length + hex prefix.
function previewBytes(bytes, max = 400) {
let text = null;
try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch {}
if (text != null && !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(text)) {
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 previewText(text, Math.max(max, 1500));
}
return `<${bytes.length} bytes: 0x${Buffer.from(bytes.subarray(0, 30)).toString("hex")}${bytes.length > 30 ? "…" : ""}>`;
}
// ---- Solana message parsing ------------------------------------------------
const SOL_TOKEN_PROGRAMS = new Set([
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", // SPL Token
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", // Token-2022
]);
// Full wire: compact-u16(sigCount) || sig[64]*sigCount || message.
function splitSolWire(wire) {
let off = 0;
const compact = () => {
let n = 0, shift = 0;
for (;;) {
if (off >= wire.length) throw new Error("truncated tx wire");
const b = wire[off++];
n |= (b & 0x7f) << shift;
if ((b & 0x80) === 0) break;
shift += 7;
if (shift > 14) throw new Error("compact-u16 too long");
}
return n;
};
const sigCount = compact();
if (sigCount < 1 || sigCount > 32) throw new Error("bad signature count " + sigCount);
const sigsStart = off;
const messageStart = sigsStart + sigCount * 64;
if (wire.length < messageStart) throw new Error("truncated tx wire");
return { sigCount, sigsStart, messageBytes: wire.slice(messageStart) };
}
// Structural parse of a legacy or v0 message. `ok:false` means the bytes
// cannot be a message — used both to sign transactions and to REFUSE
// transaction-shaped payloads handed to signMessage.
function parseSolMessage(msg, ourPub) {
try {
let off = 0, version = null;
if (!(msg instanceof Uint8Array) || msg.length < 3 + 1 + 32 + 32) throw new Error("too short");
if (msg[0] & 0x80) {
version = msg[0] & 0x7f; off = 1;
if (version !== 0) throw new Error("unsupported message version " + version);
}
const numRequiredSigs = msg[off], numReadonlySigned = msg[off + 1], numReadonlyUnsigned = msg[off + 2];
off += 3;
const compact = () => {
let n = 0, shift = 0;
for (;;) {
if (off >= msg.length) throw new Error("truncated");
const b = msg[off++];
n |= (b & 0x7f) << shift;
if ((b & 0x80) === 0) break;
shift += 7;
if (shift > 14) throw new Error("bad compact-u16");
}
return n;
};
const keyCount = compact();
if (keyCount < 1 || keyCount > 256) throw new Error("bad account key count");
if (numRequiredSigs < 1 || numRequiredSigs > keyCount) throw new Error("bad header");
if (numReadonlySigned > numRequiredSigs || numReadonlyUnsigned > keyCount - numRequiredSigs) throw new Error("bad header");
if (msg.length < off + keyCount * 32 + 32) throw new Error("truncated keys");
const keys = [];
for (let i = 0; i < keyCount; i++) { keys.push(msg.subarray(off, off + 32)); off += 32; }
const blockhash = msg.subarray(off, off + 32); off += 32;
const ixCount = compact();
const instructions = [];
for (let i = 0; i < ixCount; i++) {
if (off >= msg.length) throw new Error("truncated instruction");
const programIdx = msg[off++];
const na = compact(); const accounts = [];
for (let k = 0; k < na; k++) { if (off >= msg.length) throw new Error("truncated accounts"); accounts.push(msg[off++]); }
const nd = compact();
if (off + nd > msg.length) throw new Error("truncated instruction data");
const data = msg.subarray(off, off + nd); off += nd;
instructions.push({ programIdx, accounts, data });
}
let lookupTables = 0;
if (version === 0) lookupTables = compact(); // static keys suffice for the signer checks
let ourIndex = -1;
if (ourPub) {
for (let i = 0; i < keyCount; i++) {
let eq = true;
for (let j = 0; j < 32; j++) if (keys[i][j] !== ourPub[j]) { eq = false; break; }
if (eq) { ourIndex = i; break; }
}
}
return { ok: true, version, numRequiredSigs, keys, blockhash, instructions, lookupTables, ourIndex, length: msg.length };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
}
// Overlay rows: System transfers and SPL transfer/approve/set-authority are
// decoded; everything else is named by program so the user at least sees
// how much of the transaction Aegis could not read.
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
// Overlay rows for a Solana message, plus what makes it risky. The
// network fee is shown in full: a dapp-added ComputeBudget price is paid on
// top of the base fee and used to be invisible ("program ComputeB…: not
// decoded"), so a transaction could burn the balance in priority fees.
// `rows.risks` lists what deserves the danger button; `rows.feeLamports`
// is the most this transaction can cost in fees.
const SOL_COMPUTE_BUDGET = "ComputeBudget111111111111111111111111111111";
const SOL_SYSTEM = "11111111111111111111111111111111";
function solInstructionRows(parsed, balanceLamports = null) {
const b58 = (b) => ctx.d.base58check.encodeBase58(b);
const u64le = (d, o) => { let v = 0n; for (let i = 7; i >= 0; i--) v = (v << 8n) | BigInt(d[o + i] || 0); return v; };
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
const u32le = (d, o) => (d[o] | (d[o + 1] << 8) | (d[o + 2] << 16) | (d[o + 3] << 24)) >>> 0;
const ours = parsed.ourIndex >= 0 ? b58(parsed.keys[parsed.ourIndex]) : null;
const rows = [];
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
const risks = [];
let unitLimit = null, unitPrice = 0n, undecoded = 0, budgetIxs = 0;
const lines = parsed.instructions.map((ix, i) => {
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
if (ix.programIdx >= parsed.keys.length) { undecoded++; return `${i + 1}. program from a lookup table (not decoded)`; }
const progB58 = b58(parsed.keys[ix.programIdx]);
const acct = (n) => {
const idx = ix.accounts[n];
return idx == null ? "?" : (idx < parsed.keys.length ? b58(parsed.keys[idx]) : `lookup-table account #${idx}`);
};
const d = ix.data;
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
const n = i + 1;
if (progB58 === SOL_COMPUTE_BUDGET && d.length >= 1) {
budgetIxs++;
if (d[0] === 2 && d.length >= 5) { unitLimit = u32le(d, 1); return `${n}. Compute limit ${unitLimit.toLocaleString("en-US")} units`; }
if (d[0] === 3 && d.length >= 9) { unitPrice = u64le(d, 1); return `${n}. Priority fee ${unitPrice.toString()} micro-lamports per unit`; }
if (d[0] === 1) return `${n}. Heap frame request`;
if (d[0] === 4) return `${n}. Account data limit`;
return `${n}. Compute budget (${d.length} bytes, not decoded)`;
}
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
if (progB58 === SOL_SYSTEM && d.length >= 4) {
const t = u32le(d, 0);
if (t === 2 && d.length >= 12) return `${n}. System transfer ${fmtTokenAmount(u64le(d, 4), 9)} SOL → ${acct(1)}`;
if (t === 0 && d.length >= 12) return `${n}. Create account ${acct(1)} funded with ${fmtTokenAmount(u64le(d, 4), 9)} SOL`;
if (t === 11 && d.length >= 12) return `${n}. System transfer ${fmtTokenAmount(u64le(d, 4), 9)} SOL → ${acct(2)}`;
if (t === 1 || t === 10) {
risks.push("reassigns an account to another program");
return `${n}. ASSIGN account ${acct(0)} to another program — it hands over control of that account`;
}
if (t === 4) {
risks.push("uses a durable nonce: the signed transaction never expires and can be sent whenever the site likes");
return `${n}. Advance durable nonce — this signature stays valid indefinitely`;
}
if (t === 8 || t === 9) return `${n}. Allocate account space`;
undecoded++;
return `${n}. System instruction ${t} (not decoded)`;
}
if (SOL_TOKEN_PROGRAMS.has(progB58) && d.length >= 1) {
const t = d[0];
if (t === 3 && d.length >= 9) return `${n}. Token transfer ${u64le(d, 1).toString()} units → ${acct(1)}`;
if (t === 12 && d.length >= 10) return `${n}. Token transfer ${fmtTokenAmount(u64le(d, 1), d[9])} → ${acct(2)}`;
if ((t === 4 && d.length >= 9) || (t === 13 && d.length >= 10)) {
risks.push("approves another account to spend tokens");
const amt = t === 13 ? fmtTokenAmount(u64le(d, 1), d[9]) : u64le(d, 1).toString() + " units";
return `${n}. Token APPROVE: delegate ${acct(t === 13 ? 2 : 1)} may spend ${amt}`;
}
if (t === 6) { risks.push("changes a token account's authority"); return `${n}. Token SET AUTHORITY on ${acct(0)} — hands the account to someone else`; }
if (t === 9) {
const dest = acct(1);
if (dest !== ours) risks.push("closes a token account and sends its SOL to someone else");
return `${n}. CLOSE token account ${acct(0)}; its SOL goes to ${dest === ours ? "you" : dest}`;
}
if (t === 5) return `${n}. Revoke a token delegate`;
if (t === 8 || t === 15) return `${n}. BURN tokens from ${acct(0)}`;
if (t === 1 || t === 16 || t === 18) return `${n}. Initialize token account ${acct(0)}`;
undecoded++;
return `${n}. Token instruction ${t} (not decoded)`;
}
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
undecoded++;
return `${n}. program ${progB58.slice(0, 8)}…: ${d.length} bytes, ${ix.accounts.length} account${ix.accounts.length === 1 ? "" : "s"} (not decoded)`;
});
rows.push({ label: parsed.instructions.length === 1 ? "Instruction" : "Instructions", value: lines.join("\n") || "(none)", mono: true });
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
// Fee: 5000 lamports per signature, plus limit × price. With no explicit
// limit the runtime allows 200k units per non-budget instruction, up to 1.4M.
const limit = BigInt(unitLimit != null ? Math.min(unitLimit, 1_400_000) : Math.min(1_400_000, 200_000 * Math.max(1, parsed.instructions.length - budgetIxs)));
const priority = (limit * unitPrice + 999_999n) / 1_000_000n;
const feeLamports = 5000n * BigInt(Math.max(1, parsed.numRequiredSigs)) + priority;
rows.push({ label: "Network fee (max)", value: `${fmtTokenAmount(feeLamports, 9)} SOL${priority > 0n ? ` (incl. ${fmtTokenAmount(priority, 9)} SOL priority fee set by the site)` : ""}`, strong: priority > 0n });
const bal = balanceLamports != null ? BigInt(balanceLamports) : null;
if (priority > 50_000_000n || (bal != null && bal > 0n && feeLamports * 10n > bal)) {
risks.push(`carries an unusually high network fee (${fmtTokenAmount(feeLamports, 9)} SOL)`);
}
if (parsed.version === 0) {
rows.push({ label: "Format", value: `v0 · ${parsed.lookupTables} address-lookup table${parsed.lookupTables === 1 ? "" : "s"} (accounts inside them are not shown)` });
}
const others = parsed.numRequiredSigs - 1;
rows.push({ label: "Signers", value: others ? `${parsed.numRequiredSigs} — you (slot #${parsed.ourIndex}) + ${others} other${others === 1 ? "" : "s"}` : "1 — you" });
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
if (risks.length) rows.unshift({ label: "Warning", value: [...new Set(risks)].map((r) => "• This transaction " + r + ".").join("\n"), strong: true });
rows.risks = risks;
rows.undecoded = undecoded;
rows.feeLamports = feeLamports;
return rows;
}
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 ETH_RPC_PRECONNECT = new Set(["eth_chainId", "net_version", "eth_blockNumber"]);
const ETH_RPC_NEVER = /^(eth_sign|eth_signTransaction|eth_sendTransaction|eth_accounts|eth_requestAccounts|eth_coinbase|eth_signTypedData|eth_newFilter|eth_newBlockFilter|eth_newPendingTransactionFilter|eth_uninstallFilter|eth_getFilterChanges|eth_getFilterLogs|eth_subscribe|eth_unsubscribe|eth_mining|eth_submit|eth_getWork)/;
// EIP-2612 Permit, DAI-style permit, and Uniswap Permit2 (PermitSingle /
// PermitBatch / PermitTransferFrom / PermitBatchTransferFrom). Returns the
// overlay rows that say who may spend what until when, or null when the
// typed data is not a spending approval.
// What the signature actually covers. The EIP-712 encoder reads only the
// fields each type declares; anything else in `message` is ignored by the
// digest but was read by the overlay, so a dapp could add a decoy
// `allowed: false` or `details: {amount: "1"}` beside an unlimited permit and
// have the overlay show the decoy. Everything shown is built from this view.
function signedView(td) {
const types = (td && typeof td.types === "object" && td.types) || {};
let dropped = 0;
const walk = (type, v) => {
const arr = /\[\d*\]$/.exec(type);
if (arr) {
const inner = type.slice(0, arr.index);
return Array.isArray(v) ? v.map((x) => walk(inner, x)) : v;
}
const fields = types[type];
if (!Array.isArray(fields)) return v; // atomic
const src = (v && typeof v === "object") ? v : {};
for (const k of Object.keys(src)) if (!fields.some((f) => f && f.name === k)) dropped++;
const out = {};
for (const f of fields) if (f && typeof f.name === "string") out[f.name] = walk(String(f.type), src[f.name]);
return out;
};
return { message: walk(String(td?.primaryType || ""), td?.message), dropped };
}
// The chain check applies only when the domain actually signs a chainId.
function domDeclaredEarly(td) {
return Array.isArray(td?.types?.EIP712Domain) && td.types.EIP712Domain.some((x) => x && x.name === "chainId");
}
// What a non-Permit typed-data signature can do, judged from its declared
// structure rather than its name. The name list (Seaport, SafeTx, …) missed
// every marketplace, relayer and account-abstraction type it did not know,
// and those got a primary "Sign" button and no PIN. Here every signed field
// is listed with its type, and the payload counts as asset-moving when it
// names an address other than the user's and the verifying contract
// together with an amount-like number, or carries raw bytes (an arbitrary
// call). Text-only payloads (logins, votes) stay ordinary.
function assessTypedData(td, view, ownAddress) {
const types = (td && typeof td.types === "object" && td.types) || {};
const own = String(ownAddress || "").toLowerCase();
const vc = String(td?.domain?.verifyingContract || "").toLowerCase();
const fields = [];
let foreignAddr = false, amount = false, rawBytes = false, numbers = false, addresses = false;
const AMOUNTISH = /amount|value|price|wad|qty|quantity|fee|tip|salary|reward|consideration|offer|balance|allowance/i;
const DEADLINEISH = /deadline|expir|valid(to|until|before)|endtime|end_time|until/i;
const walk = (type, v, path, depth) => {
if (fields.length > 200 || depth > 8) return;
const arr = /^(.+)\[(\d*)\]$/.exec(type);
if (arr) { (Array.isArray(v) ? v : []).forEach((x, i) => walk(arr[1], x, `${path}[${i}]`, depth + 1)); return; }
if (Array.isArray(types[type])) { for (const fd of types[type]) if (fd && typeof fd.name === "string") walk(String(fd.type), v && typeof v === "object" ? v[fd.name] : undefined, path ? `${path}.${fd.name}` : fd.name, depth + 1); return; }
const name = path.split(".").pop().replace(/\[\d+\]$/, "");
let shown = String(v);
if (type === "address") {
addresses = true;
const a = String(v).toLowerCase();
if (a !== own && a !== vc && !/^0x0{40}$/.test(a)) foreignAddr = true;
shown = String(v) + (a === own ? " (you)" : a === vc ? " (the verifying contract)" : "");
} else if (/^u?int\d*$/.test(type)) {
numbers = true;
let n = null; try { n = BigInt(v); } catch {}
if (n != null) {
if (DEADLINEISH.test(name) && n > 0n) shown = n >= 4102444800n ? `${n} (never expires in practice)` : `${n} (${new Date(Number(n) * 1000).toISOString().slice(0, 16).replace("T", " ")} UTC)`;
else if (n >= (1n << 96n)) { shown = `${n} (≈ unlimited)`; amount = true; }
else shown = n.toString();
if (AMOUNTISH.test(name) && n > 0n) amount = true;
}
} else if (type === "bytes") {
const h = String(v || "").replace(/^0x/i, "");
if (h.length) rawBytes = true;
shown = h.length ? `0x${h.slice(0, 64)}${h.length > 64 ? "…" : ""} (${h.length / 2} bytes)` : "(empty)";
} else if (type === "string") {
shown = JSON.stringify(plainLabel(v, 200));
}
fields.push(`${path}: ${shown} [${type}]`);
};
walk(String(td?.primaryType || ""), view.message, "", 0);
const movesAssets = (foreignAddr && amount) || rawBytes;
return { fields, movesAssets, plain: !addresses && !numbers };
}
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 describePermit(td) {
const primary = String(td?.primaryType || "");
if (!/^Permit/.test(primary)) return null;
const msg = signedView(td).message || {};
const declared = (t) => (Array.isArray(td?.types?.[t]) ? td.types[t].map((f) => f && f.name) : []);
const pf = declared(primary);
const has = (...names) => names.every((n) => pf.includes(n));
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 UNLIMITED = 1n << 159n; // ≥ half of uint160 covers Permit2's max and every uint256 max
// No token has a supply anywhere near this, so an amount this large is an
// unlimited approval in all but name (2^96 ≈ 7.9e28 base units: 79 billion
// tokens at 18 decimals).
const HUGE = 1n << 96n;
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 big = (v) => { try { return BigInt(v); } catch { return null; } };
const amountText = (v) => {
const b = big(v);
if (b === null) return String(v);
if (b >= UNLIMITED) return "UNLIMITED (∞)";
return b.toString() + " units" + (b >= HUGE ? " (effectively unlimited)" : "");
};
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 when = (v) => {
const b = big(v);
if (b === null) return String(v);
if (b >= 4102444800n) return "never expires"; // 2100-01-01 and beyond
try { return new Date(Number(b) * 1000).toISOString().replace("T", " ").slice(0, 16) + " UTC"; } catch { return b.toString(); }
};
const rows = [];
let risky = false;
const spender = has("spender") ? msg.spender : null;
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
rows.push({ label: "Spender", value: spender ? String(spender) : "(none named — anyone holding this signature)", mono: true, strong: true });
if (!spender) risky = true;
const items = [];
// The variant is chosen by the declared type, never by what the message
// happens to carry.
if (has("holder", "spender", "nonce", "expiry", "allowed")) {
// DAI-style: a bool, encoded by truthiness — so is this.
items.push({ token: td.domain?.verifyingContract, amount: msg.allowed ? UNLIMITED : 0n });
rows.push({ label: "Valid until", value: when(msg.expiry) });
} else if (has("owner", "spender", "value", "deadline")) {
// EIP-2612.
items.push({ token: td.domain?.verifyingContract, amount: msg.value });
rows.push({ label: "Valid until", value: when(msg.deadline) });
} else if (has("details", "spender", "sigDeadline")) {
// Permit2 PermitSingle / PermitBatch.
const list = Array.isArray(msg.details) ? msg.details : [msg.details];
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
for (const d of list) items.push({ token: d?.token, amount: d?.amount, expiration: d?.expiration });
rows.push({ label: "Signature valid until", value: when(msg.sigDeadline) });
} else if (has("permitted", "spender", "deadline")) {
// Permit2 PermitTransferFrom / PermitBatchTransferFrom (and *Witness*).
const list = Array.isArray(msg.permitted) ? msg.permitted : [msg.permitted];
for (const d of list) items.push({ token: d?.token, amount: d?.amount });
rows.push({ label: "Signature valid until", value: when(msg.deadline) });
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
}
items.slice(0, 10).forEach((it, i) => {
const b = big(it.amount);
if (b === null || b >= HUGE) risky = 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
rows.push({
label: items.length > 1 ? `Token #${i + 1}` : "Token",
value: `${it.token || "?"} — ${amountText(it.amount)}${it.expiration != null ? ` · allowance ${when(it.expiration)}` : ""}`,
mono: true,
});
});
if (items.length > 10) { rows.push({ label: "Tokens", value: `… and ${items.length - 10} more` }); risky = true; }
if (!items.length) { rows.push({ label: "Token", value: "(not a permit shape Aegis can read — treat as unlimited)" }); risky = 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
return { rows, risky };
}
async function withOriginLock(origin, fn) {
if (pendingByOrigin.has(origin)) throw new Error("a wallet request from this site is already waiting for approval");
pendingByOrigin.add(origin);
try { return await fn(); } finally { pendingByOrigin.delete(origin); }
}
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
// Only .x sites (BCNR-native TLD) get the BCH bridge, matching the pre-
// multi-wallet gate. Widening the manifest to https://*/* makes the Tron
// bridge available everywhere.
// Any ordinary web origin may talk to the BCH bridge. The old test here
// required a ".x" hostname, which locked out every third-party BCH dapp
// (Cauldron, bch.guru) and our own dapps on other TLDs — while the TRX / ETH
// / SOL bridges and WizardConnect accepted any origin all along. The real
// protection is downstream and unchanged: an approval overlay on every
// action plus per-origin permissions. This only keeps non-web schemes out.
function isDappOrigin(origin) {
try { const u = new URL(origin); return u.protocol === "https:" || u.protocol === "http:"; }
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
catch { return false; }
}
function legacyBchRuntime() {
const rt = ctx.runtimes.get(LEGACY_BCH_WALLET_ID);
if (!rt || rt.phase !== "ready" || !rt.adapter) throw new Error("wallet is not ready (vault locked?)");
return rt;
}
// The Tron bridge routes to the currently-selected wallet if it is Tron;
// otherwise it looks for the first ready Tron wallet on the selected network
// hint; else rejects with "no tron wallet".
function activeTronRuntime(origin) {
const api = ctx.api;
const bound = boundRuntime(api, origin, "trx", "trx");
if (bound) return bound;
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 selId = selectedWalletId();
const selRt = selId && ctx.runtimes.get(selId);
if (selRt && selRt.entry.chain === "trx" && selRt.phase === "ready") return bindGrant(api, origin, "trx", selRt);
for (const rt of ctx.runtimes.values()) if (rt.entry.chain === "trx" && rt.phase === "ready") return bindGrant(api, origin, "trx", rt);
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
throw new Error("no Tron wallet available — add one in the Aegis sidebar");
}
function registerPageMessages(api) {
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
// ---- BCH bridge (unchanged behavior; wallet source is legacy default) ----
api.onMessage("getAddress", async (_p, m) => {
const origin = fromPage(m);
if (!isDappOrigin(origin)) throw new Error("this page cannot use the wallet bridge");
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 rt = legacyBchRuntime();
if (grantFor(api, origin, "bch")) return rt.adapter.current().address;
return withOriginLock(origin, async () => {
const pick = await api.approvalModal({
title: "Share your Bitcoin Cash address?",
origin,
body: "The site will see your current receiving address and can look up its balance and history on the public chain.",
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
rows: [{ label: "Address", value: rt.adapter.current().address, mono: true }],
actions: [{ id: "allow", label: "Share", primary: true }],
checkbox: { id: "always", label: "Always allow this site to see my address" },
});
if (!pick.startsWith("allow")) throw new Error("user rejected");
setGrant(api, origin, "bch", { readAddress: true }, pick === "allow+always");
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 rt.adapter.current().address;
});
});
api.onMessage("signAndSend", async (p, m) => {
const origin = fromPage(m);
if (!isDappOrigin(origin)) throw new Error("this page cannot use the wallet bridge");
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 rt = legacyBchRuntime();
return withOriginLock(origin, async () => {
let plan;
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 { plan = rt.adapter.plan(p || {}); }
catch (e) { throw new Error(/insufficient funds|too small/i.test(e?.message) ? "insufficient funds" : e?.message || String(e)); }
const d = describePlan(plan);
if (d.recipients.length > 8) throw new Error("too many outputs");
const perms = permissions(api);
const budget = perms[origin] && perms[origin].sendTx;
const remaining = budget ? Math.max(0, (budget.capSats | 0) - (budget.usedSats | 0)) : 0;
// The PIN wait below can last two minutes, during which the user may
// revoke the site or other permissions may change. Everything after it
// re-reads permissions and changes only this origin's sendTx; writing
// back the snapshot taken here used to restore a revoked allowance and
// wipe any other change made meanwhile.
const patchSendTx = (fn) => {
const now = permissions(api);
const cur = { ...(now[origin] || {}) };
const next = fn(cur.sendTx);
if (!next && !now[origin]) return; // revoked meanwhile: nothing to change
if (next) cur.sendTx = next; else delete cur.sendTx;
now[origin] = cur;
api.storage.set("permissions", now);
};
// A payment carrying data for the chain is never silent.
if (budget && d.total <= remaining && !plan.memo) {
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
// An allowance skips the overlay, not the PIN the user asked for on
// every transaction.
await requireDappTxPin(origin, "Bitcoin Cash payment");
const fresh = (permissions(api)[origin] || {}).sendTx;
const left = fresh ? Math.max(0, (fresh.capSats | 0) - (fresh.usedSats | 0)) : 0;
if (!fresh || fresh.grantedAt !== budget.grantedAt || d.total > left) throw new Error("this site's allowance changed while waiting for the PIN; ask again");
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 r = await rt.adapter.signAndBroadcast(plan);
patchSendTx((t) => (t ? { ...t, usedSats: (t.usedSats | 0) + d.total } : t));
emitState();
api.log(`silent send ${d.total} sat for ${origin}, ${left - d.total} sat of allowance left`);
return { txid: r.txid };
}
const rows = d.recipients.map((r, i) => ({ label: d.recipients.length > 1 ? `To #${i + 1}` : "To", value: r.to, mono: true }));
rows.push({ label: "Amount", value: fmtBch(d.recipients.reduce((a, r) => a + r.value, 0)) + " BCH", strong: 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
rows.push({ label: "Fee", value: `${plan.fee} sat (${plan.feeRate} sat/B)` });
rows.push({ label: "Total", value: fmtBch(d.total) + " BCH" });
// The site's OP_RETURN data is written on-chain under this payment.
if (plan.memo) rows.push({ label: "Data (OP_RETURN)", value: previewText(plan.memo, 220), mono: true });
const pick = await api.approvalModal({
title: "Send Bitcoin Cash?",
origin,
body: budget
? `This payment is over what is left of the site's allowance (${fmtBch(remaining)} BCH). Check the address and amount.`
: "This site is asking your wallet to pay. Check the address and amount.",
rows,
actions: [{ id: "send", label: "Send", primary: true }],
select: {
id: "cap", label: "Afterwards",
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
options: [{ value: "", label: "ask every time" }, ...BCH_ALLOWANCES.map((s) => ({ value: String(s), label: `allow up to ${fmtBch(s)} BCH more without asking` }))],
},
});
const [action, ...flags] = pick.split("+");
if (action !== "send") throw new Error("user rejected");
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 requireDappTxPin(origin, "Bitcoin Cash payment");
const cap = flags.find((f) => f.startsWith("cap="));
const capSats = cap ? Number(cap.slice(4)) : 0;
if (BCH_ALLOWANCES.includes(capSats)) patchSendTx(() => ({ capSats, usedSats: 0, grantedAt: Date.now() }));
else if (budget) patchSendTx(() => null);
emitState();
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 r = await rt.adapter.signAndBroadcast(plan);
return { txid: r.txid };
});
});
api.onMessage("signMessage", async (p, m) => {
const origin = fromPage(m);
if (!isDappOrigin(origin)) throw new Error("this page cannot use the wallet bridge");
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 rt = legacyBchRuntime();
const message = String(p && p.message != null ? p.message : "");
if (message.length > 4096) throw new Error("message too long");
return withOriginLock(origin, async () => {
const pick = await api.approvalModal({
title: "Sign a message?",
origin,
body: "Signing proves you control the address below. It moves no coins.",
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
rows: [
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
{ label: "Message", value: previewText(message), mono: 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
{ label: "Address", value: rt.adapter.current().address, mono: true },
],
actions: [{ id: "sign", label: "Sign", primary: true }],
});
if (pick !== "sign") throw new Error("user rejected");
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 rt.adapter.signMessage(message);
});
});
// BCH message verification (BIP-137). Panel-only path: given a message,
// a base64 signature, and an address, return { valid, address,
// recoveredHash }. No approval modal (nothing spendable happens), no
// wallet lookup — pure crypto against the given address.
// Panel → BCMR resolver. Batched: pass an array of category hex strings,
// get back { <categoryHex>: {name, symbol, iconUri, decimals, source} }
// for every one that resolved. Missed categories map to null. This
// triggers a background fetch for anything not in the disk cache, so
// the second call for the same set returns instantly.
api.onMessage("tokenMetadata", async (p, m) => {
fromPanel(m);
const cats = Array.isArray(p?.categories) ? p.categories.map(String).filter((c) => /^[0-9a-f]{64}$/i.test(c)) : [];
if (!cats.length) return {};
const entries = await ctx.bcmr.lookupMany(cats);
const out = {};
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
// Pass the category so a local name can win over the registry — and so
// a category with ONLY a local name still comes back named instead of
// null, which is the whole point for self-minted tokens.
for (const cat of cats) out[cat] = ctx.bcmr.metadataOf(entries[cat], cat);
// Tell the panel whether the registries themselves are reachable. A dead
// registry used to be indistinguishable from an unregistered token, and
// both defaults were 404 for who knows how long because of it.
const reachable = Object.values(entries).some((e) => e && e.snapshot);
return { meta: out, registriesAnswered: reachable, registries: ctx.bcmr.registryList().length };
});
// Name a token yourself. The only way to label a category that publishes
// no metadata anywhere — no registry entry, and commonly not even an
// OP_RETURN in its genesis transaction.
api.onMessage("setTokenLabel", (p, m) => {
fromPanel(m);
const cat = String(p?.category || "");
if (!/^[0-9a-f]{64}$/i.test(cat)) throw new Error("not a token category");
const rec = ctx.bcmr.setLocalName(cat, {
name: p?.name, symbol: p?.symbol, decimals: p?.decimals,
});
return { category: cat, local: rec };
});
// Read the configured BCMR registry list (defaults + any user additions).
api.onMessage("bcmrRegistries", (_p, m) => {
fromPanel(m);
return { registries: ctx.bcmr.registryList() };
});
// Overwrite the registry list. Empty array restores defaults on next read.
api.onMessage("setBcmrRegistries", (p, m) => {
fromPanel(m);
ctx.bcmr.setRegistries(Array.isArray(p?.registries) ? p.registries : []);
return { registries: ctx.bcmr.registryList() };
});
api.onMessage("verifyMessage", (p, m) => {
fromPanel(m);
const message = String(p?.message != null ? p.message : "");
const signature = String(p?.signature || "");
const address = String(p?.address || "");
if (!signature || !address) throw new Error("signature and address are required");
try {
return ctx.d.keysLib.verifyMessage(message, signature, address, {
cashaddr: ctx.d.cashaddr, secp256k1: ctx.d.secp256k1,
});
} catch (e) {
return { valid: false, error: e?.message || String(e) };
}
});
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
// ---- Tron bridge (tronWeb / tronLink) -----------------------------------
api.onMessage("trx.requestAccounts", async (_p, m) => {
const origin = fromPage(m);
const rt = activeTronRuntime(origin);
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 snap = rt.adapter.snapshot();
if (grantFor(api, origin, "trx")) return { code: 200, address: snap.address, network: snap.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
return withOriginLock(origin, async () => {
const pick = await api.approvalModal({
title: "Connect this site to your Tron wallet?",
origin,
body: "The site will see this address and can build transactions for you to sign.",
rows: [
{ label: "Address", value: snap.address, mono: true },
{ label: "Network", value: snap.network === "nile" ? "Nile testnet" : "Tron mainnet" },
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
{ label: "Wallet", value: `${rt.entry.label} — Tron · ${snap.network === "nile" ? "Nile testnet" : "Mainnet"}` },
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
],
actions: [{ id: "allow", label: "Connect", primary: true }],
checkbox: { id: "always", label: "Always allow this site to see this address" },
});
if (!pick.startsWith("allow")) throw new Error("user rejected");
setGrant(api, origin, "trx", { readAddress: true, network: snap.network, walletId: rt.entry.id, address: snap.address }, pick === "allow+always");
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 { code: 200, address: snap.address, network: snap.network };
});
});
api.onMessage("trx.getAccount", (_p, m) => {
const origin = fromPage(m);
if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first");
const rt = activeTronRuntime(origin);
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 snap = rt.adapter.snapshot();
return { address: snap.address, network: snap.network };
});
// Sign an arbitrary raw_data_hex the dapp built (with its own tronWeb).
// Everything the overlay shows is decoded from raw_data_hex — the bytes
// that get signed. The overlay used to read the dapp's raw_data JSON,
// which can say "1 TRX to X" over bytes that do something else entirely.
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
api.onMessage("trx.signTransaction", async (p, m) => {
const origin = fromPage(m);
const rt = activeTronRuntime(origin);
if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first");
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 tx = p && p.transaction;
if (!tx || typeof tx !== "object" || !tx.raw_data_hex) throw new Error("bad transaction");
// The dapp's raw_data JSON and txID are cross-checked, never trusted.
let decoded;
try { decoded = ctx.d.tronDecode.decodeRawData(tx.raw_data_hex); }
catch (e) { throw new Error("cannot decode raw_data_hex: " + (e?.message || e)); }
if (tx.txID && String(tx.txID).toLowerCase() !== decoded.txid) throw new Error("txID does not match raw_data_hex");
const me = rt.adapter.address;
for (const c of decoded.contracts) {
if (!c.owner) throw new Error(`cannot read the owner of ${c.typeName}; refusing to sign`);
if (c.owner !== me) throw new Error(`transaction owner ${c.owner} is not this wallet`);
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
// A known method with missing arguments would show "undefined".
if (c.call && c.call.malformed) throw new Error(`the ${c.call.name} call is truncated; refusing to sign`);
}
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 withOriginLock(origin, async () => {
const rows = [];
decoded.contracts.forEach((c, i) => {
const n = decoded.contracts.length > 1 ? ` #${i + 1}` : "";
rows.push({ label: "Type" + n, value: c.typeName + (c.dangerous ? " — grants control to another account" : "") });
if (c.type === 1) {
rows.push({ label: "To", value: c.to || "(none)", mono: true });
rows.push({ label: "Amount", value: `${fmtTrx(Number(c.amount))} TRX`, strong: true });
} else if (c.type === 2) {
rows.push({ label: "To", value: c.to || "(none)", mono: true });
rows.push({ label: "Amount", value: `${String(c.amount)} units of asset ${c.assetName || "?"}`, strong: true });
} else if (c.type === 31) {
rows.push({ label: "Contract", value: c.contract || "(none)", mono: true });
const call = c.call;
if (call && call.name === "transfer") rows.push({ label: "Call", value: `transfer ${call.unlimited ? "UNLIMITED" : String(call.amount)} token units to ${call.to}`, strong: true });
else if (call && (call.name === "approve" || call.name === "increaseAllowance")) rows.push({ label: "Call", value: `${call.name}: let ${call.spender} spend ${call.unlimited ? "UNLIMITED (∞)" : String(call.amount)} token units`, strong: true });
else if (call && call.name === "transferFrom") rows.push({ label: "Call", value: `transferFrom ${call.from} → ${call.to}: ${call.unlimited ? "UNLIMITED" : String(call.amount)} units`, strong: true });
else rows.push({ label: "Call", value: call && call.selector ? `unknown method 0x${call.selector} (${c.data.length / 2} bytes, not decoded)` : "(no call data)", mono: true });
if (c.callValue) rows.push({ label: "TRX attached", value: `${fmtTrx(Number(c.callValue))} TRX` });
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
if (c.tokenValue) rows.push({ label: "TRC10 attached", value: `${String(c.tokenValue)} units of token #${String(c.tokenId ?? "?")}`, strong: true });
} else {
rows.push({ label: "Details", value: "not decoded — Aegis cannot show what this transaction does", strong: true });
}
});
if (decoded.feeLimit != null) rows.push({ label: "Fee limit", value: `${fmtTrx(Number(decoded.feeLimit))} TRX` });
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
if (decoded.memo) rows.push({ label: "Memo", value: previewText(decoded.memo, 200), mono: true });
if (decoded.expiration) {
const left = decoded.expiration - Date.now();
rows.push({ label: "Valid until", value: new Date(decoded.expiration).toISOString().replace("T", " ").slice(0, 16) + " UTC" + (left > 24 * 3600e3 ? " — unusually long; the site can broadcast it any time until then" : "") });
}
rows.push({ label: "Tx ID", value: decoded.txid, mono: 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
rows.push({ label: "Wallet", value: `${rt.entry.label} — Tron · ${rt.adapter.snapshot().network === "nile" ? "Nile testnet" : "Mainnet"}` });
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
const risky = decoded.contracts.some((c) => c.dangerous || c.undecoded || (c.tokenValue && c.tokenValue > 0n) || (c.call && c.call.unlimited))
|| (decoded.expiration && decoded.expiration - Date.now() > 24 * 3600e3);
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 pick = await api.approvalModal({
title: "Sign a Tron transaction?",
origin,
body: risky
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
? "WARNING: this transaction does something Aegis flags as risky (open-ended control over funds, tokens sent along with a call, a type Aegis cannot read, or a very long validity). Only sign if you fully trust this site."
: "Decoded from the bytes that will be signed. Check the type, amount and destination.",
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
rows,
actions: [{ id: "sign", label: risky ? "Sign anyway" : "Sign", primary: !risky, danger: risky }],
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 (pick !== "sign") throw new Error("user rejected");
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 requireDappTxPin(origin, "Tron transaction");
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 sig = rt.adapter.signRawData(tx.raw_data_hex);
rememberSignedTron(decoded.txid);
return { ...tx, txID: decoded.txid, signature: [sig] };
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
});
});
api.onMessage("trx.sendRawTransaction", async (p, m) => {
const origin = fromPage(m);
const rt = activeTronRuntime(origin);
if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first");
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 signedTx = p && p.transaction;
if (!signedTx || !signedTx.raw_data_hex || !Array.isArray(signedTx.signature)) throw new Error("bad signed tx");
// No approval here — the sign step carried it. But only what Aegis
// signed goes out: the bridge is not a relay for foreign signatures.
let txid;
try { txid = ctx.d.tronDecode.decodeRawData(signedTx.raw_data_hex).txid; }
catch (e) { throw new Error("cannot decode raw_data_hex: " + (e?.message || e)); }
if (!signedTronTxids.has(txid)) throw new Error("refusing to broadcast a transaction this wallet did not sign");
return rt.adapter.broadcastSignedTx({ ...signedTx, txID: txid });
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
});
api.onMessage("trx.signMessageV2", async (p, m) => {
const origin = fromPage(m);
const rt = activeTronRuntime(origin);
if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first");
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 message = String(p && p.message != null ? p.message : "");
if (message.length > 4096) throw new Error("message too long");
return withOriginLock(origin, async () => {
const snap = rt.adapter.snapshot();
const pick = await api.approvalModal({
title: "Sign a Tron message?",
origin,
body: "Signing proves you control this address. It moves no coins.",
rows: [
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
{ label: "Message", value: previewText(message), mono: 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
{ label: "Address", value: snap.address, mono: true },
],
actions: [{ id: "sign", label: "Sign", primary: true }],
});
if (pick !== "sign") throw new Error("user rejected");
return rt.adapter.signMessageV2(message);
});
});
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
// ---- Ethereum (EIP-1193) ---------------------------------------------
function ethRuntimeForChain(chainId) {
for (const rt of ctx.runtimes.values()) {
if (rt.entry.chain !== "eth" || rt.phase !== "ready") continue;
if (Number(rt.adapter.snapshot().chainId) === Number(chainId)) return rt;
}
return null;
}
// The wallet a given origin talks to. Chain is per origin — fixed at
// connect, changed only by that origin's own wallet_switchEthereumChain /
// wallet_addEthereumChain. The sidebar selection is a UI preference and
// never redirects a connected dapp. Unconnected origins get the chain
// they asked for before connecting, else the selected ETH wallet.
function activeEthRuntime(origin) {
const bound = boundRuntime(api, origin, "eth", "eth");
if (bound) return bound;
const g = origin ? grantFor(api, origin, "eth") : null;
const wanted = g?.chainId ?? (origin ? sessionGrants.get(origin)?.eth?.chainId : null);
if (wanted != null) { const rt = ethRuntimeForChain(wanted); if (rt) return bindGrant(api, origin, "eth", rt); }
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const selId = selectedWalletId();
const selRt = selId && ctx.runtimes.get(selId);
if (selRt && selRt.entry.chain === "eth" && selRt.phase === "ready") return bindGrant(api, origin, "eth", selRt);
for (const rt of ctx.runtimes.values()) if (rt.entry.chain === "eth" && rt.phase === "ready") return bindGrant(api, origin, "eth", rt);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
throw new Error("no Ethereum wallet available — add one in the Aegis sidebar");
}
// Move a CONNECTED site to the wallet on another chain. The same address
// moves silently; a different one is a new disclosure and is asked for —
// otherwise a connected site could enumerate every EVM wallet's address by
// looping wallet_switchEthereumChain over chain ids and reading eth.state.
async function moveEthGrant(origin, chainId, consented = false) {
const g = grantFor(api, origin, "eth");
if (!g) return false;
const cur = (() => { try { return activeEthRuntime(origin); } catch { return null; } })();
const curAddr = String(g.address || cur?.adapter.snapshot().address || "").toLowerCase();
let target = null;
for (const rt of ctx.runtimes.values()) {
if (rt.entry.chain !== "eth" || rt.phase !== "ready" || Number(rt.adapter.snapshot().chainId) !== Number(chainId)) continue;
if (rt.adapter.snapshot().address.toLowerCase() === curAddr) { target = rt; break; }
if (!target) target = rt;
}
if (!target) return false;
const snap = target.adapter.snapshot();
if (snap.address.toLowerCase() !== curAddr && !consented) {
const ask = (opts) => withOriginLock(origin, () => api.approvalModal(opts));
const pick = await ask(({
title: "Let this site see another Ethereum address?",
origin,
body: "The site asked to switch networks. On that network Aegis uses a different wallet, so switching shows the site this address too.",
rows: [
{ label: "Address", value: snap.address, mono: true },
{ label: "Network", value: chainMeta("eth", target.entry.network)?.label || snap.network },
{ label: "Wallet", value: target.entry.label },
],
actions: [{ id: "allow", label: "Switch", primary: true }],
}));
if (!String(pick || "").startsWith("allow")) throw ethError("user rejected the network switch", 4001);
}
patchGrant(api, origin, "eth", { chainId: Number(chainId), walletId: target.entry.id, address: snap.address });
return true;
}
function ethConnectedFor(origin) { return !!grantFor(api, origin, "eth"); }
function ethError(message, code) { const e = new Error(message); e.code = code; return e; }
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
api.onMessage("eth.requestAccounts", async (_p, m) => {
const origin = fromPage(m);
const rt = activeEthRuntime(origin);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const snap = rt.adapter.snapshot();
const chainIdHex = "0x" + Number(snap.chainId).toString(16);
const networkVersion = String(snap.chainId);
if (ethConnectedFor(origin)) return { address: snap.address, chainIdHex, networkVersion };
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
return withOriginLock(origin, async () => {
const pick = await api.approvalModal({
title: "Connect this site to your Ethereum wallet?",
origin,
body: "The site will see this address and can build transactions for you to sign.",
rows: [
{ label: "Address", value: snap.address, mono: true },
{ label: "Network", value: chainMeta("eth", rt.entry.network)?.label || snap.network },
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
{ label: "Wallet", value: `${rt.entry.label} — Ethereum · ${snap.network}` },
],
actions: [{ id: "allow", label: "Connect", primary: true }],
checkbox: { id: "always", label: "Always allow this site to see this address" },
});
if (!pick.startsWith("allow")) throw new Error("user rejected");
setGrant(api, origin, "eth", { readAddress: true, chainId: snap.chainId, walletId: rt.entry.id, address: snap.address }, pick === "allow+always");
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
return { address: snap.address, chainIdHex, networkVersion };
});
});
api.onMessage("eth.personalSign", async (p, m) => {
const origin = fromPage(m);
if (!ethConnectedFor(origin)) throw new Error("not connected — call eth_requestAccounts first");
const rt = activeEthRuntime(origin);
// ethers / viem / wagmi send hex-encoded bytes; signing that hex as
// literal text produced signatures no dapp could verify. The overlay
// shows the decoded text.
const bytes = bytesFromDappMessage(p && p.message);
if (bytes.length > 16384) throw new Error("message too long");
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
// Exactly 32 bytes that are not readable text is a hash, and contracts
// accept a personal_sign over a hash as approval of whatever it hashes:
// a Gnosis Safe takes it for its safeTxHash (v > 30), many order books
// and relayers verify toEthSignedMessageHash(orderHash). That is not a
// login message, so it gets the danger button and the PIN.
let textual = false;
try { new TextDecoder("utf-8", { fatal: true }).decode(bytes); textual = !INVISIBLE_RE.test(Buffer.from(bytes).toString("utf8")); } catch {}
INVISIBLE_RE.lastIndex = 0;
const hashLike = bytes.length === 32 && !textual;
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
return withOriginLock(origin, async () => {
const pick = await api.approvalModal({
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
title: hashLike ? "Sign a 32-byte hash?" : "Sign an Ethereum message?",
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
origin,
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
body: hashLike
? "WARNING: this is not a readable message but a 32-byte hash. Contracts such as Safe wallets and order books accept a signature over a hash as approval of whatever it stands for — a transaction or a trade Aegis cannot show you. Only sign if you know exactly what this hash is."
: "Signing proves you control this address. It moves no ETH.",
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
rows: [
{ label: "Message", value: previewBytes(bytes), mono: true },
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
{ label: "Address", value: rt.adapter.snapshot().address, mono: true },
],
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
actions: [{ id: "sign", label: hashLike ? "Sign anyway" : "Sign", primary: !hashLike, danger: hashLike }],
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
});
if (pick !== "sign") throw new Error("user rejected");
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
if (hashLike) await requireDappTxPin(origin, "signature over a 32-byte hash");
return rt.adapter.signMessage(bytes);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
});
});
api.onMessage("eth.sendTransaction", async (p, m) => {
const origin = fromPage(m);
if (!ethConnectedFor(origin)) throw new Error("not connected — call eth_requestAccounts first");
const rt = activeEthRuntime(origin);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const tx = (p && p.tx) || {};
if (!tx.to) throw new Error("tx.to required");
if (tx.from && String(tx.from).toLowerCase() !== rt.adapter.address.toLowerCase()) throw new Error("tx.from is not the connected account");
// MetaMask semantics: every field the dapp set is honoured verbatim —
// value, data, gas, fee caps, nonce. plan() fills in what is missing.
// The calldata used to be dropped, so a token transfer went out as a
// plain 0-value send to the token contract.
const ethLib = ctx.d.ethAdapter;
const data = ethLib.normalizeData(tx.data ?? tx.input);
const valueWei = tx.value ? ethLib.toBig(tx.value).toString() : "0";
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
return withOriginLock(origin, async () => {
const snap = rt.adapter.snapshot();
const plan = await rt.adapter.plan({
to: tx.to, amount: valueWei, sendMax: false, data,
gasLimit: tx.gas ?? tx.gasLimit, maxFeePerGas: tx.maxFeePerGas,
maxPriorityFeePerGas: tx.maxPriorityFeePerGas, gasPrice: tx.gasPrice, nonce: tx.nonce,
});
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const meta = chainMeta("eth", rt.entry.network);
const ticker = snap.ticker || meta.ticker;
const isCall = data !== "0x";
const rows = [{ label: "To", value: ethLib.eip55(plan.recipients[0].to), mono: true }];
let body = `This site is asking your wallet to send ${ticker}.`;
let risky = false;
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
let approval = false;
if (isCall) {
let code = "0x";
try { code = await rt.adapter._client.call("eth_getCode", [plan.recipients[0].to, "latest"]); } catch {}
rows.push({ label: "Destination", value: code && code !== "0x" ? "smart contract" : "NOT a contract — calldata sent to a plain address does nothing" });
const call = ethLib.decodeCalldata(data);
if (call && call.name === "transfer") {
rows.push({ label: "Call", value: `transfer ${call.unlimited ? "UNLIMITED" : call.amount.toString()} token units to ${call.to}`, strong: true });
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
} else if (call && (call.name === "approve" || call.name === "increaseAllowance" || call.name === "increaseApproval")) {
risky = !!call.unlimited;
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
approval = true;
rows.push({ label: "Call", value: `${call.name}: let ${call.spender} spend ${call.unlimited ? "UNLIMITED (∞)" : call.amount.toString() + " token units (for an NFT contract: token #" + call.amount.toString() + ")"}`, strong: true });
} else if (call && call.name === "permit2Approve") {
risky = true;
approval = true;
rows.push({ label: "Call", value: `Permit2 approve: ${call.spender} may spend ${call.unlimited ? "UNLIMITED (∞)" : call.amount.toString() + " units"} of token ${call.token} until ${call.deadline ? new Date(Number(call.deadline) * 1000).toISOString().slice(0, 10) : "?"}`, strong: true });
} else if (call && (call.name === "safeTransferFrom" || call.name === "safeTransferFrom1155")) {
rows.push({ label: "Call", value: `transfer NFT/token #${call.tokenId?.toString()}${call.amount != null ? " × " + call.amount.toString() : ""}: ${call.from} → ${call.to}`, strong: true });
} else if (call && call.name === "safeBatchTransferFrom") {
risky = true;
rows.push({ label: "Call", value: `batch transfer of several tokens: ${call.from} → ${call.to} (items not decoded)`, strong: true });
} else if (call && call.name === "multicall") {
rows.push({ label: "Call", value: `multicall: several calls bundled together (not decoded, ${call.bytes} bytes)`, mono: true });
} else if (call && call.name === "transferFrom") {
rows.push({ label: "Call", value: `transferFrom ${call.from} → ${call.to}: ${call.amount.toString()} units`, strong: true });
} else if (call && call.name === "setApprovalForAll") {
risky = !!call.approved;
rows.push({ label: "Call", value: `setApprovalForAll: ${call.approved ? "GRANT" : "revoke"} ${call.operator} control over every NFT in this collection`, strong: true });
} else if (call && call.name === "permit") {
risky = !!call.unlimited;
rows.push({ label: "Call", value: `permit: ${call.spender} may spend ${call.unlimited ? "UNLIMITED (∞)" : call.value.toString()} units`, strong: true });
} else {
rows.push({ label: "Call", value: call ? `unknown method 0x${call.selector} (${call.bytes} bytes, not decoded)` : `${(data.length - 2) / 2} bytes of calldata`, mono: true });
}
body = risky
? "WARNING: this call grants another address open-ended control over your tokens. Only sign if you fully trust this site."
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
: approval
? "This call lets another address spend your tokens up to the amount shown. Check the spender and the amount."
: "This transaction calls a contract. Aegis decoded what it could — check the destination, the call and the value.";
}
if (plan.feeWarning) {
risky = true;
rows.unshift({ label: "Warning", value: plan.feeWarning, strong: true });
}
rows.push({ label: "Amount", value: `${fmtValue(plan.recipients[0].value, meta.decimals)} ${ticker}`, strong: true });
rows.push({ label: "Fee (est.)", value: `${fmtValue(plan.feeEstimate, meta.decimals)} ${ticker} · max ${fmtValue(plan.fee, meta.decimals)} ${ticker}` });
rows.push({ label: "Gas", value: `${plan.gasLimit} · nonce ${plan._draft.nonce}` });
rows.push({ label: "Wallet", value: `${rt.entry.label} — ${meta.label}` });
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const pick = await api.approvalModal({
title: isCall ? "Send Ethereum contract call?" : "Send Ethereum transaction?",
origin, body, rows,
actions: [{ id: "send", label: risky ? "Send anyway" : "Send", primary: !risky, danger: risky }],
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
});
if (pick !== "send") throw new Error("user rejected");
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 requireDappTxPin(origin, "Ethereum transaction");
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const r = await rt.adapter.signAndBroadcast(plan);
return { txid: r.txid };
});
});
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
api.onMessage("eth.signTypedData", async (p, m) => {
const origin = fromPage(m);
if (!ethConnectedFor(origin)) throw new Error("not connected — call eth_requestAccounts first");
const rt = activeEthRuntime(origin);
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
// Dapps send typedData as either a JSON string (older MetaMask spec) or
// an object (v4). Accept both; the encoder wants an object.
let td = p && p.typedData;
if (typeof td === "string") { try { td = JSON.parse(td); } catch (e) { throw new Error("typedData: JSON parse failed: " + e.message); } }
if (!td || typeof td !== "object") throw new Error("typedData required");
// Compute the digest first — if the encoder rejects the input the user
// never sees an approval overlay for a broken payload.
let digest;
try { digest = ctx.d.eip712.digest(td); }
catch (e) { throw new Error("EIP-712 encode failed: " + e.message); }
// Approval overlay: show domain (name + chain), primary type, and a
// truncated JSON preview of the message so the user has a fighting
// chance to spot phishing.
const dom = td.domain || {};
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 snapTd = rt.adapter.snapshot();
// A signature for another chain is the standard way to get an approval
// the user believes is for a testnet or a sidechain. MetaMask refuses a
// domain whose chainId is not the active chain; so does Aegis.
if (domDeclaredEarly(td) && dom.chainId != null && dom.chainId !== "") {
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 domChain = null;
try { domChain = BigInt(dom.chainId); } catch {}
if (domChain === null || domChain !== BigInt(snapTd.chainId)) {
throw ethError(`typed data is for chain ${dom.chainId} but this site is connected on chain ${snapTd.chainId}`, 4901);
}
}
// Only the domain fields EIP712Domain declares are hashed. A site could
// declare [name] alone and still send verifyingContract/chainId, which
// were shown (and chain-checked) although the signature never covers
// them; they are now shown as not signed.
const domDeclared = Array.isArray(td.types?.EIP712Domain) ? td.types.EIP712Domain.map((x) => x && x.name) : [];
const dd = (k) => (domDeclared.includes(k) ? dom[k] : undefined);
const domainSummary = [dd("name") && plainLabel(dd("name"), 60), dd("version") && `v${plainLabel(dd("version"), 20)}`, dd("chainId") != null && `chain ${dd("chainId")}`].filter(Boolean).join(" · ") || "(no domain)";
const undeclaredDomain = Object.keys(dom).filter((k) => !domDeclared.includes(k));
// Preview only what the signature covers (see signedView).
const view = signedView(td);
const assessed = assessTypedData(td, view, snapTd.address);
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 rows = [{ label: "Domain", value: domainSummary }];
if (dd("verifyingContract")) rows.push({ label: "Contract", value: String(dd("verifyingContract")), mono: true });
if (undeclaredDomain.length) rows.push({ label: "Not signed", value: `domain field${undeclaredDomain.length === 1 ? "" : "s"} ${undeclaredDomain.join(", ")} (sent but not covered by the signature)` });
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
rows.push({ label: "Primary type", value: String(td.primaryType || "") });
// Off-chain approvals. A Permit / Permit2 signature moves no gas and
// shows no transaction, yet lets the spender take the tokens later — it
// is how most wallet drains happen now. Pull the spender, the amounts
// and the deadline out of the message and say what they mean.
const permit = describePermit(td);
if (permit) for (const r of permit.rows) rows.push(r);
// Marketplace orders and multisig transactions are not permits but move
// NFTs, tokens or a whole Safe just as surely once signed.
// The name list now only labels what the structure test already found.
const MOVES_ASSETS = /^(OrderComponents|BulkOrder|Order|MakerOrder|TakerOrder|SafeTx|SafeMessage|MetaTransaction|ForwardRequest)$/;
const movesAssets = !permit && (assessed.movesAssets || MOVES_ASSETS.test(String(td.primaryType || "")));
const fieldText = assessed.fields.slice(0, 60).join("\n") + (assessed.fields.length > 60 ? `\n… ${assessed.fields.length - 60} more fields are NOT shown but will be signed` : "");
rows.push({ label: "Signed fields", value: previewText(fieldText || "(none)", 4000), mono: true });
if (view.dropped) rows.push({ label: "Not signed", value: `${view.dropped} field${view.dropped === 1 ? "" : "s"} the site sent are not covered by this signature and are not shown` });
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
rows.push({ label: "Address", value: snapTd.address, mono: true });
const risky = !!(permit && permit.risky) || movesAssets;
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
return withOriginLock(origin, async () => {
const pick = await api.approvalModal({
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
title: permit ? "Sign a token spending permit?" : "Sign typed data (EIP-712)?",
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
origin,
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
body: permit
? (risky
? "WARNING: signing this lets the spender below take these tokens at any time, without another prompt and without a transaction from you. Only sign if you fully trust this site."
: "Signing this lets the spender below move the stated amount of your tokens without a transaction from you.")
: movesAssets
? `WARNING: this ${plainLabel(td.primaryType, 60)} names another address together with an amount (or carries raw call data). Signed, it can move your tokens, NFTs or a Safe without another prompt. Only sign if you fully trust this site and every field below is what you expect.`
: assessed.plain
? "The site is asking you to sign a structured message with no addresses or amounts in it. Verify the domain matches the site you're on."
: "The site is asking you to sign structured data that contains addresses or numbers. Verify the domain matches the site you're on and check every field — a mismatched domain is the classic phishing tell.",
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
rows,
actions: [{ id: "sign", label: risky ? "Sign anyway" : "Sign", primary: !risky, danger: risky }],
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
});
if (pick !== "sign") throw new Error("user rejected");
// A permit, an order or a Safe transaction moves assets as surely as a
// transaction does.
if (permit || movesAssets) await requireDappTxPin(origin, permit ? "token spending permit" : "signature that can move assets");
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
return rt.adapter.signTypedDataDigest(digest);
});
});
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
api.onMessage("eth.switchChain", async (p, m) => {
const origin = fromPage(m);
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
const wantHex = String(p && p.chainId || "").toLowerCase();
const wantId = Number(wantHex);
if (!Number.isFinite(wantId) || wantId <= 0) throw new Error("bad chainId");
// EIP-3326: the well-known "chain not added" code makes dapps fall back
// to wallet_addEthereumChain.
if (!ethRuntimeForChain(wantId)) throw ethError(`Aegis: chainId ${wantHex} is not added. Ask via wallet_addEthereumChain.`, 4902);
// Per origin only: THIS site moves to the wallet on that chain. It used
// to flip the global selected wallet, so any site — connected or not —
// could move every other connected dapp onto another chain. Unconnected
// sites just have the preference remembered for their connect prompt.
if (!(await moveEthGrant(origin, wantId))) rememberPendingChain(origin, wantId);
return null;
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
});
// EIP-3085: dapp asks Aegis to add a new EVM chain. On approval, we
// persist the chain config and create a wallet on it under the same
// vault-derived key. Existing addresses on that chain remain visible on
// whatever wallet they were funded on — a chain add doesn't move any
// key material, just registers the network.
api.onMessage("eth.addChain", async (p, m) => {
const origin = fromPage(m);
const spec = (p && p.params) || {};
const chainIdHex = String(spec.chainId || "").toLowerCase();
const chainId = Number(chainIdHex);
if (!chainIdHex.startsWith("0x") || !Number.isFinite(chainId) || chainId <= 0) {
throw new Error("wallet_addEthereumChain: chainId must be a positive hex integer (e.g. '0x89')");
}
const chainName = String(spec.chainName || "").trim() || `EVM #${chainId}`;
const rpcUrls = Array.isArray(spec.rpcUrls) ? spec.rpcUrls.filter((u) => /^https:\/\//i.test(u)) : [];
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
const rpcUrl = rpcUrls[0];
if (!rpcUrl) throw new Error("wallet_addEthereumChain: at least one https rpcUrls entry is required");
const explorerBase = Array.isArray(spec.blockExplorerUrls) && spec.blockExplorerUrls[0]
? String(spec.blockExplorerUrls[0]).replace(/\/+$/, "")
: null;
const nc = spec.nativeCurrency || {};
const ticker = String(nc.symbol || "ETH").slice(0, 6).toUpperCase();
// Reject if a wallet on this chain already exists — no-op success per
// EIP-3085 conventions.
// Built-in networks carry no chainId in chainMeta, so "add chain 1 with
// my RPC" used to create a second mainnet wallet whose fees, nonce and
// balance came from the site's RPC. Their ids are refused outright.
const builtinIds = Object.values(ctx.d?.ethAdapter?.NETWORKS || {}).map((n) => Number(n.chainId));
if (builtinIds.includes(Number(chainId)) && !walletEntries().some((w) => w.chain === "eth" && Number(ctx.runtimes.get(w.id)?.adapter?.snapshot?.()?.chainId) === Number(chainId))) {
throw ethError(`chain ${chainId} is built into Aegis; add it in the Aegis panel, not from a site`, 4001);
}
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
const existing = walletEntries().find((w) => w.chain === "eth"
&& (w.network === CUSTOM_ETH_PREFIX + chainId
|| (chainMeta("eth", w.network)?.chainId === chainId)
|| Number(ctx.runtimes.get(w.id)?.adapter?.snapshot?.()?.chainId) === Number(chainId)));
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
if (existing) {
// Already known: behaves like a switch for THIS origin only. Never a
// grant — this used to persist a connection without any prompt, so
// any site could read the address by "adding" a chain Aegis already
// had. An unconnected site gets the chain remembered for its eventual
// eth_requestAccounts and learns nothing else.
if (!(await moveEthGrant(origin, chainId))) rememberPendingChain(origin, chainId);
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
return null;
}
return withOriginLock(origin, async () => {
const pick = await api.approvalModal({
title: "Add an Ethereum chain?",
origin,
body: "The site is asking to add a new EVM network to Aegis. Verify the RPC and chain ID — a malicious 'chain add' can point you at a fraudulent RPC that intercepts your reads or signs." + (grantFor(api, origin, "eth") ? " Aegis makes a new wallet for it, and a connected site will see that wallet's address." : ""),
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
rows: [
{ label: "Chain name", value: chainName },
{ label: "Chain ID", value: `${chainId} (${chainIdHex})` },
{ label: "Native ticker", value: ticker },
{ label: "RPC", value: rpcUrl, mono: true },
{ label: "Explorer", value: explorerBase || "(none)", mono: true },
],
actions: [{ id: "add", label: "Add chain", primary: true }],
});
if (pick !== "add") throw new Error("user rejected");
// Persist chain config + create a wallet on it.
const chains = customEthChains(api);
chains[String(chainId)] = {
chainId, chainName, rpcUrl,
explorerTx: explorerBase ? explorerBase + "/tx/" : "",
explorerAddr: explorerBase ? explorerBase + "/address/" : "",
ticker, addedAt: Date.now(), addedByOrigin: origin,
};
api.storage.set("customEthChains", chains);
const network = CUSTOM_ETH_PREFIX + chainId;
const meta = chainMeta("eth", network);
const list = walletEntries().slice();
const index = nextIndex(list, meta);
const purpose = meta.purposePrefix + index;
const id = makeWalletId(meta, index);
const label = `${chainName} — ${meta.short}`;
const entry = { id, label, chain: "eth", network, purpose, createdAt: Date.now() };
list.push(entry);
writeWallets(api, list);
api.storage.set("selectedWalletId", id);
ctx.runtimes.set(id, { entry, phase: "locked", error: null, adapter: null });
emitState();
await mountWallet(entry);
// Adding a chain is not connecting. A connected site moves to the new
// chain; an unconnected one has it remembered for its connect prompt.
if (!(await moveEthGrant(origin, chainId, true))) rememberPendingChain(origin, chainId);
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
return null;
});
});
// Cheap state peek — used by the main-world bridge right after a switch
// or add to emit accountsChanged / chainChanged without needing another
// approval overlay. Only returns the wallet the origin already sees.
api.onMessage("eth.state", (_p, m) => {
const origin = fromPage(m);
if (!ethConnectedFor(origin)) return { address: null, chainIdHex: "0x0", networkVersion: "0" };
const rt = activeEthRuntime(origin);
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
const snap = rt.adapter.snapshot();
return {
address: snap.address,
chainIdHex: "0x" + Number(snap.chainId).toString(16),
networkVersion: String(snap.chainId),
};
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
});
// Read passthrough: forward eth_getBalance / eth_call / etc. to the
// wallet's own configured RPC. Nothing here reveals the private key.
api.onMessage("eth.rpc", async (p, m) => {
const origin = fromPage(m);
const rt = activeEthRuntime(origin);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const method = String(p && p.method || "");
const params = (p && p.params) || [];
if (!/^eth_|^net_|^web3_/.test(method)) throw new Error("Aegis: only eth_/net_/web3_ read methods are passed through");
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 user's RPC endpoint is theirs (often a keyed, metered one). A site
// that has not connected gets the three calls every dapp makes before
// asking to connect and nothing else; signing, account and filter-state
// methods are never relayed, and broadcasting needs a connection.
const connected = ethConnectedFor(origin);
if (!connected && !ETH_RPC_PRECONNECT.has(method)) throw ethError("not connected — call eth_requestAccounts first", 4100);
if (ETH_RPC_NEVER.test(method)) throw ethError(`Aegis does not relay ${method}`, 4200);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
return rt.adapter._client.call(method, params);
});
// ---- Solana (wallet-adapter) ------------------------------------------
function activeSolRuntime(origin) {
const bound = boundRuntime(api, origin, "sol", "sol");
if (bound) return bound;
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const selId = selectedWalletId();
const selRt = selId && ctx.runtimes.get(selId);
if (selRt && selRt.entry.chain === "sol" && selRt.phase === "ready") return bindGrant(api, origin, "sol", selRt);
for (const rt of ctx.runtimes.values()) if (rt.entry.chain === "sol" && rt.phase === "ready") return bindGrant(api, origin, "sol", rt);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
throw new Error("no Solana wallet available — add one in the Aegis sidebar");
}
function solConnectedFor(origin) { return !!grantFor(api, origin, "sol"); }
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
// Is this site already connected? Lets a Wallet Standard "silent" connect
// (what adapters do on page load to restore a session) succeed without a
// prompt for a connected site and stay quiet for everyone else.
api.onMessage("sol.state", (_p, m) => {
const origin = fromPage(m);
if (!solConnectedFor(origin)) return { address: null };
try { const snap = activeSolRuntime(origin).adapter.snapshot(); return { address: snap.address, network: snap.network }; }
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
catch { return { address: null }; }
});
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
api.onMessage("sol.connect", async (_p, m) => {
const origin = fromPage(m);
const rt = activeSolRuntime(origin);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const snap = rt.adapter.snapshot();
if (solConnectedFor(origin)) return { address: snap.address, network: snap.network };
return withOriginLock(origin, async () => {
const pick = await api.approvalModal({
title: "Connect this site to your Solana wallet?",
origin,
body: "The site will see this address and can build transactions for you to sign.",
rows: [
{ label: "Address", value: snap.address, mono: true },
{ label: "Network", value: snap.network === "mainnet" ? "Mainnet-beta" : "Devnet" },
{ label: "Wallet", value: `${rt.entry.label} — Solana · ${snap.network}` },
],
actions: [{ id: "allow", label: "Connect", primary: true }],
checkbox: { id: "always", label: "Always allow this site to see this address" },
});
if (!pick.startsWith("allow")) throw new Error("user rejected");
setGrant(api, origin, "sol", { readAddress: true, network: snap.network, walletId: rt.entry.id, address: snap.address }, pick === "allow+always");
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
return { address: snap.address, network: snap.network };
});
});
api.onMessage("sol.signMessage", async (p, m) => {
const origin = fromPage(m);
if (!solConnectedFor(origin)) throw new Error("not connected — call solana.connect first");
const rt = activeSolRuntime(origin);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const b64 = String(p && p.messageB64 || "");
const bytes = new Uint8Array(Buffer.from(b64, "base64"));
if (bytes.length > 16384) throw new Error("message too long");
// A "message" that parses as a transaction message would, once signed,
// be a valid transaction signature behind a "moves no SOL" overlay.
// Phantom refuses these; so do we.
if (parseSolMessage(bytes, rt.adapter._pub).ok) {
throw new Error("refusing to sign: this message is a Solana transaction. Use signTransaction.");
}
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
return withOriginLock(origin, async () => {
const pick = await api.approvalModal({
title: "Sign a Solana message?",
origin,
body: "Signing proves you control this address. It moves no SOL.",
rows: [
{ label: "Message", value: previewBytes(bytes), mono: true },
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
{ label: "Address", value: rt.adapter.snapshot().address, mono: true },
],
actions: [{ id: "sign", label: "Sign", primary: true }],
});
if (pick !== "sign") throw new Error("user rejected");
return rt.adapter.signBytes(bytes);
});
});
// Dapp-built transaction the dapp will broadcast itself (window.solana
// .signTransaction). It used to go through signMessage and its "moves no
// SOL" overlay; it gets its own decoded overlay now: signing IS sending.
api.onMessage("sol.signTransaction", async (p, m) => {
const origin = fromPage(m);
if (!solConnectedFor(origin)) throw new Error("not connected — call solana.connect first");
const rt = activeSolRuntime(origin);
const messageBytes = new Uint8Array(Buffer.from(String(p && p.messageB64 || ""), "base64"));
const parsed = parseSolMessage(messageBytes, rt.adapter._pub);
if (!parsed.ok) throw new Error("cannot parse transaction message: " + parsed.error);
if (parsed.ourIndex < 0) throw new Error("this wallet's key is not among the transaction's account keys");
if (parsed.ourIndex >= parsed.numRequiredSigs) throw new Error(`this wallet's key is not a required signer (index ${parsed.ourIndex}, requiredSigs ${parsed.numRequiredSigs})`);
return withOriginLock(origin, async () => {
const snap = rt.adapter.snapshot();
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
const solBal = (() => { const b = snap.balance; const v = b && typeof b === "object" ? (b.confirmed ?? b.lamports) : b; try { return v != null ? BigInt(v) : null; } catch { return null; } })();
const rows = solInstructionRows(parsed, solBal);
rows.push({ label: "Address", value: snap.address, mono: true });
rows.push({ label: "Wallet", value: `${rt.entry.label} — Solana · ${snap.network}` });
const pick = await api.approvalModal({
title: "Sign a Solana transaction?",
origin,
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
body: "The site built this transaction and will broadcast it itself — signing it is the same as sending it."
+ (rows.risks.length ? " Read the warning: this is not an ordinary payment." : rows.undecoded ? " Parts marked 'not decoded' are programs Aegis cannot read." : ""),
rows,
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
actions: [{ id: "sign", label: rows.risks.length ? "Sign anyway" : "Sign", primary: !rows.risks.length, danger: rows.risks.length > 0 }],
});
if (pick !== "sign") throw new Error("user rejected");
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 requireDappTxPin(origin, "Solana transaction");
return rt.adapter.signBytes(messageBytes);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
});
});
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
// Dapp-built transaction. The main-world bridge passes the FULL wire
// (tx.serialize({requireAllSignatures:false, verifySignatures:false}))
// — signature slots the dapp already filled with partialSign() are
// preserved; our wallet only overwrites its own slot. That's the only
// way to sign multi-signer transactions the dapp has partially
// co-signed (co-signer sigs, ephemeral session keys, etc.).
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
api.onMessage("sol.signAndSend", async (p, m) => {
const origin = fromPage(m);
if (!solConnectedFor(origin)) throw new Error("not connected — call solana.connect first");
const rt = activeSolRuntime(origin);
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
const wireB64 = String(p && p.wireB64 || "");
if (!wireB64) throw new Error("wireB64 required (full serialized transaction)");
const wire = new Uint8Array(Buffer.from(wireB64, "base64"));
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
const { sigCount, sigsStart, messageBytes } = splitSolWire(wire);
// Legacy and v0 messages both parse (v0 used to be read with its
// version byte as the header, so the signer lookup was garbage); our
// key must be a required signer.
const parsed = parseSolMessage(messageBytes, rt.adapter._pub);
if (!parsed.ok) throw new Error("cannot parse transaction message: " + parsed.error);
const { numRequiredSigs, ourIndex } = parsed;
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
if (ourIndex < 0) throw new Error("this wallet's key is not among the transaction's account keys");
if (ourIndex >= numRequiredSigs) throw new Error(`this wallet's key is not a required signer (index ${ourIndex}, requiredSigs ${numRequiredSigs})`);
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
// Our signature is written at sigsStart + ourIndex*64; with fewer slots
// than signers that would overwrite message bytes.
if (sigCount !== numRequiredSigs) throw new Error(`the transaction has ${sigCount} signature slots for ${numRequiredSigs} signers`);
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
return withOriginLock(origin, async () => {
const snap = rt.adapter.snapshot();
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
const solBal = (() => { const b = snap.balance; const v = b && typeof b === "object" ? (b.confirmed ?? b.lamports) : b; try { return v != null ? BigInt(v) : null; } catch { return null; } })();
const rows = solInstructionRows(parsed, solBal);
rows.push({ label: "Address", value: snap.address, mono: true });
rows.push({ label: "Wallet", value: `${rt.entry.label} — Solana · ${snap.network}` });
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const pick = await api.approvalModal({
title: "Sign + send a Solana transaction?",
origin,
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
body: "The site built this transaction. Check every instruction — anything marked 'not decoded' is a program Aegis cannot read."
+ (rows.risks.length ? " Read the warning: this is not an ordinary payment." : ""),
rows,
Aegis: dapp overlays show fees and flag risky Solana, Ethereum and Tron calls Solana: a ComputeBudget price the site added was paid on top of the base fee but shown as "program ComputeB...: not decoded", so a transaction could burn the balance in priority fees behind a plain Sign button. The maximum network fee is now a row, and Assign, durable nonces, Approve/ApproveChecked, SetAuthority, closing a token account to someone else and very high fees get a warning and the danger button. signAndSend also requires one signature slot per required signer. Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96 and up now does, and Permit2 approve, increaseApproval, NFT safeTransferFrom and multicall are decoded. The estimate shown was gas x the node's price even when the site set a far higher tip, which is what is actually paid; it now uses base fee + the real tip (or the full legacy gasPrice) and warns when the site's fee is far above the network's or a fifth of the balance. A personal_sign over 32 raw bytes is a hash a Safe or an order book will take as approval of something unseen, so it gets the danger button and the PIN. Tron: TRC10 sent along with a contract call (call_token_value) was never read, contract types Aegis does not decode were shown by name as if harmless, a truncated TRC20 call rendered as "undefined", and the validity window was hidden. Those are now shown, flagged or refused; the TronGrid draft check also refuses a memo, a permission id or an expiration more than a day away.
2026-10-04 03:56:45 +02:00
actions: [{ id: "send", label: rows.risks.length ? "Sign & send anyway" : "Sign & send", primary: !rows.risks.length, danger: rows.risks.length > 0 }],
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
});
if (pick !== "send") throw new Error("user rejected");
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 requireDappTxPin(origin, "Solana transaction");
// Sign the exact message bytes and patch our slot. signMessage() ran
// the bytes through String(), so every signature was over "1,2,3,…"
// and the network rejected it. Partial signatures already in the wire
// (tx.partialSign()) at other slots are preserved.
const sigInfo = rt.adapter.signBytes(messageBytes);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const sigBytes = ctx.d.base58check.decodeBase58(sigInfo.signature);
if (sigBytes.length !== 64) throw new Error("bad ed25519 signature length");
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
const wireOut = new Uint8Array(wire); // copy so we don't mutate caller
wireOut.set(sigBytes, sigsStart + ourIndex * 64);
const wireB58 = ctx.d.base58check.encodeBase58(wireOut);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
const txid = await rt.adapter._client.call("sendTransaction", [wireB58]);
if (typeof txid !== "string" || !txid.length) throw new Error("bad txid from RPC: " + JSON.stringify(txid));
setTimeout(() => rt.adapter.refresh().catch(() => {}), 4000);
return { txid };
});
});
}
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
// ---- activate ---------------------------------------------------------------
module.exports = {
// Returns a promise that settles once Aegis can serve calls. Aegis starts
// on first use, so the call that woke it is the first thing it sees, and
// the host holds that call (up to 5 s) until this promise settles.
activate(api) {
// No explicit icon — inherit manifest.icon (branded shield data URI)
// so the toolbar dock button renders the aegis.x brand mark instead
// of a fallback emoji. Same id as the panel addon.json declares, which
// this replaces.
api.registerSidebarPanel({ id: "main", title: "Wallet", page: "panel.html" });
perf(aegis): stop freezing Theseus — a 7.4 MB store parsed on every read Reported as "opening Aegis makes Theseus get stuck / not responding", and it was Aegis's fault. The host's add-on store is ONE JSON file per add-on, and storage.get() does a readFileSync plus a JSON.parse of the whole thing on every call — synchronously, on the Electron main thread, the thread that drives the entire browser. storage.set() additionally stringifies and writes all of it. That store had grown to 7.4 MB, 99.9% of it one wallet's txCache: getTx() kept every transaction it ever fetched, with full vin/vout arrays, and a busy chipnet test wallet had thousands. Measured on the real file: parse 59 ms, stringify 39 ms. So one storage.get blocked the UI for ~60 ms, one set for ~99 ms, and fullState() — which reads the store seven times over selectedWalletId, walletRoles, walletEntries, snapshotForSelected and the server list — cost ~420 ms. emitState() runs on every adapter change, across nine wallets, so the main thread was never given back. Three changes: - txCache is capped at 400 entries, pruned newest-first by block time (unconfirmed entries sort as newest — they are the current ones). Worst case ~0.3 MB per wallet instead of unbounded. - TX_CACHE_VERSION 3, so existing oversized caches are discarded on first load rather than needing a manual clear. - loadHistory() only persists when something actually changed; an idle wallet was rewriting the whole file on every poll for nothing. - api.storage gains a write-through read cache, so repeated gets cost one parse per process instead of one per call. Writes still go to the host unchanged. Safe because this process is the only writer — Aegis's panel talks over addon messages and never touches addon storage; if that changes, the cache has to go.
2026-10-03 10:56:59 +02:00
// Serve reads from memory. The host's storage.get() does a readFileSync
// plus a JSON.parse of the ENTIRE add-on store on every single call, on
// the Electron main thread. fullState() alone reads it seven-odd times
// (selectedWalletId, walletRoles, walletEntries, snapshotForSelected,
// the server list...) and emitState() runs on every adapter change, so a
// large store made opening Aegis hang the whole browser.
//
// Safe because this process is the only writer: Aegis's panel talks to
// the host over addon messages and never touches addon storage directly.
// If that ever changes, this cache has to go or be invalidated.
installStorageCache(api, (key) => { if (LAUNCH_START_KEYS.test(key)) syncLaunchStart(api); });
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 c = ctx = {
api,
d: null,
runtimes: new Map(), // walletId -> { entry, phase, error, adapter }
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
priceFeed: require("./lib/prices.js")({
log: (...a) => api.log("prices", ...a),
onChange: () => emitState(),
}),
// BCMR (CashTokens metadata registry) — resolves category hex to
// { name, symbol, iconUri, decimals }. Storage-scoped so per-user
// caches don't stomp each other; disk-cached with 6h TTL.
bcmr: require("./lib/bcmr.js")({
storage: api.storage,
log: (...a) => api.log("bcmr", ...a),
}),
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
wc: null, // WizardConnect manager, initialised when deps load
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
};
migrateLegacyStorage(api);
registerPanelMessages(api);
registerPageMessages(api);
launchStartAsked = null;
syncLaunchStart(api);
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
// Restore the user's opt-in choice from storage. Off by default so a
// fresh install never hits any oracle without asking. Source can also
// be pre-restored so a user who picked Kraken stays on Kraken.
const savedSource = String(api.storage.get("pricesSource", "") || "").trim();
if (savedSource) c.priceFeed.setSource(savedSource).catch(() => {});
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 (api.storage.get("pricesEnabled", false)) c.priceFeed.setEnabled(true).catch(() => {});
fix(theseus/boot): paint the toolbar first — stop gating startup on chrome.html's load event Users saw a blank window with a white strip across the top for seconds on launch. Root cause: every part of startup, including session restore, waited for chrome.html's did-finish-load. That event also waits for the page's subresources, and the bookmarks bar loads its favicons over bns:// — a BNS lookup plus a network fetch each — so a slow link held the whole boot. On top of that, seven hidden overlay renderers, every restored tab, the BNS index build and three network fetches all started in the same tick and stalled the main thread ~1 s while the toolbar tried to paint. - Continue boot at chrome.html's dom-ready (toolbar scripts have run, IPC listeners exist) instead of did-finish-load; 8 s fallback timer. - Window and chrome view get the toolbar's --bg for the active theme so the pre-paint frame is never white. - Overlay pages (site info, engine picker, downloads, suggestions, password fill, link status, approval) load 250 ms after the toolbar or on first use; the approval modal awaits its page so a dapp request can't hang. - Session restore is staggered: active tab first, then one background tab per 150 ms slotted into its saved strip position. Session file v2 records the active index; v1 arrays still load (active = last, as the old loop effectively did). - AddonHost gains api.whenUiReady(); Aegis 0.6.2 defers its heavy dependency loading (noble precompute, bitcoinjs, libauth, WizardConnect) behind it. - BNS snapshot warm-up still starts right after createWindow (bookmark favicons need it); Sia refresh, update check and home-card fetch move to the post-paint phase. Measured on a clone of the real profile with nine restored tabs: toolbar usable at ~0.7 s instead of ~1.5 s, main-thread stall during toolbar load down from ~1.1 s to ~0.2 s.
2026-09-09 11:40:45 +02:00
// The deps are heavy to evaluate (noble curve precompute, bitcoinjs,
// libauth, WizardConnect) and that all happens on the main thread. Wait
// for the browser chrome to paint so the cost never delays Theseus's
// first frame; older hosts without whenUiReady load immediately.
const uiReady = typeof api.whenUiReady === "function" ? api.whenUiReady() : Promise.resolve();
const started = uiReady.then(() => loadDeps(api)).then(async (d) => {
if (ctx !== c) return;
c.d = d;
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
// Start the WizardConnect manager once deps are ready. Per-wallet
// adapters get spun up in mountWallet() as each BCH wallet becomes
// available (only BCH today — hdwalletv1 is BCH-scoped).
c.wc = require("./lib/wc.js")({
HDKey: d.HDKey, secp256k1: d.secp256k1, sha256: d.sha256, hkdf: d.hkdf,
WalletConnectionManager: d.wcWallet.WalletConnectionManager,
wcCore: d.wcCore, libauth: d.libauth,
log: (...a) => api.log("wc", ...a),
api,
// Bridge sign approvals through the addon's approval-modal capability.
approvalRequest: async (payload) => {
// The host overlay only knows `actions`; the old `approve`/`reject`
// keys fell back to a lone "OK" button whose id never matched, so
// every WizardConnect signing request was refused, and the HTML
// body was shown as literal markup.
const { body, rows, dappName, unreadable, origin: wcOrigin } = buildWcApproval(payload);
if (unreadable) { api.log(`wc sign: refused an unreadable request from ${dappName}`); return { approved: false }; }
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 pick = await api.approvalModal({
title: "Sign a Bitcoin Cash transaction (WizardConnect)?",
origin: wcOrigin,
body, rows,
actions: [{ id: "approve", label: "Sign", primary: 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
});
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 (pick !== "approve") return { approved: false };
try { await requireDappTxPin(dappName, "Bitcoin Cash transaction (WizardConnect)"); }
catch (e) { api.log("wc sign:", e?.message || e); return { approved: false }; }
return { approved: 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
},
});
c.wc.onStateChange(() => emitState());
await tryAutoUnlock(api);
// vault.derive() polls until the user unlocks, so with a locked vault
// mountAllWallets() never settles. Only wait for it when the vault is
// open; otherwise the waking call gets the usual locked answer now
// rather than after the host's 5 s timeout.
const st = await api.vault.lifecycle.status().catch(() => null);
const mounting = mountAllWallets();
if (st && st.unlocked) await mounting;
else mounting.catch((e) => api.log("mount:", e?.message || e));
});
started.catch((e) => {
if (ctx !== c) return;
api.log("startup failed:", e?.message);
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
emitState();
});
return started.catch(() => {});
},
deactivate() {
const c = ctx; ctx = null;
if (!c) 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
try { c.priceFeed && c.priceFeed.dispose(); } 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
for (const rt of c.runtimes.values()) {
try { rt.adapter && rt.adapter.dispose(); } catch {}
}
c.runtimes.clear();
},
};