Migrate audit dashboard to main Liquid page

This commit is contained in:
natsoni 2024-02-13 00:39:34 +01:00
parent 99aa6c9ed3
commit 69747a0028
No known key found for this signature in database
GPG Key ID: C65917583181743B
18 changed files with 279 additions and 884 deletions

View File

@ -18,7 +18,7 @@ import { EChartsOption } from '../../graphs/echarts';
}) })
export class LbtcPegsGraphComponent implements OnInit, OnChanges { export class LbtcPegsGraphComponent implements OnInit, OnChanges {
@Input() data: any; @Input() data: any;
@Input() height: number | string = '320'; @Input() height: number | string = '270';
pegsChartOptions: EChartsOption; pegsChartOptions: EChartsOption;
right: number | string = '10'; right: number | string = '10';

View File

@ -78,9 +78,6 @@
<li class="nav-item" routerLinkActive="active" id="btn-assets"> <li class="nav-item" routerLinkActive="active" id="btn-assets">
<a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a> <a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a>
</li> </li>
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-audit">
<a class="nav-link" [routerLink]="['/audit']" (click)="collapse()"><fa-icon [icon]="['fas', 'scale-balanced']" [fixedWidth]="true" i18n-title="master-page.btc-reserves-audit" title="BTC Reserves Audit"></fa-icon></a>
</li>
<li class="nav-item mr-2" routerLinkActive="active" id="btn-docs"> <li class="nav-item mr-2" routerLinkActive="active" id="btn-docs">
<a class="nav-link" [routerLink]="['/docs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'book']" [fixedWidth]="true" i18n-title="master-page.docs" title="Docs"></fa-icon></a> <a class="nav-link" [routerLink]="['/docs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'book']" [fixedWidth]="true" i18n-title="master-page.docs" title="Docs"></fa-icon></a>
</li> </li>

View File

@ -23,21 +23,11 @@ li.nav-item {
margin: auto 10px; margin: auto 10px;
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
@media (max-width: 992px) {
margin: auto 7px;
padding-left: 8px;
padding-right: 8px;
}
@media (max-width: 429px) { @media (max-width: 429px) {
margin: auto 5px; margin: auto 5px;
padding-left: 6px; padding-left: 6px;
padding-right: 6px; padding-right: 6px;
} }
@media (max-width: 369px) {
margin: auto 3px;
padding-left: 4px;
padding-right: 4px;
}
} }
@media (min-width: 992px) { @media (min-width: 992px) {

View File

@ -20,6 +20,9 @@ export class FederationAddressesStatsComponent implements OnInit {
this.federationUtxosNumber$ ?? of(undefined) this.federationUtxosNumber$ ?? of(undefined)
]).pipe( ]).pipe(
map(([address_count, utxo_count]) => { map(([address_count, utxo_count]) => {
if (address_count === undefined || utxo_count === undefined) {
return undefined;
}
return { address_count, utxo_count} return { address_count, utxo_count}
}) })
) )

View File

