Merge pull request #5916 from mempool/natsoni/liquid-usd

Add USD series to Liquid reserves graph
This commit is contained in:
wiz
2025-06-02 15:19:20 -07:00
committed by GitHub
4 changed files with 160 additions and 15 deletions

View File

@@ -4,6 +4,7 @@ import config from '../../config';
import elementsParser from './elements-parser';
import icons from './icons';
import { handleError } from '../../utils/api';
import PricesRepository from '../../repositories/PricesRepository';
class LiquidRoutes {
public initRoutes(app: Application) {
@@ -31,6 +32,7 @@ class LiquidRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/emergency-spent', this.$getEmergencySpentUtxos)
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/emergency-spent/stats', this.$getEmergencySpentUtxosStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/status', this.$getFederationAuditStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'historical-price', this.$getHistoricalPrice)
;
}
}
@@ -255,6 +257,34 @@ class LiquidRoutes {
}
}
private async $getHistoricalPrice(req: Request, res: Response): Promise<void> {
try {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Prices are not available on testnets.');
return;
}
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
const currency = req.query.currency as string;
let response;
if (timestamp && currency) {
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
} else if (timestamp) {
response = await PricesRepository.$getNearestHistoricalPrice(timestamp);
} else if (currency) {
response = await PricesRepository.$getHistoricalPrices(currency);
} else {
response = await PricesRepository.$getHistoricalPrices();
}
res.status(200).send(response);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical prices');
}
}
}
export default new LiquidRoutes();

View File

@@ -1,4 +1,4 @@
<div class="echarts" *browserOnly echarts [initOpts]="pegsChartInitOption" [options]="pegsChartOptions" (chartRendered)="rendered()"></div>
<div class="echarts" *browserOnly echarts [initOpts]="pegsChartInitOption" [options]="pegsChartOptions" (chartInit)="onChartInit($event)" (chartRendered)="rendered()"></div>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View File

