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' ;
2020-11-15 14:22:47 +07:00
import axios from 'axios' ;
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' ;
2020-10-17 18:13:09 +07:00
import { StaticPool } from 'node-worker-threads-pool' ;
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 {
2020-10-19 11:57:02 +07:00
private static BLOCKS_JSON_FILE_PATH = config . BISQ_BLOCKS . DATA_PATH + '/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 [ ] = [ ] ;
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-10-17 18:13:09 +07:00
private jsonParsePool = new StaticPool ( {
size : 4 ,
task : ( blob : string ) = > JSON . parse ( blob ) ,
} ) ;
2020-07-03 23:45:19 +07:00
constructor ( ) { }
startBisqService ( ) : void {
2020-07-21 10:23:21 +07:00
this . checkForBisqDataFolder ( ) ;
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 {
2020-09-27 17:21:18 +07:00
if ( block . height - 2 > 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. ` ) ;
2020-07-21 10:23:21 +07:00
return process . exit ( 1 ) ;
}
}
private startTopDirectoryWatcher() {
if ( this . topDirectoryWatcher ) {
this . topDirectoryWatcher . close ( ) ;
}
2020-07-18 18:17:24 +07:00
let fsWait : NodeJS.Timeout | null = null ;
2020-10-19 11:57:02 +07:00
this . topDirectoryWatcher = fs . watch ( config . BISQ_BLOCKS . DATA_PATH , ( ) = > {
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 ;
2020-10-19 11:57:02 +07:00
this . subdirectoryWatcher = fs . watch ( config . BISQ_BLOCKS . DATA_PATH + '/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 ) ;
} ) ;
}
2020-07-14 21:26:02 +07:00
private updatePrice() {
2021-01-30 16:25:22 +07:00
axios . get < BisqTrade [ ] > ( 'https://bisq.markets/api/trades/?market=bsq_btc' , { timeout : 10000 } )
2020-11-15 14:22:47 +07:00
. then ( ( response ) = > {
const prices : number [ ] = [ ] ;
response . data . forEach ( ( trade ) = > {
prices . push ( parseFloat ( trade . price ) * 100000000 ) ;
} ) ;
prices . sort ( ( a , b ) = > a - b ) ;
this . price = Common . median ( prices ) ;
if ( this . priceUpdateCallbackFunction ) {
this . priceUpdateCallbackFunction ( this . price ) ;
}
} ) . catch ( ( err ) = > {
logger . err ( 'Error updating Bisq market price: ' + err ) ;
2020-07-14 21:26:02 +07:00
} ) ;
}
2020-07-13 15:16:12 +07:00
private async loadBisqDumpFile ( ) : Promise < void > {
try {
const data = await this . loadData ( ) ;
await this . loadBisqBlocksDump ( data ) ;
this . buildIndex ( ) ;
2020-07-14 14:38:52 +07:00
this . calculateStats ( ) ;
2020-07-13 15:16:12 +07:00
} catch ( e ) {
2020-10-28 11:00:48 +07:00
logger . err ( 'loadBisqDumpFile() 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 = { } ;
2020-07-03 23:45:19 +07:00
this . blocks . 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 ,
} ;
}
2020-07-11 00:17:13 +07:00
private async loadBisqBlocksDump ( cacheData : string ) : Promise < void > {
2020-07-03 23:45:19 +07:00
const start = new Date ( ) . getTime ( ) ;
2020-07-10 14:13:07 +07:00
if ( cacheData && cacheData . length !== 0 ) {
2020-10-13 16:43:09 +07:00
logger . debug ( 'Processing Bisq data dump...' ) ;
2020-10-17 18:13:09 +07:00
const data : BisqBlocks = await this . jsonParsePool . exec ( cacheData ) ;
2020-07-11 00:17:13 +07:00
if ( data . blocks && data . blocks . length !== this . blocks . length ) {
2020-07-13 23:22:24 +07:00
this . blocks = data . blocks . filter ( ( block ) = > block . txs . length > 0 ) ;
2020-07-13 15:16:12 +07:00
this . blocks . reverse ( ) ;
2020-07-15 13:10:13 +07:00
this . latestBlockHeight = data . chainHeight ;
2020-07-13 15:16:12 +07:00
const time = new Date ( ) . getTime ( ) - start ;
2020-10-19 11:57:02 +07:00
logger . debug ( 'Bisq dump processed in ' + time + ' ms (worker thread)' ) ;
2020-07-03 23:45:19 +07:00
} else {
throw new Error ( ` Bisq dump didn't contain any blocks ` ) ;
}
}
}
private loadData ( ) : Promise < string > {
return new Promise ( ( resolve , reject ) = > {
2020-10-19 11:57:02 +07:00
if ( ! fs . existsSync ( Bisq . BLOCKS_JSON_FILE_PATH ) ) {
2020-07-21 10:23:21 +07:00
return reject ( Bisq . BLOCKS_JSON_FILE_PATH + ` doesn't exist ` ) ;
}
2020-10-19 11:57:02 +07:00
fs . readFile ( Bisq . BLOCKS_JSON_FILE_PATH , 'utf8' , ( err , data ) = > {
2020-07-03 23:45:19 +07:00
if ( err ) {
reject ( err ) ;
}
resolve ( data ) ;
} ) ;
} ) ;
}
}
export default new Bisq ( ) ;