@ -15,7 +15,7 @@ import { SeoService } from '../../../services/seo.service';
}) })
export class RecentPegsListComponent implements OnInit { export class RecentPegsListComponent implements OnInit {
@Input() widget: boolean = false; @Input() widget: boolean = false;
@Input() recentPegsList$: Observable<RecentPeg[]> = of([]); @Input() recentPegsList$: Observable<RecentPeg[]>;
env: Env; env: Env;
isLoading = true; isLoading = true;

View File

@ -1,98 +0,0 @@
<div class="container-xl dashboard-container" *ngIf="(auditStatus$ | async)?.isAuditSynced; else auditInProgress">
<div class="row row-cols-1 row-cols-md-2">
<div class="col">
<div class="card">
<div class="card-body">
<app-reserves-supply-stats [currentPeg$]="currentPeg$" [currentReserves$]="currentReserves$"></app-reserves-supply-stats>
<app-reserves-ratio [currentPeg]="currentPeg$ | async" [currentReserves]="currentReserves$ | async"></app-reserves-ratio>
</div>
</div>
</div>
<div class="col" style="margin-bottom: 1.47rem">
<div class="card">
<div class="card-title">
<app-reserves-ratio-stats [fullHistory$]="fullHistory$"></app-reserves-ratio-stats>
</div>
<div class="card-body pl-0" style="padding-top: 10px;">
<app-reserves-ratio-graph [data]="fullHistory$ | async" [height]="pegRatioGraphHeight"></app-reserves-ratio-graph>
</div>
</div>
</div>
<div class="col">
<div class="card smaller">
<div class="card-body">
<app-recent-pegs-stats [pegsVolume$]="pegsVolume$"></app-recent-pegs-stats>
<app-recent-pegs-list [recentPegsList$]="recentPegsList$" [widget]="true"></app-recent-pegs-list>
</div>
</div>
</div>
<div class="col" style="margin-bottom: 1.47rem">
<div class="card smaller">
<div class="card-body">
<app-federation-addresses-stats [federationAddressesNumber$]="federationAddressesNumber$" [federationUtxosNumber$]="federationUtxosNumber$"></app-federation-addresses-stats>
<app-federation-addresses-list [federationAddresses$]="federationAddresses$" [widget]="true"></app-federation-addresses-list>
</div>
</div>
</div>
</div>
</div>
<ng-template #loadingSkeleton>
<div class="container-xl dashboard-container">
<div class="row row-cols-1 row-cols-md-2">
<div class="col">
<div class="card">
<div class="card-body">
<app-reserves-supply-stats></app-reserves-supply-stats>
<app-reserves-ratio></app-reserves-ratio>
</div>
</div>
</div>
<div class="col" style="margin-bottom: 1.47rem">
<div class="card">
<div class="card-title">
<app-reserves-ratio-stats></app-reserves-ratio-stats>
</div>
<div class="card-body pl-0" style="padding-top: 10px;">
<app-reserves-ratio-graph></app-reserves-ratio-graph>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<app-recent-pegs-stats></app-recent-pegs-stats>
<app-recent-pegs-list [widget]="true"></app-recent-pegs-list>
</div>
</div>
</div>
<div class="col" style="margin-bottom: 1.47rem">
<div class="card">
<div class="card-body">
<app-federation-addresses-stats></app-federation-addresses-stats>
<app-federation-addresses-list [widget]="true"></app-federation-addresses-list>
</div>
</div>
</div>
</div>
</div>
</ng-template>
<ng-template #auditInProgress>
<ng-container *ngIf="(auditStatus$ | async) as auditStatus; else loadingSkeleton">
<div class="in-progress-message" *ngIf="auditStatus.lastBlockAudit && auditStatus.bitcoinHeaders; else loadingSkeleton">
<span i18n="liquid.audit-in-progress">Audit in progress: Bitcoin block height #{{ auditStatus.lastBlockAudit }} / #{{ auditStatus.bitcoinHeaders }}</span>
</div>
</ng-container>
</ng-template>

View File

@ -1,135 +0,0 @@
.dashboard-container {
text-align: center;
margin-top: 0.5rem;
.col {
margin-bottom: 1.5rem;
}
}
.card {
background-color: #1d1f31;
height: 418px;
@media (min-width: 992px) {
height: 512px;
}
&.smaller {
height: 408px;
}
}
.card-title {
padding-top: 20px;
}
.card-body.pool-ranking {
padding: 1.25rem 0.25rem 0.75rem 0.25rem;
}
.card-text {
font-size: 22px;
}
#blockchain-container {
position: relative;
overflow-x: scroll;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
#blockchain-container::-webkit-scrollbar {
display: none;
}
.fade-border {
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
}
.in-progress-message {
position: relative;
color: #ffffff91;
margin-top: 20px;
text-align: center;
padding-bottom: 3px;
font-weight: 500;
}
.more-padding {
padding: 24px 20px !important;
}
.card-wrapper {
.card {
height: auto !important;
}
.card-body {
display: flex;
flex: inherit;
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 22px 20px;
}
}
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}
.card-text {
font-size: 22px;
}
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
display: block;
margin-bottom: 10px;
text-decoration: none;
color: inherit;
}
.lastest-blocks-table {
width: 100%;
text-align: left;
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.8rem !important;
}
.table-cell-height {
width: 25%;
}
.table-cell-fee {
width: 25%;
text-align: right;
}
.table-cell-pool {
text-align: left;
width: 30%;
@media (max-width: 875px) {
display: none;
}
.pool-name {
margin-left: 1em;
}
}
.table-cell-acceleration-count {
text-align: right;
width: 20%;
}
}
.mempool-block-wrapper {
max-height: 380px;
max-width: 380px;
margin: auto;
}

View File

@ -1,211 +0,0 @@
import { ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core';
import { SeoService } from '../../../services/seo.service';
import { WebsocketService } from '../../../services/websocket.service';
import { StateService } from '../../../services/state.service';
import { Observable, Subject, combineLatest, delayWhen, filter, interval, map, of, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime, timer } from 'rxjs';
import { ApiService } from '../../../services/api.service';
import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo, PegsVolume, RecentPeg } from '../../../interfaces/node-api.interface';
@Component({
selector: 'app-reserves-audit-dashboard',
templateUrl: './reserves-audit-dashboard.component.html',
styleUrls: ['./reserves-audit-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReservesAuditDashboardComponent implements OnInit {
auditStatus$: Observable<AuditStatus>;
auditUpdated$: Observable<boolean>;
currentPeg$: Observable<CurrentPegs>;
currentReserves$: Observable<CurrentPegs>;
recentPegsList$: Observable<RecentPeg[]>;
pegsVolume$: Observable<PegsVolume[]>;
federationAddresses$: Observable<FederationAddress[]>;
federationAddressesNumber$: Observable<number>;
federationUtxosNumber$: Observable<number>;
liquidPegsMonth$: Observable<any>;
liquidReservesMonth$: Observable<any>;
pegRatioGraphHeight: number = 320;
fullHistory$: Observable<any>;
isLoad: boolean = true;
private lastPegBlockUpdate: number = 0;
private lastPegAmount: string = '';
private lastReservesBlockUpdate: number = 0;
private destroy$ = new Subject();
constructor(
private seoService: SeoService,
private websocketService: WebsocketService,
private apiService: ApiService,
private stateService: StateService,
) {
this.seoService.setTitle($localize`:@@liquid.reserves-audit:Reserves Audit Dashboard`);
}
ngOnInit(): void {
this.websocketService.want(['blocks', 'mempool-blocks']);
this.auditStatus$ = this.stateService.blocks$.pipe(
takeUntil(this.destroy$),
throttleTime(40000),
delayWhen(_ => this.isLoad ? timer(0) : timer(2000)),
tap(() => this.isLoad = false),
switchMap(() => this.apiService.federationAuditSynced$()),
shareReplay(1),
);
this.currentPeg$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
switchMap(_ =>
this.apiService.liquidPegs$().pipe(
filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate),
tap((currentPegs) => {
this.lastPegBlockUpdate = currentPegs.lastBlockUpdate;
})
)
),
share()
);
this.auditUpdated$ = combineLatest([
this.auditStatus$,
this.currentPeg$
]).pipe(
filter(([auditStatus, _]) => auditStatus.isAuditSynced === true),
map(([auditStatus, currentPeg]) => ({
lastBlockAudit: auditStatus.lastBlockAudit,
currentPegAmount: currentPeg.amount
})),
switchMap(({ lastBlockAudit, currentPegAmount }) => {
const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate;
const amountCheck = currentPegAmount !== this.lastPegAmount;
this.lastPegAmount = currentPegAmount;
return of(blockAuditCheck || amountCheck);
}),
share()
);
this.currentReserves$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ =>
this.apiService.liquidReserves$().pipe(
filter((currentReserves) => currentReserves.lastBlockUpdate >= this.lastReservesBlockUpdate),
tap((currentReserves) => {
this.lastReservesBlockUpdate = currentReserves.lastBlockUpdate;
})
)
),
share()
);
this.recentPegsList$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.recentPegsList$()),
share()
);
this.pegsVolume$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.pegsVolume$()),
share()
);
this.federationAddresses$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.federationAddresses$()),
share()
);
this.federationAddressesNumber$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.federationAddressesNumber$()),
map(count => count.address_count),
share()
);
this.federationUtxosNumber$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.federationUtxosNumber$()),
map(count => count.utxo_count),
share()
);
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);
series.reduce((prev, curr, i) => series[i] = prev + curr, 0);
return {
series,
labels
};
}),
share(),
);
this.liquidReservesMonth$ = interval(60 * 60 * 1000).pipe(
startWith(0),
switchMap(() => this.apiService.listLiquidReservesMonth$()),
map(reserves => {
const labels = reserves.map(stats => stats.date);
const series = reserves.map(stats => parseFloat(stats.amount) / 100000000);
return {
series,
labels
};
}),
share()
);
this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$, this.currentReserves$])
.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);
liquidReserves.labels.push(liquidPegs.labels[liquidPegs.labels.length - 1]);
} else {
liquidReserves = {
series: [],
labels: []
};
}
return {
liquidPegs,
liquidReserves
};
}),
share()
);
}
ngOnDestroy(): void {
this.destroy$.next(1);
this.destroy$.complete();
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (window.innerWidth >= 992) {
this.pegRatioGraphHeight = 320;
} else if (window.innerWidth >= 768) {
this.pegRatioGraphHeight = 230;
} else {
this.pegRatioGraphHeight = 220;
}
}
}

