// BCH chain adapter — wraps the existing keys.js / wallet.js / tx.js / // electrum.js / cashaddr.js code with the common adapter shape that the // multi-wallet manager talks to. Every BCH wallet is one BIP32 account // derived from its own 32-byte root (from api.vault.derive). // // Deps are handed in so index.js loads @noble/* once and shares them across // every wallet, rather than each adapter dynamic-importing on its own. module.exports = function makeBchAdapter({ HDKey, secp256k1, sha256, ripemd160, cashaddr, keysLib, tx, electrum, WebSocket, }) { const NETWORK = "mainnet"; const PREFIX = "bitcoincash"; const DEFAULT_ACCOUNT_PATH = "m/44'/145'/0'"; const EXPLORER_TX = "https://blockchair.com/bitcoin-cash/transaction/"; const EXPLORER_ADDR = "https://blockchair.com/bitcoin-cash/address/"; // storage is the FULL api.storage. keyPrefix scopes every read/write under // "wallets//…" so multiple BCH wallets don't stomp each other. function scopedStorage(storage, keyPrefix) { const k = (key) => keyPrefix + key; return { get: (key, fallback = null) => storage.get(k(key), fallback), set: (key, value) => storage.set(k(key), value), }; } class BchWallet { constructor(root32, { walletId, storage, log = () => {}, onChange = () => {}, servers, accountPath = DEFAULT_ACCOUNT_PATH, } = {}) { if (!walletId) throw new Error("chain-bch: walletId required"); this.walletId = walletId; this.chain = "bch"; this.network = NETWORK; this.log = log; this.onChange = onChange; this.storage = scopedStorage(storage, `wallets/${walletId}/`); this._servers = Array.isArray(servers) && servers.length ? servers : []; this._accountPath = /^m(\/\d+'?)+$/.test(accountPath) ? accountPath : DEFAULT_ACCOUNT_PATH; this._client = new electrum.Client(this._servers); this._client.onServer = () => this._emit(); this._keys = new keysLib.WalletKeys(root32, this._accountPath, PREFIX); this._root = new Uint8Array(root32); const walletFactory = require("./wallet.js"); this._wallet = walletFactory({ client: this._client, keys: this._keys, tx, cashaddr, sha256, storage: this.storage, log: (...a) => this.log(...a), onChange: () => this._emit(), }); } setServers(list) { this._servers = Array.isArray(list) && list.length ? list : []; this._client.setServers(this._servers); } _emit() { try { this.onChange(); } catch {} } snapshot() { const w = this._wallet.snapshot(); return { chain: "bch", network: NETWORK, ticker: "BCH", decimals: 8, address: w.address, addressIndex: w.addressIndex, addressPath: w.addressPath, balance: w.balance, height: w.height, history: w.history, scanning: w.scanning, error: w.error, server: this._client.url || null, servers: this._servers, accountPath: this._accountPath, xpub: this._keys.xpub, explorerTx: EXPLORER_TX, explorerAddr: EXPLORER_ADDR, faucet: null, }; } async refresh(full) { return this._wallet.refresh(!!full); } nextAddress() { return this._wallet.nextUnusedAddress(); } current() { return this._wallet.current(); } plan(spec) { const targets = Array.isArray(spec.outputs) && spec.outputs.length ? spec.outputs.map((o) => ({ to: o.to, value: o.amount ?? o.value })) : [{ to: spec.to, value: spec.amount ?? spec.value }]; return this._wallet.plan({ targets, feeRate: spec.feeRate, sendMax: !!spec.sendMax }); } async signAndBroadcast(plan) { return this._wallet.signAndBroadcast(plan); } // 65-byte BIP-137 recoverable signature — the format Electron Cash and // most BCH tooling verify against. signMessage(message) { const enc = new TextEncoder(); const varstr = (s) => { const b = enc.encode(s); if (b.length >= 0xfd) throw new Error("too long"); return Uint8Array.from([b.length, ...b]); }; const MAGIC = "Bitcoin Signed Message:\n"; const payload = Uint8Array.from([...varstr(MAGIC), ...varstr(String(message))]); const digest = sha256(sha256(payload)); const entry = this.current(); const sig = this._keys.signRecoverable(entry, digest); return { address: entry.address, signature: Buffer.from(sig).toString("base64") }; } recovery() { return { accountPath: this._accountPath, xpub: this._keys.xpub, xprv: this._keys.xprv }; } dispose() { try { this._wallet.dispose(); } catch {} try { this._keys.wipe(); } catch {} try { this._client.disconnect(); } catch {} if (this._root) this._root.fill(0); } } return { BchWallet, DEFAULT_ACCOUNT_PATH, PREFIX, EXPLORER_TX, EXPLORER_ADDR }; };