diff --git a/backend/src/repositories/PricesRepository.ts b/backend/src/repositories/PricesRepository.ts index 639f16dc6..f460a504f 100644 --- a/backend/src/repositories/PricesRepository.ts +++ b/backend/src/repositories/PricesRepository.ts @@ -28,6 +28,16 @@ export interface Conversion { exchangeRates: ExchangeRates; } +export const MAX_PRICES = { + USD: 100000000, + EUR: 100000000, + GBP: 100000000, + CAD: 100000000, + CHF: 100000000, + AUD: 100000000, + JPY: 10000000000, +}; + class PricesRepository { public async $savePrices(time: number, prices: IConversionRates): Promise { if (prices.USD === 0) { @@ -36,6 +46,14 @@ class PricesRepository { return; } + // Sanity check + for (const currency of Object.keys(prices)) { + if (prices[currency] < -1 || prices[currency] > MAX_PRICES[currency]) { // We use -1 to mark a "missing data, so it's a valid entry" + logger.info(`Ignore BTC${currency} price of ${prices[currency]}`); + prices[currency] = 0; + } + } + try { await DB.query(` INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY) diff --git a/backend/src/tasks/price-updater.ts b/backend/src/tasks/price-updater.ts index 939a1ea85..b39e152ae 100644 --- a/backend/src/tasks/price-updater.ts +++ b/backend/src/tasks/price-updater.ts @@ -3,7 +3,7 @@ import path from 'path'; import config from '../config'; import logger from '../logger'; import { IConversionRates } from '../mempool.interfaces'; -import PricesRepository from '../repositories/PricesRepository'; +import PricesRepository, { MAX_PRICES } from '../repositories/PricesRepository'; import BitfinexApi from './price-feeds/bitfinex-api'; import BitflyerApi from './price-feeds/bitflyer-api'; import CoinbaseApi from './price-feeds/coinbase-api'; @@ -46,13 +46,13 @@ class PriceUpdater { public getEmptyPricesObj(): IConversionRates { return { - USD: 0, - EUR: 0, - GBP: 0, - CAD: 0, - CHF: 0, - AUD: 0, - JPY: 0, + USD: -1, + EUR: -1, + GBP: -1, + CAD: -1, + CHF: -1, + AUD: -1, + JPY: -1, }; } @@ -115,7 +115,7 @@ class PriceUpdater { if (feed.currencies.includes(currency)) { try { const price = await feed.$fetchPrice(currency); - if (price > 0) { + if (price > -1 && price < MAX_PRICES[currency]) { prices.push(price); } logger.debug(`${feed.name} BTC/${currency} price: ${price}`, logger.tags.mining); @@ -239,7 +239,7 @@ class PriceUpdater { for (const currency of this.currencies) { const price = historicalEntry[time][currency]; - if (price > 0) { + if (price > -1 && price < MAX_PRICES[currency]) { grouped[time][currency].push(typeof price === 'string' ? parseInt(price, 10) : price); } } diff --git a/frontend/src/app/components/amount/amount.component.html b/frontend/src/app/components/amount/amount.component.html index 4a57e72e2..ce9c02d78 100644 --- a/frontend/src/app/components/amount/amount.component.html +++ b/frontend/src/app/components/amount/amount.component.html @@ -3,7 +3,7 @@ {{ addPlus && satoshis >= 0 ? '+' : '' }} {{ ( - (blockConversion.price[currency] > 0 ? blockConversion.price[currency] : null) ?? + (blockConversion.price[currency] >= 0 ? blockConversion.price[currency] : null) ?? (blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0 ) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }} diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 9e476ac61..5e0465fe1 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -443,9 +443,9 @@ export class BlockComponent implements OnInit, OnDestroy { } this.priceSubscription = block$.pipe( switchMap((block) => { - return this.priceService.getPrices().pipe( - tap(() => { - this.blockConversion = this.priceService.getPriceForTimestamp(block.timestamp); + return this.priceService.getBlockPrice$(block.timestamp).pipe( + tap((price) => { + this.blockConversion = price; }) ); }) diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 06a4c5836..0c3a2b331 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -327,9 +327,9 @@ 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); + this.priceService.getBlockPrice$(tx.status.block_time).pipe( + tap((price) => { + this.blockConversion = price; }) ).subscribe(); 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 bfdaa02bc..6422d8507 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -6,7 +6,7 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter import { ElectrsApiService } from '../../services/electrs-api.service'; import { environment } from '../../../environments/environment'; import { AssetsService } from '../../services/assets.service'; -import { filter, map, tap, switchMap } from 'rxjs/operators'; +import { filter, map, tap, switchMap, shareReplay } from 'rxjs/operators'; import { BlockExtended } from '../../interfaces/node-api.interface'; import { ApiService } from '../../services/api.service'; import { PriceService } from 'src/app/services/price.service'; @@ -150,10 +150,8 @@ export class TransactionsListComponent implements OnInit, OnChanges { tx['addressValue'] = addressIn - addressOut; } - this.priceService.getPrices().pipe( - tap(() => { - tx['price'] = this.priceService.getPriceForTimestamp(tx.status.block_time); - }) + this.priceService.getBlockPrice$(tx.status.block_time).pipe( + tap((price) => tx['price'] = price) ).subscribe(); }); const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid); diff --git a/frontend/src/app/fiat/fiat.component.html b/frontend/src/app/fiat/fiat.component.html index a1bf79978..998153d29 100644 --- a/frontend/src/app/fiat/fiat.component.html +++ b/frontend/src/app/fiat/fiat.component.html @@ -1,7 +1,7 @@ {{ ( - (blockConversion.price[currency] > 0 ? blockConversion.price[currency] : null) ?? + (blockConversion.price[currency] >= 0 ? blockConversion.price[currency] : null) ?? (blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0 ) * value / 100000000 | fiatCurrency : digitsInfo : currency }} diff --git a/frontend/src/app/services/price.service.ts b/frontend/src/app/services/price.service.ts index fe6d67bb6..409ff05fd 100644 --- a/frontend/src/app/services/price.service.ts +++ b/frontend/src/app/services/price.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { map, Observable, of, shareReplay } from 'rxjs'; +import { map, Observable, of, share, shareReplay, tap } from 'rxjs'; import { ApiService } from './api.service'; // nodejs backend interfaces @@ -40,6 +40,8 @@ export interface ConversionDict { providedIn: 'root' }) export class PriceService { + priceObservable$: Observable; + historicalPrice: ConversionDict = { prices: null, exchangeRates: null, @@ -61,65 +63,53 @@ export class PriceService { }; } - /** - * Fetch prices from the nodejs backend only once - */ - getPrices(): Observable { - if (this.historicalPrice.prices) { - return of(null); + getBlockPrice$(blockTimestamp: number): Observable { + if (!this.priceObservable$) { + this.priceObservable$ = this.apiService.getHistoricalPrice$().pipe(shareReplay()); } - return this.apiService.getHistoricalPrice$().pipe( - map((conversion: Conversion) => { - if (!this.historicalPrice.prices) { - this.historicalPrice.prices = Object(); + return this.priceObservable$.pipe( + map((conversion) => { + if (!blockTimestamp) { + return undefined; } + + const historicalPrice = { + prices: {}, + exchangeRates: conversion.exchangeRates, + }; for (const price of conversion.prices) { - this.historicalPrice.prices[price.time] = { + 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 { - if (!blockTimestamp) { - return undefined; - } - - const priceTimestamps = Object.keys(this.historicalPrice.prices); - priceTimestamps.push(Number.MAX_SAFE_INTEGER.toString()); - priceTimestamps.sort().reverse(); + const priceTimestamps = Object.keys(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 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 - // 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(); + for (const t of priceTimestamps) { + const priceTimestamp = parseInt(t, 10); + if (blockTimestamp > priceTimestamp) { + return { + price: historicalPrice.prices[priceTimestamp], + exchangeRates: historicalPrice.exchangeRates, + }; + } + } + + return this.getEmptyPrice(); + }) + ); } }