From 0b4ddee8c5b0c1b0b30d90b357e39f771dc8cf8e Mon Sep 17 00:00:00 2001 From: Local Dev Date: Fri, 2 Oct 2026 00:39:54 +0200 Subject: [PATCH] =?UTF-8?q?feat(aegis):=200.24.0=20=E2=80=94=20ask=20the?= =?UTF-8?q?=20chain=20which=20derivation=20path=20holds=20the=20funds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A seed import offered exactly one prefilled path and no alternatives, so a seed from a wallet on a different path derived a valid but empty address and reported a 0 balance. Nothing distinguished "wrong path" from "empty wallet", which is why repeated imports of a funded wallet all looked identical. Both real cases hit it. Bitcoin.com derives mainnet BCH from coin type 0' — its Copay lineage predates the 145' split — while Aegis prefilled m/44'/145'/0'/0/0. And chipnet tooling derives from 145', while Aegis prefilled BIP44's testnet 1'. - A preset dropdown per coin and network, each entry naming the wallets that path belongs to, with the free-text field kept for anything unlisted. - "Check which path has my funds" derives each candidate and asks Electrum for balance and history, five addresses deep per candidate so a wallet whose first address is spent clean is still found. Results are listed with balances and a one-click Use; when nothing matches, it says so instead of implying the wallet is empty. - The chipnet IMPORT prefill becomes 145'. The vault-derived chipnet account path stays m/44'/1'/0' — changing that would move the addresses of wallets that already exist. The scan derives, queries and returns counts; it stores nothing, and the mnemonic crosses the same boundary importWallet already uses. --- bundled-addons/aegis/addon.json | 2 +- bundled-addons/aegis/index.js | 101 ++++++++++++++++++++++++++++ bundled-addons/aegis/panel.js | 113 +++++++++++++++++++++++++++++++- 3 files changed, 212 insertions(+), 4 deletions(-) diff --git a/bundled-addons/aegis/addon.json b/bundled-addons/aegis/addon.json index f36c65f1..4e7b6d76 100644 --- a/bundled-addons/aegis/addon.json +++ b/bundled-addons/aegis/addon.json @@ -1,7 +1,7 @@ { "id": "aegis", "name": "Aegis Wallet", - "version": "0.23.0", + "version": "0.24.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 a85d3042..8bf656bb 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -2058,6 +2058,107 @@ function registerPanelMessages(api) { 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" }, + ], + "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 || "")}`; + return { key, candidates: SEED_PATH_CANDIDATES[key] || [] }; + }); + + 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)); } + + const prefix = network === "mainnet" ? "bitcoincash" : "bchtest"; + const servers = network === "mainnet" + ? bchServerList(api) + : ["wss://chipnet.imaginary.cash:50004", "wss://chipnet.bch.ninja:50004"]; + const client = new ctx.d.electrum.Client(servers); + // Electrum keys a script by sha256(scriptPubKey), little-endian. + const 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"); + }; + // 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; + try { addr = deriveCashaddrFromSeed(seedHex, pth, prefix); } + 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({ + path: c.path, note: c.note, accountPath: acct, + 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, + }; + }); + api.onMessage("permissions", (_p, m) => { fromPanel(m); return permissions(api); }); api.onMessage("revoke", (p, m) => { fromPanel(m); diff --git a/bundled-addons/aegis/panel.js b/bundled-addons/aegis/panel.js index f32b409c..3552fca8 100644 --- a/bundled-addons/aegis/panel.js +++ b/bundled-addons/aegis/panel.js @@ -1426,7 +1426,15 @@ const IMPORT_COIN_CONFIG = { bch: { label: "Bitcoin Cash", logo: "bch", networks: [ - { id: "chipnet", label: "Chipnet testnet", defaultPath: "m/44'/1'/0'/0/0", testnet: true }, + // Chipnet prefills the BCH coin type, not BIP44's testnet 1'. Chipnet + // is a BCH testnet and the tooling around it (keystore exports, + // faucets, test wallets) overwhelmingly derives from 145'; prefilling + // 1' sent every chipnet seed import to an empty address. 1' is still + // one of the presets, and the scan finds whichever is real. + // NOTE: this is the IMPORT prefill only. Vault-derived chipnet wallets + // keep m/44'/1'/0' (chain-bch.js) — changing that would move the + // addresses of wallets that already exist. + { id: "chipnet", label: "Chipnet testnet", defaultPath: "m/44'/145'/0'/0/0", testnet: true }, { id: "mainnet", label: "Mainnet", defaultPath: "m/44'/145'/0'/0/0" }, ], formats: [ @@ -1733,7 +1741,17 @@ function openImportModal(initialChain) {
Derivation path
+ + + +