Merge branch 'master' into regtest-1
This commit is contained in:
		
						commit
						d179a563e4
					
				@ -89,19 +89,19 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddressPrefix(prefix: string): string[] {
 | 
			
		||||
    const found: string[] = [];
 | 
			
		||||
    const found: { [address: string]: string } = {};
 | 
			
		||||
    const mp = mempool.getMempool();
 | 
			
		||||
    for (const tx in mp) {
 | 
			
		||||
      for (const vout of mp[tx].vout) {
 | 
			
		||||
        if (vout.scriptpubkey_address.indexOf(prefix) === 0) {
 | 
			
		||||
          found.push(vout.scriptpubkey_address);
 | 
			
		||||
          if (found.length >= 10) {
 | 
			
		||||
            return found;
 | 
			
		||||
          found[vout.scriptpubkey_address] = '';
 | 
			
		||||
          if (Object.keys(found).length >= 10) {
 | 
			
		||||
            return Object.keys(found);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return found;
 | 
			
		||||
    return Object.keys(found);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $sendRawTransaction(rawTransaction: string): Promise<string> {
 | 
			
		||||
 | 
			
		||||
@ -171,7 +171,7 @@ class Blocks {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Index all blocks metadata for the mining dashboard
 | 
			
		||||
   * [INDEXING] Index all blocks metadata for the mining dashboard
 | 
			
		||||
   */
 | 
			
		||||
  public async $generateBlockDatabase() {
 | 
			
		||||
    if (this.blockIndexingStarted) {
 | 
			
		||||
@ -218,31 +218,28 @@ class Blocks {
 | 
			
		||||
          if (blockHeight < lastBlockToIndex) {
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
          try {
 | 
			
		||||
            ++indexedThisRun;
 | 
			
		||||
            if (++totaIndexed % 100 === 0 || blockHeight === lastBlockToIndex) {
 | 
			
		||||
              const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
 | 
			
		||||
              const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
 | 
			
		||||
              const progress = Math.round(totaIndexed / indexingBlockAmount * 100);
 | 
			
		||||
              const timeLeft = Math.round((indexingBlockAmount - totaIndexed) / blockPerSeconds);
 | 
			
		||||
              logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds} blocks/sec | total: ${totaIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${elapsedSeconds} seconds | left: ~${timeLeft} seconds`);
 | 
			
		||||
            }
 | 
			
		||||
            const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
 | 
			
		||||
            const block = await bitcoinApi.$getBlock(blockHash);
 | 
			
		||||
            const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true);
 | 
			
		||||
            const blockExtended = await this.$getBlockExtended(block, transactions);
 | 
			
		||||
            await blocksRepository.$saveBlockInDatabase(blockExtended);
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            logger.err(`Something went wrong while indexing blocks.` + e);
 | 
			
		||||
          ++indexedThisRun;
 | 
			
		||||
          if (++totaIndexed % 100 === 0 || blockHeight === lastBlockToIndex) {
 | 
			
		||||
            const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
 | 
			
		||||
            const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
 | 
			
		||||
            const progress = Math.round(totaIndexed / indexingBlockAmount * 100);
 | 
			
		||||
            const timeLeft = Math.round((indexingBlockAmount - totaIndexed) / blockPerSeconds);
 | 
			
		||||
            logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds} blocks/sec | total: ${totaIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${elapsedSeconds} seconds | left: ~${timeLeft} seconds`);
 | 
			
		||||
          }
 | 
			
		||||
          const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
 | 
			
		||||
          const block = await bitcoinApi.$getBlock(blockHash);
 | 
			
		||||
          const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true);
 | 
			
		||||
          const blockExtended = await this.$getBlockExtended(block, transactions);
 | 
			
		||||
          await blocksRepository.$saveBlockInDatabase(blockExtended);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        currentBlockHeight -= chunkSize;
 | 
			
		||||
      }
 | 
			
		||||
      logger.info('Block indexing completed');
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('An error occured in $generateBlockDatabase(). Skipping block indexing. ' + e);
 | 
			
		||||
      console.log(e);
 | 
			
		||||
      logger.err('An error occured in $generateBlockDatabase(). Trying again later. ' + e);
 | 
			
		||||
      this.blockIndexingStarted = false;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.blockIndexingCompleted = true;
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import logger from '../logger';
 | 
			
		||||
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 8;
 | 
			
		||||
  private static currentVersion = 9;
 | 
			
		||||
  private queryTimeout = 120000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
 | 
			
		||||
@ -133,6 +133,17 @@ class DatabaseMigration {
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (databaseSchemaVersion < 9 && isBitcoin === true) {
 | 
			
		||||
        logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.'`);
 | 
			
		||||
        await this.$executeQuery(connection, 'TRUNCATE hashrates;'); // Need to re-index
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (databaseSchemaVersion < 10 && isBitcoin === true) {
 | 
			
		||||
        await this.$executeQuery(connection, 'ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      connection.release();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
@ -276,6 +287,10 @@ class DatabaseMigration {
 | 
			
		||||
      queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (version < 9) {
 | 
			
		||||
      queries.push(`INSERT INTO state(name, number, string) VALUES ('last_weekly_hashrates_indexing', 0, NULL)`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return queries;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
 | 
			
		||||
import loadingIndicators from './loading-indicators';
 | 
			
		||||
import bitcoinClient from './bitcoin/bitcoin-client';
 | 
			
		||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
 | 
			
		||||
import rbfCache from './rbf-cache';
 | 
			
		||||
 | 
			
		||||
class Mempool {
 | 
			
		||||
  private static WEBSOCKET_REFRESH_RATE_MS = 10000;
 | 
			
		||||
@ -200,6 +201,17 @@ class Mempool {
 | 
			
		||||
    logger.debug('Mempool updated in ' + time / 1000 + ' seconds');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
 | 
			
		||||
    for (const rbfTransaction in rbfTransactions) {
 | 
			
		||||
      if (this.mempoolCache[rbfTransaction]) {
 | 
			
		||||
        // Store replaced transactions
 | 
			
		||||
        rbfCache.add(rbfTransaction, rbfTransactions[rbfTransaction].txid);
 | 
			
		||||
        // Erase the replaced transactions from the local mempool
 | 
			
		||||
        delete this.mempoolCache[rbfTransaction];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateTxPerSecond() {
 | 
			
		||||
    const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
 | 
			
		||||
    this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import blocks from './blocks';
 | 
			
		||||
 | 
			
		||||
class Mining {
 | 
			
		||||
  hashrateIndexingStarted = false;
 | 
			
		||||
  weeklyHashrateIndexingStarted = false;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
  }
 | 
			
		||||
@ -74,140 +75,205 @@ class Mining {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Return the historical difficulty adjustments and oldest indexed block timestamp
 | 
			
		||||
   * [INDEXING] Generate weekly mining pool hashrate history
 | 
			
		||||
   */
 | 
			
		||||
  public async $getHistoricalDifficulty(interval: string | null): Promise<object> {
 | 
			
		||||
    return await BlocksRepository.$getBlocksDifficulty(interval);
 | 
			
		||||
  }
 | 
			
		||||
  public async $generatePoolHashrateHistory(): Promise<void> {
 | 
			
		||||
    if (!blocks.blockIndexingCompleted || this.weeklyHashrateIndexingStarted) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Return the historical hashrates and oldest indexed block timestamp
 | 
			
		||||
   */
 | 
			
		||||
  public async $getNetworkHistoricalHashrates(interval: string | null): Promise<object> {
 | 
			
		||||
    return await HashratesRepository.$getNetworkDailyHashrate(interval);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Return the historical hashrates and oldest indexed block timestamp for one or all pools
 | 
			
		||||
   */
 | 
			
		||||
   public async $getPoolsHistoricalHashrates(interval: string | null, poolId: number): Promise<object> {
 | 
			
		||||
    return await HashratesRepository.$getPoolsWeeklyHashrate(interval);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Generate daily hashrate data
 | 
			
		||||
   */
 | 
			
		||||
  public async $generateNetworkHashrateHistory(): Promise<void> {
 | 
			
		||||
    // We only run this once a day
 | 
			
		||||
    const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp();
 | 
			
		||||
    // We only run this once a week
 | 
			
		||||
    const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp('last_weekly_hashrates_indexing');
 | 
			
		||||
    const now = new Date().getTime() / 1000;
 | 
			
		||||
    if (now - latestTimestamp < 86400) {
 | 
			
		||||
    if (now - latestTimestamp < 604800) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.hashrateIndexingStarted = true;
 | 
			
		||||
    try {
 | 
			
		||||
      this.weeklyHashrateIndexingStarted = true;
 | 
			
		||||
 | 
			
		||||
    logger.info(`Indexing hashrates`);
 | 
			
		||||
      logger.info(`Indexing mining pools weekly hashrates`);
 | 
			
		||||
 | 
			
		||||
    const totalDayIndexed = (await BlocksRepository.$blockCount(null, null)) / 144;
 | 
			
		||||
    const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
 | 
			
		||||
    let startedAt = new Date().getTime() / 1000;
 | 
			
		||||
    const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
 | 
			
		||||
    const lastMidnight = new Date();
 | 
			
		||||
    lastMidnight.setUTCHours(0); lastMidnight.setUTCMinutes(0); lastMidnight.setUTCSeconds(0); lastMidnight.setUTCMilliseconds(0);
 | 
			
		||||
    let toTimestamp = Math.round(lastMidnight.getTime() / 1000);
 | 
			
		||||
    let indexedThisRun = 0;
 | 
			
		||||
    let totalIndexed = 0;
 | 
			
		||||
      const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
 | 
			
		||||
      const hashrates: any[] = [];
 | 
			
		||||
      const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
 | 
			
		||||
      const lastMidnight = this.getDateMidnight(new Date());
 | 
			
		||||
      let toTimestamp = Math.round((lastMidnight.getTime() - 604800) / 1000);
 | 
			
		||||
 | 
			
		||||
    const hashrates: any[] = [];
 | 
			
		||||
      const totalWeekIndexed = (await BlocksRepository.$blockCount(null, null)) / 1008;
 | 
			
		||||
      let indexedThisRun = 0;
 | 
			
		||||
      let totalIndexed = 0;
 | 
			
		||||
      let startedAt = new Date().getTime() / 1000;
 | 
			
		||||
 | 
			
		||||
    while (toTimestamp > genesisTimestamp) {
 | 
			
		||||
      const fromTimestamp = toTimestamp - 86400;
 | 
			
		||||
      if (indexedTimestamp.includes(fromTimestamp)) {
 | 
			
		||||
        toTimestamp -= 86400;
 | 
			
		||||
        ++totalIndexed;
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      while (toTimestamp > genesisTimestamp) {
 | 
			
		||||
        const fromTimestamp = toTimestamp - 604800;
 | 
			
		||||
 | 
			
		||||
      const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
 | 
			
		||||
        null, fromTimestamp, toTimestamp);
 | 
			
		||||
      if (blockStats.blockCount === 0) { // We are done indexing, no blocks left
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
        // Skip already indexed weeks
 | 
			
		||||
        if (indexedTimestamp.includes(toTimestamp + 1)) {
 | 
			
		||||
          toTimestamp -= 604800;
 | 
			
		||||
          ++totalIndexed;
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      let lastBlockHashrate = 0;
 | 
			
		||||
      lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
 | 
			
		||||
        blockStats.lastBlockHeight);
 | 
			
		||||
        const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
 | 
			
		||||
          null, fromTimestamp, toTimestamp);
 | 
			
		||||
        if (blockStats.blockCount === 0) { // We are done indexing, no blocks left
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      if (totalIndexed % 7 === 0 && !indexedTimestamp.includes(fromTimestamp + 1)) { // Save weekly pools hashrate
 | 
			
		||||
        logger.debug("Indexing weekly hashrates for mining pools");
 | 
			
		||||
        let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp - 604800, fromTimestamp);
 | 
			
		||||
        const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
 | 
			
		||||
          blockStats.lastBlockHeight);
 | 
			
		||||
 | 
			
		||||
        let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp, toTimestamp);
 | 
			
		||||
        const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0);
 | 
			
		||||
        pools = pools.map((pool: any) => {
 | 
			
		||||
          pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
 | 
			
		||||
          pool.share = (pool.blockCount / totalBlocks);
 | 
			
		||||
          return pool;
 | 
			
		||||
        });
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
        for (const pool of pools) {
 | 
			
		||||
          hashrates.push({
 | 
			
		||||
            hashrateTimestamp: fromTimestamp + 1,
 | 
			
		||||
            hashrateTimestamp: toTimestamp + 1,
 | 
			
		||||
            avgHashrate: pool['hashrate'],
 | 
			
		||||
            poolId: pool.poolId,
 | 
			
		||||
            share: pool['share'],
 | 
			
		||||
            type: 'weekly',
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      hashrates.push({
 | 
			
		||||
        hashrateTimestamp: fromTimestamp,
 | 
			
		||||
        avgHashrate: lastBlockHashrate,
 | 
			
		||||
        poolId: null,
 | 
			
		||||
        share: 1,
 | 
			
		||||
        type: 'daily',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (hashrates.length > 10) {
 | 
			
		||||
        await HashratesRepository.$saveHashrates(hashrates);
 | 
			
		||||
        hashrates.length = 0;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
 | 
			
		||||
      if (elapsedSeconds > 5) {
 | 
			
		||||
        const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
 | 
			
		||||
        const formattedDate = new Date(fromTimestamp * 1000).toUTCString();
 | 
			
		||||
        const daysLeft = Math.round(totalDayIndexed - totalIndexed);
 | 
			
		||||
        logger.debug(`Getting hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ~${daysLeft} days left to index`);
 | 
			
		||||
        startedAt = new Date().getTime() / 1000;
 | 
			
		||||
        indexedThisRun = 0;
 | 
			
		||||
      }
 | 
			
		||||
        const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
 | 
			
		||||
        if (elapsedSeconds > 5) {
 | 
			
		||||
          const weeksPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
 | 
			
		||||
          const formattedDate = new Date(fromTimestamp * 1000).toUTCString();
 | 
			
		||||
          const weeksLeft = Math.round(totalWeekIndexed - totalIndexed);
 | 
			
		||||
          logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds} weeks/sec | ~${weeksLeft} weeks left to index`);
 | 
			
		||||
          startedAt = new Date().getTime() / 1000;
 | 
			
		||||
          indexedThisRun = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      toTimestamp -= 86400;
 | 
			
		||||
      ++indexedThisRun;
 | 
			
		||||
      ++totalIndexed;
 | 
			
		||||
        toTimestamp -= 604800;
 | 
			
		||||
        ++indexedThisRun;
 | 
			
		||||
        ++totalIndexed;
 | 
			
		||||
      }
 | 
			
		||||
      this.weeklyHashrateIndexingStarted = false;
 | 
			
		||||
      await HashratesRepository.$setLatestRunTimestamp('last_weekly_hashrates_indexing');
 | 
			
		||||
      logger.info(`Weekly pools hashrate indexing completed`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this.weeklyHashrateIndexingStarted = false;
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * [INDEXING] Generate daily hashrate data
 | 
			
		||||
   */
 | 
			
		||||
  public async $generateNetworkHashrateHistory(): Promise<void> {
 | 
			
		||||
    if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add genesis block manually
 | 
			
		||||
    if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) {
 | 
			
		||||
      hashrates.push({
 | 
			
		||||
        hashrateTimestamp: genesisTimestamp,
 | 
			
		||||
        avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
 | 
			
		||||
        poolId: null,
 | 
			
		||||
        type: 'daily',
 | 
			
		||||
      });
 | 
			
		||||
    // We only run this once a day
 | 
			
		||||
    const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp('last_hashrates_indexing');
 | 
			
		||||
    const now = new Date().getTime() / 1000;
 | 
			
		||||
    if (now - latestTimestamp < 86400) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (hashrates.length > 0) {
 | 
			
		||||
    try {
 | 
			
		||||
      this.hashrateIndexingStarted = true;
 | 
			
		||||
 | 
			
		||||
      logger.info(`Indexing network daily hashrate`);
 | 
			
		||||
 | 
			
		||||
      const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
 | 
			
		||||
      const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
 | 
			
		||||
      const lastMidnight = this.getDateMidnight(new Date());
 | 
			
		||||
      let toTimestamp = Math.round(lastMidnight.getTime() / 1000);
 | 
			
		||||
      const hashrates: any[] = [];
 | 
			
		||||
 | 
			
		||||
      const totalDayIndexed = (await BlocksRepository.$blockCount(null, null)) / 144;
 | 
			
		||||
      let indexedThisRun = 0;
 | 
			
		||||
      let totalIndexed = 0;
 | 
			
		||||
      let startedAt = new Date().getTime() / 1000;
 | 
			
		||||
 | 
			
		||||
      while (toTimestamp > genesisTimestamp) {
 | 
			
		||||
        const fromTimestamp = toTimestamp - 86400;
 | 
			
		||||
 | 
			
		||||
        // Skip already indexed weeks
 | 
			
		||||
        if (indexedTimestamp.includes(fromTimestamp)) {
 | 
			
		||||
          toTimestamp -= 86400;
 | 
			
		||||
          ++totalIndexed;
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
 | 
			
		||||
          null, fromTimestamp, toTimestamp);
 | 
			
		||||
        if (blockStats.blockCount === 0) { // We are done indexing, no blocks left
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
 | 
			
		||||
          blockStats.lastBlockHeight);
 | 
			
		||||
 | 
			
		||||
        hashrates.push({
 | 
			
		||||
          hashrateTimestamp: toTimestamp,
 | 
			
		||||
          avgHashrate: lastBlockHashrate,
 | 
			
		||||
          poolId: null,
 | 
			
		||||
          share: 1,
 | 
			
		||||
          type: 'daily',
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (hashrates.length > 10) {
 | 
			
		||||
          await HashratesRepository.$saveHashrates(hashrates);
 | 
			
		||||
          hashrates.length = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
 | 
			
		||||
        if (elapsedSeconds > 5) {
 | 
			
		||||
          const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
 | 
			
		||||
          const formattedDate = new Date(fromTimestamp * 1000).toUTCString();
 | 
			
		||||
          const daysLeft = Math.round(totalDayIndexed - totalIndexed);
 | 
			
		||||
          logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ~${daysLeft} days left to index`);
 | 
			
		||||
          startedAt = new Date().getTime() / 1000;
 | 
			
		||||
          indexedThisRun = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        toTimestamp -= 86400;
 | 
			
		||||
        ++indexedThisRun;
 | 
			
		||||
        ++totalIndexed;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Add genesis block manually
 | 
			
		||||
      if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) {
 | 
			
		||||
        hashrates.push({
 | 
			
		||||
          hashrateTimestamp: genesisTimestamp,
 | 
			
		||||
          avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
 | 
			
		||||
          poolId: null,
 | 
			
		||||
          type: 'daily',
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await HashratesRepository.$saveHashrates(hashrates);
 | 
			
		||||
    }
 | 
			
		||||
    await HashratesRepository.$setLatestRunTimestamp();
 | 
			
		||||
    this.hashrateIndexingStarted = false;
 | 
			
		||||
 | 
			
		||||
    logger.info(`Hashrates indexing completed`);
 | 
			
		||||
      await HashratesRepository.$setLatestRunTimestamp('last_hashrates_indexing');
 | 
			
		||||
      this.hashrateIndexingStarted = false;
 | 
			
		||||
      logger.info(`Daily network hashrate indexing completed`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this.hashrateIndexingStarted = false;
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getDateMidnight(date: Date): Date {
 | 
			
		||||
    date.setUTCHours(0);
 | 
			
		||||
    date.setUTCMinutes(0);
 | 
			
		||||
    date.setUTCSeconds(0);
 | 
			
		||||
    date.setUTCMilliseconds(0);
 | 
			
		||||
 | 
			
		||||
    return date;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										34
									
								
								backend/src/api/rbf-cache.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								backend/src/api/rbf-cache.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
export interface CachedRbf {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  expires: Date;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class RbfCache {
 | 
			
		||||
  private cache: { [txid: string]: CachedRbf; } = {};
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public add(replacedTxId: string, newTxId: string): void {
 | 
			
		||||
    this.cache[replacedTxId] = {
 | 
			
		||||
      expires: new Date(Date.now() + 1000 * 604800), // 1 week
 | 
			
		||||
      txid: newTxId,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public get(txId: string): CachedRbf | undefined {
 | 
			
		||||
    return this.cache[txId];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private cleanup(): void {
 | 
			
		||||
    const currentDate = new Date();
 | 
			
		||||
    for (const c in this.cache) {
 | 
			
		||||
      if (this.cache[c].expires < currentDate) {
 | 
			
		||||
        delete this.cache[c];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new RbfCache();
 | 
			
		||||
@ -11,6 +11,7 @@ import { Common } from './common';
 | 
			
		||||
import loadingIndicators from './loading-indicators';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
import rbfCache from './rbf-cache';
 | 
			
		||||
 | 
			
		||||
class WebsocketHandler {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
@ -48,29 +49,38 @@ class WebsocketHandler {
 | 
			
		||||
          if (parsedMessage && parsedMessage['track-tx']) {
 | 
			
		||||
            if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
 | 
			
		||||
              client['track-tx'] = parsedMessage['track-tx'];
 | 
			
		||||
              // Client is telling the transaction wasn't found but it might have appeared before we had the time to start watching for it
 | 
			
		||||
              // Client is telling the transaction wasn't found
 | 
			
		||||
              if (parsedMessage['watch-mempool']) {
 | 
			
		||||
                const tx = memPool.getMempool()[client['track-tx']];
 | 
			
		||||
                if (tx) {
 | 
			
		||||
                  if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
                    response['tx'] = tx;
 | 
			
		||||
                const rbfCacheTx = rbfCache.get(client['track-tx']);
 | 
			
		||||
                if (rbfCacheTx) {
 | 
			
		||||
                  response['txReplaced'] = {
 | 
			
		||||
                    txid: rbfCacheTx.txid,
 | 
			
		||||
                  };
 | 
			
		||||
                  client['track-tx'] = null;
 | 
			
		||||
                } else {
 | 
			
		||||
                  // It might have appeared before we had the time to start watching for it
 | 
			
		||||
                  const tx = memPool.getMempool()[client['track-tx']];
 | 
			
		||||
                  if (tx) {
 | 
			
		||||
                    if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
                      response['tx'] = tx;
 | 
			
		||||
                    } else {
 | 
			
		||||
                      // tx.prevout is missing from transactions when in bitcoind mode
 | 
			
		||||
                      try {
 | 
			
		||||
                        const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
 | 
			
		||||
                        response['tx'] = fullTx;
 | 
			
		||||
                      } catch (e) {
 | 
			
		||||
                        logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                  } else {
 | 
			
		||||
                    // tx.prevouts is missing from transactions when in bitcoind mode
 | 
			
		||||
                    try {
 | 
			
		||||
                      const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
 | 
			
		||||
                      const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
 | 
			
		||||
                      response['tx'] = fullTx;
 | 
			
		||||
                    } catch (e) {
 | 
			
		||||
                      logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
                      logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
                      client['track-mempool-tx'] = parsedMessage['track-tx'];
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                } else {
 | 
			
		||||
                  try {
 | 
			
		||||
                    const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
 | 
			
		||||
                    response['tx'] = fullTx;
 | 
			
		||||
                  } catch (e) {
 | 
			
		||||
                    logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
                    client['track-mempool-tx'] = parsedMessage['track-tx'];
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
@ -221,14 +231,10 @@ class WebsocketHandler {
 | 
			
		||||
 | 
			
		||||
    mempoolBlocks.updateMempoolBlocks(newMempool);
 | 
			
		||||
    const mBlocks = mempoolBlocks.getMempoolBlocks();
 | 
			
		||||
    const mempool = memPool.getMempool();
 | 
			
		||||
    const mempoolInfo = memPool.getMempoolInfo();
 | 
			
		||||
    const vBytesPerSecond = memPool.getVBytesPerSecond();
 | 
			
		||||
    const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
 | 
			
		||||
 | 
			
		||||
    for (const rbfTransaction in rbfTransactions) {
 | 
			
		||||
      delete mempool[rbfTransaction];
 | 
			
		||||
    }
 | 
			
		||||
    memPool.handleRbfTransactions(rbfTransactions);
 | 
			
		||||
 | 
			
		||||
    this.wss.clients.forEach(async (client: WebSocket) => {
 | 
			
		||||
      if (client.readyState !== WebSocket.OPEN) {
 | 
			
		||||
@ -331,21 +337,29 @@ class WebsocketHandler {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-tx'] && rbfTransactions[client['track-tx']]) {
 | 
			
		||||
        for (const rbfTransaction in rbfTransactions) {
 | 
			
		||||
          if (client['track-tx'] === rbfTransaction) {
 | 
			
		||||
            const rbfTx = rbfTransactions[rbfTransaction];
 | 
			
		||||
            if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
              try {
 | 
			
		||||
                const fullTx = await transactionUtils.$getTransactionExtended(rbfTransaction, true);
 | 
			
		||||
                response['rbfTransaction'] = fullTx;
 | 
			
		||||
              } catch (e) {
 | 
			
		||||
                logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              response['rbfTransaction'] = rbfTx;
 | 
			
		||||
      if (client['track-tx']) {
 | 
			
		||||
        const outspends: object = {};
 | 
			
		||||
        newTransactions.forEach((tx) => tx.vin.forEach((vin, i) => {
 | 
			
		||||
          if (vin.txid === client['track-tx']) {
 | 
			
		||||
            outspends[vin.vout] = {
 | 
			
		||||
              vin: i,
 | 
			
		||||
              txid: tx.txid,
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        if (Object.keys(outspends).length) {
 | 
			
		||||
          response['utxoSpent'] = outspends;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (rbfTransactions[client['track-tx']]) {
 | 
			
		||||
          for (const rbfTransaction in rbfTransactions) {
 | 
			
		||||
            if (client['track-tx'] === rbfTransaction) {
 | 
			
		||||
              response['rbfTransaction'] = {
 | 
			
		||||
                txid: rbfTransactions[rbfTransaction].txid,
 | 
			
		||||
              };
 | 
			
		||||
              break;
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@ -405,7 +419,6 @@ class WebsocketHandler {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-tx'] && txIds.indexOf(client['track-tx']) > -1) {
 | 
			
		||||
        client['track-tx'] = null;
 | 
			
		||||
        response['txConfirmed'] = true;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ export class DB {
 | 
			
		||||
    password: config.DATABASE.PASSWORD,
 | 
			
		||||
    connectionLimit: 10,
 | 
			
		||||
    supportBigNumbers: true,
 | 
			
		||||
    timezone: '+00:00',
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -96,8 +96,8 @@ class Server {
 | 
			
		||||
          await Common.sleep(5000);
 | 
			
		||||
          await databaseMigration.$truncateIndexedData(tables);
 | 
			
		||||
        }
 | 
			
		||||
        await this.$resetHashratesIndexingState();
 | 
			
		||||
        await databaseMigration.$initializeOrMigrateDatabase();
 | 
			
		||||
        await this.$resetHashratesIndexingState();
 | 
			
		||||
        await poolsParser.migratePoolsJson();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        throw new Error(e instanceof Error ? e.message : 'Error');
 | 
			
		||||
@ -167,7 +167,8 @@ class Server {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $resetHashratesIndexingState() {
 | 
			
		||||
    return await HashratesRepository.$setLatestRunTimestamp(0);    
 | 
			
		||||
    await HashratesRepository.$setLatestRunTimestamp('last_hashrates_indexing', 0);
 | 
			
		||||
    await HashratesRepository.$setLatestRunTimestamp('last_weekly_hashrates_indexing', 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $runIndexingWhenReady() {
 | 
			
		||||
@ -176,8 +177,9 @@ class Server {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await blocks.$generateBlockDatabase();
 | 
			
		||||
      await mining.$generateNetworkHashrateHistory();
 | 
			
		||||
      blocks.$generateBlockDatabase();
 | 
			
		||||
      mining.$generateNetworkHashrateHistory();
 | 
			
		||||
      mining.$generatePoolHashrateHistory();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Unable to run indexing right now, trying again later. ` + e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -53,15 +53,17 @@ class BlocksRepository {
 | 
			
		||||
 | 
			
		||||
      // logger.debug(query);
 | 
			
		||||
      await connection.query(query, params);
 | 
			
		||||
      connection.release();
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      if (e.errno === 1062) { // ER_DUP_ENTRY
 | 
			
		||||
        logger.debug(`$saveBlockInDatabase() - Block ${block.height} has already been indexed, ignoring`);
 | 
			
		||||
      } else {
 | 
			
		||||
        connection.release();
 | 
			
		||||
        logger.err('$saveBlockInDatabase() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
        throw e;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connection.release();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -73,20 +75,26 @@ class BlocksRepository {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows]: any[] = await connection.query(`
 | 
			
		||||
      SELECT height
 | 
			
		||||
      FROM blocks
 | 
			
		||||
      WHERE height <= ? AND height >= ?
 | 
			
		||||
      ORDER BY height DESC;
 | 
			
		||||
    `, [startHeight, endHeight]);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await connection.query(`
 | 
			
		||||
        SELECT height
 | 
			
		||||
        FROM blocks
 | 
			
		||||
        WHERE height <= ? AND height >= ?
 | 
			
		||||
        ORDER BY height DESC;
 | 
			
		||||
      `, [startHeight, endHeight]);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    const indexedBlockHeights: number[] = [];
 | 
			
		||||
    rows.forEach((row: any) => { indexedBlockHeights.push(row.height); });
 | 
			
		||||
    const seekedBlocks: number[] = Array.from(Array(startHeight - endHeight + 1).keys(), n => n + endHeight).reverse();
 | 
			
		||||
    const missingBlocksHeights = seekedBlocks.filter(x => indexedBlockHeights.indexOf(x) === -1);
 | 
			
		||||
      const indexedBlockHeights: number[] = [];
 | 
			
		||||
      rows.forEach((row: any) => { indexedBlockHeights.push(row.height); });
 | 
			
		||||
      const seekedBlocks: number[] = Array.from(Array(startHeight - endHeight + 1).keys(), n => n + endHeight).reverse();
 | 
			
		||||
      const missingBlocksHeights = seekedBlocks.filter(x => indexedBlockHeights.indexOf(x) === -1);
 | 
			
		||||
 | 
			
		||||
    return missingBlocksHeights;
 | 
			
		||||
      return missingBlocksHeights;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$getMissingBlocksBetweenHeights() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -111,10 +119,16 @@ class BlocksRepository {
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query, params);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows] = await connection.query(query, params);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    return <EmptyBlocks[]>rows;
 | 
			
		||||
      return <EmptyBlocks[]>rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$getEmptyBlocks() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -143,10 +157,16 @@ class BlocksRepository {
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query, params);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows] = await connection.query(query, params);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    return <number>rows[0].blockCount;
 | 
			
		||||
      return <number>rows[0].blockCount;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$blockCount() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -177,10 +197,16 @@ class BlocksRepository {
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query, params);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows] = await connection.query(query, params);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    return <number>rows[0];
 | 
			
		||||
      return <number>rows[0];
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$blockCountBetweenTimestamp() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -194,23 +220,26 @@ class BlocksRepository {
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows]: any[] = await connection.query(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await connection.query(query);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    if (rows.length <= 0) {
 | 
			
		||||
      return -1;
 | 
			
		||||
      if (rows.length <= 0) {
 | 
			
		||||
        return -1;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return <number>rows[0].blockTimestamp;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$oldestBlockTimestamp() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return <number>rows[0].blockTimestamp;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get blocks mined by a specific mining pool
 | 
			
		||||
   */
 | 
			
		||||
  public async $getBlocksByPool(
 | 
			
		||||
    poolId: number,
 | 
			
		||||
    startHeight: number | null = null
 | 
			
		||||
  ): Promise<object[]> {
 | 
			
		||||
  public async $getBlocksByPool(poolId: number, startHeight: number | null = null): Promise<object[]> {
 | 
			
		||||
    const params: any[] = [];
 | 
			
		||||
    let query = `SELECT height, hash as id, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, reward
 | 
			
		||||
      FROM blocks
 | 
			
		||||
@ -227,14 +256,20 @@ class BlocksRepository {
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query, params);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows] = await connection.query(query, params);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    for (const block of <object[]>rows) {
 | 
			
		||||
      delete block['blockTimestamp'];
 | 
			
		||||
      for (const block of <object[]>rows) {
 | 
			
		||||
        delete block['blockTimestamp'];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return <object[]>rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$getBlocksByPool() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return <object[]>rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -242,19 +277,25 @@ class BlocksRepository {
 | 
			
		||||
   */
 | 
			
		||||
   public async $getBlockByHeight(height: number): Promise<object | null> {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows]: any[] = await connection.query(`
 | 
			
		||||
      SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.addresses as pool_addresses, pools.regexes as pool_regexes
 | 
			
		||||
      FROM blocks
 | 
			
		||||
      JOIN pools ON blocks.pool_id = pools.id
 | 
			
		||||
      WHERE height = ${height};
 | 
			
		||||
    `);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await connection.query(`
 | 
			
		||||
        SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.addresses as pool_addresses, pools.regexes as pool_regexes
 | 
			
		||||
        FROM blocks
 | 
			
		||||
        JOIN pools ON blocks.pool_id = pools.id
 | 
			
		||||
        WHERE height = ${height};
 | 
			
		||||
      `);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    if (rows.length <= 0) {
 | 
			
		||||
      return null;
 | 
			
		||||
      if (rows.length <= 0) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return rows[0];
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$getBlockByHeight() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return rows[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -297,21 +338,34 @@ class BlocksRepository {
 | 
			
		||||
      ORDER BY t.height
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    const [rows]: any[] = await connection.query(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await connection.query(query);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    for (let row of rows) {
 | 
			
		||||
      delete row['rn'];
 | 
			
		||||
      for (let row of rows) {
 | 
			
		||||
        delete row['rn'];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$getBlocksDifficulty() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getOldestIndexedBlockHeight(): Promise<number> {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    return rows[0].minHeight;
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
      return rows[0].minHeight;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$getOldestIndexedBlockHeight() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,10 @@ class HashratesRepository {
 | 
			
		||||
   * Save indexed block data in the database
 | 
			
		||||
   */
 | 
			
		||||
  public async $saveHashrates(hashrates: any) {
 | 
			
		||||
    if (hashrates.length === 0) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let query = `INSERT INTO
 | 
			
		||||
      hashrates(hashrate_timestamp, avg_hashrate, pool_id, share, type) VALUES`;
 | 
			
		||||
 | 
			
		||||
@ -20,11 +24,12 @@ class HashratesRepository {
 | 
			
		||||
    try {
 | 
			
		||||
      // logger.debug(query);
 | 
			
		||||
      await connection.query(query);
 | 
			
		||||
      connection.release();
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$saveHashrateInDatabase() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connection.release();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
 | 
			
		||||
@ -46,10 +51,33 @@ class HashratesRepository {
 | 
			
		||||
 | 
			
		||||
    query += ` ORDER by hashrate_timestamp`;
 | 
			
		||||
 | 
			
		||||
    const [rows]: any[] = await connection.query(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await connection.query(query);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    return rows;
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$getNetworkDailyHashrate() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getWeeklyHashrateTimestamps(): Promise<number[]> {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
 | 
			
		||||
    const query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp FROM hashrates where type = 'weekly' GROUP BY hashrate_timestamp`;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await connection.query(query);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
      return rows.map(row => row.timestamp);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$getWeeklyHashrateTimestamps() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -76,26 +104,44 @@ class HashratesRepository {
 | 
			
		||||
 | 
			
		||||
    query += ` ORDER by hashrate_timestamp, FIELD(pool_id, ${topPoolsId})`;
 | 
			
		||||
 | 
			
		||||
    const [rows]: any[] = await connection.query(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await connection.query(query);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    return rows;
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$getPoolsWeeklyHashrate() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $setLatestRunTimestamp(val: any = null) {
 | 
			
		||||
  public async $setLatestRunTimestamp(key: string, val: any = null) {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const query = `UPDATE state SET number = ? WHERE name = 'last_hashrates_indexing'`;
 | 
			
		||||
    const query = `UPDATE state SET number = ? WHERE name = ?`;
 | 
			
		||||
 | 
			
		||||
    await connection.query<any>(query, (val === null) ? [Math.round(new Date().getTime() / 1000)] : [val]);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      await connection.query<any>(query, (val === null) ? [Math.round(new Date().getTime() / 1000), key] : [val, key]);
 | 
			
		||||
      connection.release();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getLatestRunTimestamp(): Promise<number> {
 | 
			
		||||
  public async $getLatestRunTimestamp(key: string): Promise<number> {
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const query = `SELECT number FROM state WHERE name = 'last_hashrates_indexing'`;
 | 
			
		||||
    const [rows] = await connection.query<any>(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    return rows[0]['number'];
 | 
			
		||||
    const query = `SELECT number FROM state WHERE name = ?`;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows] = await connection.query<any>(query, [key]);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
      return rows[0]['number'];
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$setLatestRunTimestamp() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -43,26 +43,38 @@ class PoolsRepository {
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows] = await connection.query(query);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    return <PoolInfo[]>rows;
 | 
			
		||||
      return <PoolInfo[]>rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$getPoolsInfo() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get basic pool info and block count between two timestamp
 | 
			
		||||
   */
 | 
			
		||||
   public async $getPoolsInfoBetween(from: number, to: number): Promise<PoolInfo[]> {
 | 
			
		||||
    let query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName
 | 
			
		||||
    const query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName
 | 
			
		||||
      FROM pools
 | 
			
		||||
      LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?)
 | 
			
		||||
      GROUP BY pools.id`;
 | 
			
		||||
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query, [from, to]);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows] = await connection.query(query, [from, to]);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    return <PoolInfo[]>rows;
 | 
			
		||||
      return <PoolInfo[]>rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$getPoolsInfoBetween() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -76,13 +88,19 @@ class PoolsRepository {
 | 
			
		||||
 | 
			
		||||
    // logger.debug(query);
 | 
			
		||||
    const connection = await DB.pool.getConnection();
 | 
			
		||||
    const [rows] = await connection.query(query, [poolId]);
 | 
			
		||||
    connection.release();
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows] = await connection.query(query, [poolId]);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
    rows[0].regexes = JSON.parse(rows[0].regexes);
 | 
			
		||||
    rows[0].addresses = JSON.parse(rows[0].addresses);
 | 
			
		||||
      rows[0].regexes = JSON.parse(rows[0].regexes);
 | 
			
		||||
      rows[0].addresses = JSON.parse(rows[0].addresses);
 | 
			
		||||
 | 
			
		||||
    return rows[0];
 | 
			
		||||
      return rows[0];
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      connection.release();
 | 
			
		||||
      logger.err('$getPool() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ import miningStats from './api/mining';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import mining from './api/mining';
 | 
			
		||||
import BlocksRepository from './repositories/BlocksRepository';
 | 
			
		||||
import HashratesRepository from './repositories/HashratesRepository';
 | 
			
		||||
 | 
			
		||||
class Routes {
 | 
			
		||||
  constructor() {}
 | 
			
		||||
@ -576,7 +577,7 @@ class Routes {
 | 
			
		||||
 | 
			
		||||
  public async $getHistoricalDifficulty(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const stats = await mining.$getHistoricalDifficulty(req.params.interval ?? null);
 | 
			
		||||
      const stats = await BlocksRepository.$getBlocksDifficulty(req.params.interval ?? null);
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
			
		||||
@ -588,7 +589,7 @@ class Routes {
 | 
			
		||||
 | 
			
		||||
  public async $getPoolsHistoricalHashrate(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const hashrates = await mining.$getPoolsHistoricalHashrates(req.params.interval ?? null, parseInt(req.params.poolId, 10));
 | 
			
		||||
      const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval ?? null);
 | 
			
		||||
      const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
@ -604,8 +605,8 @@ class Routes {
 | 
			
		||||
 | 
			
		||||
  public async $getHistoricalHashrate(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const hashrates = await mining.$getNetworkHistoricalHashrates(req.params.interval ?? null);
 | 
			
		||||
      const difficulty = await mining.$getHistoricalDifficulty(req.params.interval ?? null);
 | 
			
		||||
      const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval ?? null);
 | 
			
		||||
      const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval ?? null);
 | 
			
		||||
      const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								contributors/bosch-0.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/bosch-0.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
 | 
			
		||||
 | 
			
		||||
Signed: Bosch-0
 | 
			
		||||
@ -74,6 +74,7 @@ import { HashrateChartComponent } from './components/hashrate-chart/hashrate-cha
 | 
			
		||||
import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component';
 | 
			
		||||
import { MiningStartComponent } from './components/mining-start/mining-start.component';
 | 
			
		||||
import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
 | 
			
		||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
@ -154,6 +155,7 @@ import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
 | 
			
		||||
    SeoService,
 | 
			
		||||
    StorageService,
 | 
			
		||||
    LanguageService,
 | 
			
		||||
    ShortenStringPipe,
 | 
			
		||||
    { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
 | 
			
		||||
  ],
 | 
			
		||||
  bootstrap: [AppComponent]
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
 | 
			
		||||
<div *ngIf="showTitle" class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
 | 
			
		||||
<div class="card-wrapper">
 | 
			
		||||
  <div class="card">
 | 
			
		||||
    <div class="card-body more-padding">
 | 
			
		||||
@ -47,7 +47,7 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="item" *ngIf="showHalving">
 | 
			
		||||
          <h5 class="card-title" i18n="difficulty-box.next-halving">Next halving</h5>
 | 
			
		||||
          <h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
 | 
			
		||||
          <div class="card-text">
 | 
			
		||||
            <ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
 | 
			
		||||
            <ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
 | 
			
		||||
 | 
			
		||||
@ -28,8 +28,9 @@ export class DifficultyComponent implements OnInit {
 | 
			
		||||
  isLoadingWebSocket$: Observable<boolean>;
 | 
			
		||||
  difficultyEpoch$: Observable<EpochProgress>;
 | 
			
		||||
 | 
			
		||||
  @Input() showProgress: boolean = true;
 | 
			
		||||
  @Input() showHalving: boolean = false;
 | 
			
		||||
  @Input() showProgress = true;
 | 
			
		||||
  @Input() showHalving = false;
 | 
			
		||||
  @Input() showTitle = true;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
@ -97,7 +98,7 @@ export class DifficultyComponent implements OnInit {
 | 
			
		||||
            colorPreviousAdjustments = '#ffffff66';
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const blocksUntilHalving = block.height % 210000;
 | 
			
		||||
          const blocksUntilHalving = 210000 - (block.height % 210000);
 | 
			
		||||
          const timeUntilHalving = (blocksUntilHalving * timeAvgMins * 60 * 1000) + (now * 1000);
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<div [class]="widget === false ? 'full-container' : ''">
 | 
			
		||||
 | 
			
		||||
  <div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
 | 
			
		||||
  <div *ngIf="!tableOnly" class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
 | 
			
		||||
    <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as hashrates">
 | 
			
		||||
      <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
 | 
			
		||||
        <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90">
 | 
			
		||||
@ -25,34 +25,29 @@
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="hashrateObservable$ | async" [class]="!widget ? 'chart' : 'chart-widget'"
 | 
			
		||||
  <div *ngIf="!tableOnly" [class]="!widget ? 'chart' : 'chart-widget'"
 | 
			
		||||
    echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
 | 
			
		||||
  <div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
    <div class="spinner-border text-light"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
  <!-- <div class="mt-3" *ngIf="!widget">
 | 
			
		||||
    <table class="table table-borderless table-sm text-center">
 | 
			
		||||
      <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
          <th i18n="mining.rank">Block</th>
 | 
			
		||||
          <th class="d-none d-md-block" i18n="block.timestamp">Timestamp</th>
 | 
			
		||||
          <th i18n="mining.adjusted">Adjusted</th>
 | 
			
		||||
          <th i18n="mining.difficulty">Difficulty</th>
 | 
			
		||||
          <th i18n="mining.change">Change</th>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody *ngIf="(hashrateObservable$ | async) as data">
 | 
			
		||||
        <tr *ngFor="let diffChange of data.difficulty">
 | 
			
		||||
          <td><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
 | 
			
		||||
          <td class="d-none d-md-block">‎{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
 | 
			
		||||
          <td><app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since></td>
 | 
			
		||||
          <td class="d-none d-md-block">{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}</td>
 | 
			
		||||
          <td class="d-block d-md-none">{{ diffChange.difficultyShorten }}</td>
 | 
			
		||||
          <td [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
  </div> -->
 | 
			
		||||
  <table *ngIf="tableOnly" class="table latest-transactions" style="min-height: 295px">
 | 
			
		||||
    <thead>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th class="d-none d-md-block" i18n="block.height">Height</th>
 | 
			
		||||
        <th i18n="mining.adjusted" class="text-left">Adjusted</th>
 | 
			
		||||
        <th i18n="mining.difficulty" class="text-right">Difficulty</th>
 | 
			
		||||
        <th i18n="mining.change" class="text-right">Change</th>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody *ngIf="(hashrateObservable$ | async) as data">
 | 
			
		||||
      <tr *ngFor="let diffChange of data.difficulty">
 | 
			
		||||
        <td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
 | 
			
		||||
        <td class="text-left"><app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since></td>
 | 
			
		||||
        <td class="text-right">{{ diffChange.difficultyShorten }}</td>
 | 
			
		||||
        <td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@
 | 
			
		||||
.chart-widget {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  max-height: 275px;
 | 
			
		||||
  max-height: 293px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.formRadioGroup {
 | 
			
		||||
@ -48,3 +48,44 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.latest-transactions {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  table-layout:fixed;
 | 
			
		||||
  tr, td, th {
 | 
			
		||||
    border: 0px;
 | 
			
		||||
  }
 | 
			
		||||
  td {
 | 
			
		||||
    width: 25%;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-satoshis {
 | 
			
		||||
    display: none;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
    @media (min-width: 576px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 768px) {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 1100px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-fiat {
 | 
			
		||||
    display: none;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
    @media (min-width: 485px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 768px) {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 992px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-fees {
 | 
			
		||||
    text-align: right;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,14 +15,15 @@ import { selectPowerOfTen } from 'src/app/bitcoin.utils';
 | 
			
		||||
  styles: [`
 | 
			
		||||
    .loadingGraphs {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 38%;
 | 
			
		||||
      top: 50%;
 | 
			
		||||
      left: calc(50% - 15px);
 | 
			
		||||
      z-index: 100;
 | 
			
		||||
    }
 | 
			
		||||
  `],
 | 
			
		||||
})
 | 
			
		||||
export class HashrateChartComponent implements OnInit {
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
  @Input() tableOnly = false;
 | 
			
		||||
  @Input() widget = false;
 | 
			
		||||
  @Input() right: number | string = 45;
 | 
			
		||||
  @Input() left: number | string = 75;
 | 
			
		||||
 | 
			
		||||
@ -114,7 +115,7 @@ export class HashrateChartComponent implements OnInit {
 | 
			
		||||
                }
 | 
			
		||||
                return {
 | 
			
		||||
                  availableTimespanDay: availableTimespanDay,
 | 
			
		||||
                  difficulty: tableData
 | 
			
		||||
                  difficulty: this.tableOnly ? tableData.slice(0, 5) : tableData
 | 
			
		||||
                };
 | 
			
		||||
              }),
 | 
			
		||||
            );
 | 
			
		||||
@ -141,6 +142,7 @@ export class HashrateChartComponent implements OnInit {
 | 
			
		||||
        bottom: this.widget ? 30 : 60,
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        show: !this.isMobile() || !this.widget,
 | 
			
		||||
        trigger: 'axis',
 | 
			
		||||
        axisPointer: {
 | 
			
		||||
          type: 'line'
 | 
			
		||||
 | 
			
		||||
@ -25,9 +25,9 @@
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="hashrateObservable$ | async" [class]="!widget ? 'chart' : 'chart-widget'"
 | 
			
		||||
  <div [class]="!widget ? 'chart' : 'chart-widget'"
 | 
			
		||||
    echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
 | 
			
		||||
  <div class="text-center loadingGraphs" [class]="widget ? 'widget' : ''" *ngIf="isLoading">
 | 
			
		||||
  <div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
    <div class="spinner-border text-light"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@
 | 
			
		||||
.chart-widget {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  max-height: 275px;
 | 
			
		||||
  max-height: 293px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.formRadioGroup {
 | 
			
		||||
 | 
			
		||||
@ -78,7 +78,7 @@ export class HashrateChartPoolsComponent implements OnInit {
 | 
			
		||||
                    name: name,
 | 
			
		||||
                    showSymbol: false,
 | 
			
		||||
                    symbol: 'none',
 | 
			
		||||
                    data: grouped[name].map((val) => [val.timestamp * 1000, (val.share * 100).toFixed(2)]),
 | 
			
		||||
                    data: grouped[name].map((val) => [val.timestamp * 1000, val.share * 100]),
 | 
			
		||||
                    type: 'line',
 | 
			
		||||
                    lineStyle: { width: 0 },
 | 
			
		||||
                    areaStyle: { opacity: 1 },
 | 
			
		||||
@ -132,6 +132,7 @@ export class HashrateChartPoolsComponent implements OnInit {
 | 
			
		||||
        top: this.widget ? 10 : 40,
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        show: !this.isMobile() || !this.widget,
 | 
			
		||||
        trigger: 'axis',
 | 
			
		||||
        axisPointer: {
 | 
			
		||||
          type: 'line'
 | 
			
		||||
@ -149,7 +150,7 @@ export class HashrateChartPoolsComponent implements OnInit {
 | 
			
		||||
          data.sort((a, b) => b.data[1] - a.data[1]);
 | 
			
		||||
          for (const pool of data) {
 | 
			
		||||
            if (pool.data[1] > 0) {
 | 
			
		||||
              tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1]}%<br>`
 | 
			
		||||
              tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1].toFixed(2)}%<br>`;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return tooltip;
 | 
			
		||||
 | 
			
		||||
@ -2,36 +2,124 @@
 | 
			
		||||
 | 
			
		||||
  <div class="row row-cols-1 row-cols-md-2">
 | 
			
		||||
 | 
			
		||||
    <!-- Temporary stuff here - Will be moved to a component once we have more useful data to show -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card double">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <!-- pool distribution -->
 | 
			
		||||
          <h5 class="card-title">
 | 
			
		||||
            <a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="mining.pool-share">
 | 
			
		||||
              Mining Pools Share (1w)
 | 
			
		||||
            </a>
 | 
			
		||||
          </h5>
 | 
			
		||||
          <app-pool-ranking [widget]=true></app-pool-ranking>
 | 
			
		||||
      <div class="main-title">Reward stats</div>
 | 
			
		||||
      <div class="card" style="height: 123px">
 | 
			
		||||
        <div class="card-body more-padding">
 | 
			
		||||
          <div class="fee-estimation-container" *ngIf="$rewardStats | async as rewardStats; else loadingReward">
 | 
			
		||||
            <div class="item">
 | 
			
		||||
              <h5 class="card-title" i18n="">Miners Reward</h5>
 | 
			
		||||
              <div class="card-text">
 | 
			
		||||
                <app-amount [satoshis]="rewardStats.totalReward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
 | 
			
		||||
                <div class="symbol">in the last 8 blocks</div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="item">
 | 
			
		||||
              <h5 class="card-title" i18n="">Reward Per Tx</h5>
 | 
			
		||||
              <div class="card-text">
 | 
			
		||||
                {{ rewardStats.rewardPerTx | amountShortener }}
 | 
			
		||||
                <span class="symbol">sats/tx</span>
 | 
			
		||||
                <div class="symbol">in the last 8 blocks</div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="item">
 | 
			
		||||
              <h5 class="card-title" i18n="">Average Fee</h5>
 | 
			
		||||
              <div class="card-text">
 | 
			
		||||
                {{ rewardStats.feePerTx | amountShortener}}
 | 
			
		||||
                <span class="symbol">sats/tx</span>
 | 
			
		||||
                <div class="symbol">in the last 8 blocks</div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <ng-template #loadingReward>
 | 
			
		||||
      <div class="fee-estimation-container">
 | 
			
		||||
        <div class="item">
 | 
			
		||||
          <h5 class="card-title" i18n="">Miners Reward</h5>
 | 
			
		||||
          <div class="card-text">
 | 
			
		||||
            <div class="skeleton-loader"></div>
 | 
			
		||||
            <div class="skeleton-loader"></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="item">
 | 
			
		||||
          <h5 class="card-title" i18n="">Reward Per Tx</h5>
 | 
			
		||||
          <div class="card-text">
 | 
			
		||||
            <div class="skeleton-loader"></div>
 | 
			
		||||
            <div class="skeleton-loader"></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="item">
 | 
			
		||||
          <h5 class="card-title" i18n="">Average Fee</h5>
 | 
			
		||||
          <div class="card-text">
 | 
			
		||||
            <div class="skeleton-loader"></div>
 | 
			
		||||
            <div class="skeleton-loader"></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
 | 
			
		||||
          <!-- pools hashrate -->
 | 
			
		||||
    <!-- 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>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- pool distribution -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card" style="height: 385px">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <app-pool-ranking [widget]=true></app-pool-ranking>
 | 
			
		||||
          <div class="mt-1"><a [routerLink]="['/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more
 | 
			
		||||
              »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- hashrate -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card" style="height: 385px">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <h5 class="card-title">
 | 
			
		||||
            Hashrate (1y)
 | 
			
		||||
          </h5>
 | 
			
		||||
          <app-hashrate-chart [widget]=true></app-hashrate-chart>
 | 
			
		||||
          <div class="mt-1"><a [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="dashboard.view-more">View more
 | 
			
		||||
              »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- pool dominance -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card" style="height: 385px">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <h5 class="card-title">
 | 
			
		||||
            Mining Pools Dominance (1y)
 | 
			
		||||
          </h5>
 | 
			
		||||
          <app-hashrate-chart-pools [widget]=true></app-hashrate-chart-pools>
 | 
			
		||||
          <div class="mt-1"><a [routerLink]="['/mining/hashrate/pools' | relativeUrl]" i18n="dashboard.view-more">View
 | 
			
		||||
              more »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card">
 | 
			
		||||
      <div class="card" style="height: 385px">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <!-- hashrate -->
 | 
			
		||||
          <h5 class="card-title">
 | 
			
		||||
            <a class="link" href="" [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="mining.hashrate">
 | 
			
		||||
              Hashrate (1y)
 | 
			
		||||
            </a>
 | 
			
		||||
            Adjustments
 | 
			
		||||
          </h5>
 | 
			
		||||
          <app-hashrate-chart [widget]=true></app-hashrate-chart>
 | 
			
		||||
          <app-hashrate-chart [tableOnly]=true [widget]=true></app-hashrate-chart>
 | 
			
		||||
          <div class="mt-1"><a [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="dashboard.view-more">View more
 | 
			
		||||
              »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>  
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -12,14 +12,11 @@
 | 
			
		||||
 | 
			
		||||
.card {
 | 
			
		||||
  background-color: #1d1f31;
 | 
			
		||||
  height: 340px;
 | 
			
		||||
}
 | 
			
		||||
.card.double {
 | 
			
		||||
  height: 620px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-title {
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
  color: #4a68b9;
 | 
			
		||||
}
 | 
			
		||||
.card-title > a {
 | 
			
		||||
  color: #4a68b9;
 | 
			
		||||
@ -47,7 +44,7 @@
 | 
			
		||||
.fade-border {
 | 
			
		||||
  -webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
 | 
			
		||||
}
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
.main-title {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: #ffffff91;
 | 
			
		||||
@ -58,3 +55,63 @@
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding-bottom: 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fee-estimation-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  @media (min-width: 376px) {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
  .item {
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    width: -webkit-fill-available;
 | 
			
		||||
    @media (min-width: 376px) {
 | 
			
		||||
      margin: 0 auto 0px;
 | 
			
		||||
    }
 | 
			
		||||
    &:first-child{
 | 
			
		||||
      display: none;
 | 
			
		||||
      @media (min-width: 485px) {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
      @media (min-width: 768px) {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
      @media (min-width: 992px) {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
    .card-text span {
 | 
			
		||||
      color: #ffffff66;
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
      top: 0px;
 | 
			
		||||
    }
 | 
			
		||||
    .fee-text{
 | 
			
		||||
      border-bottom: 1px solid #ffffff1c;
 | 
			
		||||
      width: fit-content;
 | 
			
		||||
      margin: auto;
 | 
			
		||||
      line-height: 1.45;
 | 
			
		||||
      padding: 0px 2px;
 | 
			
		||||
    }
 | 
			
		||||
    .fiat {
 | 
			
		||||
      display: block;
 | 
			
		||||
      font-size: 14px !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.skeleton-loader {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  display: block;
 | 
			
		||||
  &:first-child {
 | 
			
		||||
    max-width: 90px;
 | 
			
		||||
    margin: 15px auto 3px;
 | 
			
		||||
  }
 | 
			
		||||
  &:last-child {
 | 
			
		||||
    margin: 10px auto 3px;
 | 
			
		||||
    max-width: 55px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,8 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { map } from 'rxjs/operators';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-mining-dashboard',
 | 
			
		||||
@ -8,12 +11,35 @@ import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class MiningDashboardComponent implements OnInit {
 | 
			
		||||
  private blocks = [];
 | 
			
		||||
 | 
			
		||||
  constructor(private seoService: SeoService) {
 | 
			
		||||
  public $rewardStats: Observable<any>;
 | 
			
		||||
  public totalReward = 0;
 | 
			
		||||
  public rewardPerTx = '~';
 | 
			
		||||
  public feePerTx = '~';
 | 
			
		||||
 | 
			
		||||
  constructor(private seoService: SeoService,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    @Inject(LOCALE_ID) private locale: string,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
    this.$rewardStats = this.stateService.blocks$.pipe(
 | 
			
		||||
      map(([block]) => {
 | 
			
		||||
        this.blocks.unshift(block);
 | 
			
		||||
        this.blocks = this.blocks.slice(0, 8);
 | 
			
		||||
        const totalTx = this.blocks.reduce((acc, block) => acc + block.tx_count, 0);
 | 
			
		||||
        const totalFee = this.blocks.reduce((acc, block) => acc + block.extras?.totalFees ?? 0, 0);
 | 
			
		||||
        const totalReward = this.blocks.reduce((acc, block) => acc + block.extras?.reward ?? 0, 0);
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          'totalReward': totalReward,
 | 
			
		||||
          'rewardPerTx': totalReward / totalTx,
 | 
			
		||||
          'feePerTx': totalFee / totalTx,
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,31 @@
 | 
			
		||||
<div [class]="widget === false ? 'container-xl' : ''">
 | 
			
		||||
 | 
			
		||||
  <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>
 | 
			
		||||
        <p class="card-text">
 | 
			
		||||
          {{ miningStats['minersLuck'] }}%
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <h5 class="card-title" i18n="master-page.blocks">Blocks (1w)</h5>
 | 
			
		||||
        <p class="card-text">
 | 
			
		||||
          {{ miningStats.blockCount }}
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <h5 class="card-title" i18n="mining.miners-count">Pools count (1w)</h5>
 | 
			
		||||
        <p class="card-text">
 | 
			
		||||
          {{ miningStats.pools.length }}
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div [class]="widget ? 'chart-widget' : 'chart'"
 | 
			
		||||
    echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
 | 
			
		||||
  <div class="text-center loadingGraphs" [class]="widget ? 'widget' : ''" *ngIf="isLoading">
 | 
			
		||||
  <div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
    <div class="spinner-border text-light"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -59,7 +82,7 @@
 | 
			
		||||
        <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=""><a [routerLink]="[('/mining/pool/' + pool.poolId) | relativeUrl]">{{ pool.name }}</a></td>
 | 
			
		||||
        <td class="" *ngIf="this.poolsWindowPreference === '24h'">{{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }}</td>
 | 
			
		||||
        <td class="" *ngIf="this.poolsWindowPreference === '24h' && !isLoading">{{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }}</td>
 | 
			
		||||
        <td class="">{{ pool['blockText'] }}</td>
 | 
			
		||||
        <td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
@ -75,3 +98,27 @@
 | 
			
		||||
  </table>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingReward>
 | 
			
		||||
  <div class="pool-distribution">
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <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>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="master-page.blocks">Blocks (1w)</h5>
 | 
			
		||||
      <p class="card-text">
 | 
			
		||||
        <span class="skeleton-loader skeleton-loader-big"></span>
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <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>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -1,13 +1,16 @@
 | 
			
		||||
.chart {
 | 
			
		||||
  max-height: 400px;
 | 
			
		||||
  @media (max-width: 767.98px) {
 | 
			
		||||
    max-height: 300px;
 | 
			
		||||
    max-height: 270px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.chart-widget {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  max-height: 275px;
 | 
			
		||||
  max-height: 270px;
 | 
			
		||||
  @media (max-width: 767.98px) {
 | 
			
		||||
    max-height: 200px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.formRadioGroup {
 | 
			
		||||
@ -44,3 +47,66 @@
 | 
			
		||||
.loadingGraphs.widget {
 | 
			
		||||
  top: 25%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pool-distribution {
 | 
			
		||||
  min-height: 56px;
 | 
			
		||||
  display: block;
 | 
			
		||||
  @media (min-width: 485px) {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
  h5 {
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
  }
 | 
			
		||||
  .item {
 | 
			
		||||
    width: 50%;
 | 
			
		||||
    margin: 0px auto 10px;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    @media (min-width: 485px) {
 | 
			
		||||
      margin: 0px auto 10px;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 785px) {
 | 
			
		||||
      margin: 0px auto 0px;
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin: 0px auto 0px;
 | 
			
		||||
    }
 | 
			
		||||
    &:nth-child(2) {
 | 
			
		||||
      order: 2;
 | 
			
		||||
      @media (min-width: 485px) {
 | 
			
		||||
        order: 3;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &:nth-child(3) {
 | 
			
		||||
      order: 3;
 | 
			
		||||
      @media (min-width: 485px) {
 | 
			
		||||
        order: 2;
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
      @media (min-width: 768px) {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
      @media (min-width: 992px) {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .card-title {
 | 
			
		||||
      font-size: 1rem;
 | 
			
		||||
      color: #4a68b9;
 | 
			
		||||
    }
 | 
			
		||||
    .card-text {
 | 
			
		||||
      font-size: 18px;
 | 
			
		||||
      span {
 | 
			
		||||
        color: #ffffff66;
 | 
			
		||||
        font-size: 12px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.skeleton-loader {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  display: block;
 | 
			
		||||
  max-width: 80px;
 | 
			
		||||
  margin: 15px auto 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit } from '@angular/core';
 | 
			
		||||
import { FormBuilder, FormGroup } from '@angular/forms';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { EChartsOption, PieSeriesOption } from 'echarts';
 | 
			
		||||
@ -41,6 +41,7 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
    private miningService: MiningService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private zone: NgZone,
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -49,7 +50,7 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
      this.poolsWindowPreference = '1w';
 | 
			
		||||
    } else {
 | 
			
		||||
      this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
 | 
			
		||||
      this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w';    
 | 
			
		||||
      this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w';
 | 
			
		||||
    }
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference);
 | 
			
		||||
@ -85,6 +86,7 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
        }),
 | 
			
		||||
        map(data => {
 | 
			
		||||
          data.pools = data.pools.map((pool: SinglePoolStats) => this.formatPoolUI(pool));
 | 
			
		||||
          data['minersLuck'] = (100 * (data.blockCount / 1008)).toFixed(2); // luck 1w
 | 
			
		||||
          return data;
 | 
			
		||||
        }),
 | 
			
		||||
        tap(data => {
 | 
			
		||||
@ -105,24 +107,40 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  generatePoolsChartSerieData(miningStats) {
 | 
			
		||||
    const poolShareThreshold = this.isMobile() ? 1 : 0.5; // Do not draw pools which hashrate share is lower than that
 | 
			
		||||
    const poolShareThreshold = this.isMobile() ? 2 : 1; // Do not draw pools which hashrate share is lower than that
 | 
			
		||||
    const data: object[] = [];
 | 
			
		||||
    let totalShareOther = 0;
 | 
			
		||||
    let totalBlockOther = 0;
 | 
			
		||||
    let totalEstimatedHashrateOther = 0;
 | 
			
		||||
 | 
			
		||||
    let edgeDistance: any = '20%';
 | 
			
		||||
    if (this.isMobile() && this.widget) {
 | 
			
		||||
      edgeDistance = 0;
 | 
			
		||||
    } else if (this.isMobile() && !this.widget || this.widget) {
 | 
			
		||||
      edgeDistance = 35;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    miningStats.pools.forEach((pool) => {
 | 
			
		||||
      if (parseFloat(pool.share) < poolShareThreshold) {
 | 
			
		||||
        totalShareOther += parseFloat(pool.share);
 | 
			
		||||
        totalBlockOther += pool.blockCount;
 | 
			
		||||
        totalEstimatedHashrateOther += pool.lastEstimatedHashrate;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      data.push({
 | 
			
		||||
        itemStyle: {
 | 
			
		||||
          color: poolsColor[pool.name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
 | 
			
		||||
          color: poolsColor[pool.name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()],
 | 
			
		||||
        },
 | 
			
		||||
        value: pool.share,
 | 
			
		||||
        name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`),
 | 
			
		||||
        name: pool.name + ((this.isMobile() || this.widget) ? `` : ` (${pool.share}%)`),
 | 
			
		||||
        label: {
 | 
			
		||||
          overflow: 'none',
 | 
			
		||||
          color: '#b1b1b1',
 | 
			
		||||
          overflow: 'break',
 | 
			
		||||
          alignTo: 'edge',
 | 
			
		||||
          edgeDistance: edgeDistance,
 | 
			
		||||
        },
 | 
			
		||||
        tooltip: {
 | 
			
		||||
          show: !this.isMobile() || !this.widget,
 | 
			
		||||
          backgroundColor: 'rgba(17, 19, 31, 1)',
 | 
			
		||||
          borderRadius: 4,
 | 
			
		||||
          shadowColor: 'rgba(0, 0, 0, 0.5)',
 | 
			
		||||
@ -144,6 +162,42 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
        data: pool.poolId,
 | 
			
		||||
      } as PieSeriesOption);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 'Other'
 | 
			
		||||
    data.push({
 | 
			
		||||
      itemStyle: {
 | 
			
		||||
        color: 'grey',
 | 
			
		||||
      },
 | 
			
		||||
      value: totalShareOther,
 | 
			
		||||
      name: 'Other' + (this.isMobile() ? `` : ` (${totalShareOther.toFixed(2)}%)`),
 | 
			
		||||
      label: {
 | 
			
		||||
        overflow: 'none',
 | 
			
		||||
        color: '#b1b1b1',
 | 
			
		||||
        alignTo: 'edge',
 | 
			
		||||
        edgeDistance: edgeDistance
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        backgroundColor: 'rgba(17, 19, 31, 1)',
 | 
			
		||||
        borderRadius: 4,
 | 
			
		||||
        shadowColor: 'rgba(0, 0, 0, 0.5)',
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: '#b1b1b1',
 | 
			
		||||
        },
 | 
			
		||||
        borderColor: '#000',
 | 
			
		||||
        formatter: () => {
 | 
			
		||||
          if (this.poolsWindowPreference === '24h') {
 | 
			
		||||
            return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
 | 
			
		||||
              totalEstimatedHashrateOther.toString() + ' PH/s' +
 | 
			
		||||
              `<br>` + totalBlockOther.toString() + ` blocks`;
 | 
			
		||||
          } else {
 | 
			
		||||
            return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
 | 
			
		||||
              totalBlockOther.toString() + ` blocks`;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      data: 9999 as any,
 | 
			
		||||
    } as PieSeriesOption);
 | 
			
		||||
 | 
			
		||||
    return data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -154,9 +208,22 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
    }
 | 
			
		||||
    network = network.charAt(0).toUpperCase() + network.slice(1);
 | 
			
		||||
 | 
			
		||||
    let radius: any[] = ['20%', '70%'];
 | 
			
		||||
    if (this.isMobile() || this.widget) {
 | 
			
		||||
      radius = ['20%', '60%'];
 | 
			
		||||
    let radius: any[] = ['20%', '80%'];
 | 
			
		||||
    let top: any = undefined; let bottom = undefined; let height = undefined;
 | 
			
		||||
    if (this.isMobile() && this.widget) {
 | 
			
		||||
      top = -30;
 | 
			
		||||
      height = 270;
 | 
			
		||||
      radius = ['10%', '50%'];
 | 
			
		||||
    } else if (this.isMobile() && !this.widget) {
 | 
			
		||||
      top = 0;
 | 
			
		||||
      height = 300;
 | 
			
		||||
      radius = ['10%', '50%'];
 | 
			
		||||
    } else if (this.widget) {
 | 
			
		||||
      radius = ['15%', '60%'];
 | 
			
		||||
      top = -20;
 | 
			
		||||
      height = 330;
 | 
			
		||||
    } else {
 | 
			
		||||
      top = 35;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
@ -180,14 +247,15 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
      },
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          top: this.widget ? 0 : 35,
 | 
			
		||||
          minShowLabelAngle: 3.6,
 | 
			
		||||
          top: top,
 | 
			
		||||
          bottom: bottom,
 | 
			
		||||
          height: height,
 | 
			
		||||
          name: 'Mining pool',
 | 
			
		||||
          type: 'pie',
 | 
			
		||||
          radius: radius,
 | 
			
		||||
          data: this.generatePoolsChartSerieData(miningStats),
 | 
			
		||||
          labelLine: {
 | 
			
		||||
            length: this.isMobile() ? 10 : 15,
 | 
			
		||||
            length2: this.isMobile() ? 0 : 15,
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              width: 2,
 | 
			
		||||
            },
 | 
			
		||||
@ -223,14 +291,19 @@ export class PoolRankingComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    this.chartInstance = ec;
 | 
			
		||||
    this.chartInstance.on('click', (e) => {
 | 
			
		||||
      this.router.navigate(['/mining/pool/', e.data.data]);
 | 
			
		||||
      if (e.data.data === 9999) { // "Other"
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this.zone.run(() => {
 | 
			
		||||
        this.router.navigate(['/mining/pool/', e.data.data]);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Default mining stats if something goes wrong
 | 
			
		||||
   */
 | 
			
		||||
  getEmptyMiningStat() {
 | 
			
		||||
  getEmptyMiningStat(): MiningStats {
 | 
			
		||||
    return {
 | 
			
		||||
      lastEstimatedHashrate: 'Error',
 | 
			
		||||
      blockCount: 0,
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
 | 
			
		||||
  <div class="d-flex">
 | 
			
		||||
    <div class="search-box-container mr-2">
 | 
			
		||||
      <input #instance="ngbTypeahead" [ngbTypeahead]="typeaheadSearchFn" (selectItem)="itemSelected()" (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="TXID, block height, hash or address">
 | 
			
		||||
      <input #instance="ngbTypeahead" [ngbTypeahead]="typeaheadSearchFn" [resultFormatter]="formatterFn" (selectItem)="itemSelected()" (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="TXID, block height, hash or address">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div>
 | 
			
		||||
      <button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary"><fa-icon [icon]="['fas', 'search']" [fixedWidth]="true" i18n-title="search-form.search-title" title="Search"></fa-icon></button>
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,19 @@
 | 
			
		||||
:host ::ng-deep .dropdown-item {
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  width: 375px;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
:host ::ng-deep {
 | 
			
		||||
  .dropdown-item {
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    width: calc(100% - 34px);
 | 
			
		||||
  }
 | 
			
		||||
  .dropdown-menu {
 | 
			
		||||
    width: calc(100% - 34px);
 | 
			
		||||
  }
 | 
			
		||||
  @media (min-width: 768px) {
 | 
			
		||||
    .dropdown-item {
 | 
			
		||||
      width: 410px;
 | 
			
		||||
    }
 | 
			
		||||
    .dropdown-menu {
 | 
			
		||||
      width: 410px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form {
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import { debounceTime, distinctUntilChanged, switchMap, filter, catchError, map
 | 
			
		||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
 | 
			
		||||
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
 | 
			
		||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
import { ShortenStringPipe } from 'src/app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-search-form',
 | 
			
		||||
@ -22,6 +23,7 @@ export class SearchFormComponent implements OnInit {
 | 
			
		||||
  typeaheadSearchFn: ((text: Observable<string>) => Observable<readonly any[]>);
 | 
			
		||||
 | 
			
		||||
  searchForm: FormGroup;
 | 
			
		||||
  isMobile = (window.innerWidth <= 767.98);
 | 
			
		||||
  @Output() searchTriggered = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
  regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/;
 | 
			
		||||
@ -33,6 +35,8 @@ export class SearchFormComponent implements OnInit {
 | 
			
		||||
  focus$ = new Subject<string>();
 | 
			
		||||
  click$ = new Subject<string>();
 | 
			
		||||
 | 
			
		||||
  formatterFn = (address: string) => this.shortenStringPipe.transform(address, this.isMobile ? 33 : 40);
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private formBuilder: FormBuilder,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
@ -40,6 +44,7 @@ export class SearchFormComponent implements OnInit {
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private relativeUrlPipe: RelativeUrlPipe,
 | 
			
		||||
    private shortenStringPipe: ShortenStringPipe,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
 | 
			
		||||
@ -3,13 +3,13 @@
 | 
			
		||||
  <div class="title-block">
 | 
			
		||||
    <div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
 | 
			
		||||
      <span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
 | 
			
		||||
      <a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]" [state]="{ data: rbfTransaction }">
 | 
			
		||||
      <a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]" [state]="{ data: rbfTransaction.size ? rbfTransaction : null }">
 | 
			
		||||
        <span class="d-inline d-lg-none">{{ rbfTransaction.txid | shortenString : 24 }}</span>
 | 
			
		||||
        <span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span>
 | 
			
		||||
      </a>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <ng-container>
 | 
			
		||||
    <ng-container *ngIf="!rbfTransaction || rbfTransaction?.size">
 | 
			
		||||
      <h1 i18n="shared.transaction">Transaction</h1>
 | 
			
		||||
 | 
			
		||||
      <span class="tx-link float-left">
 | 
			
		||||
 | 
			
		||||
@ -37,6 +37,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
  transactionTime = -1;
 | 
			
		||||
  subscription: Subscription;
 | 
			
		||||
  fetchCpfpSubscription: Subscription;
 | 
			
		||||
  txReplacedSubscription: Subscription;
 | 
			
		||||
  blocksSubscription: Subscription;
 | 
			
		||||
  rbfTransaction: undefined | Transaction;
 | 
			
		||||
  cpfpInfo: CpfpInfo | null;
 | 
			
		||||
  showCpfpDetails = false;
 | 
			
		||||
@ -185,15 +187,12 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
          this.error = undefined;
 | 
			
		||||
          this.waitingForTransaction = false;
 | 
			
		||||
          this.setMempoolBlocksSubscription();
 | 
			
		||||
          this.websocketService.startTrackTransaction(tx.txid);
 | 
			
		||||
 | 
			
		||||
          if (!tx.status.confirmed) {
 | 
			
		||||
            this.websocketService.startTrackTransaction(tx.txid);
 | 
			
		||||
 | 
			
		||||
            if (tx.firstSeen) {
 | 
			
		||||
              this.transactionTime = tx.firstSeen;
 | 
			
		||||
            } else {
 | 
			
		||||
              this.getTransactionTime();
 | 
			
		||||
            }
 | 
			
		||||
          if (!tx.status.confirmed && tx.firstSeen) {
 | 
			
		||||
            this.transactionTime = tx.firstSeen;
 | 
			
		||||
          } else {
 | 
			
		||||
            this.getTransactionTime();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (this.tx.status.confirmed) {
 | 
			
		||||
@ -220,7 +219,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
 | 
			
		||||
    this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
 | 
			
		||||
      this.latestBlock = block;
 | 
			
		||||
 | 
			
		||||
      if (txConfirmed && this.tx) {
 | 
			
		||||
@ -235,9 +234,13 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.stateService.txReplaced$.subscribe(
 | 
			
		||||
      (rbfTransaction) => (this.rbfTransaction = rbfTransaction)
 | 
			
		||||
    );
 | 
			
		||||
    this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => {
 | 
			
		||||
      if (!this.tx) {
 | 
			
		||||
        this.error = new Error();
 | 
			
		||||
        this.waitingForTransaction = false;
 | 
			
		||||
      }
 | 
			
		||||
      this.rbfTransaction = rbfTransaction;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleLoadElectrsTransactionError(error: any): Observable<any> {
 | 
			
		||||
@ -305,6 +308,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.subscription.unsubscribe();
 | 
			
		||||
    this.fetchCpfpSubscription.unsubscribe();
 | 
			
		||||
    this.txReplacedSubscription.unsubscribe();
 | 
			
		||||
    this.blocksSubscription.unsubscribe();
 | 
			
		||||
    this.leaveTransaction();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -73,16 +73,19 @@
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="text-right nowrap amount">
 | 
			
		||||
                  <ng-template [ngIf]="vin.prevout && vin.prevout.asset && vin.prevout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
 | 
			
		||||
                    <div *ngIf="assetsMinimal && assetsMinimal[vin.prevout.asset]">
 | 
			
		||||
                    <div *ngIf="assetsMinimal && assetsMinimal[vin.prevout.asset] else assetVinNotFound">
 | 
			
		||||
                      <ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vin.prevout }"></ng-container>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <ng-template #assetVinNotFound>
 | 
			
		||||
                      {{ vin.prevout.value }} <a [routerLink]="['/assets/asset/' | relativeUrl, vin.prevout.asset]">{{ vin.prevout.asset | slice : 0 : 7 }}</a>
 | 
			
		||||
                    </ng-template>
 | 
			
		||||
                  </ng-template>
 | 
			
		||||
                  <ng-template #defaultOutput>
 | 
			
		||||
                    <app-amount *ngIf="vin.prevout" [satoshis]="vin.prevout.value"></app-amount>
 | 
			
		||||
                  </ng-template>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr *ngIf="displayDetails">
 | 
			
		||||
              <tr *ngIf="(showDetails$ | async) === true">
 | 
			
		||||
                <td colspan="3" class="details-container" >
 | 
			
		||||
                  <table class="table table-striped table-borderless details-table mb-3">
 | 
			
		||||
                    <tbody>
 | 
			
		||||
@ -204,7 +207,7 @@
 | 
			
		||||
                  </ng-template>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr *ngIf="displayDetails">
 | 
			
		||||
              <tr *ngIf="(showDetails$ | async) === true">
 | 
			
		||||
                <td colspan="3" class=" details-container" >
 | 
			
		||||
                  <table class="table table-striped table-borderless details-table mb-3">
 | 
			
		||||
                    <tbody>
 | 
			
		||||
@ -240,7 +243,7 @@
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div>
 | 
			
		||||
      <div class="float-left mt-2-5" *ngIf="!transactionPage && tx.fee">
 | 
			
		||||
      <div class="float-left mt-2-5" *ngIf="!transactionPage && !tx.vin[0].is_coinbase">
 | 
			
		||||
        {{ tx.fee / (tx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="d-none d-sm-inline-block"> – {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></span>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,11 @@
 | 
			
		||||
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, ChangeDetectorRef, Output, EventEmitter } from '@angular/core';
 | 
			
		||||
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Observable, forkJoin } from 'rxjs';
 | 
			
		||||
import { Observable, forkJoin, ReplaySubject, BehaviorSubject, merge, of, Subject, Subscription } from 'rxjs';
 | 
			
		||||
import { Outspend, Transaction } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { environment } from 'src/environments/environment';
 | 
			
		||||
import { AssetsService } from 'src/app/services/assets.service';
 | 
			
		||||
import { map } from 'rxjs/operators';
 | 
			
		||||
import { map, share, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
@ -17,7 +17,6 @@ import { BlockExtended } from 'src/app/interfaces/node-api.interface';
 | 
			
		||||
export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
  network = '';
 | 
			
		||||
  nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
 | 
			
		||||
  displayDetails = false;
 | 
			
		||||
 | 
			
		||||
  @Input() transactions: Transaction[];
 | 
			
		||||
  @Input() showConfirmations = false;
 | 
			
		||||
@ -28,7 +27,10 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Output() loadMore = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
  latestBlock$: Observable<BlockExtended>;
 | 
			
		||||
  outspends: Outspend[] = [];
 | 
			
		||||
  outspendsSubscription: Subscription;
 | 
			
		||||
  refreshOutspends$: ReplaySubject<object> = new ReplaySubject();
 | 
			
		||||
  showDetails$ = new BehaviorSubject<boolean>(false);
 | 
			
		||||
  outspends: Outspend[][] = [];
 | 
			
		||||
  assetsMinimal: any;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
@ -47,6 +49,34 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
        this.assetsMinimal = assets;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.outspendsSubscription = merge(
 | 
			
		||||
      this.refreshOutspends$
 | 
			
		||||
        .pipe(
 | 
			
		||||
          switchMap((observableObject) => forkJoin(observableObject)),
 | 
			
		||||
          map((outspends: any) => {
 | 
			
		||||
            const newOutspends: Outspend[] = [];
 | 
			
		||||
            for (const i in outspends) {
 | 
			
		||||
              if (outspends.hasOwnProperty(i)) {
 | 
			
		||||
                newOutspends.push(outspends[i]);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            this.outspends = this.outspends.concat(newOutspends);
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
      this.stateService.utxoSpent$
 | 
			
		||||
        .pipe(
 | 
			
		||||
          map((utxoSpent) => {
 | 
			
		||||
            for (const i in utxoSpent) {
 | 
			
		||||
              this.outspends[0][i] = {
 | 
			
		||||
                spent: true,
 | 
			
		||||
                txid: utxoSpent[i].txid,
 | 
			
		||||
                vin: utxoSpent[i].vin,
 | 
			
		||||
              };
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        )
 | 
			
		||||
    ).subscribe(() => this.ref.markForCheck());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges() {
 | 
			
		||||
@ -70,18 +100,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
      }
 | 
			
		||||
      observableObject[i] = this.electrsApiService.getOutspends$(tx.txid);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    forkJoin(observableObject)
 | 
			
		||||
      .subscribe((outspends: any) => {
 | 
			
		||||
        const newOutspends = [];
 | 
			
		||||
        for (const i in outspends) {
 | 
			
		||||
          if (outspends.hasOwnProperty(i)) {
 | 
			
		||||
            newOutspends.push(outspends[i]);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        this.outspends = this.outspends.concat(newOutspends);
 | 
			
		||||
        this.ref.markForCheck();
 | 
			
		||||
      });
 | 
			
		||||
    this.refreshOutspends$.next(observableObject);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onScroll() {
 | 
			
		||||
@ -129,7 +148,14 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleDetails() {
 | 
			
		||||
    this.displayDetails = !this.displayDetails;
 | 
			
		||||
    this.ref.markForCheck();
 | 
			
		||||
    if (this.showDetails$.value === true) {
 | 
			
		||||
      this.showDetails$.next(false);
 | 
			
		||||
    } else {
 | 
			
		||||
      this.showDetails$.next(true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.outspendsSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ export interface SinglePoolStats {
 | 
			
		||||
  blockCount: number;
 | 
			
		||||
  emptyBlocks: number;
 | 
			
		||||
  rank: number;
 | 
			
		||||
  share: string;
 | 
			
		||||
  share: number;
 | 
			
		||||
  lastEstimatedHashrate: string;
 | 
			
		||||
  emptyBlockRatio: string;
 | 
			
		||||
  logo: string;
 | 
			
		||||
@ -75,13 +75,6 @@ export interface PoolsStats {
 | 
			
		||||
  oldestIndexedBlockTimestamp: number;
 | 
			
		||||
  pools: SinglePoolStats[];
 | 
			
		||||
}
 | 
			
		||||
export interface MiningStats {
 | 
			
		||||
  lastEstimatedHashrate: string;
 | 
			
		||||
  blockCount: number;
 | 
			
		||||
  totalEmptyBlock: number;
 | 
			
		||||
  totalEmptyBlockRatio: string;
 | 
			
		||||
  pools: SinglePoolStats[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Pool component
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,9 @@ export interface WebsocketResponse {
 | 
			
		||||
  action?: string;
 | 
			
		||||
  data?: string[];
 | 
			
		||||
  tx?: Transaction;
 | 
			
		||||
  rbfTransaction?: Transaction;
 | 
			
		||||
  rbfTransaction?: ReplacedTransaction;
 | 
			
		||||
  txReplaced?: ReplacedTransaction;
 | 
			
		||||
  utxoSpent?: object;
 | 
			
		||||
  transactions?: TransactionStripped[];
 | 
			
		||||
  loadingIndicators?: ILoadingIndicators;
 | 
			
		||||
  backendInfo?: IBackendInfo;
 | 
			
		||||
@ -26,6 +28,9 @@ export interface WebsocketResponse {
 | 
			
		||||
  'track-bisq-market'?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReplacedTransaction extends Transaction {
 | 
			
		||||
  txid: string;
 | 
			
		||||
}
 | 
			
		||||
export interface MempoolBlock {
 | 
			
		||||
  blink?: boolean;
 | 
			
		||||
  height?: number;
 | 
			
		||||
 | 
			
		||||
@ -73,7 +73,7 @@ export class MiningService {
 | 
			
		||||
    const totalEmptyBlockRatio = (totalEmptyBlock / stats.blockCount * 100).toFixed(2);
 | 
			
		||||
    const poolsStats = stats.pools.map((poolStat) => {
 | 
			
		||||
      return {
 | 
			
		||||
        share: (poolStat.blockCount / stats.blockCount * 100).toFixed(2),
 | 
			
		||||
        share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)),
 | 
			
		||||
        lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider).toFixed(2),
 | 
			
		||||
        emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
 | 
			
		||||
        logo: `./resources/mining-pools/` + poolStat.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg',
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
 | 
			
		||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
 | 
			
		||||
import { Transaction } from '../interfaces/electrs.interface';
 | 
			
		||||
import { IBackendInfo, MempoolBlock, MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
import { IBackendInfo, MempoolBlock, MempoolInfo, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
 | 
			
		||||
import { Router, NavigationStart } from '@angular/router';
 | 
			
		||||
import { isPlatformBrowser } from '@angular/common';
 | 
			
		||||
@ -82,7 +82,8 @@ export class StateService {
 | 
			
		||||
  bsqPrice$ = new ReplaySubject<number>(1);
 | 
			
		||||
  mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
 | 
			
		||||
  mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
 | 
			
		||||
  txReplaced$ = new Subject<Transaction>();
 | 
			
		||||
  txReplaced$ = new Subject<ReplacedTransaction>();
 | 
			
		||||
  utxoSpent$ = new Subject<object>();
 | 
			
		||||
  mempoolTransactions$ = new Subject<Transaction>();
 | 
			
		||||
  blockTransactions$ = new Subject<Transaction>();
 | 
			
		||||
  isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
 | 
			
		||||
 | 
			
		||||
@ -239,6 +239,10 @@ export class WebsocketService {
 | 
			
		||||
      this.stateService.txReplaced$.next(response.rbfTransaction);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response.txReplaced) {
 | 
			
		||||
      this.stateService.txReplaced$.next(response.txReplaced);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response['mempool-blocks']) {
 | 
			
		||||
      this.stateService.mempoolBlocks$.next(response['mempool-blocks']);
 | 
			
		||||
    }
 | 
			
		||||
@ -251,6 +255,10 @@ export class WebsocketService {
 | 
			
		||||
      this.stateService.bsqPrice$.next(response['bsq-price']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response.utxoSpent) {
 | 
			
		||||
      this.stateService.utxoSpent$.next(response.utxoSpent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response.backendInfo) {
 | 
			
		||||
      this.stateService.backendInfo$.next(response.backendInfo);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 19 KiB  | 
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user