Merge branch 'master' into simon/track-utxos

This commit is contained in:
softsimon 2022-03-07 15:12:36 +01:00 committed by GitHub
commit 00ad58c26d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 754 additions and 237 deletions

View File

@ -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) {

View File

@ -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;
}

View File

@ -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<object> {
return await BlocksRepository.$getBlocksDifficulty(interval);
}
/**
* Return the historical hashrates and oldest indexed block timestamp
*/
public async $getNetworkHistoricalHashrates(interval: string | null): Promise<object> {
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<object> {
return await HashratesRepository.$getPoolsWeeklyHashrate(interval);
}
/**
* Generate daily hashrate data
*/
public async $generateNetworkHashrateHistory(): Promise<void> {
// 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<void> {
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<void> {
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();

View File

@ -11,6 +11,7 @@ export class DB {
password: config.DATABASE.PASSWORD,
connectionLimit: 10,
supportBigNumbers: true,
timezone: '+00:00',
});
}

View File

@ -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);
}

View File

@ -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 <EmptyBlocks[]>rows;
return <EmptyBlocks[]>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 <number>rows[0].blockCount;
return <number>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 <number>rows[0];
return <number>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 <number>rows[0].blockTimestamp;
} catch (e) {
connection.release();
logger.err('$oldestBlockTimestamp() error' + (e instanceof Error ? e.message : e));
throw e;
}
return <number>rows[0].blockTimestamp;
}
/**
* Get blocks mined by a specific mining pool
*/
public async $getBlocksByPool(
poolId: number,
startHeight: number | null = null
): Promise<object[]> {
public async $getBlocksByPool(poolId: number, startHeight: number | null = null): Promise<object[]> {
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 <object[]>rows) {
delete block['blockTimestamp'];
for (const block of <object[]>rows) {
delete block['blockTimestamp'];
}
return <object[]>rows;
} catch (e) {
connection.release();
logger.err('$getBlocksByPool() error' + (e instanceof Error ? e.message : e));
throw e;
}
return <object[]>rows;
}
/**
@ -242,19 +277,25 @@ class BlocksRepository {
*/
public async $getBlockByHeight(height: number): Promise<object | null> {
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<number> {
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;
}
}
}

View File

@ -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<any[]> {
@ -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<number[]> {
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<any>(query, (val === null) ? [Math.round(new Date().getTime() / 1000)] : [val]);
connection.release();
try {
await connection.query<any>(query, (val === null) ? [Math.round(new Date().getTime() / 1000), key] : [val, key]);
connection.release();
} catch (e) {
connection.release();
}
}
public async $getLatestRunTimestamp(): Promise<number> {
public async $getLatestRunTimestamp(key: string): Promise<number> {
const connection = await DB.pool.getConnection();
const query = `SELECT number FROM state WHERE name = 'last_hashrates_indexing'`;
const [rows] = await connection.query<any>(query);
connection.release();
return rows[0]['number'];
const query = `SELECT number FROM state WHERE name = ?`;
try {
const [rows] = await connection.query<any>(query, [key]);
connection.release();
return rows[0]['number'];
} catch (e) {
connection.release();
logger.err('$setLatestRunTimestamp() error' + (e instanceof Error ? e.message : e));
throw e;
}
}
}

View File

@ -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 <PoolInfo[]>rows;
return <PoolInfo[]>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<PoolInfo[]> {
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 <PoolInfo[]>rows;
return <PoolInfo[]>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;
}
}
}

View File

@ -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');

View File

@ -1,4 +1,4 @@
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<div *ngIf="showTitle" class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<div class="card-wrapper">
<div class="card">
<div class="card-body more-padding">
@ -47,7 +47,7 @@
</div>
</div>
<div class="item" *ngIf="showHalving">
<h5 class="card-title" i18n="difficulty-box.next-halving">Next halving</h5>
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>

View File

@ -28,8 +28,9 @@ export class DifficultyComponent implements OnInit {
isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>;
@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 {

View File

@ -1,6 +1,6 @@
<div [class]="widget === false ? 'full-container' : ''">
<div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
<div *ngIf="!tableOnly" class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as hashrates">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90">
@ -25,34 +25,31 @@
</form>
</div>
<div *ngIf="hashrateObservable$ | async" [class]="!widget ? 'chart' : 'chart-widget'"
<div *ngIf="(hashrateObservable$ | async) && !tableOnly" [class]="!widget ? 'chart' : 'chart-widget'"
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
<!-- <div class="mt-3" *ngIf="!widget">
<table class="table table-borderless table-sm text-center">
<div [class]="!widget ? 'mt-3 p-2' : 'ml-4 mr-4 mt-1'" *ngIf="tableOnly">
<table class="table table-borderless table-sm text-left" [class]="widget ? 'compact' : ''">
<thead>
<tr>
<th i18n="mining.rank">Block</th>
<th class="d-none d-md-block" i18n="block.timestamp">Timestamp</th>
<th i18n="mining.adjusted">Adjusted</th>
<th i18n="mining.difficulty">Difficulty</th>
<th i18n="mining.change">Change</th>
<th i18n="mining.difficulty" class="text-right">Difficulty</th>
<th i18n="mining.change" class="text-right">Change</th>
</tr>
</thead>
<tbody *ngIf="(hashrateObservable$ | async) as data">
<tr *ngFor="let diffChange of data.difficulty">
<td><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
<td class="d-none d-md-block">&lrm;{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
<td><app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since></td>
<td class="d-none d-md-block">{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}</td>
<td class="d-block d-md-none">{{ diffChange.difficultyShorten }}</td>
<td [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
<td class="text-right">{{ diffChange.difficultyShorten }}</td>
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
</tr>
</tbody>
</table>
</div> -->
</div>
</div>

View File

@ -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;
}

View File

@ -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'

View File

@ -27,7 +27,7 @@
<div *ngIf="hashrateObservable$ | async" [class]="!widget ? 'chart' : 'chart-widget'"
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div class="text-center loadingGraphs" [class]="widget ? 'widget' : ''" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>

View File

@ -29,7 +29,7 @@
.chart-widget {
width: 100%;
height: 100%;
max-height: 275px;
max-height: 293px;
}
.formRadioGroup {

View File

@ -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]}%<br>`
tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1].toFixed(2)}%<br>`;
}
}
return tooltip;

