diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index 81c7b5a99..393ea119a 100644 --- a/backend/src/api/mining/mining-routes.ts +++ b/backend/src/api/mining/mining-routes.ts @@ -1,13 +1,13 @@ import { Application, Request, Response } from 'express'; import config from "../../config"; import logger from '../../logger'; -import audits from '../audit'; import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository'; import BlocksRepository from '../../repositories/BlocksRepository'; import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository'; import HashratesRepository from '../../repositories/HashratesRepository'; import bitcoinClient from '../bitcoin/bitcoin-client'; import mining from "./mining"; +import PricesRepository from '../../repositories/PricesRepository'; class MiningRoutes { public initRoutes(app: Application) { @@ -32,9 +32,18 @@ class MiningRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/score/:hash', this.$getBlockAuditScore) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp) + .get(config.MEMPOOL.API_URL_PREFIX + 'historical-price', this.$getHistoricalPrice) ; } + private async $getHistoricalPrice(req: Request, res: Response): Promise { + try { + res.status(200).send(await PricesRepository.$getHistoricalPrice()); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getPool(req: Request, res: Response): Promise { try { const stats = await mining.$getPoolStat(req.params.slug); diff --git a/backend/src/repositories/PricesRepository.ts b/backend/src/repositories/PricesRepository.ts index bc606e68b..639f16dc6 100644 --- a/backend/src/repositories/PricesRepository.ts +++ b/backend/src/repositories/PricesRepository.ts @@ -3,6 +3,31 @@ import logger from '../logger'; import { IConversionRates } from '../mempool.interfaces'; import priceUpdater from '../tasks/price-updater'; +export interface ApiPrice { + time?: number, + USD: number, + EUR: number, + GBP: number, + CAD: number, + CHF: number, + AUD: number, + JPY: number, +} + +export interface ExchangeRates { + USDEUR: number, + USDGBP: number, + USDCAD: number, + USDCHF: number, + USDAUD: number, + USDJPY: number, +} + +export interface Conversion { + prices: ApiPrice[], + exchangeRates: ExchangeRates; +} + class PricesRepository { public async $savePrices(time: number, prices: IConversionRates): Promise { if (prices.USD === 0) { @@ -60,6 +85,34 @@ class PricesRepository { } return rates[0]; } + + public async $getHistoricalPrice(): Promise { + try { + const [rates]: any[] = await DB.query(`SELECT *, UNIX_TIMESTAMP(time) as time FROM prices ORDER BY time DESC`); + if (!rates) { + throw Error(`Cannot get average historical price from the database`); + } + + // Compute fiat exchange rates + const latestPrice: ApiPrice = rates[0]; + const exchangeRates: ExchangeRates = { + USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100, + USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100, + USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100, + USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100, + USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100, + USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100, + }; + + return { + prices: rates, + exchangeRates: exchangeRates + }; + } catch (e) { + logger.err(`Cannot fetch averaged historical prices from the db. Reason ${e instanceof Error ? e.message : e}`); + return null; + } + } } export default new PricesRepository(); diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index f26b4a924..7afdba1f8 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -7,6 +7,7 @@ import { AppComponent } from './components/app/app.component'; import { ElectrsApiService } from './services/electrs-api.service'; import { StateService } from './services/state.service'; import { CacheService } from './services/cache.service'; +import { PriceService } from './services/price.service'; import { EnterpriseService } from './services/enterprise.service'; import { WebsocketService } from './services/websocket.service'; import { AudioService } from './services/audio.service'; @@ -26,6 +27,7 @@ const providers = [ ElectrsApiService, StateService, CacheService, + PriceService, WebsocketService, AudioService, SeoService, diff --git a/frontend/src/app/components/amount/amount.component.html b/frontend/src/app/components/amount/amount.component.html index 3526b554b..4a57e72e2 100644 --- a/frontend/src/app/components/amount/amount.component.html +++ b/frontend/src/app/components/amount/amount.component.html @@ -1,7 +1,19 @@ - {{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }} + + {{ addPlus && satoshis >= 0 ? '+' : '' }} + {{ + ( + (blockConversion.price[currency] > 0 ? blockConversion.price[currency] : null) ?? + (blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0 + ) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency + }} + + + {{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }} + - + + Confidential diff --git a/frontend/src/app/components/amount/amount.component.ts b/frontend/src/app/components/amount/amount.component.ts index cfdc50468..927504012 100644 --- a/frontend/src/app/components/amount/amount.component.ts +++ b/frontend/src/app/components/amount/amount.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { StateService } from '../../services/state.service'; import { Observable, Subscription } from 'rxjs'; +import { Price } from 'src/app/services/price.service'; @Component({ selector: 'app-amount', @@ -21,6 +22,7 @@ export class AmountComponent implements OnInit, OnDestroy { @Input() digitsInfo = '1.8-8'; @Input() noFiat = false; @Input() addPlus = false; + @Input() blockConversion: Price; constructor( private stateService: StateService, diff --git a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.html b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.html index 679854535..77c35cea8 100644 --- a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.html +++ b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.html @@ -10,7 +10,7 @@
-
+
diff --git a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss index f916bfc79..ec1755e7d 100644 --- a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss +++ b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss @@ -78,3 +78,8 @@ } } } + +.disabled { + pointer-events: none; + opacity: 0.5; +} \ No newline at end of file diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html index cbe742ed4..76071be96 100644 --- a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html @@ -10,7 +10,7 @@
-
+
diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.scss b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.scss index f916bfc79..ec1755e7d 100644 --- a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.scss +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.scss @@ -78,3 +78,8 @@ } } } + +.disabled { + pointer-events: none; + opacity: 0.5; +} \ No newline at end of file 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 42667126f..96bd0697c 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,9 +4,9 @@ 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 { download, formatterXAxis } from '../../shared/graphs.utils'; import { StateService } from '../../services/state.service'; import { StorageService } from '../../services/storage.service'; import { MiningService } from '../../services/mining.service'; @@ -92,6 +92,7 @@ export class BlockFeesGraphComponent implements OnInit { .pipe( startWith(this.radioGroupForm.controls.dateSpan.value), switchMap((timespan) => { + this.isLoading = true; this.storageService.setValue('miningWindowPreference', timespan); this.timespan = timespan; this.isLoading = true; diff --git a/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.html b/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.html index fc67b5d98..7dcd81c69 100644 --- a/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.html +++ b/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.html @@ -10,7 +10,7 @@
-
+
diff --git a/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.scss b/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.scss index f916bfc79..ec1755e7d 100644 --- a/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.scss +++ b/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.scss @@ -78,3 +78,8 @@ } } } + +.disabled { + pointer-events: none; + opacity: 0.5; +} \ No newline at end of file diff --git a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.html b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.html index 4134f6310..198153583 100644 --- a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.html +++ b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.html @@ -11,7 +11,7 @@
-
+
@@ -31,7 +31,7 @@ 3Y
diff --git a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss index f916bfc79..ec1755e7d 100644 --- a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss +++ b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss @@ -78,3 +78,8 @@ } } } + +.disabled { + pointer-events: none; + opacity: 0.5; +} \ No newline at end of file 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 0e5e339fa..ca1853633 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 @@ -80,7 +80,7 @@ export class BlockRewardsGraphComponent implements OnInit { this.route .fragment .subscribe((fragment) => { - if (['3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) { + if (['1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) { this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); } }); diff --git a/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.html b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.html index b6787a3cc..122b5e7ca 100644 --- a/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.html +++ b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.html @@ -9,7 +9,7 @@
-
+
diff --git a/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.scss b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.scss index e5e4bfd9a..85765e0e1 100644 --- a/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.scss +++ b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.scss @@ -78,3 +78,8 @@ } } } + +.disabled { + pointer-events: none; + opacity: 0.5; +} \ No newline at end of file diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 0fda470d6..bb4d2082c 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -124,7 +124,13 @@ Median fee - ~{{ block?.extras?.medianFee | number:'1.0-0' }} sat/vB + ~{{ block?.extras?.medianFee | number:'1.0-0' }} sat/vB + + + + @@ -132,13 +138,13 @@ - +   + [blockConversion]="blockConversion" [value]="fees * 100000000" digitsInfo="1.2-2"> @@ -147,7 +153,7 @@ - + diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index ba5dd8cf7..9e476ac61 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -13,6 +13,7 @@ import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces import { ApiService } from '../../services/api.service'; import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; import { detectWebGL } from '../../shared/graphs.utils'; +import { PriceService, Price } from 'src/app/services/price.service'; @Component({ selector: 'app-block', @@ -81,6 +82,9 @@ export class BlockComponent implements OnInit, OnDestroy { timeLtr: boolean; childChangeSubscription: Subscription; auditPrefSubscription: Subscription; + + priceSubscription: Subscription; + blockConversion: Price; @ViewChildren('blockGraphProjected') blockGraphProjected: QueryList; @ViewChildren('blockGraphActual') blockGraphActual: QueryList; @@ -94,7 +98,8 @@ export class BlockComponent implements OnInit, OnDestroy { private seoService: SeoService, private websocketService: WebsocketService, private relativeUrlPipe: RelativeUrlPipe, - private apiService: ApiService + private apiService: ApiService, + private priceService: PriceService, ) { this.webGlEnabled = detectWebGL(); } @@ -432,6 +437,19 @@ export class BlockComponent implements OnInit, OnDestroy { } } }); + + if (this.priceSubscription) { + this.priceSubscription.unsubscribe(); + } + this.priceSubscription = block$.pipe( + switchMap((block) => { + return this.priceService.getPrices().pipe( + tap(() => { + this.blockConversion = this.priceService.getPriceForTimestamp(block.timestamp); + }) + ); + }) + ).subscribe(); } ngAfterViewInit(): void { diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html index cf22548a6..83f8a3a4c 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -31,7 +31,7 @@
-
+
diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss index 8718caf9b..154d46fa6 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss @@ -131,4 +131,9 @@ display: block; max-width: 80px; margin: 15px auto 3px; +} + +.disabled { + pointer-events: none; + opacity: 0.5; } \ No newline at end of file diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html index 107e30147..bbdc745fe 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html @@ -11,7 +11,7 @@
-
+
diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss index b59e21af3..00414e4df 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss @@ -82,3 +82,8 @@ .loadingGraphs.widget { top: 75%; } + +.disabled { + pointer-events: none; + opacity: 0.5; +} \ No newline at end of file diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts index ed3683e9b..df7780fee 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts @@ -5,7 +5,7 @@ import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/op import { ApiService } from '../../services/api.service'; import { SeoService } from '../../services/seo.service'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { poolsColor } from '../../app.constants'; +import { chartColors, poolsColor } from '../../app.constants'; import { StorageService } from '../../services/storage.service'; import { MiningService } from '../../services/mining.service'; import { download } from '../../shared/graphs.utils'; @@ -173,6 +173,7 @@ export class HashrateChartPoolsComponent implements OnInit { this.chartOptions = { title: title, animation: false, + color: chartColors, grid: { right: this.right, left: this.left, diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.html b/frontend/src/app/components/pool-ranking/pool-ranking.component.html index 35ab709c5..32a186fb8 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.html +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.html @@ -40,7 +40,7 @@
-
+
@@ -104,7 +104,7 @@ {{ pool.name }} - {{ pool.lastEstimatedHashrate }} {{ + {{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }} {{ pool.blockCount }} ({{ pool.share }}%) diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.scss b/frontend/src/app/components/pool-ranking/pool-ranking.component.scss index 8cb82d92d..277c7d4ad 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.scss +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.scss @@ -139,3 +139,8 @@ max-width: 80px; margin: 15px auto 3px; } + +.disabled { + pointer-events: none; + opacity: 0.5; +} \ No newline at end of file diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts index ed27b6de7..8284d81da 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -79,6 +79,7 @@ export class PoolRankingComponent implements OnInit { .pipe( startWith(this.radioGroupForm.controls.dateSpan.value), // (trigger when the page loads) tap((value) => { + this.isLoading = true; this.timespan = value; if (!this.widget) { this.storageService.setValue('miningWindowPreference', value); diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index f0b25d60e..07234f7bc 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -469,7 +469,7 @@ Fee - {{ tx.fee | number }} sat + {{ tx.fee | number }} sat Fee rate diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 7e1ae525e..06a4c5836 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -8,10 +8,11 @@ import { retryWhen, delay, map, - mergeMap + mergeMap, + tap } from 'rxjs/operators'; import { Transaction } from '../../interfaces/electrs.interface'; -import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from, throwError } from 'rxjs'; +import { of, merge, Subscription, Observable, Subject, timer, from, throwError } from 'rxjs'; import { StateService } from '../../services/state.service'; import { CacheService } from '../../services/cache.service'; import { WebsocketService } from '../../services/websocket.service'; @@ -21,6 +22,7 @@ import { SeoService } from '../../services/seo.service'; import { BlockExtended, CpfpInfo } from '../../interfaces/node-api.interface'; import { LiquidUnblinding } from './liquid-ublinding'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { Price, PriceService } from 'src/app/services/price.service'; @Component({ selector: 'app-transaction', @@ -69,7 +71,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { hideFlow: boolean = this.stateService.hideFlow.value; overrideFlowPreference: boolean = null; flowEnabled: boolean; - + blockConversion: Price; tooltipPosition: { x: number, y: number }; @ViewChild('graphContainer') @@ -85,7 +87,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { private websocketService: WebsocketService, private audioService: AudioService, private apiService: ApiService, - private seoService: SeoService + private seoService: SeoService, + private priceService: PriceService, ) {} ngOnInit() { @@ -323,6 +326,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } this.fetchRbfHistory$.next(this.tx.txid); } + + this.priceService.getPrices().pipe( + tap(() => { + this.blockConversion = this.priceService.getPriceForTimestamp(tx.status.block_time); + }) + ).subscribe(); + setTimeout(() => { this.applyFragment(); }, 0); }, (error) => { diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 7602e40cc..e5280d572 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -88,7 +88,7 @@ - + @@ -216,7 +216,7 @@ - + @@ -283,7 +283,9 @@
- {{ tx.fee / (tx.weight / 4) | feeRounding }} sat/vB  – {{ tx.fee | number }} sat + {{ tx.fee / (tx.weight / 4) | feeRounding }} sat/vB  – {{ tx.fee | number }} sat
Show more inputs to reveal fee data
@@ -301,12 +303,12 @@
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index 67df2daa2..bfdaa02bc 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -9,6 +9,7 @@ import { AssetsService } from '../../services/assets.service'; import { filter, map, tap, switchMap } from 'rxjs/operators'; import { BlockExtended } from '../../interfaces/node-api.interface'; import { ApiService } from '../../services/api.service'; +import { PriceService } from 'src/app/services/price.service'; @Component({ selector: 'app-transactions-list', @@ -50,6 +51,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { private apiService: ApiService, private assetsService: AssetsService, private ref: ChangeDetectorRef, + private priceService: PriceService, ) { } ngOnInit(): void { @@ -147,6 +149,12 @@ export class TransactionsListComponent implements OnInit, OnChanges { tx['addressValue'] = addressIn - addressOut; } + + this.priceService.getPrices().pipe( + tap(() => { + tx['price'] = this.priceService.getPriceForTimestamp(tx.status.block_time); + }) + ).subscribe(); }); const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid); if (txIds.length) { diff --git a/frontend/src/app/fiat/fiat.component.html b/frontend/src/app/fiat/fiat.component.html index 99a177cc0..a1bf79978 100644 --- a/frontend/src/app/fiat/fiat.component.html +++ b/frontend/src/app/fiat/fiat.component.html @@ -1 +1,14 @@ -{{ (conversions ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }} \ No newline at end of file + + {{ + ( + (blockConversion.price[currency] > 0 ? blockConversion.price[currency] : null) ?? + (blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0 + ) * value / 100000000 | fiatCurrency : digitsInfo : currency + }} + + + + + {{ (conversions[currency] ?? conversions['USD'] ?? 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 bc0f6a0de..909b249c0 100644 --- a/frontend/src/app/fiat/fiat.component.ts +++ b/frontend/src/app/fiat/fiat.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; +import { Price } from '../services/price.service'; import { StateService } from '../services/state.service'; @Component({ @@ -15,6 +16,7 @@ export class FiatComponent implements OnInit, OnDestroy { @Input() value: number; @Input() digitsInfo = '1.2-2'; + @Input() blockConversion: Price; constructor( private stateService: StateService, diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index 63dec7abd..dcccfb67c 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -1,3 +1,4 @@ +import { Price } from '../services/price.service'; import { IChannel } from './node-api.interface'; export interface Transaction { @@ -23,6 +24,7 @@ export interface Transaction { _deduced?: boolean; _outspends?: Outspend[]; _channels?: TransactionChannels; + price?: Price; } export interface TransactionChannels { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 04b2b72e2..2c74de361 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -6,6 +6,7 @@ import { Observable } from 'rxjs'; import { StateService } from './state.service'; import { WebsocketResponse } from '../interfaces/websocket.interface'; import { Outspend, Transaction } from '../interfaces/electrs.interface'; +import { Conversion } from './price.service'; @Injectable({ providedIn: 'root' @@ -303,4 +304,8 @@ export class ApiService { (style !== undefined ? `?style=${style}` : '') ); } + + getHistoricalPrice$(): Observable { + return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + '/api/v1/historical-price'); + } } diff --git a/frontend/src/app/services/price.service.ts b/frontend/src/app/services/price.service.ts new file mode 100644 index 000000000..3320280e9 --- /dev/null +++ b/frontend/src/app/services/price.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@angular/core'; +import { map, Observable, of, shareReplay } from 'rxjs'; +import { ApiService } from './api.service'; + +// nodejs backend interfaces +export interface ApiPrice { + time?: number, + USD: number, + EUR: number, + GBP: number, + CAD: number, + CHF: number, + AUD: number, + JPY: number, +} +export interface ExchangeRates { + USDEUR: number, + USDGBP: number, + USDCAD: number, + USDCHF: number, + USDAUD: number, + USDJPY: number, +} +export interface Conversion { + prices: ApiPrice[], + exchangeRates: ExchangeRates; +} + +// frontend interface +export interface Price { + price: ApiPrice, + exchangeRates: ExchangeRates, +} +export interface ConversionDict { + prices: { [timestamp: number]: ApiPrice } + exchangeRates: ExchangeRates; +} + +@Injectable({ + providedIn: 'root' +}) +export class PriceService { + historicalPrice: ConversionDict = { + prices: null, + exchangeRates: null, + }; + + constructor( + private apiService: ApiService + ) { + } + + getEmptyPrice(): Price { + return { + price: { + USD: 0, EUR: 0, GBP: 0, CAD: 0, CHF: 0, AUD: 0, JPY: 0, + }, + exchangeRates: { + USDEUR: 0, USDGBP: 0, USDCAD: 0, USDCHF: 0, USDAUD: 0, USDJPY: 0, + }, + }; + } + + /** + * Fetch prices from the nodejs backend only once + */ + getPrices(): Observable { + if (this.historicalPrice.prices) { + return of(null); + } + + return this.apiService.getHistoricalPrice$().pipe( + map((conversion: Conversion) => { + if (!this.historicalPrice.prices) { + this.historicalPrice.prices = Object(); + } + for (const price of conversion.prices) { + this.historicalPrice.prices[price.time] = { + USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD, + CHF: price.CHF, AUD: price.AUD, JPY: price.JPY + }; + } + this.historicalPrice.exchangeRates = conversion.exchangeRates; + return; + }), + shareReplay(), + ); + } + + /** + * Note: The first block with a price we have is block 68952 (using MtGox price history) + * + * @param blockTimestamp + */ + getPriceForTimestamp(blockTimestamp: number): Price | null { + const priceTimestamps = Object.keys(this.historicalPrice.prices); + priceTimestamps.push(Number.MAX_SAFE_INTEGER.toString()); + priceTimestamps.sort().reverse(); + + // Small trick here. Because latest blocks have higher timestamps than our + // latest price timestamp (we only insert once every hour), we have no price for them. + // Therefore we want to fallback to the websocket price by returning an undefined `price` field. + // Since this.historicalPrice.prices[Number.MAX_SAFE_INTEGER] does not exists + // it will return `undefined` and automatically use the websocket price. + // This way we can differenciate blocks without prices like the genesis block + // vs ones without a price (yet) like the latest blocks + + for (const t of priceTimestamps) { + const priceTimestamp = parseInt(t, 10); + if (blockTimestamp > priceTimestamp) { + return { + price: this.historicalPrice.prices[priceTimestamp], + exchangeRates: this.historicalPrice.exchangeRates, + }; + } + } + + return this.getEmptyPrice(); + } +} +