diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index 53c731e1f..266be5f1e 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -2,7 +2,7 @@ import { IEsploraApi } from './esplora-api.interface'; export interface AbstractBitcoinApi { $getRawMempool(): Promise; - $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise; + $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, blockHash?: string): Promise; $getBlockHeightTip(): Promise; $getTxIdsForBlock(hash: string): Promise; $getBlockHash(height: number): Promise; diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 8d66f82ef..27b021af0 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -14,14 +14,14 @@ class BitcoinApi implements AbstractBitcoinApi { this.bitcoindClient = bitcoinClient; } - $getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise { + $getRawTransaction(txId: string, skipConversion = false, addPrevout = false, blockHash?: string): Promise { // If the transaction is in the mempool we already converted and fetched the fee. Only prevouts are missing const txInMempool = mempool.getMempool()[txId]; if (txInMempool && addPrevout) { return this.$addPrevouts(txInMempool); } - return this.bitcoindClient.getRawTransaction(txId, true) + return this.bitcoindClient.getRawTransaction(txId, true, blockHash) .then((transaction: IBitcoinApi.Transaction) => { if (skipConversion) { transaction.vout.forEach((vout) => { diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 8f066b5a4..bff73dd54 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -12,6 +12,7 @@ import poolsRepository from '../repositories/PoolsRepository'; import blocksRepository from '../repositories/BlocksRepository'; import loadingIndicators from './loading-indicators'; import BitcoinApi from './bitcoin/bitcoin-api'; +import { prepareBlock } from '../utils/blocks-utils'; class Blocks { private blocks: BlockExtended[] = []; @@ -108,9 +109,7 @@ class Blocks { const blockExtended: BlockExtended = Object.assign({ extras: {} }, block); blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0); blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]); - - const coinbaseRaw: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(transactions[0].txid, true); - blockExtended.extras.coinbaseRaw = coinbaseRaw.hex; + blockExtended.extras.coinbaseRaw = blockExtended.extras.coinbaseTx.vin[0].scriptsig; if (block.height === 0) { blockExtended.extras.medianFee = 0; // 50th percentiles @@ -119,7 +118,9 @@ class Blocks { blockExtended.extras.avgFee = 0; blockExtended.extras.avgFeeRate = 0; } else { - const stats = await bitcoinClient.getBlockStats(block.id); + const stats = await bitcoinClient.getBlockStats(block.id, [ + 'feerate_percentiles', 'minfeerate', 'maxfeerate', 'totalfee', 'avgfee', 'avgfeerate' + ]); blockExtended.extras.medianFee = stats.feerate_percentiles[2]; // 50th percentiles blockExtended.extras.feeRange = [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(); blockExtended.extras.totalFees = stats.totalfee; @@ -336,7 +337,7 @@ class Blocks { public async $indexBlock(height: number): Promise { const dbBlock = await blocksRepository.$getBlockByHeight(height); if (dbBlock != null) { - return this.prepareBlock(dbBlock); + return prepareBlock(dbBlock); } const blockHash = await bitcoinApi.$getBlockHash(height); @@ -346,17 +347,17 @@ class Blocks { await blocksRepository.$saveBlockInDatabase(blockExtended); - return this.prepareBlock(blockExtended); + return prepareBlock(blockExtended); } - public async $getBlocksExtras(fromHeight: number, limit: number = 15): Promise { + public async $getBlocksExtras(fromHeight?: number, limit: number = 15): Promise { // Note - This API is breaking if indexing is not available. For now it is okay because we only // use it for the mining pages, and mining pages should not be available if indexing is turned off. // I'll need to fix it before we refactor the block(s) related pages try { loadingIndicators.setProgress('blocks', 0); - let currentHeight = fromHeight ? fromHeight : this.getCurrentBlockHeight(); + let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight(); const returnBlocks: BlockExtended[] = []; if (currentHeight < 0) { @@ -378,7 +379,7 @@ class Blocks { if (!block && Common.indexingEnabled()) { block = await this.$indexBlock(currentHeight); } else if (!block) { - block = this.prepareBlock(await bitcoinApi.$getBlock(nextHash)); + block = prepareBlock(await bitcoinApi.$getBlock(nextHash)); } returnBlocks.push(block); nextHash = block.previousblockhash; @@ -393,33 +394,6 @@ class Blocks { } } - private prepareBlock(block: any): BlockExtended { - return { - id: block.id ?? block.hash, // hash for indexed block - timestamp: block.timestamp ?? block.blockTimestamp, // blockTimestamp for indexed block - height: block.height, - version: block.version, - bits: block.bits, - nonce: block.nonce, - difficulty: block.difficulty, - merkle_root: block.merkle_root, - tx_count: block.tx_count, - size: block.size, - weight: block.weight, - previousblockhash: block.previousblockhash, - extras: { - medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee, - feeRange: block.feeRange ?? block.fee_range ?? block?.extras?.feeSpan, - reward: block.reward ?? block?.extras?.reward, - totalFees: block.totalFees ?? block?.fees ?? block?.extras.totalFees, - pool: block?.extras?.pool ?? (block?.pool_id ? { - id: block.pool_id, - name: block.pool_name, - } : undefined), - } - }; - } - public getLastDifficultyAdjustmentTime(): number { return this.lastDifficultyAdjustmentTime; } diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index c9c1da8e8..ffa9041e3 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -6,7 +6,7 @@ import logger from '../logger'; const sleep = (ms: number) => new Promise(res => setTimeout(res, ms)); class DatabaseMigration { - private static currentVersion = 14; + private static currentVersion = 15; private queryTimeout = 120000; private statisticsAddedIndexed = false; diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 0af5e2252..40c705bdb 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -2,6 +2,7 @@ import { BlockExtended, PoolTag } from '../mempool.interfaces'; import { DB } from '../database'; import logger from '../logger'; import { Common } from '../api/common'; +import { prepareBlock } from '../utils/blocks-utils'; class BlocksRepository { /** @@ -56,7 +57,7 @@ class BlocksRepository { logger.debug(`$saveBlockInDatabase() - Block ${block.height} has already been indexed, ignoring`); } else { connection.release(); - logger.err('$saveBlockInDatabase() error' + (e instanceof Error ? e.message : e)); + logger.err('$saveBlockInDatabase() error: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -153,7 +154,6 @@ class BlocksRepository { query += ` blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; } - // logger.debug(query); const connection = await DB.getConnection(); try { const [rows] = await connection.query(query, params); @@ -169,10 +169,10 @@ class BlocksRepository { /** * Get blocks count between two dates - * @param poolId + * @param poolId * @param from - The oldest timestamp * @param to - The newest timestamp - * @returns + * @returns */ public async $blockCountBetweenTimestamp(poolId: number | null, from: number, to: number): Promise { const params: any[] = []; @@ -193,7 +193,6 @@ class BlocksRepository { } query += ` blockTimestamp BETWEEN FROM_UNIXTIME('${from}') AND FROM_UNIXTIME('${to}')`; - // logger.debug(query); const connection = await DB.getConnection(); try { const [rows] = await connection.query(query, params); @@ -216,7 +215,6 @@ class BlocksRepository { ORDER BY height LIMIT 1;`; - // logger.debug(query); const connection = await DB.getConnection(); try { const [rows]: any[] = await connection.query(query); @@ -237,14 +235,15 @@ class BlocksRepository { /** * Get blocks mined by a specific mining pool */ - public async $getBlocksByPool(poolId: number, startHeight: number | null = null): Promise { + public async $getBlocksByPool(poolId: number, startHeight: number | undefined = undefined): Promise { const params: any[] = []; - let query = `SELECT height, hash as id, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, reward + let query = ` SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, + previous_block_hash as previousblockhash FROM blocks WHERE pool_id = ?`; params.push(poolId); - if (startHeight) { + if (startHeight !== undefined) { query += ` AND height < ?`; params.push(startHeight); } @@ -252,17 +251,17 @@ class BlocksRepository { query += ` ORDER BY height DESC LIMIT 10`; - // logger.debug(query); const connection = await DB.getConnection(); try { const [rows] = await connection.query(query, params); connection.release(); - for (const block of rows) { - delete block['blockTimestamp']; + const blocks: BlockExtended[] = []; + for (let block of rows) { + blocks.push(prepareBlock(block)); } - return rows; + return blocks; } catch (e) { connection.release(); logger.err('$getBlocksByPool() error' + (e instanceof Error ? e.message : e)); @@ -314,7 +313,7 @@ class BlocksRepository { let query = ` SELECT * - FROM + FROM ( SELECT UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, height, @@ -322,7 +321,7 @@ class BlocksRepository { IF(@prevStatus := YT.difficulty, @rn := 1, @rn := 1) ) AS rn FROM blocks YT - CROSS JOIN + CROSS JOIN ( SELECT @prevStatus := -1, @rn := 1 ) AS var diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 7f3a98296..2f4cdff3a 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -553,7 +553,7 @@ class Routes { try { const poolBlocks = await BlocksRepository.$getBlocksByPool( parseInt(req.params.poolId, 10), - parseInt(req.params.height, 10) ?? null, + req.params.height === undefined ? undefined : parseInt(req.params.height, 10), ); res.header('Pragma', 'public'); res.header('Cache-control', 'public'); @@ -659,7 +659,8 @@ class Routes { public async getBlocksExtras(req: Request, res: Response) { try { - res.json(await blocks.$getBlocksExtras(parseInt(req.params.height, 10), 15)); + const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); + res.json(await blocks.$getBlocksExtras(height, 15)); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } diff --git a/backend/src/utils/blocks-utils.ts b/backend/src/utils/blocks-utils.ts new file mode 100644 index 000000000..107099ba3 --- /dev/null +++ b/backend/src/utils/blocks-utils.ts @@ -0,0 +1,29 @@ +import { BlockExtended } from "../mempool.interfaces"; + +export function prepareBlock(block: any): BlockExtended { + return { + id: block.id ?? block.hash, // hash for indexed block + timestamp: block.timestamp ?? block.blockTimestamp, // blockTimestamp for indexed block + height: block.height, + version: block.version, + bits: block.bits, + nonce: block.nonce, + difficulty: block.difficulty, + merkle_root: block.merkle_root, + tx_count: block.tx_count, + size: block.size, + weight: block.weight, + previousblockhash: block.previousblockhash, + extras: { + coinbaseRaw: block.coinbase_raw ?? block.extras.coinbaseRaw, + medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee, + feeRange: block.feeRange ?? block.fee_range ?? block?.extras?.feeSpan, + reward: block.reward ?? block?.extras?.reward, + totalFees: block.totalFees ?? block?.fees ?? block?.extras.totalFees, + pool: block?.extras?.pool ?? (block?.pool_id ? { + id: block.pool_id, + name: block.pool_name, + } : undefined), + } + }; +} diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.html b/frontend/src/app/components/blocks-list/blocks-list.component.html index f50a5fff2..e71ad7817 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.html +++ b/frontend/src/app/components/blocks-list/blocks-list.component.html @@ -24,11 +24,14 @@ }} - - - {{ block.extras.pool.name }} - +
+ + + {{ block.extras.pool.name }} + + {{ block.extras.coinbaseRaw | hex2ascii }} +
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.scss b/frontend/src/app/components/blocks-list/blocks-list.component.scss index 9414348c1..354c403af 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.scss +++ b/frontend/src/app/components/blocks-list/blocks-list.component.scss @@ -34,8 +34,9 @@ td { } .pool.widget { width: 40%; + padding-left: 30px; @media (max-width: 576px) { - padding-left: 30px; + padding-left: 40px; width: 60%; } } @@ -122,3 +123,34 @@ td { display: none; } } + +/* Tooltip text */ +.tooltip-custom { + position: relative; +} + +.tooltip-custom .tooltiptext { + visibility: hidden; + color: #fff; + text-align: center; + padding: 5px 0; + border-radius: 6px; + position: absolute; + z-index: 1; + top: -40px; + left: 0; +} + +/* Show the tooltip text when you mouse over the tooltip container */ +.tooltip-custom:hover .tooltiptext { + visibility: visible; +} + +.scriptmessage { + overflow: hidden; + display: inline-block; + text-overflow: ellipsis; + vertical-align: middle; + max-width: 50vw; + text-align: left; +} \ No newline at end of file diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.ts b/frontend/src/app/components/blocks-list/blocks-list.component.ts index 72727b734..ade191f7a 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.ts +++ b/frontend/src/app/components/blocks-list/blocks-list.component.ts @@ -47,7 +47,7 @@ export class BlocksList implements OnInit { .pipe( tap(blocks => { if (this.blocksCount === undefined) { - this.blocksCount = blocks[0].height; + this.blocksCount = blocks[0].height + 1; } this.isLoading = false; }), @@ -77,7 +77,7 @@ export class BlocksList implements OnInit { this.lastPage = this.page; return blocks[0]; } - this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height); + this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1; // @ts-ignore: Need to add an extra field for the template blocks[1][0].extras.pool.logo = `./resources/mining-pools/` + blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; @@ -89,7 +89,7 @@ export class BlocksList implements OnInit { } pageChange(page: number) { - this.fromHeightSubject.next(this.blocksCount - (page - 1) * 15); + this.fromHeightSubject.next((this.blocksCount - 1) - (page - 1) * 15); } trackByBlock(index: number, block: BlockExtended) { diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html index 9788437a9..d5cc61e91 100644 --- a/frontend/src/app/components/graphs/graphs.component.html +++ b/frontend/src/app/components/graphs/graphs.component.html @@ -1,4 +1,4 @@ -