@@ -1,7 +1,10 @@
import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit, ChangeDetectorRef } from '@angular/core';
import { formatDate, formatNumber } from '@angular/common';
import { EChartsOption } from '@app/graphs/echarts';
import { StateService } from '@app/services/state.service';
import { map, Subscription, switchMap } from 'rxjs';
import { PriceService } from '../../services/price.service';
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
@Component({
selector: 'app-lbtc-pegs-graph',
@@ -21,19 +24,31 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
@Input() data: any;
@Input() height: number | string = '360';
pegsChartOptions: EChartsOption;
subscription: Subscription;
right: number | string = '10';
right: number | string = '5';
top: number | string = '20';
left: number | string = '50';
left: number | string = '60';
template: ('widget' | 'advanced') = 'widget';
isLoading = true;
chartInstance: any = undefined;
pegsChartInitOption = {
renderer: 'svg'
};
adjustedLeft: number;
adjustedRight: number;
selected = {
'L-BTC': true,
'BTC': true,
'USD': false,
};
constructor(
public stateService: StateService,
public priceService: PriceService,
public amountShortenerPipe: AmountShortenerPipe,
public cd: ChangeDetectorRef,
@Inject(LOCALE_ID) private locale: string,
) { }
@@ -42,14 +57,25 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
}
ngOnChanges() {
if (!this.data?.liquidPegs) {
if (!this.data?.liquidReserves) {
return;
}
if (!this.data.liquidReserves) {
this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels);
} else {
this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels, this.data.liquidReserves.series);
}
this.subscription = this.stateService.conversions$.pipe(
switchMap(conversions =>
this.priceService.getPriceByBulk$(this.data.liquidPegs.labels.map((date: string) => Math.floor(new Date(date).getTime() / 1000)).slice(0, -1), 'USD')
.pipe(
map(prices => this.data.liquidReserves.series.map((value, i) => value * (prices[i]?.price.USD || conversions['USD'])))
)
)
).subscribe((usdBanlance: any) => {
if (!this.data.liquidReserves) {
this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels);
} else {
this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels, this.data.liquidReserves.series, usdBanlance);
}
this.cd.markForCheck();
});
}
rendered() {
@@ -59,7 +85,7 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
this.isLoading = false;
}
createChartOptions(pegSeries: number[], labels: string[], reservesSeries?: number[],): EChartsOption {
createChartOptions(pegSeries: number[], labels: string[], reservesSeries?: number[], usdBalance?: number[]): EChartsOption {
return {
grid: {
height: this.height,
@@ -89,6 +115,35 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
}
}
}],
legend: {
data: [
{
name: 'L-BTC',
inactiveColor: 'var(--grey)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
},
{
name: 'BTC',
inactiveColor: 'var(--grey)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
},
{
name: 'USD',
inactiveColor: 'var(--grey)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
}
],
selected: this.selected,
},
tooltip: {
trigger: 'axis',
position: (pos, params, el, elRect, size) => {
@@ -109,10 +164,12 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
for (let index = params.length - 1; index >= 0; index--) {
const item = params[index];
if (index < 26) {
let formattedValue;
item.seriesName === 'USD' ? formattedValue = this.amountShortenerPipe.transform(item.value, 3, undefined, true, true) : formattedValue = formatNumber(item.value, this.locale, '1.2-2');
itemFormatted += `<div class="item">
<div class="indicator-container">${colorSpan(item.color)}</div>
<div style="margin-right: 5px"></div>
<div class="value">${formatNumber(item.value, this.locale, '1.2-2')} <span class="symbol">${item.seriesName}</span></div>
<div class="value">${formattedValue} <span class="symbol">${item.seriesName}</span></div>
</div>`;
}
}
@@ -129,10 +186,13 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
boundaryGap: false,
data: labels.map((value: any) => `${formatDate(value, 'MMM\ny', this.locale)}`),
},
yAxis: {
yAxis: [{
type: 'value',
axisLabel: {
fontSize: 11,
formatter: (val): string => {
return `${this.amountShortenerPipe.transform(Math.round(val), 0, undefined, true)} BTC`;
}
},
splitLine: {
lineStyle: {
@@ -142,10 +202,23 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
}
}
},
{
type: 'value',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: function(val) {
return `$${this.amountShortenerPipe.transform(val, 3, undefined, true, true)}`;
}.bind(this)
},
splitLine: {
show: false,
},
}],
series: [
{
data: pegSeries,
name: 'L-BTC',
yAxisIndex: 0,
color: '#116761',
type: 'line',
stack: 'total',
@@ -163,6 +236,7 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
{
data: reservesSeries,
name: 'BTC',
yAxisIndex: 0,
color: '#EA983B',
type: 'line',
smooth: true,
@@ -172,8 +246,49 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
color: '#EA983B',
},
},
{
data: usdBalance,
name: 'USD',
yAxisIndex: 1,
color: '#4CAF50',
type: 'line',
smooth: true,
showSymbol: false,
lineStyle: {
width: 2,
color: '#3BCC49',
},
},
],
};
}
onLegendSelectChanged(e) {
this.selected = e.selected;
this.adjustedRight = this.selected['USD'] ? +this.right + 40 : +this.right;
this.adjustedLeft = this.selected['L-BTC'] || this.selected['BTC'] ? +this.left : +this.left - 40;
this.pegsChartOptions = {
...this.pegsChartOptions,
grid: {
...this.pegsChartOptions.grid,
right: this.adjustedRight,
left: this.adjustedLeft,
},
legend: {
...this.pegsChartOptions.legend,
selected: this.selected,
},
};
}
onChartInit(ec) {
this.chartInstance = ec;
this.chartInstance.on('legendselectchanged', this.onLegendSelectChanged.bind(this));
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}

View File

@@ -251,7 +251,7 @@ export class PriceService {
}
getPriceByBulk$(timestamps: number[], currency: string): Observable<Price[]> {
if (this.stateService.env.BASE_MODULE !== 'mempool' || !this.stateService.env.HISTORICAL_PRICE) {
if (!this.stateService.env.HISTORICAL_PRICE) {
return of([]);
}