Redo/Fix completely failed PR #3092 + add PR #3105

This commit is contained in:
nymkappa 2023-02-23 09:50:34 +09:00
parent 6e7ed29caa
commit f44eacd5d5
No known key found for this signature in database
GPG Key ID: E155910B16E8BD04
8 changed files with 79 additions and 73 deletions

View File

@ -28,6 +28,16 @@ export interface Conversion {
exchangeRates: ExchangeRates; exchangeRates: ExchangeRates;
} }
export const MAX_PRICES = {
USD: 100000000,
EUR: 100000000,
GBP: 100000000,
CAD: 100000000,
CHF: 100000000,
AUD: 100000000,
JPY: 10000000000,
};
class PricesRepository { class PricesRepository {
public async $savePrices(time: number, prices: IConversionRates): Promise<void> { public async $savePrices(time: number, prices: IConversionRates): Promise<void> {
if (prices.USD === 0) { if (prices.USD === 0) {
@ -36,6 +46,14 @@ class PricesRepository {
return; 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 { try {
await DB.query(` await DB.query(`
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY) INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY)

View File

@ -3,7 +3,7 @@ import path from 'path';
import config from '../config'; import config from '../config';
import logger from '../logger'; import logger from '../logger';
import { IConversionRates } from '../mempool.interfaces'; 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 BitfinexApi from './price-feeds/bitfinex-api';
import BitflyerApi from './price-feeds/bitflyer-api'; import BitflyerApi from './price-feeds/bitflyer-api';
import CoinbaseApi from './price-feeds/coinbase-api'; import CoinbaseApi from './price-feeds/coinbase-api';
@ -46,13 +46,13 @@ class PriceUpdater {
public getEmptyPricesObj(): IConversionRates { public getEmptyPricesObj(): IConversionRates {
return { return {
USD: 0, USD: -1,
EUR: 0, EUR: -1,
GBP: 0, GBP: -1,
CAD: 0, CAD: -1,
CHF: 0, CHF: -1,
AUD: 0, AUD: -1,
JPY: 0, JPY: -1,
}; };
} }
@ -115,7 +115,7 @@ class PriceUpdater {
if (feed.currencies.includes(currency)) { if (feed.currencies.includes(currency)) {
try { try {
const price = await feed.$fetchPrice(currency); const price = await feed.$fetchPrice(currency);
if (price > 0) { if (price > -1 && price < MAX_PRICES[currency]) {
prices.push(price); prices.push(price);
} }
logger.debug(`${feed.name} BTC/${currency} price: ${price}`, logger.tags.mining); logger.debug(`${feed.name} BTC/${currency} price: ${price}`, logger.tags.mining);
@ -239,7 +239,7 @@ class PriceUpdater {
for (const currency of this.currencies) { for (const currency of this.currencies) {
const price = historicalEntry[time][currency]; 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); grouped[time][currency].push(typeof price === 'string' ? parseInt(price, 10) : price);
} }
} }

View File

@ -3,7 +3,7 @@
{{ addPlus && satoshis >= 0 ? '+' : '' }} {{ 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 (blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0
) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency ) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency
}} }}

View File

@ -443,9 +443,9 @@ export class BlockComponent implements OnInit, OnDestroy {
} }
this.priceSubscription = block$.pipe( this.priceSubscription = block$.pipe(
switchMap((block) => { switchMap((block) => {
return this.priceService.getPrices().pipe( return this.priceService.getBlockPrice$(block.timestamp).pipe(
tap(() => { tap((price) => {
this.blockConversion = this.priceService.getPriceForTimestamp(block.timestamp); this.blockConversion = price;
}) })
); );
}) })

View File

@ -327,9 +327,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.fetchRbfHistory$.next(this.tx.txid); this.fetchRbfHistory$.next(this.tx.txid);
} }
this.priceService.getPrices().pipe( this.priceService.getBlockPrice$(tx.status.block_time).pipe(
tap(() => { tap((price) => {
this.blockConversion = this.priceService.getPriceForTimestamp(tx.status.block_time); this.blockConversion = price;
}) })
).subscribe(); ).subscribe();

View File

@ -6,7 +6,7 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { AssetsService } from '../../services/assets.service'; 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 { BlockExtended } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { PriceService } from 'src/app/services/price.service'; import { PriceService } from 'src/app/services/price.service';
@ -150,10 +150,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
tx['addressValue'] = addressIn - addressOut; tx['addressValue'] = addressIn - addressOut;
} }
this.priceService.getPrices().pipe( this.priceService.getBlockPrice$(tx.status.block_time).pipe(
tap(() => { tap((price) => tx['price'] = price)
tx['price'] = this.priceService.getPriceForTimestamp(tx.status.block_time);
})
).subscribe(); ).subscribe();
}); });
const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid); const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);

