Fetch historical data based on timestamp and currency

This commit is contained in:
natsoni 2024-03-09 16:32:21 +01:00
parent 23076172e4
commit ccf1121f19
No known key found for this signature in database
GPG Key ID: C65917583181743B
7 changed files with 100 additions and 29 deletions

View File

@ -329,14 +329,29 @@ class PricesRepository {
throw Error(`Cannot get single historical price from the database`); throw Error(`Cannot get single historical price from the database`);
} }
let pricesUsedForExchangeRates; // If we don't have a fx API key, we need to use the latest prices to compute the exchange rates
if (!config.MEMPOOL.CURRENCY_API_KEY) {
const [latestPrices] = await DB.query(`
SELECT ${ApiPriceFields}
FROM prices
ORDER BY time DESC
LIMIT 1
`);
if (!Array.isArray(latestPrices)) {
throw Error(`Cannot get single historical price from the database`);
}
pricesUsedForExchangeRates = latestPrices[0] as ApiPrice;
} else {
pricesUsedForExchangeRates = rates[0] as ApiPrice;
}
// Compute fiat exchange rates // Compute fiat exchange rates
let latestPrice = rates[0] as ApiPrice; let latestPrice = pricesUsedForExchangeRates;
if (!latestPrice || latestPrice.USD === -1) { if (!latestPrice || latestPrice.USD === -1) {
latestPrice = priceUpdater.getEmptyPricesObj(); latestPrice = priceUpdater.getEmptyPricesObj();
} }
const computeFx = (usd: number, other: number): number => const computeFx = (usd: number, other: number): number => usd <= 0.05 ? 0 : Math.round(Math.max(other, 0) / usd * 100) / 100;
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
const exchangeRates: ExchangeRates = config.MEMPOOL.CURRENCY_API_KEY ? const exchangeRates: ExchangeRates = config.MEMPOOL.CURRENCY_API_KEY ?
{ {
@ -388,7 +403,8 @@ class PricesRepository {
const filteredRates = rates.map((rate: any) => { const filteredRates = rates.map((rate: any) => {
return { return {
time: rate.time, time: rate.time,
[currency]: rate[currency] [currency]: rate[currency],
['USD']: rate['USD']
}; };
}); });
return { return {
@ -424,8 +440,8 @@ class PricesRepository {
latestPrice = priceUpdater.getEmptyPricesObj(); latestPrice = priceUpdater.getEmptyPricesObj();
} }
const computeFx = (usd: number, other: number): number => const computeFx = (usd: number, other: number): number =>
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100; usd <= 0 ? 0 : Math.round(Math.max(other, 0) / usd * 100) / 100;
const exchangeRates: ExchangeRates = config.MEMPOOL.CURRENCY_API_KEY ? const exchangeRates: ExchangeRates = config.MEMPOOL.CURRENCY_API_KEY ?
{ {
@ -477,7 +493,8 @@ class PricesRepository {
const filteredRates = rates.map((rate: any) => { const filteredRates = rates.map((rate: any) => {
return { return {
time: rate.time, time: rate.time,
[currency]: rate[currency] [currency]: rate[currency],
['USD']: rate['USD']
}; };
}); });
return { return {

View File

@ -335,7 +335,7 @@
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
<app-transactions-list [transactions]="transactions" [paginated]="true"></app-transactions-list> <app-transactions-list [transactions]="transactions" [paginated]="true" [blockTime]="block.timestamp"></app-transactions-list>
<ng-template [ngIf]="transactionsError"> <ng-template [ngIf]="transactionsError">
<div class="text-center"> <div class="text-center">

View File

@ -526,9 +526,9 @@ export class BlockComponent implements OnInit, OnDestroy {
if (this.priceSubscription) { if (this.priceSubscription) {
this.priceSubscription.unsubscribe(); this.priceSubscription.unsubscribe();
} }
this.priceSubscription = block$.pipe( this.priceSubscription = combineLatest([this.stateService.fiatCurrency$, block$]).pipe(
switchMap((block) => { switchMap(([currency, block]) => {
return this.priceService.getBlockPrice$(block.timestamp).pipe( return this.priceService.getBlockPrice$(block.timestamp, true, currency).pipe(
tap((price) => { tap((price) => {
this.blockConversion = price; this.blockConversion = price;
}) })

View File

@ -76,6 +76,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
mempoolBlocksSubscription: Subscription; mempoolBlocksSubscription: Subscription;
blocksSubscription: Subscription; blocksSubscription: Subscription;
miningSubscription: Subscription; miningSubscription: Subscription;
currencyChangeSubscription: Subscription;
fragmentParams: URLSearchParams; fragmentParams: URLSearchParams;
rbfTransaction: undefined | Transaction; rbfTransaction: undefined | Transaction;
replaced: boolean = false; replaced: boolean = false;
@ -493,10 +494,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} }
} }
this.fetchRbfHistory$.next(this.tx.txid); this.fetchRbfHistory$.next(this.tx.txid);
this.currencyChangeSubscription?.unsubscribe();
this.priceService.getBlockPrice$(tx.status?.block_time, true).pipe( this.currencyChangeSubscription = this.stateService.fiatCurrency$.pipe(
tap((price) => { switchMap((currency) => {
this.blockConversion = price; return tx.status.block_time ? this.priceService.getBlockPrice$(tx.status.block_time, true, currency).pipe(
tap((price) => tx['price'] = price),
) : of(undefined);
}) })
).subscribe(); ).subscribe();
@ -810,6 +813,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.mempoolBlocksSubscription.unsubscribe(); this.mempoolBlocksSubscription.unsubscribe();
this.blocksSubscription.unsubscribe(); this.blocksSubscription.unsubscribe();
this.miningSubscription?.unsubscribe(); this.miningSubscription?.unsubscribe();
this.currencyChangeSubscription?.unsubscribe();
this.leaveTransaction(); this.leaveTransaction();
} }
} }

View File

@ -32,11 +32,14 @@ export class TransactionsListComponent implements OnInit, OnChanges {
@Input() outputIndex: number; @Input() outputIndex: number;
@Input() address: string = ''; @Input() address: string = '';
@Input() rowLimit = 12; @Input() rowLimit = 12;
@Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block
@Output() loadMore = new EventEmitter(); @Output() loadMore = new EventEmitter();
latestBlock$: Observable<BlockExtended>; latestBlock$: Observable<BlockExtended>;
outspendsSubscription: Subscription; outspendsSubscription: Subscription;
currencyChangeSubscription: Subscription;
currency: string;
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject(); refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
refreshChannels$: ReplaySubject<string[]> = new ReplaySubject(); refreshChannels$: ReplaySubject<string[]> = new ReplaySubject();
showDetails$ = new BehaviorSubject<boolean>(false); showDetails$ = new BehaviorSubject<boolean>(false);
@ -125,6 +128,35 @@ export class TransactionsListComponent implements OnInit, OnChanges {
) )
, ,
).subscribe(() => this.ref.markForCheck()); ).subscribe(() => this.ref.markForCheck());
this.currencyChangeSubscription = this.stateService.fiatCurrency$
.subscribe(currency => {
this.currency = currency;
this.refreshPrice();
});
}
refreshPrice(): void {
// Loop over all transactions
if (!this.transactions || !this.transactions.length || !this.currency) {
return;
}
const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
if (!this.blockTime) {
this.transactions.forEach((tx) => {
if (!this.blockTime) {
if (tx.status.block_time) {
this.priceService.getBlockPrice$(tx.status.block_time, confirmedTxs < 10, this.currency).pipe(
tap((price) => tx['price'] = price),
).subscribe();
}
}
});
} else {
this.priceService.getBlockPrice$(this.blockTime, true, this.currency).pipe(
tap((price) => this.transactions.forEach((tx) => tx['price'] = price)),
).subscribe();
}
} }
ngOnChanges(changes): void { ngOnChanges(changes): void {
@ -148,6 +180,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
this.transactionsLength = this.transactions.length; this.transactionsLength = this.transactions.length;
this.cacheService.setTxCache(this.transactions); this.cacheService.setTxCache(this.transactions);
const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
this.transactions.forEach((tx) => { this.transactions.forEach((tx) => {
tx['@voutLimit'] = true; tx['@voutLimit'] = true;
tx['@vinLimit'] = true; tx['@vinLimit'] = true;
@ -197,10 +230,18 @@ export class TransactionsListComponent implements OnInit, OnChanges {
} }
} }
this.priceService.getBlockPrice$(tx.status.block_time).pipe( if (!this.blockTime && tx.status.block_time && this.currency) {
tap((price) => tx['price'] = price) this.priceService.getBlockPrice$(tx.status.block_time, confirmedTxs < 10, this.currency).pipe(
).subscribe(); tap((price) => tx['price'] = price),
).subscribe();
}
}); });
if (this.blockTime && this.transactions?.length && this.currency) {
this.priceService.getBlockPrice$(this.blockTime, true, this.currency).pipe(
tap((price) => this.transactions.forEach((tx) => tx['price'] = price)),
).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);
if (txIds.length && !this.cached) { if (txIds.length && !this.cached) {
this.refreshOutspends$.next(txIds); this.refreshOutspends$.next(txIds);
@ -308,5 +349,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
ngOnDestroy(): void { ngOnDestroy(): void {
this.outspendsSubscription.unsubscribe(); this.outspendsSubscription.unsubscribe();
this.currencyChangeSubscription?.unsubscribe();
} }
} }

View File

@ -1,5 +1,5 @@
import { Component, ElementRef, ViewChild, Input, OnChanges, OnInit } from '@angular/core'; import { Component, ElementRef, ViewChild, Input, OnChanges, OnInit } from '@angular/core';
import { tap } from 'rxjs'; import { Subscription, of, switchMap, tap } from 'rxjs';
import { Price, PriceService } from '../../services/price.service'; import { Price, PriceService } from '../../services/price.service';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
@ -35,6 +35,7 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
tooltipPosition = { x: 0, y: 0 }; tooltipPosition = { x: 0, y: 0 };
blockConversion: Price; blockConversion: Price;
currencyChangeSubscription: Subscription;
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
@ -47,11 +48,14 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
ngOnChanges(changes): void { ngOnChanges(changes): void {
if (changes.line?.currentValue) { if (changes.line?.currentValue) {
this.priceService.getBlockPrice$(changes.line?.currentValue.timestamp, true).pipe( this.currencyChangeSubscription?.unsubscribe();
tap((price) => { this.currencyChangeSubscription = this.stateService.fiatCurrency$.pipe(
this.blockConversion = price; switchMap((currency) => {
}) return changes.line?.currentValue.timestamp ? this.priceService.getBlockPrice$(changes.line?.currentValue.timestamp, true, currency).pipe(
).subscribe(); tap((price) => this.blockConversion = price),
) : of(undefined);
})
).subscribe();
} }
if (changes.cursorPosition && changes.cursorPosition.currentValue) { if (changes.cursorPosition && changes.cursorPosition.currentValue) {

View File

@ -98,6 +98,8 @@ export class PriceService {
lastQueriedTimestamp: number; lastQueriedTimestamp: number;
lastPriceHistoryUpdate: number; lastPriceHistoryUpdate: number;
lastQueriedCurrency: string;
lastQueriedHistoricalCurrency: string;
historicalPrice: ConversionDict = { historicalPrice: ConversionDict = {
prices: null, prices: null,
@ -130,7 +132,7 @@ export class PriceService {
}; };
} }
getBlockPrice$(blockTimestamp: number, singlePrice = false): Observable<Price | undefined> { getBlockPrice$(blockTimestamp: number, singlePrice = false, currency: string): Observable<Price | undefined> {
if (this.stateService.env.BASE_MODULE !== 'mempool' || !this.stateService.env.HISTORICAL_PRICE) { if (this.stateService.env.BASE_MODULE !== 'mempool' || !this.stateService.env.HISTORICAL_PRICE) {
return of(undefined); return of(undefined);
} }
@ -142,9 +144,10 @@ export class PriceService {
* query a different timestamp than the last one * query a different timestamp than the last one
*/ */
if (singlePrice) { if (singlePrice) {
if (!this.singlePriceObservable$ || (this.singlePriceObservable$ && blockTimestamp !== this.lastQueriedTimestamp)) { if (!this.singlePriceObservable$ || (this.singlePriceObservable$ && (blockTimestamp !== this.lastQueriedTimestamp || currency !== this.lastQueriedCurrency))) {
this.singlePriceObservable$ = this.apiService.getHistoricalPrice$(blockTimestamp).pipe(shareReplay()); this.singlePriceObservable$ = this.apiService.getHistoricalPrice$(blockTimestamp, currency).pipe(shareReplay());
this.lastQueriedTimestamp = blockTimestamp; this.lastQueriedTimestamp = blockTimestamp;
this.lastQueriedCurrency = currency;
} }
return this.singlePriceObservable$.pipe( return this.singlePriceObservable$.pipe(
@ -177,9 +180,10 @@ export class PriceService {
* Query all price history only once. The observable is invalidated after 1 hour * Query all price history only once. The observable is invalidated after 1 hour
*/ */
else { else {
if (!this.priceObservable$ || (this.priceObservable$ && (now - this.lastPriceHistoryUpdate > 3600))) { if (!this.priceObservable$ || (this.priceObservable$ && (now - this.lastPriceHistoryUpdate > 3600 || currency !== this.lastQueriedHistoricalCurrency))) {
this.priceObservable$ = this.apiService.getHistoricalPrice$(undefined).pipe(shareReplay()); this.priceObservable$ = this.apiService.getHistoricalPrice$(undefined, currency).pipe(shareReplay());
this.lastPriceHistoryUpdate = new Date().getTime() / 1000; this.lastPriceHistoryUpdate = new Date().getTime() / 1000;
this.lastQueriedHistoricalCurrency = currency;
} }
return this.priceObservable$.pipe( return this.priceObservable$.pipe(