import config from '../config'; import bitcoinApi from './bitcoin/bitcoin-api-factory'; import logger from '../logger'; import memPool from './mempool'; import { BlockExtended, TransactionExtended } from '../mempool.interfaces'; import { Common } from './common'; import diskCache from './disk-cache'; import transactionUtils from './transaction-utils'; class Blocks { private static INITIAL_BLOCK_AMOUNT = 8; private blocks: BlockExtended[] = []; private currentBlockHeight = 0; private lastDifficultyAdjustmentTime = 0; private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; constructor() { } public getBlocks(): BlockExtended[] { return this.blocks; } public setBlocks(blocks: BlockExtended[]) { this.blocks = blocks; } public setNewBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void) { this.newBlockCallbacks.push(fn); } public async $updateBlocks() { const blockHeightTip = await bitcoinApi.$getBlockHeightTip(); if (this.blocks.length === 0) { this.currentBlockHeight = blockHeightTip - Blocks.INITIAL_BLOCK_AMOUNT; } else { this.currentBlockHeight = this.blocks[this.blocks.length - 1].height; } if (blockHeightTip - this.currentBlockHeight > Blocks.INITIAL_BLOCK_AMOUNT * 2) { logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${Blocks.INITIAL_BLOCK_AMOUNT} recent blocks`); this.currentBlockHeight = blockHeightTip - Blocks.INITIAL_BLOCK_AMOUNT; } if (!this.lastDifficultyAdjustmentTime) { const heightDiff = blockHeightTip % 2016; const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff); const block = await bitcoinApi.$getBlock(blockHash); this.lastDifficultyAdjustmentTime = block.timestamp; } while (this.currentBlockHeight < blockHeightTip) { if (this.currentBlockHeight === 0) { this.currentBlockHeight = blockHeightTip; } else { this.currentBlockHeight++; logger.debug(`New block found (#${this.currentBlockHeight})!`); } const transactions: TransactionExtended[] = []; const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight); const block = await bitcoinApi.$getBlock(blockHash); const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash); const mempool = memPool.getMempool(); let transactionsFound = 0; for (let i = 0; i < txIds.length; i++) { if (mempool[txIds[i]]) { transactions.push(mempool[txIds[i]]); transactionsFound++; } else if (config.MEMPOOL.BACKEND === 'esplora' || memPool.isInSync() || i === 0) { logger.debug(`Fetching block tx ${i} of ${txIds.length}`); try { const tx = await transactionUtils.$getTransactionExtended(txIds[i]); transactions.push(tx); } catch (e) { logger.debug('Error fetching block tx: ' + e.message || e); if (i === 0) { throw new Error('Failed to fetch Coinbase transaction: ' + txIds[i]); } } } } logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${txIds.length - transactionsFound} not found.`); const blockExtended: BlockExtended = Object.assign({}, block); blockExtended.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0); blockExtended.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]); transactions.sort((a, b) => b.feePerVsize - a.feePerVsize); blockExtended.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0; blockExtended.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions.slice(0, transactions.length - 1), 8) : [0, 0]; if (block.height % 2016 === 0) { this.lastDifficultyAdjustmentTime = block.timestamp; } this.blocks.push(blockExtended); if (this.blocks.length > Blocks.INITIAL_BLOCK_AMOUNT * 4) { this.blocks = this.blocks.slice(-Blocks.INITIAL_BLOCK_AMOUNT * 4); } if (this.newBlockCallbacks.length) { this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions)); } if (memPool.isInSync()) { diskCache.$saveCacheToDisk(); } } } public getLastDifficultyAdjustmentTime(): number { return this.lastDifficultyAdjustmentTime; } public getCurrentBlockHeight(): number { return this.currentBlockHeight; } } export default new Blocks();