Merge branch 'master' into shrink-og-avatar
This commit is contained in:
		
						commit
						2b1d25c784
					
				@ -17,7 +17,6 @@ import { prepareBlock } from '../utils/blocks-utils';
 | 
			
		||||
import BlocksRepository from '../repositories/BlocksRepository';
 | 
			
		||||
import HashratesRepository from '../repositories/HashratesRepository';
 | 
			
		||||
import indexer from '../indexer';
 | 
			
		||||
import fiatConversion from './fiat-conversion';
 | 
			
		||||
import poolsParser from './pools-parser';
 | 
			
		||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
 | 
			
		||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
 | 
			
		||||
@ -170,7 +169,7 @@ class Blocks {
 | 
			
		||||
    blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
 | 
			
		||||
    blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
 | 
			
		||||
    blockExtended.extras.coinbaseRaw = blockExtended.extras.coinbaseTx.vin[0].scriptsig;
 | 
			
		||||
    blockExtended.extras.usd = fiatConversion.getConversionRates().USD;
 | 
			
		||||
    blockExtended.extras.usd = priceUpdater.latestPrices.USD;
 | 
			
		||||
 | 
			
		||||
    if (block.height === 0) {
 | 
			
		||||
      blockExtended.extras.medianFee = 0; // 50th percentiles
 | 
			
		||||
 | 
			
		||||
@ -1,123 +0,0 @@
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import * as http from 'http';
 | 
			
		||||
import * as https from 'https';
 | 
			
		||||
import axios, { AxiosResponse } from 'axios';
 | 
			
		||||
import { IConversionRates } from '../mempool.interfaces';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import backendInfo from './backend-info';
 | 
			
		||||
import { SocksProxyAgent } from 'socks-proxy-agent';
 | 
			
		||||
 | 
			
		||||
class FiatConversion {
 | 
			
		||||
  private debasingFiatCurrencies = ['AED', 'AUD', 'BDT', 'BHD', 'BMD', 'BRL', 'CAD', 'CHF', 'CLP',
 | 
			
		||||
    'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HUF', 'IDR', 'ILS', 'INR', 'JPY', 'KRW', 'KWD',
 | 
			
		||||
    'LKR', 'MMK', 'MXN', 'MYR', 'NGN', 'NOK', 'NZD', 'PHP', 'PKR', 'PLN', 'RUB', 'SAR', 'SEK',
 | 
			
		||||
    'SGD', 'THB', 'TRY', 'TWD', 'UAH', 'USD', 'VND', 'ZAR'];
 | 
			
		||||
  private conversionRates: IConversionRates = {};
 | 
			
		||||
  private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined;
 | 
			
		||||
  public ratesInitialized = false; // If true, it means rates are ready for use
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    for (const fiat of this.debasingFiatCurrencies) {
 | 
			
		||||
      this.conversionRates[fiat] = 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setProgressChangedCallback(fn: (rates: IConversionRates) => void) {
 | 
			
		||||
    this.ratesChangedCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public startService() {
 | 
			
		||||
    const fiatConversionUrl = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.PRICE_DATA_SERVER.TOR_URL : config.PRICE_DATA_SERVER.CLEARNET_URL;
 | 
			
		||||
    logger.info('Starting currency rates service');
 | 
			
		||||
    if (config.SOCKS5PROXY.ENABLED) {
 | 
			
		||||
      logger.info(`Currency rates service will be queried over the Tor network using ${fiatConversionUrl}`);
 | 
			
		||||
    } else {
 | 
			
		||||
      logger.info(`Currency rates service will be queried over clearnet using ${config.PRICE_DATA_SERVER.CLEARNET_URL}`);
 | 
			
		||||
    }
 | 
			
		||||
    setInterval(this.updateCurrency.bind(this), 1000 * config.MEMPOOL.PRICE_FEED_UPDATE_INTERVAL);
 | 
			
		||||
    this.updateCurrency();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getConversionRates() {
 | 
			
		||||
    return this.conversionRates;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async updateCurrency(): Promise<void> {
 | 
			
		||||
    type axiosOptions = {
 | 
			
		||||
      headers: {
 | 
			
		||||
        'User-Agent': string
 | 
			
		||||
      };
 | 
			
		||||
      timeout: number;
 | 
			
		||||
      httpAgent?: http.Agent;
 | 
			
		||||
      httpsAgent?: https.Agent;
 | 
			
		||||
    }
 | 
			
		||||
    const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
 | 
			
		||||
    const fiatConversionUrl = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.PRICE_DATA_SERVER.TOR_URL : config.PRICE_DATA_SERVER.CLEARNET_URL;
 | 
			
		||||
    const isHTTP = (new URL(fiatConversionUrl).protocol.split(':')[0] === 'http') ? true : false;
 | 
			
		||||
    const axiosOptions: axiosOptions = {
 | 
			
		||||
      headers: {
 | 
			
		||||
        'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
 | 
			
		||||
      },
 | 
			
		||||
      timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let retry = 0;
 | 
			
		||||
 | 
			
		||||
    while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
 | 
			
		||||
      try {
 | 
			
		||||
        if (config.SOCKS5PROXY.ENABLED) {
 | 
			
		||||
          let socksOptions: any = {
 | 
			
		||||
            agentOptions: {
 | 
			
		||||
              keepAlive: true,
 | 
			
		||||
            },
 | 
			
		||||
            hostname: config.SOCKS5PROXY.HOST,
 | 
			
		||||
            port: config.SOCKS5PROXY.PORT
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
          if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
 | 
			
		||||
            socksOptions.username = config.SOCKS5PROXY.USERNAME;
 | 
			
		||||
            socksOptions.password = config.SOCKS5PROXY.PASSWORD;
 | 
			
		||||
          } else {
 | 
			
		||||
            // Retry with different tor circuits https://stackoverflow.com/a/64960234
 | 
			
		||||
            socksOptions.username = `circuit${retry}`;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Handle proxy agent for onion addresses
 | 
			
		||||
          if (isHTTP) {
 | 
			
		||||
            axiosOptions.httpAgent = new SocksProxyAgent(socksOptions);
 | 
			
		||||
          } else {
 | 
			
		||||
            axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        logger.debug('Querying currency rates service...');
 | 
			
		||||
 | 
			
		||||
        const response: AxiosResponse = await axios.get(`${fiatConversionUrl}`, axiosOptions);
 | 
			
		||||
 | 
			
		||||
        if (response.statusText === 'error' || !response.data) {
 | 
			
		||||
          throw new Error(`Could not fetch data from ${fiatConversionUrl}, Error: ${response.status}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const rate of response.data.data) {
 | 
			
		||||
          if (this.debasingFiatCurrencies.includes(rate.currencyCode) && rate.provider === 'Bisq-Aggregate') {
 | 
			
		||||
            this.conversionRates[rate.currencyCode] = Math.round(100 * rate.price) / 100;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.ratesInitialized = true;
 | 
			
		||||
        logger.debug(`USD Conversion Rate: ${this.conversionRates.USD}`);
 | 
			
		||||
 | 
			
		||||
        if (this.ratesChangedCallback) {
 | 
			
		||||
          this.ratesChangedCallback(this.conversionRates);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        logger.err('Error updating fiat conversion rates: '  + (e instanceof Error ? e.message : e));
 | 
			
		||||
        await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
 | 
			
		||||
        retry++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new FiatConversion();
 | 
			
		||||
@ -127,7 +127,7 @@ class PoolsParser {
 | 
			
		||||
        if (!equals(JSON.parse(existingPool.addresses), poolObj.addresses) || !equals(JSON.parse(existingPool.regexes), poolObj.regexes)) {
 | 
			
		||||
          finalPoolDataUpdate.push(poolObj);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
      } else if (config.DATABASE.ENABLED) {
 | 
			
		||||
        // Double check that if we're not just renaming a pool (same address same regex)
 | 
			
		||||
        const [poolToRename]: any[] = await DB.query(`
 | 
			
		||||
          SELECT * FROM pools
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,6 @@ import blocks from './blocks';
 | 
			
		||||
import memPool from './mempool';
 | 
			
		||||
import backendInfo from './backend-info';
 | 
			
		||||
import mempoolBlocks from './mempool-blocks';
 | 
			
		||||
import fiatConversion from './fiat-conversion';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import loadingIndicators from './loading-indicators';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
@ -20,6 +19,7 @@ import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
 | 
			
		||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
 | 
			
		||||
import Audit from './audit';
 | 
			
		||||
import { deepClone } from '../utils/clone';
 | 
			
		||||
import priceUpdater from '../tasks/price-updater';
 | 
			
		||||
 | 
			
		||||
class WebsocketHandler {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
@ -214,7 +214,7 @@ class WebsocketHandler {
 | 
			
		||||
      'mempoolInfo': memPool.getMempoolInfo(),
 | 
			
		||||
      'vBytesPerSecond': memPool.getVBytesPerSecond(),
 | 
			
		||||
      'blocks': _blocks,
 | 
			
		||||
      'conversions': fiatConversion.getConversionRates(),
 | 
			
		||||
      'conversions': priceUpdater.latestPrices,
 | 
			
		||||
      'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
 | 
			
		||||
      'transactions': memPool.getLatestTransactions(),
 | 
			
		||||
      'backendInfo': backendInfo.getBackendInfo(),
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,6 @@ import memPool from './api/mempool';
 | 
			
		||||
import diskCache from './api/disk-cache';
 | 
			
		||||
import statistics from './api/statistics/statistics';
 | 
			
		||||
import websocketHandler from './api/websocket-handler';
 | 
			
		||||
import fiatConversion from './api/fiat-conversion';
 | 
			
		||||
import bisq from './api/bisq/bisq';
 | 
			
		||||
import bisqMarkets from './api/bisq/markets';
 | 
			
		||||
import logger from './logger';
 | 
			
		||||
@ -36,6 +35,7 @@ import liquidRoutes from './api/liquid/liquid.routes';
 | 
			
		||||
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
 | 
			
		||||
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
 | 
			
		||||
import forensicsService from './tasks/lightning/forensics.service';
 | 
			
		||||
import priceUpdater from './tasks/price-updater';
 | 
			
		||||
 | 
			
		||||
class Server {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
@ -78,25 +78,6 @@ class Server {
 | 
			
		||||
  async startServer(worker = false): Promise<void> {
 | 
			
		||||
    logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
 | 
			
		||||
 | 
			
		||||
    this.app
 | 
			
		||||
      .use((req: Request, res: Response, next: NextFunction) => {
 | 
			
		||||
        res.setHeader('Access-Control-Allow-Origin', '*');
 | 
			
		||||
        next();
 | 
			
		||||
      })
 | 
			
		||||
      .use(express.urlencoded({ extended: true }))
 | 
			
		||||
      .use(express.text({ type: ['text/plain', 'application/base64'] }))
 | 
			
		||||
      ;
 | 
			
		||||
 | 
			
		||||
    this.server = http.createServer(this.app);
 | 
			
		||||
    this.wss = new WebSocket.Server({ server: this.server });
 | 
			
		||||
 | 
			
		||||
    this.setUpWebsocketHandling();
 | 
			
		||||
 | 
			
		||||
    await syncAssets.syncAssets$();
 | 
			
		||||
    if (config.MEMPOOL.ENABLED) {
 | 
			
		||||
      diskCache.loadMempoolCache();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.DATABASE.ENABLED) {
 | 
			
		||||
      await DB.checkDbConnection();
 | 
			
		||||
      try {
 | 
			
		||||
@ -115,6 +96,29 @@ class Server {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.app
 | 
			
		||||
      .use((req: Request, res: Response, next: NextFunction) => {
 | 
			
		||||
        res.setHeader('Access-Control-Allow-Origin', '*');
 | 
			
		||||
        next();
 | 
			
		||||
      })
 | 
			
		||||
      .use(express.urlencoded({ extended: true }))
 | 
			
		||||
      .use(express.text({ type: ['text/plain', 'application/base64'] }))
 | 
			
		||||
      ;
 | 
			
		||||
 | 
			
		||||
    if (config.DATABASE.ENABLED) {
 | 
			
		||||
      await priceUpdater.$initializeLatestPriceWithDb();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.server = http.createServer(this.app);
 | 
			
		||||
    this.wss = new WebSocket.Server({ server: this.server });
 | 
			
		||||
 | 
			
		||||
    this.setUpWebsocketHandling();
 | 
			
		||||
 | 
			
		||||
    await syncAssets.syncAssets$();
 | 
			
		||||
    if (config.MEMPOOL.ENABLED) {
 | 
			
		||||
      diskCache.loadMempoolCache();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
 | 
			
		||||
      statistics.startStatistics();
 | 
			
		||||
    }
 | 
			
		||||
@ -127,7 +131,7 @@ class Server {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fiatConversion.startService();
 | 
			
		||||
    priceUpdater.$run();
 | 
			
		||||
 | 
			
		||||
    this.setUpHttpApiRoutes();
 | 
			
		||||
 | 
			
		||||
@ -221,7 +225,7 @@ class Server {
 | 
			
		||||
      memPool.setAsyncMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
 | 
			
		||||
      blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
 | 
			
		||||
    }
 | 
			
		||||
    fiatConversion.setProgressChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
 | 
			
		||||
    priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
 | 
			
		||||
    loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
@ -521,7 +521,7 @@ class BlocksRepository {
 | 
			
		||||
        CAST(AVG(blocks.height) as INT) as avgHeight,
 | 
			
		||||
        CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
 | 
			
		||||
        CAST(AVG(fees) as INT) as avgFees,
 | 
			
		||||
        prices.USD
 | 
			
		||||
        prices.*
 | 
			
		||||
        FROM blocks
 | 
			
		||||
        JOIN blocks_prices on blocks_prices.height = blocks.height
 | 
			
		||||
        JOIN prices on prices.id = blocks_prices.price_id
 | 
			
		||||
@ -550,7 +550,7 @@ class BlocksRepository {
 | 
			
		||||
        CAST(AVG(blocks.height) as INT) as avgHeight,
 | 
			
		||||
        CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
 | 
			
		||||
        CAST(AVG(reward) as INT) as avgRewards,
 | 
			
		||||
        prices.USD
 | 
			
		||||
        prices.*
 | 
			
		||||
        FROM blocks
 | 
			
		||||
        JOIN blocks_prices on blocks_prices.height = blocks.height
 | 
			
		||||
        JOIN prices on prices.id = blocks_prices.price_id
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,13 @@
 | 
			
		||||
import DB from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { Prices } from '../tasks/price-updater';
 | 
			
		||||
import { IConversionRates } from '../mempool.interfaces';
 | 
			
		||||
import priceUpdater from '../tasks/price-updater';
 | 
			
		||||
 | 
			
		||||
class PricesRepository {
 | 
			
		||||
  public async $savePrices(time: number, prices: Prices): Promise<void> {
 | 
			
		||||
    if (prices.USD === -1) {
 | 
			
		||||
      // Some historical price entries have not USD prices, so we just ignore them to avoid future UX issues
 | 
			
		||||
      // As of today there are only 4 (on 2013-09-05, 2013-09-19, 2013-09-12 and 2013-09-26) so that's fine
 | 
			
		||||
  public async $savePrices(time: number, prices: IConversionRates): Promise<void> {
 | 
			
		||||
    if (prices.USD === 0) {
 | 
			
		||||
      // Some historical price entries have no USD prices, so we just ignore them to avoid future UX issues
 | 
			
		||||
      // As of today there are only 4 (on 2013-09-05, 2013-0909, 2013-09-12 and 2013-09-26) so that's fine
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -23,22 +24,22 @@ class PricesRepository {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getOldestPriceTime(): Promise<number> {
 | 
			
		||||
    const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time LIMIT 1`);
 | 
			
		||||
    const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time LIMIT 1`);
 | 
			
		||||
    return oldestRow[0] ? oldestRow[0].time : 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getLatestPriceId(): Promise<number | null> {
 | 
			
		||||
    const [oldestRow] = await DB.query(`SELECT id from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
 | 
			
		||||
    const [oldestRow] = await DB.query(`SELECT id from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`);
 | 
			
		||||
    return oldestRow[0] ? oldestRow[0].id : null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getLatestPriceTime(): Promise<number> {
 | 
			
		||||
    const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
 | 
			
		||||
    const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`);
 | 
			
		||||
    return oldestRow[0] ? oldestRow[0].time : 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getPricesTimes(): Promise<number[]> {
 | 
			
		||||
    const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time`);
 | 
			
		||||
    const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time`);
 | 
			
		||||
    return times.map(time => time.time);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -46,6 +47,19 @@ class PricesRepository {
 | 
			
		||||
    const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time, id, USD from prices ORDER BY time`);
 | 
			
		||||
    return times;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getLatestConversionRates(): Promise<any> {
 | 
			
		||||
    const [rates]: any[] = await DB.query(`
 | 
			
		||||
      SELECT USD, EUR, GBP, CAD, CHF, AUD, JPY
 | 
			
		||||
      FROM prices
 | 
			
		||||
      ORDER BY time DESC
 | 
			
		||||
      LIMIT 1`
 | 
			
		||||
    );
 | 
			
		||||
    if (!rates || rates.length === 0) {
 | 
			
		||||
      return priceUpdater.getEmptyPricesObj();
 | 
			
		||||
    }
 | 
			
		||||
    return rates[0];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new PricesRepository();
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import path from "path";
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { IConversionRates } from '../mempool.interfaces';
 | 
			
		||||
import PricesRepository from '../repositories/PricesRepository';
 | 
			
		||||
import BitfinexApi from './price-feeds/bitfinex-api';
 | 
			
		||||
import BitflyerApi from './price-feeds/bitflyer-api';
 | 
			
		||||
@ -20,17 +21,7 @@ export interface PriceFeed {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PriceHistory {
 | 
			
		||||
  [timestamp: number]: Prices;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Prices {
 | 
			
		||||
  USD: number;
 | 
			
		||||
  EUR: number;
 | 
			
		||||
  GBP: number;
 | 
			
		||||
  CAD: number;
 | 
			
		||||
  CHF: number;
 | 
			
		||||
  AUD: number;
 | 
			
		||||
  JPY: number;
 | 
			
		||||
  [timestamp: number]: IConversionRates;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PriceUpdater {
 | 
			
		||||
@ -40,7 +31,8 @@ class PriceUpdater {
 | 
			
		||||
  running = false;
 | 
			
		||||
  feeds: PriceFeed[] = [];
 | 
			
		||||
  currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
 | 
			
		||||
  latestPrices: Prices;
 | 
			
		||||
  latestPrices: IConversionRates;
 | 
			
		||||
  private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.latestPrices = this.getEmptyPricesObj();
 | 
			
		||||
@ -52,18 +44,30 @@ class PriceUpdater {
 | 
			
		||||
    this.feeds.push(new GeminiApi());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getEmptyPricesObj(): Prices {
 | 
			
		||||
  public getEmptyPricesObj(): IConversionRates {
 | 
			
		||||
    return {
 | 
			
		||||
      USD: -1,
 | 
			
		||||
      EUR: -1,
 | 
			
		||||
      GBP: -1,
 | 
			
		||||
      CAD: -1,
 | 
			
		||||
      CHF: -1,
 | 
			
		||||
      AUD: -1,
 | 
			
		||||
      JPY: -1,
 | 
			
		||||
      USD: 0,
 | 
			
		||||
      EUR: 0,
 | 
			
		||||
      GBP: 0,
 | 
			
		||||
      CAD: 0,
 | 
			
		||||
      CHF: 0,
 | 
			
		||||
      AUD: 0,
 | 
			
		||||
      JPY: 0,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setRatesChangedCallback(fn: (rates: IConversionRates) => void) {
 | 
			
		||||
    this.ratesChangedCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * We execute this function before the websocket initialization since
 | 
			
		||||
   * the websocket init is not done asyncronously
 | 
			
		||||
   */
 | 
			
		||||
  public async $initializeLatestPriceWithDb(): Promise<void> {
 | 
			
		||||
    this.latestPrices = await PricesRepository.$getLatestConversionRates();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $run(): Promise<void> {
 | 
			
		||||
    if (this.running === true) {
 | 
			
		||||
      return;
 | 
			
		||||
@ -76,10 +80,9 @@ class PriceUpdater {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await this.$updatePrice();
 | 
			
		||||
      if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
 | 
			
		||||
        await this.$insertHistoricalPrices();
 | 
			
		||||
      } else {
 | 
			
		||||
        await this.$updatePrice();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
 | 
			
		||||
@ -144,6 +147,10 @@ class PriceUpdater {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.ratesChangedCallback) {
 | 
			
		||||
      this.ratesChangedCallback(this.latestPrices);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.lastRun = new Date().getTime() / 1000;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -213,7 +220,7 @@ class PriceUpdater {
 | 
			
		||||
 | 
			
		||||
    // Group them by timestamp and currency, for example
 | 
			
		||||
    // grouped[123456789]['USD'] = [1, 2, 3, 4];
 | 
			
		||||
    const grouped: Object = {};
 | 
			
		||||
    const grouped: any = {};
 | 
			
		||||
    for (const historicalEntry of historicalPrices) {
 | 
			
		||||
      for (const time in historicalEntry) {
 | 
			
		||||
        if (existingPriceTimes.includes(parseInt(time, 10))) {
 | 
			
		||||
@ -229,7 +236,7 @@ class PriceUpdater {
 | 
			
		||||
        for (const currency of this.currencies) {
 | 
			
		||||
          const price = historicalEntry[time][currency];
 | 
			
		||||
          if (price > 0) {
 | 
			
		||||
            grouped[time][currency].push(parseInt(price, 10));
 | 
			
		||||
            grouped[time][currency].push(typeof price === 'string' ? parseInt(price, 10) : price);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@ -238,7 +245,7 @@ class PriceUpdater {
 | 
			
		||||
    // Average prices and insert everything into the db
 | 
			
		||||
    let totalInserted = 0;
 | 
			
		||||
    for (const time in grouped) {
 | 
			
		||||
      const prices: Prices = this.getEmptyPricesObj();
 | 
			
		||||
      const prices: IConversionRates = this.getEmptyPricesObj();
 | 
			
		||||
      for (const currency in grouped[time]) {
 | 
			
		||||
        if (grouped[time][currency].length === 0) {
 | 
			
		||||
          continue;
 | 
			
		||||
 | 
			
		||||
@ -157,3 +157,41 @@ export const specialBlocks = {
 | 
			
		||||
    labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const fiatCurrencies = {
 | 
			
		||||
  AUD: {
 | 
			
		||||
    name: 'Australian Dollar',
 | 
			
		||||
    code: 'AUD',
 | 
			
		||||
    indexed: true,
 | 
			
		||||
  },
 | 
			
		||||
  CAD: {
 | 
			
		||||
    name: 'Canadian Dollar',
 | 
			
		||||
    code: 'CAD',
 | 
			
		||||
    indexed: true,
 | 
			
		||||
  },
 | 
			
		||||
  CHF: {
 | 
			
		||||
    name: 'Swiss Franc',
 | 
			
		||||
    code: 'CHF',
 | 
			
		||||
    indexed: true,
 | 
			
		||||
  },
 | 
			
		||||
  EUR: {
 | 
			
		||||
    name: 'Euro',
 | 
			
		||||
    code: 'EUR',
 | 
			
		||||
    indexed: true,
 | 
			
		||||
  },
 | 
			
		||||
  GBP: {
 | 
			
		||||
    name: 'Pound Sterling',
 | 
			
		||||
    code: 'GBP',
 | 
			
		||||
    indexed: true,
 | 
			
		||||
  },
 | 
			
		||||
  JPY: {
 | 
			
		||||
    name: 'Japanese Yen',
 | 
			
		||||
    code: 'JPY',
 | 
			
		||||
    indexed: true,
 | 
			
		||||
  },
 | 
			
		||||
  USD: {
 | 
			
		||||
    name: 'US Dollar',
 | 
			
		||||
    code: 'USD',
 | 
			
		||||
    indexed: true,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@ -17,6 +17,7 @@ import { StorageService } from './services/storage.service';
 | 
			
		||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
 | 
			
		||||
import { LanguageService } from './services/language.service';
 | 
			
		||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
 | 
			
		||||
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
 | 
			
		||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
 | 
			
		||||
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
 | 
			
		||||
import { AppPreloadingStrategy } from './app.preloading-strategy';
 | 
			
		||||
@ -34,6 +35,7 @@ const providers = [
 | 
			
		||||
  LanguageService,
 | 
			
		||||
  ShortenStringPipe,
 | 
			
		||||
  FiatShortenerPipe,
 | 
			
		||||
  FiatCurrencyPipe,
 | 
			
		||||
  CapAddressPipe,
 | 
			
		||||
  AppPreloadingStrategy,
 | 
			
		||||
  { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
 | 
			
		||||
  <span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ conversions.USD * (satoshis / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
 | 
			
		||||
  <span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}</span>
 | 
			
		||||
</ng-container>
 | 
			
		||||
<ng-template #viewFiatVin>
 | 
			
		||||
  <ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy } from '@angular/core';
 | 
			
		||||
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Observable, Subscription } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@ -10,10 +10,12 @@ import { Observable, Subscription } from 'rxjs';
 | 
			
		||||
})
 | 
			
		||||
export class AmountComponent implements OnInit, OnDestroy {
 | 
			
		||||
  conversions$: Observable<any>;
 | 
			
		||||
  currency: string;
 | 
			
		||||
  viewFiat$: Observable<boolean>;
 | 
			
		||||
  network = '';
 | 
			
		||||
 | 
			
		||||
  stateSubscription: Subscription;
 | 
			
		||||
  currencySubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  @Input() satoshis: number;
 | 
			
		||||
  @Input() digitsInfo = '1.8-8';
 | 
			
		||||
@ -22,7 +24,13 @@ export class AmountComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) { }
 | 
			
		||||
    private cd: ChangeDetectorRef,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
 | 
			
		||||
      this.currency = fiat;
 | 
			
		||||
      this.cd.markForCheck();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.viewFiat$ = this.stateService.viewFiat$.asObservable();
 | 
			
		||||
@ -34,6 +42,7 @@ export class AmountComponent implements OnInit, OnDestroy {
 | 
			
		||||
    if (this.stateSubscription) {
 | 
			
		||||
      this.stateSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    this.currencySubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,19 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
 | 
			
		||||
import { EChartsOption, graphic } from 'echarts';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { Observable, Subscription } from 'rxjs';
 | 
			
		||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
 | 
			
		||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
 | 
			
		||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { StorageService } from '../../services/storage.service';
 | 
			
		||||
import { MiningService } from '../../services/mining.service';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
 | 
			
		||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
 | 
			
		||||
import { fiatCurrencies } from '../../app.constants';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-block-fees-graph',
 | 
			
		||||
@ -44,6 +47,9 @@ export class BlockFeesGraphComponent implements OnInit {
 | 
			
		||||
  timespan = '';
 | 
			
		||||
  chartInstance: any = undefined;
 | 
			
		||||
 | 
			
		||||
  currencySubscription: Subscription;
 | 
			
		||||
  currency: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
@ -51,11 +57,21 @@ export class BlockFeesGraphComponent implements OnInit {
 | 
			
		||||
    private formBuilder: UntypedFormBuilder,
 | 
			
		||||
    private storageService: StorageService,
 | 
			
		||||
    private miningService: MiningService,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    private fiatShortenerPipe: FiatShortenerPipe,
 | 
			
		||||
    private fiatCurrencyPipe: FiatCurrencyPipe,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue('1y');
 | 
			
		||||
 | 
			
		||||
    this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
 | 
			
		||||
      if (fiat && fiatCurrencies[fiat]?.indexed) {
 | 
			
		||||
        this.currency = fiat;
 | 
			
		||||
      } else {
 | 
			
		||||
        this.currency = 'USD';
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
@ -84,7 +100,7 @@ export class BlockFeesGraphComponent implements OnInit {
 | 
			
		||||
              tap((response) => {
 | 
			
		||||
                this.prepareChartOptions({
 | 
			
		||||
                  blockFees: response.body.map(val => [val.timestamp * 1000, val.avgFees / 100000000, val.avgHeight]),
 | 
			
		||||
                  blockFeesUSD: response.body.filter(val => val.USD > 0).map(val => [val.timestamp * 1000, val.avgFees / 100000000 * val.USD, val.avgHeight]),
 | 
			
		||||
                  blockFeesFiat: response.body.filter(val => val[this.currency] > 0).map(val => [val.timestamp * 1000, val.avgFees / 100000000 * val[this.currency], val.avgHeight]),
 | 
			
		||||
                });
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
              }),
 | 
			
		||||
@ -157,7 +173,7 @@ export class BlockFeesGraphComponent implements OnInit {
 | 
			
		||||
            if (tick.seriesIndex === 0) {
 | 
			
		||||
              tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC<br>`;
 | 
			
		||||
            } else if (tick.seriesIndex === 1) {
 | 
			
		||||
              tooltip += `${tick.marker} ${tick.seriesName}: ${formatCurrency(tick.data[1], this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0')}<br>`;
 | 
			
		||||
              tooltip += `${tick.marker} ${tick.seriesName}: ${this.fiatCurrencyPipe.transform(tick.data[1], null, this.currency) }<br>`;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
@ -184,7 +200,7 @@ export class BlockFeesGraphComponent implements OnInit {
 | 
			
		||||
            icon: 'roundRect',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Fees USD',
 | 
			
		||||
            name: 'Fees ' + this.currency,
 | 
			
		||||
            inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
            textStyle: {  
 | 
			
		||||
              color: 'white',
 | 
			
		||||
@ -216,7 +232,7 @@ export class BlockFeesGraphComponent implements OnInit {
 | 
			
		||||
          axisLabel: {
 | 
			
		||||
            color: 'rgb(110, 112, 121)',
 | 
			
		||||
            formatter: function(val) {
 | 
			
		||||
              return this.fiatShortenerPipe.transform(val);
 | 
			
		||||
              return this.fiatShortenerPipe.transform(val, null, this.currency);
 | 
			
		||||
            }.bind(this)
 | 
			
		||||
          },
 | 
			
		||||
          splitLine: {
 | 
			
		||||
@ -243,8 +259,8 @@ export class BlockFeesGraphComponent implements OnInit {
 | 
			
		||||
          legendHoverLink: false,
 | 
			
		||||
          zlevel: 1,
 | 
			
		||||
          yAxisIndex: 1,
 | 
			
		||||
          name: 'Fees USD',
 | 
			
		||||
          data: data.blockFeesUSD,
 | 
			
		||||
          name: 'Fees ' + this.currency,
 | 
			
		||||
          data: data.blockFeesFiat,
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: 0.25,
 | 
			
		||||
          symbol: 'none',
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,19 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
 | 
			
		||||
import { EChartsOption, graphic } from 'echarts';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { Observable, Subscription } from 'rxjs';
 | 
			
		||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
 | 
			
		||||
import { formatNumber } from '@angular/common';
 | 
			
		||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
 | 
			
		||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
 | 
			
		||||
import { MiningService } from '../../services/mining.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { StorageService } from '../../services/storage.service';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
 | 
			
		||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
 | 
			
		||||
import { fiatCurrencies } from '../../app.constants';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-block-rewards-graph',
 | 
			
		||||
@ -44,16 +47,28 @@ export class BlockRewardsGraphComponent implements OnInit {
 | 
			
		||||
  timespan = '';
 | 
			
		||||
  chartInstance: any = undefined;
 | 
			
		||||
 | 
			
		||||
  currencySubscription: Subscription;
 | 
			
		||||
  currency: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private formBuilder: UntypedFormBuilder,
 | 
			
		||||
    private miningService: MiningService,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private storageService: StorageService,
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    private fiatShortenerPipe: FiatShortenerPipe,
 | 
			
		||||
    private fiatCurrencyPipe: FiatCurrencyPipe,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
 | 
			
		||||
      if (fiat && fiatCurrencies[fiat]?.indexed) {
 | 
			
		||||
        this.currency = fiat;
 | 
			
		||||
      } else {
 | 
			
		||||
        this.currency = 'USD';
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
@ -82,7 +97,7 @@ export class BlockRewardsGraphComponent implements OnInit {
 | 
			
		||||
              tap((response) => {
 | 
			
		||||
                this.prepareChartOptions({
 | 
			
		||||
                  blockRewards: response.body.map(val => [val.timestamp * 1000, val.avgRewards / 100000000, val.avgHeight]),
 | 
			
		||||
                  blockRewardsUSD: response.body.filter(val => val.USD > 0).map(val => [val.timestamp * 1000, val.avgRewards / 100000000 * val.USD, val.avgHeight]),
 | 
			
		||||
                  blockRewardsFiat: response.body.filter(val => val[this.currency] > 0).map(val => [val.timestamp * 1000, val.avgRewards / 100000000 * val[this.currency], val.avgHeight]),
 | 
			
		||||
                });
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
              }),
 | 
			
		||||
@ -157,7 +172,7 @@ export class BlockRewardsGraphComponent implements OnInit {
 | 
			
		||||
            if (tick.seriesIndex === 0) {
 | 
			
		||||
              tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC<br>`;
 | 
			
		||||
            } else if (tick.seriesIndex === 1) {
 | 
			
		||||
              tooltip += `${tick.marker} ${tick.seriesName}: ${formatCurrency(tick.data[1], this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0')}<br>`;
 | 
			
		||||
              tooltip += `${tick.marker} ${tick.seriesName}: ${this.fiatCurrencyPipe.transform(tick.data[1], null, this.currency)}<br>`;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
@ -184,7 +199,7 @@ export class BlockRewardsGraphComponent implements OnInit {
 | 
			
		||||
            icon: 'roundRect',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Rewards USD',
 | 
			
		||||
            name: 'Rewards ' + this.currency,
 | 
			
		||||
            inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
            textStyle: {  
 | 
			
		||||
              color: 'white',
 | 
			
		||||
@ -228,7 +243,7 @@ export class BlockRewardsGraphComponent implements OnInit {
 | 
			
		||||
          axisLabel: {
 | 
			
		||||
            color: 'rgb(110, 112, 121)',
 | 
			
		||||
            formatter: function(val) {
 | 
			
		||||
              return this.fiatShortenerPipe.transform(val);
 | 
			
		||||
              return this.fiatShortenerPipe.transform(val, null, this.currency);
 | 
			
		||||
            }.bind(this)
 | 
			
		||||
          },
 | 
			
		||||
          splitLine: {
 | 
			
		||||
@ -251,8 +266,8 @@ export class BlockRewardsGraphComponent implements OnInit {
 | 
			
		||||
          legendHoverLink: false,
 | 
			
		||||
          zlevel: 1,
 | 
			
		||||
          yAxisIndex: 1,
 | 
			
		||||
          name: 'Rewards USD',
 | 
			
		||||
          data: data.blockRewardsUSD,
 | 
			
		||||
          name: 'Rewards ' + this.currency,
 | 
			
		||||
          data: data.blockRewardsFiat,
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: 0.25,
 | 
			
		||||
          symbol: 'none',
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,5 @@
 | 
			
		||||
<div [formGroup]="fiatForm" class="text-small text-center">
 | 
			
		||||
    <select formControlName="fiat" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 200px;" (change)="changeFiat()">
 | 
			
		||||
        <option *ngFor="let currency of currencies" [value]="currency[1].code">{{ currency[1].name + " (" + currency[1].code + ")" }}</option>
 | 
			
		||||
    </select>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,45 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
 | 
			
		||||
import { StorageService } from '../../services/storage.service';
 | 
			
		||||
import { fiatCurrencies } from '../../app.constants';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-fiat-selector',
 | 
			
		||||
  templateUrl: './fiat-selector.component.html',
 | 
			
		||||
  styleUrls: ['./fiat-selector.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush
 | 
			
		||||
})
 | 
			
		||||
export class FiatSelectorComponent implements OnInit {
 | 
			
		||||
  fiatForm: UntypedFormGroup;
 | 
			
		||||
  currencies = Object.entries(fiatCurrencies).sort((a: any, b: any) => {
 | 
			
		||||
    if (a[1].name < b[1].name) {
 | 
			
		||||
      return -1;
 | 
			
		||||
    }
 | 
			
		||||
    if (a[1].name > b[1].name) {
 | 
			
		||||
      return 1;
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private formBuilder: UntypedFormBuilder,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private storageService: StorageService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.fiatForm = this.formBuilder.group({
 | 
			
		||||
      fiat: ['USD']
 | 
			
		||||
    });
 | 
			
		||||
    this.stateService.fiatCurrency$.subscribe((fiat) => {
 | 
			
		||||
      this.fiatForm.get('fiat')?.setValue(fiat);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  changeFiat() {
 | 
			
		||||
    const newFiat = this.fiatForm.get('fiat')?.value;
 | 
			
		||||
    this.storageService.setValue('fiat-preference', newFiat);
 | 
			
		||||
    this.stateService.fiatCurrency$.next(newFiat);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
<div [formGroup]="languageForm" class="text-small text-center">
 | 
			
		||||
    <select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 130px;" (change)="changeLanguage()">
 | 
			
		||||
    <select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 200px;" (change)="changeLanguage()">
 | 
			
		||||
        <option *ngFor="let lang of languages" [value]="lang.code">{{ lang.name }}</option>
 | 
			
		||||
    </select>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -74,4 +74,24 @@
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="pref-selectors">
 | 
			
		||||
    <div class="selector">
 | 
			
		||||
      <app-language-selector></app-language-selector>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="selector">
 | 
			
		||||
      <app-fiat-selector></app-fiat-selector>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="terms-of-service">
 | 
			
		||||
    <a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
 | 
			
		||||
    |
 | 
			
		||||
    <a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a>
 | 
			
		||||
    |
 | 
			
		||||
    <a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -103,4 +103,23 @@
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.terms-of-service {
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pref-selectors {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
 | 
			
		||||
  .selector {
 | 
			
		||||
    margin-left: .5em;
 | 
			
		||||
    margin-bottom: .5em;
 | 
			
		||||
    &:first {
 | 
			
		||||
      margin-left: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -235,10 +235,10 @@
 | 
			
		||||
            </span>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="reward text-right">
 | 
			
		||||
            <app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2"></app-amount>
 | 
			
		||||
            <app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="fees text-right">
 | 
			
		||||
            <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2"></app-amount>
 | 
			
		||||
            <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="txs text-right">
 | 
			
		||||
            {{ block.tx_count | number }}
 | 
			
		||||
 | 
			
		||||
@ -122,7 +122,7 @@
 | 
			
		||||
            <thead>
 | 
			
		||||
              <th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
 | 
			
		||||
              <th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
 | 
			
		||||
              <th class="table-cell-fiat" *ngIf="(network$ | async) === ''" i18n="dashboard.latest-transactions.USD">USD</th>
 | 
			
		||||
              <th class="table-cell-fiat" *ngIf="(network$ | async) === ''">{{ currency }}</th>
 | 
			
		||||
              <th class="table-cell-fees" i18n="transaction.fee|Transaction fee">Fee</th>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
@ -144,7 +144,14 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <app-language-selector></app-language-selector>
 | 
			
		||||
  <div class="pref-selectors">
 | 
			
		||||
    <div class="selector">
 | 
			
		||||
      <app-language-selector></app-language-selector>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="selector">
 | 
			
		||||
      <app-fiat-selector></app-fiat-selector>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="terms-of-service">
 | 
			
		||||
    <a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
 | 
			
		||||
 | 
			
		||||
@ -323,4 +323,19 @@
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pref-selectors {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
 | 
			
		||||
  .selector {
 | 
			
		||||
    margin-left: .5em;
 | 
			
		||||
    margin-bottom: .5em;
 | 
			
		||||
    &:first {
 | 
			
		||||
      margin-left: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
 | 
			
		||||
import { combineLatest, merge, Observable, of } from 'rxjs';
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
 | 
			
		||||
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
 | 
			
		||||
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
@ -7,7 +7,6 @@ import { ApiService } from '../services/api.service';
 | 
			
		||||
import { StateService } from '../services/state.service';
 | 
			
		||||
import { WebsocketService } from '../services/websocket.service';
 | 
			
		||||
import { SeoService } from '../services/seo.service';
 | 
			
		||||
import { StorageService } from '../services/storage.service';
 | 
			
		||||
 | 
			
		||||
interface MempoolBlocksData {
 | 
			
		||||
  blocks: number;
 | 
			
		||||
@ -32,7 +31,7 @@ interface MempoolStatsData {
 | 
			
		||||
  styleUrls: ['./dashboard.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush
 | 
			
		||||
})
 | 
			
		||||
export class DashboardComponent implements OnInit {
 | 
			
		||||
export class DashboardComponent implements OnInit, OnDestroy {
 | 
			
		||||
  featuredAssets$: Observable<any>;
 | 
			
		||||
  network$: Observable<string>;
 | 
			
		||||
  mempoolBlocksData$: Observable<MempoolBlocksData>;
 | 
			
		||||
@ -47,16 +46,20 @@ export class DashboardComponent implements OnInit {
 | 
			
		||||
  transactionsWeightPerSecondOptions: any;
 | 
			
		||||
  isLoadingWebSocket$: Observable<boolean>;
 | 
			
		||||
  liquidPegsMonth$: Observable<any>;
 | 
			
		||||
  currencySubscription: Subscription;
 | 
			
		||||
  currency: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) private locale: string,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private storageService: StorageService,
 | 
			
		||||
    private seoService: SeoService
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.currencySubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
 | 
			
		||||
    this.seoService.resetTitle();
 | 
			
		||||
@ -213,6 +216,10 @@ export class DashboardComponent implements OnInit {
 | 
			
		||||
          share(),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
 | 
			
		||||
      this.currency = fiat;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
 | 
			
		||||
 | 
			
		||||
@ -1 +1 @@
 | 
			
		||||
<span class="green-color">{{ (conversions$ | async)?.USD * value / 100000000 | currency:'USD':'symbol':digitsInfo }}</span>
 | 
			
		||||
<span class="green-color" *ngIf="(conversions$ | async) as conversions">{{ (conversions ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }}</span>
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core';
 | 
			
		||||
import { Observable, Subscription } from 'rxjs';
 | 
			
		||||
import { StateService } from '../services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
@ -8,18 +8,30 @@ import { StateService } from '../services/state.service';
 | 
			
		||||
  styleUrls: ['./fiat.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class FiatComponent implements OnInit {
 | 
			
		||||
export class FiatComponent implements OnInit, OnDestroy {
 | 
			
		||||
  conversions$: Observable<any>;
 | 
			
		||||
  currencySubscription: Subscription;
 | 
			
		||||
  currency: string;
 | 
			
		||||
 | 
			
		||||
  @Input() value: number;
 | 
			
		||||
  @Input() digitsInfo = '1.2-2';
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) { }
 | 
			
		||||
    private cd: ChangeDetectorRef,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
 | 
			
		||||
      this.currency = fiat;
 | 
			
		||||
      this.cd.markForCheck();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.conversions$ = this.stateService.conversions$.asObservable();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.currencySubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="box-right">
 | 
			
		||||
    <div class="second-line"><ng-container *ngTemplateOutlet="xChannels; context: {$implicit: channel.channels }"></ng-container></div>
 | 
			
		||||
    <div class="second-line"><app-amount [satoshis]="channel.capacity" digitsInfo="1.2-2"></app-amount></div>
 | 
			
		||||
    <div class="second-line"><app-amount [satoshis]="channel.capacity" digitsInfo="1.2-2" [noFiat]="true"></app-amount></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@
 | 
			
		||||
          <span i18n="shared.sats">sats</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <span class="fiat" *ngIf="statistics.previous">
 | 
			
		||||
          <app-change [current]="statistics.latest?.avg_capacity" [previous]="statistics.previous?.avg_capacity"></app-change>
 | 
			
		||||
          <app-fiat [value]="statistics.latest?.avg_capacity" digitsInfo="1.0-0" ></app-fiat>
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@ -63,7 +63,7 @@
 | 
			
		||||
          <span i18n="shared.sats">sats</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <span class="fiat" *ngIf="statistics.previous">
 | 
			
		||||
          <app-change [current]="statistics.latest?.med_capacity" [previous]="statistics.previous?.med_capacity"></app-change>
 | 
			
		||||
          <app-fiat [value]="statistics.latest?.med_capacity" digitsInfo="1.0-0" ></app-fiat>
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -84,8 +84,22 @@
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="text-small text-center mt-1" *ngIf="officialMempoolSpace">
 | 
			
		||||
    <a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]">Connect to our nodes</a>
 | 
			
		||||
  <div class="pref-selectors">
 | 
			
		||||
    <div class="selector">
 | 
			
		||||
      <app-language-selector></app-language-selector>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="selector">
 | 
			
		||||
      <app-fiat-selector></app-fiat-selector>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="terms-of-service">
 | 
			
		||||
    <a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
 | 
			
		||||
    |
 | 
			
		||||
    <a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a>
 | 
			
		||||
    |
 | 
			
		||||
    <a *ngIf="officialMempoolSpace" [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]">Connect to our nodes</a>
 | 
			
		||||
    <a *ngIf="!officialMempoolSpace" [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
@ -103,4 +103,23 @@
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.terms-of-service {
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pref-selectors {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
 | 
			
		||||
  .selector {
 | 
			
		||||
    margin-left: .5em;
 | 
			
		||||
    margin-bottom: .5em;
 | 
			
		||||
    &:first {
 | 
			
		||||
      margin-left: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -5,11 +5,10 @@
 | 
			
		||||
      <div class="card-text" i18n-ngbTooltip="mining.percentage-change-last-week" ngbTooltip="Percentage change past week"
 | 
			
		||||
        [disableTooltip]="!statistics.previous" placement="bottom">
 | 
			
		||||
        <div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
 | 
			
		||||
          <app-amount [satoshis]="statistics.latest?.total_capacity" digitsInfo="1.2-2"></app-amount>
 | 
			
		||||
          <app-amount [satoshis]="statistics.latest?.total_capacity" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
 | 
			
		||||
        </div>
 | 
			
		||||
        <span class="fiat" *ngIf="statistics.previous">
 | 
			
		||||
          <app-change [current]="statistics.latest?.total_capacity" [previous]="statistics.previous?.total_capacity">
 | 
			
		||||
          </app-change>
 | 
			
		||||
          <app-fiat [value]="statistics.latest?.total_capacity" digitsInfo="1.0-0" ></app-fiat>
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -121,6 +121,7 @@ export class StateService {
 | 
			
		||||
  timeLtr: BehaviorSubject<boolean>;
 | 
			
		||||
  hideFlow: BehaviorSubject<boolean>;
 | 
			
		||||
  hideAudit: BehaviorSubject<boolean>;
 | 
			
		||||
  fiatCurrency$: BehaviorSubject<string>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(PLATFORM_ID) private platformId: any,
 | 
			
		||||
@ -186,6 +187,9 @@ export class StateService {
 | 
			
		||||
    this.hideAudit.subscribe((hide) => {
 | 
			
		||||
      this.storageService.setValue('audit-preference', hide ? 'hide' : 'show');
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    const fiatPreference = this.storageService.getValue('fiat-preference');
 | 
			
		||||
    this.fiatCurrency$ = new BehaviorSubject<string>(fiatPreference || 'USD');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setNetworkBasedonUrl(url: string) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								frontend/src/app/shared/pipes/fiat-currency.pipe.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								frontend/src/app/shared/pipes/fiat-currency.pipe.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
import { formatCurrency, getCurrencySymbol } from '@angular/common';
 | 
			
		||||
import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
  name: 'fiatCurrency'
 | 
			
		||||
})
 | 
			
		||||
export class FiatCurrencyPipe implements PipeTransform {
 | 
			
		||||
  fiatSubscription: Subscription;
 | 
			
		||||
  currency: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.fiatSubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
 | 
			
		||||
      this.currency = fiat;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  transform(num: number, ...args: any[]): unknown {
 | 
			
		||||
    const digits = args[0] || 1;
 | 
			
		||||
    const currency = args[1] || this.currency || 'USD';
 | 
			
		||||
 | 
			
		||||
    return new Intl.NumberFormat(this.locale, { style: 'currency', currency }).format(num);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,20 +1,30 @@
 | 
			
		||||
import { formatCurrency, getCurrencySymbol } from '@angular/common';
 | 
			
		||||
import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
  name: 'fiatShortener'
 | 
			
		||||
})
 | 
			
		||||
export class FiatShortenerPipe implements PipeTransform {
 | 
			
		||||
  fiatSubscription: Subscription;
 | 
			
		||||
  currency: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string
 | 
			
		||||
  ) {}
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.fiatSubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
 | 
			
		||||
      this.currency = fiat;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  transform(num: number, ...args: any[]): unknown {
 | 
			
		||||
    const digits = args[0] || 1;
 | 
			
		||||
    const unit = args[1] || undefined;
 | 
			
		||||
    const currency = args[1] || this.currency || 'USD';
 | 
			
		||||
 | 
			
		||||
    if (num < 1000) {
 | 
			
		||||
      return num.toFixed(digits);
 | 
			
		||||
      return new Intl.NumberFormat(this.locale, { style: 'currency', currency, maximumFractionDigits: 1 }).format(num);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const lookup = [
 | 
			
		||||
@ -30,8 +40,8 @@ export class FiatShortenerPipe implements PipeTransform {
 | 
			
		||||
    const item = lookup.slice().reverse().find((item) => num >= item.value);
 | 
			
		||||
 | 
			
		||||
    let result = item ? (num / item.value).toFixed(digits).replace(rx, '$1') : '0';
 | 
			
		||||
    result = formatCurrency(parseInt(result, 10), this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0');
 | 
			
		||||
 | 
			
		||||
    result = new Intl.NumberFormat(this.locale, { style: 'currency', currency, maximumFractionDigits: 0 }).format(item ? num / item.value : 0);
 | 
			
		||||
    
 | 
			
		||||
    return result + item.symbol;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -23,6 +23,7 @@ import { RelativeUrlPipe } from './pipes/relative-url/relative-url.pipe';
 | 
			
		||||
import { ScriptpubkeyTypePipe } from './pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe';
 | 
			
		||||
import { BytesPipe } from './pipes/bytes-pipe/bytes.pipe';
 | 
			
		||||
import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe';
 | 
			
		||||
import { FiatCurrencyPipe } from './pipes/fiat-currency.pipe';
 | 
			
		||||
import { BlockchainComponent } from '../components/blockchain/blockchain.component';
 | 
			
		||||
import { TimeSinceComponent } from '../components/time-since/time-since.component';
 | 
			
		||||
import { TimeUntilComponent } from '../components/time-until/time-until.component';
 | 
			
		||||
@ -34,6 +35,7 @@ import { TxFeaturesComponent } from '../components/tx-features/tx-features.compo
 | 
			
		||||
import { TxFeeRatingComponent } from '../components/tx-fee-rating/tx-fee-rating.component';
 | 
			
		||||
import { ReactiveFormsModule } from '@angular/forms';
 | 
			
		||||
import { LanguageSelectorComponent } from '../components/language-selector/language-selector.component';
 | 
			
		||||
import { FiatSelectorComponent } from '../components/fiat-selector/fiat-selector.component';
 | 
			
		||||
import { ColoredPriceDirective } from './directives/colored-price.directive';
 | 
			
		||||
import { NoSanitizePipe } from './pipes/no-sanitize.pipe';
 | 
			
		||||
import { MempoolBlocksComponent } from '../components/mempool-blocks/mempool-blocks.component';
 | 
			
		||||
@ -93,6 +95,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
 | 
			
		||||
    TxFeaturesComponent,
 | 
			
		||||
    TxFeeRatingComponent,
 | 
			
		||||
    LanguageSelectorComponent,
 | 
			
		||||
    FiatSelectorComponent,
 | 
			
		||||
    ScriptpubkeyTypePipe,
 | 
			
		||||
    RelativeUrlPipe,
 | 
			
		||||
    NoSanitizePipe,
 | 
			
		||||
@ -107,6 +110,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
 | 
			
		||||
    CapAddressPipe,
 | 
			
		||||
    Decimal2HexPipe,
 | 
			
		||||
    FeeRoundingPipe,
 | 
			
		||||
    FiatCurrencyPipe,
 | 
			
		||||
    ColoredPriceDirective,
 | 
			
		||||
    BlockchainComponent,
 | 
			
		||||
    MempoolBlocksComponent,
 | 
			
		||||
@ -199,6 +203,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
 | 
			
		||||
    TxFeaturesComponent,
 | 
			
		||||
    TxFeeRatingComponent,
 | 
			
		||||
    LanguageSelectorComponent,
 | 
			
		||||
    FiatSelectorComponent,
 | 
			
		||||
    ScriptpubkeyTypePipe,
 | 
			
		||||
    RelativeUrlPipe,
 | 
			
		||||
    Hex2asciiPipe,
 | 
			
		||||
@ -207,6 +212,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
 | 
			
		||||
    BytesPipe,
 | 
			
		||||
    VbytesPipe,
 | 
			
		||||
    WuBytesPipe,
 | 
			
		||||
    FiatCurrencyPipe,
 | 
			
		||||
    CeilPipe,
 | 
			
		||||
    ShortenStringPipe,
 | 
			
		||||
    CapAddressPipe,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user