276 lines
8.5 KiB
TypeScript
Raw Normal View History

import config from '../../config';
import * as fs from 'fs';
import axios from 'axios';
2020-09-10 14:46:23 +07:00
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
import { Common } from '../common';
import { BlockExtended } from '../../mempool.interfaces';
import { StaticPool } from 'node-worker-threads-pool';
import logger from '../../logger';
class Bisq {
private static BLOCKS_JSON_FILE_PATH = config.BISQ_BLOCKS.DATA_PATH + '/all/blocks.json';
private latestBlockHeight = 0;
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;
private topDirectoryWatcher: fs.FSWatcher | undefined;
private subdirectoryWatcher: fs.FSWatcher | undefined;
private jsonParsePool = new StaticPool({
size: 4,
task: (blob: string) => JSON.parse(blob),
});
constructor() {}
startBisqService(): void {
this.checkForBisqDataFolder();
this.loadBisqDumpFile();
2020-07-14 21:26:02 +07:00
setInterval(this.updatePrice.bind(this), 1000 * 60 * 60);
this.updatePrice();
this.startTopDirectoryWatcher();
this.startSubDirectoryWatcher();
}
handleNewBitcoinBlock(block: BlockExtended): void {
if (block.height - 2 > this.latestBlockHeight && this.latestBlockHeight !== 0) {
logger.warn(`Bitcoin block height (#${block.height}) has diverged from the latest Bisq block height (#${this.latestBlockHeight}). Restarting watchers...`);
this.startTopDirectoryWatcher();
this.startSubDirectoryWatcher();
}
}
getTransaction(txId: string): BisqTransaction | undefined {
2020-07-13 21:46:25 +07:00
return this.transactionIndex[txId];
}
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];
}
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];
}
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;
}
getLatestBlockHeight(): number {
return this.latestBlockHeight;
}
private checkForBisqDataFolder() {
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.`);
return process.exit(1);
}
}
private startTopDirectoryWatcher() {
if (this.topDirectoryWatcher) {
this.topDirectoryWatcher.close();
}
let fsWait: NodeJS.Timeout | null = null;
this.topDirectoryWatcher = fs.watch(config.BISQ_BLOCKS.DATA_PATH, () => {
if (fsWait) {
clearTimeout(fsWait);
}
if (this.subdirectoryWatcher) {
this.subdirectoryWatcher.close();
}
fsWait = setTimeout(() => {
logger.debug(`Bisq restart detected. Resetting both watchers in 3 minutes.`);
setTimeout(() => {
this.startTopDirectoryWatcher();
this.startSubDirectoryWatcher();
this.loadBisqDumpFile();
}, 180000);
}, 15000);
});
}
private startSubDirectoryWatcher() {
if (this.subdirectoryWatcher) {
this.subdirectoryWatcher.close();
}
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.`);
setTimeout(() => this.startSubDirectoryWatcher(), 180000);
return;
}
let fsWait: NodeJS.Timeout | null = null;
this.subdirectoryWatcher = fs.watch(config.BISQ_BLOCKS.DATA_PATH + '/all', () => {
if (fsWait) {
clearTimeout(fsWait);
}
fsWait = setTimeout(() => {
logger.debug(`Change detected in the Bisq data folder.`);
this.loadBisqDumpFile();
}, 2000);
});
}
2020-07-14 21:26:02 +07:00
private updatePrice() {
axios.get<BisqTrade[]>('https://bisq.markets/api/trades/?market=bsq_btc', { timeout: 10000 })
.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
});
}
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();
} catch (e) {
2020-10-28 11:00:48 +07:00
logger.err('loadBisqDumpFile() error.' + e.message || e);
}
}
private buildIndex() {
const start = new Date().getTime();
this.transactions = [];
2020-07-13 21:46:25 +07:00
this.transactionIndex = {};
this.addressIndex = {};
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-13 21:46:25 +07:00
/* Build transactions index */
block.txs.forEach((tx) => {
this.transactions.push(tx);
2020-07-13 21:46:25 +07:00
this.transactionIndex[tx.id] = tx;
});
});
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);
}
});
});
const time = new Date().getTime() - start;
logger.debug('Bisq data index rebuilt in ' + time + ' ms');
}
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 / 100,
burnt: burned / 100,
2020-07-14 14:38:52 +07:00
spent_txos: spent,
unspent_txos: unspent,
};
}
private async loadBisqBlocksDump(cacheData: string): Promise<void> {
const start = new Date().getTime();
2020-07-10 14:13:07 +07:00
if (cacheData && cacheData.length !== 0) {
logger.debug('Processing Bisq data dump...');
const data: BisqBlocks = await this.jsonParsePool.exec(cacheData);
if (data.blocks && data.blocks.length !== this.blocks.length) {
this.blocks = data.blocks.filter((block) => block.txs.length > 0);
this.blocks.reverse();
this.latestBlockHeight = data.chainHeight;
const time = new Date().getTime() - start;
logger.debug('Bisq dump processed in ' + time + ' ms (worker thread)');
} else {
throw new Error(`Bisq dump didn't contain any blocks`);
}
}
}
private loadData(): Promise<string> {
return new Promise((resolve, reject) => {
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
return reject(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist`);
}
fs.readFile(Bisq.BLOCKS_JSON_FILE_PATH, 'utf8', (err, data) => {
if (err) {
reject(err);
}
resolve(data);
});
});
}
}
export default new Bisq();