View File

@ -1,4 +0,0 @@
<div class="echarts" echarts [initOpts]="ratioHistoryChartInitOptions" [options]="ratioHistoryChartOptions" (chartRendered)="rendered()"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>

View File

@ -1,6 +0,0 @@
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 16px);
z-index: 100;
}

View File

@ -1,195 +0,0 @@
import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
import { formatDate, formatNumber } from '@angular/common';
import { EChartsOption } from '../../../graphs/echarts';
@Component({
selector: 'app-reserves-ratio-graph',
templateUrl: './reserves-ratio-graph.component.html',
styleUrls: ['./reserves-ratio-graph.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReservesRatioGraphComponent implements OnInit, OnChanges {
@Input() data: any;
@Input() height: number | string = '320';
ratioHistoryChartOptions: EChartsOption;
ratioSeries: number[] = [];
right: number | string = '10';
top: number | string = '20';
left: number | string = '50';
template: ('widget' | 'advanced') = 'widget';
isLoading = true;
ratioHistoryChartInitOptions = {
renderer: 'svg'
};
constructor(
@Inject(LOCALE_ID) private locale: string,
) { }
ngOnInit() {
this.isLoading = true;
}
ngOnChanges() {
if (!this.data) {
return;
}
// Compute the ratio series: the ratio of the reserves to the pegs
this.ratioSeries = this.data.liquidReserves.series.map((value: number, index: number) => value / this.data.liquidPegs.series[index]);
// Truncate the ratio series and labels series to last 3 years
this.ratioSeries = this.ratioSeries.slice(Math.max(this.ratioSeries.length - 36, 0));
this.data.liquidPegs.labels = this.data.liquidPegs.labels.slice(Math.max(this.data.liquidPegs.labels.length - 36, 0));
// Cut the values that are too high or too low
this.ratioSeries = this.ratioSeries.map((value: number) => Math.min(Math.max(value, 0.995), 1.005));
this.ratioHistoryChartOptions = this.createChartOptions(this.ratioSeries, this.data.liquidPegs.labels);
}
rendered() {
if (!this.data) {
return;
}
this.isLoading = false;
}
createChartOptions(ratioSeries: number[], labels: string[]): EChartsOption {
return {
grid: {
height: this.height,
right: this.right,
top: this.top,
left: this.left,
},
animation: false,
dataZoom: [{
type: 'inside',
realtime: true,
zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
maxSpan: 100,
minSpan: 10,
}, {
show: (this.template === 'advanced') ? true : false,
type: 'slider',
brushSelect: false,
realtime: true,
selectedDataBackground: {
lineStyle: {
color: '#fff',
opacity: 0.45,
},
areaStyle: {
opacity: 0,
}
}
}],
tooltip: {
trigger: 'axis',
position: (pos, params, el, elRect, size) => {
const obj = { top: -20 };
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
return obj;
},
extraCssText: `width: ${(this.template === 'widget') ? '125px' : '135px'};
background: transparent;
border: none;
box-shadow: none;`,
axisPointer: {
type: 'line',
},
formatter: (params: any) => {
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ${color};"></span>`;
let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
const item = params[0];
const formattedValue = formatNumber(item.value, this.locale, '1.5-5');
const symbol = (item.value === 1.005) ? '≥ ' : (item.value === 0.995) ? '≤ ' : '';
itemFormatted += `<div class="item">
<div class="indicator-container">${colorSpan(item.color)}</div>
<div style="margin-right: 5px"></div>
<div class="value">${symbol}${formattedValue}</div>
</div>`;
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
}
},
xAxis: {
type: 'category',
axisLabel: {
align: 'center',
fontSize: 11,
lineHeight: 12
},
boundaryGap: false,
data: labels.map((value: any) => `${formatDate(value, 'MMM\ny', this.locale)}`),
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 11,
},
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
},
min: 0.995,
max: 1.005,
},
series: [
{
data: ratioSeries,
name: '',
type: 'line',
smooth: true,
showSymbol: false,
lineStyle: {
width: 3,
},
markLine: {
silent: true,
symbol: 'none',
lineStyle: {
color: '#fff',
opacity: 1,
width: 1,
},
data: [{
yAxis: 1,
label: {
show: false,
color: '#ffffff',
}
}],
},
},
],
visualMap: {
show: false,
top: 50,
right: 10,
pieces: [{
gt: 0,
lte: 0.999,
color: '#D81B60'
},
{
gt: 0.999,
lte: 1.001,
color: '#FDD835'
},
{
gt: 1.001,
lte: 2,
color: '#7CB342'
}
],
outOfRange: {
color: '#999'
}
},
};
}
}

