diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index d6d37b5c4..f0c04455b 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -171,7 +171,7 @@ class Blocks { } /** - * Index all blocks metadata for the mining dashboard + * [INDEXING] Index all blocks metadata for the mining dashboard */ public async $generateBlockDatabase() { if (this.blockIndexingStarted) { diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 49e9ef9c4..c66148fe0 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 = 8; + private static currentVersion = 9; private queryTimeout = 120000; private statisticsAddedIndexed = false; @@ -133,6 +133,13 @@ class DatabaseMigration { await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"'); } + if (databaseSchemaVersion < 9) { + logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.'`); + await this.$executeQuery(connection, 'TRUNCATE hashrates;'); // Need to re-index + await this.$executeQuery(connection, 'ALTER TABLE `state` CHANGE `name` `name` varchar(100)'); + await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)'); + } + connection.release(); } catch (e) { connection.release(); @@ -276,6 +283,10 @@ class DatabaseMigration { queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`); } + if (version < 9) { + queries.push(`INSERT INTO state(name, number, string) VALUES ('last_weekly_hashrates_indexing', 0, NULL)`); + } + return queries; } diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index 1d2f47561..4423e5f16 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -8,6 +8,7 @@ import blocks from './blocks'; class Mining { hashrateIndexingStarted = false; + weeklyHashrateIndexingStarted = false; constructor() { } @@ -74,60 +75,134 @@ class Mining { } /** - * Return the historical difficulty adjustments and oldest indexed block timestamp + * [INDEXING] Generate weekly mining pool hashrate history */ - public async $getHistoricalDifficulty(interval: string | null): Promise { - return await BlocksRepository.$getBlocksDifficulty(interval); - } - - /** - * Return the historical hashrates and oldest indexed block timestamp - */ - public async $getNetworkHistoricalHashrates(interval: string | null): Promise { - return await HashratesRepository.$getNetworkDailyHashrate(interval); - } - - /** - * Return the historical hashrates and oldest indexed block timestamp for one or all pools - */ - public async $getPoolsHistoricalHashrates(interval: string | null, poolId: number): Promise { - return await HashratesRepository.$getPoolsWeeklyHashrate(interval); - } - - /** - * Generate daily hashrate data - */ - public async $generateNetworkHashrateHistory(): Promise { - // We only run this once a day - const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp(); - const now = new Date().getTime() / 1000; - if (now - latestTimestamp < 86400) { + public async $generatePoolHashrateHistory(): Promise { + if (!blocks.blockIndexingCompleted || this.weeklyHashrateIndexingStarted) { return; } + // We only run this once a week + const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp('last_weekly_hashrates_indexing'); + const now = new Date().getTime() / 1000; + if (now - latestTimestamp < 604800) { + return; + } + + try { + this.weeklyHashrateIndexingStarted = true; + + logger.info(`Indexing mining pools weekly hashrates`); + + const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps(); + const hashrates: any[] = []; + const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f + const lastMidnight = this.getDateMidnight(new Date()); + let toTimestamp = Math.round((lastMidnight.getTime() - 604800) / 1000); + + const totalWeekIndexed = (await BlocksRepository.$blockCount(null, null)) / 1008; + let indexedThisRun = 0; + let totalIndexed = 0; + let startedAt = new Date().getTime() / 1000; + + while (toTimestamp > genesisTimestamp) { + const fromTimestamp = toTimestamp - 604800; + + // Skip already indexed weeks + if (indexedTimestamp.includes(toTimestamp + 1)) { + toTimestamp -= 604800; + ++totalIndexed; + continue; + } + + const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp( + null, fromTimestamp, toTimestamp); + if (blockStats.blockCount === 0) { // We are done indexing, no blocks left + break; + } + + const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, + blockStats.lastBlockHeight); + + let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp, toTimestamp); + const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0); + pools = pools.map((pool: any) => { + pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate; + pool.share = (pool.blockCount / totalBlocks); + return pool; + }); + + for (const pool of pools) { + hashrates.push({ + hashrateTimestamp: toTimestamp + 1, + avgHashrate: pool['hashrate'], + poolId: pool.poolId, + share: pool['share'], + type: 'weekly', + }); + } + + await HashratesRepository.$saveHashrates(hashrates); + hashrates.length = 0; + + const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); + if (elapsedSeconds > 5) { + const weeksPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2); + const formattedDate = new Date(fromTimestamp * 1000).toUTCString(); + const weeksLeft = Math.round(totalWeekIndexed - totalIndexed); + logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds} weeks/sec | ~${weeksLeft} weeks left to index`); + startedAt = new Date().getTime() / 1000; + indexedThisRun = 0; + } + + toTimestamp -= 604800; + ++indexedThisRun; + ++totalIndexed; + } + this.weeklyHashrateIndexingStarted = false; + await HashratesRepository.$setLatestRunTimestamp('last_weekly_hashrates_indexing'); + logger.info(`Weekly pools hashrate indexing completed`); + } catch (e) { + this.weeklyHashrateIndexingStarted = false; + throw e; + } + } + + /** + * [INDEXING] Generate daily hashrate data + */ + public async $generateNetworkHashrateHistory(): Promise { if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) { return; } + // We only run this once a day + const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp('last_hashrates_indexing'); + const now = new Date().getTime() / 1000; + if (now - latestTimestamp < 86400) { + return; + } + try { this.hashrateIndexingStarted = true; - logger.info(`Indexing hashrates`); + logger.info(`Indexing network daily hashrate`); + + const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp); + const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f + const lastMidnight = this.getDateMidnight(new Date()); + let toTimestamp = Math.round(lastMidnight.getTime() / 1000); + const hashrates: any[] = []; const totalDayIndexed = (await BlocksRepository.$blockCount(null, null)) / 144; - const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp); - let startedAt = new Date().getTime() / 1000; - const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f - const lastMidnight = new Date(); - lastMidnight.setUTCHours(0); lastMidnight.setUTCMinutes(0); lastMidnight.setUTCSeconds(0); lastMidnight.setUTCMilliseconds(0); - let toTimestamp = Math.round(lastMidnight.getTime() / 1000); let indexedThisRun = 0; let totalIndexed = 0; - - const hashrates: any[] = []; + let startedAt = new Date().getTime() / 1000; while (toTimestamp > genesisTimestamp) { const fromTimestamp = toTimestamp - 86400; + + // Skip already indexed weeks if (indexedTimestamp.includes(fromTimestamp)) { toTimestamp -= 86400; ++totalIndexed; @@ -140,33 +215,11 @@ class Mining { break; } - let lastBlockHashrate = 0; - lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, + const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, blockStats.lastBlockHeight); - if (totalIndexed > 7 && totalIndexed % 7 === 0 && !indexedTimestamp.includes(fromTimestamp + 1)) { // Save weekly pools hashrate - logger.debug("Indexing weekly hashrates for mining pools"); - let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp - 604800, fromTimestamp); - const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0); - pools = pools.map((pool: any) => { - pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate; - pool.share = (pool.blockCount / totalBlocks); - return pool; - }); - - for (const pool of pools) { - hashrates.push({ - hashrateTimestamp: fromTimestamp + 1, - avgHashrate: pool['hashrate'], - poolId: pool.poolId, - share: pool['share'], - type: 'weekly', - }); - } - } - hashrates.push({ - hashrateTimestamp: fromTimestamp, + hashrateTimestamp: toTimestamp, avgHashrate: lastBlockHashrate, poolId: null, share: 1, @@ -183,7 +236,7 @@ class Mining { const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2); const formattedDate = new Date(fromTimestamp * 1000).toUTCString(); const daysLeft = Math.round(totalDayIndexed - totalIndexed); - logger.debug(`Getting hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ~${daysLeft} days left to index`); + logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ~${daysLeft} days left to index`); startedAt = new Date().getTime() / 1000; indexedThisRun = 0; } @@ -203,18 +256,25 @@ class Mining { }); } - if (hashrates.length > 0) { - await HashratesRepository.$saveHashrates(hashrates); - } - await HashratesRepository.$setLatestRunTimestamp(); - this.hashrateIndexingStarted = false; + await HashratesRepository.$saveHashrates(hashrates); - logger.info(`Hashrates indexing completed`); + await HashratesRepository.$setLatestRunTimestamp('last_hashrates_indexing'); + this.hashrateIndexingStarted = false; + logger.info(`Daily network hashrate indexing completed`); } catch (e) { this.hashrateIndexingStarted = false; throw e; } } + + private getDateMidnight(date: Date): Date { + date.setUTCHours(0); + date.setUTCMinutes(0); + date.setUTCSeconds(0); + date.setUTCMilliseconds(0); + + return date; + } } export default new Mining(); diff --git a/backend/src/database.ts b/backend/src/database.ts index b0d39b301..9f2655016 100644 --- a/backend/src/database.ts +++ b/backend/src/database.ts @@ -11,6 +11,7 @@ export class DB { password: config.DATABASE.PASSWORD, connectionLimit: 10, supportBigNumbers: true, + timezone: '+00:00', }); } diff --git a/backend/src/index.ts b/backend/src/index.ts index 51cee34c4..4ede865a6 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -167,7 +167,8 @@ class Server { } async $resetHashratesIndexingState() { - return await HashratesRepository.$setLatestRunTimestamp(0); + await HashratesRepository.$setLatestRunTimestamp('last_hashrates_indexing', 0); + await HashratesRepository.$setLatestRunTimestamp('last_weekly_hashrates_indexing', 0); } async $runIndexingWhenReady() { @@ -176,8 +177,9 @@ class Server { } try { - await blocks.$generateBlockDatabase(); - await mining.$generateNetworkHashrateHistory(); + blocks.$generateBlockDatabase(); + mining.$generateNetworkHashrateHistory(); + mining.$generatePoolHashrateHistory(); } catch (e) { logger.err(`Unable to run indexing right now, trying again later. ` + e); } diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index d91777880..844f62bad 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -53,15 +53,17 @@ class BlocksRepository { // logger.debug(query); await connection.query(query, params); + connection.release(); } catch (e: any) { + connection.release(); if (e.errno === 1062) { // ER_DUP_ENTRY logger.debug(`$saveBlockInDatabase() - Block ${block.height} has already been indexed, ignoring`); } else { + connection.release(); logger.err('$saveBlockInDatabase() error' + (e instanceof Error ? e.message : e)); + throw e; } } - - connection.release(); } /** @@ -73,20 +75,26 @@ class BlocksRepository { } const connection = await DB.pool.getConnection(); - const [rows]: any[] = await connection.query(` - SELECT height - FROM blocks - WHERE height <= ? AND height >= ? - ORDER BY height DESC; - `, [startHeight, endHeight]); - connection.release(); + try { + const [rows]: any[] = await connection.query(` + SELECT height + FROM blocks + 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 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); - return missingBlocksHeights; + return missingBlocksHeights; + } catch (e) { + connection.release(); + logger.err('$getMissingBlocksBetweenHeights() error' + (e instanceof Error ? e.message : e)); + throw e; + } } /** @@ -111,10 +119,16 @@ class BlocksRepository { // logger.debug(query); const connection = await DB.pool.getConnection(); - const [rows] = await connection.query(query, params); - connection.release(); + try { + const [rows] = await connection.query(query, params); + connection.release(); - return rows; + return rows; + } catch (e) { + connection.release(); + logger.err('$getEmptyBlocks() error' + (e instanceof Error ? e.message : e)); + throw e; + } } /** @@ -143,10 +157,16 @@ class BlocksRepository { // logger.debug(query); const connection = await DB.pool.getConnection(); - const [rows] = await connection.query(query, params); - connection.release(); + try { + const [rows] = await connection.query(query, params); + connection.release(); - return rows[0].blockCount; + return rows[0].blockCount; + } catch (e) { + connection.release(); + logger.err('$blockCount() error' + (e instanceof Error ? e.message : e)); + throw e; + } } /** @@ -177,10 +197,16 @@ class BlocksRepository { // logger.debug(query); const connection = await DB.pool.getConnection(); - const [rows] = await connection.query(query, params); - connection.release(); + try { + const [rows] = await connection.query(query, params); + connection.release(); - return rows[0]; + return rows[0]; + } catch (e) { + connection.release(); + logger.err('$blockCountBetweenTimestamp() error' + (e instanceof Error ? e.message : e)); + throw e; + } } /** @@ -194,23 +220,26 @@ class BlocksRepository { // logger.debug(query); const connection = await DB.pool.getConnection(); - const [rows]: any[] = await connection.query(query); - connection.release(); + try { + const [rows]: any[] = await connection.query(query); + connection.release(); - if (rows.length <= 0) { - return -1; + if (rows.length <= 0) { + return -1; + } + + return rows[0].blockTimestamp; + } catch (e) { + connection.release(); + logger.err('$oldestBlockTimestamp() error' + (e instanceof Error ? e.message : e)); + throw e; } - - return rows[0].blockTimestamp; } /** * Get blocks mined by a specific mining pool */ - public async $getBlocksByPool( - poolId: number, - startHeight: number | null = null - ): Promise { + public async $getBlocksByPool(poolId: number, startHeight: number | null = null): Promise { const params: any[] = []; let query = `SELECT height, hash as id, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, reward FROM blocks @@ -227,14 +256,20 @@ class BlocksRepository { // logger.debug(query); const connection = await DB.pool.getConnection(); - const [rows] = await connection.query(query, params); - connection.release(); + try { + const [rows] = await connection.query(query, params); + connection.release(); - for (const block of rows) { - delete block['blockTimestamp']; + for (const block of rows) { + delete block['blockTimestamp']; + } + + return rows; + } catch (e) { + connection.release(); + logger.err('$getBlocksByPool() error' + (e instanceof Error ? e.message : e)); + throw e; } - - return rows; } /** @@ -242,19 +277,25 @@ class BlocksRepository { */ 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(); + try { + 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; + if (rows.length <= 0) { + return null; + } + + return rows[0]; + } catch (e) { + connection.release(); + logger.err('$getBlockByHeight() error' + (e instanceof Error ? e.message : e)); + throw e; } - - return rows[0]; } /** @@ -297,21 +338,34 @@ class BlocksRepository { ORDER BY t.height `; - const [rows]: any[] = await connection.query(query); - connection.release(); + try { + const [rows]: any[] = await connection.query(query); + connection.release(); - for (let row of rows) { - delete row['rn']; + for (let row of rows) { + delete row['rn']; + } + + return rows; + } catch (e) { + connection.release(); + logger.err('$getBlocksDifficulty() error' + (e instanceof Error ? e.message : e)); + throw e; } - - return rows; } public async $getOldestIndexedBlockHeight(): Promise { const connection = await DB.pool.getConnection(); - const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`); - connection.release(); - return rows[0].minHeight; + try { + const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`); + connection.release(); + + return rows[0].minHeight; + } catch (e) { + connection.release(); + logger.err('$getOldestIndexedBlockHeight() error' + (e instanceof Error ? e.message : e)); + throw e; + } } } diff --git a/backend/src/repositories/HashratesRepository.ts b/backend/src/repositories/HashratesRepository.ts index 5a33d7e77..3523004d5 100644 --- a/backend/src/repositories/HashratesRepository.ts +++ b/backend/src/repositories/HashratesRepository.ts @@ -8,6 +8,10 @@ class HashratesRepository { * Save indexed block data in the database */ public async $saveHashrates(hashrates: any) { + if (hashrates.length === 0) { + return; + } + let query = `INSERT INTO hashrates(hashrate_timestamp, avg_hashrate, pool_id, share, type) VALUES`; @@ -20,12 +24,12 @@ class HashratesRepository { try { // logger.debug(query); await connection.query(query); + connection.release(); } catch (e: any) { + connection.release(); logger.err('$saveHashrateInDatabase() error' + (e instanceof Error ? e.message : e)); throw e; } - - connection.release(); } public async $getNetworkDailyHashrate(interval: string | null): Promise { @@ -47,10 +51,33 @@ class HashratesRepository { query += ` ORDER by hashrate_timestamp`; - const [rows]: any[] = await connection.query(query); - connection.release(); + try { + const [rows]: any[] = await connection.query(query); + connection.release(); - return rows; + return rows; + } catch (e) { + connection.release(); + logger.err('$getNetworkDailyHashrate() error' + (e instanceof Error ? e.message : e)); + throw e; + } + } + + public async $getWeeklyHashrateTimestamps(): Promise { + const connection = await DB.pool.getConnection(); + + const query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp FROM hashrates where type = 'weekly' GROUP BY hashrate_timestamp`; + + try { + const [rows]: any[] = await connection.query(query); + connection.release(); + + return rows.map(row => row.timestamp); + } catch (e) { + connection.release(); + logger.err('$getWeeklyHashrateTimestamps() error' + (e instanceof Error ? e.message : e)); + throw e; + } } /** @@ -77,26 +104,44 @@ class HashratesRepository { query += ` ORDER by hashrate_timestamp, FIELD(pool_id, ${topPoolsId})`; - const [rows]: any[] = await connection.query(query); - connection.release(); + try { + const [rows]: any[] = await connection.query(query); + connection.release(); - return rows; + return rows; + } catch (e) { + connection.release(); + logger.err('$getPoolsWeeklyHashrate() error' + (e instanceof Error ? e.message : e)); + throw e; + } } - public async $setLatestRunTimestamp(val: any = null) { + public async $setLatestRunTimestamp(key: string, val: any = null) { const connection = await DB.pool.getConnection(); - const query = `UPDATE state SET number = ? WHERE name = 'last_hashrates_indexing'`; + const query = `UPDATE state SET number = ? WHERE name = ?`; - await connection.query(query, (val === null) ? [Math.round(new Date().getTime() / 1000)] : [val]); - connection.release(); + try { + await connection.query(query, (val === null) ? [Math.round(new Date().getTime() / 1000), key] : [val, key]); + connection.release(); + } catch (e) { + connection.release(); + } } - public async $getLatestRunTimestamp(): Promise { + public async $getLatestRunTimestamp(key: string): Promise { const connection = await DB.pool.getConnection(); - const query = `SELECT number FROM state WHERE name = 'last_hashrates_indexing'`; - const [rows] = await connection.query(query); - connection.release(); - return rows[0]['number']; + const query = `SELECT number FROM state WHERE name = ?`; + + try { + const [rows] = await connection.query(query, [key]); + connection.release(); + + return rows[0]['number']; + } catch (e) { + connection.release(); + logger.err('$setLatestRunTimestamp() error' + (e instanceof Error ? e.message : e)); + throw e; + } } } diff --git a/backend/src/repositories/PoolsRepository.ts b/backend/src/repositories/PoolsRepository.ts index ea617322a..3f904888d 100644 --- a/backend/src/repositories/PoolsRepository.ts +++ b/backend/src/repositories/PoolsRepository.ts @@ -43,26 +43,38 @@ class PoolsRepository { // logger.debug(query); const connection = await DB.pool.getConnection(); - const [rows] = await connection.query(query); - connection.release(); + try { + const [rows] = await connection.query(query); + connection.release(); - return rows; + return rows; + } catch (e) { + connection.release(); + logger.err('$getPoolsInfo() error' + (e instanceof Error ? e.message : e)); + throw e; + } } /** * Get basic pool info and block count between two timestamp */ public async $getPoolsInfoBetween(from: number, to: number): Promise { - let query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName + const query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName FROM pools LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?) GROUP BY pools.id`; const connection = await DB.pool.getConnection(); - const [rows] = await connection.query(query, [from, to]); - connection.release(); + try { + const [rows] = await connection.query(query, [from, to]); + connection.release(); - return rows; + return rows; + } catch (e) { + connection.release(); + logger.err('$getPoolsInfoBetween() error' + (e instanceof Error ? e.message : e)); + throw e; + } } /** @@ -76,13 +88,19 @@ class PoolsRepository { // logger.debug(query); const connection = await DB.pool.getConnection(); - const [rows] = await connection.query(query, [poolId]); - connection.release(); + try { + 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); + rows[0].regexes = JSON.parse(rows[0].regexes); + rows[0].addresses = JSON.parse(rows[0].addresses); - return rows[0]; + return rows[0]; + } catch (e) { + connection.release(); + logger.err('$getPool() error' + (e instanceof Error ? e.message : e)); + throw e; + } } } diff --git a/backend/src/routes.ts b/backend/src/routes.ts index dea9da2f2..6b1a365b6 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 mining from './api/mining'; import BlocksRepository from './repositories/BlocksRepository'; +import HashratesRepository from './repositories/HashratesRepository'; class Routes { constructor() {} @@ -576,7 +577,7 @@ class Routes { public async $getHistoricalDifficulty(req: Request, res: Response) { try { - const stats = await mining.$getHistoricalDifficulty(req.params.interval ?? null); + 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()); @@ -588,7 +589,7 @@ class Routes { public async $getPoolsHistoricalHashrate(req: Request, res: Response) { try { - const hashrates = await mining.$getPoolsHistoricalHashrates(req.params.interval ?? null, parseInt(req.params.poolId, 10)); + const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval ?? null); const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp(); res.header('Pragma', 'public'); res.header('Cache-control', 'public'); @@ -604,8 +605,8 @@ class Routes { public async $getHistoricalHashrate(req: Request, res: Response) { try { - const hashrates = await mining.$getNetworkHistoricalHashrates(req.params.interval ?? null); - const difficulty = await mining.$getHistoricalDifficulty(req.params.interval ?? null); + const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval ?? null); + const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval ?? null); const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp(); res.header('Pragma', 'public'); res.header('Cache-control', 'public'); diff --git a/frontend/src/app/components/difficulty/difficulty.component.html b/frontend/src/app/components/difficulty/difficulty.component.html index fc3030286..3684b8de4 100644 --- a/frontend/src/app/components/difficulty/difficulty.component.html +++ b/frontend/src/app/components/difficulty/difficulty.component.html @@ -1,4 +1,4 @@ -
Difficulty Adjustment
+
Difficulty Adjustment
@@ -47,7 +47,7 @@
-
Next halving
+
Next Halving
{{ i }} blocks diff --git a/frontend/src/app/components/difficulty/difficulty.component.ts b/frontend/src/app/components/difficulty/difficulty.component.ts index ff44e5aeb..b22001ef1 100644 --- a/frontend/src/app/components/difficulty/difficulty.component.ts +++ b/frontend/src/app/components/difficulty/difficulty.component.ts @@ -28,8 +28,9 @@ export class DifficultyComponent implements OnInit { isLoadingWebSocket$: Observable; difficultyEpoch$: Observable; - @Input() showProgress: boolean = true; - @Input() showHalving: boolean = false; + @Input() showProgress = true; + @Input() showHalving = false; + @Input() showTitle = true; constructor( public stateService: StateService, @@ -97,7 +98,7 @@ export class DifficultyComponent implements OnInit { colorPreviousAdjustments = '#ffffff66'; } - const blocksUntilHalving = block.height % 210000; + const blocksUntilHalving = 210000 - (block.height % 210000); const timeUntilHalving = (blocksUntilHalving * timeAvgMins * 60 * 1000) + (now * 1000); return { diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html index ea5c5a2a7..eaa9fa809 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -1,6 +1,6 @@
-
+
-
- +
diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss index 316f0fc47..4d9e0e5fa 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss @@ -29,7 +29,7 @@ .chart-widget { width: 100%; height: 100%; - max-height: 275px; + max-height: 293px; } .formRadioGroup { @@ -48,3 +48,8 @@ } } } + +.compact td { + padding: 0 !important; + margin: 0.15rem !important; +} \ No newline at end of file diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts index 7efb83098..a5f2b63b8 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -22,7 +22,8 @@ import { selectPowerOfTen } from 'src/app/bitcoin.utils'; `], }) export class HashrateChartComponent implements OnInit { - @Input() widget: boolean = false; + @Input() tableOnly = false; + @Input() widget = false; @Input() right: number | string = 45; @Input() left: number | string = 75; @@ -114,7 +115,7 @@ export class HashrateChartComponent implements OnInit { } return { availableTimespanDay: availableTimespanDay, - difficulty: tableData + difficulty: this.tableOnly ? (this.isMobile() ? tableData.slice(0, 12) : tableData.slice(0, 9)) : tableData }; }), ); @@ -141,6 +142,7 @@ export class HashrateChartComponent implements OnInit { bottom: this.widget ? 30 : 60, }, tooltip: { + show: !this.isMobile() || !this.widget, trigger: 'axis', axisPointer: { type: 'line' diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html index 93cec63ca..8750caa56 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html @@ -27,7 +27,7 @@
-
+
diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss index 06a3eeb25..4f15e51d6 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss @@ -29,7 +29,7 @@ .chart-widget { width: 100%; height: 100%; - max-height: 275px; + max-height: 293px; } .formRadioGroup { diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts index 3d7935e3d..7e3f081a6 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts @@ -78,7 +78,7 @@ export class HashrateChartPoolsComponent implements OnInit { name: name, showSymbol: false, symbol: 'none', - data: grouped[name].map((val) => [val.timestamp * 1000, (val.share * 100).toFixed(2)]), + data: grouped[name].map((val) => [val.timestamp * 1000, val.share * 100]), type: 'line', lineStyle: { width: 0 }, areaStyle: { opacity: 1 }, @@ -132,6 +132,7 @@ export class HashrateChartPoolsComponent implements OnInit { top: this.widget ? 10 : 40, }, tooltip: { + show: !this.isMobile() || !this.widget, trigger: 'axis', axisPointer: { type: 'line' @@ -149,7 +150,7 @@ export class HashrateChartPoolsComponent implements OnInit { data.sort((a, b) => b.data[1] - a.data[1]); for (const pool of data) { if (pool.data[1] > 0) { - tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1]}%
` + tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1].toFixed(2)}%
`; } } return tooltip; diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html index 4d811fc9a..6dbb541c3 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html @@ -3,35 +3,97 @@
-
-
- -
- - Mining Pools Share (1w) - -
- +
Reward stats
+
+
+
+
+
Miners Reward
+
+ +
in the last 8 blocks
+
+
+
+
Reward Per Tx
+
+ {{ rewardStats.rewardPerTx }} + sats/tx +
in the last 8 blocks
+
+
+
+
Average Fee
+
+ {{ rewardStats.feePerTx }} + sats/tx +
in the last 8 blocks
+
+
+
+
+
+
- + +
+
Difficulty Adjustment
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+
+
+ Hashrate (1y) +
+ + +
+
+
+ + +
+
+
+
+ Mining Pools Dominance (1y) +
+
-
+
-
- - Hashrate (1y) - + Adjusments
- + +
-
+
\ No newline at end of file diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss index 4c75e9ea6..828ee7ed0 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss @@ -12,14 +12,11 @@ .card { background-color: #1d1f31; - height: 340px; -} -.card.double { - height: 620px; } .card-title { font-size: 1rem; + color: #4a68b9; } .card-title > a { color: #4a68b9; @@ -58,3 +55,91 @@ text-align: center; padding-bottom: 3px; } + +.general-stats { + min-height: 56px; + display: block; + @media (min-width: 485px) { + display: flex; + flex-direction: row; + } + h5 { + margin-bottom: 10px; + } + .item { + width: 50%; + margin: 0px auto 10px; + display: inline-block; + @media (min-width: 485px) { + margin: 0px auto 10px; + } + @media (min-width: 785px) { + margin: 0px auto 0px; + } + &:last-child { + margin: 0px auto 0px; + } + &:nth-child(2) { + order: 2; + @media (min-width: 485px) { + order: 3; + } + } + &:nth-child(3) { + order: 3; + @media (min-width: 485px) { + order: 2; + display: block; + } + @media (min-width: 768px) { + display: none; + } + @media (min-width: 992px) { + display: block; + } + } + .card-title { + font-size: 1rem; + color: #4a68b9; + } + .card-text { + font-size: 18px; + span { + color: #ffffff66; + font-size: 12px; + } + } + } +} + +.difficulty-adjustment-container { + display: flex; + flex-direction: row; + justify-content: space-around; + height: 76px; + .shared-block { + color: #ffffff66; + font-size: 12px; + } + .item { + padding: 0 5px; + width: 100%; + &:nth-child(1) { + display: none; + @media (min-width: 485px) { + display: table-cell; + } + @media (min-width: 768px) { + display: none; + } + @media (min-width: 992px) { + display: table-cell; + } + } + } + .card-text { + font-size: 22px; + margin-top: -9px; + position: relative; + } +} diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts index aac546ca1..b4cc95a1e 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts @@ -1,5 +1,10 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnDestroy, OnInit } from '@angular/core'; +import { map } from 'rxjs/operators'; import { SeoService } from 'src/app/services/seo.service'; +import { StateService } from 'src/app/services/state.service'; +import { formatNumber } from '@angular/common'; +import { WebsocketService } from 'src/app/services/websocket.service'; +import { Observable } from 'rxjs'; @Component({ selector: 'app-mining-dashboard', @@ -8,12 +13,36 @@ import { SeoService } from 'src/app/services/seo.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class MiningDashboardComponent implements OnInit { + private blocks = []; - constructor(private seoService: SeoService) { + public $rewardStats: Observable; + public totalReward = 0; + public rewardPerTx = '~'; + public feePerTx = '~'; + + constructor(private seoService: SeoService, + public stateService: StateService, + private websocketService: WebsocketService, + @Inject(LOCALE_ID) private locale: string, + ) { this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`); } ngOnInit(): void { - } + this.$rewardStats = this.stateService.blocks$.pipe( + map(([block]) => { + this.blocks.push(block); + this.blocks = this.blocks.slice(0, 8); + const totalTx = this.blocks.reduce((acc, block) => acc + block.tx_count, 0); + const totalFee = this.blocks.reduce((acc, block) => acc + block.extras?.totalFees ?? 0, 0); + const totalReward = this.blocks.reduce((acc, block) => acc + block.extras?.reward ?? 0, 0); + return { + 'totalReward': totalReward, + 'rewardPerTx': formatNumber(totalReward / totalTx, this.locale, '1.0-0'), + 'feePerTx': formatNumber(totalFee / totalTx, this.locale, '1.0-0'), + } + }) + ); + } } 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 067c61646..6bba39df4 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.html +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.html @@ -1,8 +1,29 @@
+
+
+
Pools luck (1w)
+

