Aegis Wallet 0.4.4 → 0.6.1:
- Vault lifecycle from the wallet gate. The locked / not-yet-created states
now show a master-password form (with optional BIP39 mnemonic on setup)
instead of redirecting users to Settings › Passwords. New
api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by
the existing "vault-derive" capability. api.openSettings(section) also
added; settings.html honours a #section hash on open.
- Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44
path or a WIF; the cashaddr is derived in the add-on, the signer material
goes to a separate wallet-imports.enc via api.vault.imports {list, add,
remove, signer}. Argus password-vault gains createImports / unlockImports /
saveImports with its own KDF salt so the imports key is disjoint from the
passwords key. lib/chain-bch-imported.js is a single-address Electrum
adapter; spend support is deferred to M.1b.
- Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted
in add-on storage. Fiat lines under balances, in the wallet picker, and a
portfolio total when 2+ wallets are open. Settings tab is now reachable
while the vault is locked so the toggle is always available.
- WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js).
@wizardconnect/{core,wallet} are loaded dynamically via api.import to stay
on the right side of LGPL §4d. Sign requests go through approvalModal and
are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS.
- DGB adapter load is now soft-fail: when Aegis runs from userData/addons the
bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of
taking the whole add-on down.
146 lines
6.1 KiB
JavaScript
146 lines
6.1 KiB
JavaScript
// Imported BCH wallet — single-address, key material lives in Theseus's
|
|
// wallet-imports.enc (design §3.2). This adapter mirrors chain-bch.js's
|
|
// public shape (snapshot, refresh, plan, signAndBroadcast, dispose) but
|
|
// does NOT go through vault.derive + HKDF: derivation is direct from the
|
|
// seed+path or WIF that the user imported.
|
|
//
|
|
// M.1a scope: read-only (balance + history over Electrum). planSend/send
|
|
// throw with a clear message until M.1b lands the sign path.
|
|
|
|
module.exports = function makeImportedBchAdapter({ sha256, ripemd160, cashaddr, electrum, WebSocket, tx }) {
|
|
|
|
// Same electrum scripthash convention chain-bch uses: sha256(script), byte-
|
|
// reversed, hex. P2PKH-only for imports today — that's what every entry in
|
|
// Deviant's keystore is.
|
|
const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
const p2pkhScript = (h160) => Uint8Array.from([0x76, 0xa9, 0x14, ...h160, 0x88, 0xac]);
|
|
const scripthashOf = (script) => toHex(sha256(script).slice().reverse());
|
|
const hash160 = (b) => ripemd160(sha256(b));
|
|
|
|
const IMPORTED_BCH_NETWORKS = {
|
|
mainnet: {
|
|
id: "mainnet", label: "Mainnet", prefix: "bitcoincash",
|
|
explorerTx: "https://blockchair.com/bitcoin-cash/transaction/",
|
|
explorerAddr: "https://blockchair.com/bitcoin-cash/address/",
|
|
defaultServers: [
|
|
"wss://bch.imaginary.cash:50004",
|
|
"wss://cashnode.bch.ninja:50004",
|
|
"wss://electroncash.dk:50004",
|
|
"wss://fulcrum.jettscythe.xyz:50004",
|
|
],
|
|
},
|
|
chipnet: {
|
|
id: "chipnet", label: "Chipnet testnet", prefix: "bchtest",
|
|
explorerTx: "https://chipnet.imaginary.cash/tx/",
|
|
explorerAddr: "https://chipnet.imaginary.cash/address/",
|
|
defaultServers: [
|
|
"wss://chipnet.imaginary.cash:50004",
|
|
"wss://chipnet.bch.ninja:50004",
|
|
],
|
|
faucet: "https://tbch.googol.cash/",
|
|
},
|
|
};
|
|
|
|
// Decode a cashaddr → 20-byte hash160 payload. We stored cashaddr at import
|
|
// time and use it here to compute the scripthash for Electrum without ever
|
|
// asking main for the signer material — that only happens at sign time.
|
|
function h160OfCashaddr(addr) {
|
|
const clean = String(addr || "").replace(/^bitcoincash:|^bchtest:/, "");
|
|
const { type, hash } = cashaddr.decode(addr.includes(":") ? addr : "bitcoincash:" + clean);
|
|
if (type !== 0) throw new Error(`imported wallet must be P2PKH (got type ${type})`);
|
|
return hash;
|
|
}
|
|
|
|
class ImportedBchWallet {
|
|
constructor({ walletId, storage, log = () => {}, onChange = () => {}, network = "mainnet", cashaddr: address, servers } = {}) {
|
|
const net = IMPORTED_BCH_NETWORKS[network];
|
|
if (!net) throw new Error(`chain-bch-imported: unknown network ${network}`);
|
|
if (!address) throw new Error("chain-bch-imported: cashaddr required");
|
|
this.walletId = walletId;
|
|
this.chain = "bch";
|
|
this.network = net.id;
|
|
this._net = net;
|
|
this.log = log;
|
|
this.onChange = onChange;
|
|
this._address = address;
|
|
this._h160 = h160OfCashaddr(address);
|
|
this._script = p2pkhScript(this._h160);
|
|
this._scripthash = scripthashOf(this._script);
|
|
this._scriptHex = toHex(this._script);
|
|
this._servers = Array.isArray(servers) && servers.length ? servers : net.defaultServers.slice();
|
|
this._client = new electrum.Client(this._servers);
|
|
this._client.onServer = () => this._emit();
|
|
this._state = {
|
|
balance: { confirmed: 0, unconfirmed: 0 },
|
|
history: [],
|
|
height: 0,
|
|
scanning: false,
|
|
error: null,
|
|
};
|
|
}
|
|
|
|
setServers(list) {
|
|
this._servers = Array.isArray(list) && list.length ? list : this._net.defaultServers.slice();
|
|
this._client.setServers(this._servers);
|
|
}
|
|
|
|
_emit() { try { this.onChange(); } catch {} }
|
|
|
|
snapshot() {
|
|
return {
|
|
chain: "bch",
|
|
network: this._net.id,
|
|
ticker: "BCH",
|
|
decimals: 8,
|
|
address: this._address,
|
|
addressIndex: 0,
|
|
addressPath: null,
|
|
balance: this._state.balance,
|
|
height: this._state.height,
|
|
history: this._state.history,
|
|
scanning: this._state.scanning,
|
|
error: this._state.error,
|
|
server: this._client.url || null,
|
|
servers: this._servers,
|
|
imported: true,
|
|
explorerTx: this._net.explorerTx,
|
|
explorerAddr: this._net.explorerAddr,
|
|
faucet: this._net.faucet,
|
|
};
|
|
}
|
|
|
|
async refresh(full) {
|
|
this._state.scanning = true; this._emit();
|
|
try {
|
|
// Balance for this single scripthash.
|
|
const bal = await this._client.request("blockchain.scripthash.get_balance", [this._scripthash]);
|
|
this._state.balance = { confirmed: Number(bal?.confirmed || 0), unconfirmed: Number(bal?.unconfirmed || 0) };
|
|
if (full) {
|
|
const hist = await this._client.request("blockchain.scripthash.get_history", [this._scripthash]);
|
|
this._state.history = (hist || []).slice(-50).map((h) => ({
|
|
txid: h.tx_hash, time: 0, delta: 0, confirmations: h.height > 0 ? 1 : 0,
|
|
}));
|
|
}
|
|
this._state.error = null;
|
|
} catch (e) {
|
|
this._state.error = e?.message || String(e);
|
|
} finally {
|
|
this._state.scanning = false;
|
|
this._emit();
|
|
}
|
|
}
|
|
|
|
nextAddress() { return { address: this._address, index: 0 }; }
|
|
current() { return { address: this._address, index: 0, branch: 0, path: null, h160: this._h160, script: this._script, scripthash: this._scripthash, scriptHex: this._scriptHex }; }
|
|
|
|
plan() { throw new Error("Imported wallets are read-only in this build. Spending support ships in the next Aegis update."); }
|
|
signAndBroadcast() { throw new Error("Imported wallets are read-only in this build."); }
|
|
signMessage() { throw new Error("Imported wallets are read-only in this build."); }
|
|
|
|
recovery() { return { accountPath: null, xpub: null, xprv: null, note: "Recovery lives in the source of the import (Deviant keystore or wherever you got the seed/WIF from)." }; }
|
|
|
|
dispose() { try { this._client.disconnect(); } catch {} }
|
|
}
|
|
|
|
return { ImportedBchWallet, IMPORTED_BCH_NETWORKS };
|
|
};
|