diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index af317af14..e89060422 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -17,6 +17,9 @@ 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 RatesRepository from '../repositories/RatesRepository'; +import database from '../database'; import poolsParser from './pools-parser'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; import mining from './mining/mining'; @@ -150,6 +153,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; if (block.height === 0) { blockExtended.extras.medianFee = 0; // 50th percentiles diff --git a/backend/src/api/mining/mining.ts b/backend/src/api/mining/mining.ts index 0ee8bcf00..433e0cae9 100644 --- a/backend/src/api/mining/mining.ts +++ b/backend/src/api/mining/mining.ts @@ -31,7 +31,7 @@ class Mining { */ public async $getHistoricalBlockFees(interval: string | null = null): Promise { return await BlocksRepository.$getHistoricalBlockFees( - this.getTimeRange(interval), + this.getTimeRangeForAmounts(interval), Common.getSqlInterval(interval) ); } @@ -41,7 +41,7 @@ class Mining { */ public async $getHistoricalBlockRewards(interval: string | null = null): Promise { return await BlocksRepository.$getHistoricalBlockRewards( - this.getTimeRange(interval), + this.getTimeRangeForAmounts(interval), Common.getSqlInterval(interval) ); } @@ -462,6 +462,21 @@ class Mining { return date; } + private getTimeRangeForAmounts(interval: string | null): number { + switch (interval) { + case '3y': return 1296000; + case '2y': return 864000; + case '1y': return 432000; + case '6m': return 216000; + case '3m': return 108000; + case '1m': return 36000; + case '1w': return 8400; + case '3d': return 3600; + case '24h': return 1200; + default: return 3888000; + } + } + private getTimeRange(interval: string | null): number { switch (interval) { case '3y': return 43200; // 12h @@ -473,7 +488,7 @@ class Mining { case '1w': return 300; // 5min case '3d': return 1; case '24h': return 1; - default: return 86400; // 24h + default: return 86400; } } } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 477c6a920..a7f8095c5 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -109,6 +109,7 @@ export interface BlockExtension { avgFee?: number; avgFeeRate?: number; coinbaseRaw?: string; + usd?: number | null; } export interface BlockExtended extends IEsploraApi.Block { diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 83f368248..36a41597c 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -256,7 +256,7 @@ class BlocksRepository { const params: any[] = []; let query = ` SELECT - height, + blocks.height, hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, size, @@ -274,8 +274,10 @@ class BlocksRepository { merkle_root, previous_block_hash as previousblockhash, avg_fee, - avg_fee_rate + avg_fee_rate, + IFNULL(JSON_EXTRACT(rates.bisq_rates, '$.USD'), null) as usd FROM blocks + LEFT JOIN rates on rates.height = blocks.height WHERE pool_id = ?`; params.push(pool.id); @@ -308,7 +310,7 @@ class BlocksRepository { public async $getBlockByHeight(height: number): Promise { try { const [rows]: any[] = await DB.query(`SELECT - height, + blocks.height, hash, hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, @@ -333,10 +335,12 @@ class BlocksRepository { merkle_root, previous_block_hash as previousblockhash, avg_fee, - avg_fee_rate + avg_fee_rate, + IFNULL(JSON_EXTRACT(rates.bisq_rates, '$.USD'), null) as usd FROM blocks JOIN pools ON blocks.pool_id = pools.id - WHERE height = ${height}; + LEFT JOIN rates on rates.height = blocks.height + WHERE blocks.height = ${height}; `); if (rows.length <= 0) { @@ -357,12 +361,14 @@ class BlocksRepository { public async $getBlockByHash(hash: string): Promise { try { const query = ` - SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, hash as id, + SELECT *, blocks.height, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, hash as id, pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.slug as pool_slug, pools.addresses as pool_addresses, pools.regexes as pool_regexes, - previous_block_hash as previousblockhash + previous_block_hash as previousblockhash, + IFNULL(JSON_EXTRACT(rates.bisq_rates, '$.USD'), null) as usd FROM blocks JOIN pools ON blocks.pool_id = pools.id + LEFT JOIN rates on rates.height = blocks.height WHERE hash = '${hash}'; `; const [rows]: any[] = await DB.query(query); @@ -473,10 +479,12 @@ class BlocksRepository { public async $getHistoricalBlockFees(div: number, interval: string | null): Promise { try { let query = `SELECT - CAST(AVG(height) as INT) as avgHeight, + CAST(AVG(blocks.height) as INT) as avgHeight, CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp, - CAST(AVG(fees) as INT) as avgFees - FROM blocks`; + CAST(AVG(fees) as INT) as avgFees, + IFNULL(JSON_EXTRACT(rates.bisq_rates, '$.USD'), null) as usd + FROM blocks + LEFT JOIN rates on rates.height = blocks.height`; if (interval !== null) { query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; @@ -498,10 +506,12 @@ class BlocksRepository { public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise { try { let query = `SELECT - CAST(AVG(height) as INT) as avgHeight, + CAST(AVG(blocks.height) as INT) as avgHeight, CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp, - CAST(AVG(reward) as INT) as avgRewards - FROM blocks`; + CAST(AVG(reward) as INT) as avgRewards, + IFNULL(JSON_EXTRACT(rates.bisq_rates, '$.USD'), null) as usd + FROM blocks + LEFT JOIN rates on rates.height = blocks.height`; if (interval !== null) { query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; diff --git a/backend/src/utils/blocks-utils.ts b/backend/src/utils/blocks-utils.ts index 0f282bdeb..b933d6ae7 100644 --- a/backend/src/utils/blocks-utils.ts +++ b/backend/src/utils/blocks-utils.ts @@ -27,6 +27,7 @@ export function prepareBlock(block: any): BlockExtended { name: block.pool_name, slug: block.pool_slug, } : undefined), + usd: block?.extras?.usd ?? block.usd ?? null, } }; } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 8845f4255..8f95920f3 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -13,6 +13,7 @@ import { SharedModule } from './shared/shared.module'; 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 { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe'; @@ -37,6 +38,7 @@ import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe StorageService, LanguageService, ShortenStringPipe, + FiatShortenerPipe, CapAddressPipe, { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true } ], diff --git a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts index 37243bd4a..949024d4c 100644 --- a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts +++ b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts @@ -180,8 +180,8 @@ export class BlockFeeRatesGraphComponent implements OnInit { } let tooltip = `${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))}
`; - for (const pool of data.reverse()) { - tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1]} sats/vByte
`; + for (const rate of data.reverse()) { + tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1]} sats/vByte
`; } if (['24h', '3d'].includes(this.timespan)) { 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 1a74416e7..6d6e88122 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 @@ -4,12 +4,13 @@ import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { ApiService } from 'src/app/services/api.service'; import { SeoService } from 'src/app/services/seo.service'; -import { formatNumber } from '@angular/common'; +import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { download, formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils'; +import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils'; import { StorageService } from 'src/app/services/storage.service'; import { MiningService } from 'src/app/services/mining.service'; import { ActivatedRoute } from '@angular/router'; +import { FiatShortenerPipe } from 'src/app/shared/pipes/fiat-shortener.pipe'; @Component({ selector: 'app-block-fees-graph', @@ -51,6 +52,7 @@ export class BlockFeesGraphComponent implements OnInit { private storageService: StorageService, private miningService: MiningService, private route: ActivatedRoute, + private fiatShortenerPipe: FiatShortenerPipe, ) { this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); this.radioGroupForm.controls.dateSpan.setValue('1y'); @@ -82,6 +84,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]), }); this.isLoading = false; }), @@ -98,16 +101,17 @@ export class BlockFeesGraphComponent implements OnInit { prepareChartOptions(data) { this.chartOptions = { - animation: false, color: [ - new graphic.LinearGradient(0, 0, 0, 0.65, [ - { offset: 0, color: '#F4511E' }, - { offset: 0.25, color: '#FB8C00' }, - { offset: 0.5, color: '#FFB300' }, - { offset: 0.75, color: '#FDD835' }, - { offset: 1, color: '#7CB342' } + new graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: '#00ACC1' }, + { offset: 1, color: '#0D47A1' }, + ]), + new graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: '#FDD835' }, + { offset: 1, color: '#FB8C00' }, ]), ], + animation: false, grid: { top: 30, bottom: 80, @@ -128,28 +132,52 @@ export class BlockFeesGraphComponent implements OnInit { align: 'left', }, borderColor: '#000', - formatter: (ticks) => { - let tooltip = `${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10))}
`; - tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1], this.locale, '1.3-3')} BTC`; - tooltip += `
`; + formatter: function (data) { + if (data.length <= 0) { + return ''; + } + let tooltip = ` + ${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))}
`; - if (['24h', '3d'].includes(this.timespan)) { - tooltip += `` + $localize`At block: ${ticks[0].data[2]}` + ``; - } else { - tooltip += `` + $localize`Around block: ${ticks[0].data[2]}` + ``; + for (const tick of data) { + 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 += `* On average around block ${data[0].data[2]}`; return tooltip; - } + }.bind(this) }, - xAxis: { - name: formatterXAxisLabel(this.locale, this.timespan), - nameLocation: 'middle', - nameTextStyle: { - padding: [10, 0, 0, 0], - }, + xAxis: data.blockFees.length === 0 ? undefined : + { type: 'time', splitNumber: this.isMobile() ? 5 : 10, + axisLabel: { + hideOverlap: true, + } + }, + legend: { + data: [ + { + name: 'Fees BTC', + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: 'Fees USD', + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + ], }, yAxis: [ { @@ -160,6 +188,9 @@ export class BlockFeesGraphComponent implements OnInit { return `${val} BTC`; } }, + max: (value) => { + return Math.floor(value.max * 2 * 10) / 10; + }, splitLine: { lineStyle: { type: 'dotted', @@ -168,18 +199,47 @@ export class BlockFeesGraphComponent implements OnInit { } }, }, + { + type: 'value', + position: 'right', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: function(val) { + return this.fiatShortenerPipe.transform(val); + }.bind(this) + }, + splitLine: { + show: false, + }, + }, ], series: [ { + legendHoverLink: false, zlevel: 0, - name: $localize`:@@c20172223f84462032664d717d739297e5a9e2fe:Fees`, - showSymbol: false, - symbol: 'none', + yAxisIndex: 0, + name: 'Fees BTC', data: data.blockFees, type: 'line', + smooth: 0.25, + symbol: 'none', + areaStyle: { + opacity: 0.25, + }, + }, + { + legendHoverLink: false, + zlevel: 1, + yAxisIndex: 1, + name: 'Fees USD', + data: data.blockFeesUSD, + type: 'line', + smooth: 0.25, + symbol: 'none', lineStyle: { width: 2, - }, + opacity: 0.75, + } }, ], dataZoom: [{ 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 995fb31fb..b3d60c0a5 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 @@ -4,12 +4,13 @@ import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { ApiService } from 'src/app/services/api.service'; import { SeoService } from 'src/app/services/seo.service'; -import { formatNumber } from '@angular/common'; +import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { download, formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils'; +import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils'; import { MiningService } from 'src/app/services/mining.service'; import { StorageService } from 'src/app/services/storage.service'; import { ActivatedRoute } from '@angular/router'; +import { FiatShortenerPipe } from 'src/app/shared/pipes/fiat-shortener.pipe'; @Component({ selector: 'app-block-rewards-graph', @@ -51,6 +52,7 @@ export class BlockRewardsGraphComponent implements OnInit { private miningService: MiningService, private storageService: StorageService, private route: ActivatedRoute, + private fiatShortenerPipe: FiatShortenerPipe, ) { } @@ -80,6 +82,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]), }); this.isLoading = false; }), @@ -95,15 +98,18 @@ export class BlockRewardsGraphComponent implements OnInit { } prepareChartOptions(data) { + const scaleFactor = 0.1; + this.chartOptions = { animation: false, color: [ - new graphic.LinearGradient(0, 0, 0, 0.65, [ - { offset: 0, color: '#F4511E' }, - { offset: 0.25, color: '#FB8C00' }, - { offset: 0.5, color: '#FFB300' }, - { offset: 0.75, color: '#FDD835' }, - { offset: 1, color: '#7CB342' } + new graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: '#00ACC1' }, + { offset: 1, color: '#0D47A1' }, + ]), + new graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: '#FDD835' }, + { offset: 1, color: '#FB8C00' }, ]), ], grid: { @@ -126,33 +132,55 @@ export class BlockRewardsGraphComponent implements OnInit { align: 'left', }, borderColor: '#000', - formatter: (ticks) => { - let tooltip = `${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10))}
`; - tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1], this.locale, '1.3-3')} BTC`; - tooltip += `
`; + formatter: function (data) { + if (data.length <= 0) { + return ''; + } + let tooltip = ` + ${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))}
`; - if (['24h', '3d'].includes(this.timespan)) { - tooltip += `` + $localize`At block: ${ticks[0].data[2]}` + ``; - } else { - tooltip += `` + $localize`Around block: ${ticks[0].data[2]}` + ``; + for (const tick of data) { + 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 += `* On average around block ${data[0].data[2]}`; return tooltip; - } + }.bind(this) }, - xAxis: { - name: formatterXAxisLabel(this.locale, this.timespan), - nameLocation: 'middle', - nameTextStyle: { - padding: [10, 0, 0, 0], - }, + xAxis: data.blockRewards.length === 0 ? undefined : + { type: 'time', splitNumber: this.isMobile() ? 5 : 10, + axisLabel: { + hideOverlap: true, + } + }, + legend: { + data: [ + { + name: 'Rewards BTC', + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: 'Rewards USD', + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + ], }, yAxis: [ { - min: value => Math.round(10 * value.min * 0.99) / 10, - max: value => Math.round(10 * value.max * 1.01) / 10, type: 'value', axisLabel: { color: 'rgb(110, 112, 121)', @@ -160,6 +188,12 @@ export class BlockRewardsGraphComponent implements OnInit { return `${val} BTC`; } }, + min: (value) => { + return Math.round(value.min * (1.0 - scaleFactor) * 10) / 10; + }, + max: (value) => { + return Math.round(value.max * (1.0 + scaleFactor) * 10) / 10; + }, splitLine: { lineStyle: { type: 'dotted', @@ -168,18 +202,53 @@ export class BlockRewardsGraphComponent implements OnInit { } }, }, + { + min: (value) => { + return Math.round(value.min * (1.0 - scaleFactor) * 10) / 10; + }, + max: (value) => { + return Math.round(value.max * (1.0 + scaleFactor) * 10) / 10; + }, + type: 'value', + position: 'right', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: function(val) { + return this.fiatShortenerPipe.transform(val); + }.bind(this) + }, + splitLine: { + show: false, + }, + }, ], series: [ { + legendHoverLink: false, zlevel: 0, - name: $localize`:@@12f86e6747a5ad39e62d3480ddc472b1aeab5b76:Reward`, - showSymbol: false, - symbol: 'none', + yAxisIndex: 0, + name: 'Rewards BTC', data: data.blockRewards, type: 'line', + smooth: 0.25, + symbol: 'none', + areaStyle: { + opacity: 0.25, + }, + }, + { + legendHoverLink: false, + zlevel: 1, + yAxisIndex: 1, + name: 'Rewards USD', + data: data.blockRewardsUSD, + type: 'line', + smooth: 0.25, + symbol: 'none', lineStyle: { width: 2, - }, + opacity: 0.75, + } }, ], dataZoom: [{ diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts index 385be0669..cacbe4198 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -351,6 +351,7 @@ export class HashrateChartComponent implements OnInit { series: data.hashrates.length === 0 ? [] : [ { zlevel: 0, + yAxisIndex: 0, name: $localize`:@@79a9dc5b1caca3cbeb1733a19515edacc5fc7920:Hashrate`, showSymbol: false, symbol: 'none', diff --git a/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts b/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts new file mode 100644 index 000000000..8c534f93f --- /dev/null +++ b/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts @@ -0,0 +1,37 @@ +import { formatCurrency, getCurrencySymbol } from '@angular/common'; +import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'fiatShortener' +}) +export class FiatShortenerPipe implements PipeTransform { + constructor( + @Inject(LOCALE_ID) public locale: string + ) {} + + transform(num: number, ...args: any[]): unknown { + const digits = args[0] || 1; + const unit = args[1] || undefined; + + if (num < 1000) { + return num.toFixed(digits); + } + + const lookup = [ + { value: 1, symbol: '' }, + { value: 1e3, symbol: 'k' }, + { value: 1e6, symbol: 'M' }, + { value: 1e9, symbol: 'G' }, + { value: 1e12, symbol: 'T' }, + { value: 1e15, symbol: 'P' }, + { value: 1e18, symbol: 'E' } + ]; + const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; + 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'); + + return result + item.symbol; + } +} \ No newline at end of file