Merge branch 'master' into regtest-1
@ -91,11 +91,11 @@ JSON:
 | 
			
		||||
    "PRICE_FEED_UPDATE_INTERVAL": 600,
 | 
			
		||||
    "USE_SECOND_NODE_FOR_MINFEE": false,
 | 
			
		||||
    "EXTERNAL_ASSETS": ["https://mempool.space/resources/pools.json"],
 | 
			
		||||
    "STDOUT_LOG_MIN_PRIORITY": "debug"
 | 
			
		||||
    "STDOUT_LOG_MIN_PRIORITY": "info"
 | 
			
		||||
  },
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
docker-compose overrides::
 | 
			
		||||
docker-compose overrides:
 | 
			
		||||
```
 | 
			
		||||
      MEMPOOL_NETWORK: ""
 | 
			
		||||
      MEMPOOL_BACKEND: ""
 | 
			
		||||
 | 
			
		||||
@ -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() { }
 | 
			
		||||
 | 
			
		||||
@ -115,6 +116,9 @@ class Blocks {
 | 
			
		||||
      Common.median(transactionsTmp.map((tx) => tx.effectiveFeePerVsize)) : 0;
 | 
			
		||||
    blockExtended.extras.feeRange = transactionsTmp.length > 0 ?
 | 
			
		||||
      Common.getFeesInRange(transactionsTmp, 8) : [0, 0];
 | 
			
		||||
    blockExtended.extras.totalFees = transactionsTmp.reduce((acc, tx) => {
 | 
			
		||||
      return acc + tx.fee;
 | 
			
		||||
    }, 0)
 | 
			
		||||
 | 
			
		||||
    if (Common.indexingEnabled()) {
 | 
			
		||||
      let pool: PoolTag;
 | 
			
		||||
@ -170,10 +174,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 +244,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 = 8;
 | 
			
		||||
  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);
 | 
			
		||||
      }
 | 
			
		||||
@ -92,11 +92,13 @@ class DatabaseMigration {
 | 
			
		||||
        await this.$executeQuery(connection, this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
 | 
			
		||||
      }
 | 
			
		||||
      if (databaseSchemaVersion < 5 && isBitcoin === true) {
 | 
			
		||||
        logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.'`);
 | 
			
		||||
        await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (databaseSchemaVersion < 6 && isBitcoin === true) {
 | 
			
		||||
        logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.'`);
 | 
			
		||||
        await this.$executeQuery(connection, 'TRUNCATE blocks;');  // Need to re-index
 | 
			
		||||
        // Cleanup original blocks fields type
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
@ -116,6 +118,21 @@ 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'));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (databaseSchemaVersion < 8 && isBitcoin === true) {
 | 
			
		||||
        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 `hashrates` DROP INDEX `PRIMARY`');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      connection.release();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
@ -143,10 +160,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 +181,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 +272,10 @@ class DatabaseMigration {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (version < 7) {
 | 
			
		||||
      queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return queries;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -272,9 +293,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 +419,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++;
 | 
			
		||||
@ -37,15 +42,13 @@ class Mining {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    poolsStatistics['pools'] = poolsStats;
 | 
			
		||||
 | 
			
		||||
    const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
 | 
			
		||||
    poolsStatistics['oldestIndexedBlockTimestamp'] = oldestBlock.getTime();
 | 
			
		||||
    poolsStatistics['oldestIndexedBlockTimestamp'] = await BlocksRepository.$oldestBlockTimestamp();
 | 
			
		||||
 | 
			
		||||
    const blockCount: number = await BlocksRepository.$blockCount(null, interval);
 | 
			
		||||
    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;
 | 
			
		||||
@ -74,13 +77,137 @@ class Mining {
 | 
			
		||||
   * Return the historical difficulty adjustments and oldest indexed block timestamp
 | 
			
		||||
   */
 | 
			
		||||
  public async $getHistoricalDifficulty(interval: string | null): Promise<object> {
 | 
			
		||||
    const difficultyAdjustments = await BlocksRepository.$getBlocksDifficulty(interval);
 | 
			
		||||
    const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
 | 
			
		||||
    return await BlocksRepository.$getBlocksDifficulty(interval);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      adjustments: difficultyAdjustments,
 | 
			
		||||
      oldestIndexedBlockTimestamp: oldestBlock.getTime(),
 | 
			
		||||
  /**
 | 
			
		||||
   * 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
 | 
			
		||||
    let startedAt = new Date().getTime() / 1000;
 | 
			
		||||
    const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
 | 
			
		||||
    const lastMidnight = new Date();
 | 
			
		||||
    lastMidnight.setUTCHours(0); lastMidnight.setUTCMinutes(0); lastMidnight.setUTCSeconds(0); lastMidnight.setUTCMilliseconds(0);
 | 
			
		||||
    let toTimestamp = Math.round(lastMidnight.getTime() / 1000);
 | 
			
		||||
    let indexedThisRun = 0;
 | 
			
		||||
    let totalIndexed = 0;
 | 
			
		||||
 | 
			
		||||
    const hashrates: any[] = [];
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
      if (totalIndexed % 7 === 0 && !indexedTimestamp.includes(fromTimestamp + 1)) { // Save weekly pools hashrate
 | 
			
		||||
        logger.debug("Indexing weekly hashrates for mining pools");
 | 
			
		||||
        let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp - 604800, fromTimestamp);
 | 
			
		||||
        const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0);
 | 
			
		||||
        pools = pools.map((pool: any) => {
 | 
			
		||||
          pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
 | 
			
		||||
          pool.share = (pool.blockCount / totalBlocks);
 | 
			
		||||
          return pool;
 | 
			
		||||
        });
 | 
			
		||||
  
 | 
			
		||||
        for (const pool of pools) {
 | 
			
		||||
          hashrates.push({
 | 
			
		||||
            hashrateTimestamp: fromTimestamp + 1,
 | 
			
		||||
            avgHashrate: pool['hashrate'],
 | 
			
		||||
            poolId: pool.poolId,
 | 
			
		||||
            share: pool['share'],
 | 
			
		||||
            type: 'weekly',
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      hashrates.push({
 | 
			
		||||
        hashrateTimestamp: fromTimestamp,
 | 
			
		||||
        avgHashrate: lastBlockHashrate,
 | 
			
		||||
        poolId: null,
 | 
			
		||||
        share: 1,
 | 
			
		||||
        type: 'daily',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (hashrates.length > 10) {
 | 
			
		||||
        await HashratesRepository.$saveHashrates(hashrates);
 | 
			
		||||
        hashrates.length = 0;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
 | 
			
		||||
      if (elapsedSeconds > 5) {
 | 
			
		||||
        const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
 | 
			
		||||
        const formattedDate = new Date(fromTimestamp * 1000).toUTCString();
 | 
			
		||||
        const daysLeft = Math.round(totalDayIndexed - totalIndexed);
 | 
			
		||||
        logger.debug(`Getting hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ~${daysLeft} days left to index`);
 | 
			
		||||
        startedAt = new Date().getTime() / 1000;
 | 
			
		||||
        indexedThisRun = 0;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      toTimestamp -= 86400;
 | 
			
		||||
      ++indexedThisRun;
 | 
			
		||||
      ++totalIndexed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add genesis block manually
 | 
			
		||||
    if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) {
 | 
			
		||||
      hashrates.push({
 | 
			
		||||
        hashrateTimestamp: genesisTimestamp,
 | 
			
		||||
        avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
 | 
			
		||||
        poolId: null,
 | 
			
		||||
        type: 'daily',
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (hashrates.length > 0) {
 | 
			
		||||
      await HashratesRepository.$saveHashrates(hashrates);
 | 
			
		||||
    }
 | 
			
		||||
    await HashratesRepository.$setLatestRunTimestamp();
 | 
			
		||||
    this.hashrateIndexingStarted = false;
 | 
			
		||||
 | 
			
		||||
    logger.info(`Hashrates indexing completed`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,8 @@ 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';
 | 
			
		||||
import HashratesRepository from './repositories/HashratesRepository';
 | 
			
		||||
 | 
			
		||||
class Server {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
@ -88,6 +90,13 @@ 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 this.$resetHashratesIndexingState();
 | 
			
		||||
        await databaseMigration.$initializeOrMigrateDatabase();
 | 
			
		||||
        await poolsParser.migratePoolsJson();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
@ -138,7 +147,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 +166,23 @@ class Server {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $resetHashratesIndexingState() {
 | 
			
		||||
    return await HashratesRepository.$setLatestRunTimestamp(0);    
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 +302,12 @@ 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/pools', routes.$getPoolsHistoricalHashrate)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate)
 | 
			
		||||
        .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) {
 | 
			
		||||
 | 
			
		||||
@ -78,6 +78,7 @@ export interface TransactionStripped {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface BlockExtension {
 | 
			
		||||
  totalFees?: number;
 | 
			
		||||
  medianFee?: number;
 | 
			
		||||
  feeRange?: number[];
 | 
			
		||||
  reward?: number;
 | 
			
		||||
 | 
			
		||||
@ -149,16 +149,49 @@ 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 += ` blockTimestamp BETWEEN FROM_UNIXTIME('${from}') AND FROM_UNIXTIME('${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
 | 
			
		||||
   */
 | 
			
		||||
  public async $oldestBlockTimestamp(): Promise<number> {
 | 
			
		||||
    const query = `SELECT blockTimestamp
 | 
			
		||||
    const query = `SELECT UNIX_TIMESTAMP(blockTimestamp) as blockTimestamp
 | 
			
		||||
      FROM blocks
 | 
			
		||||
      ORDER BY height
 | 
			
		||||
      LIMIT 1;`;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows]: any[] = await connection.query(query);
 | 
			
		||||
@ -232,21 +265,54 @@ class BlocksRepository {
 | 
			
		||||
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
 | 
			
		||||
    let query = `SELECT MIN(UNIX_TIMESTAMP(blockTimestamp)) as timestamp, difficulty, height
 | 
			
		||||
      FROM blocks`;
 | 
			
		||||
    // :D ... Yeah don't ask me about this one https://stackoverflow.com/a/40303162
 | 
			
		||||
    // Basically, using temporary user defined fields, we are able to extract all
 | 
			
		||||
    // difficulty adjustments from the blocks tables.
 | 
			
		||||
    // This allow use to avoid indexing it in another table.
 | 
			
		||||
    let query = `
 | 
			
		||||
      SELECT
 | 
			
		||||
      *
 | 
			
		||||
      FROM 
 | 
			
		||||
      (
 | 
			
		||||
        SELECT
 | 
			
		||||
        UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, height,
 | 
			
		||||
        IF(@prevStatus = YT.difficulty, @rn := @rn + 1,
 | 
			
		||||
          IF(@prevStatus := YT.difficulty, @rn := 1, @rn := 1)
 | 
			
		||||
        ) AS rn
 | 
			
		||||
        FROM blocks YT
 | 
			
		||||
        CROSS JOIN 
 | 
			
		||||
        (
 | 
			
		||||
          SELECT @prevStatus := -1, @rn := 1
 | 
			
		||||
        ) AS var
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    if (interval) {
 | 
			
		||||
      query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    query += ` GROUP BY difficulty
 | 
			
		||||
      ORDER BY blockTimestamp DESC`;
 | 
			
		||||
    query += `
 | 
			
		||||
        ORDER BY YT.height
 | 
			
		||||
      ) AS t
 | 
			
		||||
      WHERE t.rn = 1
 | 
			
		||||
      ORDER BY t.height
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    const [rows]: any[] = await connection.query(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    for (let row of rows) {
 | 
			
		||||
      delete row['rn'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										102
									
								
								backend/src/repositories/HashratesRepository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,102 @@
 | 
			
		||||
import { Common } from '../api/common';
 | 
			
		||||
import { DB } from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import PoolsRepository from './PoolsRepository';
 | 
			
		||||
 | 
			
		||||
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, share, type) VALUES`;
 | 
			
		||||
 | 
			
		||||
    for (const hashrate of hashrates) {
 | 
			
		||||
      query += ` (FROM_UNIXTIME(${hashrate.hashrateTimestamp}), ${hashrate.avgHashrate}, ${hashrate.poolId}, ${hashrate.share}, "${hashrate.type}"),`;
 | 
			
		||||
    }
 | 
			
		||||
    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();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getNetworkDailyHashrate(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()
 | 
			
		||||
        AND hashrates.type = 'daily'
 | 
			
		||||
        AND pool_id IS NULL`;
 | 
			
		||||
    } else {
 | 
			
		||||
      query += ` WHERE hashrates.type = 'daily'
 | 
			
		||||
        AND pool_id IS NULL`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    query += ` ORDER by hashrate_timestamp`;
 | 
			
		||||
 | 
			
		||||
    const [rows]: any[] = await connection.query(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the current biggest pool hashrate history
 | 
			
		||||
   */
 | 
			
		||||
  public async $getPoolsWeeklyHashrate(interval: string | null): Promise<any[]> {
 | 
			
		||||
    interval = Common.getSqlInterval(interval);
 | 
			
		||||
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const topPoolsId = (await PoolsRepository.$getPoolsInfo('1w')).map((pool) => pool.poolId);
 | 
			
		||||
 | 
			
		||||
    let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate, share, pools.name as poolName
 | 
			
		||||
      FROM hashrates
 | 
			
		||||
      JOIN pools on pools.id = pool_id`;
 | 
			
		||||
 | 
			
		||||
    if (interval) {
 | 
			
		||||
      query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()
 | 
			
		||||
        AND hashrates.type = 'weekly'
 | 
			
		||||
        AND pool_id IN (${topPoolsId})`;
 | 
			
		||||
    } else {
 | 
			
		||||
      query += ` WHERE hashrates.type = 'weekly'
 | 
			
		||||
        AND pool_id IN (${topPoolsId})`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    query += ` ORDER by hashrate_timestamp, FIELD(pool_id, ${topPoolsId})`;
 | 
			
		||||
 | 
			
		||||
    const [rows]: any[] = await connection.query(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $setLatestRunTimestamp(val: any = null) {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const query = `UPDATE state SET number = ? WHERE name = 'last_hashrates_indexing'`;
 | 
			
		||||
 | 
			
		||||
    await connection.query<any>(query, (val === null) ? [Math.round(new Date().getTime() / 1000)] : [val]);
 | 
			
		||||
    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();
 | 
			
		||||
@ -49,6 +49,22 @@ class PoolsRepository {
 | 
			
		||||
    return <PoolInfo[]>rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get basic pool info and block count between two timestamp
 | 
			
		||||
   */
 | 
			
		||||
   public async $getPoolsInfoBetween(from: number, to: number): Promise<PoolInfo[]> {
 | 
			
		||||
    let query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName
 | 
			
		||||
      FROM pools
 | 
			
		||||
      LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?)
 | 
			
		||||
      GROUP BY pools.id`;
 | 
			
		||||
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query, [from, to]);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    return <PoolInfo[]>rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get mining pool statistics for one pool
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
@ -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,40 @@ class Routes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getPoolsHistoricalHashrate(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const hashrates = await mining.$getPoolsHistoricalHashrates(req.params.interval ?? null, parseInt(req.params.poolId, 10));
 | 
			
		||||
      const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
			
		||||
      res.json({
 | 
			
		||||
        oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp,
 | 
			
		||||
        hashrates: hashrates,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getHistoricalHashrate(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const hashrates = await mining.$getNetworkHistoricalHashrates(req.params.interval ?? null);
 | 
			
		||||
      const difficulty = await mining.$getHistoricalDifficulty(req.params.interval ?? null);
 | 
			
		||||
      const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
			
		||||
      res.json({
 | 
			
		||||
        oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp,
 | 
			
		||||
        hashrates: hashrates,
 | 
			
		||||
        difficulty: difficulty,
 | 
			
		||||
      });
 | 
			
		||||
    } 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);
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ __MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=1100}
 | 
			
		||||
__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600}
 | 
			
		||||
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
 | 
			
		||||
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[\"https://mempool.space/resources/pools.json\"]}
 | 
			
		||||
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=debug}
 | 
			
		||||
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
 | 
			
		||||
 | 
			
		||||
# CORE_RPC
 | 
			
		||||
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
 | 
			
		||||
 | 
			
		||||
@ -66,7 +66,7 @@ describe('Mainnet', () => {
 | 
			
		||||
      cy.get('[id^="bitcoin-block-"]').should('have.length', 8);
 | 
			
		||||
      cy.get('.footer').should('be.visible');
 | 
			
		||||
      cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
 | 
			
		||||
        expect(text).to.match(/Tx vBytes per second:.* vB\/s/);
 | 
			
		||||
        expect(text).to.match(/Incoming transactions.* vB\/s/);
 | 
			
		||||
      });
 | 
			
		||||
      cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
 | 
			
		||||
        expect(text).to.match(/Unconfirmed:(.*)/);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8507
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						@ -59,39 +59,38 @@
 | 
			
		||||
    "cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
 | 
			
		||||
    "cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
 | 
			
		||||
    "cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@angular-devkit/build-angular": "^13.1.2",
 | 
			
		||||
    "@angular/animations": "~13.1.1",
 | 
			
		||||
    "@angular/cli": "~13.0.4",
 | 
			
		||||
    "@angular/common": "~13.1.1",
 | 
			
		||||
    "@angular/compiler": "~13.1.1",
 | 
			
		||||
    "@angular/core": "~13.1.1",
 | 
			
		||||
    "@angular/forms": "~13.1.1",
 | 
			
		||||
    "@angular/localize": "^13.1.1",
 | 
			
		||||
    "@angular/platform-browser": "~13.1.1",
 | 
			
		||||
    "@angular/platform-browser-dynamic": "~13.1.1",
 | 
			
		||||
    "@angular/platform-server": "~13.1.1",
 | 
			
		||||
    "@angular/router": "~13.1.1",
 | 
			
		||||
    "@fortawesome/angular-fontawesome": "^0.8.2",
 | 
			
		||||
    "@fortawesome/fontawesome-common-types": "^0.2.35",
 | 
			
		||||
    "@fortawesome/fontawesome-svg-core": "^1.2.35",
 | 
			
		||||
    "@fortawesome/free-solid-svg-icons": "^5.15.3",
 | 
			
		||||
    "@angular-devkit/build-angular": "^13.2.4",
 | 
			
		||||
    "@angular/animations": "~13.2.3",
 | 
			
		||||
    "@angular/cli": "~13.2.4",
 | 
			
		||||
    "@angular/common": "~13.2.3",
 | 
			
		||||
    "@angular/compiler": "~13.2.3",
 | 
			
		||||
    "@angular/core": "~13.2.3",
 | 
			
		||||
    "@angular/forms": "~13.2.3",
 | 
			
		||||
    "@angular/localize": "^13.2.3",
 | 
			
		||||
    "@angular/platform-browser": "~13.2.3",
 | 
			
		||||
    "@angular/platform-browser-dynamic": "~13.2.3",
 | 
			
		||||
    "@angular/platform-server": "~13.2.3",
 | 
			
		||||
    "@angular/router": "~13.2.3",
 | 
			
		||||
    "@fortawesome/angular-fontawesome": "0.10.1",
 | 
			
		||||
    "@fortawesome/fontawesome-common-types": "0.3.0",
 | 
			
		||||
    "@fortawesome/fontawesome-svg-core": "1.3.0",
 | 
			
		||||
    "@fortawesome/free-solid-svg-icons": "6.0.0",
 | 
			
		||||
    "@juggle/resize-observer": "^3.3.1",
 | 
			
		||||
    "@mempool/mempool.js": "2.3.0",
 | 
			
		||||
    "@ng-bootstrap/ng-bootstrap": "^11.0.0",
 | 
			
		||||
    "@nguniversal/express-engine": "11.2.1",
 | 
			
		||||
    "@nguniversal/express-engine": "12.1.3",
 | 
			
		||||
    "@types/qrcode": "1.4.1",
 | 
			
		||||
    "bootstrap": "4.5.0",
 | 
			
		||||
    "browserify": "^17.0.0",
 | 
			
		||||
    "clipboard": "^2.0.4",
 | 
			
		||||
    "domino": "^2.1.6",
 | 
			
		||||
    "echarts": "^5.1.2",
 | 
			
		||||
    "echarts": "5.3.0",
 | 
			
		||||
    "express": "^4.17.1",
 | 
			
		||||
    "lightweight-charts": "^3.3.0",
 | 
			
		||||
    "ngx-bootrap-multiselect": "^2.0.0",
 | 
			
		||||
    "ngx-echarts": "^7.0.1",
 | 
			
		||||
    "ngx-echarts": "8.0.1",
 | 
			
		||||
    "ngx-infinite-scroll": "^10.0.1",
 | 
			
		||||
    "qrcode": "1.5.0",
 | 
			
		||||
    "rxjs": "^6.6.7",
 | 
			
		||||
@ -101,9 +100,9 @@
 | 
			
		||||
    "zone.js": "~0.11.4"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@angular/compiler-cli": "~13.1.1",
 | 
			
		||||
    "@angular/language-service": "~13.1.1",
 | 
			
		||||
    "@nguniversal/builders": "^11.2.1",
 | 
			
		||||
    "@angular/compiler-cli": "~13.2.3",
 | 
			
		||||
    "@angular/language-service": "~13.2.3",
 | 
			
		||||
    "@nguniversal/builders": "~13.0.2",
 | 
			
		||||
    "@types/express": "^4.17.0",
 | 
			
		||||
    "@types/jasmine": "~3.6.0",
 | 
			
		||||
    "@types/jasminewd2": "~2.0.3",
 | 
			
		||||
@ -123,10 +122,10 @@
 | 
			
		||||
  },
 | 
			
		||||
  "optionalDependencies": {
 | 
			
		||||
    "@cypress/schematic": "^1.3.0",
 | 
			
		||||
    "cypress": "^9.3.1",
 | 
			
		||||
    "cypress": "^9.5.0",
 | 
			
		||||
    "cypress-fail-on-console-error": "^2.1.3",
 | 
			
		||||
    "cypress-wait-until": "^1.7.1",
 | 
			
		||||
    "mock-socket": "^9.0.3",
 | 
			
		||||
    "start-server-and-test": "^1.12.6"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@ -42,7 +42,28 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
 | 
			
		||||
      pathRewrite: {
 | 
			
		||||
          "^/liquid/api/": "/api/v1/"
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      context: ['/liquidtestnet/api/v1/**'],
 | 
			
		||||
      target: `http://localhost:8999`,
 | 
			
		||||
      secure: false,
 | 
			
		||||
      ws: true,
 | 
			
		||||
      changeOrigin: true,
 | 
			
		||||
      proxyTimeout: 30000,
 | 
			
		||||
      pathRewrite: {
 | 
			
		||||
          "^/liquidtestnet": ""
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      context: ['/liquidtestnet/api/**'],
 | 
			
		||||
      target: `http://localhost:8999`,
 | 
			
		||||
      secure: false,
 | 
			
		||||
      changeOrigin: true,
 | 
			
		||||
      proxyTimeout: 30000,
 | 
			
		||||
      pathRewrite: {
 | 
			
		||||
          "^/liquidtestnet/api/": "/api/v1/"
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,24 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
 | 
			
		||||
      changeOrigin: true,
 | 
			
		||||
      proxyTimeout: 30000,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      context: ['/liquidtestnet/api/v1/**'],
 | 
			
		||||
      target: `http://localhost:8999`,
 | 
			
		||||
      secure: false,
 | 
			
		||||
      ws: true,
 | 
			
		||||
      changeOrigin: true,
 | 
			
		||||
      proxyTimeout: 30000,
 | 
			
		||||
      pathRewrite: {
 | 
			
		||||
          "^/liquidtestnet": ""
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      context: ['/liquidtestnet/api/**'],
 | 
			
		||||
      target: `https://liquid.network`,
 | 
			
		||||
      secure: false,
 | 
			
		||||
      changeOrigin: true,
 | 
			
		||||
      proxyTimeout: 30000,
 | 
			
		||||
    },
 | 
			
		||||
  ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,9 @@ import { AssetsFeaturedComponent } from './components/assets/assets-featured/ass
 | 
			
		||||
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 { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component';
 | 
			
		||||
import { MiningStartComponent } from './components/mining-start/mining-start.component';
 | 
			
		||||
 | 
			
		||||
let routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
@ -70,16 +72,31 @@ 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: 'hashrate',
 | 
			
		||||
            component: HashrateChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'hashrate/pools',
 | 
			
		||||
            component: HashrateChartPoolsComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'pools',
 | 
			
		||||
            component: PoolRankingComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'pool',
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
                path: ':poolId',
 | 
			
		||||
                component: PoolComponent,
 | 
			
		||||
              },
 | 
			
		||||
            ]
 | 
			
		||||
          },
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'graphs',
 | 
			
		||||
@ -170,16 +187,31 @@ 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: 'hashrate',
 | 
			
		||||
                component: HashrateChartComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'hashrate/pools',
 | 
			
		||||
                component: HashrateChartPoolsComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'pools',
 | 
			
		||||
                component: PoolRankingComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'pool',
 | 
			
		||||
                children: [
 | 
			
		||||
                  {
 | 
			
		||||
                    path: ':poolId',
 | 
			
		||||
                    component: PoolComponent,
 | 
			
		||||
                  },
 | 
			
		||||
                ]
 | 
			
		||||
              },
 | 
			
		||||
            ]
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'graphs',
 | 
			
		||||
@ -264,16 +296,31 @@ 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: 'hashrate',
 | 
			
		||||
                component: HashrateChartComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'hashrate/pools',
 | 
			
		||||
                component: HashrateChartPoolsComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'pools',
 | 
			
		||||
                component: PoolRankingComponent,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'pool',
 | 
			
		||||
                children: [
 | 
			
		||||
                  {
 | 
			
		||||
                    path: ':poolId',
 | 
			
		||||
                    component: PoolComponent,
 | 
			
		||||
                  },
 | 
			
		||||
                ]
 | 
			
		||||
              },
 | 
			
		||||
            ]
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'graphs',
 | 
			
		||||
 | 
			
		||||
@ -71,7 +71,23 @@ export const chartColors = [
 | 
			
		||||
  "#263238",
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
 | 
			
		||||
export const poolsColor = {
 | 
			
		||||
   'foundryusa': '#D81B60',
 | 
			
		||||
   'antpool': '#8E24AA',
 | 
			
		||||
   'f2pool': '#5E35B1',
 | 
			
		||||
   'poolin': '#3949AB',
 | 
			
		||||
   'binancepool': '#1E88E5',
 | 
			
		||||
   'viabtc': '#039BE5',
 | 
			
		||||
   'btccom': '#00897B',
 | 
			
		||||
   'slushpool': '#00ACC1',
 | 
			
		||||
   'sbicrypto': '#43A047',
 | 
			
		||||
   'marapool': '#7CB342',
 | 
			
		||||
   'luxor': '#C0CA33',
 | 
			
		||||
   'unknown': '#FDD835',
 | 
			
		||||
   'okkong': '#FFB300',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
 | 
			
		||||
  250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
 | 
			
		||||
 | 
			
		||||
export interface Language {
 | 
			
		||||
 | 
			
		||||
@ -68,8 +68,12 @@ import { PushTransactionComponent } from './components/push-transaction/push-tra
 | 
			
		||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
 | 
			
		||||
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
 | 
			
		||||
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
 | 
			
		||||
import { 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 { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component';
 | 
			
		||||
import { MiningStartComponent } from './components/mining-start/mining-start.component';
 | 
			
		||||
import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
@ -120,8 +124,12 @@ import { DifficultyChartComponent } from './components/difficulty-chart/difficul
 | 
			
		||||
    AssetsNavComponent,
 | 
			
		||||
    AssetsFeaturedComponent,
 | 
			
		||||
    AssetGroupComponent,
 | 
			
		||||
    AssetCirculationComponent,
 | 
			
		||||
    MiningDashboardComponent,
 | 
			
		||||
    DifficultyChartComponent,
 | 
			
		||||
    HashrateChartComponent,
 | 
			
		||||
    HashrateChartPoolsComponent,
 | 
			
		||||
    MiningStartComponent,
 | 
			
		||||
    AmountShortenerPipe,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
 | 
			
		||||
 | 
			
		||||
@ -69,7 +69,7 @@ export function calcSegwitFeeGains(tx: Transaction) {
 | 
			
		||||
export function moveDec(num: number, n: number) {
 | 
			
		||||
  let frac, int, neg, ref;
 | 
			
		||||
  if (n === 0) {
 | 
			
		||||
    return num;
 | 
			
		||||
    return num.toString();
 | 
			
		||||
  }
 | 
			
		||||
  ref = ('' + num).split('.'), int = ref[0], frac = ref[1];
 | 
			
		||||
  int || (int = '0');
 | 
			
		||||
@ -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;
 | 
			
		||||
}
 | 
			
		||||
@ -8,13 +8,10 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="about-text" *ngIf="stateService.env.BASE_MODULE === 'mempool'; else marginBox">
 | 
			
		||||
  <div class="about-text">
 | 
			
		||||
    <h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></h5>
 | 
			
		||||
    <p i18n>Building a mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, without any advertising, altcoins, or third-party trackers.</p>
 | 
			
		||||
    <p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
 | 
			
		||||
  </div>
 | 
			
		||||
  <ng-template #marginBox>
 | 
			
		||||
    <div class="no-about-margin"></div>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <div class="social-icons">
 | 
			
		||||
    <a target="_blank" href="https://github.com/mempool/mempool">
 | 
			
		||||
@ -31,31 +28,128 @@
 | 
			
		||||
    </a>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <br><br>
 | 
			
		||||
  <div class="sponsor-button">
 | 
			
		||||
    <button [hidden]="showNavigateToSponsor" type="button" class="btn btn-primary" (click)="sponsor()" i18n="about.become-a-sponsor">Become a sponsor ❤️</button>
 | 
			
		||||
    <ng-container *ngIf="showNavigateToSponsor" i18n="about.navigate-to-sponsor">Navigate to <a href="https://mempool.space/sponsor" target="_blank">https://mempool.space/sponsor</a> to sponsor</ng-container>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="enterprise-sponsor">
 | 
			
		||||
    <h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
 | 
			
		||||
    <div class="wrapper">
 | 
			
		||||
      <a href="https://spiral.xyz/" target="_blank" title="Spiral">
 | 
			
		||||
        <img class="image" src="/resources/profile/spiral.svg" />
 | 
			
		||||
        <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-115 -15 879 679" style="background-color: rgb(27,20,100)" class="image">
 | 
			
		||||
        <defs>
 | 
			
		||||
        <style>.cls-1{fill:url(#linear-gradient);}</style>
 | 
			
		||||
        <linearGradient id="linear-gradient" x1="81.36" y1="311.35" x2="541.35" y2="311.35" gradientUnits="userSpaceOnUse">
 | 
			
		||||
          <stop offset="0.18" stop-color="blue"/>
 | 
			
		||||
          <stop offset="1" stop-color="#f0f"/>
 | 
			
		||||
        </linearGradient>
 | 
			
		||||
        </defs>
 | 
			
		||||
        <path class="cls-1" d="M326.4,572.09C201.2,572.09,141,503,112.48,445,84.26,387.47,81.89,330.44,81.69,322.31c-4.85-77,41-231.78,249.58-271.2a28.05,28.05,0,0,1,10.41,55.13c-213.12,40.28-204.44,206-204,213,0,.53.06,1.06.07,1.6C137.9,328.74,142.85,516,326.4,516,394.74,516,443,486.6,470,428.63c24.48-52.74,19.29-112.45-13.52-155.83-22.89-30.27-52.46-45-90.38-45-34.46,0-63.47,9.88-86.21,29.37A91.5,91.5,0,0,0,248,322.3c-1.41,25.4,7.14,49.36,24.07,67.49C287.27,406,305,413.9,326.4,413.9c27.46,0,45.52-9,53.66-26.81,8.38-18.3,3.61-38.93-.19-43.33-9.11-10-18.69-13.68-22.48-13-2.53.43-5.78,4.61-8.48,10.92a28,28,0,0,1-51.58-22c14.28-33.44,37.94-42,50.76-44.2,24.78-4.18,52.17,7.3,73.34,30.65s25.51,68.55,10.15,103.22C421.54,432,394.52,470,326.4,470c-36.72,0-69.67-14.49-95.29-41.92C203.64,398.68,189.77,360,192,319.19a149.1,149.1,0,0,1,51.31-104.6c33.19-28.45,74.48-42.87,122.71-42.87,55.12,0,101.85,23.25,135.12,67.23,45.36,60,52.9,141.71,19.66,213.3C495.45,506.92,441.12,572.09,326.4,572.09Z"/>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span>Spiral</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://gemini.com/" target="_blank" title="Gemini">
 | 
			
		||||
        <img class="image" src="/resources/profile/gemini.svg" />
 | 
			
		||||
        <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
 | 
			
		||||
          <rect style="fill: white" width="360" height="360" />
 | 
			
		||||
          <g transform="matrix(0.62 0 0 0.62 180 180)">
 | 
			
		||||
            <path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
 | 
			
		||||
          </g>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span>Gemini</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://exodus.com/" target="_blank" title="Exodus">
 | 
			
		||||
        <img class="image" src="/resources/profile/exodus.svg" />
 | 
			
		||||
        <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
 | 
			
		||||
          <defs>
 | 
			
		||||
            <linearGradient x1="0%" y1="50%" x2="100%" y2="50%" id="linearGradient-1">
 | 
			
		||||
              <stop stop-color="#00BFFF" offset="0%"></stop>
 | 
			
		||||
              <stop stop-color="#6619FF" offset="100%"></stop>
 | 
			
		||||
            </linearGradient>
 | 
			
		||||
          </defs>
 | 
			
		||||
          <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
 | 
			
		||||
            <g>
 | 
			
		||||
              <rect fill="#1A1D40" x="0" y="0" width="400" height="400"></rect>
 | 
			
		||||
              <path d="M244.25,200 L310,265.75 L286.8,265.75 C282.823093,265.746499 279.010347,264.16385 276.2,261.35 L215,200 L276.25,138.6 C279.068515,135.804479 282.880256,134.240227 286.85,134.249954 L310,134.249954 L244.25,200 Z M123.75,138.6 C120.931485,135.804479 117.119744,134.240227 113.15,134.249954 L90,134.249954 L155.75,200 L90,265.75 L113.2,265.75 C117.176907,265.746499 120.989653,264.16385 123.8,261.35 L185,200 L123.75,138.6 Z M200,215 L138.6,276.25 C135.804479,279.068515 134.240227,282.880256 134.249954,286.85 L134.249954,310 L200,244.25 L265.750046,310 L265.750046,286.85 C265.759773,282.880256 264.195521,279.068515 261.4,276.25 L200,215 Z M200,185 L261.4,123.75 C264.195521,120.931485 265.759773,117.119744 265.750046,113.15 L265.750046,90 L200,155.75 L134.249954,90 L134.249954,113.15 C134.240227,117.119744 135.804479,120.931485 138.6,123.75 L200,185 Z" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
 | 
			
		||||
            </g>
 | 
			
		||||
          </g>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span>Exodus</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://foundrydigital.com/" target="_blank" title="Foundry">
 | 
			
		||||
        <img class="image" src="/resources/profile/foundry.svg" />
 | 
			
		||||
        <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
 | 
			
		||||
          <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
 | 
			
		||||
            <g>
 | 
			
		||||
              <rect fill="#87E1A1" fill-rule="nonzero" x="0" y="0" width="400" height="400"></rect>
 | 
			
		||||
              <path d="M124,149.256434 L169.106586,149.256434 L169.106586,128.378728 C169.106586,102.958946 183.316852,90 207.489341,90 L276.773787,90 L276.773787,119.404671 L222.192348,119.404671 C216.458028,119.404671 213.968815,122.397366 213.968815,127.633575 L213.968815,149.256434 L276.023264,149.256434 L276.023264,181.902184 L213.968815,181.902184 L213.968815,310 L169.106586,310 L169.106586,181.902184 L124,181.902184 L124,149.256434" fill="#000000"></path>
 | 
			
		||||
            </g>
 | 
			
		||||
          </g>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span>Foundry</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://unchained.com/" target="_blank" title="Unchained">
 | 
			
		||||
        <img class="image" src="/resources/profile/unchained.svg" />
 | 
			
		||||
        <svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 216 216" class="image" style="enable-background:new 0 0 216 216;">
 | 
			
		||||
        <style type="text/css">
 | 
			
		||||
          .ucst0{fill:#002248;}
 | 
			
		||||
          .ucst1{opacity:0.5;fill:#FFFFFF;}
 | 
			
		||||
          .ucst2{fill:#FFFFFF;}
 | 
			
		||||
          .ucst3{opacity:0.75;fill:#FFFFFF;}
 | 
			
		||||
        </style>
 | 
			
		||||
        <rect class="ucst0" width="216" height="216"/>
 | 
			
		||||
        <g>
 | 
			
		||||
          <g>
 | 
			
		||||
            <path class="ucst1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
 | 
			
		||||
            <path class="ucst2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
 | 
			
		||||
            <path class="ucst3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
 | 
			
		||||
          </g>
 | 
			
		||||
        </g>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span>Unchained</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://blockstream.com/" target="_blank" title="Blockstream">
 | 
			
		||||
        <img class="image" src="/resources/profile/blockstream.svg" />
 | 
			
		||||
        <svg xmlns="http://www.w3.org/2000/svg" version="1.0" x="0px" y="0px" viewBox="200 200 600 600" class="image" style="enable-background:new 0 0 1000 1000;background-color: #111316 !important">
 | 
			
		||||
          <style type="text/css">
 | 
			
		||||
            .st0{fill:#111316;}
 | 
			
		||||
            .st1{fill:#00C3FF;}
 | 
			
		||||
            .st2{fill:#7EE0FF;}
 | 
			
		||||
          </style>
 | 
			
		||||
          <path class="st1" d="M659.7,392.3c10.2,14.3,18.4,29.9,24.5,46.4l21.8-7.1c-6.9-18.9-16.4-36.8-28.1-53.1L659.7,392.3z"/>
 | 
			
		||||
          <path class="st1" d="M510.6,289.2c-5.8-0.2-11.7-0.2-17.5,0l1.6,22.8c8.8-0.3,17.6-0.1,26.3,0.7c8.7,0.8,17.4,2.2,26,4.2l5.8-22.1  c-9.8-2.3-19.7-3.9-29.7-4.8C519,289.6,514.7,289.3,510.6,289.2z"/>
 | 
			
		||||
          <path class="st1" d="M297.1,605.5c-9.1-18.6-15.7-38.3-19.5-58.6l-23.9,3.8c4.2,23,11.6,45.3,22,66.2L297.1,605.5z"/>
 | 
			
		||||
          <path class="st1" d="M284.8,375.6l21.2,11.8c10.6-17.8,23.5-34,38.5-48.3l-16.2-18C311.3,337.2,296.7,355.5,284.8,375.6z"/>
 | 
			
		||||
          <path class="st1" d="M254.8,453.5l23.8,4.2c4.2-20.3,11.2-39.9,20.7-58.3l-21.2-11.7C267.3,408.5,259.5,430.6,254.8,453.5z"/>
 | 
			
		||||
          <path class="st1" d="M409.9,268.8l9.5,22.2c19.3-7.6,39.5-12.5,60.1-14.5l-1.7-24.1C454.5,254.6,431.7,260.1,409.9,268.8z"/>
 | 
			
		||||
          <path class="st1" d="M338.5,311.8l16.2,18c15.8-13.4,33.3-24.6,52.1-33.4l-9.5-22.2C376,283.9,356.2,296.6,338.5,311.8z"/>
 | 
			
		||||
          <path class="st1" d="M697.1,667.6l-18.9-15.1c-13.4,15.8-28.9,29.7-46,41.4l13,20.5C664.6,701.3,682.1,685.6,697.1,667.6z"/>
 | 
			
		||||
          <path class="st1" d="M402.5,710.7c-18.6-9.1-35.9-20.7-51.4-34.5l-16.5,17.7c17.4,15.6,37,28.6,58,38.8L402.5,710.7z"/>
 | 
			
		||||
          <path class="st1" d="M755.4,528.2c3.1-32.6-0.2-65.5-9.7-96.8l-23,7.6c13.2,44.4,12.7,91.7-1.3,135.8l22.8,8.1  C749.9,565.2,753.7,546.8,755.4,528.2z"/>
 | 
			
		||||
          <path class="st1" d="M614.2,689.2L602,670c-15.1,9-31.3,16-48.3,20.7l5.4,22.2C578.5,707.5,597,699.6,614.2,689.2z"/>
 | 
			
		||||
          <path class="st1" d="M314.5,528.8c-1.7-14.2-1.9-28.6-0.5-42.9c0.3-3.5,0.7-6.5,1.2-9.6l-22.5-4c-0.5,3.8-1,7.6-1.4,11.5  c-1.5,16.1-1.3,32.4,0.7,48.5L314.5,528.8z"/>
 | 
			
		||||
          <path class="st1" d="M568.2,284.7c19.9,5.8,38.9,14.4,56.4,25.4l13.5-20.2c-19.8-12.5-41.2-22.1-63.7-28.7L568.2,284.7z"/>
 | 
			
		||||
          <path class="st1" d="M469.8,755.8l2.3-24.1c-19.5-2.6-38.6-7.8-56.8-15.3l-10.1,22.2C425.8,747.1,447.6,752.9,469.8,755.8z"/>
 | 
			
		||||
          <path class="st1" d="M351.3,657.7l15.7-16.6c-12.4-12.5-23.1-26.5-31.8-41.8l-20.3,10.7C324.8,627.4,337.1,643.5,351.3,657.7z"/>
 | 
			
		||||
          <path class="st1" d="M649.5,297.7l-13.6,20.2c16.9,12,32,26.3,45.1,42.4l19.4-14.8C685.7,327.2,668.6,311.2,649.5,297.7z"/>
 | 
			
		||||
          <path class="st1" d="M672.7,633.2c12-16.1,21.8-33.7,29.1-52.5l-21.5-7.7c-6.4,16.4-15,31.9-25.5,46L672.7,633.2z"/>
 | 
			
		||||
          <path class="st2" d="M690.6,449.6l-21.6,7.2c6,20.7,8,42.4,6,63.8c-1.1,11.9-3.4,23.7-6.9,35.2l21.5,7.6c4.1-13.2,6.9-26.9,8.2-40.7  C700.1,498.1,697.6,473.3,690.6,449.6z"/>
 | 
			
		||||
          <path class="st2" d="M475.2,698l2.1-22.7c-13.3-2-26.4-5.5-38.9-10.5l-9.4,20.7C443.8,691.5,459.3,695.7,475.2,698z"/>
 | 
			
		||||
          <path class="st2" d="M631.8,456.2l20.4-6.9c-4.9-12.9-11.4-25.2-19.4-36.6l-17.1,13C622.3,435.2,627.7,445.4,631.8,456.2z"/>
 | 
			
		||||
          <path class="st2" d="M508.4,345.7h-11.2l1.5,21.4c11.5-0.3,22.9,0.7,34.2,3.2l5.5-20.7c-6.8-1.5-13.6-2.6-20.5-3.2  C514.8,346.1,511.6,345.9,508.4,345.7z"/>
 | 
			
		||||
          <path class="st2" d="M335.5,403.8l20,11.1c7.5-12.4,16.5-23.7,26.9-33.8L367,364.1C354.8,375.9,344.2,389.2,335.5,403.8z"/>
 | 
			
		||||
          <path class="st2" d="M553.8,339.5c13.8,4.2,27.1,10.2,39.4,17.7l12.7-19c-14.4-8.9-30-15.8-46.2-20.7L553.8,339.5z"/>
 | 
			
		||||
          <path class="st2" d="M635.9,394.5l18.1-13.8c-10.7-13.2-23.2-24.9-36.9-34.8l-12.7,19C616.2,373.4,626.7,383.3,635.9,394.5z"/>
 | 
			
		||||
          <path class="st2" d="M611.5,584.6l16.8,13.4c8.2-11.2,14.9-23.3,20.1-36.2l-20.2-7.2C623.8,565.2,618.2,575.3,611.5,584.6z"/>
 | 
			
		||||
          <path class="st2" d="M389.9,635.1l-15.6,16.6c12.8,11.2,26.9,20.7,42.2,28.2l9.4-20.7C412.9,652.8,400.8,644.6,389.9,635.1z"/>
 | 
			
		||||
          <path class="st2" d="M369.2,520.2c-1-9.7-1.1-19.5-0.2-29.2c0.2-1.7,0.4-3.5,0.6-5.1l-21.1-3.8c-0.3,2.3-0.6,4.6-0.8,6.9  c-1.1,11.5-0.9,23,0.3,34.5L369.2,520.2z"/>
 | 
			
		||||
          <path class="st2" d="M333.6,538l-22.6,3.5c3.2,16.7,8.6,33,16,48.3l20.2-10.7C340.9,566,336.4,552.2,333.6,538z"/>
 | 
			
		||||
          <path class="st2" d="M601.7,646.3l12.3,19.2c14-9.6,26.7-21,37.7-33.8l-17.9-14.2C624.4,628.4,613.6,638.1,601.7,646.3z"/>
 | 
			
		||||
          <path class="st2" d="M348.8,426.9l-19.9-11c-7.8,15.1-13.5,31.2-17,47.8l22.5,4C337.4,453.5,342.2,439.8,348.8,426.9z"/>
 | 
			
		||||
          <path class="st2" d="M540.6,636.9l5,20.7c13.3-3.8,26.1-9.2,38.1-16.2l-11.6-18.1C562.2,629,551.6,633.6,540.6,636.9z"/>
 | 
			
		||||
          <path class="st2" d="M384,573.5l-19,9.9c6.9,12,15.4,23,25.1,32.9l14.8-15.7C396.9,592.4,389.9,583.3,384,573.5z"/>
 | 
			
		||||
          <path class="st2" d="M496.7,677.1c-1.9,0-3.8-0.2-5.7-0.4l-2.1,22.7c17.9,1.3,35.9,0.1,53.4-3.5l-5.3-22.2  C523.8,676.5,510.2,677.6,496.7,677.1z"/>
 | 
			
		||||
          <path class="st2" d="M377.3,354.9l15.3,16.9c11.1-9.3,23.3-17.1,36.4-23.3l-9-21C404.6,334.7,390.3,343.9,377.3,354.9z"/>
 | 
			
		||||
          <path class="st2" d="M432.7,322.1l9,21c13.5-5.2,27.6-8.7,42-10.3L482,310C465.1,311.9,448.5,315.9,432.7,322.1z"/>
 | 
			
		||||
          <path class="st1" d="M490.3,757.5c21.5,0.7,43-1.1,64.2-5.2l-5-23.3c-18.3,3.8-37,5.3-55.8,4.6c-3,0-5.2-0.4-8.2-0.6l-2.1,24.4  c2.3,0.1,4.6,0.1,6.9,0L490.3,757.5z"/>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span>Blockstream</span>
 | 
			
		||||
      </a>
 | 
			
		||||
    </div>
 | 
			
		||||
@ -73,9 +167,6 @@
 | 
			
		||||
        </ng-template>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <button [hidden]="showNavigateToSponsor" type="button" class="btn btn-primary" (click)="sponsor()" i18n="about.become-a-sponsor">Become a sponsor ❤️</button>
 | 
			
		||||
    <ng-container *ngIf="showNavigateToSponsor" i18n="about.navigate-to-sponsor">Navigate to <a href="https://mempool.space/sponsor" target="_blank">https://mempool.space/sponsor</a> to sponsor</ng-container>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="community-integrations-sponsor">
 | 
			
		||||
@ -106,6 +197,10 @@
 | 
			
		||||
        <img class="image" src="/resources/profile/runcitadel.svg" />
 | 
			
		||||
        <span>Citadel</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://github.com/fort-nix/nix-bitcoin" target="_blank" title="nix-bitcoin">
 | 
			
		||||
        <img class="image" src="/resources/profile/nix-bitcoin.png" />
 | 
			
		||||
        <span>NixOS</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet">
 | 
			
		||||
        <img class="image" src="/resources/profile/electrum.jpg" />
 | 
			
		||||
        <span>Electrum</span>
 | 
			
		||||
@ -142,10 +237,6 @@
 | 
			
		||||
        <img class="image" src="/resources/profile/marina.svg" />
 | 
			
		||||
        <span>Marina</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://github.com/Satpile/satpile" target="_blank" title="Satpile Watch-Only Wallet">
 | 
			
		||||
        <img class="image" src="/resources/profile/satpile.jpg" />
 | 
			
		||||
        <span>Satpile</span>
 | 
			
		||||
      </a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -177,7 +268,7 @@
 | 
			
		||||
    </div>
 | 
			
		||||
    <br>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngIf="allContributors$ | async as contributors else loadingSponsors">
 | 
			
		||||
    <div class="contributors">
 | 
			
		||||
      <h3 i18n="about.contributors">Project Contributors</h3>
 | 
			
		||||
@ -252,7 +343,7 @@
 | 
			
		||||
    <a href="/3rdpartylicenses.txt">Third-party Licenses</a>
 | 
			
		||||
    <a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  <div class="footer-version" *ngIf="officialMempoolSpace">
 | 
			
		||||
    {{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>]
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    margin: 25px;
 | 
			
		||||
    line-height: 32px;
 | 
			
		||||
  }  
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .intro {
 | 
			
		||||
    margin: 25px auto 30px;
 | 
			
		||||
@ -41,7 +41,7 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .alliances,
 | 
			
		||||
  .enterprise-sponsor, 
 | 
			
		||||
  .enterprise-sponsor,
 | 
			
		||||
  .community-integrations-sponsor,
 | 
			
		||||
  .maintainers {
 | 
			
		||||
    margin-top: 68px;
 | 
			
		||||
@ -58,7 +58,7 @@
 | 
			
		||||
    .wrapper {
 | 
			
		||||
      margin: 20px auto;
 | 
			
		||||
    }
 | 
			
		||||
    .btn-primary {   
 | 
			
		||||
    .btn-primary {
 | 
			
		||||
      max-width: 250px;
 | 
			
		||||
      margin: auto;
 | 
			
		||||
      height: 45px;
 | 
			
		||||
@ -68,7 +68,7 @@
 | 
			
		||||
 | 
			
		||||
  .alliances {
 | 
			
		||||
    margin-bottom: 100px;
 | 
			
		||||
    a {    
 | 
			
		||||
    a {
 | 
			
		||||
      &:nth-child(3) {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        top: 10px;
 | 
			
		||||
@ -88,17 +88,17 @@
 | 
			
		||||
        margin: 50px 30px 0px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .liquid {    
 | 
			
		||||
    .liquid {
 | 
			
		||||
      top: 7px;
 | 
			
		||||
      position: relative;
 | 
			
		||||
    }
 | 
			
		||||
    .copa {    
 | 
			
		||||
    .copa {
 | 
			
		||||
      height: auto;
 | 
			
		||||
      top: 23px;
 | 
			
		||||
      position: relative;
 | 
			
		||||
      width: 300px;
 | 
			
		||||
    }
 | 
			
		||||
    .bisq {    
 | 
			
		||||
    .bisq {
 | 
			
		||||
      top: 3px;
 | 
			
		||||
      position: relative;
 | 
			
		||||
    }
 | 
			
		||||
@ -115,15 +115,15 @@
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        &:hover {
 | 
			
		||||
          text-decoration: none;
 | 
			
		||||
          img {
 | 
			
		||||
          img, svg {
 | 
			
		||||
            transform: scale(1.1);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        img, span{
 | 
			
		||||
        img, svg, span {
 | 
			
		||||
          display: block;
 | 
			
		||||
          transition: 150ms all;
 | 
			
		||||
        }
 | 
			
		||||
        img {
 | 
			
		||||
        img, svg {
 | 
			
		||||
          margin: 40px 29px 10px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -46,53 +46,68 @@ export class AddressLabelsComponent implements OnInit {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      [
 | 
			
		||||
        // {regexp: /^OP_DUP OP_HASH160/, label: 'HTLC'},
 | 
			
		||||
        {regexp: /^OP_IF OP_PUSHBYTES_33 \w{33} OP_ELSE OP_PUSHBYTES_2 \w{2} OP_CSV OP_DROP/, label: 'Force Close'}
 | 
			
		||||
      ].forEach((item) => {
 | 
			
		||||
          if (item.regexp.test(this.vin.inner_witnessscript_asm)) {
 | 
			
		||||
            this.lightning = item.label;
 | 
			
		||||
      // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
 | 
			
		||||
      if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSHBYTES_(1 \w{2}|2 \w{4}) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(this.vin.inner_witnessscript_asm)) {
 | 
			
		||||
          if (this.vin.witness[this.vin.witness.length - 2] == '01') {
 | 
			
		||||
              this.lightning = 'Revoked Force Close';
 | 
			
		||||
          } else {
 | 
			
		||||
              this.lightning = 'Force Close';
 | 
			
		||||
          }
 | 
			
		||||
      // https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
 | 
			
		||||
      } else if (/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CHECKSEQUENCEVERIFY OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) {
 | 
			
		||||
        if (this.vin.witness[this.vin.witness.length - 2].length == 66) {
 | 
			
		||||
            this.lightning = 'Revoked HTLC';
 | 
			
		||||
        } else {
 | 
			
		||||
            this.lightning = 'HTLC';
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this.lightning) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this.vin.inner_witnessscript_asm.indexOf('OP_CHECKMULTISIG') > -1) {
 | 
			
		||||
        const matches = this.getMatches(this.vin.inner_witnessscript_asm, /OP_PUSHNUM_([0-9])/g, 1);
 | 
			
		||||
        this.multisig = true;
 | 
			
		||||
        this.multisigM = parseInt(matches[0], 10);
 | 
			
		||||
        this.multisigN = parseInt(matches[1], 10);
 | 
			
		||||
      this.detectMultisig(this.vin.inner_witnessscript_asm);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        if (this.multisigM === 1 && this.multisigN === 1) {
 | 
			
		||||
          this.multisig = false;
 | 
			
		||||
        }
 | 
			
		||||
    this.detectMultisig(this.vin.inner_redeemscript_asm);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  detectMultisig(script: string) {
 | 
			
		||||
    if (!script) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const ops = script.split(' ');
 | 
			
		||||
    if (ops.length < 3 || ops.pop() != 'OP_CHECKMULTISIG') {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const opN = ops.pop();
 | 
			
		||||
    if (!opN.startsWith('OP_PUSHNUM_')) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const n = parseInt(opN.match(/[0-9]+/)[0]);
 | 
			
		||||
    if (ops.length < n * 2 + 1) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // pop n public keys
 | 
			
		||||
    for (var i = 0; i < n; i++) {
 | 
			
		||||
      if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop())) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop())) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.vin.inner_redeemscript_asm && this.vin.inner_redeemscript_asm.indexOf('OP_CHECKMULTISIG') > -1) {
 | 
			
		||||
      const matches = this.getMatches(this.vin.inner_redeemscript_asm, /OP_PUSHNUM_([0-9])/g, 1);
 | 
			
		||||
      this.multisig = true;
 | 
			
		||||
      this.multisigM = matches[0];
 | 
			
		||||
      this.multisigN = matches[1];
 | 
			
		||||
    const opM = ops.pop();
 | 
			
		||||
    if (!opM.startsWith('OP_PUSHNUM_')) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const m = parseInt(opM.match(/[0-9]+/)[0]);
 | 
			
		||||
 | 
			
		||||
    this.multisig = true;
 | 
			
		||||
    this.multisigM = m;
 | 
			
		||||
    this.multisigN = n;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleVout() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMatches(str: string, regex: RegExp, index: number) {
 | 
			
		||||
    if (!index) {
 | 
			
		||||
      index = 1;
 | 
			
		||||
    }
 | 
			
		||||
    const matches = [];
 | 
			
		||||
    let match;
 | 
			
		||||
    while (match = regex.exec(str)) {
 | 
			
		||||
      matches.push(match[index]);
 | 
			
		||||
    }
 | 
			
		||||
    return matches;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,6 @@
 | 
			
		||||
<ng-container *ngIf="(circulatingAmount$ | async) as circulating">
 | 
			
		||||
  <ng-template [ngIf]="circulating.amount === -1" [ngIfElse]="default" i18n="shared.confidential">Confidential</ng-template>
 | 
			
		||||
  <ng-template #default>
 | 
			
		||||
    <span class="d-inline-block d-md-none">{{ circulating.amount | amountShortener }}</span>
 | 
			
		||||
    <span class="d-none d-md-inline-block">{{ circulating.amount | number: '1.2-2' }}</span> <span class="ticker">{{ circulating.ticker }}</span></ng-template>
 | 
			
		||||
</ng-container>
 | 
			
		||||
@ -0,0 +1,3 @@
 | 
			
		||||
.ticker {
 | 
			
		||||
  color: grey;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,60 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
 | 
			
		||||
import { combineLatest, Observable } from 'rxjs';
 | 
			
		||||
import { map } from 'rxjs/operators';
 | 
			
		||||
import { moveDec } from 'src/app/bitcoin.utils';
 | 
			
		||||
import { AssetsService } from 'src/app/services/assets.service';
 | 
			
		||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
 | 
			
		||||
import { formatNumber } from '@angular/common';
 | 
			
		||||
import { environment } from 'src/environments/environment';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-asset-circulation',
 | 
			
		||||
  templateUrl: './asset-circulation.component.html',
 | 
			
		||||
  styleUrls: ['./asset-circulation.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class AssetCirculationComponent implements OnInit {
 | 
			
		||||
  @Input() assetId: string;
 | 
			
		||||
 | 
			
		||||
  circulatingAmount$: Observable<{ amount: number, ticker: string}>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private assetsService: AssetsService,
 | 
			
		||||
    @Inject(LOCALE_ID) private locale: string,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.circulatingAmount$ = combineLatest([
 | 
			
		||||
      this.electrsApiService.getAsset$(this.assetId),
 | 
			
		||||
      this.assetsService.getAssetsMinimalJson$]
 | 
			
		||||
    )
 | 
			
		||||
    .pipe(
 | 
			
		||||
      map(([asset, assetsMinimal]) => {
 | 
			
		||||
        const assetData = assetsMinimal[asset.asset_id];
 | 
			
		||||
        if (!asset.chain_stats.has_blinded_issuances) {
 | 
			
		||||
          if (asset.asset_id === environment.nativeAssetId) {
 | 
			
		||||
            return {
 | 
			
		||||
              amount: this.formatAmount(asset.chain_stats.peg_in_amount - asset.chain_stats.burned_amount - asset.chain_stats.peg_out_amount, assetData[3]),
 | 
			
		||||
              ticker: assetData[1]
 | 
			
		||||
            };
 | 
			
		||||
          } else {
 | 
			
		||||
            return {
 | 
			
		||||
              amount: this.formatAmount(asset.chain_stats.issued_amount - asset.chain_stats.burned_amount, assetData[3]),
 | 
			
		||||
              ticker: assetData[1]
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          return {
 | 
			
		||||
            amount: -1,
 | 
			
		||||
            ticker: '',
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  formatAmount(value: number, precision = 0): number {
 | 
			
		||||
    return parseFloat(moveDec(value, -precision));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -13,7 +13,10 @@
 | 
			
		||||
        <div class="fee-span">
 | 
			
		||||
          {{ block?.extras?.feeRange[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div>
 | 
			
		||||
        <div *ngIf="showMiningInfo" class="block-size">
 | 
			
		||||
          <app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div>
 | 
			
		||||
        <div class="transaction-count">
 | 
			
		||||
          <ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
 | 
			
		||||
          <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
 | 
			
		||||
@ -21,10 +24,10 @@
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    <div class="" *ngIf="showMiningInfo === true">
 | 
			
		||||
      <a class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.id) | relativeUrl]">
 | 
			
		||||
        {{ block.extras.pool.name}}</a>
 | 
			
		||||
    </div>
 | 
			
		||||
      <div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
 | 
			
		||||
        <a class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.id) | relativeUrl]">
 | 
			
		||||
          {{ block.extras.pool.name}}</a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
 | 
			
		||||
 | 
			
		||||
@ -129,4 +129,15 @@
 | 
			
		||||
  position: relative;
 | 
			
		||||
  top: 15px;
 | 
			
		||||
  z-index: 101;
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.animated {
 | 
			
		||||
  transition: all 0.15s ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
.show {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
.hide {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  pointer-events : none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,10 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core';
 | 
			
		||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { Observable, Subscription } from 'rxjs';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { specialBlocks } from 'src/app/app.constants';
 | 
			
		||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
 | 
			
		||||
import { Location } from '@angular/common';
 | 
			
		||||
import { config } from 'process';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-blockchain-blocks',
 | 
			
		||||
@ -12,7 +13,6 @@ import { BlockExtended } from 'src/app/interfaces/node-api.interface';
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class BlockchainBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  @Input() showMiningInfo: boolean = false;
 | 
			
		||||
  specialBlocks = specialBlocks;
 | 
			
		||||
  network = '';
 | 
			
		||||
  blocks: BlockExtended[] = [];
 | 
			
		||||
@ -32,6 +32,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  arrowLeftPx = 30;
 | 
			
		||||
  blocksFilled = false;
 | 
			
		||||
  transition = '1s';
 | 
			
		||||
  showMiningInfo = false;
 | 
			
		||||
 | 
			
		||||
  gradientColors = {
 | 
			
		||||
    '': ['#9339f4', '#105fb0'],
 | 
			
		||||
@ -45,11 +46,22 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private cd: ChangeDetectorRef,
 | 
			
		||||
  ) { }
 | 
			
		||||
    private location: Location,
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  enabledMiningInfoIfNeeded(url) {
 | 
			
		||||
    this.showMiningInfo = url === '/mining';
 | 
			
		||||
    this.cd.markForCheck(); // Need to update the view asap
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
 | 
			
		||||
      this.enabledMiningInfoIfNeeded(this.location.path());
 | 
			
		||||
      this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
 | 
			
		||||
      this.feeRounding = '1.0-1';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
<div class="text-center" class="blockchain-wrapper animate" #container>
 | 
			
		||||
<div class="text-center" class="blockchain-wrapper" #container>
 | 
			
		||||
  <div class="position-container {{ network }}">
 | 
			
		||||
    <span>
 | 
			
		||||
      <app-mempool-blocks></app-mempool-blocks>
 | 
			
		||||
      <app-blockchain-blocks [showMiningInfo]="showMiningInfo"></app-blockchain-blocks>
 | 
			
		||||
      <app-blockchain-blocks></app-blockchain-blocks>
 | 
			
		||||
      <div id="divider"></div>
 | 
			
		||||
    </span>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -59,14 +59,4 @@
 | 
			
		||||
  width: 300px;
 | 
			
		||||
  left: -150px;
 | 
			
		||||
  top: 0px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.animate {
 | 
			
		||||
  transition: all 1s ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
.move-left {
 | 
			
		||||
  transform: translate(-40%, 0);
 | 
			
		||||
	@media (max-width: 767.98px) {
 | 
			
		||||
    transform: translate(-85%, 0);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@ -8,7 +8,6 @@ import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class BlockchainComponent implements OnInit {
 | 
			
		||||
  showMiningInfo: boolean = false;
 | 
			
		||||
  network: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
 | 
			
		||||
@ -1,53 +0,0 @@
 | 
			
		||||
<div [class]="widget === false ? 'container-xl' : ''">
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="difficultyObservable$ | async" class="" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
 | 
			
		||||
  <div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
    <div class="spinner-border text-light"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="card-header mb-0 mb-lg-4" [style]="widget ? 'display:none' : ''">
 | 
			
		||||
    <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(difficultyObservable$ | async) as diffChanges">
 | 
			
		||||
      <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 90">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 180">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 365">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 730">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 1095">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'all'" [routerLink]="['/mining/difficulty' | relativeUrl]" fragment="all"> ALL
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <table class="table table-borderless table-sm text-center" *ngIf="!widget">
 | 
			
		||||
    <thead>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th i18n="mining.rank">Block</th>
 | 
			
		||||
        <th i18n="block.timestamp">Timestamp</th>
 | 
			
		||||
        <th i18n="mining.difficulty">Difficulty</th>
 | 
			
		||||
        <th i18n="mining.change">Change</th>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody *ngIf="(difficultyObservable$ | async) as diffChanges">
 | 
			
		||||
      <tr *ngFor="let diffChange of diffChanges.data">
 | 
			
		||||
        <td><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
 | 
			
		||||
        <td>‎{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
 | 
			
		||||
        <td class="d-none d-md-block">{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}</td>
 | 
			
		||||
        <td class="d-block d-md-none">{{ diffChange.difficultyShorten }}</td>
 | 
			
		||||
        <td [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,10 +0,0 @@
 | 
			
		||||
.main-title {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: #ffffff91;
 | 
			
		||||
  margin-top: -13px;
 | 
			
		||||
  font-size: 10px;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding-bottom: 3px;
 | 
			
		||||
}
 | 
			
		||||
@ -1,154 +0,0 @@
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-difficulty-chart',
 | 
			
		||||
  templateUrl: './difficulty-chart.component.html',
 | 
			
		||||
  styleUrls: ['./difficulty-chart.component.scss'],
 | 
			
		||||
  styles: [`
 | 
			
		||||
    .loadingGraphs {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 38%;
 | 
			
		||||
      left: calc(50% - 15px);
 | 
			
		||||
      z-index: 100;
 | 
			
		||||
    }
 | 
			
		||||
  `],
 | 
			
		||||
})
 | 
			
		||||
export class DifficultyChartComponent implements OnInit {
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
 | 
			
		||||
  radioGroupForm: FormGroup;
 | 
			
		||||
 | 
			
		||||
  chartOptions: EChartsOption = {};
 | 
			
		||||
  chartInitOptions = {
 | 
			
		||||
    renderer: 'svg'
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  difficultyObservable$: 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.difficulty:Difficulty`);
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue('1y');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    const powerOfTen = {
 | 
			
		||||
      terra: Math.pow(10, 12),
 | 
			
		||||
      giga: Math.pow(10, 9),
 | 
			
		||||
      mega: Math.pow(10, 6),
 | 
			
		||||
      kilo: Math.pow(10, 3),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.difficultyObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
 | 
			
		||||
      .pipe(
 | 
			
		||||
        startWith('1y'),
 | 
			
		||||
        switchMap((timespan) => {
 | 
			
		||||
          return this.apiService.getHistoricalDifficulty$(timespan)
 | 
			
		||||
            .pipe(
 | 
			
		||||
              tap(data => {
 | 
			
		||||
                this.prepareChartOptions(data.adjustments.map(val => [val.timestamp * 1000, val.difficulty]));
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
              }),
 | 
			
		||||
              map(data => {
 | 
			
		||||
                const availableTimespanDay = (
 | 
			
		||||
                  (new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp / 1000)
 | 
			
		||||
                ) / 3600 / 24;
 | 
			
		||||
 | 
			
		||||
                const tableData = [];
 | 
			
		||||
                for (let i = 0; i < data.adjustments.length - 1; ++i) {
 | 
			
		||||
                  const change = (data.adjustments[i].difficulty / data.adjustments[i + 1].difficulty - 1) * 100;
 | 
			
		||||
                  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,
 | 
			
		||||
                    difficultyShorten: formatNumber(
 | 
			
		||||
                      data.adjustments[i].difficulty / selectedPowerOfTen.divider,
 | 
			
		||||
                      this.locale, '1.2-2') + selectedPowerOfTen.unit
 | 
			
		||||
                  }));
 | 
			
		||||
                }
 | 
			
		||||
                return {
 | 
			
		||||
                  availableTimespanDay: availableTimespanDay,
 | 
			
		||||
                  data: tableData
 | 
			
		||||
                };
 | 
			
		||||
              }),
 | 
			
		||||
            );
 | 
			
		||||
          }),
 | 
			
		||||
          share()
 | 
			
		||||
        );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(data) {
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
      title: {
 | 
			
		||||
        text: this.widget? '' : $localize`:@@mining.difficulty:Difficulty`,
 | 
			
		||||
        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 diff = val / Math.pow(10, 12); // terra
 | 
			
		||||
            return diff.toString() + 'T';
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        splitLine: {
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            type: 'dotted',
 | 
			
		||||
            color: '#ffffff66',
 | 
			
		||||
            opacity: 0.25,
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          data: data,
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: false,
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            width: 3,
 | 
			
		||||
          },
 | 
			
		||||
          areaStyle: {}
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isMobile() {
 | 
			
		||||
    return (window.innerWidth <= 767.98);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -2,15 +2,18 @@
 | 
			
		||||
  <div class="container-xl">
 | 
			
		||||
    <div class="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData">
 | 
			
		||||
      <div class="col d-none d-sm-block">
 | 
			
		||||
        <span class="txPerSecond" i18n="footer.tx-vbytes-per-second">Tx vBytes per second:</span>
 | 
			
		||||
        <span *ngIf="mempoolInfoData.vBytesPerSecond === 0; else inSync">
 | 
			
		||||
           <span class="badge badge-pill badge-warning" i18n="dashboard.backend-is-synchronizing">Backend is synchronizing</span>
 | 
			
		||||
        </span>
 | 
			
		||||
        <ng-template #inSync>
 | 
			
		||||
          <div class="progress sub-text">
 | 
			
		||||
            <div class="progress-bar {{ mempoolInfoData.progressClass }}" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth}">{{ mempoolInfoData.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </ng-template>    
 | 
			
		||||
        <span class="txPerSecond" i18n="dashboard.incoming-transactions">Incoming transactions</span> 
 | 
			
		||||
        <ng-template [ngIf]="(isLoadingWebSocket$ | async) === false && mempoolInfoData" [ngIfElse]="loadingTransactions">
 | 
			
		||||
          <span *ngIf="(mempoolLoadingStatus$ | async) !== 100; else inSync">
 | 
			
		||||
             <span class="badge badge-pill badge-warning"><ng-container i18n="dashboard.backend-is-synchronizing">Backend is synchronizing</ng-container> ({{ mempoolLoadingStatus$ | async }}%)</span>
 | 
			
		||||
          </span>
 | 
			
		||||
          <ng-template #inSync>
 | 
			
		||||
            <div class="progress inc-tx-progress-bar">
 | 
			
		||||
              <div class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth, 'background-color': mempoolInfoData.progressColor}"> </div>
 | 
			
		||||
              <div class="progress-text">‎{{ mempoolInfoData.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
        </ng-template>  
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <span class="unconfirmedTx"><ng-container i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</ng-container>:</span>
 | 
			
		||||
@ -25,3 +28,7 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</footer>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingTransactions>
 | 
			
		||||
  <div class="skeleton-loader skeleton-loader-transactions"></div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -13,7 +13,7 @@ interface MempoolInfoData {
 | 
			
		||||
  memPoolInfo: MempoolInfo;
 | 
			
		||||
  vBytesPerSecond: number;
 | 
			
		||||
  progressWidth: string;
 | 
			
		||||
  progressClass: string;
 | 
			
		||||
  progressColor: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
@ -26,35 +26,62 @@ export class FooterComponent implements OnInit {
 | 
			
		||||
  mempoolBlocksData$: Observable<MempoolBlocksData>;
 | 
			
		||||
  mempoolInfoData$: Observable<MempoolInfoData>;
 | 
			
		||||
  vBytesPerSecondLimit = 1667;
 | 
			
		||||
  isLoadingWebSocket$: Observable<boolean>;
 | 
			
		||||
  mempoolLoadingStatus$: Observable<number>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.mempoolInfoData$ = combineLatest([
 | 
			
		||||
      this.stateService.mempoolInfo$,
 | 
			
		||||
      this.stateService.vbytesPerSecond$
 | 
			
		||||
    ])
 | 
			
		||||
    .pipe(
 | 
			
		||||
      map(([mempoolInfo, vbytesPerSecond]) => {
 | 
			
		||||
        const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
 | 
			
		||||
    this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
 | 
			
		||||
    this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
        let progressClass = 'bg-danger';
 | 
			
		||||
        if (percent <= 75) {
 | 
			
		||||
          progressClass = 'bg-success';
 | 
			
		||||
        } else if (percent <= 99) {
 | 
			
		||||
          progressClass = 'bg-warning';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          memPoolInfo: mempoolInfo,
 | 
			
		||||
          vBytesPerSecond: vbytesPerSecond,
 | 
			
		||||
          progressWidth: percent + '%',
 | 
			
		||||
          progressClass: progressClass,
 | 
			
		||||
        };
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
      this.mempoolInfoData$ = combineLatest([
 | 
			
		||||
        this.stateService.mempoolInfo$,
 | 
			
		||||
        this.stateService.vbytesPerSecond$
 | 
			
		||||
      ])
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map(([mempoolInfo, vbytesPerSecond]) => {
 | 
			
		||||
          const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
 | 
			
		||||
  
 | 
			
		||||
          let progressColor = '#7CB342';
 | 
			
		||||
          if (vbytesPerSecond > 1667) {
 | 
			
		||||
            progressColor = '#FDD835';
 | 
			
		||||
          }
 | 
			
		||||
          if (vbytesPerSecond > 2000) {
 | 
			
		||||
            progressColor = '#FFB300';
 | 
			
		||||
          }
 | 
			
		||||
          if (vbytesPerSecond > 2500) {
 | 
			
		||||
            progressColor = '#FB8C00';
 | 
			
		||||
          }
 | 
			
		||||
          if (vbytesPerSecond > 3000) {
 | 
			
		||||
            progressColor = '#F4511E';
 | 
			
		||||
          }
 | 
			
		||||
          if (vbytesPerSecond > 3500) {
 | 
			
		||||
            progressColor = '#D81B60';
 | 
			
		||||
          }
 | 
			
		||||
  
 | 
			
		||||
          const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
 | 
			
		||||
          let mempoolSizeProgress = 'bg-danger';
 | 
			
		||||
          if (mempoolSizePercentage <= 50) {
 | 
			
		||||
            mempoolSizeProgress = 'bg-success';
 | 
			
		||||
          } else if (mempoolSizePercentage <= 75) {
 | 
			
		||||
            mempoolSizeProgress = 'bg-warning';
 | 
			
		||||
          }
 | 
			
		||||
  
 | 
			
		||||
          return {
 | 
			
		||||
            memPoolInfo: mempoolInfo,
 | 
			
		||||
            vBytesPerSecond: vbytesPerSecond,
 | 
			
		||||
            progressWidth: percent + '%',
 | 
			
		||||
            progressColor: progressColor,
 | 
			
		||||
            mempoolSizeProgress: mempoolSizeProgress,
 | 
			
		||||
          };
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
 | 
			
		||||
      .pipe(
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,58 @@
 | 
			
		||||
<div [class]="widget === false ? 'full-container' : ''">
 | 
			
		||||
 | 
			
		||||
  <div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
 | 
			
		||||
    <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as hashrates">
 | 
			
		||||
      <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90">
 | 
			
		||||
          <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 ? 'chart' : 'chart-widget'"
 | 
			
		||||
    echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
 | 
			
		||||
  <div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
    <div class="spinner-border text-light"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
  <!-- <div class="mt-3" *ngIf="!widget">
 | 
			
		||||
    <table class="table table-borderless table-sm text-center">
 | 
			
		||||
      <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
          <th i18n="mining.rank">Block</th>
 | 
			
		||||
          <th class="d-none d-md-block" i18n="block.timestamp">Timestamp</th>
 | 
			
		||||
          <th i18n="mining.adjusted">Adjusted</th>
 | 
			
		||||
          <th i18n="mining.difficulty">Difficulty</th>
 | 
			
		||||
          <th i18n="mining.change">Change</th>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody *ngIf="(hashrateObservable$ | async) as data">
 | 
			
		||||
        <tr *ngFor="let diffChange of data.difficulty">
 | 
			
		||||
          <td><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
 | 
			
		||||
          <td class="d-none d-md-block">‎{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
 | 
			
		||||
          <td><app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since></td>
 | 
			
		||||
          <td class="d-none d-md-block">{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}</td>
 | 
			
		||||
          <td class="d-block d-md-none">{{ diffChange.difficultyShorten }}</td>
 | 
			
		||||
          <td [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
  </div> -->
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,50 @@
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
.chart-widget {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  max-height: 275px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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,299 @@
 | 
			
		||||
import { Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
 | 
			
		||||
import { EChartsOption, graphic } 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 = 45;
 | 
			
		||||
  @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.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue('1y');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    if (!this.widget) {
 | 
			
		||||
      this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Difficulty`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
 | 
			
		||||
      .pipe(
 | 
			
		||||
        startWith('1y'),
 | 
			
		||||
        switchMap((timespan) => {
 | 
			
		||||
          return this.apiService.getHistoricalHashrate$(timespan)
 | 
			
		||||
            .pipe(
 | 
			
		||||
              tap((data: any) => {
 | 
			
		||||
                // We generate duplicated data point so the tooltip works nicely
 | 
			
		||||
                const diffFixed = [];
 | 
			
		||||
                let diffIndex = 1;
 | 
			
		||||
                let hashIndex = 0;
 | 
			
		||||
                while (hashIndex < data.hashrates.length) {
 | 
			
		||||
                  if (diffIndex >= data.difficulty.length) {
 | 
			
		||||
                    while (hashIndex < data.hashrates.length) {
 | 
			
		||||
                      diffFixed.push({
 | 
			
		||||
                        timestamp: data.hashrates[hashIndex].timestamp,
 | 
			
		||||
                        difficulty: data.difficulty[data.difficulty.length - 1].difficulty
 | 
			
		||||
                      });
 | 
			
		||||
                      ++hashIndex;
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  while (hashIndex < data.hashrates.length && diffIndex < data.difficulty.length &&
 | 
			
		||||
                    data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].timestamp
 | 
			
		||||
                  ) {
 | 
			
		||||
                    diffFixed.push({
 | 
			
		||||
                      timestamp: data.hashrates[hashIndex].timestamp,
 | 
			
		||||
                      difficulty: data.difficulty[diffIndex - 1].difficulty
 | 
			
		||||
                    });
 | 
			
		||||
                    ++hashIndex;
 | 
			
		||||
                  }
 | 
			
		||||
                  ++diffIndex;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                this.prepareChartOptions({
 | 
			
		||||
                  hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]),
 | 
			
		||||
                  difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty])
 | 
			
		||||
                });
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
              }),
 | 
			
		||||
              map((data: any) => {
 | 
			
		||||
                const availableTimespanDay = (
 | 
			
		||||
                  (new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp)
 | 
			
		||||
                ) / 3600 / 24;
 | 
			
		||||
 | 
			
		||||
                const tableData = [];
 | 
			
		||||
                for (let i = data.difficulty.length - 1; i > 0; --i) {
 | 
			
		||||
                  const selectedPowerOfTen: any = selectPowerOfTen(data.difficulty[i].difficulty);
 | 
			
		||||
                  const change = (data.difficulty[i].difficulty / data.difficulty[i - 1].difficulty - 1) * 100;
 | 
			
		||||
 | 
			
		||||
                  tableData.push(Object.assign(data.difficulty[i], {
 | 
			
		||||
                    change: change,
 | 
			
		||||
                    difficultyShorten: formatNumber(
 | 
			
		||||
                      data.difficulty[i].difficulty / selectedPowerOfTen.divider,
 | 
			
		||||
                      this.locale, '1.2-2') + selectedPowerOfTen.unit
 | 
			
		||||
                  }));
 | 
			
		||||
                }
 | 
			
		||||
                return {
 | 
			
		||||
                  availableTimespanDay: availableTimespanDay,
 | 
			
		||||
                  difficulty: tableData
 | 
			
		||||
                };
 | 
			
		||||
              }),
 | 
			
		||||
            );
 | 
			
		||||
        }),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(data) {
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
      color: [
 | 
			
		||||
        new graphic.LinearGradient(0, 0, 0, 0.65, [
 | 
			
		||||
          { offset: 0, color: '#F4511E' },
 | 
			
		||||
          { offset: 0.25, color: '#FB8C00' },
 | 
			
		||||
          { offset: 0.5, color: '#FFB300' },
 | 
			
		||||
          { offset: 0.75, color: '#FDD835' },
 | 
			
		||||
          { offset: 1, color: '#7CB342' }
 | 
			
		||||
        ]),
 | 
			
		||||
        '#D81B60',
 | 
			
		||||
      ],
 | 
			
		||||
      grid: {
 | 
			
		||||
        right: this.right,
 | 
			
		||||
        left: this.left,
 | 
			
		||||
        bottom: this.widget ? 30 : 60,
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        trigger: 'axis',
 | 
			
		||||
        axisPointer: {
 | 
			
		||||
          type: 'line'
 | 
			
		||||
        },
 | 
			
		||||
        backgroundColor: 'rgba(17, 19, 31, 1)',
 | 
			
		||||
        borderRadius: 4,
 | 
			
		||||
        shadowColor: 'rgba(0, 0, 0, 0.5)',
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: '#b1b1b1',
 | 
			
		||||
          align: 'left',
 | 
			
		||||
        },
 | 
			
		||||
        borderColor: '#000',
 | 
			
		||||
        formatter: function (data) {
 | 
			
		||||
          let hashratePowerOfTen: any = selectPowerOfTen(1);
 | 
			
		||||
          let hashrate = data[0].data[1];
 | 
			
		||||
          let difficultyPowerOfTen = hashratePowerOfTen;
 | 
			
		||||
          let difficulty = data[1].data[1];
 | 
			
		||||
 | 
			
		||||
          if (this.isMobile()) {
 | 
			
		||||
            hashratePowerOfTen = selectPowerOfTen(data[0].data[1]);
 | 
			
		||||
            hashrate = Math.round(data[0].data[1] / hashratePowerOfTen.divider);
 | 
			
		||||
            difficultyPowerOfTen = selectPowerOfTen(data[1].data[1]);
 | 
			
		||||
            difficulty = Math.round(data[1].data[1] / difficultyPowerOfTen.divider);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return `
 | 
			
		||||
            <b style="color: white; margin-left: 18px">${data[0].axisValueLabel}</b><br>
 | 
			
		||||
            <span>${data[0].marker} ${data[0].seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s</span><br>
 | 
			
		||||
            <span>${data[1].marker} ${data[1].seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}</span>
 | 
			
		||||
          `;
 | 
			
		||||
        }.bind(this)
 | 
			
		||||
      },
 | 
			
		||||
      xAxis: {
 | 
			
		||||
        type: 'time',
 | 
			
		||||
        splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
 | 
			
		||||
      },
 | 
			
		||||
      legend: {
 | 
			
		||||
        data: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Hashrate',
 | 
			
		||||
            inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              color: 'white',
 | 
			
		||||
            },
 | 
			
		||||
            icon: 'roundRect',
 | 
			
		||||
            itemStyle: {
 | 
			
		||||
              color: '#FFB300',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Difficulty',
 | 
			
		||||
            inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              color: 'white',
 | 
			
		||||
            },
 | 
			
		||||
            icon: 'roundRect',
 | 
			
		||||
            itemStyle: {
 | 
			
		||||
              color: '#D81B60',
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      yAxis: [
 | 
			
		||||
        {
 | 
			
		||||
          min: function (value) {
 | 
			
		||||
            return value.min * 0.9;
 | 
			
		||||
          },
 | 
			
		||||
          type: 'value',
 | 
			
		||||
          name: 'Hashrate',
 | 
			
		||||
          axisLabel: {
 | 
			
		||||
            color: 'rgb(110, 112, 121)',
 | 
			
		||||
            formatter: (val) => {
 | 
			
		||||
              const selectedPowerOfTen: any = selectPowerOfTen(val);
 | 
			
		||||
              const newVal = Math.round(val / selectedPowerOfTen.divider);
 | 
			
		||||
              return `${newVal} ${selectedPowerOfTen.unit}H/s`
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          splitLine: {
 | 
			
		||||
            show: false,
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          min: function (value) {
 | 
			
		||||
            return value.min * 0.9;
 | 
			
		||||
          },
 | 
			
		||||
          type: 'value',
 | 
			
		||||
          name: 'Difficulty',
 | 
			
		||||
          position: 'right',
 | 
			
		||||
          axisLabel: {
 | 
			
		||||
            color: 'rgb(110, 112, 121)',
 | 
			
		||||
            formatter: (val) => {
 | 
			
		||||
              const selectedPowerOfTen: any = selectPowerOfTen(val);
 | 
			
		||||
              const newVal = Math.round(val / selectedPowerOfTen.divider);
 | 
			
		||||
              return `${newVal} ${selectedPowerOfTen.unit}`
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          splitLine: {
 | 
			
		||||
            show: false,
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'Hashrate',
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          symbol: 'none',
 | 
			
		||||
          data: data.hashrates,
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            width: 2,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          yAxisIndex: 1,
 | 
			
		||||
          name: 'Difficulty',
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          symbol: 'none',
 | 
			
		||||
          data: data.difficulty,
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            width: 3,
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,34 @@
 | 
			
		||||
<div [class]="widget === false ? 'full-container' : ''">
 | 
			
		||||
 | 
			
		||||
  <div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
 | 
			
		||||
    <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as hashrates">
 | 
			
		||||
      <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90">
 | 
			
		||||
          <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 ? 'chart' : 'chart-widget'"
 | 
			
		||||
    echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
 | 
			
		||||
  <div class="text-center loadingGraphs" [class]="widget ? 'widget' : ''" *ngIf="isLoading">
 | 
			
		||||
    <div class="spinner-border text-light"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,60 @@
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
.chart-widget {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  max-height: 275px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loadingGraphs {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  left: calc(50% - 15px);
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
}
 | 
			
		||||
.loadingGraphs.widget {
 | 
			
		||||
  top: 75%;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,185 @@
 | 
			
		||||
import { ChangeDetectionStrategy, 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 { FormBuilder, FormGroup } from '@angular/forms';
 | 
			
		||||
import { poolsColor } from 'src/app/app.constants';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-hashrate-chart-pools',
 | 
			
		||||
  templateUrl: './hashrate-chart-pools.component.html',
 | 
			
		||||
  styleUrls: ['./hashrate-chart-pools.component.scss'],
 | 
			
		||||
  styles: [`
 | 
			
		||||
    .loadingGraphs {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 50%;
 | 
			
		||||
      left: calc(50% - 15px);
 | 
			
		||||
      z-index: 100;
 | 
			
		||||
    }
 | 
			
		||||
  `],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class HashrateChartPoolsComponent implements OnInit {
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
  @Input() right: number | string = 40;
 | 
			
		||||
  @Input() left: number | string = 25;
 | 
			
		||||
 | 
			
		||||
  radioGroupForm: FormGroup;
 | 
			
		||||
 | 
			
		||||
  chartOptions: EChartsOption = {};
 | 
			
		||||
  chartInitOptions = {
 | 
			
		||||
    renderer: 'svg',
 | 
			
		||||
    width: 'auto',
 | 
			
		||||
    height: 'auto',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  hashrateObservable$: Observable<any>;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private formBuilder: FormBuilder,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue('1y');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    if (!this.widget) {
 | 
			
		||||
      this.seoService.setTitle($localize`:@@mining.pools-historical-dominance:Pools Historical Dominance`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
 | 
			
		||||
      .pipe(
 | 
			
		||||
        startWith('1y'),
 | 
			
		||||
        switchMap((timespan) => {
 | 
			
		||||
          this.isLoading = true;
 | 
			
		||||
          return this.apiService.getHistoricalPoolsHashrate$(timespan)
 | 
			
		||||
            .pipe(
 | 
			
		||||
              tap((data: any) => {
 | 
			
		||||
                // Prepare series (group all hashrates data point by pool)
 | 
			
		||||
                const grouped = {};
 | 
			
		||||
                for (const hashrate of data.hashrates) {
 | 
			
		||||
                  if (!grouped.hasOwnProperty(hashrate.poolName)) {
 | 
			
		||||
                    grouped[hashrate.poolName] = [];
 | 
			
		||||
                  }
 | 
			
		||||
                  grouped[hashrate.poolName].push(hashrate);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const series = [];
 | 
			
		||||
                const legends = [];
 | 
			
		||||
                for (const name in grouped) {
 | 
			
		||||
                  series.push({
 | 
			
		||||
                    stack: 'Total',
 | 
			
		||||
                    name: name,
 | 
			
		||||
                    showSymbol: false,
 | 
			
		||||
                    symbol: 'none',
 | 
			
		||||
                    data: grouped[name].map((val) => [val.timestamp * 1000, (val.share * 100).toFixed(2)]),
 | 
			
		||||
                    type: 'line',
 | 
			
		||||
                    lineStyle: { width: 0 },
 | 
			
		||||
                    areaStyle: { opacity: 1 },
 | 
			
		||||
                    smooth: true,
 | 
			
		||||
                    color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
 | 
			
		||||
                    emphasis: {
 | 
			
		||||
                      disabled: true,
 | 
			
		||||
                      scale: false,
 | 
			
		||||
                    },
 | 
			
		||||
                  });
 | 
			
		||||
 | 
			
		||||
                  legends.push({
 | 
			
		||||
                    name: name,
 | 
			
		||||
                    inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
                    textStyle: {
 | 
			
		||||
                      color: 'white',
 | 
			
		||||
                    },
 | 
			
		||||
                    icon: 'roundRect',
 | 
			
		||||
                    itemStyle: {
 | 
			
		||||
                      color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
 | 
			
		||||
                    },
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                this.prepareChartOptions({
 | 
			
		||||
                  legends: legends,
 | 
			
		||||
                  series: series
 | 
			
		||||
                });
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
              }),
 | 
			
		||||
              map((data: any) => {
 | 
			
		||||
                const availableTimespanDay = (
 | 
			
		||||
                  (new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp)
 | 
			
		||||
                ) / 3600 / 24;
 | 
			
		||||
                return {
 | 
			
		||||
                  availableTimespanDay: availableTimespanDay,
 | 
			
		||||
                };
 | 
			
		||||
              }),
 | 
			
		||||
            );
 | 
			
		||||
        }),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(data) {
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
      grid: {
 | 
			
		||||
        right: this.right,
 | 
			
		||||
        left: this.left,
 | 
			
		||||
        bottom: this.widget ? 30 : 20,
 | 
			
		||||
        top: this.widget ? 10 : 40,
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        trigger: 'axis',
 | 
			
		||||
        axisPointer: {
 | 
			
		||||
          type: 'line'
 | 
			
		||||
        },
 | 
			
		||||
        backgroundColor: 'rgba(17, 19, 31, 1)',
 | 
			
		||||
        borderRadius: 4,
 | 
			
		||||
        shadowColor: 'rgba(0, 0, 0, 0.5)',
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: '#b1b1b1',
 | 
			
		||||
          align: 'left',
 | 
			
		||||
        },
 | 
			
		||||
        borderColor: '#000',
 | 
			
		||||
        formatter: function (data) {
 | 
			
		||||
          let tooltip = `<b style="color: white; margin-left: 18px">${data[0].axisValueLabel}</b><br>`;
 | 
			
		||||
          data.sort((a, b) => b.data[1] - a.data[1]);
 | 
			
		||||
          for (const pool of data) {
 | 
			
		||||
            if (pool.data[1] > 0) {
 | 
			
		||||
              tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1]}%<br>`
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return tooltip;
 | 
			
		||||
        }.bind(this)
 | 
			
		||||
      },
 | 
			
		||||
      xAxis: {
 | 
			
		||||
        type: 'time',
 | 
			
		||||
        splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
 | 
			
		||||
      },
 | 
			
		||||
      legend: (this.isMobile() || this.widget) ? undefined : {
 | 
			
		||||
        data: data.legends
 | 
			
		||||
      },
 | 
			
		||||
      yAxis: {
 | 
			
		||||
        position: 'right',
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          color: 'rgb(110, 112, 121)',
 | 
			
		||||
          formatter: (val) => `${val}%`,
 | 
			
		||||
        },
 | 
			
		||||
        splitLine: {
 | 
			
		||||
          show: false,
 | 
			
		||||
        },
 | 
			
		||||
        type: 'value',
 | 
			
		||||
        max: 100,
 | 
			
		||||
        min: 0,
 | 
			
		||||
      },
 | 
			
		||||
      series: data.series,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isMobile() {
 | 
			
		||||
    return (window.innerWidth <= 767.98);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -160,6 +160,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: false,
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          symbol: 'none',
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            width: 3,
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,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">
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,10 @@
 | 
			
		||||
            <div class="fee-span">
 | 
			
		||||
              {{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="block-size" [innerHTML]="'‎' + (projectedBlock.blockSize | bytes: 2)"></div>
 | 
			
		||||
            <div *ngIf="showMiningInfo" class="block-size">
 | 
			
		||||
              <app-amount [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (projectedBlock.blockSize | bytes: 2)"></div>
 | 
			
		||||
            <div class="transaction-count">
 | 
			
		||||
              <ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
 | 
			
		||||
              <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import { take, map, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
 | 
			
		||||
import { specialBlocks } from 'src/app/app.constants';
 | 
			
		||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
import { Location } from '@angular/common';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-mempool-blocks',
 | 
			
		||||
@ -32,6 +33,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  networkSubscription: Subscription;
 | 
			
		||||
  network = '';
 | 
			
		||||
  now = new Date().getTime();
 | 
			
		||||
  showMiningInfo = false;
 | 
			
		||||
 | 
			
		||||
  blockWidth = 125;
 | 
			
		||||
  blockPadding = 30;
 | 
			
		||||
@ -54,9 +56,20 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private cd: ChangeDetectorRef,
 | 
			
		||||
    private relativeUrlPipe: RelativeUrlPipe,
 | 
			
		||||
    private location: Location
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  enabledMiningInfoIfNeeded(url) {
 | 
			
		||||
    this.showMiningInfo = url === '/mining';
 | 
			
		||||
    this.cd.markForCheck(); // Need to update the view asap
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
 | 
			
		||||
      this.enabledMiningInfoIfNeeded(this.location.path());
 | 
			
		||||
      this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
 | 
			
		||||
      this.feeRounding = '1.0-1';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,30 +1,37 @@
 | 
			
		||||
<div class="container-xl dashboard-container">
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  <div class="row row-cols-1 row-cols-md-2">
 | 
			
		||||
 | 
			
		||||
    <!-- pool distribution -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="main-title" i18n="mining.pool-share">Mining Pools Share (1w)</div>
 | 
			
		||||
      <div class="card">
 | 
			
		||||
      <div class="card double">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <!-- pool distribution -->
 | 
			
		||||
          <h5 class="card-title">
 | 
			
		||||
            <a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="mining.pool-share">
 | 
			
		||||
              Mining Pools Share (1w)
 | 
			
		||||
            </a>
 | 
			
		||||
          </h5>
 | 
			
		||||
          <app-pool-ranking [widget]=true></app-pool-ranking>
 | 
			
		||||
          <div class="text-center"><a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more
 | 
			
		||||
              »</a></div>
 | 
			
		||||
 | 
			
		||||
          <!-- pools hashrate -->
 | 
			
		||||
          <app-hashrate-chart-pools [widget]=true></app-hashrate-chart-pools>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- difficulty -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="main-title" i18n="mining.difficulty">Difficulty (1y)</div>
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <app-difficulty-chart [widget]=true></app-difficulty-chart>
 | 
			
		||||
          <div class="text-center"><a href="" [routerLink]="['/mining/difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
 | 
			
		||||
              »</a></div>
 | 
			
		||||
          <!-- hashrate -->
 | 
			
		||||
          <h5 class="card-title">
 | 
			
		||||
            <a class="link" href="" [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="mining.hashrate">
 | 
			
		||||
              Hashrate (1y)
 | 
			
		||||
            </a>
 | 
			
		||||
          </h5>
 | 
			
		||||
          <app-hashrate-chart [widget]=true></app-hashrate-chart>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    </div>  
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -12,21 +12,24 @@
 | 
			
		||||
 | 
			
		||||
.card {
 | 
			
		||||
  background-color: #1d1f31;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  height: 340px;
 | 
			
		||||
}
 | 
			
		||||
.card.double {
 | 
			
		||||
  height: 620px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-wrapper {
 | 
			
		||||
  .card {
 | 
			
		||||
    height: auto !important;
 | 
			
		||||
  }
 | 
			
		||||
  .card-body {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex: inherit;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    justify-content: space-around;
 | 
			
		||||
    padding: 22px 20px;
 | 
			
		||||
  }
 | 
			
		||||
.card-title {
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
}
 | 
			
		||||
.card-title > a {
 | 
			
		||||
  color: #4a68b9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-body {
 | 
			
		||||
  padding: 1.25rem 1rem 0.75rem 1rem;
 | 
			
		||||
}
 | 
			
		||||
.card-body.pool-ranking {
 | 
			
		||||
  padding: 1.25rem 0.25rem 0.75rem 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#blockchain-container {
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-mining-dashboard',
 | 
			
		||||
@ -8,7 +9,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
})
 | 
			
		||||
export class MiningDashboardComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
  constructor(private seoService: SeoService) {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +1,12 @@
 | 
			
		||||
<div [class]="widget === false ? 'container-xl' : ''">
 | 
			
		||||
 | 
			
		||||
  <div class="hashrate-pie" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
 | 
			
		||||
  <div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
  <div [class]="widget ? 'chart-widget' : 'chart'"
 | 
			
		||||
    echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
 | 
			
		||||
  <div class="text-center loadingGraphs" [class]="widget ? 'widget' : ''" *ngIf="isLoading">
 | 
			
		||||
    <div class="spinner-border text-light"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="card-header mb-0 mb-lg-4" [style]="widget === true ? 'display:none' : ''">
 | 
			
		||||
  <div class="card-header mb-0 mb-lg-4 mt-md-3" [style]="widget ? 'display:none' : ''">
 | 
			
		||||
    <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(miningStatsObservable$ | async) as miningStats">
 | 
			
		||||
      <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 1">
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,14 @@
 | 
			
		||||
.hashrate-pie {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  min-height: 400px;
 | 
			
		||||
.chart {
 | 
			
		||||
  max-height: 400px;
 | 
			
		||||
  @media (max-width: 767.98px) {
 | 
			
		||||
    min-height: 300px;
 | 
			
		||||
    max-height: 300px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.chart-widget {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  max-height: 275px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.formRadioGroup {
 | 
			
		||||
  margin-top: 6px;
 | 
			
		||||
@ -30,3 +34,13 @@
 | 
			
		||||
      padding: .3em !important;
 | 
			
		||||
   }
 | 
			
		||||
 }
 | 
			
		||||
 | 
			
		||||
.loadingGraphs {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  left: calc(50% - 15px);
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
}
 | 
			
		||||
.loadingGraphs.widget {
 | 
			
		||||
  top: 25%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { FormBuilder, FormGroup } from '@angular/forms';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { EChartsOption, PieSeriesOption } from 'echarts';
 | 
			
		||||
@ -9,20 +9,13 @@ import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { StorageService } from '../..//services/storage.service';
 | 
			
		||||
import { MiningService, MiningStats } from '../../services/mining.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { chartColors } from 'src/app/app.constants';
 | 
			
		||||
import { chartColors, poolsColor } from 'src/app/app.constants';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-pool-ranking',
 | 
			
		||||
  templateUrl: './pool-ranking.component.html',
 | 
			
		||||
  styleUrls: ['./pool-ranking.component.scss'],
 | 
			
		||||
  styles: [`
 | 
			
		||||
    .loadingGraphs {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 38%;
 | 
			
		||||
      left: calc(50% - 15px);
 | 
			
		||||
      z-index: 100;
 | 
			
		||||
    }
 | 
			
		||||
  `],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class PoolRankingComponent implements OnInit {
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
@ -33,7 +26,9 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  chartOptions: EChartsOption = {};
 | 
			
		||||
  chartInitOptions = {
 | 
			
		||||
    renderer: 'svg'
 | 
			
		||||
    renderer: 'svg',
 | 
			
		||||
    width: 'auto',
 | 
			
		||||
    height: 'auto',
 | 
			
		||||
  };
 | 
			
		||||
  chartInstance: any = undefined;
 | 
			
		||||
 | 
			
		||||
@ -47,13 +42,13 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    if (this.widget) {
 | 
			
		||||
      this.poolsWindowPreference = '1w';
 | 
			
		||||
    } else {
 | 
			
		||||
      this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
 | 
			
		||||
      this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w';    
 | 
			
		||||
    }
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference });
 | 
			
		||||
@ -118,24 +113,30 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      data.push({
 | 
			
		||||
        itemStyle: {
 | 
			
		||||
          color: poolsColor[pool.name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
 | 
			
		||||
        },
 | 
			
		||||
        value: pool.share,
 | 
			
		||||
        name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`),
 | 
			
		||||
        label: {
 | 
			
		||||
          color: '#FFFFFF',
 | 
			
		||||
          color: '#b1b1b1',
 | 
			
		||||
          overflow: 'break',
 | 
			
		||||
        },
 | 
			
		||||
        tooltip: {
 | 
			
		||||
          backgroundColor: '#282d47',
 | 
			
		||||
          backgroundColor: 'rgba(17, 19, 31, 1)',
 | 
			
		||||
          borderRadius: 4,
 | 
			
		||||
          shadowColor: 'rgba(0, 0, 0, 0.5)',
 | 
			
		||||
          textStyle: {
 | 
			
		||||
            color: '#FFFFFF',
 | 
			
		||||
            color: '#b1b1b1',
 | 
			
		||||
          },
 | 
			
		||||
          borderColor: '#000',
 | 
			
		||||
          formatter: () => {
 | 
			
		||||
            if (this.poolsWindowPreference === '24h') {
 | 
			
		||||
              return `<u><b>${pool.name} (${pool.share}%)</b></u><br>` +
 | 
			
		||||
              return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
 | 
			
		||||
                pool.lastEstimatedHashrate.toString() + ' PH/s' +
 | 
			
		||||
                `<br>` + pool.blockCount.toString() + ` blocks`;
 | 
			
		||||
            } else {
 | 
			
		||||
              return `<u><b>${pool.name} (${pool.share}%)</b></u><br>` +
 | 
			
		||||
              return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
 | 
			
		||||
                pool.blockCount.toString() + ` blocks`;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
@ -153,7 +154,13 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
    }
 | 
			
		||||
    network = network.charAt(0).toUpperCase() + network.slice(1);
 | 
			
		||||
 | 
			
		||||
    let radius: any[] = ['20%', '70%'];
 | 
			
		||||
    if (this.isMobile() || this.widget) {
 | 
			
		||||
      radius = ['20%', '60%'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
      color: chartColors,
 | 
			
		||||
      title: {
 | 
			
		||||
        text: this.widget ? '' : $localize`:@@mining.pool-chart-title:${network}:NETWORK: mining pools share`,
 | 
			
		||||
        left: 'center',
 | 
			
		||||
@ -166,17 +173,21 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        trigger: 'item'
 | 
			
		||||
        trigger: 'item',
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          align: 'left',
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          top: this.widget ? '0%' : (this.isMobile() ? '5%' : '10%'),
 | 
			
		||||
          bottom: this.widget ? '0%' : (this.isMobile() ? '0%' : '5%'),
 | 
			
		||||
          top: this.widget ? 0 : 35,
 | 
			
		||||
          name: 'Mining pool',
 | 
			
		||||
          type: 'pie',
 | 
			
		||||
          radius: this.widget ? ['20%', '60%'] : (this.isMobile() ? ['10%', '50%'] : ['20%', '70%']),
 | 
			
		||||
          radius: radius,
 | 
			
		||||
          data: this.generatePoolsChartSerieData(miningStats),
 | 
			
		||||
          labelLine: {
 | 
			
		||||
            length: this.isMobile() ? 10 : 15,
 | 
			
		||||
            length2: this.isMobile() ? 0 : 15,
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              width: 2,
 | 
			
		||||
            },
 | 
			
		||||
@ -202,7 +213,6 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      color: chartColors
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -172,9 +172,12 @@
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="text-right nowrap amount">
 | 
			
		||||
                  <ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
 | 
			
		||||
                    <div *ngIf="assetsMinimal && assetsMinimal[vout.asset]">
 | 
			
		||||
                    <div *ngIf="assetsMinimal && assetsMinimal[vout.asset] else assetNotFound">
 | 
			
		||||
                      <ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <ng-template #assetNotFound>
 | 
			
		||||
                      {{ vout.value }} <a [routerLink]="['/assets/asset/' | relativeUrl, vout.asset]">{{ vout.asset | slice : 0 : 7 }}</a>
 | 
			
		||||
                    </ng-template>
 | 
			
		||||
                  </ng-template>
 | 
			
		||||
                  <ng-template #defaultOutput>
 | 
			
		||||
                    <app-amount [satoshis]="vout.value"></app-amount>
 | 
			
		||||
@ -254,7 +257,7 @@
 | 
			
		||||
           
 | 
			
		||||
        </span>
 | 
			
		||||
        <button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">
 | 
			
		||||
          <ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
 | 
			
		||||
          <ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
 | 
			
		||||
          <ng-template #defaultAmount>
 | 
			
		||||
            <app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
 | 
			
		||||
@ -95,6 +95,10 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  haveBlindedOutputValues(tx: Transaction): boolean {
 | 
			
		||||
    return tx.vout.some((v: any) => v.value === undefined);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getTotalTxOutput(tx: Transaction) {
 | 
			
		||||
    return tx.vout.map((v: any) => v.value || 0).reduce((a: number, b: number) => a + b);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -16,58 +16,79 @@
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <div class="card">
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
 | 
			
		||||
            <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <div class="card">
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
 | 
			
		||||
            <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? mempoolTable : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
    <ng-template #expanded>
 | 
			
		||||
      <div class="col card-wrapper" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
 | 
			
		||||
        <div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
 | 
			
		||||
        <div class="card">
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <app-fees-box class="d-block"></app-fees-box>
 | 
			
		||||
      <ng-container *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
 | 
			
		||||
        <div class="col card-wrapper">
 | 
			
		||||
          <div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
 | 
			
		||||
          <div class="card">
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
              <app-fees-box class="d-block"></app-fees-box>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
 | 
			
		||||
        <app-difficulty></app-difficulty>
 | 
			
		||||
      </div>
 | 
			
		||||
        <div class="col">
 | 
			
		||||
          <app-difficulty></app-difficulty>
 | 
			
		||||
        </div>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <div class="card graph-card">
 | 
			
		||||
          <div class="card-body pl-0">
 | 
			
		||||
            <div style="padding-left: 1.25rem;">
 | 
			
		||||
              <ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
 | 
			
		||||
              <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
 | 
			
		||||
              <hr>
 | 
			
		||||
            </div>
 | 
			
		||||
            <ng-container *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
 | 
			
		||||
              <div class="mempool-graph">
 | 
			
		||||
                <app-mempool-graph
 | 
			
		||||
                [template]="'widget'"
 | 
			
		||||
                [limitFee]="150"
 | 
			
		||||
                [limitFilterFee]="1"
 | 
			
		||||
                [data]="mempoolStats.value?.mempool"
 | 
			
		||||
                [windowPreferenceOverride]="'2h'"
 | 
			
		||||
                ></app-mempool-graph>
 | 
			
		||||
              </div>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
            <ng-template [ngIf]="(network$ | async) !== 'liquid'" [ngIfElse]="liquidPegs">
 | 
			
		||||
              <ng-container *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
 | 
			
		||||
                <div class="mempool-graph">
 | 
			
		||||
                  <app-mempool-graph
 | 
			
		||||
                  [template]="'widget'"
 | 
			
		||||
                  [limitFee]="150"
 | 
			
		||||
                  [limitFilterFee]="1"
 | 
			
		||||
                  [data]="mempoolStats.value?.mempool"
 | 
			
		||||
                  [windowPreferenceOverride]="'2h'"
 | 
			
		||||
                  ></app-mempool-graph>
 | 
			
		||||
                </div>
 | 
			
		||||
              </ng-container>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
            <ng-template #liquidPegs>
 | 
			
		||||
              <app-lbtc-pegs-graph [data]="liquidPegsMonth$ | async"></app-lbtc-pegs-graph>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <div class="card graph-card">
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
 | 
			
		||||
            <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? mempoolTable : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
 | 
			
		||||
            <hr>
 | 
			
		||||
            <div class="mempool-graph" *ngIf="stateService.network === 'liquid'; else mempoolGraph">
 | 
			
		||||
              <app-lbtc-pegs-graph [data]="liquidPegsMonth$ | async"></app-lbtc-pegs-graph>
 | 
			
		||||
              <table class="table table-borderless table-striped" *ngIf="(featuredAssets$ | async) as featuredAssets else loadingAssetsTable">
 | 
			
		||||
                <tbody>
 | 
			
		||||
                  <tr *ngFor="let group of featuredAssets">
 | 
			
		||||
                    <td class="asset-icon">
 | 
			
		||||
                      <a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">
 | 
			
		||||
                        <img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + group.asset + '/icon'">
 | 
			
		||||
                      </a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td class="asset-title">
 | 
			
		||||
                      <a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">{{ group.name }}</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td class="circulating-amount"><app-asset-circulation [assetId]="group.asset"></app-asset-circulation></td>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                </tbody>
 | 
			
		||||
              </table>
 | 
			
		||||
            </div>
 | 
			
		||||
            <ng-template #mempoolGraph>
 | 
			
		||||
              <div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
 | 
			
		||||
@ -158,6 +179,27 @@
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingAssetsTable>
 | 
			
		||||
  <table class="table table-borderless table-striped asset-table">
 | 
			
		||||
    <tbody>
 | 
			
		||||
      <tr *ngFor="let i of [1,2,3,4]">
 | 
			
		||||
        <td class="asset-icon">
 | 
			
		||||
          <div class="skeleton-loader skeleton-loader-transactions"></div>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td class="asset-title">
 | 
			
		||||
          <div class="skeleton-loader skeleton-loader-transactions"></div>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td class="asset-title d-none d-md-table-cell">
 | 
			
		||||
          <div class="skeleton-loader skeleton-loader-transactions"></div>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td class="asset-title">
 | 
			
		||||
          <div class="skeleton-loader skeleton-loader-transactions"></div>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingTransactions>
 | 
			
		||||
  <div class="skeleton-loader skeleton-loader-transactions"></div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
@ -283,3 +283,25 @@
 | 
			
		||||
  margin-right: -2px;
 | 
			
		||||
  font-size: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.assetIcon {
 | 
			
		||||
  width: 40px;
 | 
			
		||||
  height: 40px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.asset-title {
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.asset-icon {
 | 
			
		||||
  width: 65px;
 | 
			
		||||
  height: 65px;
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.circulating-amount {
 | 
			
		||||
  text-align: right;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
 | 
			
		||||
import { combineLatest, merge, Observable, of, timer } from 'rxjs';
 | 
			
		||||
import { combineLatest, merge, Observable, of } from 'rxjs';
 | 
			
		||||
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
 | 
			
		||||
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
@ -34,6 +34,7 @@ interface MempoolStatsData {
 | 
			
		||||
})
 | 
			
		||||
export class DashboardComponent implements OnInit {
 | 
			
		||||
  collapseLevel: string;
 | 
			
		||||
  featuredAssets$: Observable<any>;
 | 
			
		||||
  network$: Observable<string>;
 | 
			
		||||
  mempoolBlocksData$: Observable<MempoolBlocksData>;
 | 
			
		||||
  mempoolInfoData$: Observable<MempoolInfoData>;
 | 
			
		||||
@ -124,6 +125,19 @@ export class DashboardComponent implements OnInit {
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.featuredAssets$ = this.apiService.listFeaturedAssets$()
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map((featured) => {
 | 
			
		||||
          const newArray = [];
 | 
			
		||||
          for (const feature of featured) {
 | 
			
		||||
            if (feature.ticker !== 'L-BTC' && feature.asset) {
 | 
			
		||||
              newArray.push(feature);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return newArray.slice(0, 4);
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.blocks$ = this.stateService.blocks$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        tap(([block]) => {
 | 
			
		||||
 | 
			
		||||
@ -101,6 +101,7 @@ export interface PoolStat {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface BlockExtension {
 | 
			
		||||
  totalFees?: number;
 | 
			
		||||
  medianFee?: number;
 | 
			
		||||
  feeRange?: number[];
 | 
			
		||||
  reward?: number;
 | 
			
		||||
 | 
			
		||||
@ -156,4 +156,18 @@ 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}` : '')
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getHistoricalPoolsHashrate$(interval: string | undefined): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any[]>(
 | 
			
		||||
        this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate/pools` +
 | 
			
		||||
        (interval !== undefined ? `/${interval}` : '')
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -82,7 +82,7 @@ export class MiningService {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const availableTimespanDay = (
 | 
			
		||||
      (new Date().getTime() / 1000) - (stats.oldestIndexedBlockTimestamp / 1000)
 | 
			
		||||
      (new Date().getTime() / 1000) - (stats.oldestIndexedBlockTimestamp)
 | 
			
		||||
    ) / 3600 / 24;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										22
									
								
								frontend/src/app/shared/pipes/amount-shortener.pipe.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,22 @@
 | 
			
		||||
import { Pipe, PipeTransform } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
  name: 'amountShortener'
 | 
			
		||||
})
 | 
			
		||||
export class AmountShortenerPipe implements PipeTransform {
 | 
			
		||||
  transform(num: number, ...args: number[]): unknown {
 | 
			
		||||
    const digits = args[0] || 1;
 | 
			
		||||
    const lookup = [
 | 
			
		||||
      { value: 1, symbol: '' },
 | 
			
		||||
      { value: 1e3, symbol: 'k' },
 | 
			
		||||
      { value: 1e6, symbol: 'M' },
 | 
			
		||||
      { value: 1e9, symbol: 'G' },
 | 
			
		||||
      { value: 1e12, symbol: 'T' },
 | 
			
		||||
      { value: 1e15, symbol: 'P' },
 | 
			
		||||
      { value: 1e18, symbol: 'E' }
 | 
			
		||||
    ];
 | 
			
		||||
    const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
 | 
			
		||||
    var item = lookup.slice().reverse().find((item) => num >= item.value);
 | 
			
		||||
    return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,43 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" id="Layer_1" x="0px" y="0px" viewBox="200 200 600 600" style="enable-background:new 0 0 1000 1000;background-color: #111316 !important" xml:space="preserve">
 | 
			
		||||
<style type="text/css">
 | 
			
		||||
	.st0{fill:#111316;}
 | 
			
		||||
	.st1{fill:#00C3FF;}
 | 
			
		||||
	.st2{fill:#7EE0FF;}
 | 
			
		||||
</style>
 | 
			
		||||
<path class="st1" d="M659.7,392.3c10.2,14.3,18.4,29.9,24.5,46.4l21.8-7.1c-6.9-18.9-16.4-36.8-28.1-53.1L659.7,392.3z"/>
 | 
			
		||||
<path class="st1" d="M510.6,289.2c-5.8-0.2-11.7-0.2-17.5,0l1.6,22.8c8.8-0.3,17.6-0.1,26.3,0.7c8.7,0.8,17.4,2.2,26,4.2l5.8-22.1  c-9.8-2.3-19.7-3.9-29.7-4.8C519,289.6,514.7,289.3,510.6,289.2z"/>
 | 
			
		||||
<path class="st1" d="M297.1,605.5c-9.1-18.6-15.7-38.3-19.5-58.6l-23.9,3.8c4.2,23,11.6,45.3,22,66.2L297.1,605.5z"/>
 | 
			
		||||
<path class="st1" d="M284.8,375.6l21.2,11.8c10.6-17.8,23.5-34,38.5-48.3l-16.2-18C311.3,337.2,296.7,355.5,284.8,375.6z"/>
 | 
			
		||||
<path class="st1" d="M254.8,453.5l23.8,4.2c4.2-20.3,11.2-39.9,20.7-58.3l-21.2-11.7C267.3,408.5,259.5,430.6,254.8,453.5z"/>
 | 
			
		||||
<path class="st1" d="M409.9,268.8l9.5,22.2c19.3-7.6,39.5-12.5,60.1-14.5l-1.7-24.1C454.5,254.6,431.7,260.1,409.9,268.8z"/>
 | 
			
		||||
<path class="st1" d="M338.5,311.8l16.2,18c15.8-13.4,33.3-24.6,52.1-33.4l-9.5-22.2C376,283.9,356.2,296.6,338.5,311.8z"/>
 | 
			
		||||
<path class="st1" d="M697.1,667.6l-18.9-15.1c-13.4,15.8-28.9,29.7-46,41.4l13,20.5C664.6,701.3,682.1,685.6,697.1,667.6z"/>
 | 
			
		||||
<path class="st1" d="M402.5,710.7c-18.6-9.1-35.9-20.7-51.4-34.5l-16.5,17.7c17.4,15.6,37,28.6,58,38.8L402.5,710.7z"/>
 | 
			
		||||
<path class="st1" d="M755.4,528.2c3.1-32.6-0.2-65.5-9.7-96.8l-23,7.6c13.2,44.4,12.7,91.7-1.3,135.8l22.8,8.1  C749.9,565.2,753.7,546.8,755.4,528.2z"/>
 | 
			
		||||
<path class="st1" d="M614.2,689.2L602,670c-15.1,9-31.3,16-48.3,20.7l5.4,22.2C578.5,707.5,597,699.6,614.2,689.2z"/>
 | 
			
		||||
<path class="st1" d="M314.5,528.8c-1.7-14.2-1.9-28.6-0.5-42.9c0.3-3.5,0.7-6.5,1.2-9.6l-22.5-4c-0.5,3.8-1,7.6-1.4,11.5  c-1.5,16.1-1.3,32.4,0.7,48.5L314.5,528.8z"/>
 | 
			
		||||
<path class="st1" d="M568.2,284.7c19.9,5.8,38.9,14.4,56.4,25.4l13.5-20.2c-19.8-12.5-41.2-22.1-63.7-28.7L568.2,284.7z"/>
 | 
			
		||||
<path class="st1" d="M469.8,755.8l2.3-24.1c-19.5-2.6-38.6-7.8-56.8-15.3l-10.1,22.2C425.8,747.1,447.6,752.9,469.8,755.8z"/>
 | 
			
		||||
<path class="st1" d="M351.3,657.7l15.7-16.6c-12.4-12.5-23.1-26.5-31.8-41.8l-20.3,10.7C324.8,627.4,337.1,643.5,351.3,657.7z"/>
 | 
			
		||||
<path class="st1" d="M649.5,297.7l-13.6,20.2c16.9,12,32,26.3,45.1,42.4l19.4-14.8C685.7,327.2,668.6,311.2,649.5,297.7z"/>
 | 
			
		||||
<path class="st1" d="M672.7,633.2c12-16.1,21.8-33.7,29.1-52.5l-21.5-7.7c-6.4,16.4-15,31.9-25.5,46L672.7,633.2z"/>
 | 
			
		||||
<path class="st2" d="M690.6,449.6l-21.6,7.2c6,20.7,8,42.4,6,63.8c-1.1,11.9-3.4,23.7-6.9,35.2l21.5,7.6c4.1-13.2,6.9-26.9,8.2-40.7  C700.1,498.1,697.6,473.3,690.6,449.6z"/>
 | 
			
		||||
<path class="st2" d="M475.2,698l2.1-22.7c-13.3-2-26.4-5.5-38.9-10.5l-9.4,20.7C443.8,691.5,459.3,695.7,475.2,698z"/>
 | 
			
		||||
<path class="st2" d="M631.8,456.2l20.4-6.9c-4.9-12.9-11.4-25.2-19.4-36.6l-17.1,13C622.3,435.2,627.7,445.4,631.8,456.2z"/>
 | 
			
		||||
<path class="st2" d="M508.4,345.7h-11.2l1.5,21.4c11.5-0.3,22.9,0.7,34.2,3.2l5.5-20.7c-6.8-1.5-13.6-2.6-20.5-3.2  C514.8,346.1,511.6,345.9,508.4,345.7z"/>
 | 
			
		||||
<path class="st2" d="M335.5,403.8l20,11.1c7.5-12.4,16.5-23.7,26.9-33.8L367,364.1C354.8,375.9,344.2,389.2,335.5,403.8z"/>
 | 
			
		||||
<path class="st2" d="M553.8,339.5c13.8,4.2,27.1,10.2,39.4,17.7l12.7-19c-14.4-8.9-30-15.8-46.2-20.7L553.8,339.5z"/>
 | 
			
		||||
<path class="st2" d="M635.9,394.5l18.1-13.8c-10.7-13.2-23.2-24.9-36.9-34.8l-12.7,19C616.2,373.4,626.7,383.3,635.9,394.5z"/>
 | 
			
		||||
<path class="st2" d="M611.5,584.6l16.8,13.4c8.2-11.2,14.9-23.3,20.1-36.2l-20.2-7.2C623.8,565.2,618.2,575.3,611.5,584.6z"/>
 | 
			
		||||
<path class="st2" d="M389.9,635.1l-15.6,16.6c12.8,11.2,26.9,20.7,42.2,28.2l9.4-20.7C412.9,652.8,400.8,644.6,389.9,635.1z"/>
 | 
			
		||||
<path class="st2" d="M369.2,520.2c-1-9.7-1.1-19.5-0.2-29.2c0.2-1.7,0.4-3.5,0.6-5.1l-21.1-3.8c-0.3,2.3-0.6,4.6-0.8,6.9  c-1.1,11.5-0.9,23,0.3,34.5L369.2,520.2z"/>
 | 
			
		||||
<path class="st2" d="M333.6,538l-22.6,3.5c3.2,16.7,8.6,33,16,48.3l20.2-10.7C340.9,566,336.4,552.2,333.6,538z"/>
 | 
			
		||||
<path class="st2" d="M601.7,646.3l12.3,19.2c14-9.6,26.7-21,37.7-33.8l-17.9-14.2C624.4,628.4,613.6,638.1,601.7,646.3z"/>
 | 
			
		||||
<path class="st2" d="M348.8,426.9l-19.9-11c-7.8,15.1-13.5,31.2-17,47.8l22.5,4C337.4,453.5,342.2,439.8,348.8,426.9z"/>
 | 
			
		||||
<path class="st2" d="M540.6,636.9l5,20.7c13.3-3.8,26.1-9.2,38.1-16.2l-11.6-18.1C562.2,629,551.6,633.6,540.6,636.9z"/>
 | 
			
		||||
<path class="st2" d="M384,573.5l-19,9.9c6.9,12,15.4,23,25.1,32.9l14.8-15.7C396.9,592.4,389.9,583.3,384,573.5z"/>
 | 
			
		||||
<path class="st2" d="M496.7,677.1c-1.9,0-3.8-0.2-5.7-0.4l-2.1,22.7c17.9,1.3,35.9,0.1,53.4-3.5l-5.3-22.2  C523.8,676.5,510.2,677.6,496.7,677.1z"/>
 | 
			
		||||
<path class="st2" d="M377.3,354.9l15.3,16.9c11.1-9.3,23.3-17.1,36.4-23.3l-9-21C404.6,334.7,390.3,343.9,377.3,354.9z"/>
 | 
			
		||||
<path class="st2" d="M432.7,322.1l9,21c13.5-5.2,27.6-8.7,42-10.3L482,310C465.1,311.9,448.5,315.9,432.7,322.1z"/>
 | 
			
		||||
<path class="st1" d="M490.3,757.5c21.5,0.7,43-1.1,64.2-5.2l-5-23.3c-18.3,3.8-37,5.3-55.8,4.6c-3,0-5.2-0.4-8.2-0.6l-2.1,24.4  c2.3,0.1,4.6,0.1,6.9,0L490.3,757.5z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 4.9 KiB  | 
@ -1,16 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<svg width="400px" height="400px" viewBox="0 0 400 400" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
			
		||||
    <title>Exodus_logo</title>
 | 
			
		||||
    <defs>
 | 
			
		||||
        <linearGradient x1="0%" y1="50%" x2="100%" y2="50%" id="linearGradient-1">
 | 
			
		||||
            <stop stop-color="#00BFFF" offset="0%"></stop>
 | 
			
		||||
            <stop stop-color="#6619FF" offset="100%"></stop>
 | 
			
		||||
        </linearGradient>
 | 
			
		||||
    </defs>
 | 
			
		||||
    <g id="Exodus_logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
 | 
			
		||||
        <g id="Exodus-logo">
 | 
			
		||||
            <rect id="Rectangle" fill="#1A1D40" x="0" y="0" width="400" height="400"></rect>
 | 
			
		||||
            <path d="M244.25,200 L310,265.75 L286.8,265.75 C282.823093,265.746499 279.010347,264.16385 276.2,261.35 L215,200 L276.25,138.6 C279.068515,135.804479 282.880256,134.240227 286.85,134.249954 L310,134.249954 L244.25,200 Z M123.75,138.6 C120.931485,135.804479 117.119744,134.240227 113.15,134.249954 L90,134.249954 L155.75,200 L90,265.75 L113.2,265.75 C117.176907,265.746499 120.989653,264.16385 123.8,261.35 L185,200 L123.75,138.6 Z M200,215 L138.6,276.25 C135.804479,279.068515 134.240227,282.880256 134.249954,286.85 L134.249954,310 L200,244.25 L265.750046,310 L265.750046,286.85 C265.759773,282.880256 264.195521,279.068515 261.4,276.25 L200,215 Z M200,185 L261.4,123.75 C264.195521,120.931485 265.759773,117.119744 265.750046,113.15 L265.750046,90 L200,155.75 L134.249954,90 L134.249954,113.15 C134.240227,117.119744 135.804479,120.931485 138.6,123.75 L200,185 Z" id="01-Exodus-wallet" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
 | 
			
		||||
        </g>
 | 
			
		||||
    </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1.6 KiB  | 
@ -1,9 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<svg width="400px" height="400px" viewBox="0 0 400 400" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
			
		||||
    <g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
 | 
			
		||||
        <g id="exodus">
 | 
			
		||||
            <rect id="Rectangle" fill="#87E1A1" fill-rule="nonzero" x="0" y="0" width="400" height="400"></rect>
 | 
			
		||||
            <path d="M124,149.256434 L169.106586,149.256434 L169.106586,128.378728 C169.106586,102.958946 183.316852,90 207.489341,90 L276.773787,90 L276.773787,119.404671 L222.192348,119.404671 C216.458028,119.404671 213.968815,122.397366 213.968815,127.633575 L213.968815,149.256434 L276.023264,149.256434 L276.023264,181.902184 L213.968815,181.902184 L213.968815,310 L169.106586,310 L169.106586,181.902184 L124,181.902184 L124,149.256434" id="Fill-1" fill="#000000"></path>
 | 
			
		||||
        </g>
 | 
			
		||||
    </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 916 B  | 
@ -1,6 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360">
 | 
			
		||||
<rect style="fill: white" width="360" height="360" />
 | 
			
		||||
<g transform="matrix(0.62 0 0 0.62 180 180)" id="fdfc8ede-1ea7-4fcd-b8cd-38f5fb196623"  >
 | 
			
		||||
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
 | 
			
		||||
</g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								frontend/src/resources/profile/nix-bitcoin.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 18 KiB  | 
@ -1,10 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-115 -15 879 679" style="background-color: rgb(27,20,100)">
 | 
			
		||||
<defs>
 | 
			
		||||
<style>.cls-1{fill:url(#linear-gradient);}</style>
 | 
			
		||||
<linearGradient id="linear-gradient" x1="81.36" y1="311.35" x2="541.35" y2="311.35" gradientUnits="userSpaceOnUse">
 | 
			
		||||
  <stop offset="0.18" stop-color="blue"/>
 | 
			
		||||
  <stop offset="1" stop-color="#f0f"/>
 | 
			
		||||
</linearGradient>
 | 
			
		||||
</defs>
 | 
			
		||||
<path class="cls-1" d="M326.4,572.09C201.2,572.09,141,503,112.48,445,84.26,387.47,81.89,330.44,81.69,322.31c-4.85-77,41-231.78,249.58-271.2a28.05,28.05,0,0,1,10.41,55.13c-213.12,40.28-204.44,206-204,213,0,.53.06,1.06.07,1.6C137.9,328.74,142.85,516,326.4,516,394.74,516,443,486.6,470,428.63c24.48-52.74,19.29-112.45-13.52-155.83-22.89-30.27-52.46-45-90.38-45-34.46,0-63.47,9.88-86.21,29.37A91.5,91.5,0,0,0,248,322.3c-1.41,25.4,7.14,49.36,24.07,67.49C287.27,406,305,413.9,326.4,413.9c27.46,0,45.52-9,53.66-26.81,8.38-18.3,3.61-38.93-.19-43.33-9.11-10-18.69-13.68-22.48-13-2.53.43-5.78,4.61-8.48,10.92a28,28,0,0,1-51.58-22c14.28-33.44,37.94-42,50.76-44.2,24.78-4.18,52.17,7.3,73.34,30.65s25.51,68.55,10.15,103.22C421.54,432,394.52,470,326.4,470c-36.72,0-69.67-14.49-95.29-41.92C203.64,398.68,189.77,360,192,319.19a149.1,149.1,0,0,1,51.31-104.6c33.19-28.45,74.48-42.87,122.71-42.87,55.12,0,101.85,23.25,135.12,67.23,45.36,60,52.9,141.71,19.66,213.3C495.45,506.92,441.12,572.09,326.4,572.09Z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1.4 KiB  | 
@ -1,24 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 | 
			
		||||
	 viewBox="0 0 216 216" style="enable-background:new 0 0 216 216;" xml:space="preserve">
 | 
			
		||||
<style type="text/css">
 | 
			
		||||
	.st0{fill:#002248;}
 | 
			
		||||
	.st1{opacity:0.5;fill:#FFFFFF;}
 | 
			
		||||
	.st2{fill:#FFFFFF;}
 | 
			
		||||
	.st3{opacity:0.75;fill:#FFFFFF;}
 | 
			
		||||
</style>
 | 
			
		||||
<rect class="st0" width="216" height="216"/>
 | 
			
		||||
<g>
 | 
			
		||||
	<g>
 | 
			
		||||
		<path class="st1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2
 | 
			
		||||
			c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7
 | 
			
		||||
			l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
 | 
			
		||||
		<path class="st2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4
 | 
			
		||||
			c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1
 | 
			
		||||
			c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
 | 
			
		||||
		<path class="st3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7
 | 
			
		||||
			c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7
 | 
			
		||||
			c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
 | 
			
		||||
	</g>
 | 
			
		||||
</g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1.4 KiB  | 
@ -21,6 +21,18 @@ 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' \
 | 
			
		||||
	'/api/v1/mining/hashrate/pools/3m' \
 | 
			
		||||
	'/api/v1/mining/hashrate/pools/6m' \
 | 
			
		||||
	'/api/v1/mining/hashrate/pools/1y' \
 | 
			
		||||
	'/api/v1/mining/hashrate/pools/2y' \
 | 
			
		||||
	'/api/v1/mining/hashrate/pools/3y' \
 | 
			
		||||
	'/api/v1/mining/hashrate/pools/all' \
 | 
			
		||||
 | 
			
		||||
	do
 | 
			
		||||
		curl -s "https://${hostname}${url}" >/dev/null
 | 
			
		||||
 | 
			
		||||