Merge branch 'master' into simon/optimize-reconnection
This commit is contained in:
		
						commit
						8022cec36b
					
				
							
								
								
									
										2
									
								
								.github/workflows/cypress.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/cypress.yml
									
									
									
									
										vendored
									
									
								
							@ -21,7 +21,7 @@ jobs:
 | 
			
		||||
      - name: Setup node
 | 
			
		||||
        uses: actions/setup-node@v2
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 16.10.0
 | 
			
		||||
          node-version: 16.15.0
 | 
			
		||||
          cache: 'npm'
 | 
			
		||||
          cache-dependency-path: frontend/package-lock.json
 | 
			
		||||
      - name: ${{ matrix.browser }} browser tests (Mempool)
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
 | 
			
		||||
export interface AbstractBitcoinApi {
 | 
			
		||||
  $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
 | 
			
		||||
  $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, blockHash?: string): Promise<IEsploraApi.Transaction>;
 | 
			
		||||
  $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
 | 
			
		||||
  $getBlockHeightTip(): Promise<number>;
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]>;
 | 
			
		||||
  $getBlockHash(height: number): Promise<string>;
 | 
			
		||||
 | 
			
		||||
@ -14,14 +14,31 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
    this.bitcoindClient = bitcoinClient;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransaction(txId: string, skipConversion = false, addPrevout = false, blockHash?: string): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
  static convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block {
 | 
			
		||||
    return {
 | 
			
		||||
      id: block.hash,
 | 
			
		||||
      height: block.height,
 | 
			
		||||
      version: block.version,
 | 
			
		||||
      timestamp: block.time,
 | 
			
		||||
      bits: parseInt(block.bits, 16),
 | 
			
		||||
      nonce: block.nonce,
 | 
			
		||||
      difficulty: block.difficulty,
 | 
			
		||||
      merkle_root: block.merkleroot,
 | 
			
		||||
      tx_count: block.nTx,
 | 
			
		||||
      size: block.size,
 | 
			
		||||
      weight: block.weight,
 | 
			
		||||
      previousblockhash: block.previousblockhash,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    // If the transaction is in the mempool we already converted and fetched the fee. Only prevouts are missing
 | 
			
		||||
    const txInMempool = mempool.getMempool()[txId];
 | 
			
		||||
    if (txInMempool && addPrevout) {
 | 
			
		||||
      return this.$addPrevouts(txInMempool);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return this.bitcoindClient.getRawTransaction(txId, true, blockHash)
 | 
			
		||||
    return this.bitcoindClient.getRawTransaction(txId, true)
 | 
			
		||||
      .then((transaction: IBitcoinApi.Transaction) => {
 | 
			
		||||
        if (skipConversion) {
 | 
			
		||||
          transaction.vout.forEach((vout) => {
 | 
			
		||||
@ -174,35 +191,18 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (transaction.confirmations) {
 | 
			
		||||
      esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
 | 
			
		||||
    } else {
 | 
			
		||||
      esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
 | 
			
		||||
      if (addPrevout) {
 | 
			
		||||
        esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
 | 
			
		||||
    if (addPrevout) {
 | 
			
		||||
      if (transaction.confirmations) {
 | 
			
		||||
        esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction);
 | 
			
		||||
      } else {
 | 
			
		||||
        esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
 | 
			
		||||
        esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return esploraTransaction;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block {
 | 
			
		||||
    return {
 | 
			
		||||
      id: block.hash,
 | 
			
		||||
      height: block.height,
 | 
			
		||||
      version: block.version,
 | 
			
		||||
      timestamp: block.time,
 | 
			
		||||
      bits: parseInt(block.bits, 16),
 | 
			
		||||
      nonce: block.nonce,
 | 
			
		||||
      difficulty: block.difficulty,
 | 
			
		||||
      merkle_root: block.merkleroot,
 | 
			
		||||
      tx_count: block.nTx,
 | 
			
		||||
      size: block.size,
 | 
			
		||||
      weight: block.weight,
 | 
			
		||||
      previousblockhash: block.previousblockhash,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private translateScriptPubKeyType(outputType: string): string {
 | 
			
		||||
    const map = {
 | 
			
		||||
      'pubkey': 'p2pk',
 | 
			
		||||
@ -245,7 +245,7 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
      if (vin.prevout) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      const innerTx = await this.$getRawTransaction(vin.txid, false);
 | 
			
		||||
      const innerTx = await this.$getRawTransaction(vin.txid, false, false);
 | 
			
		||||
      vin.prevout = innerTx.vout[vin.vout];
 | 
			
		||||
      this.addInnerScriptsToVin(vin);
 | 
			
		||||
    }
 | 
			
		||||
@ -271,18 +271,16 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
    return this.bitcoindClient.getRawMemPool(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
  private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    if (transaction.vin[0].is_coinbase) {
 | 
			
		||||
      transaction.fee = 0;
 | 
			
		||||
      return transaction;
 | 
			
		||||
    }
 | 
			
		||||
    let totalIn = 0;
 | 
			
		||||
    for (const vin of transaction.vin) {
 | 
			
		||||
      const innerTx = await this.$getRawTransaction(vin.txid, !addPrevout);
 | 
			
		||||
      if (addPrevout) {
 | 
			
		||||
        vin.prevout = innerTx.vout[vin.vout];
 | 
			
		||||
        this.addInnerScriptsToVin(vin);
 | 
			
		||||
      }
 | 
			
		||||
      const innerTx = await this.$getRawTransaction(vin.txid, false, false);
 | 
			
		||||
      vin.prevout = innerTx.vout[vin.vout];
 | 
			
		||||
      this.addInnerScriptsToVin(vin);
 | 
			
		||||
      totalIn += innerTx.vout[vin.vout].value;
 | 
			
		||||
    }
 | 
			
		||||
    const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0);
 | 
			
		||||
 | 
			
		||||
@ -134,7 +134,7 @@ class Blocks {
 | 
			
		||||
      blockExtended.extras.avgFeeRate = stats.avgfeerate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (Common.indexingEnabled()) {
 | 
			
		||||
    if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
 | 
			
		||||
      let pool: PoolTag;
 | 
			
		||||
      if (blockExtended.extras?.coinbaseTx !== undefined) {
 | 
			
		||||
        pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
 | 
			
		||||
@ -143,7 +143,8 @@ class Blocks {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!pool) { // We should never have this situation in practise
 | 
			
		||||
        logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. Check your "pools" table entries`);
 | 
			
		||||
        logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. ` +
 | 
			
		||||
          `Check your "pools" table entries`);
 | 
			
		||||
        return blockExtended;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -389,6 +390,42 @@ class Blocks {
 | 
			
		||||
    return prepareBlock(blockExtended);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Index a block by hash if it's missing from the database. Returns the block after indexing
 | 
			
		||||
   */
 | 
			
		||||
  public async $getBlock(hash: string): Promise<BlockExtended | IEsploraApi.Block> {
 | 
			
		||||
    // Check the memory cache
 | 
			
		||||
    const blockByHash = this.getBlocks().find((b) => b.id === hash);
 | 
			
		||||
    if (blockByHash) {
 | 
			
		||||
      return blockByHash;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Block has already been indexed
 | 
			
		||||
    if (Common.indexingEnabled()) {
 | 
			
		||||
      const dbBlock = await blocksRepository.$getBlockByHash(hash);
 | 
			
		||||
      if (dbBlock != null) {
 | 
			
		||||
        return prepareBlock(dbBlock);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const block = await bitcoinApi.$getBlock(hash);
 | 
			
		||||
 | 
			
		||||
    // Not Bitcoin network, return the block as it
 | 
			
		||||
    if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
 | 
			
		||||
      return block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Bitcoin network, add our custom data on top
 | 
			
		||||
    const transactions = await this.$getTransactionsExtended(hash, block.height, true);
 | 
			
		||||
    const blockExtended = await this.$getBlockExtended(block, transactions);
 | 
			
		||||
    if (Common.indexingEnabled()) {
 | 
			
		||||
      delete(blockExtended['coinbaseTx']);
 | 
			
		||||
      await blocksRepository.$saveBlockInDatabase(blockExtended);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return blockExtended;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlocksExtras(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
 | 
			
		||||
    // Note - This API is breaking if indexing is not available. For now it is okay because we only
 | 
			
		||||
    // use it for the mining pages, and mining pages should not be available if indexing is turned off.
 | 
			
		||||
 | 
			
		||||
@ -169,12 +169,12 @@ export class Common {
 | 
			
		||||
      default: return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  static indexingEnabled(): boolean {
 | 
			
		||||
    return (
 | 
			
		||||
      ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) &&
 | 
			
		||||
      ['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK) &&
 | 
			
		||||
      config.DATABASE.ENABLED === true &&
 | 
			
		||||
      config.MEMPOOL.INDEXING_BLOCKS_AMOUNT != 0
 | 
			
		||||
      config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== 0
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ class DifficultyAdjustmentApi {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let timeAvgMins = blocksInEpoch ? diff / blocksInEpoch / 60 : 10;
 | 
			
		||||
    let timeAvgMins = blocksInEpoch && blocksInEpoch > 146 ? diff / blocksInEpoch / 60 : 10;
 | 
			
		||||
 | 
			
		||||
    // Testnet difficulty is set to 1 after 20 minutes of no blocks,
 | 
			
		||||
    // therefore the time between blocks will always be below 20 minutes (1200s).
 | 
			
		||||
 | 
			
		||||
@ -300,24 +300,12 @@ class Server {
 | 
			
		||||
 | 
			
		||||
    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'))
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/1m', routes.$getPools.bind(routes, '1m'))
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3m', routes.$getPools.bind(routes, '3m'))
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/6m', routes.$getPools.bind(routes, '6m'))
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/1y', routes.$getPools.bind(routes, '1y'))
 | 
			
		||||
        .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/pools/:interval', routes.$getPools)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', routes.$getPoolHistoricalHashrate)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', routes.$getPoolBlocks)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks/:height', routes.$getPoolBlocks)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', routes.$getPool)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/:interval', routes.$getPool)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools', routes.$getPoolsHistoricalHashrate)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate', routes.$getHistoricalHashrate)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees)
 | 
			
		||||
@ -350,7 +338,8 @@ 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);
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras/:height', routes.getBlocksExtras)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock);
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
      this.app
 | 
			
		||||
@ -362,7 +351,6 @@ class Server {
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', routes.getRawTransaction)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', routes.getTransactionStatus)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', routes.getBlockHeader)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks)
 | 
			
		||||
 | 
			
		||||
@ -287,6 +287,7 @@ class BlocksRepository {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      rows[0].fee_span = JSON.parse(rows[0].fee_span);
 | 
			
		||||
      return rows[0];
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -294,6 +295,34 @@ class BlocksRepository {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get one block by hash
 | 
			
		||||
   */
 | 
			
		||||
  public async $getBlockByHash(hash: string): Promise<object | null> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `
 | 
			
		||||
        SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, hash as id,
 | 
			
		||||
        pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.slug as pool_slug,
 | 
			
		||||
        pools.addresses as pool_addresses, pools.regexes as pool_regexes,
 | 
			
		||||
        previous_block_hash as previousblockhash
 | 
			
		||||
        FROM blocks
 | 
			
		||||
        JOIN pools ON blocks.pool_id = pools.id
 | 
			
		||||
        WHERE hash = '${hash}';
 | 
			
		||||
      `;
 | 
			
		||||
      const [rows]: any[] = await DB.query(query);
 | 
			
		||||
 | 
			
		||||
      if (rows.length <= 0) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      rows[0].fee_span = JSON.parse(rows[0].fee_span);
 | 
			
		||||
      return rows[0];
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot get indexed block ${hash}. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Return blocks difficulty
 | 
			
		||||
   */
 | 
			
		||||
@ -397,18 +426,28 @@ class BlocksRepository {
 | 
			
		||||
      const [blocks]: any[] = await DB.query(`SELECT height, hash, previous_block_hash,
 | 
			
		||||
        UNIX_TIMESTAMP(blockTimestamp) as timestamp FROM blocks ORDER BY height`);
 | 
			
		||||
 | 
			
		||||
      let currentHeight = 1;
 | 
			
		||||
      while (currentHeight < blocks.length) {
 | 
			
		||||
        if (blocks[currentHeight].previous_block_hash !== blocks[currentHeight - 1].hash) {
 | 
			
		||||
          logger.warn(`Chain divergence detected at block ${blocks[currentHeight - 1].height}, re-indexing newer blocks and hashrates`);
 | 
			
		||||
          await this.$deleteBlocksFrom(blocks[currentHeight - 1].height);
 | 
			
		||||
          await HashratesRepository.$deleteHashratesFromTimestamp(blocks[currentHeight - 1].timestamp - 604800);
 | 
			
		||||
      let partialMsg = false;
 | 
			
		||||
      let idx = 1;
 | 
			
		||||
      while (idx < blocks.length) {
 | 
			
		||||
        if (blocks[idx].height - 1 !== blocks[idx - 1].height) {
 | 
			
		||||
          if (partialMsg === false) {
 | 
			
		||||
            logger.info('Some blocks are not indexed, skipping missing blocks during chain validation');
 | 
			
		||||
            partialMsg = true;
 | 
			
		||||
          }
 | 
			
		||||
          ++idx;
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
 | 
			
		||||
          logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}, re-indexing newer blocks and hashrates`);
 | 
			
		||||
          await this.$deleteBlocksFrom(blocks[idx - 1].height);
 | 
			
		||||
          await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
        ++currentHeight;
 | 
			
		||||
        ++idx;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      logger.info(`${currentHeight} blocks hash validated in ${new Date().getTime() - start} ms`);
 | 
			
		||||
      logger.info(`${idx} blocks hash validated in ${new Date().getTime() - start} ms`);
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('Cannot validate chain of block hash. Reason: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -457,7 +496,7 @@ class BlocksRepository {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the historical averaged block rewards
 | 
			
		||||
   */
 | 
			
		||||
   public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise<any> {
 | 
			
		||||
  public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      let query = `SELECT
 | 
			
		||||
        CAST(AVG(height) as INT) as avg_height,
 | 
			
		||||
 | 
			
		||||
@ -572,9 +572,9 @@ class Routes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getPools(interval: string, req: Request, res: Response) {
 | 
			
		||||
  public async $getPools(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const stats = await miningStats.$getPoolsStats(interval);
 | 
			
		||||
      const stats = await miningStats.$getPoolsStats(req.params.interval);
 | 
			
		||||
      const blockCount = await BlocksRepository.$blockCount(null, null);
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
@ -588,7 +588,7 @@ class Routes {
 | 
			
		||||
 | 
			
		||||
  public async $getPoolsHistoricalHashrate(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval ?? null);
 | 
			
		||||
      const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval);
 | 
			
		||||
      const blockCount = await BlocksRepository.$blockCount(null, null);
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
@ -620,8 +620,8 @@ class Routes {
 | 
			
		||||
 | 
			
		||||
  public async $getHistoricalHashrate(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval ?? null);
 | 
			
		||||
      const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval ?? null);
 | 
			
		||||
      const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval);
 | 
			
		||||
      const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval);
 | 
			
		||||
      const blockCount = await BlocksRepository.$blockCount(null, null);
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
@ -640,7 +640,7 @@ class Routes {
 | 
			
		||||
 | 
			
		||||
  public async $getHistoricalBlockFees(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const blockFees = await mining.$getHistoricalBlockFees(req.params.interval ?? null);
 | 
			
		||||
      const blockFees = await mining.$getHistoricalBlockFees(req.params.interval);
 | 
			
		||||
      const blockCount = await BlocksRepository.$blockCount(null, null);
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
@ -654,7 +654,7 @@ class Routes {
 | 
			
		||||
 | 
			
		||||
  public async $getHistoricalBlockRewards(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval ?? null);
 | 
			
		||||
      const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval);
 | 
			
		||||
      const blockCount = await BlocksRepository.$blockCount(null, null);
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
@ -668,7 +668,7 @@ class Routes {
 | 
			
		||||
 | 
			
		||||
  public async $getHistoricalBlockFeeRates(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const blockFeeRates = await mining.$getHistoricalBlockFeeRates(req.params.interval ?? null);
 | 
			
		||||
      const blockFeeRates = await mining.$getHistoricalBlockFeeRates(req.params.interval);
 | 
			
		||||
      const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
@ -684,8 +684,8 @@ class Routes {
 | 
			
		||||
 | 
			
		||||
  public async $getHistoricalBlockSizeAndWeight(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const blockSizes = await mining.$getHistoricalBlockSizes(req.params.interval ?? null);
 | 
			
		||||
      const blockWeights = await mining.$getHistoricalBlockWeights(req.params.interval ?? null);
 | 
			
		||||
      const blockSizes = await mining.$getHistoricalBlockSizes(req.params.interval);
 | 
			
		||||
      const blockWeights = await mining.$getHistoricalBlockWeights(req.params.interval);
 | 
			
		||||
      const blockCount = await BlocksRepository.$blockCount(null, null);
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
@ -702,8 +702,8 @@ class Routes {
 | 
			
		||||
 | 
			
		||||
  public async getBlock(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.$getBlock(req.params.hash);
 | 
			
		||||
      res.json(result);
 | 
			
		||||
      const block = await blocks.$getBlock(req.params.hash);
 | 
			
		||||
      res.json(block);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
@ -727,7 +727,7 @@ class Routes {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  public async getBlocks(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      loadingIndicators.setProgress('blocks', 0);
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { BlockExtended } from "../mempool.interfaces";
 | 
			
		||||
import { BlockExtended } from '../mempool.interfaces';
 | 
			
		||||
 | 
			
		||||
export function prepareBlock(block: any): BlockExtended {
 | 
			
		||||
  return <BlockExtended>{
 | 
			
		||||
@ -17,9 +17,11 @@ export function prepareBlock(block: any): BlockExtended {
 | 
			
		||||
    extras: {
 | 
			
		||||
      coinbaseRaw: block.coinbase_raw ?? block.extras.coinbaseRaw,
 | 
			
		||||
      medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee,
 | 
			
		||||
      feeRange: block.feeRange ?? block.fee_range ?? block?.extras?.feeSpan,
 | 
			
		||||
      feeRange: block.feeRange ?? block.fee_span,
 | 
			
		||||
      reward: block.reward ?? block?.extras?.reward,
 | 
			
		||||
      totalFees: block.totalFees ?? block?.fees ?? block?.extras.totalFees,
 | 
			
		||||
      totalFees: block.totalFees ?? block?.fees ?? block?.extras?.totalFees,
 | 
			
		||||
      avgFee: block?.extras?.avgFee ?? block.avg_fee,
 | 
			
		||||
      avgFeeRate: block?.avgFeeRate ?? block.avg_fee_rate,
 | 
			
		||||
      pool: block?.extras?.pool ?? (block?.pool_id ? {
 | 
			
		||||
        id: block.pool_id,
 | 
			
		||||
        name: block.pool_name,
 | 
			
		||||
 | 
			
		||||
@ -155,7 +155,7 @@ Corresponding `docker-compose.yml` overrides:
 | 
			
		||||
    environment:
 | 
			
		||||
      ELECTRUM_HOST: ""
 | 
			
		||||
      ELECTRUM_PORT: ""
 | 
			
		||||
      ELECTRUM_TLS: ""
 | 
			
		||||
      ELECTRUM_TLS_ENABLED: ""
 | 
			
		||||
      ...
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:16.10.0-buster-slim AS builder
 | 
			
		||||
FROM node:16.15.0-buster-slim AS builder
 | 
			
		||||
 | 
			
		||||
ARG commitHash
 | 
			
		||||
ENV DOCKER_COMMIT_HASH=${commitHash}
 | 
			
		||||
@ -11,7 +11,7 @@ RUN apt-get install -y build-essential python3 pkg-config
 | 
			
		||||
RUN npm install
 | 
			
		||||
RUN npm run build
 | 
			
		||||
 | 
			
		||||
FROM node:16.10.0-buster-slim
 | 
			
		||||
FROM node:16.15.0-buster-slim
 | 
			
		||||
 | 
			
		||||
WORKDIR /backend
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,7 @@ __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
 | 
			
		||||
# ELECTRUM
 | 
			
		||||
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
 | 
			
		||||
__ELECTRUM_PORT__=${ELECTRUM_PORT:=50002}
 | 
			
		||||
__ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS:=false}
 | 
			
		||||
__ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
 | 
			
		||||
 | 
			
		||||
# ESPLORA
 | 
			
		||||
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:16.10.0-buster-slim AS builder
 | 
			
		||||
FROM node:16.15.0-buster-slim AS builder
 | 
			
		||||
 | 
			
		||||
ARG commitHash
 | 
			
		||||
ENV DOCKER_COMMIT_HASH=${commitHash}
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,6 @@ import { AssetComponent } from './components/asset/asset.component';
 | 
			
		||||
import { AssetsComponent } from './components/assets/assets.component';
 | 
			
		||||
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
 | 
			
		||||
import { StatusViewComponent } from './components/status-view/status-view.component';
 | 
			
		||||
import { MinerComponent } from './components/miner/miner.component';
 | 
			
		||||
import { SharedModule } from './shared/shared.module';
 | 
			
		||||
import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
 | 
			
		||||
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
 | 
			
		||||
@ -68,6 +67,7 @@ import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/
 | 
			
		||||
import { MiningStartComponent } from './components/mining-start/mining-start.component';
 | 
			
		||||
import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
 | 
			
		||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
 | 
			
		||||
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
 | 
			
		||||
import { GraphsComponent } from './components/graphs/graphs.component';
 | 
			
		||||
import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments-table/difficulty-adjustments-table.components';
 | 
			
		||||
import { BlocksList } from './components/blocks-list/blocks-list.component';
 | 
			
		||||
@ -108,7 +108,6 @@ import { BlockSizesWeightsGraphComponent } from './components/block-sizes-weight
 | 
			
		||||
    LbtcPegsGraphComponent,
 | 
			
		||||
    AssetComponent,
 | 
			
		||||
    AssetsComponent,
 | 
			
		||||
    MinerComponent,
 | 
			
		||||
    StatusViewComponent,
 | 
			
		||||
    FeesBoxComponent,
 | 
			
		||||
    DashboardComponent,
 | 
			
		||||
@ -163,6 +162,7 @@ import { BlockSizesWeightsGraphComponent } from './components/block-sizes-weight
 | 
			
		||||
    StorageService,
 | 
			
		||||
    LanguageService,
 | 
			
		||||
    ShortenStringPipe,
 | 
			
		||||
    CapAddressPipe,
 | 
			
		||||
    { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
 | 
			
		||||
  ],
 | 
			
		||||
  bootstrap: [AppComponent]
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@
 | 
			
		||||
                <td><a [routerLink]="['/block/' | relativeUrl, block.hash]" title="{{ block.hash }}">{{ block.hash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.hash"></app-clipboard></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
 | 
			
		||||
                <td i18n="block.timestamp">Timestamp</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                  ‎{{ block.time | date:'yyyy-MM-dd HH:mm' }}
 | 
			
		||||
                  <div class="lg-inline">
 | 
			
		||||
@ -83,7 +83,7 @@
 | 
			
		||||
                <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
 | 
			
		||||
                <td i18n="block.timestamp">Timestamp</td>
 | 
			
		||||
                <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
          </table>
 | 
			
		||||
 | 
			
		||||
@ -89,7 +89,7 @@
 | 
			
		||||
            </table>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-all">View all »</a></div>
 | 
			
		||||
          <div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@ -98,7 +98,7 @@
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <h5 class="card-title text-center" i18n="Latest Trades header">Latest Trades</h5>
 | 
			
		||||
          <app-bisq-trades [trades$]="trades$" view="small"></app-bisq-trades>
 | 
			
		||||
          <div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-all">View all »</a></div>
 | 
			
		||||
          <div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,7 @@
 | 
			
		||||
            <table class="table table-borderless table-striped">
 | 
			
		||||
              <tbody>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
 | 
			
		||||
                  <td i18n="block.timestamp">Timestamp</td>
 | 
			
		||||
                  <td>
 | 
			
		||||
                    ‎{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
 | 
			
		||||
                    <div class="lg-inline">
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@
 | 
			
		||||
      <th style="width: 20%;" i18n>TXID</th>
 | 
			
		||||
      <th class="d-none d-md-block" style="width: 100%;" i18n>Type</th>
 | 
			
		||||
      <th style="width: 20%;" i18n>Amount</th>
 | 
			
		||||
      <th style="width: 20%;" i18n>Confirmed</th>
 | 
			
		||||
      <th style="width: 20%;" i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</th>
 | 
			
		||||
      <th class="d-none d-md-block" i18n>Height</th>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody *ngIf="transactions.value; else loadingTmpl">
 | 
			
		||||
 | 
			
		||||
@ -118,7 +118,7 @@ export class AddressLabelsComponent implements OnInit {
 | 
			
		||||
    }
 | 
			
		||||
    const m = parseInt(opM.match(/[0-9]+/)[0], 10);
 | 
			
		||||
 | 
			
		||||
    this.label = `multisig ${m} of ${n}`;
 | 
			
		||||
    this.label = $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleVout() {
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,13 @@
 | 
			
		||||
<div *ngIf="group$ | async as group; else loading">
 | 
			
		||||
<div *ngIf="group$ | async as group; else loading;">
 | 
			
		||||
  
 | 
			
		||||
  <div class="main-title">
 | 
			
		||||
    <h2>{{ group.group.name }}</h2>
 | 
			
		||||
 | 
			
		||||
    <div class="sub-title" i18n>Group of {{ group.group.assets.length | number }} assets</div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <ng-container *ngTemplateOutlet="title; context: group.group"></ng-container>
 | 
			
		||||
 | 
			
		||||
  <ng-template #title>
 | 
			
		||||
    <div class="main-title">
 | 
			
		||||
      <h2>{{ group.name }}</h2>
 | 
			
		||||
      <div class="sub-title" i18n>Group of {{ group.assets.length | number }} assets</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,11 +2,10 @@
 | 
			
		||||
 | 
			
		||||
<div class="full-container">
 | 
			
		||||
  <div class="card-header mb-0 mb-md-4">
 | 
			
		||||
    <span i18n="mining.block-fee-rates">Block fee rates</span>
 | 
			
		||||
    <span i18n="mining.block-fee-rates">Block Fee Rates</span>
 | 
			
		||||
    <button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
 | 
			
		||||
      <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
    </button>
 | 
			
		||||
 | 
			
		||||
    <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
 | 
			
		||||
      <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 1">
 | 
			
		||||
 | 
			
		||||
@ -62,7 +62,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@mining.block-fee-rates:Block Fee Rates`);
 | 
			
		||||
    this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`);
 | 
			
		||||
    this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
 | 
			
		||||
 | 
			
		||||
@ -2,11 +2,10 @@
 | 
			
		||||
 | 
			
		||||
<div class="full-container">
 | 
			
		||||
  <div class="card-header mb-0 mb-md-4">
 | 
			
		||||
    <span i18n="mining.block-fees">Block fees</span>
 | 
			
		||||
    <button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
 | 
			
		||||
    <span i18n="mining.block-fees">Block Fees</span>
 | 
			
		||||
      <button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
 | 
			
		||||
      <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
    </button>
 | 
			
		||||
 | 
			
		||||
    <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
 | 
			
		||||
      <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,7 @@ export class BlockFeesGraphComponent implements OnInit {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@mining.block-fees:Block Fees`);
 | 
			
		||||
    this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Block Fees`);
 | 
			
		||||
    this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
 | 
			
		||||
 | 
			
		||||
@ -3,11 +3,10 @@
 | 
			
		||||
<div class="full-container">
 | 
			
		||||
 | 
			
		||||
  <div class="card-header mb-0 mb-md-4">
 | 
			
		||||
    <span i18n="mining.block-rewards">Block rewards</span>
 | 
			
		||||
  <span i18n="mining.block-rewards">Block Rewards</span>
 | 
			
		||||
    <button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
 | 
			
		||||
      <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
    </button>
 | 
			
		||||
 | 
			
		||||
    <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
 | 
			
		||||
      <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ export class BlockRewardsGraphComponent implements OnInit {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@mining.block-reward:Block Reward`);
 | 
			
		||||
    this.seoService.setTitle($localize`:@@8ba8fe810458280a83df7fdf4c614dfc1a826445:Block Rewards`);
 | 
			
		||||
    this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<div class="full-container">
 | 
			
		||||
 | 
			
		||||
  <div class="card-header mb-0 mb-md-4">
 | 
			
		||||
    <span i18n="mining.block-size-weight">Block Sizes and Weights</span>
 | 
			
		||||
    <span i18n="mining.block-sizes-weights">Block Sizes and Weights</span>
 | 
			
		||||
    <button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
 | 
			
		||||
      <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
    </button>
 | 
			
		||||
 | 
			
		||||
@ -62,7 +62,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    let firstRun = true;
 | 
			
		||||
 | 
			
		||||
    this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Weight`);
 | 
			
		||||
    this.seoService.setTitle($localize`:@@56fa1cd221491b6478998679cba2dc8d55ba330d:Block Sizes and Weights`);
 | 
			
		||||
    this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
 | 
			
		||||
 | 
			
		||||
@ -81,15 +81,26 @@
 | 
			
		||||
              <ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td i18n="block.total-fees|Total fees in a block">Total fees</td>
 | 
			
		||||
                  <td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees"><app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="fees * 100000000" digitsInfo="1.0-0"></app-fiat></span></td>
 | 
			
		||||
                  <td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees">
 | 
			
		||||
                    <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
 | 
			
		||||
                    <span class="fiat">
 | 
			
		||||
                      <app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat>
 | 
			
		||||
                    </span>
 | 
			
		||||
                  </td>
 | 
			
		||||
                  <ng-template #liquidTotalFees>
 | 
			
		||||
                    <td><app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount>  <app-fiat [value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat></td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                      <app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount>  <app-fiat
 | 
			
		||||
                        [value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat>
 | 
			
		||||
                    </td>
 | 
			
		||||
                  </ng-template>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
 | 
			
		||||
                  <td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
 | 
			
		||||
                  <td>
 | 
			
		||||
                    <app-amount [satoshis]="(blockSubsidy + fees) * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat></span>
 | 
			
		||||
                    <app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
 | 
			
		||||
                    <span class="fiat">
 | 
			
		||||
                      <app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat>
 | 
			
		||||
                    </span>
 | 
			
		||||
                  </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
@ -105,7 +116,18 @@
 | 
			
		||||
              </ng-template>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="block.miner">Miner</td>
 | 
			
		||||
                <td><app-miner [coinbaseTransaction]="coinbaseTx"></app-miner></td>
 | 
			
		||||
                <td *ngIf="stateService.env.MINING_DASHBOARD">
 | 
			
		||||
                  <a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
 | 
			
		||||
                    [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
 | 
			
		||||
                    {{ block.extras.pool.name }}
 | 
			
		||||
                  </a>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
 | 
			
		||||
                  <span placement="bottom" class="badge"
 | 
			
		||||
                    [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
 | 
			
		||||
                    {{ block.extras.pool.name }}
 | 
			
		||||
                </span>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
@ -173,7 +195,7 @@
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
    <app-transactions-list [transactions]="transactions"></app-transactions-list>
 | 
			
		||||
    <app-transactions-list [transactions]="transactions" [paginated]="true"></app-transactions-list>
 | 
			
		||||
 | 
			
		||||
    <ng-template [ngIf]="isLoadingTransactions">
 | 
			
		||||
      <div class="text-center mb-4" class="tx-skeleton">
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { WebsocketService } from 'src/app/services/websocket.service';
 | 
			
		||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-block',
 | 
			
		||||
@ -31,7 +32,6 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  blockSubsidy: number;
 | 
			
		||||
  fees: number;
 | 
			
		||||
  paginationMaxSize: number;
 | 
			
		||||
  coinbaseTx: Transaction;
 | 
			
		||||
  page = 1;
 | 
			
		||||
  itemsPerPage: number;
 | 
			
		||||
  txsLoadingStatus$: Observable<number>;
 | 
			
		||||
@ -50,10 +50,11 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    private location: Location,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private relativeUrlPipe: RelativeUrlPipe,
 | 
			
		||||
    private apiService: ApiService
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
@ -88,7 +89,6 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
        const blockHash: string = params.get('id') || '';
 | 
			
		||||
        this.block = undefined;
 | 
			
		||||
        this.page = 1;
 | 
			
		||||
        this.coinbaseTx = undefined;
 | 
			
		||||
        this.error = undefined;
 | 
			
		||||
        this.fees = undefined;
 | 
			
		||||
        this.stateService.markBlock$.next({});
 | 
			
		||||
@ -124,7 +124,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
                  this.location.replaceState(
 | 
			
		||||
                    this.router.createUrlTree([(this.network ? '/' + this.network : '') + '/block/', hash]).toString()
 | 
			
		||||
                  );
 | 
			
		||||
                  return this.electrsApiService.getBlock$(hash);
 | 
			
		||||
                  return this.apiService.getBlock$(hash);
 | 
			
		||||
                })
 | 
			
		||||
              );
 | 
			
		||||
          }
 | 
			
		||||
@ -134,7 +134,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
            return of(blockInCache);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return this.electrsApiService.getBlock$(blockHash);
 | 
			
		||||
          return this.apiService.getBlock$(blockHash);
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      tap((block: BlockExtended) => {
 | 
			
		||||
@ -145,7 +145,6 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
 | 
			
		||||
        this.isLoadingBlock = false;
 | 
			
		||||
        this.coinbaseTx = block?.extras?.coinbaseTx;
 | 
			
		||||
        this.setBlockSubsidy();
 | 
			
		||||
        if (block?.extras?.reward !== undefined) {
 | 
			
		||||
          this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
 | 
			
		||||
@ -167,9 +166,6 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
      if (this.fees === undefined && transactions[0]) {
 | 
			
		||||
        this.fees = transactions[0].vout.reduce((acc: number, curr: Vout) => acc + curr.value, 0) / 100000000 - this.blockSubsidy;
 | 
			
		||||
      }
 | 
			
		||||
      if (!this.coinbaseTx && transactions[0]) {
 | 
			
		||||
        this.coinbaseTx = transactions[0];
 | 
			
		||||
      }
 | 
			
		||||
      this.transactions = transactions;
 | 
			
		||||
      this.isLoadingTransactions = false;
 | 
			
		||||
    },
 | 
			
		||||
@ -212,13 +208,10 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    this.queryParamsSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO - Refactor this.fees/this.reward for liquid because it is not
 | 
			
		||||
  // used anymore on Bitcoin networks (we use block.extras directly)
 | 
			
		||||
  setBlockSubsidy() {
 | 
			
		||||
    if (this.network === 'liquid' || this.network === 'liquidtestnet') {
 | 
			
		||||
      this.blockSubsidy = 0;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const halvings = Math.floor(this.block.height / 210000);
 | 
			
		||||
    this.blockSubsidy = 50 * 2 ** -halvings;
 | 
			
		||||
    this.blockSubsidy = 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pageChange(page: number, target: HTMLElement) {
 | 
			
		||||
 | 
			
		||||
@ -1,27 +1,25 @@
 | 
			
		||||
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
 | 
			
		||||
 | 
			
		||||
<div class="container-xl" [class]="widget ? 'widget' : 'full-height'">
 | 
			
		||||
  <h1 *ngIf="!widget" class="float-left" i18n="latest-blocks.blocks">Blocks</h1>
 | 
			
		||||
  <h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Blocks</h1>
 | 
			
		||||
 | 
			
		||||
  <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
  <div style="min-height: 295px">
 | 
			
		||||
    <table class="table table-borderless">
 | 
			
		||||
      <thead>
 | 
			
		||||
        <th class="height" [class]="widget ? 'widget' : ''" i18n="latest-blocks.height">Height</th>
 | 
			
		||||
        <th class="pool text-left" [class]="widget ? 'widget' : ''" i18n="mining.pool-name">
 | 
			
		||||
          Pool</th>
 | 
			
		||||
        <th class="height text-left" [class]="widget ? 'widget' : ''" i18n="latest-blocks.height">Height</th>
 | 
			
		||||
        <th class="pool text-left" [class]="widget ? 'widget' : ''" i18n="mining.pool-name">Pool</th>
 | 
			
		||||
        <th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget">Timestamp</th>
 | 
			
		||||
        <th class="mined" i18n="latest-blocks.mined" *ngIf="!widget">Mined</th>
 | 
			
		||||
        <th class="reward text-right" i18n="latest-blocks.reward" [class]="widget ? 'widget' : ''">
 | 
			
		||||
          Reward</th>
 | 
			
		||||
        <th class="reward text-right" i18n="latest-blocks.reward" [class]="widget ? 'widget' : ''">Reward</th>
 | 
			
		||||
        <th class="fees text-right" i18n="latest-blocks.fees" *ngIf="!widget">Fees</th>
 | 
			
		||||
        <th class="txs text-right" i18n="latest-blocks.transactions" [class]="widget ? 'widget' : ''">Txs</th>
 | 
			
		||||
        <th class="txs text-right" i18n="dashboard.txs" [class]="widget ? 'widget' : ''">TXs</th>
 | 
			
		||||
        <th class="size" i18n="latest-blocks.size" *ngIf="!widget">Size</th>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
 | 
			
		||||
        <tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
 | 
			
		||||
          <td [class]="widget ? 'widget' : ''">
 | 
			
		||||
          <td class="text-left" [class]="widget ? 'widget' : ''">
 | 
			
		||||
            <a [routerLink]="['/block' | relativeUrl, block.height]">{{ block.height
 | 
			
		||||
              }}</a>
 | 
			
		||||
          </td>
 | 
			
		||||
@ -29,7 +27,7 @@
 | 
			
		||||
            <div class="tooltip-custom">
 | 
			
		||||
              <a class="clear-link" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]">
 | 
			
		||||
                <img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
 | 
			
		||||
                  onError="this.src = './resources/mining-pools/default.svg'">
 | 
			
		||||
                  onError="this.src = './resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
 | 
			
		||||
                <span class="pool-name">{{ block.extras.pool.name }}</span>
 | 
			
		||||
              </a>
 | 
			
		||||
              <span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
 | 
			
		||||
@ -42,10 +40,10 @@
 | 
			
		||||
            <app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="reward text-right" [class]="widget ? 'widget' : ''">
 | 
			
		||||
            <app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2"></app-amount>
 | 
			
		||||
            <app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="fees text-right" *ngIf="!widget">
 | 
			
		||||
            <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2"></app-amount>
 | 
			
		||||
            <app-amount [satoshis]="block.extras.totalFees" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="txs text-right" [class]="widget ? 'widget' : ''">
 | 
			
		||||
            {{ block.tx_count | number }}
 | 
			
		||||
@ -62,7 +60,7 @@
 | 
			
		||||
      <ng-template #skeleton>
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr *ngFor="let item of skeletonLines">
 | 
			
		||||
            <td class="height" [class]="widget ? 'widget' : ''">
 | 
			
		||||
            <td class="height text-left" [class]="widget ? 'widget' : ''">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 75px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="pool text-left" [class]="widget ? 'widget' : ''">
 | 
			
		||||
 | 
			
		||||
@ -11,13 +11,10 @@
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
td {
 | 
			
		||||
  padding-top: 0.7rem !important;
 | 
			
		||||
tr, td, th {
 | 
			
		||||
  border: 0px;
 | 
			
		||||
  padding-top: 0.65rem !important;
 | 
			
		||||
  padding-bottom: 0.7rem !important;
 | 
			
		||||
  @media (max-width: 376px) {
 | 
			
		||||
    padding-top: 0.73rem !important;
 | 
			
		||||
    padding-bottom: 0.73rem !important;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clear-link {
 | 
			
		||||
@ -41,7 +38,7 @@ td {
 | 
			
		||||
}
 | 
			
		||||
.pool.widget {
 | 
			
		||||
  width: 40%;
 | 
			
		||||
  padding-left: 30px;
 | 
			
		||||
  padding-left: 24px;
 | 
			
		||||
  @media (max-width: 376px) {
 | 
			
		||||
    width: 60%;
 | 
			
		||||
  }
 | 
			
		||||
@ -56,7 +53,7 @@ td {
 | 
			
		||||
  width: 10%;
 | 
			
		||||
}
 | 
			
		||||
.height.widget {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
  width: 15%;
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    width: 10%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ export class BlocksList implements OnInit {
 | 
			
		||||
      this.websocketService.want(['blocks']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.skeletonLines = this.widget === true ? [...Array(5).keys()] : [...Array(15).keys()];
 | 
			
		||||
    this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
 | 
			
		||||
    this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
 | 
			
		||||
 | 
			
		||||
    this.blocks$ = combineLatest([
 | 
			
		||||
@ -61,7 +61,7 @@ export class BlocksList implements OnInit {
 | 
			
		||||
                    block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
 | 
			
		||||
                }
 | 
			
		||||
                if (this.widget) {
 | 
			
		||||
                  return blocks.slice(0, 5);
 | 
			
		||||
                  return blocks.slice(0, 6);
 | 
			
		||||
                }
 | 
			
		||||
                return blocks;
 | 
			
		||||
              }),
 | 
			
		||||
@ -85,7 +85,7 @@ export class BlocksList implements OnInit {
 | 
			
		||||
          blocks[1][0].extras.pool.logo = `./resources/mining-pools/` +
 | 
			
		||||
            blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
 | 
			
		||||
          acc.unshift(blocks[1][0]);
 | 
			
		||||
          acc = acc.slice(0, this.widget ? 5 : 15);
 | 
			
		||||
          acc = acc.slice(0, this.widget ? 6 : 15);
 | 
			
		||||
          return acc;
 | 
			
		||||
        }, [])
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
    <tbody *ngIf="isLoading">
 | 
			
		||||
      <tr *ngFor="let item of [1,2,3,4,5]">
 | 
			
		||||
      <tr *ngFor="let item of [1,2,3,4,5,6]">
 | 
			
		||||
        <td class="d-none d-md-block w-75"><span class="skeleton-loader"></span></td>
 | 
			
		||||
        <td class="text-left"><span class="skeleton-loader w-75"></span></td>
 | 
			
		||||
        <td class="text-right"><span class="skeleton-loader w-75"></span></td>
 | 
			
		||||
 | 
			
		||||
@ -2,10 +2,15 @@
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  table-layout:fixed;
 | 
			
		||||
  tr, td, th {
 | 
			
		||||
  tr, th {
 | 
			
		||||
    border: 0px;
 | 
			
		||||
    padding-top: 0.65rem !important;
 | 
			
		||||
    padding-bottom: 0.7rem !important;
 | 
			
		||||
  }
 | 
			
		||||
  td {
 | 
			
		||||
    border: 0px;
 | 
			
		||||
    padding-top: 0.71rem !important;
 | 
			
		||||
    padding-bottom: 0.75rem !important;
 | 
			
		||||
    width: 25%;
 | 
			
		||||
    @media (max-width: 376px) {
 | 
			
		||||
      padding: 0.85rem;
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,7 @@ export class DifficultyAdjustmentsTable implements OnInit {
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            availableTimespanDay: availableTimespanDay,
 | 
			
		||||
            difficulty: tableData.slice(0, 5),
 | 
			
		||||
            difficulty: tableData.slice(0, 6),
 | 
			
		||||
          };
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
@ -5,33 +5,19 @@
 | 
			
		||||
    <button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="mining">Mining</button>
 | 
			
		||||
    <div ngbDropdownMenu aria-labelledby="dropdownBasic1">
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]"
 | 
			
		||||
        i18n="mining.pools">
 | 
			
		||||
        Pools ranking
 | 
			
		||||
      </a>
 | 
			
		||||
        i18n="mining.pools">Pools Ranking</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]"
 | 
			
		||||
        i18n="mining.pools-dominance">
 | 
			
		||||
        Pools dominance
 | 
			
		||||
      </a>
 | 
			
		||||
        i18n="mining.pools-dominance">Pools Dominance</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active"
 | 
			
		||||
        [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="mining.hashrate-difficulty">
 | 
			
		||||
        Hashrate & Difficulty
 | 
			
		||||
      </a>
 | 
			
		||||
        [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="mining.hashrate-difficulty">Hashrate & Difficulty</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active"
 | 
			
		||||
        [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" i18n="mining.block-fee-rates">
 | 
			
		||||
        Block Fee Rates
 | 
			
		||||
      </a>
 | 
			
		||||
        [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" i18n="mining.block-fee-rates">Block Fee Rates</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active"
 | 
			
		||||
        [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" i18n="mining.block-fees">
 | 
			
		||||
        Block Fees
 | 
			
		||||
      </a>
 | 
			
		||||
        [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" i18n="mining.block-fees">Block Fees</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active"
 | 
			
		||||
        [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" i18n="mining.block-rewards">
 | 
			
		||||
        Block Rewards
 | 
			
		||||
      </a>
 | 
			
		||||
        [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" i18n="mining.block-rewards">Block Rewards</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active"
 | 
			
		||||
        [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">
 | 
			
		||||
        Block Sizes and Weights
 | 
			
		||||
      </a>
 | 
			
		||||
        [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -52,7 +52,7 @@
 | 
			
		||||
.chart-widget {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  max-height: 270px;
 | 
			
		||||
  height: 240px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.formRadioGroup {
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ export class HashrateChartComponent implements OnInit {
 | 
			
		||||
    if (this.widget) {
 | 
			
		||||
      this.miningWindowPreference = '1y';
 | 
			
		||||
    } else {
 | 
			
		||||
      this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Difficulty`);
 | 
			
		||||
      this.seoService.setTitle($localize`:@@3510fc6daa1d975f331e3a717bdf1a34efa06dff:Hashrate & Difficulty`);
 | 
			
		||||
      this.miningWindowPreference = this.miningService.getDefaultTimespan('1m');
 | 
			
		||||
    }
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
 | 
			
		||||
 | 
			
		||||
@ -3,11 +3,11 @@
 | 
			
		||||
<div class="full-container">
 | 
			
		||||
 | 
			
		||||
  <div class="card-header  mb-0 mb-md-4">
 | 
			
		||||
    <span i18n="mining.pools-dominance">Mining pools dominance</span>
 | 
			
		||||
    <span i18n="mining.pools-dominance">Pools Dominance</span>
 | 
			
		||||
    <button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
 | 
			
		||||
      <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
    </button>
 | 
			
		||||
 | 
			
		||||
    <span i18n="mining.pools-dominance">Pools Dominance</span>
 | 
			
		||||
    <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
 | 
			
		||||
      <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
<div class="container-xl">
 | 
			
		||||
  <h1 class="float-left" i18n="latest-blocks.blocks">Blocks</h1>
 | 
			
		||||
  <h1 class="float-left" i18n="master-page.blocks">Blocks</h1>
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
  <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ export class LatestBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@f4cba7faeb126346f09cc6af30124f9a343f7a28:Blocks`);
 | 
			
		||||
    this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
 | 
			
		||||
    this.websocketService.want(['blocks']);
 | 
			
		||||
 | 
			
		||||
    this.network$ = merge(of(''), this.stateService.networkChanged$);
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
  <nav class="navbar navbar-expand-md navbar-dark bg-dark">
 | 
			
		||||
  <a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
 | 
			
		||||
    <ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
 | 
			
		||||
      <img [src]="officialMempoolSpace ? './resources/mempool-space-logo.png' : './resources/mempool-logo.png'" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
 | 
			
		||||
      <img [src]="officialMempoolSpace ? './resources/mempool-space-logo.png' : './resources/mempool-logo.png'" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
 | 
			
		||||
      <div class="connection-badge">
 | 
			
		||||
        <div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
 | 
			
		||||
        <div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
 | 
			
		||||
@ -13,16 +13,16 @@
 | 
			
		||||
 | 
			
		||||
  <div (window:resize)="onResize($event)" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
 | 
			
		||||
    <button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
 | 
			
		||||
      <img src="./resources/{{ network.val === '' ? 'bitcoin' : network.val }}-logo.png" style="width: 25px; height: 25px;" class="mr-1">
 | 
			
		||||
      <img src="./resources/{{ network.val === '' ? 'bitcoin' : network.val }}-logo.png" style="width: 25px; height: 25px;" class="mr-1" [alt]="(network.val === '' ? 'bitcoin' : network.val) + ' logo'">
 | 
			
		||||
    </button>
 | 
			
		||||
    <div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
 | 
			
		||||
      <button ngbDropdownItem class="mainnet" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 30px;" class="mr-1"> Mainnet</button>
 | 
			
		||||
      <button ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><img src="./resources/signet-logo.png" style="width: 30px;" class="mr-1"> Signet</button>
 | 
			
		||||
      <button ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</button>
 | 
			
		||||
      <button ngbDropdownItem class="mainnet" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 30px;" class="mr-1" alt="bitcoin logo"> Mainnet</button>
 | 
			
		||||
      <button ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><img src="./resources/signet-logo.png" style="width: 30px;" class="mr-1" alt="signet logo"> Signet</button>
 | 
			
		||||
      <button ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1" alt="testnet logo"> Testnet</button>
 | 
			
		||||
      <h6 *ngIf="env.LIQUID_ENABLED || env.BISQ_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
 | 
			
		||||
      <a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</a>
 | 
			
		||||
      <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1"> Liquid</a>
 | 
			
		||||
      <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><img src="./resources/liquidtestnet-logo.png" style="width: 30px;" class="mr-1"> Liquid Testnet</a>
 | 
			
		||||
      <a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1" alt="bisq logo"> Bisq</a>
 | 
			
		||||
      <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1" alt="liquid mainnet logo"> Liquid</a>
 | 
			
		||||
      <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><img src="./resources/liquidtestnet-logo.png" style="width: 30px;" class="mr-1" alt="liquid testnet logo"> Liquid Testnet</a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@
 | 
			
		||||
        <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" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="master-page.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="mining.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>
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@
 | 
			
		||||
              <td><span class="yellow-color">{{ mempoolBlock.feeRange[0] | number:'1.0-0' }} - {{ mempoolBlock.feeRange[mempoolBlock.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td i18n="mempool-block.total-fees">Total fees</td>
 | 
			
		||||
              <td i18n="block.total-fees|Total fees in a block">Total fees</td>
 | 
			
		||||
              <td><app-amount [satoshis]="mempoolBlock.totalFees" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolBlock.totalFees" digitsInfo="1.0-0"></app-fiat></span></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
 | 
			
		||||
@ -68,7 +68,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  getOrdinal(mempoolBlock: MempoolBlock): string {
 | 
			
		||||
    const blocksInBlock = Math.ceil(mempoolBlock.blockVSize / this.stateService.blockVSize);
 | 
			
		||||
    if (this.mempoolBlockIndex === 0) {
 | 
			
		||||
      return $localize`:@@mempool-block.next.block:Next block`;
 | 
			
		||||
      return $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`;
 | 
			
		||||
    } else if (this.mempoolBlockIndex === this.stateService.env.KEEP_BLOCKS_AMOUNT - 1 && blocksInBlock > 1) {
 | 
			
		||||
      return $localize`:@@mempool-block.stack.of.blocks:Stack of ${blocksInBlock}:INTERPOLATION: mempool blocks`;
 | 
			
		||||
    } else {
 | 
			
		||||
 | 
			
		||||
@ -1,12 +0,0 @@
 | 
			
		||||
<ng-template [ngIf]="loading" [ngIfElse]="done">
 | 
			
		||||
  <span class="skeleton-loader"></span>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #done>
 | 
			
		||||
  <ng-template [ngIf]="miner" [ngIfElse]="unknownMiner">
 | 
			
		||||
    <a placement="bottom" [ngbTooltip]="title" [href]="url" [target]="target" class="badge badge-primary">{{ miner }}</a>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
  <ng-template #unknownMiner>
 | 
			
		||||
    <span class="badge badge-secondary" i18n="miner.tag.unknown-miner">Unknown</span>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
.badge {
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
@ -1,94 +0,0 @@
 | 
			
		||||
import { Component, Input, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { AssetsService } from 'src/app/services/assets.service';
 | 
			
		||||
import { Transaction } from 'src/app/interfaces/electrs.interface';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-miner',
 | 
			
		||||
  templateUrl: './miner.component.html',
 | 
			
		||||
  styleUrls: ['./miner.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class MinerComponent implements OnChanges {
 | 
			
		||||
  @Input() coinbaseTransaction: Transaction;
 | 
			
		||||
  miner = '';
 | 
			
		||||
  title = '';
 | 
			
		||||
  url = '';
 | 
			
		||||
  target = '_blank';
 | 
			
		||||
  loading = true;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private assetsService: AssetsService,
 | 
			
		||||
    private cd: ChangeDetectorRef,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private relativeUrlPipe: RelativeUrlPipe,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges() {
 | 
			
		||||
    this.miner = '';
 | 
			
		||||
    if (this.stateService.env.MINING_DASHBOARD) {
 | 
			
		||||
      this.miner = 'Unknown';
 | 
			
		||||
      this.url = this.relativeUrlPipe.transform(`/mining/pool/unknown`);
 | 
			
		||||
      this.target = '';
 | 
			
		||||
    }
 | 
			
		||||
    this.loading = true;
 | 
			
		||||
    this.findMinerFromCoinbase();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  findMinerFromCoinbase() {
 | 
			
		||||
    if (this.coinbaseTransaction == null || this.coinbaseTransaction.vin == null || this.coinbaseTransaction.vin.length === 0) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.assetsService.getMiningPools$.subscribe((pools) => {
 | 
			
		||||
      for (const vout of this.coinbaseTransaction.vout) {
 | 
			
		||||
        if (!vout.scriptpubkey_address) {
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (pools.payout_addresses[vout.scriptpubkey_address]) {
 | 
			
		||||
            this.miner = pools.payout_addresses[vout.scriptpubkey_address].name;
 | 
			
		||||
            this.title = $localize`:@@miner-identified-by-payout:Identified by payout address: '${vout.scriptpubkey_address}:PAYOUT_ADDRESS:'`;
 | 
			
		||||
            const pool = pools.payout_addresses[vout.scriptpubkey_address];
 | 
			
		||||
            if (this.stateService.env.MINING_DASHBOARD && pools.slugs && pools.slugs[pool.name] !== undefined) {
 | 
			
		||||
              this.url = this.relativeUrlPipe.transform(`/mining/pool/${pools.slugs[pool.name]}`);
 | 
			
		||||
              this.target = '';
 | 
			
		||||
            } else {
 | 
			
		||||
              this.url = pool.link;
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const tag in pools.coinbase_tags) {
 | 
			
		||||
          if (pools.coinbase_tags.hasOwnProperty(tag)) {
 | 
			
		||||
            const coinbaseAscii = this.hex2ascii(this.coinbaseTransaction.vin[0].scriptsig);
 | 
			
		||||
            if (coinbaseAscii.indexOf(tag) > -1) {
 | 
			
		||||
              const pool = pools.coinbase_tags[tag];
 | 
			
		||||
              this.miner = pool.name;
 | 
			
		||||
              this.title = $localize`:@@miner-identified-by-coinbase:Identified by coinbase tag: '${tag}:TAG:'`;
 | 
			
		||||
              if (this.stateService.env.MINING_DASHBOARD && pools.slugs && pools.slugs[pool.name] !== undefined) {
 | 
			
		||||
                this.url = this.relativeUrlPipe.transform(`/mining/pool/${pools.slugs[pool.name]}`);
 | 
			
		||||
                this.target = '';
 | 
			
		||||
              } else {
 | 
			
		||||
                this.url = pool.link;
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.loading = false;
 | 
			
		||||
      this.cd.markForCheck();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  hex2ascii(hex: string) {
 | 
			
		||||
    let str = '';
 | 
			
		||||
    for (let i = 0; i < hex.length; i += 2) {
 | 
			
		||||
      str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
 | 
			
		||||
    }
 | 
			
		||||
    return str;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -11,7 +11,7 @@
 | 
			
		||||
        <span style="font-size: xx-small" i18n="mining.144-blocks">(144 blocks)</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="card-wrapper">
 | 
			
		||||
        <div class="card" style="height: 123px">
 | 
			
		||||
        <div class="card">
 | 
			
		||||
          <div class="card-body more-padding">
 | 
			
		||||
            <app-reward-stats></app-reward-stats>
 | 
			
		||||
          </div>
 | 
			
		||||
@ -22,29 +22,25 @@
 | 
			
		||||
    <!-- difficulty adjustment -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
 | 
			
		||||
      <div class="card" style="height: 123px">
 | 
			
		||||
        <app-difficulty [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
 | 
			
		||||
      </div>
 | 
			
		||||
      <app-difficulty [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- pool distribution -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
    <div class="col" style="margin-bottom: 1.47rem">
 | 
			
		||||
      <div class="card graph-card">
 | 
			
		||||
        <div class="card-body pl-2 pr-2">
 | 
			
		||||
          <app-pool-ranking [widget]=true></app-pool-ranking>
 | 
			
		||||
          <div class="mt-1"><a [routerLink]="['/graphs/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more
 | 
			
		||||
              »</a></div>
 | 
			
		||||
          <div class="mt-1"><a [routerLink]="['/graphs/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- hashrate -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
    <div class="col" style="margin-bottom: 1.47rem">
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <app-hashrate-chart [widget]=true></app-hashrate-chart>
 | 
			
		||||
          <div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
 | 
			
		||||
              »</a></div>
 | 
			
		||||
        <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
 | 
			
		||||
          <app-hashrate-chart [widget]="true"></app-hashrate-chart>
 | 
			
		||||
          <div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@ -53,12 +49,9 @@
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <h5 class="card-title">
 | 
			
		||||
            Latest blocks
 | 
			
		||||
          </h5>
 | 
			
		||||
          <h5 class="card-title" i18n="dashboard.latest-blocks">Latest blocks</h5>
 | 
			
		||||
          <app-blocks-list [widget]=true></app-blocks-list>
 | 
			
		||||
          <div><a [routerLink]="['/mining/blocks' | relativeUrl]" i18n="dashboard.view-more">View
 | 
			
		||||
              more »</a></div>
 | 
			
		||||
          <div><a [routerLink]="['/mining/blocks' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@ -67,12 +60,9 @@
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <h5 class="card-title">
 | 
			
		||||
            Adjustments
 | 
			
		||||
          </h5>
 | 
			
		||||
          <h5 class="card-title" i18n="dashboard.adjustments">Adjustments</h5>
 | 
			
		||||
          <app-difficulty-adjustments-table></app-difficulty-adjustments-table>
 | 
			
		||||
          <div><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
 | 
			
		||||
              »</a></div>
 | 
			
		||||
          <div><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,13 @@
 | 
			
		||||
  background-color: #1d1f31;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.graph-card {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  @media (min-width: 992px) {
 | 
			
		||||
    height: 385px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-title {
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
  color: #4a68b9;
 | 
			
		||||
@ -22,9 +29,6 @@
 | 
			
		||||
  color: #4a68b9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-body {
 | 
			
		||||
  padding: 1.25rem 1rem 0.75rem 1rem;
 | 
			
		||||
}
 | 
			
		||||
.card-body.pool-ranking {
 | 
			
		||||
  padding: 1.25rem 0.25rem 0.75rem 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ export class MiningDashboardComponent implements OnInit {
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`);
 | 
			
		||||
    this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
  <div *ngIf="widget">
 | 
			
		||||
    <div class="pool-distribution" *ngIf="(miningStatsObservable$ | async) as miningStats; else loadingReward">
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <h5 class="card-title" i18n="mining.miners-luck">Pools luck (1w)</h5>
 | 
			
		||||
        <h5 class="card-title" i18n="mining.miners-luck">Pools Luck (1w)</h5>
 | 
			
		||||
        <p class="card-text">
 | 
			
		||||
          {{ miningStats['minersLuck'] }}%
 | 
			
		||||
        </p>
 | 
			
		||||
@ -17,7 +17,7 @@
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <h5 class="card-title" i18n="mining.miners-count">Pools count (1w)</h5>
 | 
			
		||||
        <h5 class="card-title" i18n="mining.miners-count">Pools Count (1w)</h5>
 | 
			
		||||
        <p class="card-text">
 | 
			
		||||
          {{ miningStats.pools.length }}
 | 
			
		||||
        </p>
 | 
			
		||||
@ -26,11 +26,10 @@
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="card-header" *ngIf="!widget">
 | 
			
		||||
    <span i18n="mining.mining-pool-share">Mining pools share</span>
 | 
			
		||||
    <span i18n="mining.pools">Pools Ranking</span>
 | 
			
		||||
    <button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
 | 
			
		||||
      <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
    </button>
 | 
			
		||||
 | 
			
		||||
    <form [formGroup]="radioGroupForm" class="formRadioGroup"
 | 
			
		||||
      *ngIf="!widget && (miningStatsObservable$ | async) as stats">
 | 
			
		||||
      <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
 | 
			
		||||
@ -62,7 +61,7 @@
 | 
			
		||||
          <input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
 | 
			
		||||
        </label>
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount > 157680">
 | 
			
		||||
          <input ngbButton type="radio" [value]="'all'" fragment="all"> ALL
 | 
			
		||||
          <input ngbButton type="radio" [value]="'all'" fragment="all"><span i18n>All</span>
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
@ -87,14 +86,15 @@
 | 
			
		||||
          <th class="" i18n="mining.pool-name">Pool</th>
 | 
			
		||||
          <th class="" *ngIf="this.miningWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th>
 | 
			
		||||
          <th class="" i18n="master-page.blocks">Blocks</th>
 | 
			
		||||
          <th class="d-none d-md-block" i18n="mining.empty-blocks">Empty Blocks</th>
 | 
			
		||||
          <th class="d-none d-md-block" i18n="mining.empty-blocks">Empty blocks</th>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody *ngIf="(miningStatsObservable$ | async) as miningStats">
 | 
			
		||||
        <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="text-right">
 | 
			
		||||
            <img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.src = './resources/mining-pools/default.svg'">
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class=""><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
 | 
			
		||||
          <td class="" *ngIf="this.miningWindowPreference === '24h' && !isLoading">{{ pool.lastEstimatedHashrate }} {{
 | 
			
		||||
            miningStats.miningUnits.hashrateUnit }}</td>
 | 
			
		||||
@ -104,7 +104,7 @@
 | 
			
		||||
        <tr style="border-top: 1px solid #555">
 | 
			
		||||
          <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=""><b i18n="mining.all-miners">All miners</b></td>
 | 
			
		||||
          <td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{
 | 
			
		||||
              miningStats.miningUnits.hashrateUnit }}</b></td>
 | 
			
		||||
          <td class=""><b>{{ miningStats.blockCount }}</b></td>
 | 
			
		||||
@ -121,7 +121,7 @@
 | 
			
		||||
<ng-template #loadingReward>
 | 
			
		||||
  <div class="pool-distribution">
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="mining.miners-luck">Pools luck (1w)</h5>
 | 
			
		||||
      <h5 class="card-title" i18n="mining.miners-luck">Pools Luck (1w)</h5>
 | 
			
		||||
      <p class="card-text">
 | 
			
		||||
        <span class="skeleton-loader skeleton-loader-big"></span>
 | 
			
		||||
      </p>
 | 
			
		||||
@ -133,7 +133,7 @@
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="mining.miners-count">Pools count (1w)</h5>
 | 
			
		||||
      <h5 class="card-title" i18n="mining.miners-count">Pools Count (1w)</h5>
 | 
			
		||||
      <p class="card-text">
 | 
			
		||||
        <span class="skeleton-loader skeleton-loader-big"></span>
 | 
			
		||||
      </p>
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@
 | 
			
		||||
.chart-widget {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  max-height: 270px;
 | 
			
		||||
  height: 240px;
 | 
			
		||||
  @media (max-width: 485px) {
 | 
			
		||||
    max-height: 200px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
  <!-- Pool overview -->
 | 
			
		||||
  <div *ngIf="poolStats$ | async as poolStats; else loadingMain">
 | 
			
		||||
    <div style="display:flex" class="mb-3">
 | 
			
		||||
      <img width="50" height="50" src="{{ poolStats['logo'] }}"
 | 
			
		||||
      <img width="50" height="50" src="{{ poolStats['logo'] }}" [alt]="poolStats.pool.name + ' mining pool logo'"
 | 
			
		||||
        onError="this.src = './resources/mining-pools/default.svg'" class="mr-3">
 | 
			
		||||
      <h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
 | 
			
		||||
    </div>
 | 
			
		||||
@ -93,10 +93,8 @@
 | 
			
		||||
                  <table class="table table-xs table-data">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                      <tr>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated
 | 
			
		||||
                        </th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
 | 
			
		||||
                        </th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
@ -117,10 +115,8 @@
 | 
			
		||||
                  <table class="table table-xs table-data">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                      <tr>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated
 | 
			
		||||
                        </th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
 | 
			
		||||
                        </th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
@ -142,7 +138,7 @@
 | 
			
		||||
 | 
			
		||||
              <!-- Mined blocks desktop -->
 | 
			
		||||
              <tr *ngIf="!isMobile()" class="taller-row">
 | 
			
		||||
                <td class="label" i18n="mining.mined-blocks">Mined Blocks</td>
 | 
			
		||||
                <td class="label" i18n="mining.mined-blocks">Mined blocks</td>
 | 
			
		||||
                <td class="data">
 | 
			
		||||
                  <table class="table table-xs table-data">
 | 
			
		||||
                    <thead>
 | 
			
		||||
@ -166,7 +162,7 @@
 | 
			
		||||
              <!-- Mined blocks mobile -->
 | 
			
		||||
              <tr *ngIf="isMobile()">
 | 
			
		||||
                <td colspan=2>
 | 
			
		||||
                  <span class="label" i18n="mining.mined-blocks">Mined Blocks</span>
 | 
			
		||||
                  <span class="label" i18n="mining.mined-blocks">Mined blocks</span>
 | 
			
		||||
                  <table class="table table-xs table-data">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                      <tr>
 | 
			
		||||
@ -216,12 +212,10 @@
 | 
			
		||||
        <th class="height" i18n="latest-blocks.height">Height</th>
 | 
			
		||||
        <th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
 | 
			
		||||
        <th class="mined" i18n="latest-blocks.mined">Mined</th>
 | 
			
		||||
        <th class="coinbase text-left" i18n="latest-blocks.coinbasetag">
 | 
			
		||||
          Coinbase Tag</th>
 | 
			
		||||
        <th class="reward text-right" i18n="latest-blocks.reward">
 | 
			
		||||
          Reward</th>
 | 
			
		||||
        <th class="coinbase text-left" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
 | 
			
		||||
        <th class="reward text-right" i18n="latest-blocks.reward">Reward</th>
 | 
			
		||||
        <th class="fees text-right" i18n="latest-blocks.fees">Fees</th>
 | 
			
		||||
        <th class="txs text-right" i18n="latest-blocks.transactions">Txs</th>
 | 
			
		||||
        <th class="txs text-right" i18n="dashboard.txs">TXs</th>
 | 
			
		||||
        <th class="size" i18n="latest-blocks.size">Size</th>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody [style]="isLoading ? 'opacity: 0.75' : ''">
 | 
			
		||||
@ -266,12 +260,10 @@
 | 
			
		||||
        <th class="height" i18n="latest-blocks.height">Height</th>
 | 
			
		||||
        <th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
 | 
			
		||||
        <th class="mined" i18n="latest-blocks.mined">Mined</th>
 | 
			
		||||
        <th class="coinbase text-left" i18n="latest-blocks.coinbasetag">
 | 
			
		||||
          Coinbase Tag</th>
 | 
			
		||||
        <th class="reward text-right" i18n="latest-blocks.reward">
 | 
			
		||||
          Reward</th>
 | 
			
		||||
        <th class="coinbase text-left" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
 | 
			
		||||
        <th class="reward text-right" i18n="latest-blocks.reward">Reward</th>
 | 
			
		||||
        <th class="fees text-right" i18n="latest-blocks.fees">Fees</th>
 | 
			
		||||
        <th class="txs text-right" i18n="latest-blocks.transactions">Txs</th>
 | 
			
		||||
        <th class="txs text-right" i18n="dashboard.txs">TXs</th>
 | 
			
		||||
        <th class="size" i18n="latest-blocks.size">Size</th>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody>
 | 
			
		||||
@ -378,10 +370,8 @@
 | 
			
		||||
                  <table class="table table-xs table-data text-center">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                      <tr>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated
 | 
			
		||||
                        </th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
 | 
			
		||||
                        </th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
@ -406,10 +396,8 @@
 | 
			
		||||
                  <table class="table table-xs table-data text-center">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                      <tr>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated
 | 
			
		||||
                        </th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
 | 
			
		||||
                        </th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
@ -430,13 +418,13 @@
 | 
			
		||||
 | 
			
		||||
              <!-- Mined blocks desktop -->
 | 
			
		||||
              <tr *ngIf="!isMobile()" class="taller-row">
 | 
			
		||||
                <td class="label" i18n="mining.mined-blocks">Mined Blocks</td>
 | 
			
		||||
                <td class="label" i18n="mining.mined-blocks">Mined blocks</td>
 | 
			
		||||
                <td class="data">
 | 
			
		||||
                  <table class="table table-xs table-data text-center">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                      <tr>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="24h">24h</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%">24h</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%">1w</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 26%" i18n="all">All</th>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
@ -457,12 +445,12 @@
 | 
			
		||||
              <!-- Mined blocks mobile -->
 | 
			
		||||
              <tr *ngIf="isMobile()">
 | 
			
		||||
                <td colspan=2>
 | 
			
		||||
                  <span class="label" i18n="mining.mined-blocks">Mined Blocks</span>
 | 
			
		||||
                  <span class="label" i18n="mining.mined-blocks">Mined blocks</span>
 | 
			
		||||
                  <table class="table table-xs table-data text-center">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                      <tr>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 33%" i18n="24h">24h</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 33%">24h</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 37%">1w</th>
 | 
			
		||||
                        <th scope="col" class="block-count-title" style="width: 30%" i18n="all">All</th>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
 | 
			
		||||
  <form [formGroup]="pushTxForm" (submit)="pushTxForm.valid && postTx()" novalidate>
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
      <textarea formControlName="txHash" class="form-control" rows="5" i18n-placeholder="transaction.hex" placeholder="Transaction Hex"></textarea>
 | 
			
		||||
      <textarea formControlName="txHash" class="form-control" rows="5" i18n-placeholder="transaction.hex" placeholder="Transaction hex"></textarea>
 | 
			
		||||
    </div>
 | 
			
		||||
    <button [disabled]="isLoading" type="submit" class="btn btn-primary mr-2" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</button>
 | 
			
		||||
    <p class="red-color d-inline">{{ error }}</p> <a *ngIf="txId" [routerLink]="['/tx/' | relativeUrl, txId]">{{ txId }}</a>
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="mining.average-fee">Average Fee</h5>
 | 
			
		||||
      <h5 class="card-title" i18n="mining.average-fee">Reward Per Tx</h5>
 | 
			
		||||
      <div class="card-text" i18n-ngbTooltip="mining.average-fee"
 | 
			
		||||
        ngbTooltip="Fee paid on average for each transaction in the past 144 blocks" placement="bottom">
 | 
			
		||||
        <div class="fee-text">{{ rewardStats.feePerTx | amountShortener: 2 }}
 | 
			
		||||
@ -57,7 +57,7 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="mining.average-fee">Average Fee</h5>
 | 
			
		||||
      <h5 class="card-title" i18n="mining.average-fee">Reward Per Tx</h5>
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@
 | 
			
		||||
            <table class="table table-borderless table-striped">
 | 
			
		||||
              <tbody>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
 | 
			
		||||
                  <td i18n="block.timestamp">Timestamp</td>
 | 
			
		||||
                  <td>
 | 
			
		||||
                    ‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
 | 
			
		||||
                    <div class="lg-inline">
 | 
			
		||||
@ -209,7 +209,7 @@
 | 
			
		||||
          <table class="table table-borderless table-striped">
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="transaction.size|Transaction Size">Size</td>
 | 
			
		||||
                <td i18n="block.size">Size</td>
 | 
			
		||||
                <td [innerHTML]="'‎' + (tx.size | bytes: 2)"></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
@ -217,7 +217,7 @@
 | 
			
		||||
                <td [innerHTML]="'‎' + (tx.weight / 4 | vbytes: 2)"></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="transaction.weight|Transaction Weight">Weight</td>
 | 
			
		||||
                <td i18n="block.weight">Weight</td>
 | 
			
		||||
                <td [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
@ -235,7 +235,7 @@
 | 
			
		||||
                <td [innerHTML]="'‎' + (tx.locktime | number)"></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="transaction.hex">Transaction Hex</td>
 | 
			
		||||
                <td i18n="transaction.hex">Transaction hex</td>
 | 
			
		||||
                <td><a target="_blank" href="{{ network === '' ? '' : '/' + network }}/api/tx/{{ txId }}/hex"><fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"></fa-icon></a></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
@ -383,7 +383,7 @@
 | 
			
		||||
    <tbody>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
 | 
			
		||||
        <td>{{ tx.fee | number }} <span class="symbol" i18n="transaction.fee.sat|Transaction Fee sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></td>
 | 
			
		||||
        <td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
 | 
			
		||||
 | 
			
		||||
@ -65,7 +65,10 @@
 | 
			
		||||
                      <ng-template #defaultAddress>
 | 
			
		||||
                        <a [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
 | 
			
		||||
                          <span class="d-block d-lg-none">{{ vin.prevout.scriptpubkey_address | shortenString : 16 }}</span>
 | 
			
		||||
                          <span class="d-none d-lg-block">{{ vin.prevout.scriptpubkey_address | shortenString : 35 }}</span>
 | 
			
		||||
                          <span class="d-none d-lg-flex justify-content-start">
 | 
			
		||||
                            <span class="addr-left flex-grow-1" [style]="vin.prevout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vin.prevout.scriptpubkey_address }}</span>
 | 
			
		||||
                            <span *ngIf="vin.prevout.scriptpubkey_address.length > 40" class="addr-right">{{ vin.prevout.scriptpubkey_address | capAddress: 40: 10 }}</span>
 | 
			
		||||
                          </span>
 | 
			
		||||
                        </a>
 | 
			
		||||
                        <div>
 | 
			
		||||
                          <app-address-labels [vin]="vin"></app-address-labels>
 | 
			
		||||
@ -138,7 +141,7 @@
 | 
			
		||||
            </ng-template>
 | 
			
		||||
            <tr *ngIf="tx.vin.length > 12 && tx['@vinLimit']">
 | 
			
		||||
              <td colspan="3" class="text-center">
 | 
			
		||||
                <button class="btn btn-sm btn-primary mt-2" (click)="tx['@vinLimit'] = false;"><span i18n="transactions-list.load-all">Load all</span> ({{ tx.vin.length - 10 }})</button>
 | 
			
		||||
                <button class="btn btn-sm btn-primary mt-2" (click)="tx['@vinLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vin.length - 10 }})</button>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
@ -156,7 +159,10 @@
 | 
			
		||||
                <td>
 | 
			
		||||
                  <a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
 | 
			
		||||
                    <span class="d-block d-lg-none">{{ vout.scriptpubkey_address | shortenString : 16 }}</span>
 | 
			
		||||
                    <span class="d-none d-lg-block">{{ vout.scriptpubkey_address | shortenString : 35 }}</span>
 | 
			
		||||
                    <span class="d-none d-lg-flex justify-content-start">
 | 
			
		||||
                      <span class="addr-left flex-grow-1" [style]="vout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vout.scriptpubkey_address }}</span>
 | 
			
		||||
                      <span *ngIf="vout.scriptpubkey_address.length > 40" class="addr-right">{{ vout.scriptpubkey_address | capAddress: 40: 10 }}</span>
 | 
			
		||||
                    </span>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <app-address-labels [vout]="vout"></app-address-labels>
 | 
			
		||||
@ -232,7 +238,7 @@
 | 
			
		||||
                        <td style="text-align: left;">{{ vout.scriptpubkey }}</td>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                      <tr *ngIf="vout.scriptpubkey_type == 'op_return'">
 | 
			
		||||
                        <td>OP_RETURN <span i18n="transactions-list.vout.scriptpubkey-type.data">data</span></td>
 | 
			
		||||
                        <td>OP_RETURN <span>data</span></td>
 | 
			
		||||
                        <td style="text-align: left;">{{ vout.scriptpubkey_asm | hex2ascii }}</td>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                      <tr *ngIf="vout.scriptpubkey_type">
 | 
			
		||||
@ -246,7 +252,7 @@
 | 
			
		||||
            </ng-template>
 | 
			
		||||
            <tr *ngIf="tx.vout.length > 12 && tx['@voutLimit'] && !outputIndex">
 | 
			
		||||
              <td colspan="3" class="text-center">
 | 
			
		||||
                <button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="transactions-list.load-all">Load all</span> ({{ tx.vout.length - 10 }})</button>
 | 
			
		||||
                <button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vout.length - 10 }})</button>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
 | 
			
		||||
@ -129,3 +129,14 @@ h2 {
 | 
			
		||||
.summary {
 | 
			
		||||
	margin-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.addr-left {
 | 
			
		||||
	font-family: monospace;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
	margin-right: -7px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.addr-right {
 | 
			
		||||
	font-family: monospace;
 | 
			
		||||
}
 | 
			
		||||
@ -22,6 +22,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() showConfirmations = false;
 | 
			
		||||
  @Input() transactionPage = false;
 | 
			
		||||
  @Input() errorUnblinded = false;
 | 
			
		||||
  @Input() paginated = false;
 | 
			
		||||
  @Input() outputIndex: number;
 | 
			
		||||
  @Input() address: string = '';
 | 
			
		||||
 | 
			
		||||
@ -84,6 +85,9 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
    if (!this.transactions || !this.transactions.length) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.paginated) {
 | 
			
		||||
      this.outspends = [];
 | 
			
		||||
    }
 | 
			
		||||
    if (this.outputIndex) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        const assetBoxElements = document.getElementsByClassName('assetBox');
 | 
			
		||||
 | 
			
		||||
@ -121,7 +121,7 @@
 | 
			
		||||
                  <td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
 | 
			
		||||
                  <td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4">
 | 
			
		||||
                    <a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
 | 
			
		||||
                      <img width="20" height="20" src="{{ block.extras.pool['logo'] }}"
 | 
			
		||||
                      <img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
 | 
			
		||||
                        onError="this.src = './resources/mining-pools/default.svg'">
 | 
			
		||||
                      <span class="pool-name">{{ block.extras.pool.name }}</span>
 | 
			
		||||
                    </a>
 | 
			
		||||
@ -136,7 +136,7 @@
 | 
			
		||||
                </tr>
 | 
			
		||||
              </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
            <div class=""><a href="" [routerLink]="[(stateService.env.MINING_DASHBOARD ? '/mining/blocks' : '/blocks') | relativeUrl]" i18n="dashboard.view-all">View all »</a></div>
 | 
			
		||||
            <div class=""><a href="" [routerLink]="[(stateService.env.MINING_DASHBOARD ? '/mining/blocks' : '/blocks') | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -134,6 +134,8 @@
 | 
			
		||||
  table-layout:fixed;
 | 
			
		||||
  tr, td, th {
 | 
			
		||||
    border: 0px;
 | 
			
		||||
    padding-top: 0.71rem !important;
 | 
			
		||||
    padding-bottom: 0.75rem !important;
 | 
			
		||||
  }
 | 
			
		||||
  td {
 | 
			
		||||
    overflow:hidden;
 | 
			
		||||
@ -182,16 +184,15 @@
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  tr, td, th {
 | 
			
		||||
    border: 0px;
 | 
			
		||||
    padding-top: 0.65rem !important;
 | 
			
		||||
    padding-bottom: 0.7rem !important;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-height {
 | 
			
		||||
    width: 15%;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-mined {
 | 
			
		||||
    width: 35%;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
    @media (min-width: 376px) {
 | 
			
		||||
      text-align: left;
 | 
			
		||||
    }
 | 
			
		||||
    text-align: left;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-transaction-count {
 | 
			
		||||
    display: none;
 | 
			
		||||
 | 
			
		||||
@ -4467,6 +4467,14 @@ export const faqData = [
 | 
			
		||||
    title: "What are mining pools?",
 | 
			
		||||
    answer: "Mining pools are groups of miners that combine their computational power in order to increase the probability of finding new blocks."
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: "endpoint",
 | 
			
		||||
    category: "basics",
 | 
			
		||||
    showConditions: bitcoinNetworks,
 | 
			
		||||
    fragment: "what-is-full-mempool",
 | 
			
		||||
    title: "What does it mean for the mempool to be \"full\"?",
 | 
			
		||||
    answer: "<p>When a Bitcoin transaction is made, it is stored in a Bitcoin node's mempool before it is confirmed into a block. When the rate of incoming transactions exceeds the rate transactions are confirmed, the mempool grows in size.</p><p>The default maximum size of a Bitcoin node's mempool is 300MB, so when there are 300MB of transactions in the mempool, we say it's \"full\".</p>"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: "category",
 | 
			
		||||
    category: "help",
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ export class ApiDocsComponent implements OnInit {
 | 
			
		||||
      }
 | 
			
		||||
      window.addEventListener('scroll', function() {
 | 
			
		||||
        that.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative";
 | 
			
		||||
      });
 | 
			
		||||
      }, { passive: true} );
 | 
			
		||||
    }, 1 );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -154,6 +154,10 @@ export class ApiService {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlock$(hash: string): Observable<BlockExtended> {
 | 
			
		||||
    return this.httpClient.get<BlockExtended>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getHistoricalHashrate$(interval: string | undefined): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any[]>(
 | 
			
		||||
        this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,6 @@ export class AssetsService {
 | 
			
		||||
 | 
			
		||||
  getAssetsJson$: Observable<{ array: AssetExtended[]; objects: any}>;
 | 
			
		||||
  getAssetsMinimalJson$: Observable<any>;
 | 
			
		||||
  getMiningPools$: Observable<any>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private httpClient: HttpClient,
 | 
			
		||||
@ -66,6 +65,5 @@ export class AssetsService {
 | 
			
		||||
      }),
 | 
			
		||||
      shareReplay(1),
 | 
			
		||||
    );
 | 
			
		||||
    this.getMiningPools$ = this.httpClient.get(apiBaseUrl + '/resources/pools.json').pipe(shareReplay(1));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,12 @@
 | 
			
		||||
import { Pipe, PipeTransform } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Pipe({ name: 'capAddress' })
 | 
			
		||||
export class CapAddressPipe implements PipeTransform {
 | 
			
		||||
  transform(str: string, cap: number, leftover: number) {
 | 
			
		||||
    if (!str) { return; }
 | 
			
		||||
    if (str.length <= cap) {
 | 
			
		||||
      return str;
 | 
			
		||||
    }
 | 
			
		||||
    return str.slice(-Math.max(cap - str.length, leftover));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -29,6 +29,7 @@ import { MempoolBlocksComponent } from '../components/mempool-blocks/mempool-blo
 | 
			
		||||
import { BlockchainBlocksComponent } from '../components/blockchain-blocks/blockchain-blocks.component';
 | 
			
		||||
import { AmountComponent } from '../components/amount/amount.component';
 | 
			
		||||
import { RouterModule } from '@angular/router';
 | 
			
		||||
import { CapAddressPipe } from './pipes/cap-address-pipe/cap-address-pipe';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
@ -51,6 +52,7 @@ import { RouterModule } from '@angular/router';
 | 
			
		||||
    WuBytesPipe,
 | 
			
		||||
    CeilPipe,
 | 
			
		||||
    ShortenStringPipe,
 | 
			
		||||
    CapAddressPipe,
 | 
			
		||||
    Decimal2HexPipe,
 | 
			
		||||
    FeeRoundingPipe,
 | 
			
		||||
    ColoredPriceDirective,
 | 
			
		||||
@ -103,6 +105,7 @@ import { RouterModule } from '@angular/router';
 | 
			
		||||
    WuBytesPipe,
 | 
			
		||||
    CeilPipe,
 | 
			
		||||
    ShortenStringPipe,
 | 
			
		||||
    CapAddressPipe,
 | 
			
		||||
    Decimal2HexPipe,
 | 
			
		||||
    FeeRoundingPipe,
 | 
			
		||||
    ColoredPriceDirective,
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -82,11 +82,11 @@ pkg install -y zsh sudo git screen curl wget neovim rsync nginx openssl openssh-
 | 
			
		||||
 | 
			
		||||
### Node.js + npm
 | 
			
		||||
 | 
			
		||||
Build Node.js v16.10 and npm v7 from source using `nvm`:
 | 
			
		||||
Build Node.js v16.15 and npm v8 from source using `nvm`:
 | 
			
		||||
```
 | 
			
		||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh
 | 
			
		||||
source $HOME/.zshrc
 | 
			
		||||
nvm install v16.10.0
 | 
			
		||||
nvm install v16.15.0
 | 
			
		||||
nvm alias default node
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -854,7 +854,7 @@ echo "[*] Installing nvm.sh from GitHub"
 | 
			
		||||
osSudo "${MEMPOOL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh'
 | 
			
		||||
 | 
			
		||||
echo "[*] Building NodeJS via nvm.sh"
 | 
			
		||||
osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v16.10.0'
 | 
			
		||||
osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v16.15.0'
 | 
			
		||||
 | 
			
		||||
####################
 | 
			
		||||
# Tor installation #
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user