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 {
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-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 {
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. ` ) ;
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 ;
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 ) ;
} ) ;
}
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 {
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 ) {
2021-08-31 15:09:33 +03:00
logger . info ( 'loadBisqDumpFile() error.' + ( 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 ( ) ;