diff --git a/bundled-addons/aegis/addon.json b/bundled-addons/aegis/addon.json index 7f03d099..4e516cad 100644 --- a/bundled-addons/aegis/addon.json +++ b/bundled-addons/aegis/addon.json @@ -1,7 +1,7 @@ { "id": "aegis", "name": "Aegis Wallet", - "version": "0.9.9", + "version": "0.9.10", "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 0ee3c993..4efd8a90 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -481,6 +481,24 @@ async function mountAllWallets() { 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//: 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("/"); +} + async function mountWallet(entry) { const c = ctx; const rt = c.runtimes.get(entry.id) || { entry, phase: "locked", error: null, adapter: null }; @@ -522,13 +540,19 @@ async function mountWallet(entry) { c.api.vault.imports.signer(entry.importId).then((blob) => { if (ctx !== c) return; if (!blob || blob.kind !== "seed" || !blob.seed) { - wcIneligible.set(entry.id, "This wallet was imported from a single private key. WizardConnect needs a seed phrase to derive the per-dapp keys it signs with."); + 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. Create the wallet from your vault instead, or re-import it from its seed phrase.", + }); emitStateForWallet(entry.id); return; } const seedHex = String(blob.seed).trim(); if (!/^[0-9a-f]+$/i.test(seedHex) || seedHex.length < 32) { - wcIneligible.set(entry.id, "Imported seed material is not in a form WizardConnect can derive from."); + wcIneligible.set(entry.id, { + short: "unsupported key", + detail: "Imported seed material is not in a form WizardConnect can derive from.", + }); emitStateForWallet(entry.id); return; } @@ -537,11 +561,12 @@ async function mountWallet(entry) { 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: entry.accountPath || adapter.snapshot?.()?.accountPath || "m/44'/145'/0'", + root32: root, + accountPath: wcAccountPath(entry.accountPath) || adapter.snapshot?.()?.accountPath || "m/44'/145'/0'", }).finally(() => { try { root.fill(0); } catch {} }); }).catch((e) => { c.api.log(`[${entry.id}] wc start (imported):`, e?.message || e); - wcIneligible.set(entry.id, cleanWcErr(e)); + wcIneligible.set(entry.id, wcErrReason(e)); emitStateForWallet(entry.id); }); } @@ -803,11 +828,11 @@ function overallPhase() { // 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 cleanWcErr(e) { +function wcErrReason(e) { const m = e?.message || String(e); return /locked|vault/i.test(m) - ? "Unlock the password vault to use WizardConnect with this wallet." - : m; + ? { short: "vault locked", detail: "Unlock the password vault to use WizardConnect with this wallet." } + : { short: "unavailable", detail: m }; } function walletSummary(w) { @@ -834,7 +859,8 @@ function walletSummary(w) { tokenBalances: snap?.tokenBalances || null, phase: rt?.phase || "locked", error: rt?.error || null, - wcBlocked: w.chain === "bch" ? (wcIneligible.get(w.id) || null) : null, + wcBlocked: w.chain === "bch" ? (wcIneligible.get(w.id)?.detail || null) : null, + wcBlockedShort: w.chain === "bch" ? (wcIneligible.get(w.id)?.short || null) : null, }; } diff --git a/bundled-addons/aegis/panel.js b/bundled-addons/aegis/panel.js index 0aca5ff8..5b86e1a9 100644 --- a/bundled-addons/aegis/panel.js +++ b/bundled-addons/aegis/panel.js @@ -1917,10 +1917,19 @@ function renderConnectPane(bchWallets) { // Wallets with no derivable seed (WIF single-key imports) can't pair at // all, so they're disabled rather than silently failing on Connect. const pairable = readyBch.filter((w) => !w.wcBlocked); - const options = readyBch.map((w) => ``).join(""); - const blockedNote = (!pairable.length && readyBch.length) - ? `
${esc(readyBch[0].wcBlocked)}
` - : ""; + const options = readyBch.map((w) => ``).join(""); + // Greying an option out with "can't pair" and giving no reason anywhere on + // the page reads as a broken wallet rather than a property of how it was + // added. Show the reasons whenever ANY wallet is blocked — the old note + // only appeared when nothing at all could pair, so a user with one good + // mainnet wallet and ten WIF-imported chipnet ones saw no explanation. + const blocked = readyBch.filter((w) => w.wcBlocked); + const reasons = [...new Set(blocked.map((w) => w.wcBlocked))].map(esc).join(" "); + const blockedNote = !blocked.length + ? "" + : !pairable.length + ? `
${reasons}
` + : `
${blocked.length} of ${readyBch.length} BCH wallets can't pair. ${reasons}
`; // Flatten all connected dapps (across BCH wallets) into one list — the // user thinks "my dapps", not "dapps per wallet". const rows = []; @@ -2004,9 +2013,18 @@ function wireConnectPane() { const res = await S.invoke("wcScanPage"); const uris = res?.uris || []; if (!uris.length) { - msg.textContent = res?.origin - ? `No wiz:// pairing code found on ${res.origin}. Open the dapp's Connect dialog first, then scan again.` - : "No pairing code found on the open tab."; + // A dapp drops its pairing code from the DOM once it is connected, + // so the commonest reason a scan comes up empty is that the page is + // ALREADY paired. Telling that user to "open the Connect dialog" + // sends them looking for a dialog the dapp will not show again. + const alreadyPaired = Object.entries(state?.wc || {}) + .flatMap(([wid, conns]) => (conns || []).map((c) => ({ ...c, walletId: wid }))); + const nameOf = (c) => c.dappName || c.label || "a dapp"; + const walletLabel = (wid) => (state?.wallets || []).find((w) => w.id === wid)?.label || wid; + const where = res?.origin ? ` on ${res.origin}` : " on the open tab"; + msg.textContent = alreadyPaired.length + ? `No wiz:// pairing code found${where}. Aegis already has ${nameOf(alreadyPaired[0])} paired on ${walletLabel(alreadyPaired[0].walletId)} — a dapp removes its code once it is connected, so if this page shows itself as connected there is nothing left to scan. To pair a different wallet, disconnect on both sides first.` + : `No wiz:// pairing code found${where}. Open the dapp's Connect dialog first, then scan again.`; msg.hidden = false; return; }