parent
6e7ed29caa
commit
f44eacd5d5
@ -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)
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}}
|
}}
|
||||||
|
@ -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;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
}}
|
}}
|
||||||
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user