';
+
+ const hasTx = data[0].data[2].txid;
+ if (hasTx) {
+ const header = data.length === 1
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
: `${data.length} transactions`;
+ tooltip += `
`;
+ }
+
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);
- const color = val === 0 ? '' : (val > 0 ? 'var(--green)' : 'var(--red)');
- const symbol = val > 0 ? '+' : '';
- return `
-
-
${header}
-
- ${symbol} ${(val / 100_000_000).toFixed(8)} BTC
- ${(data[0].data[1] / 100_000_000).toFixed(8)} BTC
-
-
${date}
+
+ tooltip += `
+
`;
+
+ const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal);
+ const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
+
+ const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0);
+ const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0);
+ const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)');
+ const fiatColor = fiatVal === 0 ? '' : (fiatVal > 0 ? 'var(--green)' : 'var(--red)');
+ const btcSymbol = btcVal > 0 ? '+' : '';
+ const fiatSymbol = fiatVal > 0 ? '+' : '';
+
+ if (btcData.length && fiatData.length) {
+ tooltip += `
+ ${btcSymbol} ${formatBTC(btcVal, 4)} BTC
+ ${fiatSymbol} ${formatFiat(fiatVal)}
- `;
+
+ ${formatBTC(btcData[0].data[1], 4)} BTC
+ ${formatFiat(fiatData[0].data[1])}
+
`;
+ } else if (btcData.length) {
+ tooltip += `
${btcSymbol} ${formatBTC(btcVal, 8)} BTC
+
${formatBTC(data[0].data[1], 8)} BTC`;
+ } else {
+ if (this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]) {
+ tooltip += `
+ ${formatBTC(data[0].data[3], 4)} BTC
+ ${formatFiat(data[0].data[1])}
+
`;
+ } else {
+ tooltip += `${hasTx ? `
${fiatSymbol} ${formatFiat(fiatVal)}` : ''}
+
${formatFiat(data[0].data[1])}`;
+ }
+ }
+
+ tooltip += `
${date}`;
+ return tooltip;
}.bind(this)
},
xAxis: {
@@ -211,10 +330,24 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
},
min: this.period === 'all' ? 0 : 'dataMin'
},
+ {
+ type: 'value',
+ 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: [
{
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
+ yAxisIndex: 0,
showSymbol: false,
symbol: 'circle',
symbolSize: 8,
@@ -226,14 +359,58 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
type: 'line',
smooth: false,
step: 'end'
+ },
+ {
+ name: 'Fiat',
+ yAxisIndex: 1,
+ showSymbol: false,
+ symbol: 'circle',
+ symbolSize: 8,
+ data: this.fiatData,
+ areaStyle: {
+ opacity: 0.5,
+ },
+ triggerLineEvent: true,
+ type: 'line',
+ smooth: false,
+ step: 'end'
}
],
+ dataZoom: this.allowZoom ? [{
+ type: 'inside',
+ realtime: true,
+ zoomLock: true,
+ maxSpan: 100,
+ minSpan: 5,
+ moveOnMouseMove: false,
+ }, {
+ showDetail: false,
+ show: true,
+ type: 'slider',
+ brushSelect: false,
+ realtime: true,
+ left: this.left,
+ right: this.right,
+ selectedDataBackground: {
+ lineStyle: {
+ color: '#fff',
+ opacity: 0.45,
+ },
+ },
+ }] : undefined
};
}
onChartClick(e) {
if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) {
- this.router.navigate([this.relativeUrlPipe.transform('/tx/'), this.hoverData[0][2].txid]);
+ this.zone.run(() => {
+ const url = this.relativeUrlPipe.transform(`/tx/${this.hoverData[0][2].txid}`);
+ if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
+ window.open(url);
+ } else {
+ this.router.navigate([url]);
+ }
+ });
}
}
@@ -241,10 +418,38 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
this.hoverData = (e?.dataByCoordSys?.[0]?.dataByAxis?.[0]?.seriesDataIndices || []).map(indices => this.data[indices.dataIndex]);
}
+ onLegendSelectChanged(e) {
+ this.selected = e.selected;
+ this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
+ this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
+
+ this.chartOptions = {
+ grid: {
+ right: this.right,
+ left: this.left,
+ },
+ legend: {
+ selected: this.selected,
+ },
+ dataZoom: this.allowZoom ? [{
+ left: this.left,
+ right: this.right,
+ }, {
+ left: this.left,
+ right: this.right,
+ }] : undefined
+ };
+
+ if (this.chartInstance) {
+ this.chartInstance.setOption(this.chartOptions);
+ }
+ }
+
onChartInit(ec) {
this.chartInstance = ec;
this.chartInstance.on('showTip', this.onTooltip.bind(this));
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
+ this.chartInstance.on('legendselectchanged', this.onLegendSelectChanged.bind(this));
}
ngOnDestroy(): void {
@@ -256,4 +461,27 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
isMobile() {
return (window.innerWidth <= 767.98);
}
+
+ extendSummary(summary) {
+ let extendedSummary = summary.slice();
+
+ // Add a point at today's date to make the graph end at the current time
+ extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
+ extendedSummary.reverse();
+
+ let oneHour = 60 * 60;
+ // Fill gaps longer than interval
+ for (let i = 0; i < extendedSummary.length - 1; i++) {
+ let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);
+ if (hours > 1) {
+ for (let j = 1; j < hours; j++) {
+ let newTime = extendedSummary[i].time + oneHour * j;
+ extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
+ }
+ i += hours - 1;
+ }
+ }
+
+ return extendedSummary.reverse();
+ }
}
diff --git a/frontend/src/app/components/address-transactions-widget/address-transactions-widget.component.ts b/frontend/src/app/components/address-transactions-widget/address-transactions-widget.component.ts
index c3fc4260e..998d269ba 100644
--- a/frontend/src/app/components/address-transactions-widget/address-transactions-widget.component.ts
+++ b/frontend/src/app/components/address-transactions-widget/address-transactions-widget.component.ts
@@ -58,7 +58,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
return summary?.slice(0, 6);
}),
switchMap(txs => {
- return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, true, this.currency).pipe(
+ return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe(
map(price => {
return {
...tx,
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts
index 688c941b0..316a6ab85 100644
--- a/frontend/src/app/components/transactions-list/transactions-list.component.ts
+++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts
@@ -150,7 +150,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
this.transactions.forEach((tx) => {
if (!this.blockTime) {
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),
).subscribe();
}
@@ -235,7 +235,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}
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),
).subscribe();
}
diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts
index ab96488fe..726649090 100644
--- a/frontend/src/app/interfaces/electrs.interface.ts
+++ b/frontend/src/app/interfaces/electrs.interface.ts
@@ -156,6 +156,7 @@ export interface AddressTxSummary {
value: number;
height: number;
time: number;
+ price?: number;
}
export interface ChainStats {
diff --git a/frontend/src/app/services/price.service.ts b/frontend/src/app/services/price.service.ts
index a27c65df8..c342796e0 100644
--- a/frontend/src/app/services/price.service.ts
+++ b/frontend/src/app/services/price.service.ts
@@ -249,4 +249,75 @@ export class PriceService {
);
}
}
-}
+
+ getPriceByBulk$(timestamps: number[], currency: string): Observable
{
+ 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;
+ }));
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts b/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts
index 93ab5cf8f..4ce171054 100644
--- a/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts
+++ b/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts
@@ -31,7 +31,7 @@ export class FiatShortenerPipe implements PipeTransform {
{ value: 1, symbol: '' },
{ value: 1e3, symbol: 'k' },
{ value: 1e6, symbol: 'M' },
- { value: 1e9, symbol: 'G' },
+ { value: 1e9, symbol: 'B' },
{ value: 1e12, symbol: 'T' },
{ value: 1e15, symbol: 'P' },
{ value: 1e18, symbol: 'E' }