Merge pull request #3092 from mempool/nymkappa/feature/historical-price
Use historical price in older blocks / transactions
This commit is contained in:
commit
70113d9c91
@ -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<void> {
|
||||
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<void> {
|
||||
try {
|
||||
const stats = await mining.$getPoolStat(req.params.slug);
|
||||
|
@ -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<void> {
|
||||
if (prices.USD === 0) {
|
||||
@ -60,6 +85,34 @@ class PricesRepository {
|
||||
}
|
||||
return rates[0];
|
||||
}
|
||||
|
||||
public async $getHistoricalPrice(): Promise<Conversion | null> {
|
||||
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();
|
||||
|
@ -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,
|
||||
|
@ -1,7 +1,19 @@
|
||||
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
|
||||
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}</span>
|
||||
<span class="fiat" *ngIf="blockConversion; else noblockconversion">
|
||||
{{ addPlus && satoshis >= 0 ? '+' : '' }}
|
||||
{{
|
||||
(
|
||||
(blockConversion.price[currency] > 0 ? blockConversion.price[currency] : null) ??
|
||||
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0
|
||||
) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency
|
||||
}}
|
||||
</span>
|
||||
<ng-template #noblockconversion>
|
||||
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}</span>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-template #viewFiatVin>
|
||||
|
||||
<ng-template #viewFiatVin>
|
||||
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
|
||||
<span i18n="shared.confidential">Confidential</span>
|
||||
</ng-template>
|
||||
|
@ -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,
|
||||
|
@ -124,7 +124,13 @@
|
||||
</tr>
|
||||
<tr *ngIf="block?.extras?.medianFee != undefined">
|
||||
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
||||
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
||||
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
<span class="fiat">
|
||||
<app-fiat [blockConversion]="blockConversion" [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2"
|
||||
i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes"
|
||||
placement="bottom"></app-fiat>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
|
||||
<tr>
|
||||
@ -132,13 +138,13 @@
|
||||
<td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees">
|
||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
<span class="fiat">
|
||||
<app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat>
|
||||
<app-fiat [blockConversion]="blockConversion" [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat>
|
||||
</span>
|
||||
</td>
|
||||
<ng-template #liquidTotalFees>
|
||||
<td>
|
||||
<app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <app-fiat
|
||||
[value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat>
|
||||
[blockConversion]="blockConversion" [value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat>
|
||||
</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
@ -147,7 +153,7 @@
|
||||
<td>
|
||||
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
<span class="fiat">
|
||||
<app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat>
|
||||
<app-fiat [blockConversion]="blockConversion" [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -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<BlockOverviewGraphComponent>;
|
||||
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
|
||||
@ -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 {
|
||||
|
@ -469,7 +469,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></td>
|
||||
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="tx.fee"></app-fiat></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||
|
@ -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) => {
|
||||
|
@ -88,7 +88,7 @@
|
||||
</ng-template>
|
||||
<ng-template #defaultOutput>
|
||||
<span *ngIf="vin.lazy" class="skeleton-loader"></span>
|
||||
<app-amount *ngIf="vin.prevout" [satoshis]="vin.prevout.value"></app-amount>
|
||||
<app-amount [blockConversion]="tx.price" *ngIf="vin.prevout" [satoshis]="vin.prevout.value"></app-amount>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
@ -216,7 +216,7 @@
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template #defaultOutput>
|
||||
<app-amount [satoshis]="vout.value"></app-amount>
|
||||
<app-amount [blockConversion]="tx.price" [satoshis]="vout.value"></app-amount>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td class="arrow-td">
|
||||
@ -283,7 +283,9 @@
|
||||
|
||||
<div class="summary">
|
||||
<div class="float-left mt-2-5" *ngIf="!transactionPage && !tx.vin[0].is_coinbase && tx.fee !== -1">
|
||||
{{ tx.fee / (tx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="d-none d-sm-inline-block"> – {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></span>
|
||||
{{ tx.fee / (tx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span
|
||||
class="d-none d-sm-inline-block"> – {{ tx.fee | number }} <span class="symbol"
|
||||
i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span></span>
|
||||
</div>
|
||||
<div class="float-left mt-2-5 grey-info-text" *ngIf="tx.fee === -1" i18n="transactions-list.load-to-reveal-fee-info">Show more inputs to reveal fee data</div>
|
||||
|
||||
@ -301,12 +303,12 @@
|
||||
<button *ngIf="address === ''; else viewingAddress" type="button" class="btn btn-sm btn-primary mt-2 ml-2" (click)="switchCurrency()">
|
||||
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
|
||||
<ng-template #defaultAmount>
|
||||
<app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
|
||||
<app-amount [blockConversion]="tx.price" [satoshis]="getTotalTxOutput(tx)"></app-amount>
|
||||
</ng-template>
|
||||
</button>
|
||||
<ng-template #viewingAddress>
|
||||
<button type="button" class="btn btn-sm mt-2 ml-2" (click)="switchCurrency()" [ngClass]="{'btn-success': tx['addressValue'] >= 0, 'btn-danger': tx['addressValue'] < 0}">
|
||||
<app-amount [satoshis]="tx['addressValue']" [addPlus]="true"></app-amount>
|
||||
<app-amount [blockConversion]="tx.price" [satoshis]="tx['addressValue']" [addPlus]="true"></app-amount>
|
||||
</button>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
@ -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) {
|
||||
|
@ -1 +1,14 @@
|
||||
<span class="green-color" *ngIf="(conversions$ | async) as conversions">{{ (conversions ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }}</span>
|
||||
<span class="green-color" *ngIf="blockConversion; else noblockconversion">
|
||||
{{
|
||||
(
|
||||
(blockConversion.price[currency] > 0 ? blockConversion.price[currency] : null) ??
|
||||
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0
|
||||
) * value / 100000000 | fiatCurrency : digitsInfo : currency
|
||||
}}
|
||||
</span>
|
||||
|
||||
<ng-template #noblockconversion>
|
||||
<span class="green-color" *ngIf="(conversions$ | async) as conversions">
|
||||
{{ (conversions[currency] ?? conversions['USD'] ?? 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }}
|
||||
</span>
|
||||
</ng-template>
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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<Conversion> {
|
||||
return this.httpClient.get<Conversion>( this.apiBaseUrl + this.apiBasePath + '/api/v1/historical-price');
|
||||
}
|
||||
}
|
||||
|
121
frontend/src/app/services/price.service.ts
Normal file
121
frontend/src/app/services/price.service.ts
Normal file
@ -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<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user