// 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); } schedulePoll(ms) { clearTimeout(this._pollTimer); this._pollTimer = setTimeout(() => { this.refresh(false).catch(() => {}); this.schedulePoll(ms); }, ms); } _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() { clearTimeout(this._pollTimer); try { this._client.disconnect(); } catch {} } } return { ImportedBchWallet, IMPORTED_BCH_NETWORKS }; };