diff --git a/bundled-addons/aegis/addon.json b/bundled-addons/aegis/addon.json index 4a2306f0..69e24e31 100644 --- a/bundled-addons/aegis/addon.json +++ b/bundled-addons/aegis/addon.json @@ -1,7 +1,7 @@ { "id": "aegis", "name": "Aegis Wallet", - "version": "0.13.2", + "version": "0.14.0", "category": "plugin", "description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites; window.tronWeb / window.tronLink / window.ethereum / window.solana on any https page.", "author": "Silent Mode", diff --git a/bundled-addons/aegis/index.js b/bundled-addons/aegis/index.js index bd95eb1e..2c7e7009 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -520,56 +520,89 @@ async function mountWallet(entry) { network: entry.network, }; if (entry.chain === "bch") { - adapter = new c.d.importedBchAdapter.ImportedBchWallet({ - ...commonOpts, - cashaddr: entry.importedCashaddr || entry.importedAddress, - servers: entry.network === "mainnet" ? bchServerList(c.api) : undefined, - importId: entry.importId, - }); - adapter.schedulePoll(20_000); - // Imported BCH wallets were never registered with WizardConnect — - // startForWallet only ran in the vault-derived branch. They showed - // up in the "Sign with" picker (they mount as ready) and then failed - // on pair with "no manager". Register them here too. + // 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. // - // WC derives a child-key tree, so this needs a seed: mnemonic/seed - // imports qualify, WIF single-key imports never can. registerWcEligible - // records which is which so the panel can say so up front instead of - // offering a pairing that cannot work. - if (c.wc) { - c.api.vault.imports.signer(entry.importId).then((blob) => { - if (ctx !== c) return; - if (!blob || blob.kind !== "seed" || !blob.seed) { - 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.", - }); - emitStateForWallet(entry.id); - return; - } + // 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) { const seedHex = String(blob.seed).trim(); - if (!/^[0-9a-f]+$/i.test(seedHex) || seedHex.length < 32) { + 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.", }); - emitStateForWallet(entry.id); - return; } - const root = new Uint8Array(seedHex.match(/../g).map((x) => parseInt(x, 16))); - return c.wc.startForWallet({ - walletId: entry.id, label: entry.label, - // Same network-aware default as the vault-derived branch — - // an imported chipnet wallet had the identical mismatch. - root32: root, - accountPath: wcAccountPath(entry.accountPath) || adapter.snapshot?.()?.accountPath || "m/44'/145'/0'", - }).finally(() => { try { root.fill(0); } catch {} }); + } 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'", }).catch((e) => { c.api.log(`[${entry.id}] wc start (imported):`, e?.message || e); wcIneligible.set(entry.id, wcErrReason(e)); emitStateForWallet(entry.id); }); } + // 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, @@ -792,6 +825,18 @@ function unmountWallet(walletId) { // 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. +// 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; +} + function deriveCashaddrFromSeed(seedHex, path, prefix) { const d = ctx.d; const seed = new Uint8Array(seedHex.length / 2); @@ -1057,12 +1102,12 @@ function registerPanelMessages(api) { } 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")); - address = deriveCashaddrFromSeed(spec.seed, spec.path, prefix); + 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")); - address = deriveCashaddrFromSeed(spec.seed, spec.path, prefix); + 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") {