Add support for USD in address history graph
This commit is contained in:
parent
684ad9f0e6
commit
7bef8653b1
@ -1,13 +1,16 @@
|
|||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
||||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||||
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
|
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError, tap } from 'rxjs/operators';
|
||||||
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
|
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { PriceService } from '../../services/price.service';
|
||||||
|
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||||
|
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||||
|
|
||||||
const periodSeconds = {
|
const periodSeconds = {
|
||||||
'1d': (60 * 60 * 24),
|
'1d': (60 * 60 * 24),
|
||||||
@ -45,6 +48,8 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
data: any[] = [];
|
data: any[] = [];
|
||||||
hoverData: any[] = [];
|
hoverData: any[] = [];
|
||||||
|
showFiat = false;
|
||||||
|
conversions: any;
|
||||||
|
|
||||||
subscription: Subscription;
|
subscription: Subscription;
|
||||||
redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
@ -66,6 +71,9 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
private amountShortenerPipe: AmountShortenerPipe,
|
private amountShortenerPipe: AmountShortenerPipe,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private relativeUrlPipe: RelativeUrlPipe,
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
|
private priceService: PriceService,
|
||||||
|
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||||
|
private fiatShortenerPipe: FiatShortenerPipe
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
@ -86,10 +94,12 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
|
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
|
||||||
return of(null);
|
return of(null);
|
||||||
}),
|
}),
|
||||||
))
|
)),
|
||||||
]).subscribe(([redraw, addressSummary]) => {
|
this.stateService.conversions$
|
||||||
|
]).subscribe(([redraw, addressSummary, conversions]) => {
|
||||||
if (addressSummary) {
|
if (addressSummary) {
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
this.conversions = conversions;
|
||||||
this.prepareChartOptions(addressSummary);
|
this.prepareChartOptions(addressSummary);
|
||||||
}
|
}
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
@ -101,15 +111,36 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareChartOptions(summary): void {
|
prepareChartOptions(summary: AddressTxSummary[]) {
|
||||||
if (!summary || !this.stats) {
|
if (!summary || !this.stats) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.priceService.getPriceByBulk$(summary.map(d => d.time), 'USD').pipe(
|
||||||
|
tap((prices) => {
|
||||||
|
if (prices.length !== summary.length) {
|
||||||
|
summary = summary.map(item => ({ ...item, price: 0 }));
|
||||||
|
} else {
|
||||||
|
summary = summary.map((item, index) => {
|
||||||
|
let price = 0;
|
||||||
|
if (prices[index].price) {
|
||||||
|
price = prices[index].price['USD'];
|
||||||
|
} else if (this.conversions['USD']) {
|
||||||
|
price = this.conversions['USD'];
|
||||||
|
}
|
||||||
|
return { ...item, price: price }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
|
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
|
||||||
this.data = summary.map(d => {
|
this.data = summary.map(d => {
|
||||||
const balance = total;
|
const balance = total;
|
||||||
|
const fiatValue = balance * d.price / 100_000_000;
|
||||||
total -= d.value;
|
total -= d.value;
|
||||||
return [d.time * 1000, balance, d];
|
d.fiatValue = d.value * d.price / 100_000_000;
|
||||||
|
return [d.time * 1000, balance, d, fiatValue];
|
||||||
}).reverse();
|
}).reverse();
|
||||||
|
|
||||||
if (this.period !== 'all') {
|
if (this.period !== 'all') {
|
||||||
@ -130,6 +161,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
{ offset: 0, color: '#FDD835' },
|
{ offset: 0, color: '#FDD835' },
|
||||||
{ offset: 1, color: '#FB8C00' },
|
{ offset: 1, color: '#FB8C00' },
|
||||||
]),
|
]),
|
||||||
|
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#4CAF50' },
|
||||||
|
{ offset: 1, color: '#1B5E20' },
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
animation: false,
|
animation: false,
|
||||||
grid: {
|
grid: {
|
||||||
@ -138,6 +173,34 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
right: this.right,
|
right: this.right,
|
||||||
left: this.left,
|
left: this.left,
|
||||||
},
|
},
|
||||||
|
legend: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
|
||||||
|
inactiveColor: 'var(--grey)',
|
||||||
|
textStyle: {
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
icon: 'roundRect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Fiat',
|
||||||
|
inactiveColor: 'var(--grey)',
|
||||||
|
textStyle: {
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
icon: 'roundRect',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
selected: {
|
||||||
|
'Balance': !this.showFiat,
|
||||||
|
'Fiat': this.showFiat
|
||||||
|
},
|
||||||
|
selectedMode: 'single',
|
||||||
|
formatter: function (name) {
|
||||||
|
return name === 'Fiat' ? 'USD' : 'BTC';
|
||||||
|
}
|
||||||
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
show: !this.isMobile(),
|
show: !this.isMobile(),
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
@ -160,19 +223,35 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
|
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
|
||||||
: `${data.length} transactions`;
|
: `${data.length} transactions`;
|
||||||
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
const val = data.reduce((total, d) => total + d.data[2].value, 0);
|
if (this.showFiat) {
|
||||||
const color = val === 0 ? '' : (val > 0 ? 'var(--green)' : 'var(--red)');
|
const val = data.reduce((total, d) => total + d.data[2].fiatValue, 0);
|
||||||
const symbol = val > 0 ? '+' : '';
|
const color = val === 0 ? '' : (val > 0 ? 'var(--green)' : 'var(--red)');
|
||||||
return `
|
const symbol = val > 0 ? '+' : '';
|
||||||
<div>
|
return `
|
||||||
<span><b>${header}</b></span>
|
<div>
|
||||||
<div style="text-align: right;">
|
<span><b>${header}</b></span>
|
||||||
<span style="color: ${color}">${symbol} ${(val / 100_000_000).toFixed(8)} BTC</span><br>
|
<div style="text-align: right;">
|
||||||
<span>${(data[0].data[1] / 100_000_000).toFixed(8)} BTC</span>
|
<span style="color: ${color}">${symbol} ${this.fiatCurrencyPipe.transform(val, null, 'USD')}</span><br>
|
||||||
|
<span>${this.fiatCurrencyPipe.transform(data[0].data[1], null, 'USD')}</span>
|
||||||
|
</div>
|
||||||
|
<span>${date}</span>
|
||||||
</div>
|
</div>
|
||||||
<span>${date}</span>
|
`;
|
||||||
</div>
|
} else {
|
||||||
`;
|
const val = data.reduce((total, d) => total + d.data[2].value, 0);
|
||||||
|
const color = val === 0 ? '' : (val > 0 ? 'var(--green)' : 'var(--red)');
|
||||||
|
const symbol = val > 0 ? '+' : '';
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<span><b>${header}</b></span>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<span style="color: ${color}">${symbol} ${(val / 100_000_000).toFixed(8)} BTC</span><br>
|
||||||
|
<span>${(data[0].data[1] / 100_000_000).toFixed(8)} BTC</span>
|
||||||
|
</div>
|
||||||
|
<span>${date}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
@ -211,14 +290,44 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
},
|
},
|
||||||
min: this.period === 'all' ? 0 : 'dataMin'
|
min: this.period === 'all' ? 0 : 'dataMin'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
position: 'left',
|
||||||
|
axisLabel: {
|
||||||
|
color: 'rgb(110, 112, 121)',
|
||||||
|
formatter: function(val) {
|
||||||
|
return this.fiatShortenerPipe.transform(val, null, 'USD');
|
||||||
|
}.bind(this)
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
min: this.period === 'all' ? 0 : 'dataMin'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
|
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
|
||||||
|
yAxisIndex: 0,
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
symbol: 'circle',
|
symbol: 'circle',
|
||||||
symbolSize: 8,
|
symbolSize: 8,
|
||||||
data: this.data,
|
data: this.data.map(d => [d[0], d[1], d[2]]),
|
||||||
|
areaStyle: {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
triggerLineEvent: true,
|
||||||
|
type: 'line',
|
||||||
|
smooth: false,
|
||||||
|
step: 'end'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Fiat',
|
||||||
|
yAxisIndex: 1,
|
||||||
|
showSymbol: false,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 8,
|
||||||
|
data: this.data.map(d => [d[0], d[3], d[2]]),
|
||||||
areaStyle: {
|
areaStyle: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
@ -241,10 +350,15 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
this.hoverData = (e?.dataByCoordSys?.[0]?.dataByAxis?.[0]?.seriesDataIndices || []).map(indices => this.data[indices.dataIndex]);
|
this.hoverData = (e?.dataByCoordSys?.[0]?.dataByAxis?.[0]?.seriesDataIndices || []).map(indices => this.data[indices.dataIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLegendSelectChanged(e) {
|
||||||
|
this.showFiat = e.name === 'Fiat';
|
||||||
|
}
|
||||||
|
|
||||||
onChartInit(ec) {
|
onChartInit(ec) {
|
||||||
this.chartInstance = ec;
|
this.chartInstance = ec;
|
||||||
this.chartInstance.on('showTip', this.onTooltip.bind(this));
|
this.chartInstance.on('showTip', this.onTooltip.bind(this));
|
||||||
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
|
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
|
||||||
|
this.chartInstance.on('legendselectchanged', this.onLegendSelectChanged.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
@ -150,7 +150,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
this.transactions.forEach((tx) => {
|
this.transactions.forEach((tx) => {
|
||||||
if (!this.blockTime) {
|
if (!this.blockTime) {
|
||||||
if (tx.status.block_time) {
|
if (tx.status.block_time) {
|
||||||
this.priceService.getBlockPrice$(tx.status.block_time, confirmedTxs < 10, this.currency).pipe(
|
this.priceService.getBlockPrice$(tx.status.block_time, confirmedTxs < 3, this.currency).pipe(
|
||||||
tap((price) => tx['price'] = price),
|
tap((price) => tx['price'] = price),
|
||||||
).subscribe();
|
).subscribe();
|
||||||
}
|
}
|
||||||
@ -235,7 +235,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.blockTime && tx.status.block_time && this.currency) {
|
if (!this.blockTime && tx.status.block_time && this.currency) {
|
||||||
this.priceService.getBlockPrice$(tx.status.block_time, confirmedTxs < 10, this.currency).pipe(
|
this.priceService.getBlockPrice$(tx.status.block_time, confirmedTxs < 3, this.currency).pipe(
|
||||||
tap((price) => tx['price'] = price),
|
tap((price) => tx['price'] = price),
|
||||||
).subscribe();
|
).subscribe();
|
||||||
}
|
}
|
||||||
|
@ -156,6 +156,8 @@ export interface AddressTxSummary {
|
|||||||
value: number;
|
value: number;
|
||||||
height: number;
|
height: number;
|
||||||
time: number;
|
time: number;
|
||||||
|
price?: number;
|
||||||
|
fiatValue?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChainStats {
|
export interface ChainStats {
|
||||||
|
@ -249,4 +249,75 @@ export class PriceService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
getPriceByBulk$(timestamps: number[], currency: string): Observable<Price[]> {
|
||||||
|
if (this.stateService.env.BASE_MODULE !== 'mempool' || !this.stateService.env.HISTORICAL_PRICE) {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date().getTime() / 1000;
|
||||||
|
|
||||||
|
if (!this.priceObservable$ || (this.priceObservable$ && (now - this.lastPriceHistoryUpdate > 3600 || currency !== this.lastQueriedHistoricalCurrency || this.networkChangedSinceLastQuery))) {
|
||||||
|
this.priceObservable$ = this.apiService.getHistoricalPrice$(undefined, currency).pipe(shareReplay());
|
||||||
|
this.lastPriceHistoryUpdate = new Date().getTime() / 1000;
|
||||||
|
this.lastQueriedHistoricalCurrency = currency;
|
||||||
|
this.networkChangedSinceLastQuery = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.priceObservable$.pipe(
|
||||||
|
map((conversion) => {
|
||||||
|
if (!conversion) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const historicalPrice = {
|
||||||
|
prices: {},
|
||||||
|
exchangeRates: conversion.exchangeRates,
|
||||||
|
};
|
||||||
|
for (const price of conversion.prices) {
|
||||||
|
historicalPrice.prices[price.time] = this.stateService.env.ADDITIONAL_CURRENCIES ? {
|
||||||
|
USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD, CHF: price.CHF, AUD: price.AUD,
|
||||||
|
JPY: price.JPY, BGN: price.BGN, BRL: price.BRL, CNY: price.CNY, CZK: price.CZK, DKK: price.DKK,
|
||||||
|
HKD: price.HKD, HRK: price.HRK, HUF: price.HUF, IDR: price.IDR, ILS: price.ILS, INR: price.INR,
|
||||||
|
ISK: price.ISK, KRW: price.KRW, MXN: price.MXN, MYR: price.MYR, NOK: price.NOK, NZD: price.NZD,
|
||||||
|
PHP: price.PHP, PLN: price.PLN, RON: price.RON, RUB: price.RUB, SEK: price.SEK, SGD: price.SGD,
|
||||||
|
THB: price.THB, TRY: price.TRY, ZAR: price.ZAR
|
||||||
|
} : {
|
||||||
|
USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD, CHF: price.CHF, AUD: price.AUD, JPY: price.JPY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceTimestamps = Object.keys(historicalPrice.prices).map(Number);
|
||||||
|
priceTimestamps.push(Number.MAX_SAFE_INTEGER);
|
||||||
|
priceTimestamps.sort((a, b) => b - a);
|
||||||
|
|
||||||
|
const prices: Price[] = [];
|
||||||
|
|
||||||
|
for (const timestamp of timestamps) {
|
||||||
|
let left = 0;
|
||||||
|
let right = priceTimestamps.length - 1;
|
||||||
|
let match = -1;
|
||||||
|
|
||||||
|
// Binary search to find the closest larger element
|
||||||
|
while (left <= right) {
|
||||||
|
const mid = Math.floor((left + right) / 2);
|
||||||
|
if (priceTimestamps[mid] > timestamp) {
|
||||||
|
match = mid;
|
||||||
|
left = mid + 1;
|
||||||
|
} else {
|
||||||
|
right = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match !== -1) {
|
||||||
|
const priceTimestamp = priceTimestamps[match];
|
||||||
|
prices.push({
|
||||||
|
price: historicalPrice.prices[priceTimestamp],
|
||||||
|
exchangeRates: historicalPrice.exchangeRates,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prices;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user