Liquid: add BTC reserves to L-BTC widget and make it dynamic

This commit is contained in:
natsee 2024-01-21 13:19:02 +01:00
parent de2842b62a
commit 752eba767a
No known key found for this signature in database
GPG Key ID: 233CF3150A89BED8
6 changed files with 233 additions and 46 deletions

View File

@ -41,20 +41,24 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
}
ngOnChanges() {
if (!this.data) {
if (!this.data?.liquidPegs) {
return;
}
this.pegsChartOptions = this.createChartOptions(this.data.series, this.data.labels);
if (!this.data.liquidReserves || this.data.liquidReserves?.series.length !== this.data.liquidPegs.series.length) {
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);
}
}
rendered() {
if (!this.data) {
if (!this.data.liquidPegs) {
return;
}
this.isLoading = false;
}
createChartOptions(series: number[], labels: string[]): EChartsOption {
createChartOptions(pegSeries: number[], labels: string[], reservesSeries?: number[],): EChartsOption {
return {
grid: {
height: this.height,
@ -99,17 +103,18 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
type: 'line',
},
formatter: (params: any) => {
const colorSpan = (color: string) => `<span class="indicator" style="background-color: #116761;"></span>`;
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ${color};"></span>`;
let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
params.map((item: any, index: number) => {
for (let index = params.length - 1; index >= 0; index--) {
const item = params[index];
if (index < 26) {
itemFormatted += `<div class="item">
<div class="indicator-container">${colorSpan(item.color)}</div>
<div class="grow"></div>
<div class="value">${formatNumber(item.value, this.locale, '1.2-2')} <span class="symbol">L-BTC</span></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>`;
}
});
};
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
}
},
@ -138,20 +143,34 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
},
series: [
{
data: series,
data: pegSeries,
name: 'L-BTC',
color: '#116761',
type: 'line',
stack: 'total',
smooth: false,
smooth: true,
showSymbol: false,
areaStyle: {
opacity: 0.2,
color: '#116761',
},
lineStyle: {
width: 3,
width: 2,
color: '#116761',
},
},
{
data: reservesSeries,
name: 'BTC',
color: '#EA983B',
type: 'line',
smooth: true,
showSymbol: false,
lineStyle: {
width: 2,
color: '#EA983B',
},
},
],
};
}

View File

@ -33,7 +33,7 @@
</ng-container>
</ng-template>
<ng-template #liquidPegs>
<app-lbtc-pegs-graph [data]="liquidPegsMonth$ | async"></app-lbtc-pegs-graph>
<app-lbtc-pegs-graph [data]="fullHistory$ | async"></app-lbtc-pegs-graph>
</ng-template>
</div>
</div>
@ -270,8 +270,16 @@
<div class="mempool-info-data">
<div class="item">
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
<ng-container *ngIf="(liquidPegsMonth$ | async) as liquidPegsMonth; else loadingTransactions">
<p class="card-text">{{ liquidPegsMonth.series.slice(-1)[0] | number: '1.2-2' }} <span>L-BTC</span></p>
<ng-container *ngIf="(currentPeg$ | async) as currentPeg; else loadingTransactions">
<p i18n-ngbTooltip="liquid.last-elements-audit-block" [ngbTooltip]="'L-BTC supply last updated at Liquid block ' + (currentPeg.lastBlockUpdate)" placement="top" class="card-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span class="liquid-color">L-BTC</span></p>
</ng-container>
</div>
<div class="item">
<a class="title-link" [routerLink]="['/audit' | relativeUrl]">
<h5 class="card-title" i18n="dashboard.btc-reserves">BTC Reserves <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
<ng-container *ngIf="(currentReserves$ | async) as currentReserves; else loadingTransactions">
<p i18n-ngbTooltip="liquid.last-bitcoin-audit-block" [ngbTooltip]="'BTC reserves last updated at Bitcoin block ' + (currentReserves.lastBlockUpdate)" placement="top" class="card-text">{{ +(currentReserves.amount) / 100000000 | number: '1.2-2' }} <span class="bitcoin-color">BTC</span></p>
</ng-container>
</div>
</div>

