import config from '../../config'; import Client from '@mempool/electrum-client'; import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; import { IEsploraApi } from './esplora-api.interface'; import { IElectrumApi } from './electrum-api.interface'; import BitcoinApi from './bitcoin-api'; import logger from '../../logger'; import crypto from "crypto-js"; import loadingIndicators from '../loading-indicators'; import memoryCache from '../memory-cache'; class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi { private electrumClient: any; constructor(bitcoinClient: any) { super(bitcoinClient); const electrumConfig = { client: 'mempool-v2', version: '1.4' }; const electrumPersistencePolicy = { retryPeriod: 1000, maxRetry: Number.MAX_SAFE_INTEGER, callback: null }; const electrumCallbacks = { onConnect: (client, versionInfo) => { logger.info(`Connected to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT} (${JSON.stringify(versionInfo)})`); }, onClose: (client) => { logger.info(`Disconnected from Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT}`); }, onError: (err) => { logger.err(`Electrum error: ${JSON.stringify(err)}`); }, onLog: (str) => { logger.debug(str); }, }; this.electrumClient = new Client( config.ELECTRUM.PORT, config.ELECTRUM.HOST, config.ELECTRUM.TLS_ENABLED ? 'tls' : 'tcp', null, electrumCallbacks ); this.electrumClient.initElectrum(electrumConfig, electrumPersistencePolicy) .then(() => { }) .catch((err) => { logger.err(`Error connecting to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT}`); }); } async $getAddress(address: string): Promise { const addressInfo = await this.bitcoindClient.validateAddress(address); if (!addressInfo || !addressInfo.isvalid) { return ({ 'address': address, 'chain_stats': { 'funded_txo_count': 0, 'funded_txo_sum': 0, 'spent_txo_count': 0, 'spent_txo_sum': 0, 'tx_count': 0 }, 'mempool_stats': { 'funded_txo_count': 0, 'funded_txo_sum': 0, 'spent_txo_count': 0, 'spent_txo_sum': 0, 'tx_count': 0 } }); } try { const balance = await this.$getScriptHashBalance(addressInfo.scriptPubKey); const history = await this.$getScriptHashHistory(addressInfo.scriptPubKey); const unconfirmed = history.filter((h) => h.fee).length; return { 'address': addressInfo.address, 'chain_stats': { 'funded_txo_count': 0, 'funded_txo_sum': balance.confirmed ? balance.confirmed : 0, 'spent_txo_count': 0, 'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0, 'tx_count': history.length - unconfirmed, }, 'mempool_stats': { 'funded_txo_count': 0, 'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0, 'spent_txo_count': 0, 'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0, 'tx_count': unconfirmed, }, 'electrum': true, }; } catch (e: any) { throw new Error(typeof e === 'string' ? e : e && e.message || e); } } async $getAddressTransactions(address: string, lastSeenTxId: string): Promise { const addressInfo = await this.bitcoindClient.validateAddress(address); if (!addressInfo || !addressInfo.isvalid) { return []; } try { loadingIndicators.setProgress('address-' + address, 0); const transactions: IEsploraApi.Transaction[] = []; const history = await this.$getScriptHashHistory(addressInfo.scriptPubKey); history.sort((a, b) => (b.height || 9999999) - (a.height || 9999999)); let startingIndex = 0; if (lastSeenTxId) { const pos = history.findIndex((historicalTx) => historicalTx.tx_hash === lastSeenTxId); if (pos) { startingIndex = pos + 1; } } const endIndex = Math.min(startingIndex + 10, history.length); for (let i = startingIndex; i < endIndex; i++) { const tx = await this.$getRawTransaction(history[i].tx_hash, false, true); transactions.push(tx); loadingIndicators.setProgress('address-' + address, (i + 1) / endIndex * 100); } return transactions; } catch (e: any) { loadingIndicators.setProgress('address-' + address, 100); throw new Error(typeof e === 'string' ? e : e && e.message || e); } } async $getScriptHash(scripthash: string): Promise { try { const balance = await this.electrumClient.blockchainScripthash_getBalance(scripthash); let history = memoryCache.get('Scripthash_getHistory', scripthash); if (!history) { history = await this.electrumClient.blockchainScripthash_getHistory(scripthash); memoryCache.set('Scripthash_getHistory', scripthash, history, 2); } const unconfirmed = history ? history.filter((h) => h.fee).length : 0; return { 'scripthash': scripthash, 'chain_stats': { 'funded_txo_count': 0, 'funded_txo_sum': balance.confirmed ? balance.confirmed : 0, 'spent_txo_count': 0, 'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0, 'tx_count': (history?.length || 0) - unconfirmed, }, 'mempool_stats': { 'funded_txo_count': 0, 'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0, 'spent_txo_count': 0, 'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0, 'tx_count': unconfirmed, }, 'electrum': true, }; } catch (e: any) { throw new Error(typeof e === 'string' ? e : e && e.message || e); } } async $getScriptHashTransactions(scripthash: string, lastSeenTxId?: string): Promise { try { loadingIndicators.setProgress('address-' + scripthash, 0); const transactions: IEsploraApi.Transaction[] = []; let history = memoryCache.get('Scripthash_getHistory', scripthash); if (!history) { history = await this.electrumClient.blockchainScripthash_getHistory(scripthash); memoryCache.set('Scripthash_getHistory', scripthash, history, 2); } if (!history) { throw new Error('failed to get scripthash history'); } history.sort((a, b) => (b.height || 9999999) - (a.height || 9999999)); let startingIndex = 0; if (lastSeenTxId) { const pos = history.findIndex((historicalTx) => historicalTx.tx_hash === lastSeenTxId); if (pos) { startingIndex = pos + 1; } } const endIndex = Math.min(startingIndex + 10, history.length); for (let i = startingIndex; i < endIndex; i++) { const tx = await this.$getRawTransaction(history[i].tx_hash, false, true); transactions.push(tx); loadingIndicators.setProgress('address-' + scripthash, (i + 1) / endIndex * 100); } return transactions; } catch (e: any) { loadingIndicators.setProgress('address-' + scripthash, 100); throw new Error(typeof e === 'string' ? e : e && e.message || e); } } private $getScriptHashBalance(scriptHash: string): Promise { return this.electrumClient.blockchainScripthash_getBalance(this.encodeScriptHash(scriptHash)); } private $getScriptHashHistory(scriptHash: string): Promise { const fromCache = memoryCache.get('Scripthash_getHistory', scriptHash); if (fromCache) { return Promise.resolve(fromCache); } return this.electrumClient.blockchainScripthash_getHistory(this.encodeScriptHash(scriptHash)) .then((history) => { memoryCache.set('Scripthash_getHistory', scriptHash, history, 2); return history; }); } private encodeScriptHash(scriptPubKey: string): string { const addrScripthash = crypto.enc.Hex.stringify(crypto.SHA256(crypto.enc.Hex.parse(scriptPubKey))); return addrScripthash!.match(/.{2}/g)!.reverse().join(''); } } export default BitcoindElectrsApi;