Merge branch 'master' into address-labels
This commit is contained in:
		
						commit
						3679f197ba
					
				@ -4,6 +4,7 @@ export namespace IBitcoinApi {
 | 
			
		||||
    size: number;                    //  (numeric) Current tx count
 | 
			
		||||
    bytes: number;                   //  (numeric) Sum of all virtual transaction sizes as defined in BIP 141.
 | 
			
		||||
    usage: number;                   //  (numeric) Total memory usage for the mempool
 | 
			
		||||
    total_fee: number;               //  (numeric) Total fees of transactions in the mempool
 | 
			
		||||
    maxmempool: number;              //  (numeric) Maximum memory usage for the mempool
 | 
			
		||||
    mempoolminfee: number;           //  (numeric) Minimum fee rate in BTC/kB for tx to be accepted.
 | 
			
		||||
    minrelaytxfee: number;           //  (numeric) Current minimum relay fee for transactions
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client';
 | 
			
		||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
 | 
			
		||||
import poolsRepository from '../repositories/PoolsRepository';
 | 
			
		||||
import blocksRepository from '../repositories/BlocksRepository';
 | 
			
		||||
import loadingIndicators from './loading-indicators';
 | 
			
		||||
 | 
			
		||||
class Blocks {
 | 
			
		||||
  private blocks: BlockExtended[] = [];
 | 
			
		||||
@ -41,7 +42,12 @@ class Blocks {
 | 
			
		||||
   * @param onlyCoinbase - Set to true if you only need the coinbase transaction
 | 
			
		||||
   * @returns Promise<TransactionExtended[]>
 | 
			
		||||
   */
 | 
			
		||||
  private async $getTransactionsExtended(blockHash: string, blockHeight: number, onlyCoinbase: boolean): Promise<TransactionExtended[]> {
 | 
			
		||||
  private async $getTransactionsExtended(
 | 
			
		||||
    blockHash: string,
 | 
			
		||||
    blockHeight: number,
 | 
			
		||||
    onlyCoinbase: boolean,
 | 
			
		||||
    quiet: boolean = false,
 | 
			
		||||
  ): Promise<TransactionExtended[]> {
 | 
			
		||||
    const transactions: TransactionExtended[] = [];
 | 
			
		||||
    const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
 | 
			
		||||
 | 
			
		||||
@ -55,9 +61,9 @@ class Blocks {
 | 
			
		||||
        // optimize here by directly fetching txs in the "outdated" mempool
 | 
			
		||||
        transactions.push(mempool[txIds[i]]);
 | 
			
		||||
        transactionsFound++;
 | 
			
		||||
      } else if (config.MEMPOOL.BACKEND === 'esplora' || memPool.isInSync() || i === 0) {
 | 
			
		||||
      } else if (config.MEMPOOL.BACKEND === 'esplora' || !memPool.hasPriority() || i === 0) {
 | 
			
		||||
        // Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...)
 | 
			
		||||
        if (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length) { // Avoid log spam
 | 
			
		||||
        if (!quiet && (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length)) { // Avoid log spam
 | 
			
		||||
          logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`);
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
@ -83,7 +89,9 @@ class Blocks {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!quiet) {
 | 
			
		||||
      logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return transactions;
 | 
			
		||||
  }
 | 
			
		||||
@ -94,13 +102,10 @@ class Blocks {
 | 
			
		||||
   * @param transactions
 | 
			
		||||
   * @returns BlockExtended
 | 
			
		||||
   */
 | 
			
		||||
  private getBlockExtended(block: IEsploraApi.Block, transactions: TransactionExtended[]): BlockExtended {
 | 
			
		||||
    const blockExtended: BlockExtended = Object.assign({}, block);
 | 
			
		||||
 | 
			
		||||
    blockExtended.extras = {
 | 
			
		||||
      reward: transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0),
 | 
			
		||||
      coinbaseTx: transactionUtils.stripCoinbaseTransaction(transactions[0]),
 | 
			
		||||
    };
 | 
			
		||||
   private async $getBlockExtended(block: IEsploraApi.Block, transactions: TransactionExtended[]): Promise<BlockExtended> {
 | 
			
		||||
    const blockExtended: BlockExtended = Object.assign({extras: {}}, block);
 | 
			
		||||
    blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
 | 
			
		||||
    blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
 | 
			
		||||
 | 
			
		||||
    const transactionsTmp = [...transactions];
 | 
			
		||||
    transactionsTmp.shift();
 | 
			
		||||
@ -111,6 +116,19 @@ class Blocks {
 | 
			
		||||
    blockExtended.extras.feeRange = transactionsTmp.length > 0 ?
 | 
			
		||||
      Common.getFeesInRange(transactionsTmp, 8) : [0, 0];
 | 
			
		||||
 | 
			
		||||
    if (Common.indexingEnabled()) {
 | 
			
		||||
      let pool: PoolTag;
 | 
			
		||||
      if (blockExtended.extras?.coinbaseTx !== undefined) {
 | 
			
		||||
        pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
 | 
			
		||||
      } else {
 | 
			
		||||
        pool = await poolsRepository.$getUnknownPool();
 | 
			
		||||
      }
 | 
			
		||||
      blockExtended.extras.pool = {
 | 
			
		||||
        id: pool.id,
 | 
			
		||||
        name: pool.name
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return blockExtended;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -152,20 +170,20 @@ class Blocks {
 | 
			
		||||
   * Index all blocks metadata for the mining dashboard
 | 
			
		||||
   */
 | 
			
		||||
  public async $generateBlockDatabase() {
 | 
			
		||||
    if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || // Bitcoin only
 | 
			
		||||
      config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === 0 || // Indexing must be enabled
 | 
			
		||||
      !memPool.isInSync() || // We sync the mempool first
 | 
			
		||||
      this.blockIndexingStarted === true // Indexing must not already be in progress
 | 
			
		||||
    if (this.blockIndexingStarted === true ||
 | 
			
		||||
      !Common.indexingEnabled() ||
 | 
			
		||||
      memPool.hasPriority()
 | 
			
		||||
    ) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const blockchainInfo = await bitcoinClient.getBlockchainInfo();
 | 
			
		||||
    if (blockchainInfo.blocks !== blockchainInfo.headers) {
 | 
			
		||||
    if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.blockIndexingStarted = true;
 | 
			
		||||
    const startedAt = new Date().getTime() / 1000;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      let currentBlockHeight = blockchainInfo.blocks;
 | 
			
		||||
@ -180,6 +198,8 @@ class Blocks {
 | 
			
		||||
      logger.info(`Indexing blocks from #${currentBlockHeight} to #${lastBlockToIndex}`);
 | 
			
		||||
 | 
			
		||||
      const chunkSize = 10000;
 | 
			
		||||
      let totaIndexed = await blocksRepository.$blockCount(null, null);
 | 
			
		||||
      let indexedThisRun = 0;
 | 
			
		||||
      while (currentBlockHeight >= lastBlockToIndex) {
 | 
			
		||||
        const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1);
 | 
			
		||||
 | 
			
		||||
@ -198,21 +218,19 @@ class Blocks {
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
          try {
 | 
			
		||||
            logger.debug(`Indexing block #${blockHeight}`);
 | 
			
		||||
            ++indexedThisRun;
 | 
			
		||||
            if (++totaIndexed % 100 === 0 || blockHeight === lastBlockToIndex) {
 | 
			
		||||
              const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
 | 
			
		||||
              const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
 | 
			
		||||
              const progress = Math.round(totaIndexed / indexingBlockAmount * 100);
 | 
			
		||||
              const timeLeft = Math.round((indexingBlockAmount - totaIndexed) / blockPerSeconds);
 | 
			
		||||
              logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds} blocks/sec | total: ${totaIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${elapsedSeconds} seconds | left: ~${timeLeft} seconds`);
 | 
			
		||||
            }
 | 
			
		||||
            const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
 | 
			
		||||
            const block = await bitcoinApi.$getBlock(blockHash);
 | 
			
		||||
            const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
 | 
			
		||||
            const blockExtended = this.getBlockExtended(block, transactions);
 | 
			
		||||
 | 
			
		||||
            let miner: PoolTag;
 | 
			
		||||
            if (blockExtended?.extras?.coinbaseTx) {
 | 
			
		||||
              miner = await this.$findBlockMiner(blockExtended.extras.coinbaseTx);
 | 
			
		||||
            } else {
 | 
			
		||||
              miner = await poolsRepository.$getUnknownPool();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const coinbase: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(transactions[0].txid, true);
 | 
			
		||||
            await blocksRepository.$saveBlockInDatabase(blockExtended, blockHash, coinbase.hex, miner);
 | 
			
		||||
            const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true);
 | 
			
		||||
            const blockExtended = await this.$getBlockExtended(block, transactions);
 | 
			
		||||
            await blocksRepository.$saveBlockInDatabase(blockExtended);
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            logger.err(`Something went wrong while indexing blocks.` + e);
 | 
			
		||||
          }
 | 
			
		||||
@ -271,17 +289,10 @@ class Blocks {
 | 
			
		||||
      const block = await bitcoinApi.$getBlock(blockHash);
 | 
			
		||||
      const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
 | 
			
		||||
      const transactions = await this.$getTransactionsExtended(blockHash, block.height, false);
 | 
			
		||||
      const blockExtended: BlockExtended = this.getBlockExtended(block, transactions);
 | 
			
		||||
      const coinbase: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(transactions[0].txid, true);
 | 
			
		||||
      const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions);
 | 
			
		||||
 | 
			
		||||
      if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === true) {
 | 
			
		||||
        let miner: PoolTag;
 | 
			
		||||
        if (blockExtended?.extras?.coinbaseTx) {
 | 
			
		||||
          miner = await this.$findBlockMiner(blockExtended.extras.coinbaseTx);
 | 
			
		||||
        } else {
 | 
			
		||||
          miner = await poolsRepository.$getUnknownPool();
 | 
			
		||||
        }
 | 
			
		||||
        await blocksRepository.$saveBlockInDatabase(blockExtended, blockHash, coinbase.hex, miner);
 | 
			
		||||
      if (Common.indexingEnabled()) {
 | 
			
		||||
        await blocksRepository.$saveBlockInDatabase(blockExtended);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (block.height % 2016 === 0) {
 | 
			
		||||
@ -298,12 +309,98 @@ class Blocks {
 | 
			
		||||
      if (this.newBlockCallbacks.length) {
 | 
			
		||||
        this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
 | 
			
		||||
      }
 | 
			
		||||
      if (memPool.isInSync()) {
 | 
			
		||||
      if (!memPool.hasPriority()) {
 | 
			
		||||
        diskCache.$saveCacheToDisk();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Index a block if it's missing from the database. Returns the block after indexing
 | 
			
		||||
   */
 | 
			
		||||
   public async $indexBlock(height: number): Promise<BlockExtended> {
 | 
			
		||||
    const dbBlock = await blocksRepository.$getBlockByHeight(height);
 | 
			
		||||
    if (dbBlock != null) {
 | 
			
		||||
      return this.prepareBlock(dbBlock);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const blockHash = await bitcoinApi.$getBlockHash(height);
 | 
			
		||||
    const block = await bitcoinApi.$getBlock(blockHash);
 | 
			
		||||
    const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
 | 
			
		||||
    const blockExtended = await this.$getBlockExtended(block, transactions);
 | 
			
		||||
 | 
			
		||||
    await blocksRepository.$saveBlockInDatabase(blockExtended);
 | 
			
		||||
 | 
			
		||||
    return blockExtended;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlocksExtras(fromHeight: number): Promise<BlockExtended[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      loadingIndicators.setProgress('blocks', 0);
 | 
			
		||||
 | 
			
		||||
      let currentHeight = fromHeight ? fromHeight : this.getCurrentBlockHeight();
 | 
			
		||||
      const returnBlocks: BlockExtended[] = [];
 | 
			
		||||
 | 
			
		||||
      if (currentHeight < 0) {
 | 
			
		||||
        return returnBlocks;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Check if block height exist in local cache to skip the hash lookup
 | 
			
		||||
      const blockByHeight = this.getBlocks().find((b) => b.height === currentHeight);
 | 
			
		||||
      let startFromHash: string | null = null;
 | 
			
		||||
      if (blockByHeight) {
 | 
			
		||||
        startFromHash = blockByHeight.id;
 | 
			
		||||
      } else {
 | 
			
		||||
        startFromHash = await bitcoinApi.$getBlockHash(currentHeight);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let nextHash = startFromHash;
 | 
			
		||||
      for (let i = 0; i < 10 && currentHeight >= 0; i++) {
 | 
			
		||||
        let block = this.getBlocks().find((b) => b.height === currentHeight);
 | 
			
		||||
        if (!block && Common.indexingEnabled()) {
 | 
			
		||||
          block = this.prepareBlock(await this.$indexBlock(currentHeight));
 | 
			
		||||
        } else if (!block) {
 | 
			
		||||
          block = this.prepareBlock(await bitcoinApi.$getBlock(nextHash));
 | 
			
		||||
        }
 | 
			
		||||
        returnBlocks.push(block);
 | 
			
		||||
        nextHash = block.previousblockhash;
 | 
			
		||||
        loadingIndicators.setProgress('blocks', i / 10 * 100);
 | 
			
		||||
        currentHeight--;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return returnBlocks;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      loadingIndicators.setProgress('blocks', 100);
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private prepareBlock(block: any): BlockExtended {
 | 
			
		||||
    return <BlockExtended>{
 | 
			
		||||
      id: block.id ?? block.hash, // hash for indexed block
 | 
			
		||||
      timestamp: block?.timestamp ?? block?.blockTimestamp, // blockTimestamp for indexed block
 | 
			
		||||
      height: block?.height,
 | 
			
		||||
      version: block?.version,
 | 
			
		||||
      bits: block?.bits,
 | 
			
		||||
      nonce: block?.nonce,
 | 
			
		||||
      difficulty: block?.difficulty,
 | 
			
		||||
      merkle_root: block?.merkle_root,
 | 
			
		||||
      tx_count: block?.tx_count,
 | 
			
		||||
      size: block?.size,
 | 
			
		||||
      weight: block?.weight,
 | 
			
		||||
      previousblockhash: block?.previousblockhash,
 | 
			
		||||
      extras: {
 | 
			
		||||
        medianFee: block?.medianFee,
 | 
			
		||||
        feeRange: block?.feeRange ?? [], // TODO
 | 
			
		||||
        reward: block?.reward,
 | 
			
		||||
        pool: block?.extras?.pool ?? (block?.pool_id ? {
 | 
			
		||||
          id: block?.pool_id,
 | 
			
		||||
          name: block?.pool_name,
 | 
			
		||||
        } : undefined),
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getLastDifficultyAdjustmentTime(): number {
 | 
			
		||||
    return this.lastDifficultyAdjustmentTime;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -154,4 +154,27 @@ export class Common {
 | 
			
		||||
    });
 | 
			
		||||
    return parents;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getSqlInterval(interval: string | null): string | null {
 | 
			
		||||
    switch (interval) {
 | 
			
		||||
      case '24h': return '1 DAY';
 | 
			
		||||
      case '3d': return '3 DAY';
 | 
			
		||||
      case '1w': return '1 WEEK';
 | 
			
		||||
      case '1m': return '1 MONTH';
 | 
			
		||||
      case '3m': return '3 MONTH';
 | 
			
		||||
      case '6m': return '6 MONTH';
 | 
			
		||||
      case '1y': return '1 YEAR';
 | 
			
		||||
      case '2y': return '2 YEAR';
 | 
			
		||||
      case '3y': return '3 YEAR';
 | 
			
		||||
      default: return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  static indexingEnabled(): boolean {
 | 
			
		||||
    return (
 | 
			
		||||
      ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) &&
 | 
			
		||||
      config.DATABASE.ENABLED === true &&
 | 
			
		||||
      config.MEMPOOL.INDEXING_BLOCKS_AMOUNT != 0
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import logger from '../logger';
 | 
			
		||||
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 4;
 | 
			
		||||
  private static currentVersion = 6;
 | 
			
		||||
  private queryTimeout = 120000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
 | 
			
		||||
@ -76,6 +76,7 @@ class DatabaseMigration {
 | 
			
		||||
  private async $createMissingTablesAndIndexes(databaseSchemaVersion: number) {
 | 
			
		||||
    await this.$setStatisticsAddedIndexedFlag(databaseSchemaVersion);
 | 
			
		||||
 | 
			
		||||
    const isBitcoin = ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    try {
 | 
			
		||||
      await this.$executeQuery(connection, this.getCreateElementsTableQuery(), await this.$checkIfTableExists('elements_pegs'));
 | 
			
		||||
@ -90,6 +91,31 @@ class DatabaseMigration {
 | 
			
		||||
        await this.$executeQuery(connection, 'DROP table IF EXISTS blocks;');
 | 
			
		||||
        await this.$executeQuery(connection, this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
 | 
			
		||||
      }
 | 
			
		||||
      if (databaseSchemaVersion < 5 && isBitcoin === true) {
 | 
			
		||||
        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) {
 | 
			
		||||
        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"');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"');
 | 
			
		||||
        // We also fix the pools.id type so we need to drop/re-create the foreign key
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)');
 | 
			
		||||
        // Add new block indexing fields
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
 | 
			
		||||
        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');
 | 
			
		||||
      }
 | 
			
		||||
      connection.release();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
@ -13,8 +13,9 @@ class Mempool {
 | 
			
		||||
  private static WEBSOCKET_REFRESH_RATE_MS = 10000;
 | 
			
		||||
  private static LAZY_DELETE_AFTER_SECONDS = 30;
 | 
			
		||||
  private inSync: boolean = false;
 | 
			
		||||
  private mempoolCacheDelta: number = -1;
 | 
			
		||||
  private mempoolCache: { [txId: string]: TransactionExtended } = {};
 | 
			
		||||
  private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0,
 | 
			
		||||
  private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
 | 
			
		||||
                                                    maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
 | 
			
		||||
  private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
 | 
			
		||||
    deletedTransactions: TransactionExtended[]) => void) | undefined;
 | 
			
		||||
@ -32,6 +33,17 @@ class Mempool {
 | 
			
		||||
    setInterval(this.deleteExpiredTransactions.bind(this), 20000);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Return true if we should leave resources available for mempool tx caching
 | 
			
		||||
   */
 | 
			
		||||
  public hasPriority(): boolean {
 | 
			
		||||
    if (this.inSync) {
 | 
			
		||||
      return false;
 | 
			
		||||
    } else {
 | 
			
		||||
      return this.mempoolCacheDelta == -1 || this.mempoolCacheDelta > 25;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public isInSync(): boolean {
 | 
			
		||||
    return this.inSync;
 | 
			
		||||
  }
 | 
			
		||||
@ -100,6 +112,8 @@ class Mempool {
 | 
			
		||||
    const diff = transactions.length - currentMempoolSize;
 | 
			
		||||
    const newTransactions: TransactionExtended[] = [];
 | 
			
		||||
 | 
			
		||||
    this.mempoolCacheDelta = Math.abs(diff);
 | 
			
		||||
 | 
			
		||||
    if (!this.inSync) {
 | 
			
		||||
      loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100);
 | 
			
		||||
    }
 | 
			
		||||
@ -168,13 +182,14 @@ class Mempool {
 | 
			
		||||
    const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
 | 
			
		||||
    this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
 | 
			
		||||
 | 
			
		||||
    const syncedThreshold = 0.99; // If we synced 99% of the mempool tx count, consider we're synced
 | 
			
		||||
    if (!this.inSync && Object.keys(this.mempoolCache).length >= transactions.length * syncedThreshold) {
 | 
			
		||||
    if (!this.inSync && transactions.length === Object.keys(this.mempoolCache).length) {
 | 
			
		||||
      this.inSync = true;
 | 
			
		||||
      logger.notice('The mempool is now in sync!');
 | 
			
		||||
      loadingIndicators.setProgress('mempool', 100);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.mempoolCacheDelta = Math.abs(transactions.length - Object.keys(this.mempoolCache).length);
 | 
			
		||||
 | 
			
		||||
    if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
 | 
			
		||||
      this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -11,24 +11,10 @@ class Mining {
 | 
			
		||||
   * Generate high level overview of the pool ranks and general stats
 | 
			
		||||
   */
 | 
			
		||||
  public async $getPoolsStats(interval: string | null) : Promise<object> {
 | 
			
		||||
    let sqlInterval: string | null = null;
 | 
			
		||||
    switch (interval) {
 | 
			
		||||
      case '24h': sqlInterval = '1 DAY'; break;
 | 
			
		||||
      case '3d': sqlInterval = '3 DAY'; break;
 | 
			
		||||
      case '1w': sqlInterval = '1 WEEK'; break;
 | 
			
		||||
      case '1m': sqlInterval = '1 MONTH'; break;
 | 
			
		||||
      case '3m': sqlInterval = '3 MONTH'; break;
 | 
			
		||||
      case '6m': sqlInterval = '6 MONTH'; break;
 | 
			
		||||
      case '1y': sqlInterval = '1 YEAR'; break;
 | 
			
		||||
      case '2y': sqlInterval = '2 YEAR'; break;
 | 
			
		||||
      case '3y': sqlInterval = '3 YEAR'; break;
 | 
			
		||||
      default: sqlInterval = null; break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const poolsStatistics = {};
 | 
			
		||||
 | 
			
		||||
    const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(sqlInterval);
 | 
			
		||||
    const emptyBlocks: EmptyBlocks[] = await BlocksRepository.$countEmptyBlocks(sqlInterval);
 | 
			
		||||
    const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval);
 | 
			
		||||
    const emptyBlocks: EmptyBlocks[] = await BlocksRepository.$getEmptyBlocks(null, interval);
 | 
			
		||||
 | 
			
		||||
    const poolsStats: PoolStats[] = [];
 | 
			
		||||
    let rank = 1;
 | 
			
		||||
@ -55,7 +41,7 @@ class Mining {
 | 
			
		||||
    const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
 | 
			
		||||
    poolsStatistics['oldestIndexedBlockTimestamp'] = oldestBlock.getTime();
 | 
			
		||||
 | 
			
		||||
    const blockCount: number = await BlocksRepository.$blockCount(sqlInterval);
 | 
			
		||||
    const blockCount: number = await BlocksRepository.$blockCount(null, interval);
 | 
			
		||||
    poolsStatistics['blockCount'] = blockCount;
 | 
			
		||||
 | 
			
		||||
    const blockHeightTip = await bitcoinClient.getBlockCount();
 | 
			
		||||
@ -64,6 +50,38 @@ class Mining {
 | 
			
		||||
 | 
			
		||||
    return poolsStatistics;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get all mining pool stats for a pool
 | 
			
		||||
   */
 | 
			
		||||
  public async $getPoolStat(interval: string | null, poolId: number): Promise<object> {
 | 
			
		||||
    const pool = await PoolsRepository.$getPool(poolId);
 | 
			
		||||
    if (!pool) {
 | 
			
		||||
      throw new Error(`This mining pool does not exist`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const blockCount: number = await BlocksRepository.$blockCount(poolId, interval);
 | 
			
		||||
    const emptyBlocks: EmptyBlocks[] = await BlocksRepository.$getEmptyBlocks(poolId, interval);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      pool: pool,
 | 
			
		||||
      blockCount: blockCount,
 | 
			
		||||
      emptyBlocks: emptyBlocks,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 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 {
 | 
			
		||||
      adjustments: difficultyAdjustments,
 | 
			
		||||
      oldestIndexedBlockTimestamp: oldestBlock.getTime(),
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new Mining();
 | 
			
		||||
 | 
			
		||||
@ -256,6 +256,11 @@ class Server {
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', routes.$getStatisticsByTime.bind(routes, '1y'))
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', routes.$getStatisticsByTime.bind(routes, '2y'))
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', routes.$getStatisticsByTime.bind(routes, '3y'))
 | 
			
		||||
        ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (Common.indexingEnabled()) {
 | 
			
		||||
      this.app
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/24h', routes.$getPools.bind(routes, '24h'))
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3d', routes.$getPools.bind(routes, '3d'))
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/1w', routes.$getPools.bind(routes, '1w'))
 | 
			
		||||
@ -266,7 +271,12 @@ class Server {
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/2y', routes.$getPools.bind(routes, '2y'))
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3y', routes.$getPools.bind(routes, '3y'))
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/all', routes.$getPools.bind(routes, 'all'))
 | 
			
		||||
        ;
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks', routes.$getPoolBlocks)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks/:height', routes.$getPoolBlocks)
 | 
			
		||||
        .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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.BISQ.ENABLED) {
 | 
			
		||||
@ -290,6 +300,10 @@ class Server {
 | 
			
		||||
        ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.app
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras', routes.getBlocksExtras)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras/:height', routes.getBlocksExtras);
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
      this.app
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mempool', routes.getMempool)
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,23 @@
 | 
			
		||||
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
 | 
			
		||||
 | 
			
		||||
export interface PoolTag {
 | 
			
		||||
  id: number | null, // mysql row id
 | 
			
		||||
  name: string,
 | 
			
		||||
  link: string,
 | 
			
		||||
  regexes: string, // JSON array
 | 
			
		||||
  addresses: string, // JSON array
 | 
			
		||||
  id: number; // mysql row id
 | 
			
		||||
  name: string;
 | 
			
		||||
  link: string;
 | 
			
		||||
  regexes: string; // JSON array
 | 
			
		||||
  addresses: string; // JSON array
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PoolInfo {
 | 
			
		||||
  poolId: number, // mysql row id
 | 
			
		||||
  name: string,
 | 
			
		||||
  link: string,
 | 
			
		||||
  blockCount: number,
 | 
			
		||||
  poolId: number; // mysql row id
 | 
			
		||||
  name: string;
 | 
			
		||||
  link: string;
 | 
			
		||||
  blockCount: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PoolStats extends PoolInfo {
 | 
			
		||||
  rank: number,
 | 
			
		||||
  emptyBlocks: number,
 | 
			
		||||
  rank: number;
 | 
			
		||||
  emptyBlocks: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlock {
 | 
			
		||||
@ -83,10 +83,14 @@ export interface BlockExtension {
 | 
			
		||||
  reward?: number;
 | 
			
		||||
  coinbaseTx?: TransactionMinerInfo;
 | 
			
		||||
  matchRate?: number;
 | 
			
		||||
  pool?: {
 | 
			
		||||
    id: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface BlockExtended extends IEsploraApi.Block {
 | 
			
		||||
  extras?: BlockExtension;
 | 
			
		||||
  extras: BlockExtension;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionMinerInfo {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { BlockExtended, PoolTag } from '../mempool.interfaces';
 | 
			
		||||
import { DB } from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { Common } from '../api/common';
 | 
			
		||||
 | 
			
		||||
export interface EmptyBlocks {
 | 
			
		||||
  emptyBlocks: number;
 | 
			
		||||
@ -11,40 +12,46 @@ class BlocksRepository {
 | 
			
		||||
  /**
 | 
			
		||||
   * Save indexed block data in the database
 | 
			
		||||
   */
 | 
			
		||||
  public async $saveBlockInDatabase(
 | 
			
		||||
    block: BlockExtended,
 | 
			
		||||
    blockHash: string,
 | 
			
		||||
    coinbaseHex: string | undefined,
 | 
			
		||||
    poolTag: PoolTag
 | 
			
		||||
  ) {
 | 
			
		||||
  public async $saveBlockInDatabase(block: BlockExtended) {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `INSERT INTO blocks(
 | 
			
		||||
        height,  hash,     blockTimestamp, size,
 | 
			
		||||
        weight,  tx_count, coinbase_raw,   difficulty,
 | 
			
		||||
        pool_id, fees,     fee_span,       median_fee
 | 
			
		||||
        pool_id, fees,     fee_span,       median_fee,
 | 
			
		||||
        reward,  version,  bits,           nonce,
 | 
			
		||||
        merkle_root,       previous_block_hash
 | 
			
		||||
      ) VALUE (
 | 
			
		||||
        ?, ?, FROM_UNIXTIME(?), ?,
 | 
			
		||||
        ?, ?, ?, ?,
 | 
			
		||||
        ?, ?, ?, ?
 | 
			
		||||
        ?, ?, ?, ?,
 | 
			
		||||
        ?, ?, ?, ?,
 | 
			
		||||
        ?,    ?
 | 
			
		||||
      )`;
 | 
			
		||||
 | 
			
		||||
      const params: any[] = [
 | 
			
		||||
        block.height,
 | 
			
		||||
        blockHash,
 | 
			
		||||
        block.id,
 | 
			
		||||
        block.timestamp,
 | 
			
		||||
        block.size,
 | 
			
		||||
        block.weight,
 | 
			
		||||
        block.tx_count,
 | 
			
		||||
        coinbaseHex ? coinbaseHex : '',
 | 
			
		||||
        '',
 | 
			
		||||
        block.difficulty,
 | 
			
		||||
        poolTag.id,
 | 
			
		||||
        block.extras.pool?.id, // Should always be set to something
 | 
			
		||||
        0,
 | 
			
		||||
        '[]',
 | 
			
		||||
        block.extras ? block.extras.medianFee : 0,
 | 
			
		||||
        block.extras.medianFee ?? 0,
 | 
			
		||||
        block.extras.reward ?? 0,
 | 
			
		||||
        block.version,
 | 
			
		||||
        block.bits,
 | 
			
		||||
        block.nonce,
 | 
			
		||||
        block.merkle_root,
 | 
			
		||||
        block.previousblockhash
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      // logger.debug(query);
 | 
			
		||||
      await connection.query(query, params);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      if (e.errno === 1062) { // ER_DUP_ENTRY
 | 
			
		||||
@ -66,12 +73,12 @@ class BlocksRepository {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] : any[] = await connection.query(`
 | 
			
		||||
    const [rows]: any[] = await connection.query(`
 | 
			
		||||
      SELECT height
 | 
			
		||||
      FROM blocks
 | 
			
		||||
      WHERE height <= ${startHeight} AND height >= ${endHeight}
 | 
			
		||||
      WHERE height <= ? AND height >= ?
 | 
			
		||||
      ORDER BY height DESC;
 | 
			
		||||
    `);
 | 
			
		||||
    `, [startHeight, endHeight]);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    const indexedBlockHeights: number[] = [];
 | 
			
		||||
@ -83,18 +90,28 @@ class BlocksRepository {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Count empty blocks for all pools
 | 
			
		||||
   * Get empty blocks for one or all pools
 | 
			
		||||
   */
 | 
			
		||||
  public async $countEmptyBlocks(interval: string | null): Promise<EmptyBlocks[]> {
 | 
			
		||||
    const query = `
 | 
			
		||||
      SELECT pool_id as poolId
 | 
			
		||||
      FROM blocks
 | 
			
		||||
      WHERE tx_count = 1` +
 | 
			
		||||
      (interval != null ? ` AND blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()` : ``)
 | 
			
		||||
    ;
 | 
			
		||||
  public async $getEmptyBlocks(poolId: number | null, interval: string | null = null): Promise<EmptyBlocks[]> {
 | 
			
		||||
    interval = Common.getSqlInterval(interval);
 | 
			
		||||
 | 
			
		||||
    const params: any[] = [];
 | 
			
		||||
    let query = `SELECT height, hash, tx_count, size, pool_id, weight, UNIX_TIMESTAMP(blockTimestamp) as timestamp
 | 
			
		||||
      FROM blocks
 | 
			
		||||
      WHERE tx_count = 1`;
 | 
			
		||||
 | 
			
		||||
    if (poolId) {
 | 
			
		||||
      query += ` AND pool_id = ?`;
 | 
			
		||||
      params.push(poolId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (interval) {
 | 
			
		||||
      query += ` AND blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query);
 | 
			
		||||
    const [rows] = await connection.query(query, params);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    return <EmptyBlocks[]>rows;
 | 
			
		||||
@ -103,15 +120,30 @@ class BlocksRepository {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get blocks count for a period
 | 
			
		||||
   */
 | 
			
		||||
   public async $blockCount(interval: string | null): Promise<number> {
 | 
			
		||||
    const query = `
 | 
			
		||||
      SELECT count(height) as blockCount
 | 
			
		||||
      FROM blocks` +
 | 
			
		||||
      (interval != null ? ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()` : ``)
 | 
			
		||||
    ;
 | 
			
		||||
  public async $blockCount(poolId: number | null, interval: string | null): Promise<number> {
 | 
			
		||||
    interval = Common.getSqlInterval(interval);
 | 
			
		||||
 | 
			
		||||
    const params: any[] = [];
 | 
			
		||||
    let query = `SELECT count(height) as blockCount
 | 
			
		||||
      FROM blocks`;
 | 
			
		||||
 | 
			
		||||
    if (poolId) {
 | 
			
		||||
      query += ` WHERE pool_id = ?`;
 | 
			
		||||
      params.push(poolId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (interval) {
 | 
			
		||||
      if (poolId) {
 | 
			
		||||
        query += ` AND`;
 | 
			
		||||
      } else {
 | 
			
		||||
        query += ` WHERE`;
 | 
			
		||||
      }
 | 
			
		||||
      query += ` blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query);
 | 
			
		||||
    const [rows] = await connection.query(query, params);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    return <number>rows[0].blockCount;
 | 
			
		||||
@ -121,13 +153,15 @@ class BlocksRepository {
 | 
			
		||||
   * Get the oldest indexed block
 | 
			
		||||
   */
 | 
			
		||||
  public async $oldestBlockTimestamp(): Promise<number> {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows]: any[] = await connection.query(`
 | 
			
		||||
      SELECT blockTimestamp
 | 
			
		||||
    const query = `SELECT blockTimestamp
 | 
			
		||||
      FROM blocks
 | 
			
		||||
      ORDER BY height
 | 
			
		||||
      LIMIT 1;
 | 
			
		||||
    `);
 | 
			
		||||
      LIMIT 1;`;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows]: any[] = await connection.query(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    if (rows.length <= 0) {
 | 
			
		||||
@ -136,6 +170,83 @@ class BlocksRepository {
 | 
			
		||||
 | 
			
		||||
    return <number>rows[0].blockTimestamp;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get blocks mined by a specific mining pool
 | 
			
		||||
   */
 | 
			
		||||
  public async $getBlocksByPool(
 | 
			
		||||
    poolId: number,
 | 
			
		||||
    startHeight: number | null = null
 | 
			
		||||
  ): Promise<object[]> {
 | 
			
		||||
    const params: any[] = [];
 | 
			
		||||
    let query = `SELECT height, hash as id, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, reward
 | 
			
		||||
      FROM blocks
 | 
			
		||||
      WHERE pool_id = ?`;
 | 
			
		||||
    params.push(poolId);
 | 
			
		||||
 | 
			
		||||
    if (startHeight) {
 | 
			
		||||
      query += ` AND height < ?`;
 | 
			
		||||
      params.push(startHeight);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    query += ` ORDER BY height DESC
 | 
			
		||||
      LIMIT 10`;
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query, params);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    for (const block of <object[]>rows) {
 | 
			
		||||
      delete block['blockTimestamp'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return <object[]>rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get one block by height
 | 
			
		||||
   */
 | 
			
		||||
   public async $getBlockByHeight(height: number): Promise<object | null> {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows]: any[] = await connection.query(`
 | 
			
		||||
      SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.addresses as pool_addresses, pools.regexes as pool_regexes
 | 
			
		||||
      FROM blocks
 | 
			
		||||
      JOIN pools ON blocks.pool_id = pools.id
 | 
			
		||||
      WHERE height = ${height};
 | 
			
		||||
    `);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    if (rows.length <= 0) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return rows[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Return blocks difficulty
 | 
			
		||||
   */
 | 
			
		||||
   public async $getBlocksDifficulty(interval: string | null): Promise<object[]> {
 | 
			
		||||
    interval = Common.getSqlInterval(interval);
 | 
			
		||||
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
 | 
			
		||||
    let query = `SELECT MIN(UNIX_TIMESTAMP(blockTimestamp)) as timestamp, difficulty, height
 | 
			
		||||
      FROM blocks`;
 | 
			
		||||
 | 
			
		||||
    if (interval) {
 | 
			
		||||
      query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    query += ` GROUP BY difficulty
 | 
			
		||||
      ORDER BY blockTimestamp DESC`;
 | 
			
		||||
 | 
			
		||||
    const [rows]: any[] = await connection.query(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new BlocksRepository();
 | 
			
		||||
@ -1,4 +1,6 @@
 | 
			
		||||
import { Common } from '../api/common';
 | 
			
		||||
import { DB } from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { PoolInfo, PoolTag } from '../mempool.interfaces';
 | 
			
		||||
 | 
			
		||||
class PoolsRepository {
 | 
			
		||||
@ -7,7 +9,7 @@ class PoolsRepository {
 | 
			
		||||
   */
 | 
			
		||||
  public async $getPools(): Promise<PoolTag[]> {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query('SELECT * FROM pools;');
 | 
			
		||||
    const [rows] = await connection.query('SELECT id, name, addresses, regexes FROM pools;');
 | 
			
		||||
    connection.release();
 | 
			
		||||
    return <PoolTag[]>rows;
 | 
			
		||||
  }
 | 
			
		||||
@ -17,7 +19,7 @@ class PoolsRepository {
 | 
			
		||||
   */
 | 
			
		||||
  public async $getUnknownPool(): Promise<PoolTag> {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query('SELECT * FROM pools where name = "Unknown"');
 | 
			
		||||
    const [rows] = await connection.query('SELECT id, name FROM pools where name = "Unknown"');
 | 
			
		||||
    connection.release();
 | 
			
		||||
    return <PoolTag>rows[0];
 | 
			
		||||
  }
 | 
			
		||||
@ -25,22 +27,47 @@ class PoolsRepository {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get basic pool info and block count
 | 
			
		||||
   */
 | 
			
		||||
  public async $getPoolsInfo(interval: string | null): Promise<PoolInfo[]> {
 | 
			
		||||
    const query = `
 | 
			
		||||
      SELECT COUNT(height) as blockCount, pool_id as poolId, pools.name as name, pools.link as link
 | 
			
		||||
      FROM blocks
 | 
			
		||||
      JOIN pools on pools.id = pool_id` +
 | 
			
		||||
      (interval != null ? ` WHERE blocks.blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()` : ``) +
 | 
			
		||||
      ` GROUP BY pool_id
 | 
			
		||||
      ORDER BY COUNT(height) DESC
 | 
			
		||||
    `;
 | 
			
		||||
  public async $getPoolsInfo(interval: string | null = null): Promise<PoolInfo[]> {
 | 
			
		||||
    interval = Common.getSqlInterval(interval);
 | 
			
		||||
 | 
			
		||||
    let query = `SELECT COUNT(height) as blockCount, pool_id as poolId, pools.name as name, pools.link as link
 | 
			
		||||
      FROM blocks
 | 
			
		||||
      JOIN pools on pools.id = pool_id`;
 | 
			
		||||
 | 
			
		||||
    if (interval) {
 | 
			
		||||
      query += ` WHERE blocks.blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    query += ` GROUP BY pool_id
 | 
			
		||||
      ORDER BY COUNT(height) DESC`;
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    return <PoolInfo[]>rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get mining pool statistics for one pool
 | 
			
		||||
   */
 | 
			
		||||
   public async $getPool(poolId: any): Promise<object> {
 | 
			
		||||
    const query = `
 | 
			
		||||
      SELECT *
 | 
			
		||||
      FROM pools
 | 
			
		||||
      WHERE pools.id = ?`;
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query, [poolId]);
 | 
			
		||||
    connection.release();
 | 
			
		||||
 | 
			
		||||
    rows[0].regexes = JSON.parse(rows[0].regexes);
 | 
			
		||||
    rows[0].addresses = JSON.parse(rows[0].addresses);
 | 
			
		||||
 | 
			
		||||
    return rows[0];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new PoolsRepository();
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,9 @@ 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';
 | 
			
		||||
 | 
			
		||||
class Routes {
 | 
			
		||||
  constructor() {}
 | 
			
		||||
@ -533,9 +536,9 @@ class Routes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getPools(interval: string, req: Request, res: Response) {
 | 
			
		||||
  public async $getPool(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      let stats = await miningStats.$getPoolsStats(interval);
 | 
			
		||||
      const stats = await mining.$getPoolStat(req.params.interval ?? null, parseInt(req.params.poolId, 10));
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
@ -545,6 +548,45 @@ class Routes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getPoolBlocks(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const poolBlocks = await BlocksRepository.$getBlocksByPool(
 | 
			
		||||
        parseInt(req.params.poolId, 10),
 | 
			
		||||
        parseInt(req.params.height, 10) ?? null,
 | 
			
		||||
      );
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(poolBlocks);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getPools(interval: string, req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const stats = await miningStats.$getPoolsStats(interval);
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(stats);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getHistoricalDifficulty(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const stats = await mining.$getHistoricalDifficulty(req.params.interval ?? null);
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
			
		||||
      res.json(stats);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getBlock(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.$getBlock(req.params.hash);
 | 
			
		||||
@ -564,6 +606,14 @@ class Routes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getBlocksExtras(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      res.json(await blocks.$getBlocksExtras(parseInt(req.params.height, 10)))
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  public async getBlocks(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      loadingIndicators.setProgress('blocks', 0);
 | 
			
		||||
@ -691,7 +741,13 @@ class Routes {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getMempool(req: Request, res: Response) {
 | 
			
		||||
    res.status(501).send('Not implemented');
 | 
			
		||||
    const info = mempool.getMempoolInfo();
 | 
			
		||||
    res.json({
 | 
			
		||||
      count: info.size,
 | 
			
		||||
      vsize: info.bytes,
 | 
			
		||||
      total_fee: info.total_fee * 1e8,
 | 
			
		||||
      fee_histogram: []
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getMempoolTxIds(req: Request, res: Response) {
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,8 @@
 | 
			
		||||
    "PRICE_FEED_UPDATE_INTERVAL": __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__,
 | 
			
		||||
    "USE_SECOND_NODE_FOR_MINFEE": __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__,
 | 
			
		||||
    "EXTERNAL_ASSETS": __MEMPOOL_EXTERNAL_ASSETS__,
 | 
			
		||||
    "STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__"
 | 
			
		||||
    "STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
 | 
			
		||||
    "INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "HOST": "__CORE_RPC_HOST__",
 | 
			
		||||
 | 
			
		||||
@ -274,102 +274,6 @@ describe('Mainnet', () => {
 | 
			
		||||
            });
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    it('loads skeleton when changes between networks', () => {
 | 
			
		||||
      cy.visit('/');
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
 | 
			
		||||
      cy.changeNetwork("testnet");
 | 
			
		||||
      cy.changeNetwork("signet");
 | 
			
		||||
      cy.changeNetwork("mainnet");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it.skip('loads the dashboard with the skeleton blocks', () => {
 | 
			
		||||
      cy.mockMempoolSocket();
 | 
			
		||||
      cy.visit("/");
 | 
			
		||||
      cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
      cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
      cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
      cy.get('#mempool-block-0').should('be.visible');
 | 
			
		||||
      cy.get('#mempool-block-1').should('be.visible');
 | 
			
		||||
      cy.get('#mempool-block-2').should('be.visible');
 | 
			
		||||
 | 
			
		||||
      emitMempoolInfo({
 | 
			
		||||
        'params': {
 | 
			
		||||
          command: 'init'
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
 | 
			
		||||
      cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
 | 
			
		||||
      cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the pools screen', () => {
 | 
			
		||||
      cy.visit('/');
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
      cy.get('#btn-pools').click().then(() => {
 | 
			
		||||
        cy.waitForPageIdle();
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the graphs screen', () => {
 | 
			
		||||
      cy.visit('/');
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
      cy.get('#btn-graphs').click().then(() => {
 | 
			
		||||
        cy.wait(1000);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('graphs page', () => {
 | 
			
		||||
      it('check buttons - mobile', () => {
 | 
			
		||||
        cy.viewport('iphone-6');
 | 
			
		||||
        cy.visit('/graphs');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('.small-buttons > :nth-child(2)').should('be.visible');
 | 
			
		||||
        cy.get('#dropdownFees').should('be.visible');
 | 
			
		||||
        cy.get('.btn-group').should('be.visible');
 | 
			
		||||
      });
 | 
			
		||||
      it('check buttons - tablet', () => {
 | 
			
		||||
        cy.viewport('ipad-2');
 | 
			
		||||
        cy.visit('/graphs');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('.small-buttons > :nth-child(2)').should('be.visible');
 | 
			
		||||
        cy.get('#dropdownFees').should('be.visible');
 | 
			
		||||
        cy.get('.btn-group').should('be.visible');
 | 
			
		||||
      });
 | 
			
		||||
      it('check buttons - desktop', () => {
 | 
			
		||||
        cy.viewport('macbook-16');
 | 
			
		||||
        cy.visit('/graphs');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('.small-buttons > :nth-child(2)').should('be.visible');
 | 
			
		||||
        cy.get('#dropdownFees').should('be.visible');
 | 
			
		||||
        cy.get('.btn-group').should('be.visible');
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the tv screen - desktop', () => {
 | 
			
		||||
      cy.viewport('macbook-16');
 | 
			
		||||
      cy.visit('/');
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
      cy.get('#btn-tv').click().then(() => {
 | 
			
		||||
        cy.viewport('macbook-16');
 | 
			
		||||
        cy.get('.chart-holder');
 | 
			
		||||
        cy.get('.blockchain-wrapper').should('be.visible');
 | 
			
		||||
        cy.get('#mempool-block-0').should('be.visible');
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the tv screen - mobile', () => {
 | 
			
		||||
      cy.viewport('iphone-6');
 | 
			
		||||
      cy.visit('/tv');
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
      cy.get('.chart-holder');
 | 
			
		||||
      cy.get('.blockchain-wrapper').should('not.visible');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
        it('loads genesis block and click on the arrow left', () => {
 | 
			
		||||
          cy.viewport('macbook-16');
 | 
			
		||||
@ -383,6 +287,8 @@ describe('Mainnet', () => {
 | 
			
		||||
            cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads skeleton when changes between networks', () => {
 | 
			
		||||
      cy.visit('/');
 | 
			
		||||
 | 
			
		||||
@ -15,5 +15,6 @@
 | 
			
		||||
  "BASE_MODULE": "mempool",
 | 
			
		||||
  "MEMPOOL_WEBSITE_URL": "https://mempool.space",
 | 
			
		||||
  "LIQUID_WEBSITE_URL": "https://liquid.network",
 | 
			
		||||
  "BISQ_WEBSITE_URL": "https://bisq.markets"
 | 
			
		||||
  "BISQ_WEBSITE_URL": "https://bisq.markets",
 | 
			
		||||
  "MINING_DASHBOARD": true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,7 @@ export function app(locale: string): express.Express {
 | 
			
		||||
  server.get('/address/*', getLocalizedSSR(indexHtml));
 | 
			
		||||
  server.get('/blocks', getLocalizedSSR(indexHtml));
 | 
			
		||||
  server.get('/mining/pools', getLocalizedSSR(indexHtml));
 | 
			
		||||
  server.get('/mining/pool/*', getLocalizedSSR(indexHtml));
 | 
			
		||||
  server.get('/graphs', getLocalizedSSR(indexHtml));
 | 
			
		||||
  server.get('/liquid', getLocalizedSSR(indexHtml));
 | 
			
		||||
  server.get('/liquid/tx/*', getLocalizedSSR(indexHtml));
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,9 @@ import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.com
 | 
			
		||||
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
 | 
			
		||||
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
let routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
@ -56,16 +59,28 @@ let routes: Routes = [
 | 
			
		||||
            path: 'mempool-block/:id',
 | 
			
		||||
            component: MempoolBlockComponent
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining',
 | 
			
		||||
            component: MiningDashboardComponent,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'blocks',
 | 
			
		||||
        component: LatestBlocksComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mining/difficulty',
 | 
			
		||||
        component: DifficultyChartComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mining/pools',
 | 
			
		||||
        component: PoolRankingComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mining/pool/:poolId',
 | 
			
		||||
        component: PoolComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'graphs',
 | 
			
		||||
        component: StatisticsComponent,
 | 
			
		||||
@ -144,16 +159,28 @@ let routes: Routes = [
 | 
			
		||||
                path: 'mempool-block/:id',
 | 
			
		||||
                component: MempoolBlockComponent
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'mining',
 | 
			
		||||
                component: MiningDashboardComponent,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'blocks',
 | 
			
		||||
            component: LatestBlocksComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/difficulty',
 | 
			
		||||
            component: DifficultyChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/pools',
 | 
			
		||||
            component: PoolRankingComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/pool/:poolId',
 | 
			
		||||
            component: PoolComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'graphs',
 | 
			
		||||
            component: StatisticsComponent,
 | 
			
		||||
@ -226,16 +253,28 @@ let routes: Routes = [
 | 
			
		||||
                path: 'mempool-block/:id',
 | 
			
		||||
                component: MempoolBlockComponent
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'mining',
 | 
			
		||||
                component: MiningDashboardComponent,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'blocks',
 | 
			
		||||
            component: LatestBlocksComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/difficulty',
 | 
			
		||||
            component: DifficultyChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/pools',
 | 
			
		||||
            component: PoolRankingComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/pool/:poolId',
 | 
			
		||||
            component: PoolComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'graphs',
 | 
			
		||||
            component: StatisticsComponent,
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,7 @@ import { TimeSpanComponent } from './components/time-span/time-span.component';
 | 
			
		||||
import { SeoService } from './services/seo.service';
 | 
			
		||||
import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component';
 | 
			
		||||
import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component';
 | 
			
		||||
import { PoolComponent } from './components/pool/pool.component';
 | 
			
		||||
import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component';
 | 
			
		||||
import { AssetComponent } from './components/asset/asset.component';
 | 
			
		||||
import { AssetsComponent } from './components/assets/assets.component';
 | 
			
		||||
@ -67,6 +68,8 @@ 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 { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
 | 
			
		||||
import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
@ -96,6 +99,7 @@ import { AssetGroupComponent } from './components/assets/asset-group/asset-group
 | 
			
		||||
    IncomingTransactionsGraphComponent,
 | 
			
		||||
    MempoolGraphComponent,
 | 
			
		||||
    PoolRankingComponent,
 | 
			
		||||
    PoolComponent,
 | 
			
		||||
    LbtcPegsGraphComponent,
 | 
			
		||||
    AssetComponent,
 | 
			
		||||
    AssetsComponent,
 | 
			
		||||
@ -116,6 +120,8 @@ import { AssetGroupComponent } from './components/assets/asset-group/asset-group
 | 
			
		||||
    AssetsNavComponent,
 | 
			
		||||
    AssetsFeaturedComponent,
 | 
			
		||||
    AssetGroupComponent,
 | 
			
		||||
    MiningDashboardComponent,
 | 
			
		||||
    DifficultyChartComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
 | 
			
		||||
 | 
			
		||||
@ -21,9 +21,13 @@
 | 
			
		||||
        </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>
 | 
			
		||||
  <div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingBlocksTemplate>
 | 
			
		||||
 | 
			
		||||
@ -124,3 +124,9 @@
 | 
			
		||||
  50% {opacity: 1.0;}
 | 
			
		||||
  100% {opacity: 0.7;}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.badge {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  top: 15px;
 | 
			
		||||
  z-index: 101;
 | 
			
		||||
}
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core';
 | 
			
		||||
import { Observable, Subscription } from 'rxjs';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
@ -12,6 +12,7 @@ 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[] = [];
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
<div class="text-center" class="blockchain-wrapper">
 | 
			
		||||
<div class="text-center" class="blockchain-wrapper animate" #container>
 | 
			
		||||
  <div class="position-container {{ network }}">
 | 
			
		||||
    <span>
 | 
			
		||||
      <app-mempool-blocks></app-mempool-blocks>
 | 
			
		||||
      <app-blockchain-blocks></app-blockchain-blocks>
 | 
			
		||||
      <app-blockchain-blocks [showMiningInfo]="showMiningInfo"></app-blockchain-blocks>
 | 
			
		||||
      <div id="divider"></div>
 | 
			
		||||
    </span>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blockchain-wrapper {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  height: 250px;
 | 
			
		||||
 | 
			
		||||
  -webkit-user-select: none; /* Safari */        
 | 
			
		||||
@ -61,3 +60,13 @@
 | 
			
		||||
  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,10 +8,11 @@ import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class BlockchainComponent implements OnInit {
 | 
			
		||||
  showMiningInfo: boolean = false;
 | 
			
		||||
  network: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,53 @@
 | 
			
		||||
<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>
 | 
			
		||||
@ -0,0 +1,10 @@
 | 
			
		||||
.main-title {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: #ffffff91;
 | 
			
		||||
  margin-top: -13px;
 | 
			
		||||
  font-size: 10px;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding-bottom: 3px;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,154 @@
 | 
			
		||||
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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -31,8 +31,11 @@
 | 
			
		||||
      <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">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/mining/pools' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="master-page.mining-pools" title="Mining Pools"></fa-icon></a>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" 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">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/blocks' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cubes']" [fixedWidth]="true" i18n-title="master-page.blocks" title="Blocks"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" id="btn-graphs">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'chart-area']" [fixedWidth]="true" i18n-title="master-page.graphs" title="Graphs"></fa-icon></a>
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ export class MasterPageComponent implements OnInit {
 | 
			
		||||
  urlLanguage: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private languageService: LanguageService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,30 @@
 | 
			
		||||
<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-body">
 | 
			
		||||
          <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>
 | 
			
		||||
        </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>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,57 @@
 | 
			
		||||
.dashboard-container {
 | 
			
		||||
  padding-bottom: 60px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  margin-top: 0.5rem;
 | 
			
		||||
  @media (min-width: 992px) {
 | 
			
		||||
    padding-bottom: 0px;
 | 
			
		||||
  }
 | 
			
		||||
  .col {
 | 
			
		||||
    margin-bottom: 1.5rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card {
 | 
			
		||||
  background-color: #1d1f31;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#blockchain-container {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  overflow-x: scroll;
 | 
			
		||||
  overflow-y: hidden;
 | 
			
		||||
  scrollbar-width: none;
 | 
			
		||||
  -ms-overflow-style: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#blockchain-container::-webkit-scrollbar {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fade-border {
 | 
			
		||||
  -webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
 | 
			
		||||
}
 | 
			
		||||
  
 | 
			
		||||
.main-title {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: #ffffff91;
 | 
			
		||||
  margin-top: -13px;
 | 
			
		||||
  font-size: 10px;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding-bottom: 3px;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,16 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-mining-dashboard',
 | 
			
		||||
  templateUrl: './mining-dashboard.component.html',
 | 
			
		||||
  styleUrls: ['./mining-dashboard.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class MiningDashboardComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,49 +1,48 @@
 | 
			
		||||
<div class="container-xl">
 | 
			
		||||
  <!-- <app-difficulty [showProgress]=false [showHalving]=true></app-difficulty>  -->
 | 
			
		||||
<div [class]="widget === false ? 'container-xl' : ''">
 | 
			
		||||
 | 
			
		||||
  <div class="hashrate-pie" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
 | 
			
		||||
  <div class="hashrate-pie" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></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">
 | 
			
		||||
  <div class="card-header mb-0 mb-lg-4" [style]="widget === true ? '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">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'24h'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="24h"> 24h
 | 
			
		||||
          <input ngbButton type="radio" [value]="'24h'" fragment="24h"> 24h
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 3">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'3d'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="3d"> 3D
 | 
			
		||||
          <input ngbButton type="radio" [value]="'3d'" fragment="3d"> 3D
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 7">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'1w'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="1w"> 1W
 | 
			
		||||
          <input ngbButton type="radio" [value]="'1w'" fragment="1w"> 1W
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 30">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'1m'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="1m"> 1M
 | 
			
		||||
          <input ngbButton type="radio" [value]="'1m'" fragment="1m"> 1M
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 90">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'3m'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="3m"> 3M
 | 
			
		||||
          <input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 180">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'6m'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="6m"> 6M
 | 
			
		||||
          <input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 365">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'1y'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="1y"> 1Y
 | 
			
		||||
          <input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 730">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'2y'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="2y"> 2Y
 | 
			
		||||
          <input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 1095">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'3y'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="3y"> 3Y
 | 
			
		||||
          <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/pools' | relativeUrl]" fragment="all"> ALL
 | 
			
		||||
          <input ngbButton type="radio" [value]="'all'" fragment="all"> ALL
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <table class="table table-borderless text-center pools-table" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50">
 | 
			
		||||
  <table *ngIf="widget === false" class="table table-borderless text-center pools-table">
 | 
			
		||||
    <thead>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th class="d-none d-md-block" i18n="mining.rank">Rank</th>
 | 
			
		||||
@ -58,14 +57,14 @@
 | 
			
		||||
      <tr *ngFor="let pool of miningStats.pools">
 | 
			
		||||
        <td class="d-none d-md-block">{{ pool.rank }}</td>
 | 
			
		||||
        <td class="text-right"><img width="25" height="25" src="{{ pool.logo }}" onError="this.src = './resources/mining-pools/default.svg'"></td>
 | 
			
		||||
        <td class="">{{ pool.name }}</td>
 | 
			
		||||
        <td class=""><a [routerLink]="[('/mining/pool/' + pool.poolId) | relativeUrl]">{{ pool.name }}</a></td>
 | 
			
		||||
        <td class="" *ngIf="this.poolsWindowPreference === '24h'">{{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }}</td>
 | 
			
		||||
        <td class="">{{ pool['blockText'] }}</td>
 | 
			
		||||
        <td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr style="border-top: 1px solid #555">
 | 
			
		||||
        <td class="d-none d-md-block">-</td>
 | 
			
		||||
        <td class="text-right"><img width="25" height="25" src="./resources/mining-pools/default.svg"></td>
 | 
			
		||||
        <td class="d-none d-md-block"></td>
 | 
			
		||||
        <td class="text-right"></td>
 | 
			
		||||
        <td class="" i18n="mining.all-miners"><b>All miners</b></td>
 | 
			
		||||
        <td class="" *ngIf="this.poolsWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{ miningStats.miningUnits.hashrateUnit }}</b></td>
 | 
			
		||||
        <td class=""><b>{{ miningStats.blockCount }}</b></td>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { Component, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { FormBuilder, FormGroup } from '@angular/forms';
 | 
			
		||||
import { EChartsOption } from 'echarts';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { EChartsOption, PieSeriesOption } from 'echarts';
 | 
			
		||||
import { combineLatest, Observable, of } from 'rxjs';
 | 
			
		||||
import { catchError, map, share, skip, startWith, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { SinglePoolStats } from 'src/app/interfaces/node-api.interface';
 | 
			
		||||
@ -8,6 +9,7 @@ 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';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-pool-ranking',
 | 
			
		||||
@ -22,7 +24,9 @@ import { StateService } from '../../services/state.service';
 | 
			
		||||
    }
 | 
			
		||||
  `],
 | 
			
		||||
})
 | 
			
		||||
export class PoolRankingComponent implements OnInit, OnDestroy {
 | 
			
		||||
export class PoolRankingComponent implements OnInit {
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
 | 
			
		||||
  poolsWindowPreference: string;
 | 
			
		||||
  radioGroupForm: FormGroup;
 | 
			
		||||
 | 
			
		||||
@ -31,6 +35,7 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
 | 
			
		||||
  chartInitOptions = {
 | 
			
		||||
    renderer: 'svg'
 | 
			
		||||
  };
 | 
			
		||||
  chartInstance: any = undefined;
 | 
			
		||||
 | 
			
		||||
  miningStatsObservable$: Observable<MiningStats>;
 | 
			
		||||
 | 
			
		||||
@ -40,14 +45,20 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
 | 
			
		||||
    private formBuilder: FormBuilder,
 | 
			
		||||
    private miningService: MiningService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
  ) {
 | 
			
		||||
    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 });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    if (this.widget) {
 | 
			
		||||
      this.poolsWindowPreference = '1w';
 | 
			
		||||
    } else {
 | 
			
		||||
      this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w';    
 | 
			
		||||
    }
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference);
 | 
			
		||||
 | 
			
		||||
    // When...
 | 
			
		||||
    this.miningStatsObservable$ = combineLatest([
 | 
			
		||||
      // ...a new block is mined
 | 
			
		||||
@ -61,7 +72,9 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
 | 
			
		||||
        .pipe(
 | 
			
		||||
          startWith(this.poolsWindowPreference), // (trigger when the page loads)
 | 
			
		||||
          tap((value) => {
 | 
			
		||||
            if (!this.widget) {
 | 
			
		||||
              this.storageService.setValue('poolsWindowPreference', value);
 | 
			
		||||
            }
 | 
			
		||||
            this.poolsWindowPreference = value;
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
@ -87,9 +100,6 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  formatPoolUI(pool: SinglePoolStats) {
 | 
			
		||||
    pool['blockText'] = pool.blockCount.toString() + ` (${pool.share}%)`;
 | 
			
		||||
    return pool;
 | 
			
		||||
@ -115,9 +125,9 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
 | 
			
		||||
          overflow: 'break',
 | 
			
		||||
        },
 | 
			
		||||
        tooltip: {
 | 
			
		||||
          backgroundColor: "#282d47",
 | 
			
		||||
          backgroundColor: '#282d47',
 | 
			
		||||
          textStyle: {
 | 
			
		||||
            color: "#FFFFFF",
 | 
			
		||||
            color: '#FFFFFF',
 | 
			
		||||
          },
 | 
			
		||||
          formatter: () => {
 | 
			
		||||
            if (this.poolsWindowPreference === '24h') {
 | 
			
		||||
@ -129,8 +139,9 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
 | 
			
		||||
                pool.blockCount.toString() + ` blocks`;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
        },
 | 
			
		||||
        data: pool.poolId,
 | 
			
		||||
      } as PieSeriesOption);
 | 
			
		||||
    });
 | 
			
		||||
    return data;
 | 
			
		||||
  }
 | 
			
		||||
@ -144,8 +155,7 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
      title: {
 | 
			
		||||
        text: $localize`:@@mining.pool-chart-title:${network}:NETWORK: mining pools share`,
 | 
			
		||||
        subtext: $localize`:@@mining.pool-chart-sub-title:Estimated from the # of blocks mined`,
 | 
			
		||||
        text: this.widget ? '' : $localize`:@@mining.pool-chart-title:${network}:NETWORK: mining pools share`,
 | 
			
		||||
        left: 'center',
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: '#FFF',
 | 
			
		||||
@ -160,10 +170,11 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
 | 
			
		||||
      },
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          top: this.isMobile() ? '5%' : '20%',
 | 
			
		||||
          top: this.widget ? '0%' : (this.isMobile() ? '5%' : '10%'),
 | 
			
		||||
          bottom: this.widget ? '0%' : (this.isMobile() ? '0%' : '5%'),
 | 
			
		||||
          name: 'Mining pool',
 | 
			
		||||
          type: 'pie',
 | 
			
		||||
          radius: this.isMobile() ? ['10%', '50%'] : ['20%', '80%'],
 | 
			
		||||
          radius: this.widget ? ['20%', '60%'] : (this.isMobile() ? ['10%', '50%'] : ['20%', '70%']),
 | 
			
		||||
          data: this.generatePoolsChartSerieData(miningStats),
 | 
			
		||||
          labelLine: {
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
@ -180,11 +191,8 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
 | 
			
		||||
          },
 | 
			
		||||
          emphasis: {
 | 
			
		||||
            itemStyle: {
 | 
			
		||||
              borderWidth: 2,
 | 
			
		||||
              borderColor: '#FFF',
 | 
			
		||||
              borderRadius: 2,
 | 
			
		||||
              shadowBlur: 80,
 | 
			
		||||
              shadowColor: 'rgba(255, 255, 255, 0.75)',
 | 
			
		||||
              shadowBlur: 40,
 | 
			
		||||
              shadowColor: 'rgba(0, 0, 0, 0.75)',
 | 
			
		||||
            },
 | 
			
		||||
            labelLine: {
 | 
			
		||||
              lineStyle: {
 | 
			
		||||
@ -193,10 +201,22 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
      ],
 | 
			
		||||
      color: chartColors
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onChartInit(ec) {
 | 
			
		||||
    if (this.chartInstance !== undefined) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.chartInstance = ec;
 | 
			
		||||
    this.chartInstance.on('click', (e) => {
 | 
			
		||||
      this.router.navigate(['/mining/pool/', e.data.data]);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Default mining stats if something goes wrong
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										113
									
								
								frontend/src/app/components/pool/pool.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								frontend/src/app/components/pool/pool.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,113 @@
 | 
			
		||||
<div class="container">
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="poolStats$ | async as poolStats">
 | 
			
		||||
    <h1 class="m-0">
 | 
			
		||||
      <img width="50" src="{{ poolStats['logo'] }}" onError="this.src = './resources/mining-pools/default.svg'" class="mr-3">
 | 
			
		||||
      {{ poolStats.pool.name }}
 | 
			
		||||
    </h1>
 | 
			
		||||
 | 
			
		||||
    <div class="box pl-0 bg-transparent">
 | 
			
		||||
      <div class="card-header mb-0 mb-lg-4 pr-0 pl-0">
 | 
			
		||||
        <form [formGroup]="radioGroupForm" class="formRadioGroup ml-0">
 | 
			
		||||
          <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
 | 
			
		||||
            <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
              <input ngbButton type="radio" [value]="'24h'"> 24h
 | 
			
		||||
            </label>
 | 
			
		||||
            <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
              <input ngbButton type="radio" [value]="'3d'"> 3D
 | 
			
		||||
            </label>
 | 
			
		||||
            <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
              <input ngbButton type="radio" [value]="'1w'"> 1W
 | 
			
		||||
            </label>
 | 
			
		||||
            <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
              <input ngbButton type="radio" [value]="'1m'"> 1M
 | 
			
		||||
            </label>
 | 
			
		||||
            <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
              <input ngbButton type="radio" [value]="'3m'"> 3M
 | 
			
		||||
            </label>
 | 
			
		||||
            <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
              <input ngbButton type="radio" [value]="'6m'"> 6M
 | 
			
		||||
            </label>
 | 
			
		||||
            <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
              <input ngbButton type="radio" [value]="'1y'"> 1Y
 | 
			
		||||
            </label>
 | 
			
		||||
            <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
              <input ngbButton type="radio" [value]="'2y'"> 2Y
 | 
			
		||||
            </label>
 | 
			
		||||
            <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
              <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>
 | 
			
		||||
 | 
			
		||||
    <div class="box">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="col-lg-9">
 | 
			
		||||
          <table class="table table-borderless table-striped" style="table-layout: fixed;">
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td class="col-4 col-lg-3">Addresses</td>
 | 
			
		||||
                <td class="text-truncate" *ngIf="poolStats.pool.addresses.length else noaddress">
 | 
			
		||||
                  <div class="scrollable">
 | 
			
		||||
                    <a *ngFor="let address of poolStats.pool.addresses" [routerLink]="['/address' | relativeUrl, address]">{{ address }}<br></a>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <ng-template #noaddress><td>~</td></ng-template>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td class="col-4 col-lg-3">Coinbase Tags</td>
 | 
			
		||||
                <td class="text-truncate">{{ poolStats.pool.regexes }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-lg-3">
 | 
			
		||||
          <table class="table table-borderless table-striped">
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td class="col-4 col-lg-8">Mined Blocks</td>
 | 
			
		||||
                <td class="text-left">{{ poolStats.blockCount }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td class="col-4 col-lg-8">Empty Blocks</td>
 | 
			
		||||
                <td class="text-left">{{ poolStats.emptyBlocks.length }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
 | 
			
		||||
    <thead>
 | 
			
		||||
      <th style="width: 15%;" i18n="latest-blocks.height">Height</th>
 | 
			
		||||
      <th class="d-none d-md-block" style="width: 20%;" i18n="latest-blocks.timestamp">Timestamp</th>
 | 
			
		||||
      <th style="width: 20%;" i18n="latest-blocks.mined">Mined</th>
 | 
			
		||||
      <th style="width: 10%;" i18n="latest-blocks.reward">Reward</th>
 | 
			
		||||
      <th class="d-none d-lg-block" style="width: 15%;" i18n="latest-blocks.transactions">Transactions</th>
 | 
			
		||||
      <th style="width: 20%;" i18n="latest-blocks.size">Size</th>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody *ngIf="blocks$ | async as blocks">
 | 
			
		||||
      <tr *ngFor="let block of blocks">
 | 
			
		||||
        <td><a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a></td>
 | 
			
		||||
        <td class="d-none d-md-block">‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
 | 
			
		||||
        <td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
 | 
			
		||||
        <td class=""><app-amount [satoshis]="block['reward']" digitsInfo="1.2-2" [noFiat]="true"></app-amount></td>
 | 
			
		||||
        <td class="d-none d-lg-block">{{ block.tx_count | number }}</td>
 | 
			
		||||
        <td>
 | 
			
		||||
          <div class="progress">
 | 
			
		||||
            <div class="progress-bar progress-mempool" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div>
 | 
			
		||||
            <div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										41
									
								
								frontend/src/app/components/pool/pool.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frontend/src/app/components/pool/pool.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
.progress {
 | 
			
		||||
  background-color: #2d3348;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (min-width: 768px) {
 | 
			
		||||
  .d-md-block {
 | 
			
		||||
      display: table-cell !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@media (min-width: 992px) {
 | 
			
		||||
  .d-lg-block {
 | 
			
		||||
      display: table-cell !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.formRadioGroup {
 | 
			
		||||
  margin-top: 6px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  @media (min-width: 830px) {
 | 
			
		||||
    margin-left: 2%;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-top: 0px;
 | 
			
		||||
  }
 | 
			
		||||
  .btn-sm {
 | 
			
		||||
    font-size: 9px;
 | 
			
		||||
    @media (min-width: 830px) {
 | 
			
		||||
      font-size: 14px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.scrollable {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
  max-height: 100px;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										84
									
								
								frontend/src/app/components/pool/pool.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								frontend/src/app/components/pool/pool.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
import { FormBuilder, FormGroup } from '@angular/forms';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
 | 
			
		||||
import { distinctUntilChanged, map, startWith, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { BlockExtended, PoolStat } from 'src/app/interfaces/node-api.interface';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-pool',
 | 
			
		||||
  templateUrl: './pool.component.html',
 | 
			
		||||
  styleUrls: ['./pool.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush
 | 
			
		||||
})
 | 
			
		||||
export class PoolComponent implements OnInit {
 | 
			
		||||
  poolStats$: Observable<PoolStat>;
 | 
			
		||||
  blocks$: Observable<BlockExtended[]>;
 | 
			
		||||
 | 
			
		||||
  fromHeight: number = -1;
 | 
			
		||||
  fromHeightSubject: BehaviorSubject<number> = new BehaviorSubject(this.fromHeight);
 | 
			
		||||
 | 
			
		||||
  blocks: BlockExtended[] = [];
 | 
			
		||||
  poolId: number = undefined;
 | 
			
		||||
  radioGroupForm: FormGroup;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private formBuilder: FormBuilder,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: '1w' });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue('1w');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.poolStats$ = combineLatest([
 | 
			
		||||
      this.route.params.pipe(map((params) => params.poolId)),
 | 
			
		||||
      this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith('1w')),
 | 
			
		||||
    ])
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap((params: any) => {
 | 
			
		||||
          this.poolId = params[0];
 | 
			
		||||
          if (this.blocks.length === 0) {
 | 
			
		||||
            this.fromHeightSubject.next(undefined);
 | 
			
		||||
          }
 | 
			
		||||
          return this.apiService.getPoolStats$(this.poolId, params[1] ?? '1w');
 | 
			
		||||
        }),
 | 
			
		||||
        map((poolStats) => {
 | 
			
		||||
          let regexes = '"';
 | 
			
		||||
          for (const regex of poolStats.pool.regexes) {
 | 
			
		||||
            regexes += regex + '", "';
 | 
			
		||||
          }
 | 
			
		||||
          poolStats.pool.regexes = regexes.slice(0, -3);
 | 
			
		||||
          poolStats.pool.addresses = poolStats.pool.addresses;
 | 
			
		||||
 | 
			
		||||
          return Object.assign({
 | 
			
		||||
            logo: `./resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'
 | 
			
		||||
          }, poolStats);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.blocks$ = this.fromHeightSubject
 | 
			
		||||
      .pipe(
 | 
			
		||||
        distinctUntilChanged(),
 | 
			
		||||
        switchMap((fromHeight) => {
 | 
			
		||||
          return this.apiService.getPoolBlocks$(this.poolId, fromHeight);
 | 
			
		||||
        }),
 | 
			
		||||
        tap((newBlocks) => {
 | 
			
		||||
          this.blocks = this.blocks.concat(newBlocks);
 | 
			
		||||
        }),
 | 
			
		||||
        map(() => this.blocks)
 | 
			
		||||
      )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadMore() {
 | 
			
		||||
    this.fromHeightSubject.next(this.blocks[this.blocks.length - 1]?.height);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackByBlock(index: number, block: BlockExtended) {
 | 
			
		||||
    return block.height;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -54,8 +54,11 @@ export interface LiquidPegs {
 | 
			
		||||
 | 
			
		||||
export interface ITranslators { [language: string]: string; }
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * PoolRanking component
 | 
			
		||||
 */
 | 
			
		||||
export interface SinglePoolStats {
 | 
			
		||||
  pooldId: number;
 | 
			
		||||
  poolId: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
  link: string;
 | 
			
		||||
  blockCount: number;
 | 
			
		||||
@ -66,20 +69,35 @@ export interface SinglePoolStats {
 | 
			
		||||
  emptyBlockRatio: string;
 | 
			
		||||
  logo: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PoolsStats {
 | 
			
		||||
  blockCount: number;
 | 
			
		||||
  lastEstimatedHashrate: number;
 | 
			
		||||
  oldestIndexedBlockTimestamp: number;
 | 
			
		||||
  pools: SinglePoolStats[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MiningStats {
 | 
			
		||||
  lastEstimatedHashrate: string,
 | 
			
		||||
  blockCount: number,
 | 
			
		||||
  totalEmptyBlock: number,
 | 
			
		||||
  totalEmptyBlockRatio: string,
 | 
			
		||||
  pools: SinglePoolStats[],
 | 
			
		||||
  lastEstimatedHashrate: string;
 | 
			
		||||
  blockCount: number;
 | 
			
		||||
  totalEmptyBlock: number;
 | 
			
		||||
  totalEmptyBlockRatio: string;
 | 
			
		||||
  pools: SinglePoolStats[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Pool component
 | 
			
		||||
 */
 | 
			
		||||
export interface PoolInfo {
 | 
			
		||||
  id: number | null; // mysql row id
 | 
			
		||||
  name: string;
 | 
			
		||||
  link: string;
 | 
			
		||||
  regexes: string; // JSON array
 | 
			
		||||
  addresses: string; // JSON array
 | 
			
		||||
  emptyBlocks: number;
 | 
			
		||||
}
 | 
			
		||||
export interface PoolStat {
 | 
			
		||||
  pool: PoolInfo;
 | 
			
		||||
  blockCount: number;
 | 
			
		||||
  emptyBlocks: BlockExtended[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface BlockExtension {
 | 
			
		||||
@ -88,6 +106,10 @@ export interface BlockExtension {
 | 
			
		||||
  reward?: number;
 | 
			
		||||
  coinbaseTx?: Transaction;
 | 
			
		||||
  matchRate?: number;
 | 
			
		||||
  pool?: {
 | 
			
		||||
    id: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  stage?: number; // Frontend only
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { HttpClient, HttpParams } from '@angular/common/http';
 | 
			
		||||
import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation, LiquidPegs, ITranslators, PoolsStats } from '../interfaces/node-api.interface';
 | 
			
		||||
import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation, LiquidPegs, ITranslators, PoolsStats, PoolStat, BlockExtended, BlockExtension } from '../interfaces/node-api.interface';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { StateService } from './state.service';
 | 
			
		||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
 | 
			
		||||
@ -129,7 +129,31 @@ export class ApiService {
 | 
			
		||||
    return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listPools$(interval: string | null) : Observable<PoolsStats> {
 | 
			
		||||
    return this.httpClient.get<PoolsStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools/${interval}`);
 | 
			
		||||
  listPools$(interval: string | undefined) : Observable<PoolsStats> {
 | 
			
		||||
    return this.httpClient.get<PoolsStats>(
 | 
			
		||||
      this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools` +
 | 
			
		||||
      (interval !== undefined ? `/${interval}` : '')
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getPoolStats$(poolId: number, interval: string | undefined): Observable<PoolStat> {
 | 
			
		||||
    return this.httpClient.get<PoolStat>(
 | 
			
		||||
      this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}` +
 | 
			
		||||
      (interval !== undefined ? `/${interval}` : '')
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getPoolBlocks$(poolId: number, fromHeight: number): Observable<BlockExtended[]> {
 | 
			
		||||
    return this.httpClient.get<BlockExtended[]>(
 | 
			
		||||
        this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}/blocks` +
 | 
			
		||||
        (fromHeight !== undefined ? `/${fromHeight}` : '')
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getHistoricalDifficulty$(interval: string | undefined): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any[]>(
 | 
			
		||||
        this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty` +
 | 
			
		||||
        (interval !== undefined ? `/${interval}` : '')
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@ export interface Env {
 | 
			
		||||
  MEMPOOL_WEBSITE_URL: string;
 | 
			
		||||
  LIQUID_WEBSITE_URL: string;
 | 
			
		||||
  BISQ_WEBSITE_URL: string;
 | 
			
		||||
  MINING_DASHBOARD: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultEnv: Env = {
 | 
			
		||||
@ -59,6 +60,7 @@ const defaultEnv: Env = {
 | 
			
		||||
  'MEMPOOL_WEBSITE_URL': 'https://mempool.space',
 | 
			
		||||
  'LIQUID_WEBSITE_URL': 'https://liquid.network',
 | 
			
		||||
  'BISQ_WEBSITE_URL': 'https://bisq.markets',
 | 
			
		||||
  'MINING_DASHBOARD': true
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user