Merge pull request #1269 from nymkappa/feature/hashrate-chart
Created hashrate chart component
This commit is contained in:
		
						commit
						3f5a749352
					
				@ -20,6 +20,7 @@ class Blocks {
 | 
			
		||||
  private previousDifficultyRetarget = 0;
 | 
			
		||||
  private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
 | 
			
		||||
  private blockIndexingStarted = false;
 | 
			
		||||
  public blockIndexingCompleted = false;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
@ -170,10 +171,7 @@ class Blocks {
 | 
			
		||||
   * Index all blocks metadata for the mining dashboard
 | 
			
		||||
   */
 | 
			
		||||
  public async $generateBlockDatabase() {
 | 
			
		||||
    if (this.blockIndexingStarted === true ||
 | 
			
		||||
      !Common.indexingEnabled() ||
 | 
			
		||||
      memPool.hasPriority()
 | 
			
		||||
    ) {
 | 
			
		||||
    if (this.blockIndexingStarted) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -243,6 +241,8 @@ class Blocks {
 | 
			
		||||
      logger.err('An error occured in $generateBlockDatabase(). Skipping block indexing. ' + e);
 | 
			
		||||
      console.log(e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.blockIndexingCompleted = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $updateBlocks() {
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import logger from '../logger';
 | 
			
		||||
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 6;
 | 
			
		||||
  private static currentVersion = 7;
 | 
			
		||||
  private queryTimeout = 120000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
 | 
			
		||||
@ -15,13 +15,13 @@ class DatabaseMigration {
 | 
			
		||||
   * Entry point
 | 
			
		||||
   */
 | 
			
		||||
  public async $initializeOrMigrateDatabase(): Promise<void> {
 | 
			
		||||
    logger.info('MIGRATIONS: Running migrations');
 | 
			
		||||
    logger.debug('MIGRATIONS: Running migrations');
 | 
			
		||||
 | 
			
		||||
    await this.$printDatabaseVersion();
 | 
			
		||||
 | 
			
		||||
    // First of all, if the `state` database does not exist, create it so we can track migration version
 | 
			
		||||
    if (!await this.$checkIfTableExists('state')) {
 | 
			
		||||
      logger.info('MIGRATIONS: `state` table does not exist. Creating it.');
 | 
			
		||||
      logger.debug('MIGRATIONS: `state` table does not exist. Creating it.');
 | 
			
		||||
      try {
 | 
			
		||||
        await this.$createMigrationStateTable();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
@ -29,7 +29,7 @@ class DatabaseMigration {
 | 
			
		||||
        await sleep(10000);
 | 
			
		||||
        process.exit(-1);
 | 
			
		||||
      }
 | 
			
		||||
      logger.info('MIGRATIONS: `state` table initialized.');
 | 
			
		||||
      logger.debug('MIGRATIONS: `state` table initialized.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let databaseSchemaVersion = 0;
 | 
			
		||||
@ -41,10 +41,10 @@ class DatabaseMigration {
 | 
			
		||||
      process.exit(-1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    logger.info('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
 | 
			
		||||
    logger.info('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
 | 
			
		||||
    logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
 | 
			
		||||
    logger.debug('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
 | 
			
		||||
    if (databaseSchemaVersion >= DatabaseMigration.currentVersion) {
 | 
			
		||||
      logger.info('MIGRATIONS: Nothing to do.');
 | 
			
		||||
      logger.debug('MIGRATIONS: Nothing to do.');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -58,10 +58,10 @@ class DatabaseMigration {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (DatabaseMigration.currentVersion > databaseSchemaVersion) {
 | 
			
		||||
      logger.info('MIGRATIONS: Upgrading datababse schema');
 | 
			
		||||
      logger.notice('MIGRATIONS: Upgrading datababse schema');
 | 
			
		||||
      try {
 | 
			
		||||
        await this.$migrateTableSchemaFromVersion(databaseSchemaVersion);
 | 
			
		||||
        logger.info(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
 | 
			
		||||
        logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e);
 | 
			
		||||
      }
 | 
			
		||||
@ -116,6 +116,12 @@ class DatabaseMigration {
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (databaseSchemaVersion < 7 && isBitcoin === true) {
 | 
			
		||||
        await this.$executeQuery(connection, 'DROP table IF EXISTS hashrates;');
 | 
			
		||||
        await this.$executeQuery(connection, this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      connection.release();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
@ -143,10 +149,10 @@ class DatabaseMigration {
 | 
			
		||||
        WHERE table_schema=DATABASE() AND table_name='statistics' AND index_name='added';`;
 | 
			
		||||
      const [rows] = await this.$executeQuery(connection, query, true);
 | 
			
		||||
      if (rows[0].hasIndex === 0) {
 | 
			
		||||
        logger.info('MIGRATIONS: `statistics.added` is not indexed');
 | 
			
		||||
        logger.debug('MIGRATIONS: `statistics.added` is not indexed');
 | 
			
		||||
        this.statisticsAddedIndexed = false;
 | 
			
		||||
      } else if (rows[0].hasIndex === 1) {
 | 
			
		||||
        logger.info('MIGRATIONS: `statistics.added` is already indexed');
 | 
			
		||||
        logger.debug('MIGRATIONS: `statistics.added` is already indexed');
 | 
			
		||||
        this.statisticsAddedIndexed = true;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@ -164,7 +170,7 @@ class DatabaseMigration {
 | 
			
		||||
   */
 | 
			
		||||
  private async $executeQuery(connection: PoolConnection, query: string, silent: boolean = false): Promise<any> {
 | 
			
		||||
    if (!silent) {
 | 
			
		||||
      logger.info('MIGRATIONS: Execute query:\n' + query);
 | 
			
		||||
      logger.debug('MIGRATIONS: Execute query:\n' + query);
 | 
			
		||||
    }
 | 
			
		||||
    return connection.query<any>({ sql: query, timeout: this.queryTimeout });
 | 
			
		||||
  }
 | 
			
		||||
@ -255,6 +261,10 @@ class DatabaseMigration {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (version < 7) {
 | 
			
		||||
      queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return queries;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -272,9 +282,9 @@ class DatabaseMigration {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows] = await this.$executeQuery(connection, 'SELECT VERSION() as version;', true);
 | 
			
		||||
      logger.info(`MIGRATIONS: Database engine version '${rows[0].version}'`);
 | 
			
		||||
      logger.debug(`MIGRATIONS: Database engine version '${rows[0].version}'`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.info(`MIGRATIONS: Could not fetch database engine version. ` + e);
 | 
			
		||||
      logger.debug(`MIGRATIONS: Could not fetch database engine version. ` + e);
 | 
			
		||||
    }
 | 
			
		||||
    connection.release();
 | 
			
		||||
  }
 | 
			
		||||
@ -398,6 +408,40 @@ class DatabaseMigration {
 | 
			
		||||
      FOREIGN KEY (pool_id) REFERENCES pools (id)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateDailyStatsTableQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS hashrates (
 | 
			
		||||
      hashrate_timestamp timestamp NOT NULL,
 | 
			
		||||
      avg_hashrate double unsigned DEFAULT '0',
 | 
			
		||||
      pool_id smallint unsigned NULL,
 | 
			
		||||
      PRIMARY KEY (hashrate_timestamp),
 | 
			
		||||
      INDEX (pool_id),
 | 
			
		||||
      FOREIGN KEY (pool_id) REFERENCES pools (id)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $truncateIndexedData(tables: string[]) {
 | 
			
		||||
    const allowedTables = ['blocks', 'hashrates'];
 | 
			
		||||
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    try {
 | 
			
		||||
      for (const table of tables) {
 | 
			
		||||
        if (!allowedTables.includes(table)) {
 | 
			
		||||
          logger.debug(`Table ${table} cannot to be re-indexed (not allowed)`);
 | 
			
		||||
          continue;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        await this.$executeQuery(connection, `TRUNCATE ${table}`, true);
 | 
			
		||||
        if (table === 'hashrates') {
 | 
			
		||||
          await this.$executeQuery(connection, 'UPDATE state set number = 0 where name = "last_hashrates_indexing"', true);
 | 
			
		||||
        }
 | 
			
		||||
        logger.notice(`Table ${table} has been truncated`);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.warn(`Unable to erase indexed data`);
 | 
			
		||||
    }
 | 
			
		||||
    connection.release();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new DatabaseMigration();
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,21 @@
 | 
			
		||||
import { PoolInfo, PoolStats } from '../mempool.interfaces';
 | 
			
		||||
import BlocksRepository, { EmptyBlocks } from '../repositories/BlocksRepository';
 | 
			
		||||
import PoolsRepository from '../repositories/PoolsRepository';
 | 
			
		||||
import HashratesRepository from '../repositories/HashratesRepository';
 | 
			
		||||
import bitcoinClient from './bitcoin/bitcoin-client';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import blocks from './blocks';
 | 
			
		||||
 | 
			
		||||
class Mining {
 | 
			
		||||
  hashrateIndexingStarted = false;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Generate high level overview of the pool ranks and general stats
 | 
			
		||||
   */
 | 
			
		||||
  public async $getPoolsStats(interval: string | null) : Promise<object> {
 | 
			
		||||
  public async $getPoolsStats(interval: string | null): Promise<object> {
 | 
			
		||||
    const poolsStatistics = {};
 | 
			
		||||
 | 
			
		||||
    const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval);
 | 
			
		||||
@ -26,8 +31,8 @@ class Mining {
 | 
			
		||||
        link: poolInfo.link,
 | 
			
		||||
        blockCount: poolInfo.blockCount,
 | 
			
		||||
        rank: rank++,
 | 
			
		||||
        emptyBlocks: 0, 
 | 
			
		||||
      }
 | 
			
		||||
        emptyBlocks: 0
 | 
			
		||||
      };
 | 
			
		||||
      for (let i = 0; i < emptyBlocks.length; ++i) {
 | 
			
		||||
        if (emptyBlocks[i].poolId === poolInfo.poolId) {
 | 
			
		||||
          poolStat.emptyBlocks++;
 | 
			
		||||
@ -45,7 +50,7 @@ class Mining {
 | 
			
		||||
    poolsStatistics['blockCount'] = blockCount;
 | 
			
		||||
 | 
			
		||||
    const blockHeightTip = await bitcoinClient.getBlockCount();
 | 
			
		||||
    const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(120, blockHeightTip);
 | 
			
		||||
    const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(144, blockHeightTip);
 | 
			
		||||
    poolsStatistics['lastEstimatedHashrate'] = lastBlockHashrate;
 | 
			
		||||
 | 
			
		||||
    return poolsStatistics;
 | 
			
		||||
@ -80,7 +85,101 @@ class Mining {
 | 
			
		||||
    return {
 | 
			
		||||
      adjustments: difficultyAdjustments,
 | 
			
		||||
      oldestIndexedBlockTimestamp: oldestBlock.getTime(),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Return the historical hashrates and oldest indexed block timestamp
 | 
			
		||||
   */
 | 
			
		||||
  public async $getHistoricalHashrates(interval: string | null): Promise<object> {
 | 
			
		||||
    const hashrates = await HashratesRepository.$get(interval);
 | 
			
		||||
    const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      hashrates: hashrates,
 | 
			
		||||
      oldestIndexedBlockTimestamp: oldestBlock.getTime(),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.hashrateIndexingStarted = true;
 | 
			
		||||
 | 
			
		||||
    logger.info(`Indexing hashrates`);
 | 
			
		||||
 | 
			
		||||
    const totalDayIndexed = (await BlocksRepository.$blockCount(null, null)) / 144;
 | 
			
		||||
    const indexedTimestamp = (await HashratesRepository.$get(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[] = [];
 | 
			
		||||
 | 
			
		||||
    while (toTimestamp > genesisTimestamp) {
 | 
			
		||||
      const fromTimestamp = toTimestamp - 86400;
 | 
			
		||||
      if (indexedTimestamp.includes(fromTimestamp)) {
 | 
			
		||||
        toTimestamp -= 86400;
 | 
			
		||||
        ++totalIndexed;
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
 | 
			
		||||
        null, fromTimestamp, toTimestamp);
 | 
			
		||||
      if (blockStats.blockCount === 0) { // We are done indexing, no blocks left
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let lastBlockHashrate = 0;
 | 
			
		||||
      lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
 | 
			
		||||
        blockStats.lastBlockHeight);
 | 
			
		||||
 | 
			
		||||
      const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
 | 
			
		||||
      if (elapsedSeconds > 10) {
 | 
			
		||||
        const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
 | 
			
		||||
        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`);
 | 
			
		||||
        startedAt = new Date().getTime() / 1000;
 | 
			
		||||
        indexedThisRun = 0;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      hashrates.push({
 | 
			
		||||
        hashrateTimestamp: fromTimestamp,
 | 
			
		||||
        avgHashrate: lastBlockHashrate,
 | 
			
		||||
        poolId: null,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (hashrates.length > 100) {
 | 
			
		||||
        await HashratesRepository.$saveHashrates(hashrates);
 | 
			
		||||
        hashrates.length = 0;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      toTimestamp -= 86400;
 | 
			
		||||
      ++indexedThisRun;
 | 
			
		||||
      ++totalIndexed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await HashratesRepository.$saveHashrates(hashrates);
 | 
			
		||||
    await HashratesRepository.$setLatestRunTimestamp();
 | 
			
		||||
    this.hashrateIndexingStarted = false;
 | 
			
		||||
 | 
			
		||||
    logger.info(`Hashrates indexing completed`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,7 @@ import poolsParser from './api/pools-parser';
 | 
			
		||||
import syncAssets from './sync-assets';
 | 
			
		||||
import icons from './api/liquid/icons';
 | 
			
		||||
import { Common } from './api/common';
 | 
			
		||||
import mining from './api/mining';
 | 
			
		||||
 | 
			
		||||
class Server {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
@ -88,6 +89,12 @@ class Server {
 | 
			
		||||
    if (config.DATABASE.ENABLED) {
 | 
			
		||||
      await checkDbConnection();
 | 
			
		||||
      try {
 | 
			
		||||
        if (process.env.npm_config_reindex != undefined) { // Re-index requests
 | 
			
		||||
          const tables = process.env.npm_config_reindex.split(',');
 | 
			
		||||
          logger.warn(`Indexed data for "${process.env.npm_config_reindex}" tables will be erased in 5 seconds from now (using '--reindex') ...`);
 | 
			
		||||
          await Common.sleep(5000);
 | 
			
		||||
          await databaseMigration.$truncateIndexedData(tables);
 | 
			
		||||
        }
 | 
			
		||||
        await databaseMigration.$initializeOrMigrateDatabase();
 | 
			
		||||
        await poolsParser.migratePoolsJson();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
@ -138,7 +145,7 @@ class Server {
 | 
			
		||||
      }
 | 
			
		||||
      await blocks.$updateBlocks();
 | 
			
		||||
      await memPool.$updateMempool();
 | 
			
		||||
      blocks.$generateBlockDatabase();
 | 
			
		||||
      this.runIndexingWhenReady();
 | 
			
		||||
 | 
			
		||||
      setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
 | 
			
		||||
      this.currentBackendRetryInterval = 5;
 | 
			
		||||
@ -157,6 +164,19 @@ class Server {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async runIndexingWhenReady() {
 | 
			
		||||
    if (!Common.indexingEnabled() || mempool.hasPriority()) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await blocks.$generateBlockDatabase();
 | 
			
		||||
      await mining.$generateNetworkHashrateHistory();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Unable to run indexing right now, trying again later. ` + e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setUpWebsocketHandling() {
 | 
			
		||||
    if (this.wss) {
 | 
			
		||||
      websocketHandler.setWebsocketServer(this.wss);
 | 
			
		||||
@ -276,7 +296,9 @@ class Server {
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty', routes.$getHistoricalDifficulty)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty/:interval', routes.$getHistoricalDifficulty);
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty/:interval', routes.$getHistoricalDifficulty)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate', routes.$getHistoricalHashrate)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.BISQ.ENABLED) {
 | 
			
		||||
 | 
			
		||||
@ -149,6 +149,40 @@ class BlocksRepository {
 | 
			
		||||
    return <number>rows[0].blockCount;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get blocks count between two dates
 | 
			
		||||
   * @param poolId 
 | 
			
		||||
   * @param from - The oldest timestamp
 | 
			
		||||
   * @param to - The newest timestamp
 | 
			
		||||
   * @returns 
 | 
			
		||||
   */
 | 
			
		||||
  public async $blockCountBetweenTimestamp(poolId: number | null, from: number, to: number): Promise<number> {
 | 
			
		||||
    const params: any[] = [];
 | 
			
		||||
    let query = `SELECT
 | 
			
		||||
      count(height) as blockCount,
 | 
			
		||||
      max(height) as lastBlockHeight
 | 
			
		||||
      FROM blocks`;
 | 
			
		||||
 | 
			
		||||
    if (poolId) {
 | 
			
		||||
      query += ` WHERE pool_id = ?`;
 | 
			
		||||
      params.push(poolId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (poolId) {
 | 
			
		||||
      query += ` AND`;
 | 
			
		||||
    } else {
 | 
			
		||||
      query += ` WHERE`;
 | 
			
		||||
    }
 | 
			
		||||
    query += ` UNIX_TIMESTAMP(blockTimestamp) BETWEEN '${from}' AND '${to}'`;
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query, params);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    return <number>rows[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the oldest indexed block
 | 
			
		||||
   */
 | 
			
		||||
@ -247,6 +281,13 @@ class BlocksRepository {
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new BlocksRepository();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										68
									
								
								backend/src/repositories/HashratesRepository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								backend/src/repositories/HashratesRepository.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
			
		||||
import { Common } from '../api/common';
 | 
			
		||||
import { DB } from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
 | 
			
		||||
class HashratesRepository {
 | 
			
		||||
  /**
 | 
			
		||||
   * Save indexed block data in the database
 | 
			
		||||
   */
 | 
			
		||||
  public async $saveHashrates(hashrates: any) {
 | 
			
		||||
    let query = `INSERT INTO
 | 
			
		||||
      hashrates(hashrate_timestamp, avg_hashrate, pool_id) VALUES`;
 | 
			
		||||
 | 
			
		||||
    for (const hashrate of hashrates) {
 | 
			
		||||
      query += ` (FROM_UNIXTIME(${hashrate.hashrateTimestamp}), ${hashrate.avgHashrate}, ${hashrate.poolId}),`;
 | 
			
		||||
    }
 | 
			
		||||
    query = query.slice(0, -1);
 | 
			
		||||
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    try {
 | 
			
		||||
      // logger.debug(query);
 | 
			
		||||
      await connection.query(query);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err('$saveHashrateInDatabase() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connection.release();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an array of all timestamp we've already indexed
 | 
			
		||||
   */
 | 
			
		||||
  public async $get(interval: string | null): Promise<any[]> {
 | 
			
		||||
    interval = Common.getSqlInterval(interval);
 | 
			
		||||
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
 | 
			
		||||
    let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate
 | 
			
		||||
      FROM hashrates`;
 | 
			
		||||
 | 
			
		||||
    if (interval) {
 | 
			
		||||
      query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    query += ` ORDER by hashrate_timestamp DESC`;
 | 
			
		||||
 | 
			
		||||
    const [rows]: any[] = await connection.query(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $setLatestRunTimestamp() {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const query = `UPDATE state SET number = ? WHERE name = 'last_hashrates_indexing'`;
 | 
			
		||||
    await connection.query<any>(query, [Math.round(new Date().getTime() / 1000)]);
 | 
			
		||||
    connection.release();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getLatestRunTimestamp(): 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'];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new HashratesRepository();
 | 
			
		||||
@ -22,7 +22,6 @@ import elementsParser from './api/liquid/elements-parser';
 | 
			
		||||
import icons from './api/liquid/icons';
 | 
			
		||||
import miningStats from './api/mining';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import PoolsRepository from './repositories/PoolsRepository';
 | 
			
		||||
import mining from './api/mining';
 | 
			
		||||
import BlocksRepository from './repositories/BlocksRepository';
 | 
			
		||||
 | 
			
		||||
@ -587,6 +586,18 @@ class Routes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getHistoricalHashrate(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const stats = await mining.$getHistoricalHashrates(req.params.interval ?? null);
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
			
		||||
      res.json(stats);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getBlock(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.$getBlock(req.params.hash);
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,8 @@ import { AssetsComponent } from './components/assets/assets.component';
 | 
			
		||||
import { PoolComponent } from './components/pool/pool.component';
 | 
			
		||||
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
 | 
			
		||||
import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component';
 | 
			
		||||
import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component';
 | 
			
		||||
import { MiningStartComponent } from './components/mining-start/mining-start.component';
 | 
			
		||||
 | 
			
		||||
let routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
@ -70,16 +72,35 @@ let routes: Routes = [
 | 
			
		||||
        component: LatestBlocksComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mining/difficulty',
 | 
			
		||||
        component: DifficultyChartComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mining/pools',
 | 
			
		||||
        component: PoolRankingComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mining/pool/:poolId',
 | 
			
		||||
        component: PoolComponent,
 | 
			
		||||
        path: 'mining',
 | 
			
		||||
        component: MiningStartComponent,
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
            path: 'difficulty',
 | 
			
		||||
            component: DifficultyChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'hashrate',
 | 
			
		||||
            component: HashrateChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'pools',
 | 
			
		||||
            component: PoolRankingComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'pool',
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
                path: ':poolId',
 | 
			
		||||
                component: PoolComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: ':poolId/hashrate',
 | 
			
		||||
                component: HashrateChartComponent,
 | 
			
		||||
              },
 | 
			
		||||
            ]
 | 
			
		||||
          },
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'graphs',
 | 
			
		||||
@ -170,16 +191,35 @@ let routes: Routes = [
 | 
			
		||||
            component: LatestBlocksComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/difficulty',
 | 
			
		||||
            component: DifficultyChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/pools',
 | 
			
		||||
            component: PoolRankingComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/pool/:poolId',
 | 
			
		||||
            component: PoolComponent,
 | 
			
		||||
            path: 'mining',
 | 
			
		||||
            component: MiningStartComponent,
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
                path: 'difficulty',
 | 
			
		||||
                component: DifficultyChartComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'hashrate',
 | 
			
		||||
                component: HashrateChartComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'pools',
 | 
			
		||||
                component: PoolRankingComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'pool',
 | 
			
		||||
                children: [
 | 
			
		||||
                  {
 | 
			
		||||
                    path: ':poolId',
 | 
			
		||||
                    component: PoolComponent,
 | 
			
		||||
                  },
 | 
			
		||||
                  {
 | 
			
		||||
                    path: ':poolId/hashrate',
 | 
			
		||||
                    component: HashrateChartComponent,
 | 
			
		||||
                  },
 | 
			
		||||
                ]
 | 
			
		||||
              },
 | 
			
		||||
            ]
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'graphs',
 | 
			
		||||
@ -264,16 +304,35 @@ let routes: Routes = [
 | 
			
		||||
            component: LatestBlocksComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/difficulty',
 | 
			
		||||
            component: DifficultyChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/pools',
 | 
			
		||||
            component: PoolRankingComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/pool/:poolId',
 | 
			
		||||
            component: PoolComponent,
 | 
			
		||||
            path: 'mining',
 | 
			
		||||
            component: MiningStartComponent,
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
                path: 'difficulty',
 | 
			
		||||
                component: DifficultyChartComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'hashrate',
 | 
			
		||||
                component: HashrateChartComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'pools',
 | 
			
		||||
                component: PoolRankingComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'pool',
 | 
			
		||||
                children: [
 | 
			
		||||
                  {
 | 
			
		||||
                    path: ':poolId',
 | 
			
		||||
                    component: PoolComponent,
 | 
			
		||||
                  },
 | 
			
		||||
                  {
 | 
			
		||||
                    path: ':poolId/hashrate',
 | 
			
		||||
                    component: HashrateChartComponent,
 | 
			
		||||
                  },
 | 
			
		||||
                ]
 | 
			
		||||
              },
 | 
			
		||||
            ]
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'graphs',
 | 
			
		||||
 | 
			
		||||
@ -71,6 +71,8 @@ import { AssetGroupComponent } from './components/assets/asset-group/asset-group
 | 
			
		||||
import { AssetCirculationComponent } from './components/asset-circulation/asset-circulation.component';
 | 
			
		||||
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
 | 
			
		||||
import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component';
 | 
			
		||||
import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component';
 | 
			
		||||
import { MiningStartComponent } from './components/mining-start/mining-start.component';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
@ -124,6 +126,8 @@ import { DifficultyChartComponent } from './components/difficulty-chart/difficul
 | 
			
		||||
    AssetCirculationComponent,
 | 
			
		||||
    MiningDashboardComponent,
 | 
			
		||||
    DifficultyChartComponent,
 | 
			
		||||
    HashrateChartComponent,
 | 
			
		||||
    MiningStartComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
 | 
			
		||||
 | 
			
		||||
@ -130,3 +130,32 @@ export const formatNumber = (s, precision = null) => {
 | 
			
		||||
// Utilities for segwitFeeGains
 | 
			
		||||
const witnessSize = (vin: Vin) => vin.witness.reduce((S, w) => S + (w.length / 2), 0);
 | 
			
		||||
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
 | 
			
		||||
 | 
			
		||||
// Power of ten wrapper
 | 
			
		||||
export function selectPowerOfTen(val: number) {
 | 
			
		||||
  const powerOfTen = {
 | 
			
		||||
    exa: Math.pow(10, 18),
 | 
			
		||||
    peta: Math.pow(10, 15),
 | 
			
		||||
    terra: Math.pow(10, 12),
 | 
			
		||||
    giga: Math.pow(10, 9),
 | 
			
		||||
    mega: Math.pow(10, 6),
 | 
			
		||||
    kilo: Math.pow(10, 3),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  let selectedPowerOfTen;
 | 
			
		||||
  if (val < powerOfTen.mega) {
 | 
			
		||||
    selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
 | 
			
		||||
  } else if (val < powerOfTen.giga) {
 | 
			
		||||
    selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
 | 
			
		||||
  } else if (val < powerOfTen.terra) {
 | 
			
		||||
    selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
 | 
			
		||||
  } else if (val < powerOfTen.peta) {
 | 
			
		||||
    selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' };
 | 
			
		||||
  } else if (val < powerOfTen.exa) {
 | 
			
		||||
    selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
 | 
			
		||||
  } else {
 | 
			
		||||
    selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return selectedPowerOfTen;
 | 
			
		||||
}
 | 
			
		||||
@ -6,6 +6,7 @@ import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { formatNumber } from '@angular/common';
 | 
			
		||||
import { FormBuilder, FormGroup } from '@angular/forms';
 | 
			
		||||
import { selectPowerOfTen } from 'src/app/bitcoin.utils';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-difficulty-chart',
 | 
			
		||||
@ -70,15 +71,8 @@ export class DifficultyChartComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
                const tableData = [];
 | 
			
		||||
                for (let i = 0; i < data.adjustments.length - 1; ++i) {
 | 
			
		||||
                  const selectedPowerOfTen: any = selectPowerOfTen(data.adjustments[i].difficulty);
 | 
			
		||||
                  const change = (data.adjustments[i].difficulty / data.adjustments[i + 1].difficulty - 1) * 100;
 | 
			
		||||
                  let selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' };
 | 
			
		||||
                  if (data.adjustments[i].difficulty < powerOfTen.mega) {
 | 
			
		||||
                    selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
 | 
			
		||||
                  } else if (data.adjustments[i].difficulty < powerOfTen.giga) {
 | 
			
		||||
                    selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
 | 
			
		||||
                  } else if (data.adjustments[i].difficulty < powerOfTen.terra) {
 | 
			
		||||
                    selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  tableData.push(Object.assign(data.adjustments[i], {
 | 
			
		||||
                    change: change,
 | 
			
		||||
@ -122,8 +116,9 @@ export class DifficultyChartComponent implements OnInit {
 | 
			
		||||
        type: 'value',
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          formatter: (val) => {
 | 
			
		||||
            const diff = val / Math.pow(10, 12); // terra
 | 
			
		||||
            return diff.toString() + 'T';
 | 
			
		||||
            const selectedPowerOfTen: any = selectPowerOfTen(val);
 | 
			
		||||
            const diff = val / selectedPowerOfTen.divider;
 | 
			
		||||
            return `${diff} ${selectedPowerOfTen.unit}`;
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        splitLine: {
 | 
			
		||||
@ -134,17 +129,18 @@ export class DifficultyChartComponent implements OnInit {
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          data: data,
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: false,
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            width: 3,
 | 
			
		||||
          },
 | 
			
		||||
          areaStyle: {}
 | 
			
		||||
      series: {
 | 
			
		||||
        showSymbol: false,
 | 
			
		||||
        data: data,
 | 
			
		||||
        type: 'line',
 | 
			
		||||
        smooth: false,
 | 
			
		||||
        lineStyle: {
 | 
			
		||||
          width: 2,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
        areaStyle: {
 | 
			
		||||
          opacity: 0.25
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,33 @@
 | 
			
		||||
<div [class]="widget === false ? 'full-container' : ''">
 | 
			
		||||
 | 
			
		||||
  <div class="card-header mb-0 mb-lg-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">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'3m'"> 3M
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 180">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'6m'"> 6M
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 365">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'1y'"> 1Y
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 730">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'2y'"> 2Y
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 1095">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'3y'"> 3Y
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'all'"> ALL
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="hashrateObservable$ | async" [class]="widget === false ? 'chart' : ''" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
 | 
			
		||||
  <div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
    <div class="spinner-border text-light"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,45 @@
 | 
			
		||||
.main-title {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: #ffffff91;
 | 
			
		||||
  margin-top: -13px;
 | 
			
		||||
  font-size: 10px;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding-bottom: 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.full-container {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: calc(100% - 100px);
 | 
			
		||||
  @media (max-width: 992px) {
 | 
			
		||||
    height: calc(100% - 140px);
 | 
			
		||||
  };
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    height: calc(100% - 180px);
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chart {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  padding-bottom: 20px;
 | 
			
		||||
  padding-right: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.formRadioGroup {
 | 
			
		||||
  margin-top: 6px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  @media (min-width: 830px) {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    float: right;
 | 
			
		||||
    margin-top: 0px;
 | 
			
		||||
  }
 | 
			
		||||
  .btn-sm {
 | 
			
		||||
    font-size: 9px;
 | 
			
		||||
    @media (min-width: 830px) {
 | 
			
		||||
      font-size: 14px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,163 @@
 | 
			
		||||
import { Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
 | 
			
		||||
import { EChartsOption } from 'echarts';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { formatNumber } from '@angular/common';
 | 
			
		||||
import { FormBuilder, FormGroup } from '@angular/forms';
 | 
			
		||||
import { selectPowerOfTen } from 'src/app/bitcoin.utils';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-hashrate-chart',
 | 
			
		||||
  templateUrl: './hashrate-chart.component.html',
 | 
			
		||||
  styleUrls: ['./hashrate-chart.component.scss'],
 | 
			
		||||
  styles: [`
 | 
			
		||||
    .loadingGraphs {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 38%;
 | 
			
		||||
      left: calc(50% - 15px);
 | 
			
		||||
      z-index: 100;
 | 
			
		||||
    }
 | 
			
		||||
  `],
 | 
			
		||||
})
 | 
			
		||||
export class HashrateChartComponent implements OnInit {
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
  @Input() right: number | string = 10;
 | 
			
		||||
  @Input() left: number | string = 75;
 | 
			
		||||
 | 
			
		||||
  radioGroupForm: FormGroup;
 | 
			
		||||
 | 
			
		||||
  chartOptions: EChartsOption = {};
 | 
			
		||||
  chartInitOptions = {
 | 
			
		||||
    renderer: 'svg',
 | 
			
		||||
    width: 'auto',
 | 
			
		||||
    height: 'auto',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  hashrateObservable$: Observable<any>;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  formatNumber = formatNumber;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private formBuilder: FormBuilder,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@mining.hashrate:hashrate`);
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue('1y');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
 | 
			
		||||
      .pipe(
 | 
			
		||||
        startWith('1y'),
 | 
			
		||||
        switchMap((timespan) => {
 | 
			
		||||
          return this.apiService.getHistoricalHashrate$(timespan)
 | 
			
		||||
            .pipe(
 | 
			
		||||
              tap(data => {
 | 
			
		||||
                this.prepareChartOptions(data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]));
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
              }),
 | 
			
		||||
              map(data => {
 | 
			
		||||
                const availableTimespanDay = (
 | 
			
		||||
                  (new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp / 1000)
 | 
			
		||||
                ) / 3600 / 24;
 | 
			
		||||
                return {
 | 
			
		||||
                  availableTimespanDay: availableTimespanDay,
 | 
			
		||||
                  data: data.hashrates
 | 
			
		||||
                };
 | 
			
		||||
              }),
 | 
			
		||||
            );
 | 
			
		||||
        }),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(data) {
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
      grid: {
 | 
			
		||||
        right: this.right,
 | 
			
		||||
        left: this.left,
 | 
			
		||||
      },
 | 
			
		||||
      title: {
 | 
			
		||||
        text: this.widget ? '' : $localize`:@@mining.hashrate:Hashrate`,
 | 
			
		||||
        left: 'center',
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: '#FFF',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        show: true,
 | 
			
		||||
        trigger: 'axis',
 | 
			
		||||
      },
 | 
			
		||||
      axisPointer: {
 | 
			
		||||
        type: 'line',
 | 
			
		||||
      },
 | 
			
		||||
      xAxis: {
 | 
			
		||||
        type: 'time',
 | 
			
		||||
        splitNumber: this.isMobile() ? 5 : 10,
 | 
			
		||||
      },
 | 
			
		||||
      yAxis: {
 | 
			
		||||
        type: 'value',
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          formatter: (val) => {
 | 
			
		||||
            const selectedPowerOfTen: any = selectPowerOfTen(val);
 | 
			
		||||
            const newVal = val / selectedPowerOfTen.divider;
 | 
			
		||||
            return `${newVal} ${selectedPowerOfTen.unit}H/s`
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        splitLine: {
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            type: 'dotted',
 | 
			
		||||
            color: '#ffffff66',
 | 
			
		||||
            opacity: 0.25,
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      series: {
 | 
			
		||||
        showSymbol: false,
 | 
			
		||||
        data: data,
 | 
			
		||||
        type: 'line',
 | 
			
		||||
        smooth: false,
 | 
			
		||||
        lineStyle: {
 | 
			
		||||
          width: 2,
 | 
			
		||||
        },
 | 
			
		||||
        areaStyle: {
 | 
			
		||||
          opacity: 0.25
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      dataZoom: this.widget ? null : [{
 | 
			
		||||
        type: 'inside',
 | 
			
		||||
        realtime: true,
 | 
			
		||||
        zoomLock: true,
 | 
			
		||||
        zoomOnMouseWheel: true,
 | 
			
		||||
        moveOnMouseMove: true,
 | 
			
		||||
        maxSpan: 100,
 | 
			
		||||
        minSpan: 10,
 | 
			
		||||
      }, {
 | 
			
		||||
        showDetail: false,
 | 
			
		||||
        show: true,
 | 
			
		||||
        type: 'slider',
 | 
			
		||||
        brushSelect: false,
 | 
			
		||||
        realtime: true,
 | 
			
		||||
        bottom: 0,
 | 
			
		||||
        selectedDataBackground: {
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            color: '#fff',
 | 
			
		||||
            opacity: 0.45,
 | 
			
		||||
          },
 | 
			
		||||
          areaStyle: {
 | 
			
		||||
            opacity: 0,
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      }],
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isMobile() {
 | 
			
		||||
    return (window.innerWidth <= 767.98);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -31,7 +31,7 @@
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="master-page.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" id="btn-blocks" *ngIf="!stateService.env.MINING_DASHBOARD">
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,18 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- hashrate -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="main-title" i18n="mining.hashrate">Hashrate (1y)</div>
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <app-hashrate-chart [widget]=true></app-hashrate-chart>
 | 
			
		||||
          <div class="text-center"><a href="" [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="dashboard.view-more">View more
 | 
			
		||||
              »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- difficulty -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="main-title" i18n="mining.difficulty">Difficulty (1y)</div>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
<router-outlet></router-outlet>
 | 
			
		||||
@ -0,0 +1,14 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-mining-start',
 | 
			
		||||
  templateUrl: './mining-start.component.html',
 | 
			
		||||
})
 | 
			
		||||
export class MiningStartComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -156,4 +156,11 @@ export class ApiService {
 | 
			
		||||
        (interval !== undefined ? `/${interval}` : '')
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getHistoricalHashrate$(interval: string | undefined): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any[]>(
 | 
			
		||||
        this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +
 | 
			
		||||
        (interval !== undefined ? `/${interval}` : '')
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,12 @@ do for url in / \
 | 
			
		||||
	'/api/v1/mining/pools/2y' \
 | 
			
		||||
	'/api/v1/mining/pools/3y' \
 | 
			
		||||
	'/api/v1/mining/pools/all' \
 | 
			
		||||
	'/api/v1/mining/hashrate/3m' \
 | 
			
		||||
	'/api/v1/mining/hashrate/6m' \
 | 
			
		||||
	'/api/v1/mining/hashrate/1y' \
 | 
			
		||||
	'/api/v1/mining/hashrate/2y' \
 | 
			
		||||
	'/api/v1/mining/hashrate/3y' \
 | 
			
		||||
	'/api/v1/mining/hashrate/all' \
 | 
			
		||||
 | 
			
		||||
	do
 | 
			
		||||
		curl -s "https://${hostname}${url}" >/dev/null
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user