Merge branch 'master' into simon/track-utxos
This commit is contained in:
commit
00ad58c26d
@ -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() {
|
public async $generateBlockDatabase() {
|
||||||
if (this.blockIndexingStarted) {
|
if (this.blockIndexingStarted) {
|
||||||
|
@ -6,7 +6,7 @@ import logger from '../logger';
|
|||||||
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
|
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 8;
|
private static currentVersion = 9;
|
||||||
private queryTimeout = 120000;
|
private queryTimeout = 120000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
|
|
||||||
@ -133,6 +133,13 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
|
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();
|
connection.release();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
connection.release();
|
connection.release();
|
||||||
@ -276,6 +283,10 @@ class DatabaseMigration {
|
|||||||
queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`);
|
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;
|
return queries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import blocks from './blocks';
|
|||||||
|
|
||||||
class Mining {
|
class Mining {
|
||||||
hashrateIndexingStarted = false;
|
hashrateIndexingStarted = false;
|
||||||
|
weeklyHashrateIndexingStarted = false;
|
||||||
|
|
||||||
constructor() {
|
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> {
|
public async $generatePoolHashrateHistory(): Promise<void> {
|
||||||
return await BlocksRepository.$getBlocksDifficulty(interval);
|
if (!blocks.blockIndexingCompleted || this.weeklyHashrateIndexingStarted) {
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
return;
|
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) {
|
if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) {
|
||||||
return;
|
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 {
|
try {
|
||||||
this.hashrateIndexingStarted = true;
|
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 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 indexedThisRun = 0;
|
||||||
let totalIndexed = 0;
|
let totalIndexed = 0;
|
||||||
|
let startedAt = new Date().getTime() / 1000;
|
||||||
const hashrates: any[] = [];
|
|
||||||
|
|
||||||
while (toTimestamp > genesisTimestamp) {
|
while (toTimestamp > genesisTimestamp) {
|
||||||
const fromTimestamp = toTimestamp - 86400;
|
const fromTimestamp = toTimestamp - 86400;
|
||||||
|
|
||||||
|
// Skip already indexed weeks
|
||||||
if (indexedTimestamp.includes(fromTimestamp)) {
|
if (indexedTimestamp.includes(fromTimestamp)) {
|
||||||
toTimestamp -= 86400;
|
toTimestamp -= 86400;
|
||||||
++totalIndexed;
|
++totalIndexed;
|
||||||
@ -140,33 +215,11 @@ class Mining {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastBlockHashrate = 0;
|
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
|
||||||
lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
|
|
||||||
blockStats.lastBlockHeight);
|
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({
|
hashrates.push({
|
||||||
hashrateTimestamp: fromTimestamp + 1,
|
hashrateTimestamp: toTimestamp,
|
||||||
avgHashrate: pool['hashrate'],
|
|
||||||
poolId: pool.poolId,
|
|
||||||
share: pool['share'],
|
|
||||||
type: 'weekly',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hashrates.push({
|
|
||||||
hashrateTimestamp: fromTimestamp,
|
|
||||||
avgHashrate: lastBlockHashrate,
|
avgHashrate: lastBlockHashrate,
|
||||||
poolId: null,
|
poolId: null,
|
||||||
share: 1,
|
share: 1,
|
||||||
@ -183,7 +236,7 @@ class Mining {
|
|||||||
const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
|
const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
|
||||||
const formattedDate = new Date(fromTimestamp * 1000).toUTCString();
|
const formattedDate = new Date(fromTimestamp * 1000).toUTCString();
|
||||||
const daysLeft = Math.round(totalDayIndexed - totalIndexed);
|
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;
|
startedAt = new Date().getTime() / 1000;
|
||||||
indexedThisRun = 0;
|
indexedThisRun = 0;
|
||||||
}
|
}
|
||||||
@ -203,18 +256,25 @@ class Mining {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hashrates.length > 0) {
|
|
||||||
await HashratesRepository.$saveHashrates(hashrates);
|
await HashratesRepository.$saveHashrates(hashrates);
|
||||||
}
|
|
||||||
await HashratesRepository.$setLatestRunTimestamp();
|
|
||||||
this.hashrateIndexingStarted = false;
|
|
||||||
|
|
||||||
logger.info(`Hashrates indexing completed`);
|
await HashratesRepository.$setLatestRunTimestamp('last_hashrates_indexing');
|
||||||
|
this.hashrateIndexingStarted = false;
|
||||||
|
logger.info(`Daily network hashrate indexing completed`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.hashrateIndexingStarted = false;
|
this.hashrateIndexingStarted = false;
|
||||||
throw e;
|
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();
|
export default new Mining();
|
||||||
|
@ -11,6 +11,7 @@ export class DB {
|
|||||||
password: config.DATABASE.PASSWORD,
|
password: config.DATABASE.PASSWORD,
|
||||||
connectionLimit: 10,
|
connectionLimit: 10,
|
||||||
supportBigNumbers: true,
|
supportBigNumbers: true,
|
||||||
|
timezone: '+00:00',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +167,8 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async $resetHashratesIndexingState() {
|
async $resetHashratesIndexingState() {
|
||||||
return await HashratesRepository.$setLatestRunTimestamp(0);
|
await HashratesRepository.$setLatestRunTimestamp('last_hashrates_indexing', 0);
|
||||||
|
await HashratesRepository.$setLatestRunTimestamp('last_weekly_hashrates_indexing', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $runIndexingWhenReady() {
|
async $runIndexingWhenReady() {
|
||||||
@ -176,8 +177,9 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await blocks.$generateBlockDatabase();
|
blocks.$generateBlockDatabase();
|
||||||
await mining.$generateNetworkHashrateHistory();
|
mining.$generateNetworkHashrateHistory();
|
||||||
|
mining.$generatePoolHashrateHistory();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Unable to run indexing right now, trying again later. ` + e);
|
logger.err(`Unable to run indexing right now, trying again later. ` + e);
|
||||||
}
|
}
|
||||||
|
@ -53,15 +53,17 @@ class BlocksRepository {
|
|||||||
|
|
||||||
// logger.debug(query);
|
// logger.debug(query);
|
||||||
await connection.query(query, params);
|
await connection.query(query, params);
|
||||||
|
connection.release();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
connection.release();
|
||||||
if (e.errno === 1062) { // ER_DUP_ENTRY
|
if (e.errno === 1062) { // ER_DUP_ENTRY
|
||||||
logger.debug(`$saveBlockInDatabase() - Block ${block.height} has already been indexed, ignoring`);
|
logger.debug(`$saveBlockInDatabase() - Block ${block.height} has already been indexed, ignoring`);
|
||||||
} else {
|
} else {
|
||||||
logger.err('$saveBlockInDatabase() error' + (e instanceof Error ? e.message : e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.release();
|
connection.release();
|
||||||
|
logger.err('$saveBlockInDatabase() error' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,6 +75,7 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
|
try {
|
||||||
const [rows]: any[] = await connection.query(`
|
const [rows]: any[] = await connection.query(`
|
||||||
SELECT height
|
SELECT height
|
||||||
FROM blocks
|
FROM blocks
|
||||||
@ -87,6 +90,11 @@ class BlocksRepository {
|
|||||||
const missingBlocksHeights = seekedBlocks.filter(x => indexedBlockHeights.indexOf(x) === -1);
|
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);
|
// logger.debug(query);
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
|
try {
|
||||||
const [rows] = await connection.query(query, params);
|
const [rows] = await connection.query(query, params);
|
||||||
connection.release();
|
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);
|
// logger.debug(query);
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
|
try {
|
||||||
const [rows] = await connection.query(query, params);
|
const [rows] = await connection.query(query, params);
|
||||||
connection.release();
|
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);
|
// logger.debug(query);
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
|
try {
|
||||||
const [rows] = await connection.query(query, params);
|
const [rows] = await connection.query(query, params);
|
||||||
connection.release();
|
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,6 +220,7 @@ class BlocksRepository {
|
|||||||
|
|
||||||
// logger.debug(query);
|
// logger.debug(query);
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
|
try {
|
||||||
const [rows]: any[] = await connection.query(query);
|
const [rows]: any[] = await connection.query(query);
|
||||||
connection.release();
|
connection.release();
|
||||||
|
|
||||||
@ -202,15 +229,17 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <number>rows[0].blockTimestamp;
|
return <number>rows[0].blockTimestamp;
|
||||||
|
} catch (e) {
|
||||||
|
connection.release();
|
||||||
|
logger.err('$oldestBlockTimestamp() error' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get blocks mined by a specific mining pool
|
* Get blocks mined by a specific mining pool
|
||||||
*/
|
*/
|
||||||
public async $getBlocksByPool(
|
public async $getBlocksByPool(poolId: number, startHeight: number | null = null): Promise<object[]> {
|
||||||
poolId: number,
|
|
||||||
startHeight: number | null = null
|
|
||||||
): Promise<object[]> {
|
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
let query = `SELECT height, hash as id, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, reward
|
let query = `SELECT height, hash as id, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, reward
|
||||||
FROM blocks
|
FROM blocks
|
||||||
@ -227,6 +256,7 @@ class BlocksRepository {
|
|||||||
|
|
||||||
// logger.debug(query);
|
// logger.debug(query);
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
|
try {
|
||||||
const [rows] = await connection.query(query, params);
|
const [rows] = await connection.query(query, params);
|
||||||
connection.release();
|
connection.release();
|
||||||
|
|
||||||
@ -235,6 +265,11 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <object[]>rows;
|
return <object[]>rows;
|
||||||
|
} catch (e) {
|
||||||
|
connection.release();
|
||||||
|
logger.err('$getBlocksByPool() error' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -242,6 +277,7 @@ class BlocksRepository {
|
|||||||
*/
|
*/
|
||||||
public async $getBlockByHeight(height: number): Promise<object | null> {
|
public async $getBlockByHeight(height: number): Promise<object | null> {
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
|
try {
|
||||||
const [rows]: any[] = await connection.query(`
|
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
|
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
|
FROM blocks
|
||||||
@ -255,6 +291,11 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return rows[0];
|
return rows[0];
|
||||||
|
} catch (e) {
|
||||||
|
connection.release();
|
||||||
|
logger.err('$getBlockByHeight() error' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -297,6 +338,7 @@ class BlocksRepository {
|
|||||||
ORDER BY t.height
|
ORDER BY t.height
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
const [rows]: any[] = await connection.query(query);
|
const [rows]: any[] = await connection.query(query);
|
||||||
connection.release();
|
connection.release();
|
||||||
|
|
||||||
@ -305,13 +347,25 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
connection.release();
|
||||||
|
logger.err('$getBlocksDifficulty() error' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getOldestIndexedBlockHeight(): Promise<number> {
|
public async $getOldestIndexedBlockHeight(): Promise<number> {
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
|
try {
|
||||||
const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`);
|
const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`);
|
||||||
connection.release();
|
connection.release();
|
||||||
|
|
||||||
return rows[0].minHeight;
|
return rows[0].minHeight;
|
||||||
|
} catch (e) {
|
||||||
|
connection.release();
|
||||||
|
logger.err('$getOldestIndexedBlockHeight() error' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,10 @@ class HashratesRepository {
|
|||||||
* Save indexed block data in the database
|
* Save indexed block data in the database
|
||||||
*/
|
*/
|
||||||
public async $saveHashrates(hashrates: any) {
|
public async $saveHashrates(hashrates: any) {
|
||||||
|
if (hashrates.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let query = `INSERT INTO
|
let query = `INSERT INTO
|
||||||
hashrates(hashrate_timestamp, avg_hashrate, pool_id, share, type) VALUES`;
|
hashrates(hashrate_timestamp, avg_hashrate, pool_id, share, type) VALUES`;
|
||||||
|
|
||||||
@ -20,12 +24,12 @@ class HashratesRepository {
|
|||||||
try {
|
try {
|
||||||
// logger.debug(query);
|
// logger.debug(query);
|
||||||
await connection.query(query);
|
await connection.query(query);
|
||||||
|
connection.release();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
connection.release();
|
||||||
logger.err('$saveHashrateInDatabase() error' + (e instanceof Error ? e.message : e));
|
logger.err('$saveHashrateInDatabase() error' + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.release();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
|
public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
|
||||||
@ -47,10 +51,33 @@ class HashratesRepository {
|
|||||||
|
|
||||||
query += ` ORDER by hashrate_timestamp`;
|
query += ` ORDER by hashrate_timestamp`;
|
||||||
|
|
||||||
|
try {
|
||||||
const [rows]: any[] = await connection.query(query);
|
const [rows]: any[] = await connection.query(query);
|
||||||
connection.release();
|
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})`;
|
query += ` ORDER by hashrate_timestamp, FIELD(pool_id, ${topPoolsId})`;
|
||||||
|
|
||||||
|
try {
|
||||||
const [rows]: any[] = await connection.query(query);
|
const [rows]: any[] = await connection.query(query);
|
||||||
connection.release();
|
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 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]);
|
try {
|
||||||
|
await connection.query<any>(query, (val === null) ? [Math.round(new Date().getTime() / 1000), key] : [val, key]);
|
||||||
|
connection.release();
|
||||||
|
} catch (e) {
|
||||||
connection.release();
|
connection.release();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async $getLatestRunTimestamp(): Promise<number> {
|
public async $getLatestRunTimestamp(key: string): Promise<number> {
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
const query = `SELECT number FROM state WHERE name = 'last_hashrates_indexing'`;
|
const query = `SELECT number FROM state WHERE name = ?`;
|
||||||
const [rows] = await connection.query<any>(query);
|
|
||||||
|
try {
|
||||||
|
const [rows] = await connection.query<any>(query, [key]);
|
||||||
connection.release();
|
connection.release();
|
||||||
|
|
||||||
return rows[0]['number'];
|
return rows[0]['number'];
|
||||||
|
} catch (e) {
|
||||||
|
connection.release();
|
||||||
|
logger.err('$setLatestRunTimestamp() error' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,26 +43,38 @@ class PoolsRepository {
|
|||||||
|
|
||||||
// logger.debug(query);
|
// logger.debug(query);
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
|
try {
|
||||||
const [rows] = await connection.query(query);
|
const [rows] = await connection.query(query);
|
||||||
connection.release();
|
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
|
* Get basic pool info and block count between two timestamp
|
||||||
*/
|
*/
|
||||||
public async $getPoolsInfoBetween(from: number, to: number): Promise<PoolInfo[]> {
|
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
|
FROM pools
|
||||||
LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?)
|
LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?)
|
||||||
GROUP BY pools.id`;
|
GROUP BY pools.id`;
|
||||||
|
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
|
try {
|
||||||
const [rows] = await connection.query(query, [from, to]);
|
const [rows] = await connection.query(query, [from, to]);
|
||||||
connection.release();
|
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,6 +88,7 @@ class PoolsRepository {
|
|||||||
|
|
||||||
// logger.debug(query);
|
// logger.debug(query);
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
|
try {
|
||||||
const [rows] = await connection.query(query, [poolId]);
|
const [rows] = await connection.query(query, [poolId]);
|
||||||
connection.release();
|
connection.release();
|
||||||
|
|
||||||
@ -83,6 +96,11 @@ class PoolsRepository {
|
|||||||
rows[0].addresses = JSON.parse(rows[0].addresses);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import miningStats from './api/mining';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import mining from './api/mining';
|
import mining from './api/mining';
|
||||||
import BlocksRepository from './repositories/BlocksRepository';
|
import BlocksRepository from './repositories/BlocksRepository';
|
||||||
|
import HashratesRepository from './repositories/HashratesRepository';
|
||||||
|
|
||||||
class Routes {
|
class Routes {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
@ -576,7 +577,7 @@ class Routes {
|
|||||||
|
|
||||||
public async $getHistoricalDifficulty(req: Request, res: Response) {
|
public async $getHistoricalDifficulty(req: Request, res: Response) {
|
||||||
try {
|
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('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
@ -588,7 +589,7 @@ class Routes {
|
|||||||
|
|
||||||
public async $getPoolsHistoricalHashrate(req: Request, res: Response) {
|
public async $getPoolsHistoricalHashrate(req: Request, res: Response) {
|
||||||
try {
|
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();
|
const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
@ -604,8 +605,8 @@ class Routes {
|
|||||||
|
|
||||||
public async $getHistoricalHashrate(req: Request, res: Response) {
|
public async $getHistoricalHashrate(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const hashrates = await mining.$getNetworkHistoricalHashrates(req.params.interval ?? null);
|
const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval ?? null);
|
||||||
const difficulty = await mining.$getHistoricalDifficulty(req.params.interval ?? null);
|
const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval ?? null);
|
||||||
const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
|
const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
|
@ -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-wrapper">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body more-padding">
|
<div class="card-body more-padding">
|
||||||
@ -47,7 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" *ngIf="showHalving">
|
<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">
|
<div class="card-text">
|
||||||
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
|
<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>
|
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||||
|
@ -28,8 +28,9 @@ export class DifficultyComponent implements OnInit {
|
|||||||
isLoadingWebSocket$: Observable<boolean>;
|
isLoadingWebSocket$: Observable<boolean>;
|
||||||
difficultyEpoch$: Observable<EpochProgress>;
|
difficultyEpoch$: Observable<EpochProgress>;
|
||||||
|
|
||||||
@Input() showProgress: boolean = true;
|
@Input() showProgress = true;
|
||||||
@Input() showHalving: boolean = false;
|
@Input() showHalving = false;
|
||||||
|
@Input() showTitle = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
@ -97,7 +98,7 @@ export class DifficultyComponent implements OnInit {
|
|||||||
colorPreviousAdjustments = '#ffffff66';
|
colorPreviousAdjustments = '#ffffff66';
|
||||||
}
|
}
|
||||||
|
|
||||||
const blocksUntilHalving = block.height % 210000;
|
const blocksUntilHalving = 210000 - (block.height % 210000);
|
||||||
const timeUntilHalving = (blocksUntilHalving * timeAvgMins * 60 * 1000) + (now * 1000);
|
const timeUntilHalving = (blocksUntilHalving * timeAvgMins * 60 * 1000) + (now * 1000);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div [class]="widget === false ? 'full-container' : ''">
|
<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">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as hashrates">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90">
|
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90">
|
||||||
@ -25,34 +25,31 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</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>
|
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
||||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||||
<div class="spinner-border text-light"></div>
|
<div class="spinner-border text-light"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="mt-3" *ngIf="!widget">
|
<div [class]="!widget ? 'mt-3 p-2' : 'ml-4 mr-4 mt-1'" *ngIf="tableOnly">
|
||||||
<table class="table table-borderless table-sm text-center">
|
<table class="table table-borderless table-sm text-left" [class]="widget ? 'compact' : ''">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th i18n="mining.rank">Block</th>
|
|
||||||
<th class="d-none d-md-block" i18n="block.timestamp">Timestamp</th>
|
<th class="d-none d-md-block" i18n="block.timestamp">Timestamp</th>
|
||||||
<th i18n="mining.adjusted">Adjusted</th>
|
<th i18n="mining.adjusted">Adjusted</th>
|
||||||
<th i18n="mining.difficulty">Difficulty</th>
|
<th i18n="mining.difficulty" class="text-right">Difficulty</th>
|
||||||
<th i18n="mining.change">Change</th>
|
<th i18n="mining.change" class="text-right">Change</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody *ngIf="(hashrateObservable$ | async) as data">
|
<tbody *ngIf="(hashrateObservable$ | async) as data">
|
||||||
<tr *ngFor="let diffChange of data.difficulty">
|
<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">‎{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
|
<td class="d-none d-md-block">‎{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
|
||||||
<td><app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since></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="text-right">{{ diffChange.difficultyShorten }}</td>
|
||||||
<td class="d-block d-md-none">{{ diffChange.difficultyShorten }}</td>
|
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
|
||||||
<td [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div> -->
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
.chart-widget {
|
.chart-widget {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 275px;
|
max-height: 293px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formRadioGroup {
|
.formRadioGroup {
|
||||||
@ -48,3 +48,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compact td {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0.15rem !important;
|
||||||
|
}
|
@ -22,7 +22,8 @@ import { selectPowerOfTen } from 'src/app/bitcoin.utils';
|
|||||||
`],
|
`],
|
||||||
})
|
})
|
||||||
export class HashrateChartComponent implements OnInit {
|
export class HashrateChartComponent implements OnInit {
|
||||||
@Input() widget: boolean = false;
|
@Input() tableOnly = false;
|
||||||
|
@Input() widget = false;
|
||||||
@Input() right: number | string = 45;
|
@Input() right: number | string = 45;
|
||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
@ -114,7 +115,7 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
availableTimespanDay: availableTimespanDay,
|
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,
|
bottom: this.widget ? 30 : 60,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
show: !this.isMobile() || !this.widget,
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
type: 'line'
|
type: 'line'
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
<div *ngIf="hashrateObservable$ | async" [class]="!widget ? 'chart' : 'chart-widget'"
|
<div *ngIf="hashrateObservable$ | async" [class]="!widget ? 'chart' : 'chart-widget'"
|
||||||
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
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 class="spinner-border text-light"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
.chart-widget {
|
.chart-widget {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 275px;
|
max-height: 293px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formRadioGroup {
|
.formRadioGroup {
|
||||||
|
@ -78,7 +78,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
|||||||
name: name,
|
name: name,
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
symbol: 'none',
|
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',
|
type: 'line',
|
||||||
lineStyle: { width: 0 },
|
lineStyle: { width: 0 },
|
||||||
areaStyle: { opacity: 1 },
|
areaStyle: { opacity: 1 },
|
||||||
@ -132,6 +132,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
|||||||
top: this.widget ? 10 : 40,
|
top: this.widget ? 10 : 40,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
show: !this.isMobile() || !this.widget,
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
type: 'line'
|
type: 'line'
|
||||||
@ -149,7 +150,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
|||||||
data.sort((a, b) => b.data[1] - a.data[1]);
|
data.sort((a, b) => b.data[1] - a.data[1]);
|
||||||
for (const pool of data) {
|
for (const pool of data) {
|
||||||
if (pool.data[1] > 0) {
|
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;
|
return tooltip;
|
||||||
|
@ -3,32 +3,94 @@
|
|||||||
<div class="row row-cols-1 row-cols-md-2">
|
<div class="row row-cols-1 row-cols-md-2">
|
||||||
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card double">
|
<div class="main-title">Reward stats</div>
|
||||||
<div class="card-body">
|
<div class="card" style="height: 123px">
|
||||||
<!-- pool distribution -->
|
<div class="card-body more-padding">
|
||||||
<h5 class="card-title">
|
<div class="difficulty-adjustment-container" *ngIf="$rewardStats | async as rewardStats">
|
||||||
<a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="mining.pool-share">
|
<div class="item">
|
||||||
Mining Pools Share (1w)
|
<h5 class="card-title" i18n="">Miners Reward</h5>
|
||||||
</a>
|
<div class="card-text">
|
||||||
</h5>
|
<app-amount [satoshis]="rewardStats.totalReward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
||||||
<app-pool-ranking [widget]=true></app-pool-ranking>
|
<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
|
||||||
|
»</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
|
||||||
|
»</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>
|
<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 »</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card" style="height: 385px">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- hashrate -->
|
|
||||||
<h5 class="card-title">
|
<h5 class="card-title">
|
||||||
<a class="link" href="" [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="mining.hashrate">
|
Adjusments
|
||||||
Hashrate (1y)
|
|
||||||
</a>
|
|
||||||
</h5>
|
</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
|
||||||
|
»</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,14 +12,11 @@
|
|||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: #1d1f31;
|
background-color: #1d1f31;
|
||||||
height: 340px;
|
|
||||||
}
|
|
||||||
.card.double {
|
|
||||||
height: 620px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
color: #4a68b9;
|
||||||
}
|
}
|
||||||
.card-title > a {
|
.card-title > a {
|
||||||
color: #4a68b9;
|
color: #4a68b9;
|
||||||
@ -58,3 +55,91 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
padding-bottom: 3px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 { 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({
|
@Component({
|
||||||
selector: 'app-mining-dashboard',
|
selector: 'app-mining-dashboard',
|
||||||
@ -8,12 +13,36 @@ import { SeoService } from 'src/app/services/seo.service';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class MiningDashboardComponent implements OnInit {
|
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`);
|
this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
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'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,29 @@
|
|||||||
<div [class]="widget === false ? 'container-xl' : ''">
|
<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'"
|
<div [class]="widget ? 'chart-widget' : 'chart'"
|
||||||
echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
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 class="spinner-border text-light"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -59,7 +80,7 @@
|
|||||||
<td class="d-none d-md-block">{{ pool.rank }}</td>
|
<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="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=""><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="">{{ pool['blockText'] }}</td>
|
||||||
<td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
|
<td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
.chart {
|
.chart {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
max-height: 300px;
|
max-height: 270px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.chart-widget {
|
.chart-widget {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 275px;
|
max-height: 270px;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.formRadioGroup {
|
.formRadioGroup {
|
||||||
@ -44,3 +47,59 @@
|
|||||||
.loadingGraphs.widget {
|
.loadingGraphs.widget {
|
||||||
top: 25%;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -85,6 +85,7 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
}),
|
}),
|
||||||
map(data => {
|
map(data => {
|
||||||
data.pools = data.pools.map((pool: SinglePoolStats) => this.formatPoolUI(pool));
|
data.pools = data.pools.map((pool: SinglePoolStats) => this.formatPoolUI(pool));
|
||||||
|
data['minersLuck'] = (100 * (data.blockCount / 1008)).toFixed(2); // luck 1w
|
||||||
return data;
|
return data;
|
||||||
}),
|
}),
|
||||||
tap(data => {
|
tap(data => {
|
||||||
@ -105,24 +106,40 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generatePoolsChartSerieData(miningStats) {
|
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[] = [];
|
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) => {
|
miningStats.pools.forEach((pool) => {
|
||||||
if (parseFloat(pool.share) < poolShareThreshold) {
|
if (parseFloat(pool.share) < poolShareThreshold) {
|
||||||
|
totalShareOther += parseFloat(pool.share);
|
||||||
|
totalBlockOther += pool.blockCount;
|
||||||
|
totalEstimatedHashrateOther += pool.lastEstimatedHashrate;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data.push({
|
data.push({
|
||||||
itemStyle: {
|
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,
|
value: pool.share,
|
||||||
name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`),
|
name: pool.name + ((this.isMobile() || this.widget) ? `` : ` (${pool.share}%)`),
|
||||||
label: {
|
label: {
|
||||||
|
overflow: 'none',
|
||||||
color: '#b1b1b1',
|
color: '#b1b1b1',
|
||||||
overflow: 'break',
|
alignTo: 'edge',
|
||||||
|
edgeDistance: edgeDistance,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
show: !this.isMobile() || !this.widget,
|
||||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
@ -144,6 +161,42 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
data: pool.poolId,
|
data: pool.poolId,
|
||||||
} as PieSeriesOption);
|
} 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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,9 +207,22 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
network = network.charAt(0).toUpperCase() + network.slice(1);
|
network = network.charAt(0).toUpperCase() + network.slice(1);
|
||||||
|
|
||||||
let radius: any[] = ['20%', '70%'];
|
let radius: any[] = ['20%', '80%'];
|
||||||
if (this.isMobile() || this.widget) {
|
let top: any = undefined; let bottom = undefined; let height = undefined;
|
||||||
radius = ['20%', '60%'];
|
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 = {
|
this.chartOptions = {
|
||||||
@ -180,14 +246,15 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
top: this.widget ? 0 : 35,
|
minShowLabelAngle: 3.6,
|
||||||
|
top: top,
|
||||||
|
bottom: bottom,
|
||||||
|
height: height,
|
||||||
name: 'Mining pool',
|
name: 'Mining pool',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: radius,
|
radius: radius,
|
||||||
data: this.generatePoolsChartSerieData(miningStats),
|
data: this.generatePoolsChartSerieData(miningStats),
|
||||||
labelLine: {
|
labelLine: {
|
||||||
length: this.isMobile() ? 10 : 15,
|
|
||||||
length2: this.isMobile() ? 0 : 15,
|
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 2,
|
width: 2,
|
||||||
},
|
},
|
||||||
@ -223,6 +290,9 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
|
|
||||||
this.chartInstance = ec;
|
this.chartInstance = ec;
|
||||||
this.chartInstance.on('click', (e) => {
|
this.chartInstance.on('click', (e) => {
|
||||||
|
if (e.data.data === 9999) { // "Other"
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.router.navigate(['/mining/pool/', e.data.data]);
|
this.router.navigate(['/mining/pool/', e.data.data]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -230,7 +300,7 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Default mining stats if something goes wrong
|
* Default mining stats if something goes wrong
|
||||||
*/
|
*/
|
||||||
getEmptyMiningStat() {
|
getEmptyMiningStat(): MiningStats {
|
||||||
return {
|
return {
|
||||||
lastEstimatedHashrate: 'Error',
|
lastEstimatedHashrate: 'Error',
|
||||||
blockCount: 0,
|
blockCount: 0,
|
||||||
|
@ -64,7 +64,7 @@ export interface SinglePoolStats {
|
|||||||
blockCount: number;
|
blockCount: number;
|
||||||
emptyBlocks: number;
|
emptyBlocks: number;
|
||||||
rank: number;
|
rank: number;
|
||||||
share: string;
|
share: number;
|
||||||
lastEstimatedHashrate: string;
|
lastEstimatedHashrate: string;
|
||||||
emptyBlockRatio: string;
|
emptyBlockRatio: string;
|
||||||
logo: string;
|
logo: string;
|
||||||
@ -75,13 +75,6 @@ export interface PoolsStats {
|
|||||||
oldestIndexedBlockTimestamp: number;
|
oldestIndexedBlockTimestamp: number;
|
||||||
pools: SinglePoolStats[];
|
pools: SinglePoolStats[];
|
||||||
}
|
}
|
||||||
export interface MiningStats {
|
|
||||||
lastEstimatedHashrate: string;
|
|
||||||
blockCount: number;
|
|
||||||
totalEmptyBlock: number;
|
|
||||||
totalEmptyBlockRatio: string;
|
|
||||||
pools: SinglePoolStats[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pool component
|
* Pool component
|
||||||
|
@ -73,7 +73,7 @@ export class MiningService {
|
|||||||
const totalEmptyBlockRatio = (totalEmptyBlock / stats.blockCount * 100).toFixed(2);
|
const totalEmptyBlockRatio = (totalEmptyBlock / stats.blockCount * 100).toFixed(2);
|
||||||
const poolsStats = stats.pools.map((poolStat) => {
|
const poolsStats = stats.pools.map((poolStat) => {
|
||||||
return {
|
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),
|
lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider).toFixed(2),
|
||||||
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
|
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
|
||||||
logo: `./resources/mining-pools/` + poolStat.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg',
|
logo: `./resources/mining-pools/` + poolStat.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user