View File

@ -3,35 +3,97 @@
<div class="row row-cols-1 row-cols-md-2">
<div class="col">
<div class="card double">
<div class="card-body">
<!-- pool distribution -->
<h5 class="card-title">
<a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="mining.pool-share">
Mining Pools Share (1w)
</a>
</h5>
<app-pool-ranking [widget]=true></app-pool-ranking>
<div class="main-title">Reward stats</div>
<div class="card" style="height: 123px">
<div class="card-body more-padding">
<div class="difficulty-adjustment-container" *ngIf="$rewardStats | async as rewardStats">
<div class="item">
<h5 class="card-title" i18n="">Miners Reward</h5>
<div class="card-text">
<app-amount [satoshis]="rewardStats.totalReward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
<div class="symbol">in the last 8 blocks</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="">Reward Per Tx</h5>
<div class="card-text">
{{ rewardStats.rewardPerTx }}
<span class="symbol">sats/tx</span>
<div class="symbol">in the last 8 blocks</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="">Average Fee</h5>
<div class="card-text">
{{ rewardStats.feePerTx }}
<span class="symbol">sats/tx</span>
<div class="symbol">in the last 8 blocks</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- pools hashrate -->
<!-- difficulty adjustment -->
<div class="col">
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<div class="card" style="height: 123px">
<app-difficulty [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
</div>
</div>
<!-- pool distribution -->
<div class="col">
<div class="card" style="height: 385px">
<div class="card-body">
<app-pool-ranking [widget]=true></app-pool-ranking>
<div class="mt-1"><a [routerLink]="['/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
</div>
</div>
</div>
<!-- hashrate -->
<div class="col">
<div class="card" style="height: 385px">
<div class="card-body">
<h5 class="card-title">
Hashrate (1y)
</h5>
<app-hashrate-chart [widget]=true></app-hashrate-chart>
<div class="mt-1"><a [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
</div>
</div>
</div>
<!-- pool dominance -->
<div class="col">
<div class="card" style="height: 385px">
<div class="card-body">
<h5 class="card-title">
Mining Pools Dominance (1y)
</h5>
<app-hashrate-chart-pools [widget]=true></app-hashrate-chart-pools>
<div class="mt-1"><a [routerLink]="['/mining/hashrate/pools' | relativeUrl]" i18n="dashboard.view-more">View
more &raquo;</a></div>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card" style="height: 385px">
<div class="card-body">
<!-- hashrate -->
<h5 class="card-title">
<a class="link" href="" [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="mining.hashrate">
Hashrate (1y)
</a>
Adjusments
</h5>
<app-hashrate-chart [widget]=true></app-hashrate-chart>
<app-hashrate-chart [tableOnly]=true [widget]=true></app-hashrate-chart>
<div class="mt-1"><a [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -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;
}
}

View File

@ -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<any>;
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'),
}
})
);
}
}

View File

@ -1,8 +1,29 @@
<div [class]="widget === false ? 'container-xl' : ''">
<div class="pool-distribution" *ngIf="widget && (miningStatsObservable$ | async) as miningStats">
<div class="item">
<h5 class="card-title" i18n="mining.miners-luck">Pools luck (1w)</h5>
<p class="card-text">
{{ miningStats['minersLuck'] }}%
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="master-page.blocks">Blocks (1w)</h5>
<p class="card-text">
{{ miningStats.blockCount }}
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.miners-count">Pools count (1w)</h5>
<p class="card-text">
{{ miningStats.pools.length }}
</p>
</div>
</div>
<div [class]="widget ? 'chart-widget' : 'chart'"
echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
<div class="text-center loadingGraphs" [class]="widget ? 'widget' : ''" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
@ -59,7 +80,7 @@
<td class="d-none d-md-block">{{ pool.rank }}</td>
<td class="text-right"><img width="25" height="25" src="{{ pool.logo }}" onError="this.src = './resources/mining-pools/default.svg'"></td>
<td class=""><a [routerLink]="[('/mining/pool/' + pool.poolId) | relativeUrl]">{{ pool.name }}</a></td>
<td class="" *ngIf="this.poolsWindowPreference === '24h'">{{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }}</td>
<td class="" *ngIf="this.poolsWindowPreference === '24h' && !isLoading">{{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }}</td>
<td class="">{{ pool['blockText'] }}</td>
<td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
</tr>

View File

@ -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;
}
}
}
}

View File

@ -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 `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
totalEstimatedHashrateOther.toString() + ' PH/s' +
`<br>` + totalBlockOther.toString() + ` blocks`;
} else {
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
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,

View File

@ -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

View File

@ -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',