fix(aegis): 0.14.0 — a seed import is an HD wallet, not one address
Importing a Bitcoin.com seed showed a balance of zero. Two causes, one mine. Mine: a Copay / Bitcoin.com backup QR is "1|<words>|<network>|<ACCOUNT path>|…", and 0.13.2 dropped that path straight into a box whose contents are derived as a LEAF. Deriving the account node itself yields an address the wallet has never used — for the standard BIP39 vector, qr96x72dpwrjmg8gtmfemmdhn6u8aqdgnvn4fp2906 instead of qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6. An address with no history, so: zero. An account path now extends to /0/0 instead of being derived in place. The deeper one: a seed import mounted on the single-address adapter, which watches exactly one scripthash. Bitcoin.com, Electron Cash and the rest spread funds across a whole BIP44 account, so even with the right leaf the balance only shows if it all happens to sit on the first receive address. A seed import is an HD wallet and now mounts as one — the same BchWallet the vault-derived wallets use, whose WalletKeys walks receive AND change to a gap limit of 20. That is what actually finds the money, and it brings real spend support to seed imports as a side effect. WIF imports are unchanged: one key is one address, nothing to scan. Existing seed imports are picked up without re-importing. accountPath is stored in either shape — older imports kept the full leaf, the QR carries the account — and the mount trims both to the last hardened element before handing it to WalletKeys. The stale display address stored at import time is irrelevant, since the panel reads the address off the adapter's snapshot. Mounting now reads the signer up front to decide which adapter to use. A locked vault still mounts watch-only from the stored address rather than failing, and the WC manager gets its own copy of the root because it keeps a live reference for the per-URI relay-identity HKDF.
This commit is contained in:
parent
4075c65504
commit
6d844b03cf
2 changed files with 85 additions and 40 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue