diff --git a/frontend/src/app/components/lbtc-pegs-graph/lbtc-pegs-graph.component.ts b/frontend/src/app/components/lbtc-pegs-graph/lbtc-pegs-graph.component.ts
index c4e8cbf91..f8d0843b1 100644
--- a/frontend/src/app/components/lbtc-pegs-graph/lbtc-pegs-graph.component.ts
+++ b/frontend/src/app/components/lbtc-pegs-graph/lbtc-pegs-graph.component.ts
@@ -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) => ``;
+ const colorSpan = (color: string) => ``;
let itemFormatted = '
' + params[0].axisValue + '
';
- params.map((item: any, index: number) => {
+ for (let index = params.length - 1; index >= 0; index--) {
+ const item = params[index];
if (index < 26) {
itemFormatted += `
${colorSpan(item.color)}
-
-
${formatNumber(item.value, this.locale, '1.2-2')} L-BTC
+
+
${formatNumber(item.value, this.locale, '1.2-2')} ${item.seriesName}
`;
}
- });
+ };
return `${itemFormatted}
`;
}
},
@@ -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',
+ },
+ },
],
};
}
diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html
index 12ce14512..9603c8d93 100644
--- a/frontend/src/app/dashboard/dashboard.component.html
+++ b/frontend/src/app/dashboard/dashboard.component.html
@@ -33,7 +33,7 @@
-
+
@@ -270,8 +270,16 @@
L-BTC in circulation
-
- {{ liquidPegsMonth.series.slice(-1)[0] | number: '1.2-2' }} L-BTC
+
+ {{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} L-BTC
+
+
+
+
+ BTC Reserves
+
+
+ {{ +(currentReserves.amount) / 100000000 | number: '1.2-2' }} BTC
diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss
index 884ba1027..f10c4957f 100644
--- a/frontend/src/app/dashboard/dashboard.component.scss
+++ b/frontend/src/app/dashboard/dashboard.component.scss
@@ -97,6 +97,12 @@
color: #ffffff66;
font-size: 12px;
}
+ .liquid-color {
+ color: #116761;
+ }
+ .bitcoin-color {
+ color: #b86d12;
+ }
}
.progress {
width: 90%;
diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts
index 8a34bf768..2f97b23a0 100644
--- a/frontend/src/app/dashboard/dashboard.component.ts
+++ b/frontend/src/app/dashboard/dashboard.component.ts
@@ -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;
liquidPegsMonth$: Observable;
+ currentPeg$: Observable;
+ auditStatus$: Observable;
+ liquidReservesMonth$: Observable;
+ currentReserves$: Observable;
+ fullHistory$: Observable;
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) => {
diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts
index 9d936722d..5989be7fa 100644
--- a/frontend/src/app/interfaces/node-api.interface.ts
+++ b/frontend/src/app/interfaces/node-api.interface.ts
@@ -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; }
/**
diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts
index 854d15c2a..42dd6a26e 100644
--- a/frontend/src/app/services/api.service.ts
+++ b/frontend/src/app/services/api.service.ts
@@ -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(this.apiBaseUrl + this.apiBasePath + '/api/v1/' + (fullRbf ? 'fullrbf/' : '') + 'replacements/' + (after || ''));
}
+ liquidPegs$(): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs');
+ }
+
listLiquidPegsMonth$(): Observable {
return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month');
}
+ liquidReserves$(): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves');
+ }
+
+ listLiquidReservesMonth$(): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/month');
+ }
+
+ federationAuditSynced$(): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/status');
+ }
+
+ federationAddresses$(): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses');
+ }
+
+ federationUtxos$(): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos');
+ }
+
+ federationAddressesOneMonthAgo$(): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses/previous-month');
+ }
+
+ federationUtxosOneMonthAgo$(): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos/previous-month');
+ }
+
listFeaturedAssets$(): Observable {
return this.httpClient.get(this.apiBaseUrl + '/api/v1/assets/featured');
}