// Generic single-address read-only imported adapter for UTXO chains // (BTC + DGB). Uses electrum for balance + history, bitcoinjs to convert // the address back into a locking script for the scripthash. // // M.1b will add spending; for now these wallets show as read-only, // matching chain-bch-imported.js's stance. module.exports = function makeUtxoImportedAdapter({ sha256, bitcoinjs, dgbCore, electrum, WebSocket }) { const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join(""); const scripthashOf = (script) => toHex(sha256(script).slice().reverse()); const NETWORKS = { btc: { ticker: "BTC", decimals: 8, networks: { mainnet: { id: "mainnet", label: "Mainnet", bitcoinjsNet: bitcoinjs.networks.bitcoin, servers: ["wss://electrum.blockstream.info:50004", "wss://fulcrum.sethforprivacy.com:50002"], explorerAddr: "https://mempool.space/address/", explorerTx: "https://mempool.space/tx/", }, testnet3: { id: "testnet3", label: "Testnet3", bitcoinjsNet: bitcoinjs.networks.testnet, servers: ["wss://electrumx.tomasi.name:50004"], explorerAddr: "https://mempool.space/testnet/address/", explorerTx: "https://mempool.space/testnet/tx/", testnet: true, faucet: "https://coinfaucet.eu/en/btc-testnet/", }, signet: { id: "signet", label: "Signet", bitcoinjsNet: bitcoinjs.networks.testnet, servers: ["wss://signet-electrumx.wakiyamap.dev:50004"], explorerAddr: "https://mempool.space/signet/address/", explorerTx: "https://mempool.space/signet/tx/", testnet: true, faucet: "https://signet.bc-2.jp/", }, }, }, dgb: { ticker: "DGB", decimals: 8, networks: { mainnet: { id: "mainnet", label: "Mainnet", bitcoinjsNet: dgbCore ? dgbCore.digibyte : null, servers: ["wss://electrum1.cipig.net:20063", "wss://electrum2.cipig.net:20063"], explorerAddr: "https://chainz.cryptoid.info/dgb/address.dws?", explorerTx: "https://chainz.cryptoid.info/dgb/tx.dws?", }, }, }, }; class UtxoImportedWallet { constructor({ chain, network, address, log = () => {}, onChange = () => {} } = {}) { const cfg = NETWORKS[chain]; if (!cfg) throw new Error(`chain-utxo-imported: unknown chain ${chain}`); const net = cfg.networks[network]; if (!net) throw new Error(`chain-utxo-imported: ${chain} has no network ${network}`); if (!address) throw new Error("address required"); if (!net.bitcoinjsNet) throw new Error(`chain-utxo-imported: ${chain}/${network} missing bitcoinjs network params`); this.chain = chain; this.network = network; this._cfg = cfg; this._net = net; this.log = log; this.onChange = onChange; this._address = address; try { this._script = bitcoinjs.address.toOutputScript(address, net.bitcoinjsNet); } catch (e) { throw new Error(`invalid ${chain} address for ${network}: ${e?.message || e}`); } this._scripthash = scripthashOf(this._script); this._client = new electrum.Client(net.servers.slice()); this._client.onServer = () => this._emit(); this._state = { balance: { confirmed: 0, unconfirmed: 0 }, history: [], height: 0, scanning: false, error: null, }; } setServers(list) { this._client.setServers(list && list.length ? list : this._net.servers.slice()); } schedulePoll(ms) { clearTimeout(this._pollTimer); this._pollTimer = setTimeout(() => { this.refresh(false).catch(() => {}); this.schedulePoll(ms); }, ms); } _emit() { try { this.onChange(); } catch {} } snapshot() { return { chain: this.chain, network: this.network, ticker: this._cfg.ticker, decimals: this._cfg.decimals, address: this._address, addressIndex: 0, addressPath: null, balance: this._state.balance, history: this._state.history, height: this._state.height, scanning: this._state.scanning, error: this._state.error, server: this._client.url || null, imported: true, explorerAddr: this._net.explorerAddr, explorerTx: this._net.explorerTx, faucet: this._net.faucet || null, }; } async refresh(full) { this._state.scanning = true; this._emit(); try { 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 }; } plan() { throw new Error(`Imported ${this.chain.toUpperCase()} wallets are read-only in this build.`); } signAndBroadcast() { throw new Error("read-only"); } signMessage() { throw new Error("read-only"); } recovery() { return { accountPath: null, xpub: null, xprv: null, note: "Recovery lives in the source of the import." }; } dispose() { clearTimeout(this._pollTimer); try { this._client.disconnect(); } catch {} } } return { UtxoImportedWallet, NETWORKS }; };