View File

@ -97,6 +97,12 @@
color: #ffffff66;
font-size: 12px;
}
.liquid-color {
color: #116761;
}
.bitcoin-color {
color: #b86d12;
}
}
.progress {
width: 90%;

View File

@ -1,7 +1,7 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
import { catchError, filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { combineLatest, concat, EMPTY, interval, merge, Observable, of, Subscription } from 'rxjs';
import { catchError, delay, filter, map, mergeMap, scan, share, skip, startWith, switchMap, tap } from 'rxjs/operators';
import { AuditStatus, BlockExtended, CurrentPegs, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service';
@ -47,8 +47,15 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
transactionsWeightPerSecondOptions: any;
isLoadingWebSocket$: Observable<boolean>;
liquidPegsMonth$: Observable<any>;
currentPeg$: Observable<CurrentPegs>;
auditStatus$: Observable<AuditStatus>;
liquidReservesMonth$: Observable<any>;
currentReserves$: Observable<CurrentPegs>;
fullHistory$: Observable<any>;
currencySubscription: Subscription;
currency: string;
private lastPegBlockUpdate: number = 0;
private lastReservesBlockUpdate: number = 0;
constructor(
public stateService: StateService,
@ -82,35 +89,35 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
this.stateService.mempoolInfo$,
this.stateService.vbytesPerSecond$
])
.pipe(
map(([mempoolInfo, vbytesPerSecond]) => {
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
.pipe(
map(([mempoolInfo, vbytesPerSecond]) => {
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
let progressColor = 'bg-success';
if (vbytesPerSecond > 1667) {
progressColor = 'bg-warning';
}
if (vbytesPerSecond > 3000) {
progressColor = 'bg-danger';
}
let progressColor = 'bg-success';
if (vbytesPerSecond > 1667) {
progressColor = 'bg-warning';
}
if (vbytesPerSecond > 3000) {
progressColor = 'bg-danger';
}
const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
let mempoolSizeProgress = 'bg-danger';
if (mempoolSizePercentage <= 50) {
mempoolSizeProgress = 'bg-success';
} else if (mempoolSizePercentage <= 75) {
mempoolSizeProgress = 'bg-warning';
}
const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
let mempoolSizeProgress = 'bg-danger';
if (mempoolSizePercentage <= 50) {
mempoolSizeProgress = 'bg-success';
} else if (mempoolSizePercentage <= 75) {
mempoolSizeProgress = 'bg-warning';
}
return {
memPoolInfo: mempoolInfo,
vBytesPerSecond: vbytesPerSecond,
progressWidth: percent + '%',
progressColor: progressColor,
mempoolSizeProgress: mempoolSizeProgress,
};
})
);
return {
memPoolInfo: mempoolInfo,
vBytesPerSecond: vbytesPerSecond,
progressWidth: percent + '%',
progressColor: progressColor,
mempoolSizeProgress: mempoolSizeProgress,
};
})
);
this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
.pipe(
@ -204,8 +211,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
);
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
this.liquidPegsMonth$ = this.apiService.listLiquidPegsMonth$()
////////// Pegs historical data //////////
this.liquidPegsMonth$ = interval(60 * 60 * 1000)
.pipe(
startWith(0),
switchMap(() => this.apiService.listLiquidPegsMonth$()),
map((pegs) => {
const labels = pegs.map(stats => stats.date);
const series = pegs.map(stats => parseFloat(stats.amount) / 100000000);
@ -217,6 +227,89 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
}),
share(),
);
this.currentPeg$ = concat(
// We fetch the current peg when the page load and
// wait for the API response before listening to websocket blocks
this.apiService.liquidPegs$()
.pipe(
tap((currentPeg) => this.lastPegBlockUpdate = currentPeg.lastBlockUpdate)
),
// Or when we receive a newer block, we wait 2 seconds so that the backend updates and we fetch the current peg
this.stateService.blocks$
.pipe(
delay(2000),
switchMap((_) => this.apiService.liquidPegs$()),
filter((currentPeg) => currentPeg.lastBlockUpdate > this.lastPegBlockUpdate),
tap((currentPeg) => this.lastPegBlockUpdate = currentPeg.lastBlockUpdate)
)
).pipe(
share()
);
////////// BTC Reserves historical data //////////
this.auditStatus$ = concat(
this.apiService.federationAuditSynced$().pipe(share()),
this.stateService.blocks$.pipe(
skip(1),
delay(2000),
switchMap(() => this.apiService.federationAuditSynced$()),
share()
)
);
this.liquidReservesMonth$ = interval(60 * 60 * 1000).pipe(
startWith(0),
mergeMap(() => this.apiService.federationAuditSynced$()),
switchMap((auditStatus) => {
return auditStatus.isAuditSynced ? this.apiService.listLiquidReservesMonth$() : EMPTY;
}),
map(reserves => {
const labels = reserves.map(stats => stats.date);
const series = reserves.map(stats => parseFloat(stats.amount) / 100000000);
return {
series,
labels
};
}),
share()
);
this.currentReserves$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
switchMap(_ =>
this.apiService.liquidReserves$().pipe(
filter((currentReserves) => currentReserves.lastBlockUpdate > this.lastReservesBlockUpdate),
tap((currentReserves) => {
this.lastReservesBlockUpdate = currentReserves.lastBlockUpdate;
})
)
),
share()
);
this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$.pipe(startWith(null)), this.currentReserves$.pipe(startWith(null))])
.pipe(
map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => {
liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000;
if (liquidPegs.series.length === liquidReserves?.series.length) {
liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000;
} else if (liquidPegs.series.length === liquidReserves?.series.length + 1) {
liquidReserves.series.push(parseFloat(currentReserves?.amount) / 100000000);
} else {
liquidReserves = {
series: [],
labels: []
};
}
return {
liquidPegs,
liquidReserves
};
})
);
}
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {

View File

@ -76,6 +76,35 @@ export interface LiquidPegs {
date: string;
}
export interface CurrentPegs {
amount: string;
lastBlockUpdate: number;
hash: string;
}
export interface FederationAddress {
address: string;
balance: string;
}
export interface FederationUtxo {
txid: string;
txindex: number;
bitcoinaddress: string;
amount: number;
blocknumber: number;
blocktime: number;
pegtxid: string;
pegindex: number;
}
export interface AuditStatus {
bitcoinBlocks: number;
bitcoinHeaders: number;
lastBlockAudit: number;
isAuditSynced: boolean;
}
export interface ITranslators { [language: string]: string; }
/**

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface';
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo } from '../interfaces/node-api.interface';
import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs';
import { StateService } from './state.service';
import { IBackendInfo, WebsocketResponse } from '../interfaces/websocket.interface';
@ -178,10 +178,42 @@ export class ApiService {
return this.httpClient.get<RbfTree[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/' + (fullRbf ? 'fullrbf/' : '') + 'replacements/' + (after || ''));
}
liquidPegs$(): Observable<CurrentPegs> {
return this.httpClient.get<CurrentPegs>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs');
}
listLiquidPegsMonth$(): Observable<LiquidPegs[]> {
return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month');
}
liquidReserves$(): Observable<CurrentPegs> {
return this.httpClient.get<CurrentPegs>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves');
}
listLiquidReservesMonth$(): Observable<LiquidPegs[]> {
return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/month');
}
federationAuditSynced$(): Observable<AuditStatus> {
return this.httpClient.get<AuditStatus>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/status');
}
federationAddresses$(): Observable<FederationAddress[]> {
return this.httpClient.get<FederationAddress[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses');
}
federationUtxos$(): Observable<FederationUtxo[]> {
return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos');
}
federationAddressesOneMonthAgo$(): Observable<FederationAddress[]> {
return this.httpClient.get<FederationAddress[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses/previous-month');
}
federationUtxosOneMonthAgo$(): Observable<FederationUtxo[]> {
return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos/previous-month');
}
listFeaturedAssets$(): Observable<any[]> {
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/assets/featured');
}