// 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. const BCH_NETWORKS = { mainnet: { id: "mainnet", label: "Mainnet", prefix: "bitcoincash", defaultAccountPath: "m/44'/145'/0'", 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", ], faucet: null, }, chipnet: { id: "chipnet", label: "Chipnet testnet", prefix: "bchtest", // BIP44 testnet coin type is 1 across every chain; the account is still 0. defaultAccountPath: "m/44'/1'/0'", 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/", }, }; module.exports = function makeBchAdapter({ HDKey, secp256k1, sha256, ripemd160, cashaddr, keysLib, tx, electrum, WebSocket, }) { // 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, network = "mainnet", accountPath, } = {}) { if (!walletId) throw new Error("chain-bch: walletId required"); const net = BCH_NETWORKS[network]; if (!net) throw new Error(`chain-bch: unknown network ${network}`); this.walletId = walletId; this.chain = "bch"; this.network = net.id; this._net = net; this.log = log; this.onChange = onChange; this.storage = scopedStorage(storage, `wallets/${walletId}/`); // Servers: caller-provided override → user-set custom list (handled by // index.js already, this is a fallback path) → the network's built-in // defaults so a wallet always has somewhere to connect. this._servers = Array.isArray(servers) && servers.length ? servers : net.defaultServers.slice(); const wantPath = accountPath || net.defaultAccountPath; this._accountPath = /^m(\/\d+'?)+$/.test(wantPath) ? wantPath : net.defaultAccountPath; this._client = new electrum.Client(this._servers); this._client.onServer = () => this._emit(); this._keys = new keysLib.WalletKeys(root32, this._accountPath, net.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._net.defaultServers.slice(); this._client.setServers(this._servers); } _emit() { try { this.onChange(); } catch {} } snapshot() { const w = this._wallet.snapshot(); return { chain: "bch", network: this._net.id, 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: this._net.explorerTx, explorerAddr: this._net.explorerAddr, faucet: this._net.faucet, }; } 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, BCH_NETWORKS }; };