View File

@ -95,7 +95,7 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
}, },
title: { title: {
show: true, show: true,
offsetCenter: [0, '-117.5%'], offsetCenter: [0, '-127%'],
fontSize: 18, fontSize: 18,
color: '#4a68b9', color: '#4a68b9',
fontFamily: 'inherit', fontFamily: 'inherit',

View File

@ -1,44 +1,29 @@
<div *ngIf="(currentPeg$ | async) as currentPeg; else loadingData"> <div class="fee-estimation-container">
<div *ngIf="(currentReserves$ | async) as currentReserves; else loadingData"> <div class="item">
<div class="fee-estimation-container"> <h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
<div class="item"> <div *ngIf="(currentPeg$ | async) as currentPeg; else loadingData" class="card-text">
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5> <div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div>
<div class="card-text"> <span class="fiat">
<div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div> <span><ng-container i18n="shared.as-of-block">As of block</ng-container>&nbsp;<a [routerLink]="['/block', currentPeg.hash]">{{ currentPeg.lastBlockUpdate }}</a></span>
<span class="fiat"> </span>
<span><ng-container i18n="shared.as-of-block">As of block</ng-container>&nbsp;<a [routerLink]="['/block', currentPeg.hash]">{{ currentPeg.lastBlockUpdate }}</a></span> </div>
</span> </div>
</div> <div class="item">
</div> <h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
<div class="item"> <div *ngIf="(currentReserves$ | async) as currentReserves; else loadingData" class="card-text">
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5> <div class="fee-text">{{ (+currentReserves.amount) / 100000000 | number: '1.2-2' }} <span style="color: #b86d12;">BTC</span></div>
<div class="card-text"> <span class="fiat">
<div class="fee-text">{{ (+currentReserves.amount) / 100000000 | number: '1.2-2' }} <span style="color: #b86d12;">BTC</span></div> <span><ng-container i18n="shared.as-of-block">As of block</ng-container>&nbsp;<a href="{{ env.MEMPOOL_WEBSITE_URL + '/block/' + currentReserves.hash }}" target="_blank" style="color:#b86d12">{{ currentReserves.lastBlockUpdate }}</a></span>
<span class="fiat"> </span>
<span><ng-container i18n="shared.as-of-block">As of block</ng-container>&nbsp;<a href="{{ env.MEMPOOL_WEBSITE_URL + '/block/' + currentReserves.hash }}" target="_blank" style="color:#b86d12">{{ currentReserves.lastBlockUpdate }}</a></span>
</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<ng-template #loadingData> <ng-template #loadingData>
<div class="fee-estimation-container loading-container"> <div class="card-text">
<div class="item"> <div class="skeleton-loader"></div>
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5> <div class="skeleton-loader"></div>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div> </div>
</ng-template> </ng-template>

View File

@ -1,7 +1,7 @@
<div class="container-xl dashboard-container"> <div class="container-xl dashboard-container" *ngIf="(network$ | async) !== 'liquid'">
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData"> <div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
<ng-container *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'"> <ng-container *ngIf="(network$ | async) !== 'liquidtestnet'">
<div class="col card-wrapper"> <div class="col card-wrapper">
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div> <div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
<div class="card"> <div class="card">
@ -17,35 +17,26 @@
<div class="col"> <div class="col">
<div class="card graph-card"> <div class="card graph-card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2"> <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<ng-template [ngIf]="(network$ | async) !== 'liquid'" [ngIfElse]="liquidPegs"> <a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]"> <h5 class="card-title d-inline"><span i18n="dashboard.mempool-goggles">Mempool Goggles</span>: {{ goggleCycle[goggleIndex].name }}</h5>
<h5 class="card-title d-inline"><span i18n="dashboard.mempool-goggles">Mempool Goggles</span>: {{ goggleCycle[goggleIndex].name }}</h5> <span>&nbsp;</span>
<span>&nbsp;</span> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon> </a>
</a> <div class="quick-filter">
<div class="quick-filter"> <div class="btn-group btn-group-toggle">
<div class="btn-group btn-group-toggle"> <label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex" *ngFor="let filter of goggleCycle">
<label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex" *ngFor="let filter of goggleCycle"> <input type="radio" [value]="'3m'" fragment="3m" (click)="goggleIndex = filter.index" [attr.data-cy]="'3m'"> {{ filter.name }}
<input type="radio" [value]="'3m'" fragment="3m" (click)="goggleIndex = filter.index" [attr.data-cy]="'3m'"> {{ filter.name }} </label>
</label>
</div>
</div> </div>
<div class="mempool-block-wrapper"> </div>
<app-mempool-block-overview <div class="mempool-block-wrapper">
[index]="0" <app-mempool-block-overview
[resolution]="goggleResolution" [index]="0"
[filterFlags]="goggleCycle[goggleIndex].flag" [resolution]="goggleResolution"
filterMode="or" [filterFlags]="goggleCycle[goggleIndex].flag"
></app-mempool-block-overview> filterMode="or"
</div> ></app-mempool-block-overview>
</ng-template> </div>
<ng-template #liquidPegs>
<div style="padding-left: 1.25rem;">
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
<hr>
</div>
<app-lbtc-pegs-graph [data]="fullHistory$ | async" [height]="lbtcPegGraphHeight"></app-lbtc-pegs-graph>
</ng-template>
</div> </div>
</div> </div>
</div> </div>
@ -53,7 +44,6 @@
<div class="card graph-card"> <div class="card graph-card">
<div class="card-body"> <div class="card-body">
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container> <ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
<ng-container *ngIf="stateService.network !== 'liquid'">
<h5 class="card-title mt-3" i18n="dashboard.incoming-transactions">Incoming Transactions</h5> <h5 class="card-title mt-3" i18n="dashboard.incoming-transactions">Incoming Transactions</h5>
<div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats"> <div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
<app-incoming-transactions-graph <app-incoming-transactions-graph
@ -64,30 +54,11 @@
[windowPreferenceOverride]="'2h'" [windowPreferenceOverride]="'2h'"
></app-incoming-transactions-graph> ></app-incoming-transactions-graph>
</div> </div>
</ng-container>
<div class="mempool-graph" *ngIf="stateService.network === 'liquid'">
<hr>
<table class="table table-borderless table-striped" *ngIf="(featuredAssets$ | async) as featuredAssets else loadingAssetsTable">
<tbody>
<tr *ngFor="let group of featuredAssets">
<td class="asset-icon">
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">
<img class="assetIcon" [src]="'/api/v1/asset/' + group.asset + '/icon'">
</a>
</td>
<td class="asset-title">
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">{{ group.name }}</a>
</td>
<td class="circulating-amount"><app-asset-circulation [assetId]="group.asset"></app-asset-circulation></td>
</tr>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="col" style="max-height: 410px"> <div class="col" style="max-height: 410px">
<div class="card" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else latestBlocks"> <div class="card" *ngIf="(network$ | async) !== 'liquidtestnet'; else latestBlocks">
<div class="card-body"> <div class="card-body">
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]"> <a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.recent-rbf-replacements">Recent Replacements</h5> <h5 class="card-title d-inline" i18n="dashboard.recent-rbf-replacements">Recent Replacements</h5>
@ -171,7 +142,7 @@
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate> <app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
</a> </a>
</td> </td>
<td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td> <td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquidtestnet'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td>
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td> <td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
<td class="table-cell-fees"><app-fee-rate [fee]="transaction.fee" [weight]="transaction.vsize * 4"></app-fee-rate></td> <td class="table-cell-fees"><app-fee-rate [fee]="transaction.fee" [weight]="transaction.vsize * 4"></app-fee-rate></td>
</tr> </tr>
@ -184,26 +155,52 @@
</div> </div>
</div> </div>
<ng-template #loadingAssetsTable> <ng-container *ngIf="(network$ | async) === 'liquid'">
<table class="table table-borderless table-striped asset-table"> <div class="container-xl dashboard-container" *ngIf="(auditStatus$ | async)?.isAuditSynced; else auditInProgress">
<tbody>
<tr *ngFor="let i of getArrayFromNumber(this.nbFeaturedAssets)"> <div class="row row-cols-1 row-cols-md-2">
<td class="asset-icon">
<div class="skeleton-loader skeleton-loader-transactions"></div> <div class="col">
</td> <div class="card-liquid card">
<td class="asset-title"> <div class="card-body">
<div class="skeleton-loader skeleton-loader-transactions"></div> <app-reserves-supply-stats [currentPeg$]="currentPeg$" [currentReserves$]="currentReserves$"></app-reserves-supply-stats>
</td> <app-reserves-ratio [currentPeg]="currentPeg$ | async" [currentReserves]="currentReserves$ | async"></app-reserves-ratio>
<td class="asset-title d-none d-md-table-cell"> </div>
<div class="skeleton-loader skeleton-loader-transactions"></div> </div>
</td> </div>
<td class="asset-title">
<div class="skeleton-loader skeleton-loader-transactions"></div> <div class="col" style="margin-bottom: 1.47rem">
</td> <div class="card-liquid card">
</tr> <div class="card-title card-title-liquid">
</tbody> <app-reserves-ratio-stats [fullHistory$]="fullHistory$"></app-reserves-ratio-stats>
</table> </div>
</ng-template> <h5 *ngIf="fullHistory$ | async" class="card-title peg-historical-data">Peg Historical Data</h5>
<div class="card-body pl-0" style="padding-top: 10px;">
<app-lbtc-pegs-graph [data]="fullHistory$ | async" [height]="lbtcPegGraphHeight"></app-lbtc-pegs-graph>
</div>
</div>
</div>
<div class="col">
<div class="card card-liquid smaller">
<div class="card-body">
<app-recent-pegs-stats [pegsVolume$]="pegsVolume$"></app-recent-pegs-stats>
<app-recent-pegs-list [recentPegsList$]="recentPegsList$" [widget]="true"></app-recent-pegs-list>
</div>
</div>
</div>
<div class="col" style="margin-bottom: 1.47rem">
<div class="card-liquid card smaller">
<div class="card-body">
<app-federation-addresses-stats [federationAddressesNumber$]="federationAddressesNumber$" [federationUtxosNumber$]="federationUtxosNumber$"></app-federation-addresses-stats>
<app-federation-addresses-list [federationAddresses$]="federationAddresses$" [widget]="true"></app-federation-addresses-list>
</div>
</div>
</div>
</div>
</div>
</ng-container>
<ng-template #replacementsSkeleton> <ng-template #replacementsSkeleton>
<tbody> <tbody>
@ -283,21 +280,56 @@
</div> </div>
</ng-template> </ng-template>
<ng-template #lbtcPegs let-mempoolInfoData> <ng-template #loadingSkeletonLiquid>
<div class="mempool-info-data lbtc-pegs-stats"> <div class="container-xl dashboard-container">
<div class="item">
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5> <div class="row row-cols-1 row-cols-md-2">
<ng-container *ngIf="(currentPeg$ | async) as currentPeg; else loadingTransactions">
<div 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>L-BTC</span></div> <div class="col">
</ng-container> <div class="card-liquid card">
</div> <div class="card-body">
<div class="item"> <app-reserves-supply-stats></app-reserves-supply-stats>
<a class="title-link" [routerLink]="['/audit' | relativeUrl]"> <app-reserves-ratio></app-reserves-ratio>
<h5 class="card-title"><ng-container i18n="dashboard.btc-reserves">BTC Reserves</ng-container>&nbsp;<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5> </div>
</a> </div>
<ng-container *ngIf="(currentReserves$ | async) as currentReserves; else loadingTransactions"> </div>
<div 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></div>
</ng-container> <div class="col" style="margin-bottom: 1.47rem">
<div class="card-liquid card">
<div class="card-title card-title-liquid">
<app-reserves-ratio-stats></app-reserves-ratio-stats>
</div>
<div class="card-body pl-0" style="padding-top: 10px;">
<app-lbtc-pegs-graph></app-lbtc-pegs-graph>
</div>
</div>
</div>
<div class="col">
<div class="card card-liquid smaller">
<div class="card-body">
<app-recent-pegs-stats></app-recent-pegs-stats>
<app-recent-pegs-list [widget]="true"></app-recent-pegs-list>
</div>
</div>
</div>
<div class="col" style="margin-bottom: 1.47rem">
<div class="card-liquid card smaller">
<div class="card-body">
<app-federation-addresses-stats></app-federation-addresses-stats>
<app-federation-addresses-list [widget]="true"></app-federation-addresses-list>
</div>
</div>
</div>
</div> </div>
</div> </div>
</ng-template> </ng-template>
<ng-template #auditInProgress>
<ng-container *ngIf="(auditStatus$ | async) as auditStatus; else loadingSkeletonLiquid">
<div class="in-progress-message" *ngIf="auditStatus.lastBlockAudit && auditStatus.bitcoinHeaders; else loadingSkeletonLiquid">
<span i18n="liquid.audit-in-progress">Audit in progress: Bitcoin block height #{{ auditStatus.lastBlockAudit }} / #{{ auditStatus.bitcoinHeaders }}</span>
</div>
</ng-container>
</ng-template>

