2020-10-19 11:57:02 +07:00
import config from '../../config' ;
2020-07-03 23:45:19 +07:00
import * as fs from 'fs' ;
2022-05-20 12:55:28 +09:00
import axios , { AxiosResponse } from 'axios' ;
import * as http from 'http' ;
import * as https from 'https' ;
import { SocksProxyAgent } from 'socks-proxy-agent' ;
2020-09-10 14:46:23 +07:00
import { BisqBlocks , BisqBlock , BisqTransaction , BisqStats , BisqTrade } from './interfaces' ;
import { Common } from '../common' ;
2020-12-28 04:47:22 +07:00
import { BlockExtended } from '../../mempool.interfaces' ;
2022-05-20 23:38:16 +09:00
import backendInfo from '../backend-info' ;
2020-10-13 15:27:52 +07:00
import logger from '../../logger' ;
2020-10-17 18:13:09 +07:00
2020-07-03 23:45:19 +07:00
class Bisq {
2021-04-25 02:38:46 +04:00
private static BLOCKS_JSON_FILE_PATH = config . BISQ . DATA_PATH + '/json/all/blocks.json' ;
2020-07-15 13:10:13 +07:00
private latestBlockHeight = 0 ;
2020-07-03 23:45:19 +07:00
private blocks : BisqBlock [ ] = [ ] ;
2021-06-06 16:07:26 -04:00
private allBlocks : BisqBlock [ ] = [ ] ;
2020-07-03 23:45:19 +07:00
private transactions : BisqTransaction [ ] = [ ] ;
2020-07-13 21:46:25 +07:00
private transactionIndex : { [ txId : string ] : BisqTransaction } = { } ;
private blockIndex : { [ hash : string ] : BisqBlock } = { } ;
private addressIndex : { [ address : string ] : BisqTransaction [ ] } = { } ;
2020-07-14 14:38:52 +07:00
private stats : BisqStats = {
minted : 0 ,
burnt : 0 ,
addresses : 0 ,
unspent_txos : 0 ,
spent_txos : 0 ,
} ;
2020-07-14 21:26:02 +07:00
private price : number = 0 ;
private priceUpdateCallbackFunction : ( ( price : number ) = > void ) | undefined ;
2020-07-21 10:23:21 +07:00
private topDirectoryWatcher : fs.FSWatcher | undefined ;
2020-07-18 18:17:24 +07:00
private subdirectoryWatcher : fs.FSWatcher | undefined ;
2020-07-03 23:45:19 +07:00
constructor ( ) { }
startBisqService ( ) : void {
2022-05-13 11:54:52 +02:00
try {
this . checkForBisqDataFolder ( ) ;
} catch ( e ) {
logger . info ( 'Retrying to start bisq service in 3 minutes' ) ;
setTimeout ( this . startBisqService . bind ( this ) , 180000 ) ;
return ;
}
2020-07-03 23:45:19 +07:00
this . loadBisqDumpFile ( ) ;
2020-07-14 21:26:02 +07:00
setInterval ( this . updatePrice . bind ( this ) , 1000 * 60 * 60 ) ;
this . updatePrice ( ) ;
2020-07-21 10:23:21 +07:00
this . startTopDirectoryWatcher ( ) ;
this . startSubDirectoryWatcher ( ) ;
2020-07-03 23:45:19 +07:00
}
2020-12-28 04:47:22 +07:00
handleNewBitcoinBlock ( block : BlockExtended ) : void {
2021-01-30 23:02:20 +09:00
if ( block . height - 10 > this . latestBlockHeight && this . latestBlockHeight !== 0 ) {
2020-10-13 17:48:43 +07:00
logger . warn ( ` Bitcoin block height (# ${ block . height } ) has diverged from the latest Bisq block height (# ${ this . latestBlockHeight } ). Restarting watchers... ` ) ;
2020-09-27 17:21:18 +07:00
this . startTopDirectoryWatcher ( ) ;
this . startSubDirectoryWatcher ( ) ;
}
}
2020-07-03 23:45:19 +07:00
getTransaction ( txId : string ) : BisqTransaction | undefined {
2020-07-13 21:46:25 +07:00
return this . transactionIndex [ txId ] ;
2020-07-03 23:45:19 +07:00
}
2020-07-24 18:41:15 +07:00
getTransactions ( start : number , length : number , types : string [ ] ) : [ BisqTransaction [ ] , number ] {
let transactions = this . transactions ;
if ( types . length ) {
transactions = transactions . filter ( ( tx ) = > types . indexOf ( tx . txType ) > - 1 ) ;
}
return [ transactions . slice ( start , length + start ) , transactions . length ] ;
2020-07-03 23:45:19 +07:00
}
getBlock ( hash : string ) : BisqBlock | undefined {
2020-07-13 21:46:25 +07:00
return this . blockIndex [ hash ] ;
}
getAddress ( hash : string ) : BisqTransaction [ ] {
return this . addressIndex [ hash ] ;
2020-07-03 23:45:19 +07:00
}
2020-07-13 15:16:12 +07:00
getBlocks ( start : number , length : number ) : [ BisqBlock [ ] , number ] {
return [ this . blocks . slice ( start , length + start ) , this . blocks . length ] ;
}
2020-07-14 14:38:52 +07:00
getStats ( ) : BisqStats {
return this . stats ;
}
2020-07-14 21:26:02 +07:00
setPriceCallbackFunction ( fn : ( price : number ) = > void ) {
this . priceUpdateCallbackFunction = fn ;
}
2020-07-15 13:10:13 +07:00
getLatestBlockHeight ( ) : number {
return this . latestBlockHeight ;
}
2020-07-21 10:23:21 +07:00
private checkForBisqDataFolder() {
2020-10-19 11:57:02 +07:00
if ( ! fs . existsSync ( Bisq . BLOCKS_JSON_FILE_PATH ) ) {
logger . warn ( Bisq . BLOCKS_JSON_FILE_PATH + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server. ` ) ;
2022-05-13 11:54:52 +02:00
throw new Error ( ` Cannot load BISQ ${ Bisq . BLOCKS_JSON_FILE_PATH } file ` ) ;
2020-07-21 10:23:21 +07:00
}
}
private startTopDirectoryWatcher() {
if ( this . topDirectoryWatcher ) {
this . topDirectoryWatcher . close ( ) ;
}
2020-07-18 18:17:24 +07:00
let fsWait : NodeJS.Timeout | null = null ;
2021-04-25 02:38:46 +04:00
this . topDirectoryWatcher = fs . watch ( config . BISQ . DATA_PATH + '/json' , ( ) = > {
2020-07-18 18:17:24 +07:00
if ( fsWait ) {
clearTimeout ( fsWait ) ;
}
2020-07-21 10:23:21 +07:00
if ( this . subdirectoryWatcher ) {
this . subdirectoryWatcher . close ( ) ;
}
2020-07-18 18:17:24 +07:00
fsWait = setTimeout ( ( ) = > {
2020-10-13 16:43:09 +07:00
logger . debug ( ` Bisq restart detected. Resetting both watchers in 3 minutes. ` ) ;
2020-07-20 16:36:08 +07:00
setTimeout ( ( ) = > {
2020-07-21 10:23:21 +07:00
this . startTopDirectoryWatcher ( ) ;
this . startSubDirectoryWatcher ( ) ;
2020-07-20 16:36:08 +07:00
this . loadBisqDumpFile ( ) ;
} , 180000 ) ;
2020-07-18 18:17:24 +07:00
} , 15000 ) ;
} ) ;
}
2020-07-21 10:23:21 +07:00
private startSubDirectoryWatcher() {
2020-07-18 18:17:24 +07:00
if ( this . subdirectoryWatcher ) {
this . subdirectoryWatcher . close ( ) ;
}
2020-10-19 11:57:02 +07:00
if ( ! fs . existsSync ( Bisq . BLOCKS_JSON_FILE_PATH ) ) {
logger . warn ( Bisq . BLOCKS_JSON_FILE_PATH + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes. ` ) ;
2020-07-21 10:23:21 +07:00
setTimeout ( ( ) = > this . startSubDirectoryWatcher ( ) , 180000 ) ;
return ;
}
2020-07-18 18:17:24 +07:00
let fsWait : NodeJS.Timeout | null = null ;
2021-04-25 02:38:46 +04:00
this . subdirectoryWatcher = fs . watch ( config . BISQ . DATA_PATH + '/json/all' , ( ) = > {
2020-07-18 18:17:24 +07:00
if ( fsWait ) {
clearTimeout ( fsWait ) ;
}
fsWait = setTimeout ( ( ) = > {
2020-10-13 16:43:09 +07:00
logger . debug ( ` Change detected in the Bisq data folder. ` ) ;
2020-07-18 18:17:24 +07:00
this . loadBisqDumpFile ( ) ;
} , 2000 ) ;
} ) ;
}
2022-05-20 12:55:28 +09:00
private async updatePrice() {
type axiosOptions = {
2022-05-20 23:38:16 +09:00
headers : {
'User-Agent' : string
} ;
timeout : number ;
2022-05-20 12:55:28 +09:00
httpAgent? : http.Agent ;
httpsAgent? : https.Agent ;
}
const setDelay = ( secs : number = 1 ) : Promise < void > = > new Promise ( resolve = > setTimeout ( ( ) = > resolve ( ) , secs * 1000 ) ) ;
2022-05-21 15:38:22 +09:00
const BISQ_URL = ( config . SOCKS5PROXY . ENABLED === true ) && ( config . SOCKS5PROXY . USE_ONION === true ) ? config.EXTERNAL_DATA_SERVER.BISQ_ONION : config.EXTERNAL_DATA_SERVER.BISQ_URL ;
2022-05-20 12:55:28 +09:00
const isHTTP = ( new URL ( BISQ_URL ) . protocol . split ( ':' ) [ 0 ] === 'http' ) ? true : false ;
2022-05-20 23:38:16 +09:00
const axiosOptions : axiosOptions = {
headers : {
'User-Agent' : ( config . MEMPOOL . USER_AGENT === 'mempool' ) ? ` mempool/v ${ backendInfo . getBackendInfo ( ) . version } ` : ` ${ config . MEMPOOL . USER_AGENT } `
} ,
timeout : config.SOCKS5PROXY.ENABLED ? 30000 : 10000
} ;
2022-05-20 12:55:28 +09:00
let retry = 0 ;
while ( retry < config . MEMPOOL . EXTERNAL_MAX_RETRY ) {
try {
2022-05-23 20:05:32 +09:00
if ( config . SOCKS5PROXY . ENABLED ) {
const socksOptions : any = {
agentOptions : {
keepAlive : true ,
} ,
hostname : config.SOCKS5PROXY.HOST ,
port : config.SOCKS5PROXY.PORT
} ;
if ( config . SOCKS5PROXY . USERNAME && config . SOCKS5PROXY . PASSWORD ) {
socksOptions . username = config . SOCKS5PROXY . USERNAME ;
socksOptions . password = config . SOCKS5PROXY . PASSWORD ;
2022-05-24 15:46:15 +09:00
} else {
// Retry with different tor circuits https://stackoverflow.com/a/64960234
socksOptions . username = ` circuit ${ retry } ` ;
2022-05-23 20:05:32 +09:00
}
// Handle proxy agent for onion addresses
if ( isHTTP ) {
axiosOptions . httpAgent = new SocksProxyAgent ( socksOptions ) ;
} else {
axiosOptions . httpsAgent = new SocksProxyAgent ( socksOptions ) ;
}
}
2022-05-20 12:55:28 +09:00
const data : AxiosResponse = await axios . get ( ` ${ BISQ_URL } /trades/?market=bsq_btc ` , axiosOptions ) ;
if ( data . statusText === 'error' || ! data . data ) {
throw new Error ( ` Could not fetch data from Bisq market, Error: ${ data . status } ` ) ;
}
2020-11-15 14:22:47 +07:00
const prices : number [ ] = [ ] ;
2022-05-20 12:55:28 +09:00
data . data . forEach ( ( trade ) = > {
2020-11-15 14:22:47 +07:00
prices . push ( parseFloat ( trade . price ) * 100000000 ) ;
} ) ;
prices . sort ( ( a , b ) = > a - b ) ;
this . price = Common . median ( prices ) ;
if ( this . priceUpdateCallbackFunction ) {
this . priceUpdateCallbackFunction ( this . price ) ;
}
2022-05-20 12:55:28 +09:00
logger . debug ( 'Successfully updated Bisq market price' ) ;
break ;
} catch ( e ) {
logger . err ( 'Error updating Bisq market price: ' + ( e instanceof Error ? e.message : e ) ) ;
await setDelay ( config . MEMPOOL . EXTERNAL_RETRY_INTERVAL ) ;
retry ++ ;
}
}
2020-07-14 21:26:02 +07:00
}
2020-07-13 15:16:12 +07:00
private async loadBisqDumpFile ( ) : Promise < void > {
2022-04-27 13:06:16 +09:00
this . allBlocks = [ ] ;
2020-07-13 15:16:12 +07:00
try {
2022-04-25 11:49:04 +09:00
await this . loadData ( ) ;
2020-07-13 15:16:12 +07:00
this . buildIndex ( ) ;
2020-07-14 14:38:52 +07:00
this . calculateStats ( ) ;
2020-07-13 15:16:12 +07:00
} catch ( e ) {
2022-05-13 11:54:52 +02:00
logger . info ( 'Cannot load bisq dump file because: ' + ( e instanceof Error ? e.message : e ) ) ;
2020-07-13 15:16:12 +07:00
}
}
2020-07-03 23:45:19 +07:00
private buildIndex() {
2020-07-13 15:16:12 +07:00
const start = new Date ( ) . getTime ( ) ;
this . transactions = [ ] ;
2020-07-13 21:46:25 +07:00
this . transactionIndex = { } ;
this . addressIndex = { } ;
2021-06-06 16:07:26 -04:00
this . allBlocks . forEach ( ( block ) = > {
2020-07-13 21:46:25 +07:00
/* Build block index */
if ( ! this . blockIndex [ block . hash ] ) {
this . blockIndex [ block . hash ] = block ;
2020-07-11 00:17:13 +07:00
}
2020-07-13 21:46:25 +07:00
/* Build transactions index */
2020-07-03 23:45:19 +07:00
block . txs . forEach ( ( tx ) = > {
2020-07-13 15:16:12 +07:00
this . transactions . push ( tx ) ;
2020-07-13 21:46:25 +07:00
this . transactionIndex [ tx . id ] = tx ;
2020-07-03 23:45:19 +07:00
} ) ;
} ) ;
2020-07-13 21:46:25 +07:00
/* Build address index */
this . transactions . forEach ( ( tx ) = > {
tx . inputs . forEach ( ( input ) = > {
if ( ! this . addressIndex [ input . address ] ) {
this . addressIndex [ input . address ] = [ ] ;
}
if ( this . addressIndex [ input . address ] . indexOf ( tx ) === - 1 ) {
this . addressIndex [ input . address ] . push ( tx ) ;
}
} ) ;
tx . outputs . forEach ( ( output ) = > {
if ( ! this . addressIndex [ output . address ] ) {
this . addressIndex [ output . address ] = [ ] ;
}
if ( this . addressIndex [ output . address ] . indexOf ( tx ) === - 1 ) {
this . addressIndex [ output . address ] . push ( tx ) ;
}
} ) ;
} ) ;
2020-07-13 15:16:12 +07:00
const time = new Date ( ) . getTime ( ) - start ;
2020-10-13 16:43:09 +07:00
logger . debug ( 'Bisq data index rebuilt in ' + time + ' ms' ) ;
2020-07-03 23:45:19 +07:00
}
2020-07-14 14:38:52 +07:00
private calculateStats() {
let minted = 0 ;
let burned = 0 ;
let unspent = 0 ;
let spent = 0 ;
this . transactions . forEach ( ( tx ) = > {
tx . outputs . forEach ( ( output ) = > {
if ( output . opReturn ) {
return ;
}
if ( output . txOutputType === 'GENESIS_OUTPUT' || output . txOutputType === 'ISSUANCE_CANDIDATE_OUTPUT' && output . isVerified ) {
minted += output . bsqAmount ;
}
if ( output . isUnspent ) {
unspent ++ ;
} else {
spent ++ ;
}
} ) ;
burned += tx [ 'burntFee' ] ;
} ) ;
this . stats = {
addresses : Object.keys ( this . addressIndex ) . length ,
2021-01-23 19:26:05 +07:00
minted : minted / 100 ,
burnt : burned / 100 ,
2020-07-14 14:38:52 +07:00
spent_txos : spent ,
unspent_txos : unspent ,
} ;
}
2022-04-25 11:49:04 +09:00
private async loadData ( ) : Promise < any > {
if ( ! fs . existsSync ( Bisq . BLOCKS_JSON_FILE_PATH ) ) {
throw new Error ( Bisq . BLOCKS_JSON_FILE_PATH + ` doesn't exist ` ) ;
2020-07-03 23:45:19 +07:00
}
2022-04-25 11:49:04 +09:00
const readline = require ( 'readline' ) ;
const events = require ( 'events' ) ;
const rl = readline . createInterface ( {
input : fs.createReadStream ( Bisq . BLOCKS_JSON_FILE_PATH ) ,
crlfDelay : Infinity
} ) ;
let blockBuffer = '' ;
let readingBlock = false ;
let lineCount = 1 ;
const start = new Date ( ) . getTime ( ) ;
logger . debug ( 'Processing Bisq data dump...' ) ;
rl . on ( 'line' , ( line ) = > {
if ( lineCount === 2 ) {
line = line . replace ( ' "chainHeight": ' , '' ) ;
this . latestBlockHeight = parseInt ( line , 10 ) ;
2020-07-21 10:23:21 +07:00
}
2022-04-25 11:49:04 +09:00
if ( line === ' {' ) {
readingBlock = true ;
} else if ( line === ' },' ) {
blockBuffer += '}' ;
try {
const block : BisqBlock = JSON . parse ( blockBuffer ) ;
this . allBlocks . push ( block ) ;
readingBlock = false ;
blockBuffer = '' ;
} catch ( e ) {
logger . debug ( blockBuffer ) ;
throw Error ( ` Unable to parse Bisq data dump at line ${ lineCount } ` + ( e instanceof Error ? e.message : e ) ) ;
2020-07-03 23:45:19 +07:00
}
2022-04-25 11:49:04 +09:00
}
if ( readingBlock === true ) {
blockBuffer += line ;
}
++ lineCount ;
2020-07-03 23:45:19 +07:00
} ) ;
2022-04-25 11:49:04 +09:00
await events . once ( rl , 'close' ) ;
this . allBlocks . reverse ( ) ;
this . blocks = this . allBlocks . filter ( ( block ) = > block . txs . length > 0 ) ;
const time = new Date ( ) . getTime ( ) - start ;
logger . debug ( 'Bisq dump processed in ' + time + ' ms' ) ;
2020-07-03 23:45:19 +07:00
}
}
export default new Bisq ( ) ;