Merge branch 'master' into simon/npm-audit-fix-24-5
This commit is contained in:
		
						commit
						aac92404d2
					
				@ -15,6 +15,7 @@ import BitcoinApi from './bitcoin/bitcoin-api';
 | 
			
		||||
import { prepareBlock } from '../utils/blocks-utils';
 | 
			
		||||
import BlocksRepository from '../repositories/BlocksRepository';
 | 
			
		||||
import HashratesRepository from '../repositories/HashratesRepository';
 | 
			
		||||
import indexer from '../indexer';
 | 
			
		||||
 | 
			
		||||
class Blocks {
 | 
			
		||||
  private blocks: BlockExtended[] = [];
 | 
			
		||||
@ -23,9 +24,6 @@ class Blocks {
 | 
			
		||||
  private lastDifficultyAdjustmentTime = 0;
 | 
			
		||||
  private previousDifficultyRetarget = 0;
 | 
			
		||||
  private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
 | 
			
		||||
  private blockIndexingStarted = false;
 | 
			
		||||
  public blockIndexingCompleted = false;
 | 
			
		||||
  public reindexFlag = false;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
@ -197,24 +195,15 @@ class Blocks {
 | 
			
		||||
   * [INDEXING] Index all blocks metadata for the mining dashboard
 | 
			
		||||
   */
 | 
			
		||||
  public async $generateBlockDatabase() {
 | 
			
		||||
    if (this.blockIndexingStarted && !this.reindexFlag) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.reindexFlag = false;
 | 
			
		||||
 | 
			
		||||
    const blockchainInfo = await bitcoinClient.getBlockchainInfo();
 | 
			
		||||
    if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.blockIndexingStarted = true;
 | 
			
		||||
    this.blockIndexingCompleted = false;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      let currentBlockHeight = blockchainInfo.blocks;
 | 
			
		||||
 | 
			
		||||
      let indexingBlockAmount = config.MEMPOOL.INDEXING_BLOCKS_AMOUNT;
 | 
			
		||||
      let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, blockchainInfo.blocks);
 | 
			
		||||
      if (indexingBlockAmount <= -1) {
 | 
			
		||||
        indexingBlockAmount = currentBlockHeight + 1;
 | 
			
		||||
      }
 | 
			
		||||
@ -275,14 +264,14 @@ class Blocks {
 | 
			
		||||
      loadingIndicators.setProgress('block-indexing', 100);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('Block indexing failed. Trying again later. Reason: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      this.blockIndexingStarted = false;
 | 
			
		||||
      loadingIndicators.setProgress('block-indexing', 100);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const chainValid = await BlocksRepository.$validateChain();
 | 
			
		||||
    this.reindexFlag = !chainValid;
 | 
			
		||||
    this.blockIndexingCompleted = chainValid;
 | 
			
		||||
    if (!chainValid) {
 | 
			
		||||
      indexer.reindex();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $updateBlocks() {
 | 
			
		||||
@ -299,6 +288,8 @@ class Blocks {
 | 
			
		||||
      logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${config.MEMPOOL.INITIAL_BLOCKS_AMOUNT} recent blocks`);
 | 
			
		||||
      this.currentBlockHeight = blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT;
 | 
			
		||||
      fastForwarded = true;
 | 
			
		||||
      logger.info(`Re-indexing skipped blocks and corresponding hashrates data`);
 | 
			
		||||
      indexer.reindex(); // Make sure to index the skipped blocks #1619
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.lastDifficultyAdjustmentTime) {
 | 
			
		||||
@ -426,10 +417,7 @@ class Blocks {
 | 
			
		||||
    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.
 | 
			
		||||
    // I'll need to fix it before we refactor the block(s) related pages
 | 
			
		||||
  public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight();
 | 
			
		||||
      const returnBlocks: BlockExtended[] = [];
 | 
			
		||||
 | 
			
		||||
@ -4,15 +4,11 @@ import PoolsRepository from '../repositories/PoolsRepository';
 | 
			
		||||
import HashratesRepository from '../repositories/HashratesRepository';
 | 
			
		||||
import bitcoinClient from './bitcoin/bitcoin-client';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import blocks from './blocks';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import loadingIndicators from './loading-indicators';
 | 
			
		||||
import { escape } from 'mysql2';
 | 
			
		||||
 | 
			
		||||
class Mining {
 | 
			
		||||
  hashrateIndexingStarted = false;
 | 
			
		||||
  weeklyHashrateIndexingStarted = false;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -153,14 +149,9 @@ class Mining {
 | 
			
		||||
   * [INDEXING] Generate weekly mining pool hashrate history
 | 
			
		||||
   */
 | 
			
		||||
  public async $generatePoolHashrateHistory(): Promise<void> {
 | 
			
		||||
    if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted || this.weeklyHashrateIndexingStarted) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      this.weeklyHashrateIndexingStarted = true;
 | 
			
		||||
      const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing');
 | 
			
		||||
 | 
			
		||||
      // Run only if:
 | 
			
		||||
@ -168,11 +159,9 @@ class Mining {
 | 
			
		||||
      // * we started a new week (around Monday midnight)
 | 
			
		||||
      const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate();
 | 
			
		||||
      if (!runIndexing) {
 | 
			
		||||
        this.weeklyHashrateIndexingStarted = false;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this.weeklyHashrateIndexingStarted = false;
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -192,6 +181,7 @@ class Mining {
 | 
			
		||||
      const startedAt = new Date().getTime() / 1000;
 | 
			
		||||
      let timer = new Date().getTime() / 1000;
 | 
			
		||||
 | 
			
		||||
      logger.debug(`Indexing weekly mining pool hashrate`);
 | 
			
		||||
      loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
 | 
			
		||||
 | 
			
		||||
      while (toTimestamp > genesisTimestamp) {
 | 
			
		||||
@ -256,7 +246,6 @@ class Mining {
 | 
			
		||||
        ++indexedThisRun;
 | 
			
		||||
        ++totalIndexed;
 | 
			
		||||
      }
 | 
			
		||||
      this.weeklyHashrateIndexingStarted = false;
 | 
			
		||||
      await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate());
 | 
			
		||||
      if (newlyIndexed > 0) {
 | 
			
		||||
        logger.info(`Indexed ${newlyIndexed} pools weekly hashrate`);
 | 
			
		||||
@ -264,7 +253,6 @@ class Mining {
 | 
			
		||||
      loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
 | 
			
		||||
      this.weeklyHashrateIndexingStarted = false;
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -273,22 +261,14 @@ class Mining {
 | 
			
		||||
   * [INDEXING] Generate daily hashrate data
 | 
			
		||||
   */
 | 
			
		||||
  public async $generateNetworkHashrateHistory(): Promise<void> {
 | 
			
		||||
    if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      this.hashrateIndexingStarted = true;
 | 
			
		||||
 | 
			
		||||
      // We only run this once a day around midnight
 | 
			
		||||
      const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing');
 | 
			
		||||
      const now = new Date().getUTCDate();
 | 
			
		||||
      if (now === latestRunDate) {
 | 
			
		||||
        this.hashrateIndexingStarted = false;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this.hashrateIndexingStarted = false;
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -306,6 +286,7 @@ class Mining {
 | 
			
		||||
      const startedAt = new Date().getTime() / 1000;
 | 
			
		||||
      let timer = new Date().getTime() / 1000;
 | 
			
		||||
 | 
			
		||||
      logger.debug(`Indexing daily network hashrate`);
 | 
			
		||||
      loadingIndicators.setProgress('daily-hashrate-indexing', 0);
 | 
			
		||||
 | 
			
		||||
      while (toTimestamp > genesisTimestamp) {
 | 
			
		||||
@ -377,14 +358,12 @@ class Mining {
 | 
			
		||||
      await HashratesRepository.$saveHashrates(hashrates);
 | 
			
		||||
 | 
			
		||||
      await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate());
 | 
			
		||||
      this.hashrateIndexingStarted = false;
 | 
			
		||||
      if (newlyIndexed > 0) {
 | 
			
		||||
        logger.info(`Indexed ${newlyIndexed} day of network hashrate`);
 | 
			
		||||
      }
 | 
			
		||||
      loadingIndicators.setProgress('daily-hashrate-indexing', 100);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      loadingIndicators.setProgress('daily-hashrate-indexing', 100);
 | 
			
		||||
      this.hashrateIndexingStarted = false;
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -27,8 +27,8 @@ import icons from './api/liquid/icons';
 | 
			
		||||
import { Common } from './api/common';
 | 
			
		||||
import mining from './api/mining';
 | 
			
		||||
import HashratesRepository from './repositories/HashratesRepository';
 | 
			
		||||
import BlocksRepository from './repositories/BlocksRepository';
 | 
			
		||||
import poolsUpdater from './tasks/pools-updater';
 | 
			
		||||
import indexer from './indexer';
 | 
			
		||||
 | 
			
		||||
class Server {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
@ -99,7 +99,7 @@ class Server {
 | 
			
		||||
        }
 | 
			
		||||
        await databaseMigration.$initializeOrMigrateDatabase();
 | 
			
		||||
        if (Common.indexingEnabled()) {
 | 
			
		||||
          await this.$resetHashratesIndexingState();
 | 
			
		||||
          await indexer.$resetHashratesIndexingState();
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        throw new Error(e instanceof Error ? e.message : 'Error');
 | 
			
		||||
@ -154,7 +154,7 @@ class Server {
 | 
			
		||||
      await poolsUpdater.updatePoolsJson();
 | 
			
		||||
      await blocks.$updateBlocks();
 | 
			
		||||
      await memPool.$updateMempool();
 | 
			
		||||
      this.$runIndexingWhenReady();
 | 
			
		||||
      indexer.$run();
 | 
			
		||||
 | 
			
		||||
      setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
 | 
			
		||||
      this.currentBackendRetryInterval = 5;
 | 
			
		||||
@ -173,29 +173,6 @@ class Server {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $resetHashratesIndexingState() {
 | 
			
		||||
    try {
 | 
			
		||||
      await HashratesRepository.$setLatestRun('last_hashrates_indexing', 0);
 | 
			
		||||
      await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $runIndexingWhenReady() {
 | 
			
		||||
    if (!Common.indexingEnabled() || mempool.hasPriority()) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await blocks.$generateBlockDatabase();
 | 
			
		||||
      await mining.$generateNetworkHashrateHistory();
 | 
			
		||||
      await mining.$generatePoolHashrateHistory();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Indexing failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setUpWebsocketHandling() {
 | 
			
		||||
    if (this.wss) {
 | 
			
		||||
      websocketHandler.setWebsocketServer(this.wss);
 | 
			
		||||
@ -337,8 +314,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', routes.getBlocks.bind(routes))
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks.bind(routes))
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock);
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
@ -352,8 +329,6 @@ class Server {
 | 
			
		||||
        .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/header', routes.getBlockHeader)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', routes.getBlockTransactions)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', routes.getBlockTransactions)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										52
									
								
								backend/src/indexer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								backend/src/indexer.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
import { Common } from './api/common';
 | 
			
		||||
import blocks from './api/blocks';
 | 
			
		||||
import mempool from './api/mempool';
 | 
			
		||||
import mining from './api/mining';
 | 
			
		||||
import logger from './logger';
 | 
			
		||||
import HashratesRepository from './repositories/HashratesRepository';
 | 
			
		||||
 | 
			
		||||
class Indexer {
 | 
			
		||||
  runIndexer = true;
 | 
			
		||||
  indexerRunning = false;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public reindex() {
 | 
			
		||||
    this.runIndexer = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $run() {
 | 
			
		||||
    if (!Common.indexingEnabled() || this.runIndexer === false ||
 | 
			
		||||
      this.indexerRunning === true || mempool.hasPriority()
 | 
			
		||||
    ) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.runIndexer = false;
 | 
			
		||||
    this.indexerRunning = true;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await blocks.$generateBlockDatabase();
 | 
			
		||||
      await this.$resetHashratesIndexingState();
 | 
			
		||||
      await mining.$generateNetworkHashrateHistory();
 | 
			
		||||
      await mining.$generatePoolHashrateHistory();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this.reindex();
 | 
			
		||||
      logger.err(`Indexer failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.indexerRunning = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $resetHashratesIndexingState() {
 | 
			
		||||
    try {
 | 
			
		||||
      await HashratesRepository.$setLatestRun('last_hashrates_indexing', 0);
 | 
			
		||||
      await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new Indexer();
 | 
			
		||||
@ -81,7 +81,7 @@ export interface TransactionStripped {
 | 
			
		||||
 | 
			
		||||
export interface BlockExtension {
 | 
			
		||||
  totalFees?: number;
 | 
			
		||||
  medianFee?: number; // Actually the median fee rate that we compute ourself
 | 
			
		||||
  medianFee?: number;
 | 
			
		||||
  feeRange?: number[];
 | 
			
		||||
  reward?: number;
 | 
			
		||||
  coinbaseTx?: TransactionMinerInfo;
 | 
			
		||||
 | 
			
		||||
@ -475,9 +475,9 @@ class BlocksRepository {
 | 
			
		||||
  public async $getHistoricalBlockFees(div: number, interval: string | null): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      let query = `SELECT
 | 
			
		||||
        CAST(AVG(height) as INT) as avg_height,
 | 
			
		||||
        CAST(AVG(height) as INT) as avgHeight,
 | 
			
		||||
        CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
 | 
			
		||||
        CAST(AVG(fees) as INT) as avg_fees
 | 
			
		||||
        CAST(AVG(fees) as INT) as avgFees
 | 
			
		||||
        FROM blocks`;
 | 
			
		||||
 | 
			
		||||
      if (interval !== null) {
 | 
			
		||||
@ -500,9 +500,9 @@ class BlocksRepository {
 | 
			
		||||
  public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      let query = `SELECT
 | 
			
		||||
        CAST(AVG(height) as INT) as avg_height,
 | 
			
		||||
        CAST(AVG(height) as INT) as avgHeight,
 | 
			
		||||
        CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
 | 
			
		||||
        CAST(AVG(reward) as INT) as avg_rewards
 | 
			
		||||
        CAST(AVG(reward) as INT) as avgRewards
 | 
			
		||||
        FROM blocks`;
 | 
			
		||||
 | 
			
		||||
      if (interval !== null) {
 | 
			
		||||
@ -525,15 +525,15 @@ class BlocksRepository {
 | 
			
		||||
   public async $getHistoricalBlockFeeRates(div: number, interval: string | null): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      let query = `SELECT
 | 
			
		||||
        CAST(AVG(height) as INT) as avg_height,
 | 
			
		||||
        CAST(AVG(height) as INT) as avgHeight,
 | 
			
		||||
        CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[0]')) as INT) as avg_fee_0,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[1]')) as INT) as avg_fee_10,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[2]')) as INT) as avg_fee_25,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[3]')) as INT) as avg_fee_50,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[4]')) as INT) as avg_fee_75,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[5]')) as INT) as avg_fee_90,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[6]')) as INT) as avg_fee_100
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[0]')) as INT) as avgFee_0,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[1]')) as INT) as avgFee_10,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[2]')) as INT) as avgFee_25,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[3]')) as INT) as avgFee_50,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[4]')) as INT) as avgFee_75,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[5]')) as INT) as avgFee_90,
 | 
			
		||||
        CAST(AVG(JSON_EXTRACT(fee_span, '$[6]')) as INT) as avgFee_100
 | 
			
		||||
      FROM blocks`;
 | 
			
		||||
 | 
			
		||||
      if (interval !== null) {
 | 
			
		||||
@ -556,9 +556,9 @@ class BlocksRepository {
 | 
			
		||||
   public async $getHistoricalBlockSizes(div: number, interval: string | null): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      let query = `SELECT
 | 
			
		||||
        CAST(AVG(height) as INT) as avg_height,
 | 
			
		||||
        CAST(AVG(height) as INT) as avgHeight,
 | 
			
		||||
        CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
 | 
			
		||||
        CAST(AVG(size) as INT) as avg_size
 | 
			
		||||
        CAST(AVG(size) as INT) as avgSize
 | 
			
		||||
      FROM blocks`;
 | 
			
		||||
 | 
			
		||||
      if (interval !== null) {
 | 
			
		||||
@ -581,9 +581,9 @@ class BlocksRepository {
 | 
			
		||||
   public async $getHistoricalBlockWeights(div: number, interval: string | null): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      let query = `SELECT
 | 
			
		||||
        CAST(AVG(height) as INT) as avg_height,
 | 
			
		||||
        CAST(AVG(height) as INT) as avgHeight,
 | 
			
		||||
        CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
 | 
			
		||||
        CAST(AVG(weight) as INT) as avg_weight
 | 
			
		||||
        CAST(AVG(weight) as INT) as avgWeight
 | 
			
		||||
      FROM blocks`;
 | 
			
		||||
 | 
			
		||||
      if (interval !== null) {
 | 
			
		||||
 | 
			
		||||
@ -720,20 +720,22 @@ class Routes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getBlocksExtras(req: Request, res: Response) {
 | 
			
		||||
  public async getBlocks(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(await blocks.$getBlocksExtras(height, 15));
 | 
			
		||||
      if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
 | 
			
		||||
        const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
 | 
			
		||||
        res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
        res.json(await blocks.$getBlocks(height, 15));
 | 
			
		||||
      } else { // Liquid, Bisq
 | 
			
		||||
        return await this.getLegacyBlocks(req, res);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getBlocks(req: Request, res: Response) {
 | 
			
		||||
  public async getLegacyBlocks(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      loadingIndicators.setProgress('blocks', 0);
 | 
			
		||||
 | 
			
		||||
      const returnBlocks: IEsploraApi.Block[] = [];
 | 
			
		||||
      const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight();
 | 
			
		||||
 | 
			
		||||
@ -757,16 +759,15 @@ class Routes {
 | 
			
		||||
          returnBlocks.push(block);
 | 
			
		||||
          nextHash = block.previousblockhash;
 | 
			
		||||
        }
 | 
			
		||||
        loadingIndicators.setProgress('blocks', i / 10 * 100);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(returnBlocks);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      loadingIndicators.setProgress('blocks', 100);
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  
 | 
			
		||||
  public async getBlockTransactions(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@ export function prepareBlock(block: any): BlockExtended {
 | 
			
		||||
    weight: block.weight,
 | 
			
		||||
    previousblockhash: block.previousblockhash,
 | 
			
		||||
    extras: {
 | 
			
		||||
      coinbaseRaw: block.coinbase_raw ?? block.extras.coinbaseRaw,
 | 
			
		||||
      coinbaseRaw: block.coinbase_raw ?? block.extras?.coinbaseRaw,
 | 
			
		||||
      medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee,
 | 
			
		||||
      feeRange: block.feeRange ?? block.fee_span,
 | 
			
		||||
      reward: block.reward ?? block?.extras?.reward,
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,6 @@ import { AddressComponent } from './components/address/address.component';
 | 
			
		||||
import { MasterPageComponent } from './components/master-page/master-page.component';
 | 
			
		||||
import { AboutComponent } from './components/about/about.component';
 | 
			
		||||
import { StatusViewComponent } from './components/status-view/status-view.component';
 | 
			
		||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
 | 
			
		||||
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
 | 
			
		||||
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
 | 
			
		||||
import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component';
 | 
			
		||||
@ -36,19 +35,20 @@ let routes: Routes = [
 | 
			
		||||
        component: MasterPageComponent,
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
            path: 'tx/push',
 | 
			
		||||
            component: PushTransactionComponent,
 | 
			
		||||
            path: 'mining/blocks',
 | 
			
		||||
            redirectTo: 'blocks',
 | 
			
		||||
            pathMatch: 'full'
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'blocks',
 | 
			
		||||
            component: LatestBlocksComponent,
 | 
			
		||||
            path: 'tx/push',
 | 
			
		||||
            component: PushTransactionComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'about',
 | 
			
		||||
            component: AboutComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/blocks',
 | 
			
		||||
            path: 'blocks',
 | 
			
		||||
            component: BlocksList,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
@ -115,6 +115,11 @@ let routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: 'signet',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mining/blocks',
 | 
			
		||||
        redirectTo: 'blocks',
 | 
			
		||||
        pathMatch: 'full'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: '',
 | 
			
		||||
        pathMatch: 'full',
 | 
			
		||||
@ -128,16 +133,12 @@ let routes: Routes = [
 | 
			
		||||
            path: 'tx/push',
 | 
			
		||||
            component: PushTransactionComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'blocks',
 | 
			
		||||
            component: LatestBlocksComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'about',
 | 
			
		||||
            component: AboutComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/blocks',
 | 
			
		||||
            path: 'blocks',
 | 
			
		||||
            component: BlocksList,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
@ -211,19 +212,20 @@ let routes: Routes = [
 | 
			
		||||
    component: MasterPageComponent,
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'tx/push',
 | 
			
		||||
        component: PushTransactionComponent,
 | 
			
		||||
        path: 'mining/blocks',
 | 
			
		||||
        redirectTo: 'blocks',
 | 
			
		||||
        pathMatch: 'full'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'blocks',
 | 
			
		||||
        component: LatestBlocksComponent,
 | 
			
		||||
        path: 'tx/push',
 | 
			
		||||
        component: PushTransactionComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'about',
 | 
			
		||||
        component: AboutComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mining/blocks',
 | 
			
		||||
        path: 'blocks',
 | 
			
		||||
        component: BlocksList,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
@ -321,16 +323,12 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
              path: 'tx/push',
 | 
			
		||||
              component: PushTransactionComponent,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              path: 'blocks',
 | 
			
		||||
              component: LatestBlocksComponent,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              path: 'about',
 | 
			
		||||
              component: AboutComponent,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              path: 'mining/blocks',
 | 
			
		||||
              path: 'blocks',
 | 
			
		||||
              component: BlocksList,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
@ -429,16 +427,12 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
          path: 'tx/push',
 | 
			
		||||
          component: PushTransactionComponent,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'blocks',
 | 
			
		||||
          component: LatestBlocksComponent,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'about',
 | 
			
		||||
          component: AboutComponent,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'mining/blocks',
 | 
			
		||||
          path: 'blocks',
 | 
			
		||||
          component: BlocksList,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -250,6 +250,10 @@
 | 
			
		||||
        <img class="image" src="/resources/profile/marina.svg" />
 | 
			
		||||
        <span>Marina</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://github.com/bitcoin-wallet/bitcoin-wallet/" target="_blank" title="Bitcoin Wallet (Schildbach)">
 | 
			
		||||
        <img class="image" src="/resources/profile/schildbach.svg" />
 | 
			
		||||
        <span>Schildbach</span>
 | 
			
		||||
      </a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -89,13 +89,13 @@ export class BlockFeeRatesGraphComponent implements OnInit {
 | 
			
		||||
                };
 | 
			
		||||
                for (const rate of data.blockFeeRates) {
 | 
			
		||||
                  const timestamp = rate.timestamp * 1000;
 | 
			
		||||
                  seriesData['Min'].push([timestamp, rate.avg_fee_0, rate.avg_height]);
 | 
			
		||||
                  seriesData['10th'].push([timestamp, rate.avg_fee_10, rate.avg_height]);
 | 
			
		||||
                  seriesData['25th'].push([timestamp, rate.avg_fee_25, rate.avg_height]);
 | 
			
		||||
                  seriesData['Median'].push([timestamp, rate.avg_fee_50, rate.avg_height]);
 | 
			
		||||
                  seriesData['75th'].push([timestamp, rate.avg_fee_75, rate.avg_height]);
 | 
			
		||||
                  seriesData['90th'].push([timestamp, rate.avg_fee_90, rate.avg_height]);
 | 
			
		||||
                  seriesData['Max'].push([timestamp, rate.avg_fee_100, rate.avg_height]);
 | 
			
		||||
                  seriesData['Min'].push([timestamp, rate.avgFee_0, rate.avgHeight]);
 | 
			
		||||
                  seriesData['10th'].push([timestamp, rate.avgFee_10, rate.avgHeight]);
 | 
			
		||||
                  seriesData['25th'].push([timestamp, rate.avgFee_25, rate.avgHeight]);
 | 
			
		||||
                  seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]);
 | 
			
		||||
                  seriesData['75th'].push([timestamp, rate.avgFee_75, rate.avgHeight]);
 | 
			
		||||
                  seriesData['90th'].push([timestamp, rate.avgFee_90, rate.avgHeight]);
 | 
			
		||||
                  seriesData['Max'].push([timestamp, rate.avgFee_100, rate.avgHeight]);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Prepare chart
 | 
			
		||||
 | 
			
		||||
@ -71,7 +71,7 @@ export class BlockFeesGraphComponent implements OnInit {
 | 
			
		||||
            .pipe(
 | 
			
		||||
              tap((response) => {
 | 
			
		||||
                this.prepareChartOptions({
 | 
			
		||||
                  blockFees: response.body.map(val => [val.timestamp * 1000, val.avg_fees / 100000000]),
 | 
			
		||||
                  blockFees: response.body.map(val => [val.timestamp * 1000, val.avgFees / 100000000]),
 | 
			
		||||
                });
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
              }),
 | 
			
		||||
 | 
			
		||||
@ -69,7 +69,7 @@ export class BlockRewardsGraphComponent implements OnInit {
 | 
			
		||||
            .pipe(
 | 
			
		||||
              tap((response) => {
 | 
			
		||||
                this.prepareChartOptions({
 | 
			
		||||
                  blockRewards: response.body.map(val => [val.timestamp * 1000, val.avg_rewards / 100000000]),
 | 
			
		||||
                  blockRewards: response.body.map(val => [val.timestamp * 1000, val.avgRewards / 100000000]),
 | 
			
		||||
                });
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
              }),
 | 
			
		||||
 | 
			
		||||
@ -83,8 +83,8 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
 | 
			
		||||
              tap((response) => {
 | 
			
		||||
                const data = response.body;
 | 
			
		||||
                this.prepareChartOptions({
 | 
			
		||||
                  sizes: data.sizes.map(val => [val.timestamp * 1000, val.avg_size / 1000000, val.avg_height]),
 | 
			
		||||
                  weights: data.weights.map(val => [val.timestamp * 1000, val.avg_weight / 1000000, val.avg_height]),
 | 
			
		||||
                  sizes: data.sizes.map(val => [val.timestamp * 1000, val.avgSize / 1000000, val.avgHeight]),
 | 
			
		||||
                  weights: data.weights.map(val => [val.timestamp * 1000, val.avgWeight / 1000000, val.avgHeight]),
 | 
			
		||||
                });
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
              }),
 | 
			
		||||
 | 
			
		||||
@ -197,7 +197,18 @@
 | 
			
		||||
 | 
			
		||||
    <app-transactions-list [transactions]="transactions" [paginated]="true"></app-transactions-list>
 | 
			
		||||
 | 
			
		||||
    <ng-template [ngIf]="isLoadingTransactions">
 | 
			
		||||
    <ng-template [ngIf]="transactionsError">
 | 
			
		||||
      <div class="text-center">
 | 
			
		||||
        <br>
 | 
			
		||||
        <span i18n="error.general-loading-data">Error loading data.</span>
 | 
			
		||||
        <br><br>
 | 
			
		||||
        <i>{{ transactionsError.status }}: {{ transactionsError.error }}</i>
 | 
			
		||||
        <br>
 | 
			
		||||
        <br>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
 | 
			
		||||
    <ng-template [ngIf]="isLoadingTransactions && !transactionsError">
 | 
			
		||||
      <div class="text-center mb-4" class="tx-skeleton">
 | 
			
		||||
 | 
			
		||||
        <ng-container *ngIf="(txsLoadingStatus$ | async) as txsLoadingStatus; else headerLoader">
 | 
			
		||||
@ -271,9 +282,9 @@
 | 
			
		||||
 | 
			
		||||
  <ng-template [ngIf]="error">
 | 
			
		||||
    <div class="text-center">
 | 
			
		||||
      <span i18n="block.error.loading-block-data">Error loading block data.</span>
 | 
			
		||||
      <span i18n="error.general-loading-data">Error loading data.</span>
 | 
			
		||||
      <br><br>
 | 
			
		||||
      <i>{{ error.error }}</i>
 | 
			
		||||
      <i>{{ error.code }}: {{ error.error }}</i>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  showDetails = false;
 | 
			
		||||
  showPreviousBlocklink = true;
 | 
			
		||||
  showNextBlocklink = true;
 | 
			
		||||
  transactionsError: any = null;
 | 
			
		||||
 | 
			
		||||
  subscription: Subscription;
 | 
			
		||||
  keyNavigationSubscription: Subscription;
 | 
			
		||||
@ -152,12 +153,13 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
        this.stateService.markBlock$.next({ blockHeight: this.blockHeight });
 | 
			
		||||
        this.isLoadingTransactions = true;
 | 
			
		||||
        this.transactions = null;
 | 
			
		||||
        this.transactionsError = null;
 | 
			
		||||
      }),
 | 
			
		||||
      debounceTime(300),
 | 
			
		||||
      switchMap((block) => this.electrsApiService.getBlockTransactions$(block.id)
 | 
			
		||||
        .pipe(
 | 
			
		||||
          catchError((err) => {
 | 
			
		||||
            console.log(err);
 | 
			
		||||
            this.transactionsError = err;
 | 
			
		||||
            return of([]);
 | 
			
		||||
        }))
 | 
			
		||||
      ),
 | 
			
		||||
@ -218,9 +220,16 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    const start = (page - 1) * this.itemsPerPage;
 | 
			
		||||
    this.isLoadingTransactions = true;
 | 
			
		||||
    this.transactions = null;
 | 
			
		||||
    this.transactionsError = null;
 | 
			
		||||
    target.scrollIntoView(); // works for chrome
 | 
			
		||||
 | 
			
		||||
    this.electrsApiService.getBlockTransactions$(this.block.id, start)
 | 
			
		||||
      .pipe(
 | 
			
		||||
        catchError((err) => {
 | 
			
		||||
          this.transactionsError = err;
 | 
			
		||||
          return of([]);
 | 
			
		||||
      })
 | 
			
		||||
      )
 | 
			
		||||
     .subscribe((transactions) => {
 | 
			
		||||
        this.transactions = transactions;
 | 
			
		||||
        this.isLoadingTransactions = false;
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
 | 
			
		||||
 | 
			
		||||
<div class="container-xl" [class]="widget ? 'widget' : 'full-height'">
 | 
			
		||||
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !indexingAvailable}">
 | 
			
		||||
  <h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Blocks</h1>
 | 
			
		||||
 | 
			
		||||
  <div class="clearfix"></div>
 | 
			
		||||
@ -8,22 +8,22 @@
 | 
			
		||||
  <div style="min-height: 295px">
 | 
			
		||||
    <table class="table table-borderless">
 | 
			
		||||
      <thead>
 | 
			
		||||
        <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="fees text-right" i18n="latest-blocks.fees" *ngIf="!widget">Fees</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>
 | 
			
		||||
        <th class="height text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}" i18n="latest-blocks.height">Height</th>
 | 
			
		||||
        <th *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}" i18n="mining.pool-name">Pool</th>
 | 
			
		||||
        <th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Timestamp</th>
 | 
			
		||||
        <th class="mined" i18n="latest-blocks.mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Mined</th>
 | 
			
		||||
        <th *ngIf="indexingAvailable" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">Reward</th>
 | 
			
		||||
        <th *ngIf="indexingAvailable && !widget" class="fees text-right" i18n="latest-blocks.fees" [class]="indexingAvailable ? '' : 'legacy'">Fees</th>
 | 
			
		||||
        <th *ngIf="indexingAvailable" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">TXs</th>
 | 
			
		||||
        <th *ngIf="!indexingAvailable" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">Transactions</th>
 | 
			
		||||
        <th class="size" i18n="latest-blocks.size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">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="text-left" [class]="widget ? 'widget' : ''">
 | 
			
		||||
            <a [routerLink]="['/block' | relativeUrl, block.height]">{{ block.height
 | 
			
		||||
              }}</a>
 | 
			
		||||
            <a [routerLink]="['/block' | relativeUrl, block.height]">{{ block.height }}</a>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="pool text-left" [class]="widget ? 'widget' : ''">
 | 
			
		||||
          <td  *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
 | 
			
		||||
            <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'] }}"
 | 
			
		||||
@ -36,19 +36,19 @@
 | 
			
		||||
          <td class="timestamp" *ngIf="!widget">
 | 
			
		||||
            ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="mined" *ngIf="!widget">
 | 
			
		||||
          <td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
 | 
			
		||||
            <app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="reward text-right" [class]="widget ? 'widget' : ''">
 | 
			
		||||
          <td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
 | 
			
		||||
            <app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="fees text-right" *ngIf="!widget">
 | 
			
		||||
          <td *ngIf="indexingAvailable && !widget" class="fees text-right" [class]="indexingAvailable ? '' : 'legacy'">
 | 
			
		||||
            <app-amount [satoshis]="block.extras.totalFees" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="txs text-right" [class]="widget ? 'widget' : ''">
 | 
			
		||||
          <td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
 | 
			
		||||
            {{ block.tx_count | number }}
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="size" *ngIf="!widget">
 | 
			
		||||
          <td class="size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
 | 
			
		||||
            <div class="progress">
 | 
			
		||||
              <div class="progress-bar progress-mempool" role="progressbar"
 | 
			
		||||
                [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div>
 | 
			
		||||
@ -60,29 +60,29 @@
 | 
			
		||||
      <ng-template #skeleton>
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr *ngFor="let item of skeletonLines">
 | 
			
		||||
            <td class="height text-left" [class]="widget ? 'widget' : ''">
 | 
			
		||||
            <td class="height text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 75px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="pool text-left" [class]="widget ? 'widget' : ''">
 | 
			
		||||
            <td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
 | 
			
		||||
              <img width="1" height="25" style="opacity: 0">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 125px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="timestamp" *ngIf="!widget">
 | 
			
		||||
            <td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 150px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="mined" *ngIf="!widget">
 | 
			
		||||
            <td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 125px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="reward text-right" [class]="widget ? 'widget' : ''">
 | 
			
		||||
            <td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 75px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="fees text-right" *ngIf="!widget">
 | 
			
		||||
            <td *ngIf="indexingAvailable && !widget" class="fees text-right" [class]="indexingAvailable ? '' : 'legacy'">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 75px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="txs text-right" [class]="widget ? 'widget' : ''">
 | 
			
		||||
            <td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 75px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="size" *ngIf="!widget">
 | 
			
		||||
            <td class="size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
 | 
			
		||||
              <span class="skeleton-loader"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,9 @@
 | 
			
		||||
  padding-left: 0px;
 | 
			
		||||
  padding-bottom: 0px;
 | 
			
		||||
}
 | 
			
		||||
.container-xl.legacy {
 | 
			
		||||
  max-width: 1140px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
@ -58,6 +61,9 @@ tr, td, th {
 | 
			
		||||
    width: 10%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.height.legacy {
 | 
			
		||||
  width: 15%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.timestamp {
 | 
			
		||||
  width: 18%;
 | 
			
		||||
@ -65,6 +71,9 @@ tr, td, th {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.timestamp.legacy {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mined {
 | 
			
		||||
  width: 13%;
 | 
			
		||||
@ -72,6 +81,12 @@ tr, td, th {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.mined.legacy {
 | 
			
		||||
  width: 15%;
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    display: table-cell;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.txs {
 | 
			
		||||
  padding-right: 40px;
 | 
			
		||||
@ -88,6 +103,10 @@ tr, td, th {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.txs.legacy {
 | 
			
		||||
  padding-right: 80px;
 | 
			
		||||
  width: 10%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fees {
 | 
			
		||||
  width: 10%;
 | 
			
		||||
@ -126,6 +145,12 @@ tr, td, th {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.size.legacy {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    display: table-cell;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Tooltip text */
 | 
			
		||||
.tooltip-custom {
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ export class BlocksList implements OnInit {
 | 
			
		||||
 | 
			
		||||
  blocks$: Observable<any[]> = undefined;
 | 
			
		||||
 | 
			
		||||
  indexingAvailable = false;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  fromBlockHeight = undefined;
 | 
			
		||||
  paginationMaxSize: number;
 | 
			
		||||
@ -35,6 +36,9 @@ export class BlocksList implements OnInit {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' &&
 | 
			
		||||
      this.stateService.env.MINING_DASHBOARD === true);
 | 
			
		||||
 | 
			
		||||
    if (!this.widget) {
 | 
			
		||||
      this.websocketService.want(['blocks']);
 | 
			
		||||
    }
 | 
			
		||||
@ -55,17 +59,19 @@ export class BlocksList implements OnInit {
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
              }),
 | 
			
		||||
              map(blocks => {
 | 
			
		||||
                for (const block of blocks) {
 | 
			
		||||
                  // @ts-ignore: Need to add an extra field for the template
 | 
			
		||||
                  block.extras.pool.logo = `./resources/mining-pools/` +
 | 
			
		||||
                    block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
 | 
			
		||||
                if (this.indexingAvailable) {
 | 
			
		||||
                  for (const block of blocks) {
 | 
			
		||||
                    // @ts-ignore: Need to add an extra field for the template
 | 
			
		||||
                    block.extras.pool.logo = `./resources/mining-pools/` +
 | 
			
		||||
                      block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
                if (this.widget) {
 | 
			
		||||
                  return blocks.slice(0, 6);
 | 
			
		||||
                }
 | 
			
		||||
                return blocks;
 | 
			
		||||
              }),
 | 
			
		||||
              retryWhen(errors => errors.pipe(delayWhen(() => timer(1000))))
 | 
			
		||||
              retryWhen(errors => errors.pipe(delayWhen(() => timer(10000))))
 | 
			
		||||
            )
 | 
			
		||||
          })
 | 
			
		||||
      ),
 | 
			
		||||
@ -81,9 +87,11 @@ export class BlocksList implements OnInit {
 | 
			
		||||
            return blocks[0];
 | 
			
		||||
          }
 | 
			
		||||
          this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1;
 | 
			
		||||
          // @ts-ignore: Need to add an extra field for the template
 | 
			
		||||
          blocks[1][0].extras.pool.logo = `./resources/mining-pools/` +
 | 
			
		||||
            blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
 | 
			
		||||
          if (this.stateService.env.MINING_DASHBOARD) {
 | 
			
		||||
            // @ts-ignore: Need to add an extra field for the template
 | 
			
		||||
            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 ? 6 : 15);
 | 
			
		||||
          return acc;
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
<div style="min-height: 295px">
 | 
			
		||||
<div style="min-height: 335px">
 | 
			
		||||
  <table class="table latest-adjustments">
 | 
			
		||||
    <thead>
 | 
			
		||||
      <tr>
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<div class="mb-3 d-flex menu" style="padding: 0px 35px;">
 | 
			
		||||
<div *ngIf="stateService.env.MINING_DASHBOARD" class="mb-3 d-flex menu" style="padding: 0px 35px;">
 | 
			
		||||
  <a routerLinkActive="active" class="btn btn-primary w-50 mr-1"
 | 
			
		||||
    [routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a>
 | 
			
		||||
  <div ngbDropdown *ngIf="stateService.env.MINING_DASHBOARD" class="w-50">
 | 
			
		||||
  <div ngbDropdown class="w-50">
 | 
			
		||||
    <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]"
 | 
			
		||||
 | 
			
		||||
@ -1,57 +0,0 @@
 | 
			
		||||
<div class="container-xl">
 | 
			
		||||
  <h1 class="float-left" i18n="master-page.blocks">Blocks</h1>
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
  <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
  <table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
 | 
			
		||||
    <thead>
 | 
			
		||||
      <th style="width: 15%;" i18n="latest-blocks.height">Height</th>
 | 
			
		||||
      <th class="d-none d-md-block" style="width: 20%;" i18n="latest-blocks.timestamp">Timestamp</th>
 | 
			
		||||
      <th style="width: 20%;" i18n="latest-blocks.mined">Mined</th>
 | 
			
		||||
      <th class="d-none d-lg-block" style="width: 15%;" i18n="latest-blocks.transactions">Transactions</th>
 | 
			
		||||
      <th style="width: 20%;" i18n="latest-blocks.size">Size</th>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody>
 | 
			
		||||
      <tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
 | 
			
		||||
        <td><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
 | 
			
		||||
        <td class="d-none d-md-block">‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
 | 
			
		||||
        <td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
 | 
			
		||||
        <td class="d-none d-lg-block">{{ block.tx_count | number }}</td>
 | 
			
		||||
        <td>
 | 
			
		||||
          <div class="progress">
 | 
			
		||||
            <div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div>
 | 
			
		||||
            <div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <ng-template [ngIf]="isLoading">
 | 
			
		||||
        <tr *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
 | 
			
		||||
          <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
          <td class="d-none d-md-block"><span class="skeleton-loader"></span></td>
 | 
			
		||||
          <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
          <td class="d-none d-lg-block"><span class="skeleton-loader"></span></td>
 | 
			
		||||
          <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
        </tr>
 | 
			
		||||
      <ng-container *ngIf="(blocksLoadingStatus$ | async) as blocksLoadingStatus">
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td colspan="5">
 | 
			
		||||
          <div class="progress progress-dark">
 | 
			
		||||
            <div class="progress-bar progress-darklight" role="progressbar" [ngStyle]="{'width': blocksLoadingStatus + '%' }"></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
 | 
			
		||||
  <ng-template [ngIf]="error">
 | 
			
		||||
    <div class="text-center">
 | 
			
		||||
      <span>Error loading blocks</span>
 | 
			
		||||
      <br>
 | 
			
		||||
      <i>{{ error.error }}</i>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,14 +0,0 @@
 | 
			
		||||
.progress {
 | 
			
		||||
  background-color: #2d3348;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (min-width: 768px) {
 | 
			
		||||
  .d-md-block {
 | 
			
		||||
      display: table-cell !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@media (min-width: 992px) {
 | 
			
		||||
  .d-lg-block {
 | 
			
		||||
      display: table-cell !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,140 +0,0 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Block } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { Subscription, Observable, merge, of } from 'rxjs';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
import { WebsocketService } from 'src/app/services/websocket.service';
 | 
			
		||||
import { map } from 'rxjs/operators';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-latest-blocks',
 | 
			
		||||
  templateUrl: './latest-blocks.component.html',
 | 
			
		||||
  styleUrls: ['./latest-blocks.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush
 | 
			
		||||
})
 | 
			
		||||
export class LatestBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  network$: Observable<string>;
 | 
			
		||||
  error: any;
 | 
			
		||||
  blocks: any[] = [];
 | 
			
		||||
  blockSubscription: Subscription;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  interval: any;
 | 
			
		||||
  blocksLoadingStatus$: Observable<number>;
 | 
			
		||||
 | 
			
		||||
  latestBlockHeight: number;
 | 
			
		||||
 | 
			
		||||
  heightOfPageUntilBlocks = 150;
 | 
			
		||||
  heightOfBlocksTableChunk = 470;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private cd: ChangeDetectorRef,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
 | 
			
		||||
    this.websocketService.want(['blocks']);
 | 
			
		||||
 | 
			
		||||
    this.network$ = merge(of(''), this.stateService.networkChanged$);
 | 
			
		||||
 | 
			
		||||
    this.blocksLoadingStatus$ = this.stateService.loadingIndicators$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map((indicators) => indicators['blocks'] !== undefined ? indicators['blocks'] : 0)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.blockSubscription = this.stateService.blocks$
 | 
			
		||||
      .subscribe(([block]) => {
 | 
			
		||||
        if (block === null || !this.blocks.length) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.latestBlockHeight = block.height;
 | 
			
		||||
 | 
			
		||||
        if (block.height === this.blocks[0].height) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If we are out of sync, reload the blocks instead
 | 
			
		||||
        if (block.height > this.blocks[0].height + 1) {
 | 
			
		||||
          this.loadInitialBlocks();
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (block.height <= this.blocks[0].height) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.blocks.pop();
 | 
			
		||||
        this.blocks.unshift(block);
 | 
			
		||||
        this.cd.markForCheck();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    this.loadInitialBlocks();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    clearInterval(this.interval);
 | 
			
		||||
    this.blockSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadInitialBlocks() {
 | 
			
		||||
    this.electrsApiService.listBlocks$()
 | 
			
		||||
      .subscribe((blocks) => {
 | 
			
		||||
        this.blocks = blocks;
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
        this.error = undefined;
 | 
			
		||||
 | 
			
		||||
        this.latestBlockHeight = blocks[0].height;
 | 
			
		||||
 | 
			
		||||
        const spaceForBlocks = window.innerHeight - this.heightOfPageUntilBlocks;
 | 
			
		||||
        const chunks = Math.ceil(spaceForBlocks / this.heightOfBlocksTableChunk) - 1;
 | 
			
		||||
        if (chunks > 0) {
 | 
			
		||||
          this.loadMore(chunks);
 | 
			
		||||
        }
 | 
			
		||||
        this.cd.markForCheck();
 | 
			
		||||
      },
 | 
			
		||||
      (error) => {
 | 
			
		||||
        console.log(error);
 | 
			
		||||
        this.error = error;
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
        this.cd.markForCheck();
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadMore(chunks = 0) {
 | 
			
		||||
    if (this.isLoading) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const height = this.blocks[this.blocks.length - 1].height - 1;
 | 
			
		||||
    if (height < 0) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.isLoading = true;
 | 
			
		||||
    this.electrsApiService.listBlocks$(height)
 | 
			
		||||
      .subscribe((blocks) => {
 | 
			
		||||
        this.blocks = this.blocks.concat(blocks);
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
        this.error = undefined;
 | 
			
		||||
 | 
			
		||||
        const chunksLeft = chunks - 1;
 | 
			
		||||
        if (chunksLeft > 0) {
 | 
			
		||||
          this.loadMore(chunksLeft);
 | 
			
		||||
        }
 | 
			
		||||
        this.cd.markForCheck();
 | 
			
		||||
      },
 | 
			
		||||
      (error) => {
 | 
			
		||||
        console.log(error);
 | 
			
		||||
        this.error = error;
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
        this.cd.markForCheck();
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackByBlock(index: number, block: Block) {
 | 
			
		||||
    return block.height;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -51,7 +51,7 @@
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <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]="['/blocks' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -136,7 +136,7 @@
 | 
			
		||||
                </tr>
 | 
			
		||||
              </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
            <div class=""><a href="" [routerLink]="[(stateService.env.MINING_DASHBOARD ? '/mining/blocks' : '/blocks') | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
            <div class=""><a href="" [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -198,9 +198,7 @@
 | 
			
		||||
    display: none;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
    width: 20%;
 | 
			
		||||
    @media (min-width: 376px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
    }
 | 
			
		||||
    display: table-cell;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-size {
 | 
			
		||||
    display: none;
 | 
			
		||||
 | 
			
		||||
@ -3834,34 +3834,34 @@ export const restApiDocsData = [
 | 
			
		||||
          curl: [`1w`],
 | 
			
		||||
          response: `[
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 735644,
 | 
			
		||||
    "avgHeight": 735644,
 | 
			
		||||
    "timestamp": 1652119111,
 | 
			
		||||
    "avg_fees": 24212890
 | 
			
		||||
    "avgFees": 24212890
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 735646,
 | 
			
		||||
    "avgHeight": 735646,
 | 
			
		||||
    "timestamp": 1652120252,
 | 
			
		||||
    "avg_fees": 21655996
 | 
			
		||||
    "avgFees": 21655996
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 735648,
 | 
			
		||||
    "avgHeight": 735648,
 | 
			
		||||
    "timestamp": 1652121214,
 | 
			
		||||
    "avg_fees": 20678859
 | 
			
		||||
    "avgFees": 20678859
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 735649,
 | 
			
		||||
    "avgHeight": 735649,
 | 
			
		||||
    "timestamp": 1652121757,
 | 
			
		||||
    "avg_fees": 21020140
 | 
			
		||||
    "avgFees": 21020140
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 735650,
 | 
			
		||||
    "avgHeight": 735650,
 | 
			
		||||
    "timestamp": 1652122367,
 | 
			
		||||
    "avg_fees": 23064200
 | 
			
		||||
    "avgFees": 23064200
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 735652,
 | 
			
		||||
    "avgHeight": 735652,
 | 
			
		||||
    "timestamp": 1652122893,
 | 
			
		||||
    "avg_fees": 17620723
 | 
			
		||||
    "avgFees": 17620723
 | 
			
		||||
  },
 | 
			
		||||
  ...
 | 
			
		||||
]`
 | 
			
		||||
@ -3872,14 +3872,14 @@ export const restApiDocsData = [
 | 
			
		||||
          curl: [`1w`],
 | 
			
		||||
          response: `[
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 2224253,
 | 
			
		||||
    "avgHeight": 2224253,
 | 
			
		||||
    "timestamp": 1652346420,
 | 
			
		||||
    "avg_fees": 211686
 | 
			
		||||
    "avgFees": 211686
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 2224254,
 | 
			
		||||
    "avgHeight": 2224254,
 | 
			
		||||
    "timestamp": 1652346850,
 | 
			
		||||
    "avg_fees": 2565952
 | 
			
		||||
    "avgFees": 2565952
 | 
			
		||||
  },
 | 
			
		||||
  ...
 | 
			
		||||
]`
 | 
			
		||||
@ -3890,14 +3890,14 @@ export const restApiDocsData = [
 | 
			
		||||
          curl: [`1w`],
 | 
			
		||||
          response: `[
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 89978,
 | 
			
		||||
    "avgHeight": 89978,
 | 
			
		||||
    "timestamp": 1652346573,
 | 
			
		||||
    "avg_fees": 1071
 | 
			
		||||
    "avgFees": 1071
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 89979,
 | 
			
		||||
    "avgHeight": 89979,
 | 
			
		||||
    "timestamp": 1652346970,
 | 
			
		||||
    "avg_fees": 1224
 | 
			
		||||
    "avgFees": 1224
 | 
			
		||||
  },
 | 
			
		||||
  ...
 | 
			
		||||
]`
 | 
			
		||||
@ -3933,29 +3933,29 @@ export const restApiDocsData = [
 | 
			
		||||
          curl: [`1d`],
 | 
			
		||||
          response: `[
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 599992,
 | 
			
		||||
    "avgHeight": 599992,
 | 
			
		||||
    "timestamp": 1571438412,
 | 
			
		||||
    "avg_rewards": 1260530933
 | 
			
		||||
    "avgRewards": 1260530933
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 600000,
 | 
			
		||||
    "avgHeight": 600000,
 | 
			
		||||
    "timestamp": 1571443398,
 | 
			
		||||
    "avg_rewards": 1264314538
 | 
			
		||||
    "avgRewards": 1264314538
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 725441,
 | 
			
		||||
    "avgHeight": 725441,
 | 
			
		||||
    "timestamp": 1646139035,
 | 
			
		||||
    "avg_rewards": 637067563
 | 
			
		||||
    "avgRewards": 637067563
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 725585,
 | 
			
		||||
    "avgHeight": 725585,
 | 
			
		||||
    "timestamp": 1646222444,
 | 
			
		||||
    "avg_rewards": 646519104
 | 
			
		||||
    "avgRewards": 646519104
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 725727,
 | 
			
		||||
    "avgHeight": 725727,
 | 
			
		||||
    "timestamp": 1646308374,
 | 
			
		||||
    "avg_rewards": 638709605
 | 
			
		||||
    "avgRewards": 638709605
 | 
			
		||||
  },
 | 
			
		||||
  ...
 | 
			
		||||
]`
 | 
			
		||||
@ -3966,14 +3966,14 @@ export const restApiDocsData = [
 | 
			
		||||
          curl: [`1d`],
 | 
			
		||||
          response: `[
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 12,
 | 
			
		||||
    "avgHeight": 12,
 | 
			
		||||
    "timestamp": 1296689648,
 | 
			
		||||
    "avg_rewards": 5000000000
 | 
			
		||||
    "avgRewards": 5000000000
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 269,
 | 
			
		||||
    "avgHeight": 269,
 | 
			
		||||
    "timestamp": 1296717674,
 | 
			
		||||
    "avg_rewards": 5000091820
 | 
			
		||||
    "avgRewards": 5000091820
 | 
			
		||||
  },
 | 
			
		||||
  ...
 | 
			
		||||
]`
 | 
			
		||||
@ -3984,14 +3984,14 @@ export const restApiDocsData = [
 | 
			
		||||
          curl: [`1d`],
 | 
			
		||||
          response: `[
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 183,
 | 
			
		||||
    "avgHeight": 183,
 | 
			
		||||
    "timestamp": 1598962247,
 | 
			
		||||
    "avg_rewards": 5000000000
 | 
			
		||||
    "avgRewards": 5000000000
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "avg_height": 576,
 | 
			
		||||
    "avgHeight": 576,
 | 
			
		||||
    "timestamp": 1599047892,
 | 
			
		||||
    "avg_rewards": 5000000000
 | 
			
		||||
    "avgRewards": 5000000000
 | 
			
		||||
  },
 | 
			
		||||
  ...
 | 
			
		||||
]`
 | 
			
		||||
@ -4029,37 +4029,37 @@ export const restApiDocsData = [
 | 
			
		||||
  "oldestIndexedBlockTimestamp": 1571434851,
 | 
			
		||||
  "blockFeeRates": [
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 732152,
 | 
			
		||||
      "avgHeight": 732152,
 | 
			
		||||
      "timestamp": 1650132959,
 | 
			
		||||
      "avg_fee_0": 1,
 | 
			
		||||
      "avg_fee_10": 2,
 | 
			
		||||
      "avg_fee_25": 2,
 | 
			
		||||
      "avg_fee_50": 3,
 | 
			
		||||
      "avg_fee_75": 4,
 | 
			
		||||
      "avg_fee_90": 8,
 | 
			
		||||
      "avg_fee_100": 393
 | 
			
		||||
      "avgFee_0": 1,
 | 
			
		||||
      "avgFee_10": 2,
 | 
			
		||||
      "avgFee_25": 2,
 | 
			
		||||
      "avgFee_50": 3,
 | 
			
		||||
      "avgFee_75": 4,
 | 
			
		||||
      "avgFee_90": 8,
 | 
			
		||||
      "avgFee_100": 393
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 732158,
 | 
			
		||||
      "avgHeight": 732158,
 | 
			
		||||
      "timestamp": 1650134432,
 | 
			
		||||
      "avg_fee_0": 1,
 | 
			
		||||
      "avg_fee_10": 1,
 | 
			
		||||
      "avg_fee_25": 2,
 | 
			
		||||
      "avg_fee_50": 4,
 | 
			
		||||
      "avg_fee_75": 6,
 | 
			
		||||
      "avg_fee_90": 10,
 | 
			
		||||
      "avg_fee_100": 240
 | 
			
		||||
      "avgFee_0": 1,
 | 
			
		||||
      "avgFee_10": 1,
 | 
			
		||||
      "avgFee_25": 2,
 | 
			
		||||
      "avgFee_50": 4,
 | 
			
		||||
      "avgFee_75": 6,
 | 
			
		||||
      "avgFee_90": 10,
 | 
			
		||||
      "avgFee_100": 240
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 732161,
 | 
			
		||||
      "avgHeight": 732161,
 | 
			
		||||
      "timestamp": 1650135818,
 | 
			
		||||
      "avg_fee_0": 1,
 | 
			
		||||
      "avg_fee_10": 1,
 | 
			
		||||
      "avg_fee_25": 1,
 | 
			
		||||
      "avg_fee_50": 2,
 | 
			
		||||
      "avg_fee_75": 5,
 | 
			
		||||
      "avg_fee_90": 8,
 | 
			
		||||
      "avg_fee_100": 251
 | 
			
		||||
      "avgFee_0": 1,
 | 
			
		||||
      "avgFee_10": 1,
 | 
			
		||||
      "avgFee_25": 1,
 | 
			
		||||
      "avgFee_50": 2,
 | 
			
		||||
      "avgFee_75": 5,
 | 
			
		||||
      "avgFee_90": 8,
 | 
			
		||||
      "avgFee_100": 251
 | 
			
		||||
    },
 | 
			
		||||
    ...
 | 
			
		||||
  ]
 | 
			
		||||
@ -4073,26 +4073,26 @@ export const restApiDocsData = [
 | 
			
		||||
  "oldestIndexedBlockTimestamp": 1296688602,
 | 
			
		||||
  "blockFeeRates": [
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 2196306,
 | 
			
		||||
      "avgHeight": 2196306,
 | 
			
		||||
      "timestamp": 1650360168,
 | 
			
		||||
      "avg_fee_0": 1,
 | 
			
		||||
      "avg_fee_10": 1,
 | 
			
		||||
      "avg_fee_25": 1,
 | 
			
		||||
      "avg_fee_50": 1,
 | 
			
		||||
      "avg_fee_75": 2,
 | 
			
		||||
      "avg_fee_90": 28,
 | 
			
		||||
      "avg_fee_100": 2644
 | 
			
		||||
      "avgFee_0": 1,
 | 
			
		||||
      "avgFee_10": 1,
 | 
			
		||||
      "avgFee_25": 1,
 | 
			
		||||
      "avgFee_50": 1,
 | 
			
		||||
      "avgFee_75": 2,
 | 
			
		||||
      "avgFee_90": 28,
 | 
			
		||||
      "avgFee_100": 2644
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 2196308,
 | 
			
		||||
      "avgHeight": 2196308,
 | 
			
		||||
      "timestamp": 1650361209,
 | 
			
		||||
      "avg_fee_0": 1,
 | 
			
		||||
      "avg_fee_10": 1,
 | 
			
		||||
      "avg_fee_25": 1,
 | 
			
		||||
      "avg_fee_50": 4,
 | 
			
		||||
      "avg_fee_75": 12,
 | 
			
		||||
      "avg_fee_90": 65,
 | 
			
		||||
      "avg_fee_100": 102
 | 
			
		||||
      "avgFee_0": 1,
 | 
			
		||||
      "avgFee_10": 1,
 | 
			
		||||
      "avgFee_25": 1,
 | 
			
		||||
      "avgFee_50": 4,
 | 
			
		||||
      "avgFee_75": 12,
 | 
			
		||||
      "avgFee_90": 65,
 | 
			
		||||
      "avgFee_100": 102
 | 
			
		||||
    },
 | 
			
		||||
    ...
 | 
			
		||||
  ]
 | 
			
		||||
@ -4106,26 +4106,26 @@ export const restApiDocsData = [
 | 
			
		||||
  "oldestIndexedBlockTimestamp": 1598918400,
 | 
			
		||||
  "blockFeeRates": [
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 86620,
 | 
			
		||||
      "avgHeight": 86620,
 | 
			
		||||
      "timestamp": 1650360010,
 | 
			
		||||
      "avg_fee_0": 1,
 | 
			
		||||
      "avg_fee_10": 1,
 | 
			
		||||
      "avg_fee_25": 1,
 | 
			
		||||
      "avg_fee_50": 1,
 | 
			
		||||
      "avg_fee_75": 1,
 | 
			
		||||
      "avg_fee_90": 1,
 | 
			
		||||
      "avg_fee_100": 1
 | 
			
		||||
      "avgFee_0": 1,
 | 
			
		||||
      "avgFee_10": 1,
 | 
			
		||||
      "avgFee_25": 1,
 | 
			
		||||
      "avgFee_50": 1,
 | 
			
		||||
      "avgFee_75": 1,
 | 
			
		||||
      "avgFee_90": 1,
 | 
			
		||||
      "avgFee_100": 1
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 86623,
 | 
			
		||||
      "avgHeight": 86623,
 | 
			
		||||
      "timestamp": 1650361330,
 | 
			
		||||
      "avg_fee_0": 1,
 | 
			
		||||
      "avg_fee_10": 1,
 | 
			
		||||
      "avg_fee_25": 1,
 | 
			
		||||
      "avg_fee_50": 1,
 | 
			
		||||
      "avg_fee_75": 1,
 | 
			
		||||
      "avg_fee_90": 1,
 | 
			
		||||
      "avg_fee_100": 1
 | 
			
		||||
      "avgFee_0": 1,
 | 
			
		||||
      "avgFee_10": 1,
 | 
			
		||||
      "avgFee_25": 1,
 | 
			
		||||
      "avgFee_50": 1,
 | 
			
		||||
      "avgFee_75": 1,
 | 
			
		||||
      "avgFee_90": 1,
 | 
			
		||||
      "avgFee_100": 1
 | 
			
		||||
    },
 | 
			
		||||
    ...
 | 
			
		||||
  ]
 | 
			
		||||
@ -4163,47 +4163,47 @@ export const restApiDocsData = [
 | 
			
		||||
          response: `{
 | 
			
		||||
  "sizes": [
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 576650,
 | 
			
		||||
      "avgHeight": 576650,
 | 
			
		||||
      "timestamp": 1558212081,
 | 
			
		||||
      "avg_size": 1271404
 | 
			
		||||
      "avgSize": 1271404
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 576715,
 | 
			
		||||
      "avgHeight": 576715,
 | 
			
		||||
      "timestamp": 1558246272,
 | 
			
		||||
      "avg_size": 1105893
 | 
			
		||||
      "avgSize": 1105893
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 576797,
 | 
			
		||||
      "avgHeight": 576797,
 | 
			
		||||
      "timestamp": 1558289379,
 | 
			
		||||
      "avg_size": 1141071
 | 
			
		||||
      "avgSize": 1141071
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 576885,
 | 
			
		||||
      "avgHeight": 576885,
 | 
			
		||||
      "timestamp": 1558330184,
 | 
			
		||||
      "avg_size": 1108166
 | 
			
		||||
      "avgSize": 1108166
 | 
			
		||||
    },
 | 
			
		||||
    ...
 | 
			
		||||
  ],
 | 
			
		||||
  "weights": [
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 576650,
 | 
			
		||||
      "avgHeight": 576650,
 | 
			
		||||
      "timestamp": 1558212081,
 | 
			
		||||
      "avg_weight": 3994002
 | 
			
		||||
      "avgWeight": 3994002
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 576715,
 | 
			
		||||
      "avgHeight": 576715,
 | 
			
		||||
      "timestamp": 1558246272,
 | 
			
		||||
      "avg_weight": 3756312
 | 
			
		||||
      "avgWeight": 3756312
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 576797,
 | 
			
		||||
      "avgHeight": 576797,
 | 
			
		||||
      "timestamp": 1558289379,
 | 
			
		||||
      "avg_weight": 3719625
 | 
			
		||||
      "avgWeight": 3719625
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 576885,
 | 
			
		||||
      "avgHeight": 576885,
 | 
			
		||||
      "timestamp": 1558330184,
 | 
			
		||||
      "avg_weight": 3631381
 | 
			
		||||
      "avgWeight": 3631381
 | 
			
		||||
    },
 | 
			
		||||
    ...
 | 
			
		||||
  ]
 | 
			
		||||
@ -4216,27 +4216,27 @@ export const restApiDocsData = [
 | 
			
		||||
          response: `{
 | 
			
		||||
  "sizes": [
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 1517188,
 | 
			
		||||
      "avgHeight": 1517188,
 | 
			
		||||
      "timestamp": 1558262730,
 | 
			
		||||
      "avg_size": 25089
 | 
			
		||||
      "avgSize": 25089
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 1517275,
 | 
			
		||||
      "avgHeight": 1517275,
 | 
			
		||||
      "timestamp": 1558290933,
 | 
			
		||||
      "avg_size": 21679
 | 
			
		||||
      "avgSize": 21679
 | 
			
		||||
    },
 | 
			
		||||
    ...
 | 
			
		||||
  ],
 | 
			
		||||
  "weights": [
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 1517188,
 | 
			
		||||
      "avgHeight": 1517188,
 | 
			
		||||
      "timestamp": 1558262730,
 | 
			
		||||
      "avg_weight": 74921
 | 
			
		||||
      "avgWeight": 74921
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 1517275,
 | 
			
		||||
      "avgHeight": 1517275,
 | 
			
		||||
      "timestamp": 1558290933,
 | 
			
		||||
      "avg_weight": 65164
 | 
			
		||||
      "avgWeight": 65164
 | 
			
		||||
    },
 | 
			
		||||
    ...
 | 
			
		||||
  ]
 | 
			
		||||
@ -4249,27 +4249,27 @@ export const restApiDocsData = [
 | 
			
		||||
          response: `{
 | 
			
		||||
  "sizes": [
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 83,
 | 
			
		||||
      "avgHeight": 83,
 | 
			
		||||
      "timestamp": 1598937527,
 | 
			
		||||
      "avg_size": 329
 | 
			
		||||
      "avgSize": 329
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 266,
 | 
			
		||||
      "avgHeight": 266,
 | 
			
		||||
      "timestamp": 1598982991,
 | 
			
		||||
      "avg_size": 330
 | 
			
		||||
      "avgSize": 330
 | 
			
		||||
    },
 | 
			
		||||
    ...
 | 
			
		||||
  ],
 | 
			
		||||
  "weights": [
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 83,
 | 
			
		||||
      "avgHeight": 83,
 | 
			
		||||
      "timestamp": 1598937527,
 | 
			
		||||
      "avg_weight": 1209
 | 
			
		||||
      "avgWeight": 1209
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "avg_height": 266,
 | 
			
		||||
      "avgHeight": 266,
 | 
			
		||||
      "timestamp": 1598982991,
 | 
			
		||||
      "avg_weight": 1212
 | 
			
		||||
      "avgWeight": 1212
 | 
			
		||||
    },
 | 
			
		||||
    ...
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
@ -149,7 +149,7 @@ export class ApiService {
 | 
			
		||||
 | 
			
		||||
  getBlocks$(from: number): Observable<BlockExtended[]> {
 | 
			
		||||
    return this.httpClient.get<BlockExtended[]>(
 | 
			
		||||
      this.apiBaseUrl + this.apiBasePath + `/api/v1/blocks-extras` +
 | 
			
		||||
      this.apiBaseUrl + this.apiBasePath + `/api/v1/blocks` +
 | 
			
		||||
      (from !== undefined ? `/${from}` : ``)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,6 @@ import { TransactionsListComponent } from '../components/transactions-list/trans
 | 
			
		||||
import { BlockComponent } from '../components/block/block.component';
 | 
			
		||||
import { AddressComponent } from '../components/address/address.component';
 | 
			
		||||
import { SearchFormComponent } from '../components/search-form/search-form.component';
 | 
			
		||||
import { LatestBlocksComponent } from '../components/latest-blocks/latest-blocks.component';
 | 
			
		||||
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
 | 
			
		||||
import { FooterComponent } from '../components/footer/footer.component';
 | 
			
		||||
import { TimeSpanComponent } from '../components/time-span/time-span.component';
 | 
			
		||||
@ -113,7 +112,6 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index
 | 
			
		||||
    BlockComponent,
 | 
			
		||||
    TransactionsListComponent,
 | 
			
		||||
    AddressComponent,
 | 
			
		||||
    LatestBlocksComponent,
 | 
			
		||||
    SearchFormComponent,
 | 
			
		||||
    TimeSpanComponent,
 | 
			
		||||
    AddressLabelsComponent,
 | 
			
		||||
@ -208,7 +206,6 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index
 | 
			
		||||
    BlockComponent,
 | 
			
		||||
    TransactionsListComponent,
 | 
			
		||||
    AddressComponent,
 | 
			
		||||
    LatestBlocksComponent,
 | 
			
		||||
    SearchFormComponent,
 | 
			
		||||
    TimeSpanComponent,
 | 
			
		||||
    AddressLabelsComponent,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								frontend/src/resources/profile/schildbach.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/resources/profile/schildbach.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="-0.015 -0.015 1.03 1.03" preserveAspectRatio="xMidYMid"><defs><filter id="b"><feGaussianBlur in="SourceAlpha" result="blur-out" stdDeviation=".5"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="c"><feGaussianBlur in="SourceAlpha" result="blur-out" stdDeviation="1.3"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter><linearGradient id="a" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" style="stop-color:#f9b949"/><stop offset="100%" style="stop-color:#f7931a"/></linearGradient></defs><path d="M63.036 39.741c-4.274 17.143-21.637 27.576-38.782 23.301C7.116 58.768-3.317 41.404.959 24.262 5.23 7.117 22.594-3.317 39.734.957c17.144 4.274 27.576 21.64 23.302 38.784z" style="fill:url(#a)" filter="url(#b)" transform="scale(.01563)"/><path d="M46.1 27.441c.638-4.258-2.604-6.547-7.037-8.074l1.438-5.768-3.511-.875-1.4 5.616c-.923-.23-1.871-.447-2.813-.662l1.41-5.653-3.51-.875-1.438 5.766c-.764-.174-1.514-.346-2.242-.527l.004-.018-4.842-1.209-.934 3.75s2.605.597 2.55.634c1.422.355 1.679 1.296 1.636 2.042l-1.638 6.571c.098.025.225.061.365.117l-.371-.092-2.296 9.205c-.174.432-.615 1.08-1.61.834.036.051-2.551-.637-2.551-.637l-1.743 4.019 4.569 1.139c.85.213 1.683.436 2.503.646l-1.453 5.834 3.507.875 1.439-5.772c.958.26 1.888.5 2.798.726l-1.434 5.745 3.51.875 1.454-5.823c5.987 1.133 10.489.676 12.384-4.739 1.527-4.36-.076-6.875-3.226-8.515 2.294-.529 4.022-2.038 4.483-5.155zM38.08 38.69c-1.085 4.36-8.426 2.003-10.806 1.412l1.928-7.729c2.38.594 10.012 1.77 8.878 6.317zm1.086-11.312c-.99 3.966-7.1 1.951-9.082 1.457l1.748-7.01c1.982.494 8.365 1.416 7.334 5.553z" style="fill:#fff" filter="url(#c)" transform="scale(.01563)"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.7 KiB  | 
@ -4,6 +4,7 @@ slugs=(`curl -sSL https://raw.githubusercontent.com/mempool/mining-pools/master/
 | 
			
		||||
 | 
			
		||||
while true
 | 
			
		||||
do for url in / \
 | 
			
		||||
	'/api/v1/blocks' \
 | 
			
		||||
	'/api/v1/statistics/2h' \
 | 
			
		||||
	'/api/v1/statistics/24h' \
 | 
			
		||||
	'/api/v1/statistics/1w' \
 | 
			
		||||
@ -36,7 +37,6 @@ do for url in / \
 | 
			
		||||
	'/api/v1/mining/hashrate/pools/3y' \
 | 
			
		||||
	'/api/v1/mining/hashrate/pools/all' \
 | 
			
		||||
	'/api/v1/mining/reward-stats/144' \
 | 
			
		||||
	'/api/v1/mining/blocks-extras' \
 | 
			
		||||
	'/api/v1/mining/blocks/fees/24h' \
 | 
			
		||||
	'/api/v1/mining/blocks/fees/3d' \
 | 
			
		||||
	'/api/v1/mining/blocks/fees/1w' \
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user