From fac49d0b9850e2938efd1aacb364de7cb52e4095 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 8 Feb 2022 15:47:43 +0900 Subject: [PATCH 01/45] Added /api/v1/blocksExtras endpoint --- backend/src/api/blocks.ts | 160 +++++++++++++++---- backend/src/api/database-migration.ts | 6 +- backend/src/index.ts | 4 + backend/src/mempool.interfaces.ts | 4 +- backend/src/repositories/BlocksRepository.ts | 42 +++-- backend/src/routes.ts | 8 + 6 files changed, 179 insertions(+), 45 deletions(-) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 198f2a204..c113f4efa 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client'; import { IEsploraApi } from './bitcoin/esplora-api.interface'; import poolsRepository from '../repositories/PoolsRepository'; import blocksRepository from '../repositories/BlocksRepository'; +import loadingIndicators from './loading-indicators'; class Blocks { private blocks: BlockExtended[] = []; @@ -94,13 +95,10 @@ class Blocks { * @param transactions * @returns BlockExtended */ - private getBlockExtended(block: IEsploraApi.Block, transactions: TransactionExtended[]): BlockExtended { - const blockExtended: BlockExtended = Object.assign({}, block); - - blockExtended.extras = { - reward: transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0), - coinbaseTx: transactionUtils.stripCoinbaseTransaction(transactions[0]), - }; + private async $getBlockExtended(block: IEsploraApi.Block, transactions: TransactionExtended[]): Promise { + 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 transactionsTmp = [...transactions]; transactionsTmp.shift(); @@ -111,6 +109,24 @@ class Blocks { blockExtended.extras.feeRange = transactionsTmp.length > 0 ? Common.getFeesInRange(transactionsTmp, 8) : [0, 0]; + const indexingAvailable = + ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && + config.DATABASE.ENABLED === true; + if (indexingAvailable) { + let pool: PoolTag; + if (blockExtended.extras?.coinbaseTx !== undefined) { + pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx); + } else { + pool = await poolsRepository.$getUnknownPool(); + } + blockExtended.extras.pool = pool; + + if (transactions.length > 0) { + const coinbase: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(transactions[0].txid, true); + blockExtended.extras.coinbaseHex = coinbase.hex; + } + } + return blockExtended; } @@ -153,15 +169,16 @@ class Blocks { */ public async $generateBlockDatabase() { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || // Bitcoin only - config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === 0 || // Indexing must be enabled + config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === 0 || // Indexing of older blocks must be enabled !memPool.isInSync() || // We sync the mempool first - this.blockIndexingStarted === true // Indexing must not already be in progress + this.blockIndexingStarted === true || // Indexing must not already be in progress + config.DATABASE.ENABLED === false ) { return; } const blockchainInfo = await bitcoinClient.getBlockchainInfo(); - if (blockchainInfo.blocks !== blockchainInfo.headers) { + if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync return; } @@ -202,17 +219,8 @@ class Blocks { const blockHash = await bitcoinApi.$getBlockHash(blockHeight); const block = await bitcoinApi.$getBlock(blockHash); const transactions = await this.$getTransactionsExtended(blockHash, block.height, true); - const blockExtended = this.getBlockExtended(block, transactions); - - let miner: PoolTag; - if (blockExtended?.extras?.coinbaseTx) { - miner = await this.$findBlockMiner(blockExtended.extras.coinbaseTx); - } else { - miner = await poolsRepository.$getUnknownPool(); - } - - const coinbase: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(transactions[0].txid, true); - await blocksRepository.$saveBlockInDatabase(blockExtended, blockHash, coinbase.hex, miner); + const blockExtended = await this.$getBlockExtended(block, transactions); + await blocksRepository.$saveBlockInDatabase(blockExtended); } catch (e) { logger.err(`Something went wrong while indexing blocks.` + e); } @@ -271,17 +279,13 @@ class Blocks { const block = await bitcoinApi.$getBlock(blockHash); const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash); const transactions = await this.$getTransactionsExtended(blockHash, block.height, false); - const blockExtended: BlockExtended = this.getBlockExtended(block, transactions); - const coinbase: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(transactions[0].txid, true); + const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions); - if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === true) { - let miner: PoolTag; - if (blockExtended?.extras?.coinbaseTx) { - miner = await this.$findBlockMiner(blockExtended.extras.coinbaseTx); - } else { - miner = await poolsRepository.$getUnknownPool(); - } - await blocksRepository.$saveBlockInDatabase(blockExtended, blockHash, coinbase.hex, miner); + const indexingAvailable = + ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && + config.DATABASE.ENABLED === true; + if (indexingAvailable) { + await blocksRepository.$saveBlockInDatabase(blockExtended); } if (block.height % 2016 === 0) { @@ -304,6 +308,100 @@ class Blocks { } } + /** + * Index a block if it's missing from the database. Returns the block after indexing + */ + public async $indexBlock(height: number): Promise { + const dbBlock = await blocksRepository.$getBlockByHeight(height); + if (dbBlock != null) { + return this.prepareBlock(dbBlock); + } + + const blockHash = await bitcoinApi.$getBlockHash(height); + const block = await bitcoinApi.$getBlock(blockHash); + const transactions = await this.$getTransactionsExtended(blockHash, block.height, true); + const blockExtended = await this.$getBlockExtended(block, transactions); + + await blocksRepository.$saveBlockInDatabase(blockExtended); + + return blockExtended; + } + + public async $getBlocksExtras(fromHeight: number): Promise { + const indexingAvailable = + ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && + config.DATABASE.ENABLED === true; + + try { + loadingIndicators.setProgress('blocks', 0); + + let currentHeight = fromHeight ? fromHeight : this.getCurrentBlockHeight(); + const returnBlocks: BlockExtended[] = []; + + if (currentHeight < 0) { + return returnBlocks; + } + + // Check if block height exist in local cache to skip the hash lookup + const blockByHeight = this.getBlocks().find((b) => b.height === currentHeight); + let startFromHash: string | null = null; + if (blockByHeight) { + startFromHash = blockByHeight.id; + } else { + startFromHash = await bitcoinApi.$getBlockHash(currentHeight); + } + + let nextHash = startFromHash; + for (let i = 0; i < 10 && currentHeight >= 0; i++) { + let block = this.getBlocks().find((b) => b.height === currentHeight); + if (!block && indexingAvailable) { + block = this.prepareBlock(await this.$indexBlock(currentHeight)); + } else if (!block) { + block = this.prepareBlock(await bitcoinApi.$getBlock(nextHash)); + } + returnBlocks.push(block); + nextHash = block.previousblockhash; + loadingIndicators.setProgress('blocks', i / 10 * 100); + currentHeight--; + } + + return returnBlocks; + } catch (e) { + loadingIndicators.setProgress('blocks', 100); + throw e; + } + } + + 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, + feeRange: block?.feeRange ?? [], // TODO + reward: block?.reward, + coinbaseHex: block?.extras?.coinbaseHex ?? block?.coinbase_raw, // coinbase_raw for indexed block + pool: block?.extras?.pool ?? (block?.pool_id ? { + id: block?.pool_id, + name: block?.pool_name, + link: block?.pool_link, + regexes: block?.pool_regexes, + addresses: block?.pool_addresses, + } : undefined), + } + }; + } + public getLastDifficultyAdjustmentTime(): number { return this.lastDifficultyAdjustmentTime; } diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 24ecc03cf..b8557ff65 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 = 4; + private static currentVersion = 5; private queryTimeout = 120000; private statisticsAddedIndexed = false; @@ -229,6 +229,10 @@ class DatabaseMigration { } } + if (version < 5 && (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === true)) { + queries.push('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"'); + } + return queries; } diff --git a/backend/src/index.ts b/backend/src/index.ts index 557c269dd..07808c98a 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -290,6 +290,10 @@ class Server { ; } + this.app + .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras', routes.getBlocksExtras) + .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras/:height', routes.getBlocksExtras); + if (config.MEMPOOL.BACKEND !== 'esplora') { this.app .get(config.MEMPOOL.API_URL_PREFIX + 'mempool', routes.getMempool) diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 7dfcd3956..5559c4588 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -83,10 +83,12 @@ export interface BlockExtension { reward?: number; coinbaseTx?: TransactionMinerInfo; matchRate?: number; + coinbaseHex?: string; + pool?: PoolTag; } export interface BlockExtended extends IEsploraApi.Block { - extras?: BlockExtension; + extras: BlockExtension; } export interface TransactionMinerInfo { diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 654376402..758ae1197 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -11,38 +11,36 @@ class BlocksRepository { /** * Save indexed block data in the database */ - public async $saveBlockInDatabase( - block: BlockExtended, - blockHash: string, - coinbaseHex: string | undefined, - poolTag: PoolTag - ) { + public async $saveBlockInDatabase(block: BlockExtended) { const connection = await DB.pool.getConnection(); try { const query = `INSERT INTO blocks( height, hash, blockTimestamp, size, weight, tx_count, coinbase_raw, difficulty, - pool_id, fees, fee_span, median_fee + pool_id, fees, fee_span, median_fee, + reward ) VALUE ( ?, ?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, - ?, ?, ?, ? + ?, ?, ?, ?, + ? )`; const params: any[] = [ block.height, - blockHash, + block.id, block.timestamp, block.size, block.weight, block.tx_count, - coinbaseHex ? coinbaseHex : '', + block.extras?.coinbaseHex ?? '', block.difficulty, - poolTag.id, + block.extras?.pool?.id, // Should always be set to something 0, '[]', - block.extras ? block.extras.medianFee : 0, + block.extras.medianFee ?? 0, + block.extras?.reward ?? 0, ]; await connection.query(query, params); @@ -136,6 +134,26 @@ class BlocksRepository { return rows[0].blockTimestamp; } + + /** + * Get one block by height + */ + public async $getBlockByHeight(height: number): Promise { + const connection = await DB.pool.getConnection(); + const [rows]: any[] = await connection.query(` + SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.addresses as pool_addresses, pools.regexes as pool_regexes + FROM blocks + JOIN pools ON blocks.pool_id = pools.id + WHERE height = ${height}; + `); + connection.release(); + + if (rows.length <= 0) { + return null; + } + + return rows[0]; + } } export default new BlocksRepository(); \ No newline at end of file diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 8ae2f9609..e06177ddd 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -564,6 +564,14 @@ class Routes { } } + public async getBlocksExtras(req: Request, res: Response) { + try { + res.json(await blocks.$getBlocksExtras(parseInt(req.params.height, 10))) + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + public async getBlocks(req: Request, res: Response) { try { loadingIndicators.setProgress('blocks', 0); From ef43da05c9b647e615db07bc12db28db64835276 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 10 Feb 2022 23:02:12 +0900 Subject: [PATCH 02/45] Improve block indexing logging --- backend/src/api/blocks.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index c113f4efa..7b69fca6c 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -42,7 +42,12 @@ class Blocks { * @param onlyCoinbase - Set to true if you only need the coinbase transaction * @returns Promise */ - private async $getTransactionsExtended(blockHash: string, blockHeight: number, onlyCoinbase: boolean): Promise { + private async $getTransactionsExtended( + blockHash: string, + blockHeight: number, + onlyCoinbase: boolean, + quiet: boolean = false, + ): Promise { const transactions: TransactionExtended[] = []; const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash); @@ -58,7 +63,7 @@ class Blocks { transactionsFound++; } else if (config.MEMPOOL.BACKEND === 'esplora' || memPool.isInSync() || i === 0) { // Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...) - if (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length) { // Avoid log spam + if (!quiet && (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length)) { // Avoid log spam logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`); } try { @@ -84,7 +89,9 @@ class Blocks { } }); - logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`); + if (!quiet) { + logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`); + } return transactions; } @@ -183,6 +190,7 @@ class Blocks { } this.blockIndexingStarted = true; + const startedAt = new Date().getTime() / 1000; try { let currentBlockHeight = blockchainInfo.blocks; @@ -197,6 +205,7 @@ class Blocks { logger.info(`Indexing blocks from #${currentBlockHeight} to #${lastBlockToIndex}`); const chunkSize = 10000; + let totaIndexed = 0; while (currentBlockHeight >= lastBlockToIndex) { const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1); @@ -215,12 +224,17 @@ class Blocks { break; } try { - logger.debug(`Indexing block #${blockHeight}`); + if (totaIndexed % 100 === 0 || blockHeight === lastBlockToIndex) { + const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); + const blockPerSeconds = Math.round(totaIndexed / elapsedSeconds); + logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds} blocks/sec | total: ${totaIndexed} | elapsed: ${elapsedSeconds} seconds`); + } const blockHash = await bitcoinApi.$getBlockHash(blockHeight); const block = await bitcoinApi.$getBlock(blockHash); - const transactions = await this.$getTransactionsExtended(blockHash, block.height, true); + const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true); const blockExtended = await this.$getBlockExtended(block, transactions); await blocksRepository.$saveBlockInDatabase(blockExtended); + ++totaIndexed; } catch (e) { logger.err(`Something went wrong while indexing blocks.` + e); } From aa0e6b807a93f08c0557681a7841244bcbceddf3 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 11 Feb 2022 14:22:59 +0900 Subject: [PATCH 03/45] Add missing docker configuration variable --- docker/backend/mempool-config.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json index 3a0a0ec0b..6b2319a59 100644 --- a/docker/backend/mempool-config.json +++ b/docker/backend/mempool-config.json @@ -15,7 +15,8 @@ "PRICE_FEED_UPDATE_INTERVAL": __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__, "USE_SECOND_NODE_FOR_MINFEE": __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__, "EXTERNAL_ASSETS": __MEMPOOL_EXTERNAL_ASSETS__, - "STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__" + "STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__", + "INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__ }, "CORE_RPC": { "HOST": "__CORE_RPC_HOST__", From b8e40494aa10f501e5279ad2528dd7d4c8eee5f5 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 10 Feb 2022 10:25:14 +0900 Subject: [PATCH 04/45] Remove fields that won't be used in the frontend for now --- backend/src/api/blocks.ts | 14 ++++---------- backend/src/mempool.interfaces.ts | 8 +++++--- backend/src/repositories/BlocksRepository.ts | 2 +- backend/src/repositories/PoolsRepository.ts | 4 ++-- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 7b69fca6c..d2487414f 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -126,12 +126,10 @@ class Blocks { } else { pool = await poolsRepository.$getUnknownPool(); } - blockExtended.extras.pool = pool; - - if (transactions.length > 0) { - const coinbase: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(transactions[0].txid, true); - blockExtended.extras.coinbaseHex = coinbase.hex; - } + blockExtended.extras.pool = { + id: pool.id, + name: pool.name + }; } return blockExtended; @@ -404,13 +402,9 @@ class Blocks { medianFee: block?.medianFee, feeRange: block?.feeRange ?? [], // TODO reward: block?.reward, - coinbaseHex: block?.extras?.coinbaseHex ?? block?.coinbase_raw, // coinbase_raw for indexed block pool: block?.extras?.pool ?? (block?.pool_id ? { id: block?.pool_id, name: block?.pool_name, - link: block?.pool_link, - regexes: block?.pool_regexes, - addresses: block?.pool_addresses, } : undefined), } }; diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 5559c4588..2d5092145 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -1,7 +1,7 @@ import { IEsploraApi } from './api/bitcoin/esplora-api.interface'; export interface PoolTag { - id: number | null, // mysql row id + id: number, // mysql row id name: string, link: string, regexes: string, // JSON array @@ -83,8 +83,10 @@ export interface BlockExtension { reward?: number; coinbaseTx?: TransactionMinerInfo; matchRate?: number; - coinbaseHex?: string; - pool?: PoolTag; + pool?: { + id: number; + name: string; + } } export interface BlockExtended extends IEsploraApi.Block { diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 758ae1197..18023760f 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -34,7 +34,7 @@ class BlocksRepository { block.size, block.weight, block.tx_count, - block.extras?.coinbaseHex ?? '', + '', block.difficulty, block.extras?.pool?.id, // Should always be set to something 0, diff --git a/backend/src/repositories/PoolsRepository.ts b/backend/src/repositories/PoolsRepository.ts index d1fb0da9a..b89725452 100644 --- a/backend/src/repositories/PoolsRepository.ts +++ b/backend/src/repositories/PoolsRepository.ts @@ -7,7 +7,7 @@ class PoolsRepository { */ public async $getPools(): Promise { const connection = await DB.pool.getConnection(); - const [rows] = await connection.query('SELECT * FROM pools;'); + const [rows] = await connection.query('SELECT id, name, addresses, regexes FROM pools;'); connection.release(); return rows; } @@ -17,7 +17,7 @@ class PoolsRepository { */ public async $getUnknownPool(): Promise { const connection = await DB.pool.getConnection(); - const [rows] = await connection.query('SELECT * FROM pools where name = "Unknown"'); + const [rows] = await connection.query('SELECT id, name FROM pools where name = "Unknown"'); connection.release(); return rows[0]; } From 2e1348550e15327d8c3175719bf3041376ff9cc2 Mon Sep 17 00:00:00 2001 From: Antoni Spaanderman <56turtle56@gmail.com> Date: Sun, 13 Feb 2022 13:52:04 +0100 Subject: [PATCH 05/45] implement /api/mempool --- backend/src/api/bitcoin/bitcoin-api.interface.ts | 1 + backend/src/api/mempool.ts | 2 +- backend/src/routes.ts | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api.interface.ts b/backend/src/api/bitcoin/bitcoin-api.interface.ts index e2b9158bb..6a22af9a0 100644 --- a/backend/src/api/bitcoin/bitcoin-api.interface.ts +++ b/backend/src/api/bitcoin/bitcoin-api.interface.ts @@ -4,6 +4,7 @@ export namespace IBitcoinApi { size: number; // (numeric) Current tx count bytes: number; // (numeric) Sum of all virtual transaction sizes as defined in BIP 141. usage: number; // (numeric) Total memory usage for the mempool + total_fee: number; // (numeric) Total fees of transactions in the mempool maxmempool: number; // (numeric) Maximum memory usage for the mempool mempoolminfee: number; // (numeric) Minimum fee rate in BTC/kB for tx to be accepted. minrelaytxfee: number; // (numeric) Current minimum relay fee for transactions diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index b1bd6a159..3a389c059 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -14,7 +14,7 @@ class Mempool { private static LAZY_DELETE_AFTER_SECONDS = 30; private inSync: boolean = false; private mempoolCache: { [txId: string]: TransactionExtended } = {}; - private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, + private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 }; private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => void) | undefined; diff --git a/backend/src/routes.ts b/backend/src/routes.ts index e06177ddd..e03eb3a03 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -699,7 +699,12 @@ class Routes { } public async getMempool(req: Request, res: Response) { - res.status(501).send('Not implemented'); + const info = mempool.getMempoolInfo(); + res.json({ + count: info.size, + vsize: info.bytes, + total_fee: info.total_fee * 1e8 + }); } public async getMempoolTxIds(req: Request, res: Response) { From 3a3392423d07fc28cdb506a4da88be44525c6241 Mon Sep 17 00:00:00 2001 From: Antoni Spaanderman <56turtle56@gmail.com> Date: Sun, 13 Feb 2022 16:13:46 +0100 Subject: [PATCH 06/45] set fee_histogram to [] --- backend/src/routes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/routes.ts b/backend/src/routes.ts index e03eb3a03..9e25f0d35 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -703,7 +703,8 @@ class Routes { res.json({ count: info.size, vsize: info.bytes, - total_fee: info.total_fee * 1e8 + total_fee: info.total_fee * 1e8, + fee_histogram: [] }); } From b854c071d00e2ffcb9b92eb4a8afa6934b8aebc6 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 8 Feb 2022 18:28:53 +0900 Subject: [PATCH 07/45] Added `mining/pool/:poolId` and `mining/pool/:poolId/:interval` APIs --- backend/src/api/mining.ts | 47 ++++++++++++++------ backend/src/index.ts | 11 ++++- backend/src/repositories/BlocksRepository.ts | 19 +++++++- backend/src/repositories/PoolsRepository.ts | 17 +++++++ backend/src/routes.ts | 15 +++++++ 5 files changed, 94 insertions(+), 15 deletions(-) diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index c89ea9324..7431dc0b3 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -7,23 +7,26 @@ class Mining { constructor() { } + private getSqlInterval(interval: string | null): string | null { + switch (interval) { + case '24h': return '1 DAY'; + case '3d': return '3 DAY'; + case '1w': return '1 WEEK'; + case '1m': return '1 MONTH'; + case '3m': return '3 MONTH'; + case '6m': return '6 MONTH'; + case '1y': return '1 YEAR'; + case '2y': return '2 YEAR'; + case '3y': return '3 YEAR'; + default: return null; + } + } + /** * Generate high level overview of the pool ranks and general stats */ public async $getPoolsStats(interval: string | null) : Promise { - let sqlInterval: string | null = null; - switch (interval) { - case '24h': sqlInterval = '1 DAY'; break; - case '3d': sqlInterval = '3 DAY'; break; - case '1w': sqlInterval = '1 WEEK'; break; - case '1m': sqlInterval = '1 MONTH'; break; - case '3m': sqlInterval = '3 MONTH'; break; - case '6m': sqlInterval = '6 MONTH'; break; - case '1y': sqlInterval = '1 YEAR'; break; - case '2y': sqlInterval = '2 YEAR'; break; - case '3y': sqlInterval = '3 YEAR'; break; - default: sqlInterval = null; break; - } + const sqlInterval = this.getSqlInterval(interval); const poolsStatistics = {}; @@ -64,6 +67,24 @@ class Mining { return poolsStatistics; } + + /** + * Get all mining pool stats for a pool + */ + public async $getPoolStat(interval: string | null, poolId: number): Promise { + const pool = await PoolsRepository.$getPool(poolId); + if (!pool) { + throw new Error("This mining pool does not exist"); + } + + const sqlInterval = this.getSqlInterval(interval); + const blocks = await BlocksRepository.$getBlocksByPool(sqlInterval, poolId); + + return { + pool: pool, + blocks: blocks, + }; + } } export default new Mining(); diff --git a/backend/src/index.ts b/backend/src/index.ts index 07808c98a..b8ca55339 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -256,6 +256,14 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', routes.$getStatisticsByTime.bind(routes, '1y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', routes.$getStatisticsByTime.bind(routes, '2y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', routes.$getStatisticsByTime.bind(routes, '3y')) + ; + } + + const indexingAvailable = + ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && + config.DATABASE.ENABLED === true; + if (indexingAvailable) { + this.app .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/24h', routes.$getPools.bind(routes, '24h')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3d', routes.$getPools.bind(routes, '3d')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/1w', routes.$getPools.bind(routes, '1w')) @@ -266,7 +274,8 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/2y', routes.$getPools.bind(routes, '2y')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3y', routes.$getPools.bind(routes, '3y')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/all', routes.$getPools.bind(routes, 'all')) - ; + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool); } if (config.BISQ.ENABLED) { diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 18023760f..03545f730 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -135,6 +135,23 @@ class BlocksRepository { return rows[0].blockTimestamp; } + /** + * Get blocks mined by a specific mining pool + */ + public async $getBlocksByPool(interval: string | null, poolId: number): Promise { + const query = ` + SELECT * + FROM blocks + WHERE pool_id = ${poolId}` + + (interval != null ? ` AND blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()` : ``); + + const connection = await DB.pool.getConnection(); + const [rows] = await connection.query(query); + connection.release(); + + return rows; + } + /** * Get one block by height */ @@ -156,4 +173,4 @@ class BlocksRepository { } } -export default new BlocksRepository(); \ No newline at end of file +export default new BlocksRepository(); diff --git a/backend/src/repositories/PoolsRepository.ts b/backend/src/repositories/PoolsRepository.ts index b89725452..50f5268a7 100644 --- a/backend/src/repositories/PoolsRepository.ts +++ b/backend/src/repositories/PoolsRepository.ts @@ -41,6 +41,23 @@ class PoolsRepository { return rows; } + + /** + * Get mining pool statistics for one pool + */ + public async $getPool(poolId: number) : Promise { + const query = ` + SELECT * + FROM pools + WHERE pools.id = ${poolId} + `; + + const connection = await DB.pool.getConnection(); + const [rows] = await connection.query(query); + connection.release(); + + return rows[0]; + } } export default new PoolsRepository(); diff --git a/backend/src/routes.ts b/backend/src/routes.ts index e06177ddd..6811f9190 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -22,6 +22,8 @@ import elementsParser from './api/liquid/elements-parser'; import icons from './api/liquid/icons'; import miningStats from './api/mining'; import axios from 'axios'; +import PoolsRepository from './repositories/PoolsRepository'; +import mining from './api/mining'; class Routes { constructor() {} @@ -533,6 +535,19 @@ class Routes { } } + public async $getPool(req: Request, res: Response) { + try { + const poolId = parseInt(req.params.poolId); + const stats = await mining.$getPoolStat(req.params.interval ?? null, poolId); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(stats); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + public async $getPools(interval: string, req: Request, res: Response) { try { let stats = await miningStats.$getPoolsStats(interval); From fbda0d8186236eb39783eb22f22e678426635e9b Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 8 Feb 2022 18:56:51 +0900 Subject: [PATCH 08/45] Added /mining/pool/:poolId empty page --- frontend/server.ts | 1 + frontend/src/app/app-routing.module.ts | 13 +++++++++++++ frontend/src/app/app.module.ts | 2 ++ .../src/app/components/pool/pool.component.html | 5 +++++ .../src/app/components/pool/pool.component.scss | 0 frontend/src/app/components/pool/pool.component.ts | 14 ++++++++++++++ 6 files changed, 35 insertions(+) create mode 100644 frontend/src/app/components/pool/pool.component.html create mode 100644 frontend/src/app/components/pool/pool.component.scss create mode 100644 frontend/src/app/components/pool/pool.component.ts diff --git a/frontend/server.ts b/frontend/server.ts index df4ab1294..b6c765588 100644 --- a/frontend/server.ts +++ b/frontend/server.ts @@ -66,6 +66,7 @@ export function app(locale: string): express.Express { server.get('/address/*', getLocalizedSSR(indexHtml)); server.get('/blocks', getLocalizedSSR(indexHtml)); server.get('/mining/pools', getLocalizedSSR(indexHtml)); + server.get('/mining/pool/*', getLocalizedSSR(indexHtml)); server.get('/graphs', getLocalizedSSR(indexHtml)); server.get('/liquid', getLocalizedSSR(indexHtml)); server.get('/liquid/tx/*', getLocalizedSSR(indexHtml)); diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index aaf545206..4018ed64e 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -26,6 +26,7 @@ import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.com import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component'; import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; import { AssetsComponent } from './components/assets/assets.component'; +import { PoolComponent } from './components/pool/pool.component'; let routes: Routes = [ { @@ -66,6 +67,10 @@ let routes: Routes = [ path: 'mining/pools', component: PoolRankingComponent, }, + { + path: 'mining/pool/:poolId', + component: PoolComponent, + }, { path: 'graphs', component: StatisticsComponent, @@ -154,6 +159,10 @@ let routes: Routes = [ path: 'mining/pools', component: PoolRankingComponent, }, + { + path: 'mining/pool/:poolId', + component: PoolComponent, + }, { path: 'graphs', component: StatisticsComponent, @@ -236,6 +245,10 @@ let routes: Routes = [ path: 'mining/pools', component: PoolRankingComponent, }, + { + path: 'mining/pool/:poolId', + component: PoolComponent, + }, { path: 'graphs', component: StatisticsComponent, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 97fc16204..20eb2ea03 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -38,6 +38,7 @@ import { TimeSpanComponent } from './components/time-span/time-span.component'; import { SeoService } from './services/seo.service'; import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component'; import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component'; +import { PoolComponent } from './components/pool/pool.component'; import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component'; import { AssetComponent } from './components/asset/asset.component'; import { AssetsComponent } from './components/assets/assets.component'; @@ -96,6 +97,7 @@ import { AssetGroupComponent } from './components/assets/asset-group/asset-group IncomingTransactionsGraphComponent, MempoolGraphComponent, PoolRankingComponent, + PoolComponent, LbtcPegsGraphComponent, AssetComponent, AssetsComponent, diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html new file mode 100644 index 000000000..62c259ccf --- /dev/null +++ b/frontend/src/app/components/pool/pool.component.html @@ -0,0 +1,5 @@ +
+ + Pool + +
\ No newline at end of file diff --git a/frontend/src/app/components/pool/pool.component.scss b/frontend/src/app/components/pool/pool.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts new file mode 100644 index 000000000..907dcf0fd --- /dev/null +++ b/frontend/src/app/components/pool/pool.component.ts @@ -0,0 +1,14 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-pool', + templateUrl: './pool.component.html', + styleUrls: ['./pool.component.scss'] +}) +export class PoolComponent implements OnInit { + constructor( + ) { } + + ngOnInit(): void { + } +} From a168a223601a44e6063da7a96666a6068d7ebe1e Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 8 Feb 2022 19:04:53 +0900 Subject: [PATCH 09/45] Link PoolRanking page with new pool page --- .../src/app/components/pool-ranking/pool-ranking.component.html | 2 +- frontend/src/app/interfaces/node-api.interface.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.html b/frontend/src/app/components/pool-ranking/pool-ranking.component.html index 8deb8597a..45707d328 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.html +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.html @@ -58,7 +58,7 @@ {{ pool.rank }} - {{ pool.name }} + {{ pool.name }} {{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }} {{ pool['blockText'] }} {{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%) diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 373385422..b923d25b7 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -55,7 +55,7 @@ export interface LiquidPegs { export interface ITranslators { [language: string]: string; } export interface SinglePoolStats { - pooldId: number; + poolId: number; name: string; link: string; blockCount: number; From 3f55aabc53e98945272c9e5be82b9d7c05aae707 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 9 Feb 2022 19:41:05 +0900 Subject: [PATCH 10/45] Mining pool detail page draft PoC --- backend/src/api/common.ts | 15 +++ backend/src/api/mining.ts | 33 ++---- backend/src/index.ts | 4 +- backend/src/mempool.interfaces.ts | 22 ++-- backend/src/repositories/BlocksRepository.ts | 109 +++++++++++++----- backend/src/repositories/PoolsRepository.ts | 33 +++--- backend/src/routes.ts | 21 +++- frontend/src/app/app-routing.module.ts | 12 ++ .../app/components/pool/pool.component.html | 63 +++++++++- .../src/app/components/pool/pool.component.ts | 52 +++++++++ .../src/app/interfaces/node-api.interface.ts | 32 +++-- frontend/src/app/services/api.service.ts | 10 +- 12 files changed, 313 insertions(+), 93 deletions(-) diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 5e99e870c..c470f6fe7 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -154,4 +154,19 @@ export class Common { }); return parents; } + + static getSqlInterval(interval: string | null): string | null { + switch (interval) { + case '24h': return '1 DAY'; + case '3d': return '3 DAY'; + case '1w': return '1 WEEK'; + case '1m': return '1 MONTH'; + case '3m': return '3 MONTH'; + case '6m': return '6 MONTH'; + case '1y': return '1 YEAR'; + case '2y': return '2 YEAR'; + case '3y': return '3 YEAR'; + default: return null; + } + } } diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index 7431dc0b3..bf8c6b340 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -2,36 +2,20 @@ import { PoolInfo, PoolStats } from '../mempool.interfaces'; import BlocksRepository, { EmptyBlocks } from '../repositories/BlocksRepository'; import PoolsRepository from '../repositories/PoolsRepository'; import bitcoinClient from './bitcoin/bitcoin-client'; +import { Common } from './common'; class Mining { constructor() { } - private getSqlInterval(interval: string | null): string | null { - switch (interval) { - case '24h': return '1 DAY'; - case '3d': return '3 DAY'; - case '1w': return '1 WEEK'; - case '1m': return '1 MONTH'; - case '3m': return '3 MONTH'; - case '6m': return '6 MONTH'; - case '1y': return '1 YEAR'; - case '2y': return '2 YEAR'; - case '3y': return '3 YEAR'; - default: return null; - } - } - /** * Generate high level overview of the pool ranks and general stats */ public async $getPoolsStats(interval: string | null) : Promise { - const sqlInterval = this.getSqlInterval(interval); - const poolsStatistics = {}; - const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(sqlInterval); - const emptyBlocks: EmptyBlocks[] = await BlocksRepository.$countEmptyBlocks(sqlInterval); + const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval); + const emptyBlocks: EmptyBlocks[] = await BlocksRepository.$getEmptyBlocks(null, interval); const poolsStats: PoolStats[] = []; let rank = 1; @@ -58,7 +42,7 @@ class Mining { const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp()); poolsStatistics['oldestIndexedBlockTimestamp'] = oldestBlock.getTime(); - const blockCount: number = await BlocksRepository.$blockCount(sqlInterval); + const blockCount: number = await BlocksRepository.$blockCount(null, interval); poolsStatistics['blockCount'] = blockCount; const blockHeightTip = await bitcoinClient.getBlockCount(); @@ -74,15 +58,16 @@ class Mining { public async $getPoolStat(interval: string | null, poolId: number): Promise { const pool = await PoolsRepository.$getPool(poolId); if (!pool) { - throw new Error("This mining pool does not exist"); + throw new Error(`This mining pool does not exist`); } - const sqlInterval = this.getSqlInterval(interval); - const blocks = await BlocksRepository.$getBlocksByPool(sqlInterval, poolId); + const blockCount: number = await BlocksRepository.$blockCount(poolId, interval); + const emptyBlocks: EmptyBlocks[] = await BlocksRepository.$getEmptyBlocks(poolId, interval); return { pool: pool, - blocks: blocks, + blockCount: blockCount, + emptyBlocks: emptyBlocks, }; } } diff --git a/backend/src/index.ts b/backend/src/index.ts index b8ca55339..4b0466bf6 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -275,7 +275,9 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3y', routes.$getPools.bind(routes, '3y')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/all', routes.$getPools.bind(routes, 'all')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool); + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool-blocks/:poolId', routes.$getPoolBlocks) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool-blocks/:poolId/:height', routes.$getPoolBlocks); } if (config.BISQ.ENABLED) { diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 2d5092145..4869561c2 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -1,23 +1,23 @@ import { IEsploraApi } from './api/bitcoin/esplora-api.interface'; export interface PoolTag { - id: number, // mysql row id - name: string, - link: string, - regexes: string, // JSON array - addresses: string, // JSON array + id: number; // mysql row id + name: string; + link: string; + regexes: string; // JSON array + addresses: string; // JSON array } export interface PoolInfo { - poolId: number, // mysql row id - name: string, - link: string, - blockCount: number, + poolId: number; // mysql row id + name: string; + link: string; + blockCount: number; } export interface PoolStats extends PoolInfo { - rank: number, - emptyBlocks: number, + rank: number; + emptyBlocks: number; } export interface MempoolBlock { diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 03545f730..03b9fe5bf 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -1,6 +1,7 @@ import { BlockExtended, PoolTag } from '../mempool.interfaces'; import { DB } from '../database'; import logger from '../logger'; +import { Common } from '../api/common'; export interface EmptyBlocks { emptyBlocks: number; @@ -43,6 +44,7 @@ class BlocksRepository { block.extras?.reward ?? 0, ]; + logger.debug(query); await connection.query(query, params); } catch (e: any) { if (e.errno === 1062) { // ER_DUP_ENTRY @@ -64,35 +66,45 @@ class BlocksRepository { } const connection = await DB.pool.getConnection(); - const [rows] : any[] = await connection.query(` + const [rows]: any[] = await connection.query(` SELECT height FROM blocks - WHERE height <= ${startHeight} AND height >= ${endHeight} + WHERE height <= ? AND height >= ? ORDER BY height DESC; - `); + `, [startHeight, endHeight]); connection.release(); const indexedBlockHeights: number[] = []; rows.forEach((row: any) => { indexedBlockHeights.push(row.height); }); const seekedBlocks: number[] = Array.from(Array(startHeight - endHeight + 1).keys(), n => n + endHeight).reverse(); - const missingBlocksHeights = seekedBlocks.filter(x => indexedBlockHeights.indexOf(x) === -1); + const missingBlocksHeights = seekedBlocks.filter(x => indexedBlockHeights.indexOf(x) === -1); return missingBlocksHeights; } /** - * Count empty blocks for all pools + * Get empty blocks for one or all pools */ - public async $countEmptyBlocks(interval: string | null): Promise { - const query = ` - SELECT pool_id as poolId - FROM blocks - WHERE tx_count = 1` + - (interval != null ? ` AND blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()` : ``) - ; + public async $getEmptyBlocks(poolId: number | null, interval: string | null = null): Promise { + interval = Common.getSqlInterval(interval); + const params: any[] = []; + let query = `SELECT height, hash, tx_count, size, pool_id, weight, UNIX_TIMESTAMP(blockTimestamp) as timestamp + FROM blocks + WHERE tx_count = 1`; + + if (poolId) { + query += ` AND pool_id = ?`; + params.push(poolId); + } + + if (interval) { + query += ` AND blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; + } + + logger.debug(query); const connection = await DB.pool.getConnection(); - const [rows] = await connection.query(query); + const [rows] = await connection.query(query, params); connection.release(); return rows; @@ -101,15 +113,30 @@ class BlocksRepository { /** * Get blocks count for a period */ - public async $blockCount(interval: string | null): Promise { - const query = ` - SELECT count(height) as blockCount - FROM blocks` + - (interval != null ? ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()` : ``) - ; + public async $blockCount(poolId: number | null, interval: string | null): Promise { + interval = Common.getSqlInterval(interval); + const params: any[] = []; + let query = `SELECT count(height) as blockCount + FROM blocks`; + + if (poolId) { + query += ` WHERE pool_id = ?`; + params.push(poolId); + } + + if (interval) { + if (poolId) { + query += ` AND`; + } else { + query += ` WHERE`; + } + query += ` blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; + } + + logger.debug(query); const connection = await DB.pool.getConnection(); - const [rows] = await connection.query(query); + const [rows] = await connection.query(query, params); connection.release(); return rows[0].blockCount; @@ -119,13 +146,15 @@ class BlocksRepository { * Get the oldest indexed block */ public async $oldestBlockTimestamp(): Promise { - const connection = await DB.pool.getConnection(); - const [rows]: any[] = await connection.query(` - SELECT blockTimestamp + const query = `SELECT blockTimestamp FROM blocks ORDER BY height - LIMIT 1; - `); + LIMIT 1;`; + + + logger.debug(query); + const connection = await DB.pool.getConnection(); + const [rows]: any[] = await connection.query(query); connection.release(); if (rows.length <= 0) { @@ -138,18 +167,34 @@ class BlocksRepository { /** * Get blocks mined by a specific mining pool */ - public async $getBlocksByPool(interval: string | null, poolId: number): Promise { - const query = ` - SELECT * + public async $getBlocksByPool( + poolId: number, + startHeight: number | null = null + ): Promise { + const params: any[] = []; + let query = `SELECT height, hash, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp FROM blocks - WHERE pool_id = ${poolId}` - + (interval != null ? ` AND blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()` : ``); + WHERE pool_id = ?`; + params.push(poolId); + if (startHeight) { + query += ` AND height < ?`; + params.push(startHeight); + } + + query += ` ORDER BY height DESC + LIMIT 10`; + + logger.debug(query); const connection = await DB.pool.getConnection(); - const [rows] = await connection.query(query); + const [rows] = await connection.query(query, params); connection.release(); - return rows; + for (const block of rows) { + delete block['blockTimestamp']; + } + + return rows; } /** diff --git a/backend/src/repositories/PoolsRepository.ts b/backend/src/repositories/PoolsRepository.ts index 50f5268a7..b94f3d36d 100644 --- a/backend/src/repositories/PoolsRepository.ts +++ b/backend/src/repositories/PoolsRepository.ts @@ -1,4 +1,6 @@ +import { Common } from '../api/common'; import { DB } from '../database'; +import logger from '../logger'; import { PoolInfo, PoolTag } from '../mempool.interfaces'; class PoolsRepository { @@ -25,16 +27,21 @@ class PoolsRepository { /** * Get basic pool info and block count */ - public async $getPoolsInfo(interval: string | null): Promise { - const query = ` - SELECT COUNT(height) as blockCount, pool_id as poolId, pools.name as name, pools.link as link - FROM blocks - JOIN pools on pools.id = pool_id` + - (interval != null ? ` WHERE blocks.blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()` : ``) + - ` GROUP BY pool_id - ORDER BY COUNT(height) DESC - `; + public async $getPoolsInfo(interval: string | null = null): Promise { + interval = Common.getSqlInterval(interval); + let query = `SELECT COUNT(height) as blockCount, pool_id as poolId, pools.name as name, pools.link as link + FROM blocks + JOIN pools on pools.id = pool_id`; + + if (interval) { + query += ` WHERE blocks.blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; + } + + query += ` GROUP BY pool_id + ORDER BY COUNT(height) DESC`; + + logger.debug(query); const connection = await DB.pool.getConnection(); const [rows] = await connection.query(query); connection.release(); @@ -45,15 +52,15 @@ class PoolsRepository { /** * Get mining pool statistics for one pool */ - public async $getPool(poolId: number) : Promise { + public async $getPool(poolId: any): Promise { const query = ` SELECT * FROM pools - WHERE pools.id = ${poolId} - `; + WHERE pools.id = ?`; + logger.debug(query); const connection = await DB.pool.getConnection(); - const [rows] = await connection.query(query); + const [rows] = await connection.query(query, [poolId]); connection.release(); return rows[0]; diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 6811f9190..4e59bef3a 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -24,6 +24,7 @@ import miningStats from './api/mining'; import axios from 'axios'; import PoolsRepository from './repositories/PoolsRepository'; import mining from './api/mining'; +import BlocksRepository from './repositories/BlocksRepository'; class Routes { constructor() {} @@ -537,8 +538,7 @@ class Routes { public async $getPool(req: Request, res: Response) { try { - const poolId = parseInt(req.params.poolId); - const stats = await mining.$getPoolStat(req.params.interval ?? null, poolId); + const stats = await mining.$getPoolStat(req.params.interval ?? null, parseInt(req.params.poolId, 10)); res.header('Pragma', 'public'); res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); @@ -548,9 +548,24 @@ class Routes { } } + public async $getPoolBlocks(req: Request, res: Response) { + try { + const poolBlocks = await BlocksRepository.$getBlocksByPool( + parseInt(req.params.poolId, 10), + parseInt(req.params.height, 10) ?? null, + ); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(poolBlocks); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + public async $getPools(interval: string, req: Request, res: Response) { try { - let stats = await miningStats.$getPoolsStats(interval); + const stats = await miningStats.$getPoolsStats(interval); res.header('Pragma', 'public'); res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 4018ed64e..a13bc1e54 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -71,6 +71,10 @@ let routes: Routes = [ path: 'mining/pool/:poolId', component: PoolComponent, }, + { + path: 'mining/pool/:poolId/:interval', + component: PoolComponent, + }, { path: 'graphs', component: StatisticsComponent, @@ -163,6 +167,10 @@ let routes: Routes = [ path: 'mining/pool/:poolId', component: PoolComponent, }, + { + path: 'mining/pool/:poolId/:interval', + component: PoolComponent, + }, { path: 'graphs', component: StatisticsComponent, @@ -249,6 +257,10 @@ let routes: Routes = [ path: 'mining/pool/:poolId', component: PoolComponent, }, + { + path: 'mining/pool/:poolId/:interval', + component: PoolComponent, + }, { path: 'graphs', component: StatisticsComponent, diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index 62c259ccf..9040e4553 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -1,5 +1,66 @@
- Pool +
+

+ {{ poolStats.pool.name }} +

+ +
+
+
+ + + + + + + + + + + +
Address{{ poolStats.pool.addresses }}
Coinbase Tag{{ poolStats.pool.regexes }}
+
+
+ + + + + + + + + + + +
Mined Blocks{{ poolStats.blockCount }}
Empty Blocks{{ poolStats.emptyBlocks.length }}
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
HeightTimestampMinedTransactionsSize
{{ block.height }}‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}{{ block.tx_count | number }} +
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 907dcf0fd..8296f7520 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -1,4 +1,10 @@ import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { map, switchMap, tap } from 'rxjs/operators'; +import { BlockExtended, PoolStat } from 'src/app/interfaces/node-api.interface'; +import { ApiService } from 'src/app/services/api.service'; +import { StateService } from 'src/app/services/state.service'; @Component({ selector: 'app-pool', @@ -6,9 +12,55 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./pool.component.scss'] }) export class PoolComponent implements OnInit { + poolStats$: Observable; + isLoading = false; + + poolId: number; + interval: string; + + blocks: any[] = []; + constructor( + private apiService: ApiService, + private route: ActivatedRoute, + public stateService: StateService, ) { } ngOnInit(): void { + this.poolStats$ = this.route.params + .pipe( + switchMap((params) => { + this.poolId = params.poolId; + this.interval = params.interval; + this.loadMore(2); + return this.apiService.getPoolStats$(params.poolId, params.interval ?? 'all'); + }), + ); + } + + loadMore(chunks = 0) { + let fromHeight: number | undefined; + if (this.blocks.length > 0) { + fromHeight = this.blocks[this.blocks.length - 1].height - 1; + } + + this.apiService.getPoolBlocks$(this.poolId, fromHeight) + .subscribe((blocks) => { + this.blocks = this.blocks.concat(blocks); + + const chunksLeft = chunks - 1; + if (chunksLeft > 0) { + this.loadMore(chunksLeft); + } + // this.cd.markForCheck(); + }, + (error) => { + console.log(error); + // this.cd.markForCheck(); + }); + } + + trackByBlock(index: number, block: BlockExtended) { + return block.height; } } diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index b923d25b7..dfcb5836e 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -54,6 +54,9 @@ export interface LiquidPegs { export interface ITranslators { [language: string]: string; } +/** + * PoolRanking component + */ export interface SinglePoolStats { poolId: number; name: string; @@ -66,20 +69,35 @@ export interface SinglePoolStats { emptyBlockRatio: string; logo: string; } - export interface PoolsStats { blockCount: number; lastEstimatedHashrate: number; oldestIndexedBlockTimestamp: number; pools: SinglePoolStats[]; } - export interface MiningStats { - lastEstimatedHashrate: string, - blockCount: number, - totalEmptyBlock: number, - totalEmptyBlockRatio: string, - pools: SinglePoolStats[], + lastEstimatedHashrate: string; + blockCount: number; + totalEmptyBlock: number; + totalEmptyBlockRatio: string; + pools: SinglePoolStats[]; +} + +/** + * Pool component + */ +export interface PoolInfo { + id: number | null; // mysql row id + name: string; + link: string; + regexes: string; // JSON array + addresses: string; // JSON array + emptyBlocks: number; +} +export interface PoolStat { + pool: PoolInfo; + blockCount: number; + emptyBlocks: BlockExtended[]; } export interface BlockExtension { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index c19bf5a41..ec14d8a5d 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation, LiquidPegs, ITranslators, PoolsStats } from '../interfaces/node-api.interface'; +import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation, LiquidPegs, ITranslators, PoolsStats, PoolStat, BlockExtended, BlockExtension } from '../interfaces/node-api.interface'; import { Observable } from 'rxjs'; import { StateService } from './state.service'; import { WebsocketResponse } from '../interfaces/websocket.interface'; @@ -132,4 +132,12 @@ export class ApiService { listPools$(interval: string | null) : Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools/${interval}`); } + + getPoolStats$(poolId: number, interval: string | null): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}/${interval}`); + } + + getPoolBlocks$(poolId: number, fromHeight: number): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool-blocks/${poolId}/${fromHeight}`); + } } From f2abedfbaa60b2fc95e3bb5eca509318cc316c27 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 10 Feb 2022 19:16:00 +0900 Subject: [PATCH 11/45] Add timespan switch for pool stats and load more for pool's blocks --- frontend/src/app/app-routing.module.ts | 12 --- .../app/components/pool/pool.component.html | 44 +++++++++- .../src/app/components/pool/pool.component.ts | 81 ++++++++++--------- frontend/src/app/services/api.service.ts | 6 +- 4 files changed, 91 insertions(+), 52 deletions(-) diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index a13bc1e54..4018ed64e 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -71,10 +71,6 @@ let routes: Routes = [ path: 'mining/pool/:poolId', component: PoolComponent, }, - { - path: 'mining/pool/:poolId/:interval', - component: PoolComponent, - }, { path: 'graphs', component: StatisticsComponent, @@ -167,10 +163,6 @@ let routes: Routes = [ path: 'mining/pool/:poolId', component: PoolComponent, }, - { - path: 'mining/pool/:poolId/:interval', - component: PoolComponent, - }, { path: 'graphs', component: StatisticsComponent, @@ -257,10 +249,6 @@ let routes: Routes = [ path: 'mining/pool/:poolId', component: PoolComponent, }, - { - path: 'mining/pool/:poolId/:interval', - component: PoolComponent, - }, { path: 'graphs', component: StatisticsComponent, diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index 9040e4553..14f8fc924 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -6,6 +6,46 @@
+
+
+
+
+
+ + + + + + + + + + +
+
+
+
+
@@ -47,9 +87,9 @@ - + - + diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 8296f7520..a89c9a7b4 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -1,7 +1,9 @@ import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; -import { Observable } from 'rxjs'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { once } from 'process'; +import { BehaviorSubject, combineLatest, from, merge, Observable } from 'rxjs'; +import { delay, distinctUntilChanged, map, scan, startWith, switchMap, tap } from 'rxjs/operators'; import { BlockExtended, PoolStat } from 'src/app/interfaces/node-api.interface'; import { ApiService } from 'src/app/services/api.service'; import { StateService } from 'src/app/services/state.service'; @@ -13,51 +15,56 @@ import { StateService } from 'src/app/services/state.service'; }) export class PoolComponent implements OnInit { poolStats$: Observable; + blocks$: Observable; + + fromHeight: number = -1; + fromHeightSubject: BehaviorSubject = new BehaviorSubject(this.fromHeight); + + blocks: BlockExtended[] = []; + poolId: number = undefined; isLoading = false; - - poolId: number; - interval: string; - - blocks: any[] = []; + radioGroupForm: FormGroup; constructor( private apiService: ApiService, private route: ActivatedRoute, public stateService: StateService, - ) { } - - ngOnInit(): void { - this.poolStats$ = this.route.params - .pipe( - switchMap((params) => { - this.poolId = params.poolId; - this.interval = params.interval; - this.loadMore(2); - return this.apiService.getPoolStats$(params.poolId, params.interval ?? 'all'); - }), - ); + private formBuilder: FormBuilder, + ) { + this.radioGroupForm = this.formBuilder.group({ dateSpan: '1w' }); + this.radioGroupForm.controls.dateSpan.setValue('1w'); } - loadMore(chunks = 0) { - let fromHeight: number | undefined; - if (this.blocks.length > 0) { - fromHeight = this.blocks[this.blocks.length - 1].height - 1; - } + ngOnInit(): void { + this.poolStats$ = combineLatest([ + this.route.params.pipe(map((params) => params.poolId)), + this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith('1w')), + ]) + .pipe( + switchMap((params: any) => { + this.poolId = params[0]; + if (this.blocks.length === 0) { + this.fromHeightSubject.next(undefined); + } + return this.apiService.getPoolStats$(this.poolId, params[1] ?? '1w'); + }), + ); - this.apiService.getPoolBlocks$(this.poolId, fromHeight) - .subscribe((blocks) => { - this.blocks = this.blocks.concat(blocks); + this.blocks$ = this.fromHeightSubject + .pipe( + distinctUntilChanged(), + switchMap((fromHeight) => { + return this.apiService.getPoolBlocks$(this.poolId, fromHeight); + }), + tap((newBlocks) => { + this.blocks = this.blocks.concat(newBlocks); + }), + map(() => this.blocks) + ) + } - const chunksLeft = chunks - 1; - if (chunksLeft > 0) { - this.loadMore(chunksLeft); - } - // this.cd.markForCheck(); - }, - (error) => { - console.log(error); - // this.cd.markForCheck(); - }); + loadMore() { + this.fromHeightSubject.next(this.blocks[this.blocks.length - 1]?.height); } trackByBlock(index: number, block: BlockExtended) { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index ec14d8a5d..c1d42fd50 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -138,6 +138,10 @@ export class ApiService { } getPoolBlocks$(poolId: number, fromHeight: number): Observable { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool-blocks/${poolId}/${fromHeight}`); + if (fromHeight !== undefined) { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath +`/api/v1/mining/pool-blocks/${poolId}/${fromHeight}`); + } else { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath +`/api/v1/mining/pool-blocks/${poolId}`); + } } } From e1f3c662b2e8881358fa8ccaf5c176bac048c5dc Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 11 Feb 2022 17:55:38 +0900 Subject: [PATCH 12/45] Show block reward in the pool stat page --- backend/src/repositories/BlocksRepository.ts | 2 +- .../app/components/pool/pool.component.html | 89 ++++++++++--------- .../app/components/pool/pool.component.scss | 32 +++++++ 3 files changed, 78 insertions(+), 45 deletions(-) diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 03b9fe5bf..4b480a0a7 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -172,7 +172,7 @@ class BlocksRepository { startHeight: number | null = null ): Promise { const params: any[] = []; - let query = `SELECT height, hash, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp + let query = `SELECT height, hash, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, reward FROM blocks WHERE pool_id = ?`; params.push(poolId); diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index 14f8fc924..08e742eb9 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -1,62 +1,61 @@ -
+

{{ poolStats.pool.name }}

+
+
+
+
+ + + + + + + + + + +
+ +
+
+
-
-
-
- - - - - - - - - - -
- -
-
-
-
-
-
Transactions Size
{{ block.height }}{{ block.height }} ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} {{ block.tx_count | number }}
+
- + - +
Address{{ poolStats.pool.addresses }}{{ poolStats.pool.addresses }}
Coinbase Tag{{ poolStats.pool.regexes }}{{ poolStats.pool.regexes }}
@@ -84,6 +83,7 @@ Height Timestamp Mined + Reward Transactions Size @@ -92,6 +92,7 @@ {{ block.height }} ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} + {{ block.tx_count | number }}
diff --git a/frontend/src/app/components/pool/pool.component.scss b/frontend/src/app/components/pool/pool.component.scss index e69de29bb..a1078adc0 100644 --- a/frontend/src/app/components/pool/pool.component.scss +++ b/frontend/src/app/components/pool/pool.component.scss @@ -0,0 +1,32 @@ +.progress { + background-color: #2d3348; +} + +@media (min-width: 768px) { + .d-md-block { + display: table-cell !important; + } +} +@media (min-width: 992px) { + .d-lg-block { + display: table-cell !important; + } +} + +.formRadioGroup { + margin-top: 6px; + display: flex; + flex-direction: column; + @media (min-width: 830px) { + margin-left: 2%; + flex-direction: row; + float: left; + margin-top: 0px; + } + .btn-sm { + font-size: 9px; + @media (min-width: 830px) { + font-size: 14px; + } + } +} From 9e64592acaea9d004832508573a5bfb0f5ecfad7 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 11 Feb 2022 18:29:38 +0900 Subject: [PATCH 13/45] Add mining pool logo in the pool stats page --- frontend/src/app/components/pool/pool.component.html | 3 ++- frontend/src/app/components/pool/pool.component.ts | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index 08e742eb9..90bbc0ffc 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -1,7 +1,8 @@
-

+

+ {{ poolStats.pool.name }}

diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index a89c9a7b4..69b3ac761 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -48,6 +48,11 @@ export class PoolComponent implements OnInit { } return this.apiService.getPoolStats$(this.poolId, params[1] ?? '1w'); }), + map((poolStats) => { + return Object.assign({ + logo: `./resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg' + }, poolStats); + }) ); this.blocks$ = this.fromHeightSubject From 763ea0ce6fba3b00ccd9b5f006b95201b4d40778 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 11 Feb 2022 19:10:29 +0900 Subject: [PATCH 14/45] Show pool addresses in a scrollable div with href --- .../src/app/components/pool/pool.component.html | 15 ++++++++++----- .../src/app/components/pool/pool.component.scss | 9 +++++++++ .../src/app/components/pool/pool.component.ts | 7 +++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index 90bbc0ffc..471cd1449 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -47,21 +47,26 @@
-
+
- - + + + - +
Address{{ poolStats.pool.addresses }}Addresses + + ~
Coinbase TagCoinbase Tags {{ poolStats.pool.regexes }}
-
+
diff --git a/frontend/src/app/components/pool/pool.component.scss b/frontend/src/app/components/pool/pool.component.scss index a1078adc0..271696a39 100644 --- a/frontend/src/app/components/pool/pool.component.scss +++ b/frontend/src/app/components/pool/pool.component.scss @@ -30,3 +30,12 @@ } } } + +div.scrollable { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + overflow: auto; + max-height: 100px; +} \ No newline at end of file diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 69b3ac761..8b0d2fcaf 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -49,6 +49,13 @@ export class PoolComponent implements OnInit { return this.apiService.getPoolStats$(this.poolId, params[1] ?? '1w'); }), map((poolStats) => { + let regexes = '"'; + for (const regex of JSON.parse(poolStats.pool.regexes)) { + regexes += regex + '", "'; + } + poolStats.pool.regexes = regexes.slice(0, -3); + poolStats.pool.addresses = JSON.parse(poolStats.pool.addresses); + return Object.assign({ logo: `./resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg' }, poolStats); From c28f3fd4b6395fd2b4125f98f136c589be1070d7 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 11 Feb 2022 19:12:52 +0900 Subject: [PATCH 15/45] Disable query logger spam --- backend/src/repositories/BlocksRepository.ts | 10 +++++----- backend/src/repositories/PoolsRepository.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 4b480a0a7..17e29fc47 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -44,7 +44,7 @@ class BlocksRepository { block.extras?.reward ?? 0, ]; - logger.debug(query); + // logger.debug(query); await connection.query(query, params); } catch (e: any) { if (e.errno === 1062) { // ER_DUP_ENTRY @@ -102,7 +102,7 @@ class BlocksRepository { query += ` AND blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; } - logger.debug(query); + // logger.debug(query); const connection = await DB.pool.getConnection(); const [rows] = await connection.query(query, params); connection.release(); @@ -134,7 +134,7 @@ class BlocksRepository { query += ` blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; } - logger.debug(query); + // logger.debug(query); const connection = await DB.pool.getConnection(); const [rows] = await connection.query(query, params); connection.release(); @@ -152,7 +152,7 @@ class BlocksRepository { LIMIT 1;`; - logger.debug(query); + // logger.debug(query); const connection = await DB.pool.getConnection(); const [rows]: any[] = await connection.query(query); connection.release(); @@ -185,7 +185,7 @@ class BlocksRepository { query += ` ORDER BY height DESC LIMIT 10`; - logger.debug(query); + // logger.debug(query); const connection = await DB.pool.getConnection(); const [rows] = await connection.query(query, params); connection.release(); diff --git a/backend/src/repositories/PoolsRepository.ts b/backend/src/repositories/PoolsRepository.ts index b94f3d36d..7970f6ff1 100644 --- a/backend/src/repositories/PoolsRepository.ts +++ b/backend/src/repositories/PoolsRepository.ts @@ -41,7 +41,7 @@ class PoolsRepository { query += ` GROUP BY pool_id ORDER BY COUNT(height) DESC`; - logger.debug(query); + // logger.debug(query); const connection = await DB.pool.getConnection(); const [rows] = await connection.query(query); connection.release(); @@ -58,7 +58,7 @@ class PoolsRepository { FROM pools WHERE pools.id = ?`; - logger.debug(query); + // logger.debug(query); const connection = await DB.pool.getConnection(); const [rows] = await connection.query(query, [poolId]); connection.release(); From d8e58ee622b57b817c10703bc05dec31b9e14c65 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 11 Feb 2022 19:27:11 +0900 Subject: [PATCH 16/45] Set reward to 0 by default until reward indexing is available --- backend/src/repositories/BlocksRepository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 17e29fc47..d8465ea71 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -172,7 +172,7 @@ class BlocksRepository { startHeight: number | null = null ): Promise { const params: any[] = []; - let query = `SELECT height, hash, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, reward + let query = `SELECT height, hash, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, 0 as reward FROM blocks WHERE pool_id = ?`; params.push(poolId); From 4f02efd7fee3ae31c97751a97061daf1139f8636 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 14 Feb 2022 13:21:35 +0900 Subject: [PATCH 17/45] Fix block link in pool page - Click on chart slice open pool page --- backend/src/api/mining.ts | 1 - backend/src/repositories/BlocksRepository.ts | 2 +- .../pool-ranking/pool-ranking.component.html | 2 +- .../pool-ranking/pool-ranking.component.ts | 21 ++++++++++++++++--- .../app/components/pool/pool.component.html | 16 +++++++------- .../src/app/interfaces/node-api.interface.ts | 4 ++++ 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index bf8c6b340..2a978868f 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -2,7 +2,6 @@ import { PoolInfo, PoolStats } from '../mempool.interfaces'; import BlocksRepository, { EmptyBlocks } from '../repositories/BlocksRepository'; import PoolsRepository from '../repositories/PoolsRepository'; import bitcoinClient from './bitcoin/bitcoin-client'; -import { Common } from './common'; class Mining { constructor() { diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index d8465ea71..644c6a277 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -172,7 +172,7 @@ class BlocksRepository { startHeight: number | null = null ): Promise { const params: any[] = []; - let query = `SELECT height, hash, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, 0 as reward + let query = `SELECT height, hash as id, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, 0 as reward FROM blocks WHERE pool_id = ?`; params.push(poolId); diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.html b/frontend/src/app/components/pool-ranking/pool-ranking.component.html index 45707d328..1f6fdbc0e 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.html +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.html @@ -1,7 +1,7 @@
-
+
diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts index 27515d503..d1b64f190 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -1,6 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { EChartsOption } from 'echarts'; +import { Router } from '@angular/router'; +import { EChartsOption, PieSeriesOption } from 'echarts'; import { combineLatest, Observable, of } from 'rxjs'; import { catchError, map, share, skip, startWith, switchMap, tap } from 'rxjs/operators'; import { SinglePoolStats } from 'src/app/interfaces/node-api.interface'; @@ -31,6 +32,7 @@ export class PoolRankingComponent implements OnInit, OnDestroy { chartInitOptions = { renderer: 'svg' }; + chartInstance: any = undefined; miningStatsObservable$: Observable; @@ -40,6 +42,7 @@ export class PoolRankingComponent implements OnInit, OnDestroy { private formBuilder: FormBuilder, private miningService: MiningService, private seoService: SeoService, + private router: Router, ) { this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`); this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w'; @@ -107,7 +110,7 @@ export class PoolRankingComponent implements OnInit, OnDestroy { if (parseFloat(pool.share) < poolShareThreshold) { return; } - data.push({ + data.push({ value: pool.share, name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`), label: { @@ -129,7 +132,8 @@ export class PoolRankingComponent implements OnInit, OnDestroy { pool.blockCount.toString() + ` blocks`; } } - } + }, + data: pool.poolId, }); }); return data; @@ -197,6 +201,17 @@ export class PoolRankingComponent implements OnInit, OnDestroy { }; } + onChartInit(ec) { + if (this.chartInstance !== undefined) { + return; + } + + this.chartInstance = ec; + this.chartInstance.on('click', (e) => { + this.router.navigate(['/mining/pool/', e.data.data]); + }) + } + /** * Default mining stats if something goes wrong */ diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index 471cd1449..fff474564 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -47,11 +47,11 @@
-
+
- + - +
AddressesAddresses
{{ address }}
@@ -60,22 +60,22 @@
~
Coinbase TagsCoinbase Tags {{ poolStats.pool.regexes }}
-
+
- - + + - - + +
Mined Blocks{{ poolStats.blockCount }}Mined Blocks{{ poolStats.blockCount }}
Empty Blocks{{ poolStats.emptyBlocks.length }}Empty Blocks{{ poolStats.emptyBlocks.length }}
diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index dfcb5836e..472df0088 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -106,6 +106,10 @@ export interface BlockExtension { reward?: number; coinbaseTx?: Transaction; matchRate?: number; + pool?: { + id: number; + name: string; + } stage?: number; // Frontend only } From 09180c4f9195efb6374ba8fed18308d5be7a8bde Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 14 Feb 2022 13:58:40 +0900 Subject: [PATCH 18/45] Renamed /mining/pool-blocks/xxx -> /mining/pool/:poolId/blocks --- backend/src/index.ts | 6 +++--- frontend/src/app/services/api.service.ts | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 4b0466bf6..1f8575294 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -274,10 +274,10 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/2y', routes.$getPools.bind(routes, '2y')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3y', routes.$getPools.bind(routes, '3y')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/all', routes.$getPools.bind(routes, 'all')) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks', routes.$getPoolBlocks) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks/:height', routes.$getPoolBlocks) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool-blocks/:poolId', routes.$getPoolBlocks) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool-blocks/:poolId/:height', routes.$getPoolBlocks); + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool); } if (config.BISQ.ENABLED) { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index c1d42fd50..ec01a2a5c 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -139,9 +139,11 @@ export class ApiService { getPoolBlocks$(poolId: number, fromHeight: number): Observable { if (fromHeight !== undefined) { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath +`/api/v1/mining/pool-blocks/${poolId}/${fromHeight}`); + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + + `/api/v1/mining/pool/${poolId}/blocks/${fromHeight}`); } else { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath +`/api/v1/mining/pool-blocks/${poolId}`); + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + + `/api/v1/mining/pool/${poolId}/blocks`); } } } From a436d3a173899306eea3106716a1e00a8ceedd71 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 14 Feb 2022 14:03:05 +0900 Subject: [PATCH 19/45] Fix label width being too small on mobile --- frontend/src/app/components/pool/pool.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index fff474564..e10b8e6e4 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -51,7 +51,7 @@ - + - + @@ -70,11 +70,11 @@
AddressesAddresses
{{ address }}
@@ -60,7 +60,7 @@
~
Coinbase TagsCoinbase Tags {{ poolStats.pool.regexes }}
- + - + From f381da0f78d4fa873b6ed3ee54a6fb2cb06a5f19 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 14 Feb 2022 14:11:55 +0900 Subject: [PATCH 20/45] Show correct reward in pool stat page --- backend/src/repositories/BlocksRepository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 644c6a277..6e74e9435 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -172,7 +172,7 @@ class BlocksRepository { startHeight: number | null = null ): Promise { const params: any[] = []; - let query = `SELECT height, hash as id, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, 0 as reward + let query = `SELECT height, hash as id, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, reward FROM blocks WHERE pool_id = ?`; params.push(poolId); From 87170247bd237c2bc90eaf94e9333cd0e3201118 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 11 Feb 2022 20:35:53 +0900 Subject: [PATCH 21/45] Revert "Merge pull request #1240 from nymkappa/feature/mempool-sync-threshold" This reverts commit 2f921f4cc73994cc9ea9c317c8897de0d1881340, reversing changes made to 877be47e5be03e31b25cc12fbe2ee45be881e5d8. --- backend/src/api/mempool.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 3a389c059..a8b0b461a 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -168,8 +168,7 @@ class Mempool { const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx)); this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6); - const syncedThreshold = 0.99; // If we synced 99% of the mempool tx count, consider we're synced - if (!this.inSync && Object.keys(this.mempoolCache).length >= transactions.length * syncedThreshold) { + if (!this.inSync && transactions.length === Object.keys(this.mempoolCache).length) { this.inSync = true; logger.notice('The mempool is now in sync!'); loadingIndicators.setProgress('mempool', 100); From a88d6d2fca401e94d8e08047d538b94847170f65 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 11 Feb 2022 21:25:58 +0900 Subject: [PATCH 22/45] Don't wait for 100% mempool sync before starting block indexing --- backend/src/api/blocks.ts | 6 +++--- backend/src/api/mempool.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index d2487414f..affbf0049 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -61,7 +61,7 @@ class Blocks { // optimize here by directly fetching txs in the "outdated" mempool transactions.push(mempool[txIds[i]]); transactionsFound++; - } else if (config.MEMPOOL.BACKEND === 'esplora' || memPool.isInSync() || i === 0) { + } else if (config.MEMPOOL.BACKEND === 'esplora' || !memPool.hasPriority() || i === 0) { // Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...) if (!quiet && (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length)) { // Avoid log spam logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`); @@ -175,7 +175,7 @@ class Blocks { public async $generateBlockDatabase() { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || // Bitcoin only config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === 0 || // Indexing of older blocks must be enabled - !memPool.isInSync() || // We sync the mempool first + memPool.hasPriority() || // We sync the mempool first this.blockIndexingStarted === true || // Indexing must not already be in progress config.DATABASE.ENABLED === false ) { @@ -314,7 +314,7 @@ class Blocks { if (this.newBlockCallbacks.length) { this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions)); } - if (memPool.isInSync()) { + if (!memPool.hasPriority()) { diskCache.$saveCacheToDisk(); } } diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index a8b0b461a..64505ba2b 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -13,6 +13,7 @@ class Mempool { private static WEBSOCKET_REFRESH_RATE_MS = 10000; private static LAZY_DELETE_AFTER_SECONDS = 30; private inSync: boolean = false; + private mempoolCacheDelta: number = -1; private mempoolCache: { [txId: string]: TransactionExtended } = {}; private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 }; @@ -32,6 +33,17 @@ class Mempool { setInterval(this.deleteExpiredTransactions.bind(this), 20000); } + /** + * Return true if we should leave resources available for mempool tx caching + */ + public hasPriority(): boolean { + if (this.inSync) { + return false; + } else { + return this.mempoolCacheDelta == -1 || this.mempoolCacheDelta > 25; + } + } + public isInSync(): boolean { return this.inSync; } @@ -100,6 +112,8 @@ class Mempool { const diff = transactions.length - currentMempoolSize; const newTransactions: TransactionExtended[] = []; + this.mempoolCacheDelta = Math.abs(diff); + if (!this.inSync) { loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100); } @@ -174,6 +188,8 @@ class Mempool { loadingIndicators.setProgress('mempool', 100); } + this.mempoolCacheDelta = Math.abs(transactions.length - Object.keys(this.mempoolCache).length); + if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions); } From 1c928582a2672dcd010403b79eb7c4e43bd9a863 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 15 Feb 2022 15:56:40 +0900 Subject: [PATCH 23/45] Revert "Update tests - Replace button click blocks -> pools" This reverts commit 73019b485fefda0be8e5b6e0fc0e0569721879b5. --- .../integration/mainnet/mainnet.spec.ts | 96 ------------------- .../cypress/integration/signet/signet.spec.ts | 4 +- .../integration/testnet/testnet.spec.ts | 4 +- 3 files changed, 4 insertions(+), 100 deletions(-) diff --git a/frontend/cypress/integration/mainnet/mainnet.spec.ts b/frontend/cypress/integration/mainnet/mainnet.spec.ts index 752617092..34e5bfac9 100644 --- a/frontend/cypress/integration/mainnet/mainnet.spec.ts +++ b/frontend/cypress/integration/mainnet/mainnet.spec.ts @@ -274,102 +274,6 @@ describe('Mainnet', () => { }); }); }); - }); - }); - - - it('loads skeleton when changes between networks', () => { - cy.visit('/'); - cy.waitForSkeletonGone(); - - cy.changeNetwork("testnet"); - cy.changeNetwork("signet"); - cy.changeNetwork("mainnet"); - }); - - it.skip('loads the dashboard with the skeleton blocks', () => { - cy.mockMempoolSocket(); - cy.visit("/"); - cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible'); - cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible'); - cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible'); - cy.get('#mempool-block-0').should('be.visible'); - cy.get('#mempool-block-1').should('be.visible'); - cy.get('#mempool-block-2').should('be.visible'); - - emitMempoolInfo({ - 'params': { - command: 'init' - } - }); - - cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist'); - cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist'); - cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist'); - }); - - it('loads the pools screen', () => { - cy.visit('/'); - cy.waitForSkeletonGone(); - cy.get('#btn-pools').click().then(() => { - cy.waitForPageIdle(); - }); - }); - - it('loads the graphs screen', () => { - cy.visit('/'); - cy.waitForSkeletonGone(); - cy.get('#btn-graphs').click().then(() => { - cy.wait(1000); - }); - }); - - describe('graphs page', () => { - it('check buttons - mobile', () => { - cy.viewport('iphone-6'); - cy.visit('/graphs'); - cy.waitForSkeletonGone(); - cy.get('.small-buttons > :nth-child(2)').should('be.visible'); - cy.get('#dropdownFees').should('be.visible'); - cy.get('.btn-group').should('be.visible'); - }); - it('check buttons - tablet', () => { - cy.viewport('ipad-2'); - cy.visit('/graphs'); - cy.waitForSkeletonGone(); - cy.get('.small-buttons > :nth-child(2)').should('be.visible'); - cy.get('#dropdownFees').should('be.visible'); - cy.get('.btn-group').should('be.visible'); - }); - it('check buttons - desktop', () => { - cy.viewport('macbook-16'); - cy.visit('/graphs'); - cy.waitForSkeletonGone(); - cy.get('.small-buttons > :nth-child(2)').should('be.visible'); - cy.get('#dropdownFees').should('be.visible'); - cy.get('.btn-group').should('be.visible'); - }); - }); - - it('loads the tv screen - desktop', () => { - cy.viewport('macbook-16'); - cy.visit('/'); - cy.waitForSkeletonGone(); - cy.get('#btn-tv').click().then(() => { - cy.viewport('macbook-16'); - cy.get('.chart-holder'); - cy.get('.blockchain-wrapper').should('be.visible'); - cy.get('#mempool-block-0').should('be.visible'); - }); - }); - - it('loads the tv screen - mobile', () => { - cy.viewport('iphone-6'); - cy.visit('/tv'); - cy.waitForSkeletonGone(); - cy.get('.chart-holder'); - cy.get('.blockchain-wrapper').should('not.visible'); - }); it('loads genesis block and click on the arrow left', () => { cy.viewport('macbook-16'); diff --git a/frontend/cypress/integration/signet/signet.spec.ts b/frontend/cypress/integration/signet/signet.spec.ts index d2bbd1196..9ebf67b81 100644 --- a/frontend/cypress/integration/signet/signet.spec.ts +++ b/frontend/cypress/integration/signet/signet.spec.ts @@ -44,10 +44,10 @@ describe('Signet', () => { cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist'); }); - it('loads the pools screen', () => { + it('loads the blocks screen', () => { cy.visit('/signet'); cy.waitForSkeletonGone(); - cy.get('#btn-pools').click().then(() => { + cy.get('#btn-blocks').click().then(() => { cy.wait(1000); }); }); diff --git a/frontend/cypress/integration/testnet/testnet.spec.ts b/frontend/cypress/integration/testnet/testnet.spec.ts index c0c07aa74..6f3264244 100644 --- a/frontend/cypress/integration/testnet/testnet.spec.ts +++ b/frontend/cypress/integration/testnet/testnet.spec.ts @@ -44,10 +44,10 @@ describe('Testnet', () => { cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist'); }); - it('loads the pools screen', () => { + it('loads the blocks screen', () => { cy.visit('/testnet'); cy.waitForSkeletonGone(); - cy.get('#btn-pools').click().then(() => { + cy.get('#btn-blocks').click().then(() => { cy.wait(1000); }); }); From e9ba38755c04638c8fc9f9a9eb5e72a92c4d9131 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 15 Feb 2022 16:19:58 +0900 Subject: [PATCH 24/45] Re-apply test updates from bogus commit 73019b485fefda0be8e5b6e0fc0e0569721879b5 --- .../integration/mainnet/mainnet.spec.ts | 22 ++++++++++--------- .../cypress/integration/signet/signet.spec.ts | 4 ++-- .../integration/testnet/testnet.spec.ts | 4 ++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/frontend/cypress/integration/mainnet/mainnet.spec.ts b/frontend/cypress/integration/mainnet/mainnet.spec.ts index 34e5bfac9..473c480f4 100644 --- a/frontend/cypress/integration/mainnet/mainnet.spec.ts +++ b/frontend/cypress/integration/mainnet/mainnet.spec.ts @@ -275,16 +275,18 @@ describe('Mainnet', () => { }); }); - it('loads genesis block and click on the arrow left', () => { - cy.viewport('macbook-16'); - cy.visit('/block/0'); - cy.waitForSkeletonGone(); - cy.waitForPageIdle(); - cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible'); - cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist'); - cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').click().then(() => { - cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible'); - cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible'); + it('loads genesis block and click on the arrow left', () => { + cy.viewport('macbook-16'); + cy.visit('/block/0'); + cy.waitForSkeletonGone(); + cy.waitForPageIdle(); + cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible'); + cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist'); + cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').click().then(() => { + cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible'); + cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible'); + }); + }); }); }); diff --git a/frontend/cypress/integration/signet/signet.spec.ts b/frontend/cypress/integration/signet/signet.spec.ts index 9ebf67b81..d2bbd1196 100644 --- a/frontend/cypress/integration/signet/signet.spec.ts +++ b/frontend/cypress/integration/signet/signet.spec.ts @@ -44,10 +44,10 @@ describe('Signet', () => { cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist'); }); - it('loads the blocks screen', () => { + it('loads the pools screen', () => { cy.visit('/signet'); cy.waitForSkeletonGone(); - cy.get('#btn-blocks').click().then(() => { + cy.get('#btn-pools').click().then(() => { cy.wait(1000); }); }); diff --git a/frontend/cypress/integration/testnet/testnet.spec.ts b/frontend/cypress/integration/testnet/testnet.spec.ts index 6f3264244..c0c07aa74 100644 --- a/frontend/cypress/integration/testnet/testnet.spec.ts +++ b/frontend/cypress/integration/testnet/testnet.spec.ts @@ -44,10 +44,10 @@ describe('Testnet', () => { cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist'); }); - it('loads the blocks screen', () => { + it('loads the pools screen', () => { cy.visit('/testnet'); cy.waitForSkeletonGone(); - cy.get('#btn-blocks').click().then(() => { + cy.get('#btn-pools').click().then(() => { cy.wait(1000); }); }); From 1e96c93557dafe2eef5460ceb23e5c1b09f3e741 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 15 Feb 2022 18:36:58 +0900 Subject: [PATCH 25/45] Fix rendering issue when clicking on block link from pool page --- frontend/src/app/components/pool/pool.component.html | 2 +- frontend/src/app/components/pool/pool.component.ts | 11 +++++------ frontend/src/app/services/api.service.ts | 11 ++++------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index e10b8e6e4..43bc647e8 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -95,7 +95,7 @@ - + diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 8b0d2fcaf..230b9a0f8 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -1,9 +1,8 @@ -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; -import { once } from 'process'; -import { BehaviorSubject, combineLatest, from, merge, Observable } from 'rxjs'; -import { delay, distinctUntilChanged, map, scan, startWith, switchMap, tap } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { distinctUntilChanged, map, startWith, switchMap, tap } from 'rxjs/operators'; import { BlockExtended, PoolStat } from 'src/app/interfaces/node-api.interface'; import { ApiService } from 'src/app/services/api.service'; import { StateService } from 'src/app/services/state.service'; @@ -11,7 +10,8 @@ import { StateService } from 'src/app/services/state.service'; @Component({ selector: 'app-pool', templateUrl: './pool.component.html', - styleUrls: ['./pool.component.scss'] + styleUrls: ['./pool.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class PoolComponent implements OnInit { poolStats$: Observable; @@ -22,7 +22,6 @@ export class PoolComponent implements OnInit { blocks: BlockExtended[] = []; poolId: number = undefined; - isLoading = false; radioGroupForm: FormGroup; constructor( diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index ec01a2a5c..bf468c467 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -138,12 +138,9 @@ export class ApiService { } getPoolBlocks$(poolId: number, fromHeight: number): Observable { - if (fromHeight !== undefined) { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + - `/api/v1/mining/pool/${poolId}/blocks/${fromHeight}`); - } else { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + - `/api/v1/mining/pool/${poolId}/blocks`); - } + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}/blocks` + + (fromHeight !== undefined ? `/${fromHeight}` : '') + ); } } From fa8607c57d30880abcb17b8a5ed69db462da7012 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 15 Feb 2022 18:45:53 +0900 Subject: [PATCH 26/45] [Pool page] - Parse regexes and addresses in the backend --- backend/src/repositories/PoolsRepository.ts | 3 +++ frontend/src/app/components/pool/pool.component.ts | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/repositories/PoolsRepository.ts b/backend/src/repositories/PoolsRepository.ts index 7970f6ff1..a7b716da7 100644 --- a/backend/src/repositories/PoolsRepository.ts +++ b/backend/src/repositories/PoolsRepository.ts @@ -63,6 +63,9 @@ class PoolsRepository { const [rows] = await connection.query(query, [poolId]); connection.release(); + rows[0].regexes = JSON.parse(rows[0].regexes); + rows[0].addresses = JSON.parse(rows[0].addresses); + return rows[0]; } } diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 230b9a0f8..9d094dce0 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -49,11 +49,11 @@ export class PoolComponent implements OnInit { }), map((poolStats) => { let regexes = '"'; - for (const regex of JSON.parse(poolStats.pool.regexes)) { + for (const regex of poolStats.pool.regexes) { regexes += regex + '", "'; } poolStats.pool.regexes = regexes.slice(0, -3); - poolStats.pool.addresses = JSON.parse(poolStats.pool.addresses); + poolStats.pool.addresses = poolStats.pool.addresses; return Object.assign({ logo: `./resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg' From ca766bf40d53c0f839eb1cc8f41163db3ec7b7ff Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 15 Feb 2022 16:02:30 +0900 Subject: [PATCH 27/45] Provide a way to completely disable block indexing and mining menu --- backend/src/api/blocks.ts | 24 +++++-------------- backend/src/api/common.ts | 8 +++++++ frontend/mempool-frontend-config.sample.json | 3 ++- .../master-page/master-page.component.html | 5 +++- .../master-page/master-page.component.ts | 2 +- frontend/src/app/services/state.service.ts | 2 ++ 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index affbf0049..7513f259e 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -116,10 +116,7 @@ class Blocks { blockExtended.extras.feeRange = transactionsTmp.length > 0 ? Common.getFeesInRange(transactionsTmp, 8) : [0, 0]; - const indexingAvailable = - ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && - config.DATABASE.ENABLED === true; - if (indexingAvailable) { + if (Common.indexingEnabled()) { let pool: PoolTag; if (blockExtended.extras?.coinbaseTx !== undefined) { pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx); @@ -173,11 +170,9 @@ class Blocks { * Index all blocks metadata for the mining dashboard */ public async $generateBlockDatabase() { - if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || // Bitcoin only - config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === 0 || // Indexing of older blocks must be enabled - memPool.hasPriority() || // We sync the mempool first - this.blockIndexingStarted === true || // Indexing must not already be in progress - config.DATABASE.ENABLED === false + if (this.blockIndexingStarted === true || + !Common.indexingEnabled() || + memPool.hasPriority() ) { return; } @@ -293,10 +288,7 @@ class Blocks { const transactions = await this.$getTransactionsExtended(blockHash, block.height, false); const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions); - const indexingAvailable = - ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && - config.DATABASE.ENABLED === true; - if (indexingAvailable) { + if (Common.indexingEnabled()) { await blocksRepository.$saveBlockInDatabase(blockExtended); } @@ -340,10 +332,6 @@ class Blocks { } public async $getBlocksExtras(fromHeight: number): Promise { - const indexingAvailable = - ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && - config.DATABASE.ENABLED === true; - try { loadingIndicators.setProgress('blocks', 0); @@ -366,7 +354,7 @@ class Blocks { let nextHash = startFromHash; for (let i = 0; i < 10 && currentHeight >= 0; i++) { let block = this.getBlocks().find((b) => b.height === currentHeight); - if (!block && indexingAvailable) { + if (!block && Common.indexingEnabled()) { block = this.prepareBlock(await this.$indexBlock(currentHeight)); } else if (!block) { block = this.prepareBlock(await bitcoinApi.$getBlock(nextHash)); diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 5e99e870c..3ab5c4b9f 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -154,4 +154,12 @@ export class Common { }); return parents; } + + static indexingEnabled(): boolean { + return ( + ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && + config.DATABASE.ENABLED === true && + config.MEMPOOL.INDEXING_BLOCKS_AMOUNT != 0 + ); + } } diff --git a/frontend/mempool-frontend-config.sample.json b/frontend/mempool-frontend-config.sample.json index 0715cb0bd..231f1c7c8 100644 --- a/frontend/mempool-frontend-config.sample.json +++ b/frontend/mempool-frontend-config.sample.json @@ -15,5 +15,6 @@ "BASE_MODULE": "mempool", "MEMPOOL_WEBSITE_URL": "https://mempool.space", "LIQUID_WEBSITE_URL": "https://liquid.network", - "BISQ_WEBSITE_URL": "https://bisq.markets" + "BISQ_WEBSITE_URL": "https://bisq.markets", + "MINING_DASHBOARD": true } diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 4624340d3..af47b75c2 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -31,9 +31,12 @@ - + diff --git a/frontend/src/app/components/master-page/master-page.component.ts b/frontend/src/app/components/master-page/master-page.component.ts index fcff5629c..23a73178f 100644 --- a/frontend/src/app/components/master-page/master-page.component.ts +++ b/frontend/src/app/components/master-page/master-page.component.ts @@ -18,7 +18,7 @@ export class MasterPageComponent implements OnInit { urlLanguage: string; constructor( - private stateService: StateService, + public stateService: StateService, private languageService: LanguageService, ) { } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 230c9b150..14d67e765 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -36,6 +36,7 @@ export interface Env { MEMPOOL_WEBSITE_URL: string; LIQUID_WEBSITE_URL: string; BISQ_WEBSITE_URL: string; + MINING_DASHBOARD: boolean; } const defaultEnv: Env = { @@ -59,6 +60,7 @@ const defaultEnv: Env = { 'MEMPOOL_WEBSITE_URL': 'https://mempool.space', 'LIQUID_WEBSITE_URL': 'https://liquid.network', 'BISQ_WEBSITE_URL': 'https://bisq.markets', + 'MINING_DASHBOARD': true }; @Injectable({ From 0c1fa2b4aa22a79e674fb99b17ad41b753dad749 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 16 Feb 2022 15:19:16 +0900 Subject: [PATCH 28/45] Cleanup blocks/pools fields data type - Index more block data --- backend/src/api/database-migration.ts | 30 ++++++++++++++++---- backend/src/repositories/BlocksRepository.ts | 15 +++++++--- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index b8557ff65..f83f95034 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 = 5; + private static currentVersion = 6; private queryTimeout = 120000; private statisticsAddedIndexed = false; @@ -76,6 +76,7 @@ class DatabaseMigration { private async $createMissingTablesAndIndexes(databaseSchemaVersion: number) { await this.$setStatisticsAddedIndexedFlag(databaseSchemaVersion); + const isBitcoin = ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK); const connection = await DB.pool.getConnection(); try { await this.$executeQuery(connection, this.getCreateElementsTableQuery(), await this.$checkIfTableExists('elements_pegs')); @@ -90,6 +91,29 @@ class DatabaseMigration { await this.$executeQuery(connection, 'DROP table IF EXISTS blocks;'); await this.$executeQuery(connection, this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks')); } + if (databaseSchemaVersion < 5 && isBitcoin === true) { + await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"'); + } + + if (databaseSchemaVersion < 6 && isBitcoin === true) { + // Cleanup original blocks fields type + await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"'); + // We also fix the pools.id type so we need to drop/re-create the foreign key + await this.$executeQuery(connection, 'ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`'); + await this.$executeQuery(connection, 'ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT'); + await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL'); + await this.$executeQuery(connection, 'ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)'); + // Add new block indexing fields + await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""'); + await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL'); + } connection.release(); } catch (e) { connection.release(); @@ -229,10 +253,6 @@ class DatabaseMigration { } } - if (version < 5 && (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === true)) { - queries.push('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"'); - } - return queries; } diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 6e74e9435..adc3a1f31 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -20,12 +20,14 @@ class BlocksRepository { height, hash, blockTimestamp, size, weight, tx_count, coinbase_raw, difficulty, pool_id, fees, fee_span, median_fee, - reward + reward, version, bits, nonce, + merkle_root, previous_block_hash ) VALUE ( ?, ?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, - ? + ?, ?, ?, ?, + ?, ? )`; const params: any[] = [ @@ -37,11 +39,16 @@ class BlocksRepository { block.tx_count, '', block.difficulty, - block.extras?.pool?.id, // Should always be set to something + block.extras.pool?.id, // Should always be set to something 0, '[]', block.extras.medianFee ?? 0, - block.extras?.reward ?? 0, + block.extras.reward ?? 0, + block.version, + block.bits, + block.nonce, + block.merkle_root, + block.previousblockhash ]; // logger.debug(query); From 7270b1ccacdcbdfbbdaebff914d6d15331b2c965 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 16 Feb 2022 21:20:28 +0900 Subject: [PATCH 29/45] Create difficulty chart component --- backend/src/api/blocks.ts | 10 +- backend/src/index.ts | 9 +- backend/src/repositories/BlocksRepository.ts | 24 +++++ backend/src/routes.ts | 12 +++ frontend/src/app/app-routing.module.ts | 13 +++ frontend/src/app/app.module.ts | 2 + .../difficulty-chart.component.html | 8 ++ .../difficulty-chart.component.scss | 0 .../difficulty-chart.component.ts | 102 ++++++++++++++++++ .../pool-ranking/pool-ranking.component.ts | 7 +- frontend/src/app/services/api.service.ts | 21 +++- 11 files changed, 192 insertions(+), 16 deletions(-) create mode 100644 frontend/src/app/components/difficulty-chart/difficulty-chart.component.html create mode 100644 frontend/src/app/components/difficulty-chart/difficulty-chart.component.scss create mode 100644 frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 7513f259e..c406ae803 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -199,6 +199,7 @@ class Blocks { const chunkSize = 10000; let totaIndexed = 0; + let indexedThisRun = 0; while (currentBlockHeight >= lastBlockToIndex) { const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1); @@ -207,9 +208,11 @@ class Blocks { if (missingBlockHeights.length <= 0) { logger.debug(`No missing blocks between #${currentBlockHeight} to #${endBlock}`); currentBlockHeight -= chunkSize; + totaIndexed += chunkSize; continue; } + totaIndexed += chunkSize - missingBlockHeights.length; logger.debug(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`); for (const blockHeight of missingBlockHeights) { @@ -219,8 +222,10 @@ class Blocks { try { if (totaIndexed % 100 === 0 || blockHeight === lastBlockToIndex) { const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); - const blockPerSeconds = Math.round(totaIndexed / elapsedSeconds); - logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds} blocks/sec | total: ${totaIndexed} | elapsed: ${elapsedSeconds} seconds`); + const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); + const progress = Math.round(totaIndexed / indexingBlockAmount * 100); + const timeLeft = Math.round((indexingBlockAmount - totaIndexed) / blockPerSeconds); + logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds} blocks/sec | total: ${totaIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${elapsedSeconds} seconds | left: ~${timeLeft} seconds`); } const blockHash = await bitcoinApi.$getBlockHash(blockHeight); const block = await bitcoinApi.$getBlock(blockHash); @@ -228,6 +233,7 @@ class Blocks { const blockExtended = await this.$getBlockExtended(block, transactions); await blocksRepository.$saveBlockInDatabase(blockExtended); ++totaIndexed; + ++indexedThisRun; } catch (e) { logger.err(`Something went wrong while indexing blocks.` + e); } diff --git a/backend/src/index.ts b/backend/src/index.ts index 1f8575294..23c70f59d 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -259,10 +259,7 @@ class Server { ; } - const indexingAvailable = - ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && - config.DATABASE.ENABLED === true; - if (indexingAvailable) { + if (Common.indexingEnabled()) { this.app .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/24h', routes.$getPools.bind(routes, '24h')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3d', routes.$getPools.bind(routes, '3d')) @@ -277,7 +274,9 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks', routes.$getPoolBlocks) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks/:height', routes.$getPoolBlocks) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool); + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty', routes.$getHistoricalDifficulty) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty/:interval', routes.$getHistoricalDifficulty); } if (config.BISQ.ENABLED) { diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index adc3a1f31..d57bc8eb0 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -223,6 +223,30 @@ class BlocksRepository { return rows[0]; } + + /** + * Return blocks difficulty + */ + public async $getBlocksDifficulty(interval: string | null): Promise { + interval = Common.getSqlInterval(interval); + + const connection = await DB.pool.getConnection(); + + let query = `SELECT MIN(blockTimestamp) as timestamp, difficulty + FROM blocks`; + + if (interval) { + query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; + } + + query += ` GROUP BY difficulty + ORDER BY blockTimestamp DESC`; + + const [rows]: any[] = await connection.query(query); + connection.release(); + + return rows; + } } export default new BlocksRepository(); diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 66ecebd31..2159c0721 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -575,6 +575,18 @@ class Routes { } } + public async $getHistoricalDifficulty(req: Request, res: Response) { + try { + const stats = await BlocksRepository.$getBlocksDifficulty(req.params.interval ?? null); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); + res.json(stats); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + public async getBlock(req: Request, res: Response) { try { const result = await bitcoinApi.$getBlock(req.params.hash); diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 4018ed64e..6ce39b1d2 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -27,6 +27,7 @@ import { AssetGroupComponent } from './components/assets/asset-group/asset-group import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; import { AssetsComponent } from './components/assets/assets.component'; import { PoolComponent } from './components/pool/pool.component'; +import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component'; let routes: Routes = [ { @@ -63,6 +64,10 @@ let routes: Routes = [ path: 'blocks', component: LatestBlocksComponent, }, + { + path: 'mining/difficulty', + component: DifficultyChartComponent, + }, { path: 'mining/pools', component: PoolRankingComponent, @@ -155,6 +160,10 @@ let routes: Routes = [ path: 'blocks', component: LatestBlocksComponent, }, + { + path: 'mining/difficulty', + component: DifficultyChartComponent, + }, { path: 'mining/pools', component: PoolRankingComponent, @@ -241,6 +250,10 @@ let routes: Routes = [ path: 'blocks', component: LatestBlocksComponent, }, + { + path: 'mining/difficulty', + component: DifficultyChartComponent, + }, { path: 'mining/pools', component: PoolRankingComponent, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 20eb2ea03..15bebd033 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -68,6 +68,7 @@ import { PushTransactionComponent } from './components/push-transaction/push-tra import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component'; +import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component'; @NgModule({ declarations: [ @@ -118,6 +119,7 @@ import { AssetGroupComponent } from './components/assets/asset-group/asset-group AssetsNavComponent, AssetsFeaturedComponent, AssetGroupComponent, + DifficultyChartComponent, ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html new file mode 100644 index 000000000..0a9a60620 --- /dev/null +++ b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html @@ -0,0 +1,8 @@ +
+ +
+
+
+
+ +
diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.scss b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts new file mode 100644 index 000000000..635a8a6d9 --- /dev/null +++ b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts @@ -0,0 +1,102 @@ +import { Component, OnInit } from '@angular/core'; +import { EChartsOption } from 'echarts'; +import { Observable } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; +import { ApiService } from 'src/app/services/api.service'; +import { SeoService } from 'src/app/services/seo.service'; + +@Component({ + selector: 'app-difficulty-chart', + templateUrl: './difficulty-chart.component.html', + styleUrls: ['./difficulty-chart.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 38%; + left: calc(50% - 15px); + z-index: 100; + } + `], +}) +export class DifficultyChartComponent implements OnInit { + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg' + }; + + difficultyObservable$: Observable; + isLoading = true; + + constructor( + private seoService: SeoService, + private apiService: ApiService, + ) { + this.seoService.setTitle($localize`:@@mining.difficulty:Difficulty`); + } + + ngOnInit(): void { + this.difficultyObservable$ = this.apiService.getHistoricalDifficulty$(undefined) + .pipe( + map(data => { + return data.map(val => [val.timestamp, val.difficulty]) + }), + tap(data => { + this.prepareChartOptions(data); + this.isLoading = false; + }) + ) + } + + prepareChartOptions(data) { + this.chartOptions = { + title: { + text: $localize`:@@mining.difficulty:Difficulty`, + left: 'center', + textStyle: { + color: '#FFF', + }, + }, + tooltip: { + show: true, + trigger: 'axis', + }, + axisPointer: { + type: 'line', + }, + xAxis: [ + { + type: 'time', + } + ], + yAxis: { + type: 'value', + axisLabel: { + fontSize: 11, + formatter: function(val) { + const diff = val / Math.pow(10, 12); // terra + return diff.toString() + 'T'; + } + }, + splitLine: { + lineStyle: { + type: 'dotted', + color: '#ffffff66', + opacity: 0.25, + } + } + }, + series: [ + { + data: data, + type: 'line', + smooth: false, + lineStyle: { + width: 3, + }, + areaStyle: {} + }, + ], + }; + } + +} diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts index d1b64f190..9a7a33fc0 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { EChartsOption, PieSeriesOption } from 'echarts'; @@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service'; } `], }) -export class PoolRankingComponent implements OnInit, OnDestroy { +export class PoolRankingComponent implements OnInit { poolsWindowPreference: string; radioGroupForm: FormGroup; @@ -90,9 +90,6 @@ export class PoolRankingComponent implements OnInit, OnDestroy { ); } - ngOnDestroy(): void { - } - formatPoolUI(pool: SinglePoolStats) { pool['blockText'] = pool.blockCount.toString() + ` (${pool.share}%)`; return pool; diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index bf468c467..950a99fa7 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -129,12 +129,18 @@ export class ApiService { return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'}); } - listPools$(interval: string | null) : Observable { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools/${interval}`); + listPools$(interval: string | undefined) : Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools` + + (interval !== undefined ? `/${interval}` : '') + ); } - getPoolStats$(poolId: number, interval: string | null): Observable { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}/${interval}`); + getPoolStats$(poolId: number, interval: string | undefined): Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}` + + (interval !== undefined ? `/${interval}` : '') + ); } getPoolBlocks$(poolId: number, fromHeight: number): Observable { @@ -143,4 +149,11 @@ export class ApiService { (fromHeight !== undefined ? `/${fromHeight}` : '') ); } + + getHistoricalDifficulty$(interval: string | undefined): Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty` + + (interval !== undefined ? `/${interval}` : '') + ); + } } From 9fa7e58d827e3a6bee2a3c87794428924030c11d Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 16 Feb 2022 22:56:06 +0900 Subject: [PATCH 30/45] Show all difficulty adjustment in a table - Need pagination --- backend/src/repositories/BlocksRepository.ts | 2 +- .../difficulty-chart.component.html | 19 +++++++++++++++ .../difficulty-chart.component.ts | 24 +++++++++++++++---- .../pool-ranking/pool-ranking.component.html | 2 +- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index d57bc8eb0..5d2cdbe36 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -232,7 +232,7 @@ class BlocksRepository { const connection = await DB.pool.getConnection(); - let query = `SELECT MIN(blockTimestamp) as timestamp, difficulty + let query = `SELECT MIN(blockTimestamp) as timestamp, difficulty, height FROM blocks`; if (interval) { diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html index 0a9a60620..b63ba29f5 100644 --- a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html +++ b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html @@ -5,4 +5,23 @@
+
Mined BlocksMined Blocks {{ poolStats.blockCount }}
Empty BlocksEmpty Blocks {{ poolStats.emptyBlocks.length }}
{{ block.height }}{{ block.height }} ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
+ + + + + + + + + + + + + + + + +
BlockTimestampDifficultyChange
{{ change[2] }}‎{{ change[0] | date:'yyyy-MM-dd HH:mm' }}{{ formatNumber(change[1], locale, '1.2-2') }}{{ formatNumber(change[3], locale, '1.2-2') }}%
+
diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts index 635a8a6d9..0e343843d 100644 --- a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts +++ b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts @@ -1,9 +1,10 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; import { EChartsOption } from 'echarts'; import { Observable } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; +import { map, share, tap } from 'rxjs/operators'; import { ApiService } from 'src/app/services/api.service'; import { SeoService } from 'src/app/services/seo.service'; +import { formatNumber } from "@angular/common"; @Component({ selector: 'app-difficulty-chart', @@ -26,8 +27,10 @@ export class DifficultyChartComponent implements OnInit { difficultyObservable$: Observable; isLoading = true; + formatNumber = formatNumber; constructor( + @Inject(LOCALE_ID) public locale: string, private seoService: SeoService, private apiService: ApiService, ) { @@ -38,12 +41,25 @@ export class DifficultyChartComponent implements OnInit { this.difficultyObservable$ = this.apiService.getHistoricalDifficulty$(undefined) .pipe( map(data => { - return data.map(val => [val.timestamp, val.difficulty]) + let formatted = []; + for (let i = 0; i < data.length - 1; ++i) { + const change = (data[i].difficulty / data[i + 1].difficulty - 1) * 100; + formatted.push([ + data[i].timestamp, + data[i].difficulty, + data[i].height, + formatNumber(change, this.locale, '1.2-2'), + change, + formatNumber(data[i].difficulty, this.locale, '1.2-2'), + ]); + } + return formatted; }), tap(data => { this.prepareChartOptions(data); this.isLoading = false; - }) + }), + share() ) } diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.html b/frontend/src/app/components/pool-ranking/pool-ranking.component.html index 1f6fdbc0e..ac59ab2d2 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.html +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.html @@ -43,7 +43,7 @@
- +
From f45103e7e34ca562a1f0e8129060c87456815ef1 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 17 Feb 2022 09:41:05 +0900 Subject: [PATCH 31/45] Add difficulty chart timespan selection --- backend/src/api/mining.ts | 13 ++++ backend/src/repositories/BlocksRepository.ts | 2 +- backend/src/routes.ts | 2 +- .../difficulty-chart.component.html | 37 +++++++++-- .../difficulty-chart.component.ts | 64 +++++++++++-------- .../pool-ranking/pool-ranking.component.ts | 14 ++-- frontend/src/app/services/api.service.ts | 2 +- 7 files changed, 93 insertions(+), 41 deletions(-) diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index 2a978868f..beca52893 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -69,6 +69,19 @@ class Mining { emptyBlocks: emptyBlocks, }; } + + /** + * Return the historical difficulty adjustments and oldest indexed block timestamp + */ + public async $getHistoricalDifficulty(interval: string | null): Promise { + const difficultyAdjustments = await BlocksRepository.$getBlocksDifficulty(interval); + const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp()); + + return { + adjustments: difficultyAdjustments, + oldestIndexedBlockTimestamp: oldestBlock.getTime(), + } + } } export default new Mining(); diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 5d2cdbe36..ac0ea25bc 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -232,7 +232,7 @@ class BlocksRepository { const connection = await DB.pool.getConnection(); - let query = `SELECT MIN(blockTimestamp) as timestamp, difficulty, height + let query = `SELECT MIN(UNIX_TIMESTAMP(blockTimestamp)) as timestamp, difficulty, height FROM blocks`; if (interval) { diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 2159c0721..4a9cb1f8f 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -577,7 +577,7 @@ class Routes { public async $getHistoricalDifficulty(req: Request, res: Response) { try { - const stats = await BlocksRepository.$getBlocksDifficulty(req.params.interval ?? null); + const stats = await mining.$getHistoricalDifficulty(req.params.interval ?? null); res.header('Pragma', 'public'); res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html index b63ba29f5..78350d5d5 100644 --- a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html +++ b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html @@ -5,6 +5,31 @@
+
+
+
+ + + + + + +
+ +
+
Rank
@@ -14,12 +39,12 @@ - - - - - - + + + + + +
Change
{{ change[2] }}‎{{ change[0] | date:'yyyy-MM-dd HH:mm' }}{{ formatNumber(change[1], locale, '1.2-2') }}{{ formatNumber(change[3], locale, '1.2-2') }}%
{{ diffChange.height }}‎{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}{{ formatNumber(diffChange.change, locale, '1.2-2') }}%
diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts index 0e343843d..97be13f78 100644 --- a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts +++ b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts @@ -1,10 +1,11 @@ import { Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; import { EChartsOption } from 'echarts'; import { Observable } from 'rxjs'; -import { map, share, tap } from 'rxjs/operators'; +import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { ApiService } from 'src/app/services/api.service'; import { SeoService } from 'src/app/services/seo.service'; -import { formatNumber } from "@angular/common"; +import { formatNumber } from '@angular/common'; +import { FormBuilder, FormGroup } from '@angular/forms'; @Component({ selector: 'app-difficulty-chart', @@ -20,6 +21,8 @@ import { formatNumber } from "@angular/common"; `], }) export class DifficultyChartComponent implements OnInit { + radioGroupForm: FormGroup; + chartOptions: EChartsOption = {}; chartInitOptions = { renderer: 'svg' @@ -33,34 +36,45 @@ export class DifficultyChartComponent implements OnInit { @Inject(LOCALE_ID) public locale: string, private seoService: SeoService, private apiService: ApiService, + private formBuilder: FormBuilder, ) { this.seoService.setTitle($localize`:@@mining.difficulty:Difficulty`); + this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); + this.radioGroupForm.controls.dateSpan.setValue('1y'); } ngOnInit(): void { - this.difficultyObservable$ = this.apiService.getHistoricalDifficulty$(undefined) + this.difficultyObservable$ = this.radioGroupForm.get('dateSpan').valueChanges .pipe( - map(data => { - let formatted = []; - for (let i = 0; i < data.length - 1; ++i) { - const change = (data[i].difficulty / data[i + 1].difficulty - 1) * 100; - formatted.push([ - data[i].timestamp, - data[i].difficulty, - data[i].height, - formatNumber(change, this.locale, '1.2-2'), - change, - formatNumber(data[i].difficulty, this.locale, '1.2-2'), - ]); - } - return formatted; - }), - tap(data => { - this.prepareChartOptions(data); - this.isLoading = false; - }), - share() - ) + startWith('1y'), + switchMap((timespan) => { + return this.apiService.getHistoricalDifficulty$(timespan) + .pipe( + tap(data => { + this.prepareChartOptions(data.adjustments.map(val => [val.timestamp * 1000, val.difficulty])); + this.isLoading = false; + }), + map(data => { + const availableTimespanDay = ( + (new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp / 1000) + ) / 3600 / 24; + + const tableData = []; + for (let i = 0; i < data.adjustments.length - 1; ++i) { + const change = (data.adjustments[i].difficulty / data.adjustments[i + 1].difficulty - 1) * 100; + tableData.push(Object.assign(data.adjustments[i], { + change: change + })); + } + return { + availableTimespanDay: availableTimespanDay, + data: tableData + }; + }), + ); + }), + share() + ); } prepareChartOptions(data) { @@ -88,7 +102,7 @@ export class DifficultyChartComponent implements OnInit { type: 'value', axisLabel: { fontSize: 11, - formatter: function(val) { + formatter: (val) => { const diff = val / Math.pow(10, 12); // terra return diff.toString() + 'T'; } diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts index 9a7a33fc0..fc5a8da60 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -107,7 +107,7 @@ export class PoolRankingComponent implements OnInit { if (parseFloat(pool.share) < poolShareThreshold) { return; } - data.push({ + data.push({ value: pool.share, name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`), label: { @@ -115,9 +115,9 @@ export class PoolRankingComponent implements OnInit { overflow: 'break', }, tooltip: { - backgroundColor: "#282d47", + backgroundColor: '#282d47', textStyle: { - color: "#FFFFFF", + color: '#FFFFFF', }, formatter: () => { if (this.poolsWindowPreference === '24h') { @@ -131,7 +131,7 @@ export class PoolRankingComponent implements OnInit { } }, data: pool.poolId, - }); + } as PieSeriesOption); }); return data; } @@ -205,10 +205,10 @@ export class PoolRankingComponent implements OnInit { this.chartInstance = ec; this.chartInstance.on('click', (e) => { - this.router.navigate(['/mining/pool/', e.data.data]); - }) + this.router.navigate(['/mining/pool/', e.data.data]); + }); } - + /** * Default mining stats if something goes wrong */ diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 950a99fa7..9a6bbc0b8 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -150,7 +150,7 @@ export class ApiService { ); } - getHistoricalDifficulty$(interval: string | undefined): Observable { + getHistoricalDifficulty$(interval: string | undefined): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty` + (interval !== undefined ? `/${interval}` : '') From 1630ff717efa85b82076d234c49889cbbc27cbad Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 17 Feb 2022 10:15:41 +0900 Subject: [PATCH 32/45] On mobile, show power of ten difficulty instead of full number --- .../difficulty-chart.component.html | 3 ++- .../difficulty-chart.component.ts | 21 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html index 78350d5d5..1cdd90576 100644 --- a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html +++ b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html @@ -43,7 +43,8 @@ {{ diffChange.height }} ‎{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} - {{ formatNumber(diffChange.difficulty, locale, '1.2-2') }} + {{ formatNumber(diffChange.difficulty, locale, '1.2-2') }} + {{ diffChange.difficultyShorten }} {{ formatNumber(diffChange.change, locale, '1.2-2') }}% diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts index 97be13f78..47e0d9ea4 100644 --- a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts +++ b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts @@ -44,6 +44,13 @@ export class DifficultyChartComponent implements OnInit { } ngOnInit(): void { + const powerOfTen = { + terra: Math.pow(10, 12), + giga: Math.pow(10, 9), + mega: Math.pow(10, 6), + kilo: Math.pow(10, 3), + } + this.difficultyObservable$ = this.radioGroupForm.get('dateSpan').valueChanges .pipe( startWith('1y'), @@ -62,8 +69,20 @@ export class DifficultyChartComponent implements OnInit { const tableData = []; for (let i = 0; i < data.adjustments.length - 1; ++i) { const change = (data.adjustments[i].difficulty / data.adjustments[i + 1].difficulty - 1) * 100; + let selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' }; + if (data.adjustments[i].difficulty < powerOfTen.mega) { + selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling + } else if (data.adjustments[i].difficulty < powerOfTen.giga) { + selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' }; + } else if (data.adjustments[i].difficulty < powerOfTen.terra) { + selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' }; + } + tableData.push(Object.assign(data.adjustments[i], { - change: change + change: change, + difficultyShorten: formatNumber( + data.adjustments[i].difficulty / selectedPowerOfTen.divider, + this.locale, '1.2-2') + selectedPowerOfTen.unit })); } return { From c131c865ee577b28396fecc3b998d7f695f4c848 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 17 Feb 2022 17:52:12 +0900 Subject: [PATCH 33/45] Change pool ranking pie chart colors --- .../components/pool-ranking/pool-ranking.component.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts index fc5a8da60..17ea83b71 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -9,6 +9,7 @@ import { SeoService } from 'src/app/services/seo.service'; import { StorageService } from '../..//services/storage.service'; import { MiningService, MiningStats } from '../../services/mining.service'; import { StateService } from '../../services/state.service'; +import { chartColors } from 'src/app/app.constants'; @Component({ selector: 'app-pool-ranking', @@ -181,11 +182,8 @@ export class PoolRankingComponent implements OnInit { }, emphasis: { itemStyle: { - borderWidth: 2, - borderColor: '#FFF', - borderRadius: 2, - shadowBlur: 80, - shadowColor: 'rgba(255, 255, 255, 0.75)', + shadowBlur: 40, + shadowColor: 'rgba(0, 0, 0, 0.75)', }, labelLine: { lineStyle: { @@ -194,7 +192,8 @@ export class PoolRankingComponent implements OnInit { } } } - ] + ], + color: chartColors }; } From 15ba487ee45a13c12e607aba08dc2c1119b205bd Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 17 Feb 2022 18:02:55 +0900 Subject: [PATCH 34/45] When blocks need re-indexing, truncate the table --- backend/src/api/database-migration.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index f83f95034..b44585580 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -92,10 +92,12 @@ class DatabaseMigration { await this.$executeQuery(connection, this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks')); } if (databaseSchemaVersion < 5 && isBitcoin === true) { + await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"'); } if (databaseSchemaVersion < 6 && isBitcoin === true) { + await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index // Cleanup original blocks fields type await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"'); await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"'); From 923a2ce7f618b8b0d1941c9744fe1a7c35412b67 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 16 Feb 2022 17:32:12 +0900 Subject: [PATCH 35/45] Create basic layout for mining dashboard page - Show miner in blocks --- frontend/src/app/app-routing.module.ts | 13 +++++ frontend/src/app/app.module.ts | 2 + .../blockchain-blocks.component.html | 6 ++- .../blockchain-blocks.component.scss | 6 +++ .../blockchain-blocks.component.ts | 3 +- .../blockchain/blockchain.component.scss | 2 +- .../master-page/master-page.component.html | 2 +- .../mining-dashboard.component.html | 36 +++++++++++++ .../mining-dashboard.component.scss | 50 +++++++++++++++++++ .../mining-dashboard.component.ts | 15 ++++++ .../pool-ranking/pool-ranking.component.html | 31 ++++++------ .../pool-ranking/pool-ranking.component.ts | 21 +++++--- 12 files changed, 161 insertions(+), 26 deletions(-) create mode 100644 frontend/src/app/components/mining-dashboard/mining-dashboard.component.html create mode 100644 frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss create mode 100644 frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 6ce39b1d2..974c58e70 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -27,6 +27,7 @@ import { AssetGroupComponent } from './components/assets/asset-group/asset-group import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; import { AssetsComponent } from './components/assets/assets.component'; import { PoolComponent } from './components/pool/pool.component'; +import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component'; import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component'; let routes: Routes = [ @@ -72,6 +73,10 @@ let routes: Routes = [ path: 'mining/pools', component: PoolRankingComponent, }, + { + path: 'mining/dashboard', + component: MiningDashboardComponent, + }, { path: 'mining/pool/:poolId', component: PoolComponent, @@ -168,6 +173,10 @@ let routes: Routes = [ path: 'mining/pools', component: PoolRankingComponent, }, + { + path: 'mining/dashboard', + component: MiningDashboardComponent, + }, { path: 'mining/pool/:poolId', component: PoolComponent, @@ -258,6 +267,10 @@ let routes: Routes = [ path: 'mining/pools', component: PoolRankingComponent, }, + { + path: 'mining/dashboard', + component: MiningDashboardComponent, + }, { path: 'mining/pool/:poolId', component: PoolComponent, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 15bebd033..677d88d6e 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -68,6 +68,7 @@ import { PushTransactionComponent } from './components/push-transaction/push-tra import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component'; +import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component'; import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component'; @NgModule({ @@ -119,6 +120,7 @@ import { DifficultyChartComponent } from './components/difficulty-chart/difficul AssetsNavComponent, AssetsFeaturedComponent, AssetGroupComponent, + MiningDashboardComponent, DifficultyChartComponent, ], imports: [ diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index 50fd82b09..c8a4aa953 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -21,9 +21,13 @@
+
-
+
diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss index 3b1347cea..a20b1cb35 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss @@ -124,3 +124,9 @@ 50% {opacity: 1.0;} 100% {opacity: 0.7;} } + +.badge { + position: relative; + top: 15px; + z-index: 101; +} \ No newline at end of file diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index ef076e74b..f80134425 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; import { StateService } from 'src/app/services/state.service'; import { Router } from '@angular/router'; @@ -12,6 +12,7 @@ import { BlockExtended } from 'src/app/interfaces/node-api.interface'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class BlockchainBlocksComponent implements OnInit, OnDestroy { + @Input() miningInfo: boolean = false; specialBlocks = specialBlocks; network = ''; blocks: BlockExtended[] = []; diff --git a/frontend/src/app/components/blockchain/blockchain.component.scss b/frontend/src/app/components/blockchain/blockchain.component.scss index 0527798a2..cb5991d84 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.scss +++ b/frontend/src/app/components/blockchain/blockchain.component.scss @@ -60,4 +60,4 @@ width: 300px; left: -150px; top: 0px; -} \ No newline at end of file +} diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index af47b75c2..2943409de 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -32,7 +32,7 @@