+ {{ miningStats['minersLuck'] }}% +

+
+
+
Blocks (1w)
+

+ {{ miningStats.blockCount }} +

+
+
+
Pools count (1w)
+

+ {{ miningStats.pools.length }} +

+
+
+
-
+
@@ -59,7 +80,7 @@ {{ pool.rank }} {{ pool.name }} - {{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }} + {{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }} {{ pool['blockText'] }} {{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%) diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.scss b/frontend/src/app/components/pool-ranking/pool-ranking.component.scss index a41891a8f..f73486395 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.scss +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.scss @@ -1,13 +1,16 @@ .chart { max-height: 400px; @media (max-width: 767.98px) { - max-height: 300px; + max-height: 270px; } } .chart-widget { width: 100%; height: 100%; - max-height: 275px; + max-height: 270px; + @media (max-width: 767.98px) { + max-height: 200px; + } } .formRadioGroup { @@ -44,3 +47,59 @@ .loadingGraphs.widget { top: 25%; } + +.pool-distribution { + min-height: 56px; + display: block; + @media (min-width: 485px) { + display: flex; + flex-direction: row; + } + h5 { + margin-bottom: 10px; + } + .item { + width: 50%; + margin: 0px auto 10px; + display: inline-block; + @media (min-width: 485px) { + margin: 0px auto 10px; + } + @media (min-width: 785px) { + margin: 0px auto 0px; + } + &:last-child { + margin: 0px auto 0px; + } + &:nth-child(2) { + order: 2; + @media (min-width: 485px) { + order: 3; + } + } + &:nth-child(3) { + order: 3; + @media (min-width: 485px) { + order: 2; + display: block; + } + @media (min-width: 768px) { + display: none; + } + @media (min-width: 992px) { + display: block; + } + } + .card-title { + font-size: 1rem; + color: #4a68b9; + } + .card-text { + font-size: 18px; + span { + color: #ffffff66; + font-size: 12px; + } + } + } +} \ No newline at end of file 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 cef04cca5..64641c31d 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -49,7 +49,7 @@ export class PoolRankingComponent implements OnInit { this.poolsWindowPreference = '1w'; } else { this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`); - this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w'; + this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w'; } this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference }); this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference); @@ -85,6 +85,7 @@ export class PoolRankingComponent implements OnInit { }), map(data => { data.pools = data.pools.map((pool: SinglePoolStats) => this.formatPoolUI(pool)); + data['minersLuck'] = (100 * (data.blockCount / 1008)).toFixed(2); // luck 1w return data; }), tap(data => { @@ -105,24 +106,40 @@ export class PoolRankingComponent implements OnInit { } generatePoolsChartSerieData(miningStats) { - const poolShareThreshold = this.isMobile() ? 1 : 0.5; // Do not draw pools which hashrate share is lower than that + const poolShareThreshold = this.isMobile() ? 2 : 1; // Do not draw pools which hashrate share is lower than that const data: object[] = []; + let totalShareOther = 0; + let totalBlockOther = 0; + let totalEstimatedHashrateOther = 0; + + let edgeDistance: any = '20%'; + if (this.isMobile() && this.widget) { + edgeDistance = 0; + } else if (this.isMobile() && !this.widget || this.widget) { + edgeDistance = 35; + } miningStats.pools.forEach((pool) => { if (parseFloat(pool.share) < poolShareThreshold) { + totalShareOther += parseFloat(pool.share); + totalBlockOther += pool.blockCount; + totalEstimatedHashrateOther += pool.lastEstimatedHashrate; return; } data.push({ itemStyle: { - color: poolsColor[pool.name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()], + color: poolsColor[pool.name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()], }, value: pool.share, - name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`), + name: pool.name + ((this.isMobile() || this.widget) ? `` : ` (${pool.share}%)`), label: { + overflow: 'none', color: '#b1b1b1', - overflow: 'break', + alignTo: 'edge', + edgeDistance: edgeDistance, }, tooltip: { + show: !this.isMobile() || !this.widget, backgroundColor: 'rgba(17, 19, 31, 1)', borderRadius: 4, shadowColor: 'rgba(0, 0, 0, 0.5)', @@ -144,6 +161,42 @@ export class PoolRankingComponent implements OnInit { data: pool.poolId, } as PieSeriesOption); }); + + // 'Other' + data.push({ + itemStyle: { + color: 'grey', + }, + value: totalShareOther, + name: 'Other' + (this.isMobile() ? `` : ` (${totalShareOther.toFixed(2)}%)`), + label: { + overflow: 'none', + color: '#b1b1b1', + alignTo: 'edge', + edgeDistance: edgeDistance + }, + tooltip: { + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + }, + borderColor: '#000', + formatter: () => { + if (this.poolsWindowPreference === '24h') { + return `${'Other'} (${totalShareOther.toFixed(2)}%)
` + + totalEstimatedHashrateOther.toString() + ' PH/s' + + `
` + totalBlockOther.toString() + ` blocks`; + } else { + return `${'Other'} (${totalShareOther.toFixed(2)}%)
` + + totalBlockOther.toString() + ` blocks`; + } + } + }, + data: 9999 as any, + } as PieSeriesOption); + return data; } @@ -154,9 +207,22 @@ export class PoolRankingComponent implements OnInit { } network = network.charAt(0).toUpperCase() + network.slice(1); - let radius: any[] = ['20%', '70%']; - if (this.isMobile() || this.widget) { - radius = ['20%', '60%']; + let radius: any[] = ['20%', '80%']; + let top: any = undefined; let bottom = undefined; let height = undefined; + if (this.isMobile() && this.widget) { + top = -30; + height = 270; + radius = ['10%', '50%']; + } else if (this.isMobile() && !this.widget) { + top = 0; + height = 300; + radius = ['10%', '50%']; + } else if (this.widget) { + radius = ['15%', '60%']; + top = -20; + height = 330; + } else { + top = 35; } this.chartOptions = { @@ -180,14 +246,15 @@ export class PoolRankingComponent implements OnInit { }, series: [ { - top: this.widget ? 0 : 35, + minShowLabelAngle: 3.6, + top: top, + bottom: bottom, + height: height, name: 'Mining pool', type: 'pie', radius: radius, data: this.generatePoolsChartSerieData(miningStats), labelLine: { - length: this.isMobile() ? 10 : 15, - length2: this.isMobile() ? 0 : 15, lineStyle: { width: 2, }, @@ -223,6 +290,9 @@ export class PoolRankingComponent implements OnInit { this.chartInstance = ec; this.chartInstance.on('click', (e) => { + if (e.data.data === 9999) { // "Other" + return; + } this.router.navigate(['/mining/pool/', e.data.data]); }); } @@ -230,7 +300,7 @@ export class PoolRankingComponent implements OnInit { /** * Default mining stats if something goes wrong */ - getEmptyMiningStat() { + getEmptyMiningStat(): MiningStats { return { lastEstimatedHashrate: 'Error', blockCount: 0, diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index d8760d1f0..a05671257 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -64,7 +64,7 @@ export interface SinglePoolStats { blockCount: number; emptyBlocks: number; rank: number; - share: string; + share: number; lastEstimatedHashrate: string; emptyBlockRatio: string; logo: string; @@ -75,13 +75,6 @@ export interface PoolsStats { oldestIndexedBlockTimestamp: number; pools: SinglePoolStats[]; } -export interface MiningStats { - lastEstimatedHashrate: string; - blockCount: number; - totalEmptyBlock: number; - totalEmptyBlockRatio: string; - pools: SinglePoolStats[]; -} /** * Pool component diff --git a/frontend/src/app/services/mining.service.ts b/frontend/src/app/services/mining.service.ts index c216515b0..68f7e9da1 100644 --- a/frontend/src/app/services/mining.service.ts +++ b/frontend/src/app/services/mining.service.ts @@ -73,7 +73,7 @@ export class MiningService { const totalEmptyBlockRatio = (totalEmptyBlock / stats.blockCount * 100).toFixed(2); const poolsStats = stats.pools.map((poolStat) => { return { - share: (poolStat.blockCount / stats.blockCount * 100).toFixed(2), + share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)), lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider).toFixed(2), emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2), logo: `./resources/mining-pools/` + poolStat.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg',