diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 0f9e6b7f6..84c919e5d 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -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 diff --git a/backend/src/api/fiat-conversion.ts b/backend/src/api/fiat-conversion.ts deleted file mode 100644 index ffbe6a758..000000000 --- a/backend/src/api/fiat-conversion.ts +++ /dev/null @@ -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 { - type axiosOptions = { - headers: { - 'User-Agent': string - }; - timeout: number; - httpAgent?: http.Agent; - httpsAgent?: https.Agent; - } - const setDelay = (secs: number = 1): Promise => 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(); diff --git a/backend/src/api/pools-parser.ts b/backend/src/api/pools-parser.ts index c3cc994a2..e37414bbe 100644 --- a/backend/src/api/pools-parser.ts +++ b/backend/src/api/pools-parser.ts @@ -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 diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 9334f3717..c1c3b3995 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -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(), diff --git a/backend/src/index.ts b/backend/src/index.ts index 8371e927f..a81275066 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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 { 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)); } diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index df98719b9..355187e21 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -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 diff --git a/backend/src/repositories/PricesRepository.ts b/backend/src/repositories/PricesRepository.ts index cc79ff2a6..bc606e68b 100644 --- a/backend/src/repositories/PricesRepository.ts +++ b/backend/src/repositories/PricesRepository.ts @@ -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 { - 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 { + 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 { - 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 { - 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 { - 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 { - 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 { + 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(); diff --git a/backend/src/tasks/price-updater.ts b/backend/src/tasks/price-updater.ts index 9e7e5910a..e42c5887a 100644 --- a/backend/src/tasks/price-updater.ts +++ b/backend/src/tasks/price-updater.ts @@ -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 { + this.latestPrices = await PricesRepository.$getLatestConversionRates(); + } + public async $run(): Promise { 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; diff --git a/frontend/src/app/app.constants.ts b/frontend/src/app/app.constants.ts index 47e12cfcd..5cc446dbf 100644 --- a/frontend/src/app/app.constants.ts +++ b/frontend/src/app/app.constants.ts @@ -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, + }, +}; \ No newline at end of file diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index b7bd1526f..f26b4a924 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -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 } diff --git a/frontend/src/app/components/amount/amount.component.html b/frontend/src/app/components/amount/amount.component.html index c4946ddf8..3526b554b 100644 --- a/frontend/src/app/components/amount/amount.component.html +++ b/frontend/src/app/components/amount/amount.component.html @@ -1,5 +1,5 @@ - {{ addPlus && satoshis >= 0 ? '+' : '' }}{{ conversions.USD * (satoshis / 100000000) | currency:'USD':'symbol':'1.2-2' }} + {{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }} diff --git a/frontend/src/app/components/amount/amount.component.ts b/frontend/src/app/components/amount/amount.component.ts index f9bba4318..cfdc50468 100644 --- a/frontend/src/app/components/amount/amount.component.ts +++ b/frontend/src/app/components/amount/amount.component.ts @@ -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; + currency: string; viewFiat$: Observable; 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(); } } diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts index 0c2f228b8..42667126f 100644 --- a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts @@ -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
`; } else if (tick.seriesIndex === 1) { - tooltip += `${tick.marker} ${tick.seriesName}: ${formatCurrency(tick.data[1], this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0')}
`; + tooltip += `${tick.marker} ${tick.seriesName}: ${this.fiatCurrencyPipe.transform(tick.data[1], null, this.currency) }
`; } } @@ -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', diff --git a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts index fe80b0c97..0e5e339fa 100644 --- a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts +++ b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts @@ -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
`; } else if (tick.seriesIndex === 1) { - tooltip += `${tick.marker} ${tick.seriesName}: ${formatCurrency(tick.data[1], this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0')}
`; + tooltip += `${tick.marker} ${tick.seriesName}: ${this.fiatCurrencyPipe.transform(tick.data[1], null, this.currency)}
`; } } @@ -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', diff --git a/frontend/src/app/components/fiat-selector/fiat-selector.component.html b/frontend/src/app/components/fiat-selector/fiat-selector.component.html new file mode 100644 index 000000000..dd32b1815 --- /dev/null +++ b/frontend/src/app/components/fiat-selector/fiat-selector.component.html @@ -0,0 +1,5 @@ +
+ +
diff --git a/frontend/src/app/components/fiat-selector/fiat-selector.component.scss b/frontend/src/app/components/fiat-selector/fiat-selector.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/fiat-selector/fiat-selector.component.ts b/frontend/src/app/components/fiat-selector/fiat-selector.component.ts new file mode 100644 index 000000000..337ef11f3 --- /dev/null +++ b/frontend/src/app/components/fiat-selector/fiat-selector.component.ts @@ -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); + } +} diff --git a/frontend/src/app/components/language-selector/language-selector.component.html b/frontend/src/app/components/language-selector/language-selector.component.html index dee1e3acb..22839441c 100644 --- a/frontend/src/app/components/language-selector/language-selector.component.html +++ b/frontend/src/app/components/language-selector/language-selector.component.html @@ -1,5 +1,5 @@
-
diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html index 36aec8906..1a932567c 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html @@ -74,4 +74,24 @@ + +
+
+ +
+
+ +
+
+ + + +
+ diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss index 303591974..218b8e04d 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss @@ -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; + } + } } \ No newline at end of file diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index ef907215c..53982ca86 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -235,10 +235,10 @@ - + - + {{ block.tx_count | number }} diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 397021048..f3d96e187 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -122,7 +122,7 @@ TXID Amount - USD + {{ currency }} Fee @@ -144,7 +144,14 @@ - +
+
+ +
+
+ +
+
Terms of Service diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss index a7e21ba94..4a844eb52 100644 --- a/frontend/src/app/dashboard/dashboard.component.scss +++ b/frontend/src/app/dashboard/dashboard.component.scss @@ -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; + } + } } \ No newline at end of file diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 45b402314..1eb305220 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -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; network$: Observable; mempoolBlocksData$: Observable; @@ -47,16 +46,20 @@ export class DashboardComponent implements OnInit { transactionsWeightPerSecondOptions: any; isLoadingWebSocket$: Observable; liquidPegsMonth$: Observable; + 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[]) { diff --git a/frontend/src/app/fiat/fiat.component.html b/frontend/src/app/fiat/fiat.component.html index 19a5c8aa0..99a177cc0 100644 --- a/frontend/src/app/fiat/fiat.component.html +++ b/frontend/src/app/fiat/fiat.component.html @@ -1 +1 @@ -{{ (conversions$ | async)?.USD * value / 100000000 | currency:'USD':'symbol':digitsInfo }} \ No newline at end of file +{{ (conversions ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }} \ No newline at end of file diff --git a/frontend/src/app/fiat/fiat.component.ts b/frontend/src/app/fiat/fiat.component.ts index 9d22355d8..bc0f6a0de 100644 --- a/frontend/src/app/fiat/fiat.component.ts +++ b/frontend/src/app/fiat/fiat.component.ts @@ -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; + 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(); + } + } diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html index 9fe9b100b..effe6b891 100644 --- a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html @@ -7,7 +7,7 @@
-
+
diff --git a/frontend/src/app/lightning/channels-statistics/channels-statistics.component.html b/frontend/src/app/lightning/channels-statistics/channels-statistics.component.html index bedcc0ded..44e19b8f6 100644 --- a/frontend/src/app/lightning/channels-statistics/channels-statistics.component.html +++ b/frontend/src/app/lightning/channels-statistics/channels-statistics.component.html @@ -17,7 +17,7 @@ sats - + @@ -63,7 +63,7 @@ sats - + diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html index a371f474d..030019b3c 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html @@ -84,8 +84,22 @@ -
- Connect to our nodes +
+
+ +
+
+ +
+
+ +
diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.scss b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.scss index 303591974..218b8e04d 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.scss +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.scss @@ -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; + } + } } \ No newline at end of file diff --git a/frontend/src/app/lightning/node-statistics/node-statistics.component.html b/frontend/src/app/lightning/node-statistics/node-statistics.component.html index 74c14c8b0..97df1f546 100644 --- a/frontend/src/app/lightning/node-statistics/node-statistics.component.html +++ b/frontend/src/app/lightning/node-statistics/node-statistics.component.html @@ -5,11 +5,10 @@
- +
- - +
diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 5761e0a55..33de7823d 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -121,6 +121,7 @@ export class StateService { timeLtr: BehaviorSubject; hideFlow: BehaviorSubject; hideAudit: BehaviorSubject; + fiatCurrency$: BehaviorSubject; 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(fiatPreference || 'USD'); } setNetworkBasedonUrl(url: string) { diff --git a/frontend/src/app/shared/pipes/fiat-currency.pipe.ts b/frontend/src/app/shared/pipes/fiat-currency.pipe.ts new file mode 100644 index 000000000..3cd825291 --- /dev/null +++ b/frontend/src/app/shared/pipes/fiat-currency.pipe.ts @@ -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); + } +} \ No newline at end of file diff --git a/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts b/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts index 8c534f93f..93ab5cf8f 100644 --- a/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts +++ b/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts @@ -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; } } \ No newline at end of file diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index d82f03493..fd257db85 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -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,