View File

@ -413,3 +413,39 @@
margin-top: 5px; margin-top: 5px;
margin-bottom: 6px; margin-bottom: 6px;
} }
.card-liquid {
background-color: #1d1f31;
height: 418px;
@media (min-width: 992px) {
height: 512px;
}
&.smaller {
height: 408px;
}
}
.card-title-liquid {
padding-top: 20px;
}
.peg-historical-data {
font-size: 18px;
@media (min-width: 768px) {
padding-top: 22px;
}
@media (min-width: 992px) {
padding-top: 27px;
}
}
.in-progress-message {
position: relative;
color: #ffffff91;
margin-top: 20px;
text-align: center;
padding-bottom: 3px;
font-weight: 500;
}

View File

@ -1,7 +1,7 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, OnDestroy, OnInit } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, OnDestroy, OnInit } from '@angular/core';
import { combineLatest, EMPTY, fromEvent, merge, Observable, of, Subject, Subscription, timer } from 'rxjs'; import { combineLatest, EMPTY, fromEvent, interval, merge, Observable, of, Subject, Subscription, timer } from 'rxjs';
import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
import { AuditStatus, BlockExtended, CurrentPegs, OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, OptimizedMempoolStats, PegsVolume, RecentPeg } from '../interfaces/node-api.interface';
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface'; import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
import { ApiService } from '../services/api.service'; import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service'; import { StateService } from '../services/state.service';
@ -32,8 +32,6 @@ interface MempoolStatsData {
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
featuredAssets$: Observable<any>;
nbFeaturedAssets = 6;
network$: Observable<string>; network$: Observable<string>;
mempoolBlocksData$: Observable<MempoolBlocksData>; mempoolBlocksData$: Observable<MempoolBlocksData>;
mempoolInfoData$: Observable<MempoolInfoData>; mempoolInfoData$: Observable<MempoolInfoData>;
@ -53,13 +51,18 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
auditUpdated$: Observable<boolean>; auditUpdated$: Observable<boolean>;
liquidReservesMonth$: Observable<any>; liquidReservesMonth$: Observable<any>;
currentReserves$: Observable<CurrentPegs>; currentReserves$: Observable<CurrentPegs>;
recentPegsList$: Observable<RecentPeg[]>;
pegsVolume$: Observable<PegsVolume[]>;
federationAddresses$: Observable<FederationAddress[]>;
federationAddressesNumber$: Observable<number>;
federationUtxosNumber$: Observable<number>;
fullHistory$: Observable<any>; fullHistory$: Observable<any>;
isLoad: boolean = true; isLoad: boolean = true;
mempoolInfoSubscription: Subscription; mempoolInfoSubscription: Subscription;
currencySubscription: Subscription; currencySubscription: Subscription;
currency: string; currency: string;
incomingGraphHeight: number = 300; incomingGraphHeight: number = 300;
lbtcPegGraphHeight: number = 320; lbtcPegGraphHeight: number = 250;
private lastPegBlockUpdate: number = 0; private lastPegBlockUpdate: number = 0;
private lastPegAmount: string = ''; private lastPegAmount: string = '';
private lastReservesBlockUpdate: number = 0; private lastReservesBlockUpdate: number = 0;
@ -155,26 +158,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
}) })
); );
const windowResize$ = fromEvent(window, 'resize').pipe(
distinctUntilChanged(),
startWith(null)
);
this.featuredAssets$ = combineLatest([
this.apiService.listFeaturedAssets$(),
windowResize$
]).pipe(
map(([featured, _]) => {
const newArray = [];
for (const feature of featured) {
if (feature.ticker !== 'L-BTC' && feature.asset) {
newArray.push(feature);
}
}
return newArray.slice(0, this.nbFeaturedAssets);
}),
);
this.transactions$ = this.stateService.transactions$ this.transactions$ = this.stateService.transactions$
.pipe( .pipe(
scan((acc, tx) => { scan((acc, tx) => {
@ -240,7 +223,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
share(), share(),
); );
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') { if (this.stateService.network === 'liquid') {
this.auditStatus$ = this.stateService.blocks$.pipe( this.auditStatus$ = this.stateService.blocks$.pipe(
takeUntil(this.destroy$), takeUntil(this.destroy$),
throttleTime(40000), throttleTime(40000),
@ -250,22 +233,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
shareReplay(1) shareReplay(1)
); );
////////// Pegs historical data //////////
this.liquidPegsMonth$ = this.auditStatus$.pipe(
throttleTime(60 * 60 * 1000),
switchMap(() => this.apiService.listLiquidPegsMonth$()),
map((pegs) => {
const labels = pegs.map(stats => stats.date);
const series = pegs.map(stats => parseFloat(stats.amount) / 100000000);
series.reduce((prev, curr, i) => series[i] = prev + curr, 0);
return {
series,
labels
};
}),
share(),
);
this.currentPeg$ = this.auditStatus$.pipe( this.currentPeg$ = this.auditStatus$.pipe(
switchMap(_ => switchMap(_ =>
this.apiService.liquidPegs$().pipe( this.apiService.liquidPegs$().pipe(
@ -278,7 +245,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
share() share()
); );
////////// BTC Reserves historical data //////////
this.auditUpdated$ = combineLatest([ this.auditUpdated$ = combineLatest([
this.auditStatus$, this.auditStatus$,
this.currentPeg$ this.currentPeg$
@ -293,21 +259,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
const amountCheck = currentPegAmount !== this.lastPegAmount; const amountCheck = currentPegAmount !== this.lastPegAmount;
this.lastPegAmount = currentPegAmount; this.lastPegAmount = currentPegAmount;
return of(blockAuditCheck || amountCheck); return of(blockAuditCheck || amountCheck);
})
);
this.liquidReservesMonth$ = this.auditStatus$.pipe(
throttleTime(60 * 60 * 1000),
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() share()
); );
@ -326,11 +277,78 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
share() share()
); );
this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$.pipe(startWith(null)), this.currentReserves$.pipe(startWith(null))]) this.recentPegsList$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.recentPegsList$()),
share()
);
this.pegsVolume$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.pegsVolume$()),
share()
);
this.federationAddresses$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.federationAddresses$()),
share()
);
this.federationAddressesNumber$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.federationAddressesNumber$()),
map(count => count.address_count),
share()
);
this.federationUtxosNumber$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.federationUtxosNumber$()),
map(count => count.utxo_count),
share()
);
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);
series.reduce((prev, curr, i) => series[i] = prev + curr, 0);
return {
series,
labels
};
}),
share(),
);
this.liquidReservesMonth$ = interval(60 * 60 * 1000).pipe(
startWith(0),
switchMap(() => this.apiService.listLiquidReservesMonth$()),
map(reserves => {
const labels = reserves.map(stats => stats.date);
const series = reserves.map(stats => parseFloat(stats.amount) / 100000000);
return {
series,
labels
};
}),
share()
);
this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$, this.currentReserves$])
.pipe( .pipe(
map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => { map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => {
liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000; liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000;
if (liquidPegs.series.length === liquidReserves?.series.length) { if (liquidPegs.series.length === liquidReserves?.series.length) {
liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000; liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000;
} else if (liquidPegs.series.length === liquidReserves?.series.length + 1) { } else if (liquidPegs.series.length === liquidReserves?.series.length + 1) {
@ -342,7 +360,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
labels: [] labels: []
}; };
} }
return { return {
liquidPegs, liquidPegs,
liquidReserves liquidReserves
@ -374,24 +392,21 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
getArrayFromNumber(num: number): number[] { getArrayFromNumber(num: number): number[] {
return Array.from({ length: num }, (_, i) => i + 1); return Array.from({ length: num }, (_, i) => i + 1);
} }
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
onResize(): void { onResize(): void {
if (window.innerWidth >= 992) { if (window.innerWidth >= 992) {
this.incomingGraphHeight = 300; this.incomingGraphHeight = 300;
this.goggleResolution = 82; this.goggleResolution = 82;
this.lbtcPegGraphHeight = 320; this.lbtcPegGraphHeight = 270;
this.nbFeaturedAssets = 6;
} else if (window.innerWidth >= 768) { } else if (window.innerWidth >= 768) {
this.incomingGraphHeight = 215; this.incomingGraphHeight = 215;
this.goggleResolution = 80; this.goggleResolution = 80;
this.lbtcPegGraphHeight = 230; this.lbtcPegGraphHeight = 190;
this.nbFeaturedAssets = 4;
} else { } else {
this.incomingGraphHeight = 180; this.incomingGraphHeight = 180;
this.goggleResolution = 86; this.goggleResolution = 86;
this.lbtcPegGraphHeight = 220; this.lbtcPegGraphHeight = 200;
this.nbFeaturedAssets = 4;
} }
} }
} }

View File

@ -12,6 +12,13 @@ import { FeeDistributionGraphComponent } from '../components/fee-distribution-gr
import { IncomingTransactionsGraphComponent } from '../components/incoming-transactions-graph/incoming-transactions-graph.component'; import { IncomingTransactionsGraphComponent } from '../components/incoming-transactions-graph/incoming-transactions-graph.component';
import { MempoolGraphComponent } from '../components/mempool-graph/mempool-graph.component'; import { MempoolGraphComponent } from '../components/mempool-graph/mempool-graph.component';
import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs-graph.component'; import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs-graph.component';
import { ReservesSupplyStatsComponent } from '../components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component';
import { ReservesRatioStatsComponent } from '../components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component';
import { ReservesRatioComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component';
import { RecentPegsStatsComponent } from '../components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component';
import { RecentPegsListComponent } from '../components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component';
import { FederationAddressesStatsComponent } from '../components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component';
import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component';
import { GraphsComponent } from '../components/graphs/graphs.component'; import { GraphsComponent } from '../components/graphs/graphs.component';
import { StatisticsComponent } from '../components/statistics/statistics.component'; import { StatisticsComponent } from '../components/statistics/statistics.component';
import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component'; import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component';
@ -48,6 +55,13 @@ import { CommonModule } from '@angular/common';
IncomingTransactionsGraphComponent, IncomingTransactionsGraphComponent,
MempoolGraphComponent, MempoolGraphComponent,
LbtcPegsGraphComponent, LbtcPegsGraphComponent,
ReservesSupplyStatsComponent,
ReservesRatioStatsComponent,
ReservesRatioComponent,
RecentPegsStatsComponent,
RecentPegsListComponent,
FederationAddressesStatsComponent,
FederationAddressesListComponent,
HashrateChartComponent, HashrateChartComponent,
HashrateChartPoolsComponent, HashrateChartPoolsComponent,
BlockHealthGraphComponent, BlockHealthGraphComponent,

View File

@ -15,17 +15,10 @@ import { AssetsComponent } from '../components/assets/assets.component';
import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component' import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component'
import { AssetComponent } from '../components/asset/asset.component'; import { AssetComponent } from '../components/asset/asset.component';
import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component'; import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component';
import { ReservesAuditDashboardComponent } from '../components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component';
import { ReservesSupplyStatsComponent } from '../components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component';
import { RecentPegsStatsComponent } from '../components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component';
import { RecentPegsListComponent } from '../components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component'; import { RecentPegsListComponent } from '../components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component';
import { FederationWalletComponent } from '../components/liquid-reserves-audit/federation-wallet/federation-wallet.component'; import { FederationWalletComponent } from '../components/liquid-reserves-audit/federation-wallet/federation-wallet.component';
import { FederationUtxosListComponent } from '../components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component'; import { FederationUtxosListComponent } from '../components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component';
import { FederationAddressesStatsComponent } from '../components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component';
import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component'; import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component';
import { ReservesRatioComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component';
import { ReservesRatioStatsComponent } from '../components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component';
import { ReservesRatioGraphComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component';
const routes: Routes = [ const routes: Routes = [
{ {
@ -77,18 +70,6 @@ const routes: Routes = [
data: { preload: true, networkSpecific: true }, data: { preload: true, networkSpecific: true },
loadChildren: () => import('../components/block/block.module').then(m => m.BlockModule), loadChildren: () => import('../components/block/block.module').then(m => m.BlockModule),
}, },
{
path: 'audit',
data: { networks: ['liquid'] },
component: StartComponent,
children: [
{
path: '',
data: { networks: ['liquid'] },
component: ReservesAuditDashboardComponent,
}
]
},
{ {
path: 'audit/wallet', path: 'audit/wallet',
data: { networks: ['liquid'] }, data: { networks: ['liquid'] },
@ -180,17 +161,8 @@ export class LiquidRoutingModule { }
], ],
declarations: [ declarations: [
LiquidMasterPageComponent, LiquidMasterPageComponent,
ReservesAuditDashboardComponent,
ReservesSupplyStatsComponent,
RecentPegsStatsComponent,
RecentPegsListComponent,
FederationWalletComponent, FederationWalletComponent,
FederationUtxosListComponent, FederationUtxosListComponent,
FederationAddressesStatsComponent,
FederationAddressesListComponent,
ReservesRatioComponent,
ReservesRatioStatsComponent,
ReservesRatioGraphComponent,
] ]
}) })
export class LiquidMasterPageModule { } export class LiquidMasterPageModule { }