View File

@ -1,7 +1,7 @@
<span class="green-color" *ngIf="blockConversion; else noblockconversion"> <span class="green-color" *ngIf="blockConversion; else noblockconversion">
{{ {{
( (
(blockConversion.price[currency] > 0 ? blockConversion.price[currency] : null) ?? (blockConversion.price[currency] >= 0 ? blockConversion.price[currency] : null) ??
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0 (blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0
) * value / 100000000 | fiatCurrency : digitsInfo : currency ) * value / 100000000 | fiatCurrency : digitsInfo : currency
}} }}

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; 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'; import { ApiService } from './api.service';
// nodejs backend interfaces // nodejs backend interfaces
@ -40,6 +40,8 @@ export interface ConversionDict {
providedIn: 'root' providedIn: 'root'
}) })
export class PriceService { export class PriceService {
priceObservable$: Observable<Conversion>;
historicalPrice: ConversionDict = { historicalPrice: ConversionDict = {
prices: null, prices: null,
exchangeRates: null, exchangeRates: null,
@ -61,65 +63,53 @@ export class PriceService {
}; };
} }
/** getBlockPrice$(blockTimestamp: number): Observable<Price | undefined> {
* Fetch prices from the nodejs backend only once if (!this.priceObservable$) {
*/ this.priceObservable$ = this.apiService.getHistoricalPrice$().pipe(shareReplay());
getPrices(): Observable<void> {
if (this.historicalPrice.prices) {
return of(null);
} }
return this.apiService.getHistoricalPrice$().pipe( return this.priceObservable$.pipe(
map((conversion: Conversion) => { map((conversion) => {
if (!this.historicalPrice.prices) { if (!blockTimestamp) {
this.historicalPrice.prices = Object(); return undefined;
} }
const historicalPrice = {
prices: {},
exchangeRates: conversion.exchangeRates,
};
for (const price of conversion.prices) { 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, USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD,
CHF: price.CHF, AUD: price.AUD, JPY: price.JPY CHF: price.CHF, AUD: price.AUD, JPY: price.JPY
}; };
} }
this.historicalPrice.exchangeRates = conversion.exchangeRates;
return;
}),
shareReplay(),
);
}
/** const priceTimestamps = Object.keys(historicalPrice.prices);
* Note: The first block with a price we have is block 68952 (using MtGox price history) priceTimestamps.push(Number.MAX_SAFE_INTEGER.toString());
* priceTimestamps.sort().reverse();
* @param blockTimestamp
*/ // Small trick here. Because latest blocks have higher timestamps than our
getPriceForTimestamp(blockTimestamp: number): Price | null { // latest price timestamp (we only insert once every hour), we have no price for them.
if (!blockTimestamp) { // Therefore we want to fallback to the websocket price by returning an undefined `price` field.
return undefined; // 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
const priceTimestamps = Object.keys(this.historicalPrice.prices); // vs ones without a price (yet) like the latest blocks
priceTimestamps.push(Number.MAX_SAFE_INTEGER.toString());
priceTimestamps.sort().reverse();
// Small trick here. Because latest blocks have higher timestamps than our for (const t of priceTimestamps) {
// latest price timestamp (we only insert once every hour), we have no price for them. const priceTimestamp = parseInt(t, 10);
// Therefore we want to fallback to the websocket price by returning an undefined `price` field. if (blockTimestamp > priceTimestamp) {
// Since this.historicalPrice.prices[Number.MAX_SAFE_INTEGER] does not exists return {
// it will return `undefined` and automatically use the websocket price. price: historicalPrice.prices[priceTimestamp],
// This way we can differenciate blocks without prices like the genesis block exchangeRates: historicalPrice.exchangeRates,
// vs ones without a price (yet) like the latest blocks };
}
for (const t of priceTimestamps) { }
const priceTimestamp = parseInt(t, 10);
if (blockTimestamp > priceTimestamp) { return this.getEmptyPrice();
return { })
price: this.historicalPrice.prices[priceTimestamp], );
exchangeRates: this.historicalPrice.exchangeRates,
};
}
}
return this.getEmptyPrice();
} }
} }