fix(aegis): 0.9.10 — say why a wallet can't pair, and stop imports advertising the wrong key tree

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/<branch>/<i>: 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.
This commit is contained in:
Local Dev 2026-09-27 00:01:39 +02:00
parent e761500a2f
commit 464d83316b
3 changed files with 60 additions and 16 deletions

View file

@ -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",

View file

@ -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/<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("/");
}
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,
};
}

View file

@ -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) => `<option value="${esc(w.id)}" ${!w.wcBlocked && w.id === state.selectedWalletId ? "selected" : ""} ${w.wcBlocked ? "disabled" : ""}>${esc(w.label)} · ${esc(w.networkLabel)}${w.wcBlocked ? " — can't pair" : ""}</option>`).join("");
const blockedNote = (!pairable.length && readyBch.length)
? `<div class="msg err" style="margin-bottom:8px">${esc(readyBch[0].wcBlocked)}</div>`
: "";
const options = readyBch.map((w) => `<option value="${esc(w.id)}" ${!w.wcBlocked && w.id === state.selectedWalletId ? "selected" : ""} ${w.wcBlocked ? "disabled" : ""}>${esc(w.label)} · ${esc(w.networkLabel)}${w.wcBlocked ? ` — can't pair (${esc(w.wcBlockedShort || "unsupported")})` : ""}</option>`).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
? `<div class="msg err" style="margin-bottom:8px">${reasons}</div>`
: `<div class="hint" style="margin-bottom:8px">${blocked.length} of ${readyBch.length} BCH wallets can't pair. ${reasons}</div>`;
// 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;
}