2020-07-10 14:13:07 +07:00
const config = require ( '../../mempool-config.json' ) ;
2020-07-03 23:45:19 +07:00
import * as fs from 'fs' ;
2020-07-14 21:26:02 +07:00
import * as request from 'request' ;
import { BisqBlocks , BisqBlock , BisqTransaction , BisqStats , BisqTrade } from '../interfaces' ;
import { Common } from './common' ;
2020-07-03 23:45:19 +07:00
class Bisq {
2020-07-21 10:23:21 +07:00
private static BLOCKS_JSON_FILE_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-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
}
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
}
getTransactions ( start : number , length : number ) : [ BisqTransaction [ ] , number ] {
return [ this . transactions . slice ( start , length + start ) , this . transactions . length ] ;
}
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() {
if ( ! fs . existsSync ( config . BSQ_BLOCKS_DATA_PATH + Bisq . BLOCKS_JSON_FILE_PATH ) ) {
console . log ( config . BSQ_BLOCKS_DATA_PATH + Bisq . BLOCKS_JSON_FILE_PATH + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server. ` ) ;
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-07-21 10:23:21 +07:00
this . topDirectoryWatcher = fs . watch ( config . BSQ_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-07-21 10:23:21 +07:00
console . log ( ` 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-07-21 10:23:21 +07:00
if ( ! fs . existsSync ( config . BSQ_BLOCKS_DATA_PATH + Bisq . BLOCKS_JSON_FILE_PATH ) ) {
console . log ( config . BSQ_BLOCKS_DATA_PATH + Bisq . BLOCKS_JSON_FILE_PATH + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes. ` ) ;
setTimeout ( ( ) = > this . startSubDirectoryWatcher ( ) , 180000 ) ;
return ;
}
2020-07-18 18:17:24 +07:00
let fsWait : NodeJS.Timeout | null = null ;
this . subdirectoryWatcher = fs . watch ( config . BSQ_BLOCKS_DATA_PATH + '/all' , ( ) = > {
if ( fsWait ) {
clearTimeout ( fsWait ) ;
}
fsWait = setTimeout ( ( ) = > {
console . log ( ` Change detected in the Bisq data folder. ` ) ;
this . loadBisqDumpFile ( ) ;
} , 2000 ) ;
} ) ;
}
2020-07-14 21:26:02 +07:00
private updatePrice() {
request ( 'https://markets.bisq.network/api/trades/?market=bsq_btc' , { json : true } , ( err , res , trades : BisqTrade [ ] ) = > {
if ( err ) { return console . log ( err ) ; }
const prices : number [ ] = [ ] ;
trades . 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 ) ;
}
} ) ;
}
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 ) {
console . log ( 'loadBisqDumpFile() error.' , e . message ) ;
}
}
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 ;
console . log ( '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 ,
minted : minted ,
burnt : burned ,
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-07-21 10:23:21 +07:00
console . log ( 'Processing Bisq data dump...' ) ;
2020-07-03 23:45:19 +07:00
const data : BisqBlocks = JSON . parse ( 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-07-21 10:23:21 +07:00
console . log ( 'Bisq dump processed in ' + time + ' ms' ) ;
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-07-21 10:23:21 +07:00
if ( ! fs . existsSync ( config . BSQ_BLOCKS_DATA_PATH + Bisq . BLOCKS_JSON_FILE_PATH ) ) {
return reject ( Bisq . BLOCKS_JSON_FILE_PATH + ` doesn't exist ` ) ;
}
fs . readFile ( config . BSQ_BLOCKS_DATA_PATH + 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 ( ) ;