From 464d83316b5fa09591bb086ea69fd5cf823c4792 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 27 Sep 2026 00:01:39 +0200 Subject: [PATCH] =?UTF-8?q?fix(aegis):=200.9.10=20=E2=80=94=20say=20why=20?= =?UTF-8?q?a=20wallet=20can't=20pair,=20and=20stop=20imports=20advertising?= =?UTF-8?q?=20the=20wrong=20key=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every chipnet wallet in the picker read "can't pair" with the reason nowhere on screen: the explanatory note only rendered when NOTHING could pair, so one working mainnet wallet hid it entirely. The cause now rides on the row itself ("can't pair (WIF import)") and the note appears whenever any wallet is blocked. A single private key has no chain code, so there is no xpub for the handshake to send — the text now says that and points at the two ways out. Seed-imported wallets stored the full leaf path (m/44'/1'/0'/0/0) in accountPath, because that is what derived the one address the strip shows. WizardConnect was handed that as the BIP44 *account* node and would derive m/44'/1'/0'/0/0//: a tree the user holds no keys in. Pairing looked healthy and every sign request failed with "no path for input". Same class as the 0.9.7 chipnet mismatch, one level down. A dapp drops its pairing code from the DOM once connected, so the commonest empty scan is a page that is already paired. Sending that user to "open the Connect dialog" points at a dialog the dapp will not show again; the message now names the existing pairing instead. --- bundled-addons/aegis/addon.json | 2 +- bundled-addons/aegis/index.js | 42 ++++++++++++++++++++++++++------- bundled-addons/aegis/panel.js | 32 +++++++++++++++++++------ 3 files changed, 60 insertions(+), 16 deletions(-) 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; }