2020-12-28 04:47:22 +07:00
import config from '../../config' ;
2022-06-27 21:28:21 -07:00
import Client from '@mempool/electrum-client' ;
2020-12-28 04:47:22 +07:00
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory' ;
import { IEsploraApi } from './esplora-api.interface' ;
import { IElectrumApi } from './electrum-api.interface' ;
import BitcoinApi from './bitcoin-api' ;
2020-12-29 00:41:02 +07:00
import logger from '../../logger' ;
2022-07-06 12:34:54 -07:00
import crypto from "crypto-js" ;
2021-01-08 21:44:36 +07:00
import loadingIndicators from '../loading-indicators' ;
2021-01-10 19:58:55 +07:00
import memoryCache from '../memory-cache' ;
2020-12-29 00:41:02 +07:00
2020-12-28 04:47:22 +07:00
class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
private electrumClient : any ;
2021-09-15 01:47:24 +04:00
constructor ( bitcoinClient : any ) {
super ( bitcoinClient ) ;
2020-12-28 04:47:22 +07:00
2020-12-29 00:41:02 +07:00
const electrumConfig = { client : 'mempool-v2' , version : '1.4' } ;
2023-03-23 15:18:48 +09:00
const electrumPersistencePolicy = { retryPeriod : 1000 , maxRetry : Number.MAX_SAFE_INTEGER , callback : null } ;
2020-12-29 00:41:02 +07:00
const electrumCallbacks = {
2021-01-05 03:06:57 +07:00
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 } ` ) ; } ,
2020-12-29 00:41:02 +07:00
onError : ( err ) = > { logger . err ( ` Electrum error: ${ JSON . stringify ( err ) } ` ) ; } ,
onLog : ( str ) = > { logger . debug ( str ) ; } ,
} ;
2022-06-27 21:28:21 -07:00
this . electrumClient = new Client (
2021-01-05 03:06:57 +07:00
config . ELECTRUM . PORT ,
config . ELECTRUM . HOST ,
2021-01-06 22:49:28 +07:00
config . ELECTRUM . TLS_ENABLED ? 'tls' : 'tcp' ,
2020-12-29 00:41:02 +07:00
null ,
electrumCallbacks
2020-12-28 04:47:22 +07:00
) ;
2020-12-29 00:41:02 +07:00
this . electrumClient . initElectrum ( electrumConfig , electrumPersistencePolicy )
2022-06-27 21:28:21 -07:00
. then ( ( ) = > { } )
2020-12-29 00:41:02 +07:00
. catch ( ( err ) = > {
2021-01-05 03:06:57 +07:00
logger . err ( ` Error connecting to Electrum Server at ${ config . ELECTRUM . HOST } : ${ config . ELECTRUM . PORT } ` ) ;
2020-12-29 00:41:02 +07:00
} ) ;
2020-12-28 04:47:22 +07:00
}
async $getAddress ( address : string ) : Promise < IEsploraApi.Address > {
2021-09-15 01:47:24 +04:00
const addressInfo = await this . bitcoindClient . validateAddress ( address ) ;
2020-12-28 04:47:22 +07:00
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
}
} ) ;
}
2021-01-05 17:30:53 +07:00
try {
const balance = await this . $getScriptHashBalance ( addressInfo . scriptPubKey ) ;
const history = await this . $getScriptHashHistory ( addressInfo . scriptPubKey ) ;
const unconfirmed = history . filter ( ( h ) = > h . fee ) . length ;
2020-12-28 04:47:22 +07:00
2021-01-05 17:30:53 +07:00
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 ,
2021-02-01 04:52:24 +07:00
} ,
'electrum' : true ,
2021-01-05 17:30:53 +07:00
} ;
2021-10-23 11:46:38 +04:00
} catch ( e : any ) {
throw new Error ( typeof e === 'string' ? e : e && e . message || e ) ;
2021-01-05 17:30:53 +07:00
}
2020-12-28 04:47:22 +07:00
}
async $getAddressTransactions ( address : string , lastSeenTxId : string ) : Promise < IEsploraApi.Transaction [ ] > {
2021-09-15 01:47:24 +04:00
const addressInfo = await this . bitcoindClient . validateAddress ( address ) ;
2020-12-28 04:47:22 +07:00
if ( ! addressInfo || ! addressInfo . isvalid ) {
2022-06-27 21:28:21 -07:00
return [ ] ;
2020-12-28 04:47:22 +07:00
}
2021-01-05 17:30:53 +07:00
try {
2021-01-08 21:44:36 +07:00
loadingIndicators . setProgress ( 'address-' + address , 0 ) ;
2021-01-05 17:30:53 +07:00
const transactions : IEsploraApi.Transaction [ ] = [ ] ;
const history = await this . $getScriptHashHistory ( addressInfo . scriptPubKey ) ;
2021-01-10 17:40:05 +07:00
history . sort ( ( a , b ) = > ( b . height || 9999999 ) - ( a . height || 9999999 ) ) ;
2021-01-05 17:30:53 +07:00
let startingIndex = 0 ;
if ( lastSeenTxId ) {
const pos = history . findIndex ( ( historicalTx ) = > historicalTx . tx_hash === lastSeenTxId ) ;
if ( pos ) {
startingIndex = pos + 1 ;
}
2020-12-30 02:27:34 +07:00
}
2021-01-08 21:44:36 +07:00
const endIndex = Math . min ( startingIndex + 10 , history . length ) ;
2020-12-30 02:27:34 +07:00
2021-01-08 21:44:36 +07:00
for ( let i = startingIndex ; i < endIndex ; i ++ ) {
2021-01-05 17:30:53 +07:00
const tx = await this . $getRawTransaction ( history [ i ] . tx_hash , false , true ) ;
2021-01-08 21:44:36 +07:00
transactions . push ( tx ) ;
loadingIndicators . setProgress ( 'address-' + address , ( i + 1 ) / endIndex * 100 ) ;
2020-12-28 04:47:22 +07:00
}
2020-12-30 02:27:34 +07:00
2021-01-05 17:30:53 +07:00
return transactions ;
2021-10-23 11:46:38 +04:00
} catch ( e : any ) {
2021-01-08 21:44:36 +07:00
loadingIndicators . setProgress ( 'address-' + address , 100 ) ;
2021-10-23 11:46:38 +04:00
throw new Error ( typeof e === 'string' ? e : e && e . message || e ) ;
2023-07-22 17:51:45 +09:00
}
}
async $getScriptHash ( scripthash : string ) : Promise < IEsploraApi.ScriptHash > {
try {
const balance = await this . electrumClient . blockchainScripthash_getBalance ( scripthash ) ;
let history = memoryCache . get < IElectrumApi.ScriptHashHistory [ ] > ( '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 < IEsploraApi.Transaction [ ] > {
try {
loadingIndicators . setProgress ( 'address-' + scripthash , 0 ) ;
const transactions : IEsploraApi.Transaction [ ] = [ ] ;
let history = memoryCache . get < IElectrumApi.ScriptHashHistory [ ] > ( '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 ) ;
2021-01-05 17:30:53 +07:00
}
2020-12-28 04:47:22 +07:00
}
private $getScriptHashBalance ( scriptHash : string ) : Promise < IElectrumApi.ScriptHashBalance > {
2020-12-29 00:41:02 +07:00
return this . electrumClient . blockchainScripthash_getBalance ( this . encodeScriptHash ( scriptHash ) ) ;
2020-12-28 04:47:22 +07:00
}
private $getScriptHashHistory ( scriptHash : string ) : Promise < IElectrumApi.ScriptHashHistory [ ] > {
2021-01-10 19:58:55 +07:00
const fromCache = memoryCache . get < IElectrumApi.ScriptHashHistory [ ] > ( '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 ;
} ) ;
2020-12-28 04:47:22 +07:00
}
private encodeScriptHash ( scriptPubKey : string ) : string {
2022-07-06 12:34:54 -07:00
const addrScripthash = crypto . enc . Hex . stringify ( crypto . SHA256 ( crypto . enc . Hex . parse ( scriptPubKey ) ) ) ;
return addrScripthash ! . match ( /.{2}/g ) ! . reverse ( ) . join ( '' ) ;
2020-12-28 04:47:22 +07:00
}
}
export default BitcoindElectrsApi ;