Liquid: Federation Audit Dashboard

This commit is contained in:
natsee 2024-01-23 09:57:26 +01:00
parent b7feb0d43d
commit a6b584d964
No known key found for this signature in database
GPG Key ID: 233CF3150A89BED8
32 changed files with 913 additions and 162 deletions

View File

@ -27,7 +27,6 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
template: ('widget' | 'advanced') = 'widget';
isLoading = true;
pegsChartOption: EChartsOption = {};
pegsChartInitOption = {
renderer: 'svg'
};
@ -44,7 +43,7 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
if (!this.data?.liquidPegs) {
return;
}
if (!this.data.liquidReserves || this.data.liquidReserves?.series.length !== this.data.liquidPegs.series.length) {
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);

View File

@ -79,7 +79,7 @@
<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 class="nav-item" routerLinkActive="active" id="btn-audit">
<a class="nav-link" [routerLink]="['/audit']" (click)="collapse()"><fa-icon [icon]="['fas', 'check-circle']" [fixedWidth]="true" i18n-title="master-page.audit" title="BTC Reserves Audit"></fa-icon></a>
<a class="nav-link" [routerLink]="['/audit']" (click)="collapse()"><fa-icon [icon]="['fas', 'scale-balanced']" [fixedWidth]="true" i18n-title="master-page.audit" title="BTC Reserves Audit"></fa-icon></a>
</li>
<li [hidden]="isMobile" 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>

View File

@ -0,0 +1,74 @@
<div class="container-xl" [ngClass]="{'widget': widget, 'full-height': !widget}">
<h1 *ngIf="!widget" class="float-left" i18n="liquid.federation-addresses">Liquid Federation Addresses</h1>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div>
<div style="min-height: 295px">
<table class="table table-borderless">
<thead style="vertical-align: middle;">
<th class="address text-left" [ngClass]="{'widget': widget}" i18n="liquid.address">Address</th>
<th class="amount text-right" [ngClass]="{'widget': widget}" i18n="liquid.balance">Balance</th>
</thead>
<tbody *ngIf="federationAddresses$ | async as addresses; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<ng-container *ngIf="widget; else regularRows">
<tr *ngFor="let address of addresses | slice:0:5">
<td class="address text-left widget">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + address.bitcoinaddress }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="address.bitcoinaddress" [lastChars]="6"></app-truncate>
</a>
</td>
<td class="amount text-right widget">
<app-amount [satoshis]="+address.balance" [noFiat]="true" [forceBtc]="true"></app-amount>
</td>
</tr>
</ng-container>
<ng-template #regularRows>
<tr *ngFor="let address of addresses | slice:(page - 1) * pageSize:page * pageSize">
<td class="address text-left">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + address.bitcoinaddress }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="address.bitcoinaddress" [lastChars]="6"></app-truncate>
</a>
</td>
<td class="amount text-right">
<app-amount [satoshis]="+address.balance" [noFiat]="true" [forceBtc]="true"></app-amount>
</td>
</tr>
</ng-template>
</tbody>
<ng-template #skeleton>
<tbody *ngIf="widget; else regularRowsSkeleton">
<tr *ngFor="let item of skeletonLines">
<td class="address text-left widget">
<span class="skeleton-loader" style="max-width: 400px"></span>
</td>
<td class="amount text-right widget">
<span class="skeleton-loader" style="max-width: 350px"></span>
</td>
</tr>
</tbody>
<ng-template #regularRowsSkeleton>
<tr *ngFor="let item of skeletonLines">
<td class="address text-left">
<span class="skeleton-loader" style="max-width: 600px"></span>
</td>
<td class="amount text-right">
<span class="skeleton-loader" style="max-width: 400px"></span>
</td>
</tr>
</ng-template>
</ng-template>
</table>
<ngb-pagination *ngIf="!widget && federationAddresses$ | async as addresses" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
[collectionSize]="addresses.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
</ngb-pagination>
<ng-template [ngIf]="!widget">
<div class="clearfix"></div>
<br>
</ng-template>
</div>
</div>

View File

@ -0,0 +1,54 @@
.spinner-border {
height: 25px;
width: 25px;
margin-top: 13px;
}
.container-xl {
max-width: 1000px;
}
.container-xl.widget {
padding-left: 0px;
padding-right: 0px;
padding-bottom: 0px;
}
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.6rem !important;
padding-right: 2rem !important;
.widget {
padding-right: 1rem !important;
}
}
.clear-link {
color: white;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}
.progress {
background-color: #2d3348;
}
.address {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
}
.address.widget {
width: 60%;
}
.amount {
width: 25%;
}
.amount.widget {
width: 40%;
}

View File

@ -0,0 +1,77 @@
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { Observable, concat } from 'rxjs';
import { delay, filter, map, share, skip, switchMap, tap, throttleTime } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service';
import { Env, StateService } from '../../../services/state.service';
import { AuditStatus, FederationAddress } from '../../../interfaces/node-api.interface';
import { WebsocketService } from '../../../services/websocket.service';
@Component({
selector: 'app-federation-addresses-list',
templateUrl: './federation-addresses-list.component.html',
styleUrls: ['./federation-addresses-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FederationAddressesListComponent implements OnInit {
@Input() widget: boolean = false;
@Input() federationAddresses$: Observable<FederationAddress[]>;
env: Env;
isLoading = true;
page = 1;
pageSize = 15;
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
skeletonLines: number[] = [];
auditStatus$: Observable<AuditStatus>;
auditUpdated$: Observable<boolean>;
lastReservesBlockUpdate: number = 0;
constructor(
private apiService: ApiService,
public stateService: StateService,
private websocketService: WebsocketService
) {
}
ngOnInit(): void {
this.isLoading = !this.widget;
this.env = this.stateService.env;
this.skeletonLines = this.widget === true ? [...Array(5).keys()] : [...Array(15).keys()];
if (!this.widget) {
this.websocketService.want(['blocks']);
this.auditStatus$ = concat(
this.apiService.federationAuditSynced$(),
this.stateService.blocks$.pipe(
skip(1),
throttleTime(40000),
delay(2000),
switchMap(() => this.apiService.federationAuditSynced$()),
share()
)
);
this.auditUpdated$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
map(auditStatus => {
const beforeLastBlockAudit = this.lastReservesBlockUpdate;
this.lastReservesBlockUpdate = auditStatus.lastBlockAudit;
return auditStatus.lastBlockAudit > beforeLastBlockAudit ? true : false;
})
);
this.federationAddresses$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
switchMap(_ => this.apiService.federationAddresses$()),
tap(_ => this.isLoading = false),
share()
);
}
}
pageChange(page: number): void {
this.page = page;
}
}

View File

@ -0,0 +1,34 @@
<div *ngIf="(federationAddresses$ | async) as federationAddresses; else loadingData">
<div class="fee-estimation-container">
<div class="item">
<a class="title-link" [routerLink]="['/audit/addresses' | relativeUrl]">
<h5 class="card-title" i18n="liquid.federation-addresses">Liquid Federation Addresses <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
<div class="card-text">
<div class="fee-text">{{ federationAddresses.length }} <span i18n="liquid.addresses">addresses</span></div>
<span class="fiat" *ngIf="(federationAddressesOneMonthAgo$ | async) as federationAddressesOneMonthAgo; else loadingSkeleton" i18n-ngbTooltip="liquid.percentage-change-last-month" ngbTooltip="Percentage change past month" placement="bottom">
<app-change [current]="federationAddresses.length" [previous]="federationAddressesOneMonthAgo.addresses_count_one_month"></app-change>
</span>
</div>
</div>
</div>
</div>
<ng-template #loadingData>
<div class="fee-estimation-container loading-container">
<div class="item">
<a class="title-link" [routerLink]="['/audit/addresses' | relativeUrl]">
<h5 class="card-title" i18n="liquid.federation-addresses">Liquid Federation Addresses <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>
<ng-template #loadingSkeleton>
<div class="skeleton-loader skeleton-loader-transactions" style="margin-top: 2px; margin-bottom: 5px;"></div>
</ng-template>

View File

@ -0,0 +1,75 @@
.fee-estimation-container {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
max-width: 300px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
.card-title {
margin: 0;
color: #4a68b9;
font-size: 10px;
font-size: 1rem;
white-space: nowrap;
}
.card-text {
font-size: 22px;
span {
font-size: 11px;
position: relative;
top: -2px;
}
}
.card-text span {
color: #ffffff66;
font-size: 12px;
top: 0px;
}
.fee-text{
border-bottom: 1px solid #ffffff1c;
width: fit-content;
margin: auto;
line-height: 1.45;
padding: 0px 2px;
}
.fiat {
display: block;
font-size: 14px !important;
}
}
}
.loading-container{
min-height: 76px;
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}
}
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
display: block;
margin-bottom: 4px;
text-decoration: none;
color: inherit;
}

View File

@ -0,0 +1,20 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { FederationAddress } from '../../../interfaces/node-api.interface';
@Component({
selector: 'app-federation-addresses-stats',
templateUrl: './federation-addresses-stats.component.html',
styleUrls: ['./federation-addresses-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FederationAddressesStatsComponent implements OnInit {
@Input() federationAddresses$: Observable<FederationAddress[]>;
@Input() federationAddressesOneMonthAgo$: Observable<any>;
constructor() { }
ngOnInit(): void {
}
}

View File

@ -7,11 +7,11 @@
<div style="min-height: 295px">
<table class="table table-borderless">
<thead style="vertical-align: middle;">
<th class="txid text-left" [ngClass]="{'widget': widget}">Output</th>
<th class="address text-left" *ngIf="!widget">Address</th>
<th class="amount text-right" [ngClass]="{'widget': widget}">Amount</th>
<th class="pegin text-left" *ngIf="!widget">Liquid Peg-in</th>
<th class="timestamp text-left" i18n="latest-blocks.date" [ngClass]="{'widget': widget}">Date</th>
<th class="txid text-left" [ngClass]="{'widget': widget}" i18n="liquid.output">Output</th>
<th class="address text-left" *ngIf="!widget" i18n="liquid.address">Address</th>
<th class="amount text-right" [ngClass]="{'widget': widget}" i18n="liquid.amount">Amount</th>
<th class="pegin text-left" *ngIf="!widget" i18n="liquid.peg-in">Liquid Peg-in</th>
<th class="timestamp text-right" i18n="latest-blocks.date" [ngClass]="{'widget': widget}">Date</th>
</thead>
<tbody *ngIf="federationUtxos$ | async as utxos; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<ng-container *ngIf="widget; else regularRows">
@ -24,7 +24,7 @@
<td class="amount text-right widget">
<app-amount [satoshis]="utxo.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
</td>
<td class="timestamp text-left widget">
<td class="timestamp text-right widget">
<app-time kind="since" [time]="utxo.blocktime"></app-time>
</td>
</tr>
@ -45,11 +45,16 @@
<app-amount [satoshis]="utxo.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
</td>
<td class="pegin text-left">
<a *ngIf="utxo.pegtxid" [routerLink]="['/tx', utxo.pegtxid + ':' + utxo.pegindex]" target="_blank" style="color:#116761">
<app-truncate [text]="utxo.pegtxid + ':' + utxo.pegindex" [lastChars]="6"></app-truncate>
</a>
<ng-container *ngIf="utxo.pegtxid; else noPeginMessage">
<a [routerLink]="['/tx', utxo.pegtxid + ':' + utxo.pegindex]" target="_blank" style="color:#116761">
<app-truncate [text]="utxo.pegtxid + ':' + utxo.pegindex" [lastChars]="6"></app-truncate>
</a>
</ng-container>
<ng-template #noPeginMessage>
<span class="text-muted">-</span>
</ng-template>
</td>
<td class="timestamp text-left">
<td class="timestamp text-right">
&lrm;{{ utxo.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="utxo.blocktime"></app-time>)</i></div>
</td>
@ -60,13 +65,13 @@
<tbody *ngIf="widget; else regularRowsSkeleton">
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left widget">
<span class="skeleton-loader" style="max-width: 190px"></span>
<span class="skeleton-loader" style="max-width: 400px"></span>
</td>
<td class="amount text-right widget">
<span class="skeleton-loader" style="max-width: 140px"></span>
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="timestamp text-left widget">
<span class="skeleton-loader" style="max-width: 200px"></span>
<td class="timestamp text-right widget">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
</tr>
</tbody>
@ -84,7 +89,7 @@
<td class="pegin text-left">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="timestamp text-left">
<td class="timestamp text-right">
<span class="skeleton-loader" style="max-width: 140px"></span>
</td>
</tr>

View File

@ -71,6 +71,9 @@ tr, td, th {
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
@media (max-width: 872px) {
display: none;
}
}
.timestamp {
@ -85,8 +88,7 @@ tr, td, th {
}
}
.timestamp.widget {
width: 30%;
padding-right: 0px !important;
width: 100%;
@media (min-width: 768px) AND (max-width: 1050px) {
display: none;
}

View File

@ -1,6 +1,6 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable, timer, of, concat } from 'rxjs';
import { delay, delayWhen, filter, map, retryWhen, scan, share, skip, switchMap, tap } from 'rxjs/operators';
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { Observable, concat } from 'rxjs';
import { delay, filter, map, share, skip, switchMap, tap, throttleTime } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service';
import { Env, StateService } from '../../../services/state.service';
import { AuditStatus, FederationUtxo } from '../../../interfaces/node-api.interface';
@ -23,12 +23,13 @@ export class FederationUtxosListComponent implements OnInit {
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
skeletonLines: number[] = [];
auditStatus$: Observable<AuditStatus>;
auditUpdated$: Observable<boolean>;
lastReservesBlockUpdate: number = 0;
constructor(
private apiService: ApiService,
public stateService: StateService,
private websocketService: WebsocketService,
private cd: ChangeDetectorRef,
) {
}
@ -42,14 +43,24 @@ export class FederationUtxosListComponent implements OnInit {
this.apiService.federationAuditSynced$(),
this.stateService.blocks$.pipe(
skip(1),
throttleTime(40000),
delay(2000),
switchMap(() => this.apiService.federationAuditSynced$()),
share()
)
);
this.federationUtxos$ = this.auditStatus$.pipe(
this.auditUpdated$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
map(auditStatus => {
const beforeLastBlockAudit = this.lastReservesBlockUpdate;
this.lastReservesBlockUpdate = auditStatus.lastBlockAudit;
return auditStatus.lastBlockAudit > beforeLastBlockAudit ? true : false;
})
);
this.federationUtxos$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
switchMap(_ => this.apiService.federationUtxos$()),
tap(_ => this.isLoading = false),
share()

View File

@ -1,41 +1,26 @@
<div *ngIf="(federationUtxos$ | async) as federationUtxos; else loadingData">
<div *ngIf="(federationAddresses$ | async) as federationAddresses; else loadingData">
<div class="fee-estimation-container">
<div class="item">
<a class="title-link" [routerLink]="['/audit/utxos' | relativeUrl]">
<h5 class="card-title" i18n="dashboard.federation-utxos">Outputs <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
<h5 class="card-title" i18n="liquid.federation-utxos">Liquid Federation UTXOs <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
<div class="card-text">
<div class="fee-text">{{ federationUtxos.length }} <span>UTXOs</span></div>
<span class="fiat" *ngIf="(federationUtxosOneMonthAgo$ | async) as federationUtxosOneMonthAgo" i18n-ngbTooltip="liquid.percentage-change-last-month" ngbTooltip="Percentage change past month" placement="bottom">
<app-change [current]="federationUtxos.length" [previous]="federationUtxosOneMonthAgo.length"></app-change>
</span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="dashboard.federation-addresses">Addresses</h5>
<div class="card-text">
<div class="fee-text">{{ federationAddresses.length }} <span>addresses</span></div>
<span class="fiat" *ngIf="(federationAddressesOneMonthAgo$ | async) as federationAddressesOneMonthAgo" i18n-ngbTooltip="liquid.percentage-change-last-month" ngbTooltip="Percentage change past month" placement="bottom">
<app-change [current]="federationAddresses.length" [previous]="federationAddressesOneMonthAgo.length"></app-change>
<span class="fiat" *ngIf="(federationUtxosOneMonthAgo$ | async) as federationUtxosOneMonthAgo; else loadingSkeleton" i18n-ngbTooltip="liquid.percentage-change-last-month" ngbTooltip="Percentage change past month" placement="bottom">
<app-change [current]="federationUtxos.length" [previous]="federationUtxosOneMonthAgo.utxos_count_one_month"></app-change>
</span>
</div>
</div>
</div>
</div>
</div>
<ng-template #loadingData>
<div class="fee-estimation-container loading-container">
<div class="item">
<h5 class="card-title" i18n="dashboard.federation-utxos">Outputs</h5>
<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.federation-addresses">Addresses</h5>
<a class="title-link" [routerLink]="['/audit/utxos' | relativeUrl]">
<h5 class="card-title" i18n="liquid.federation-utxos">Liquid Federation UTXOs <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
@ -44,3 +29,6 @@
</div>
</ng-template>
<ng-template #loadingSkeleton>
<div class="skeleton-loader skeleton-loader-transactions" style="margin-top: 2px; margin-bottom: 5px;"></div>
</ng-template>

View File

@ -5,7 +5,7 @@
flex-direction: row;
}
.item {
max-width: 150px;
max-width: 300px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
@ -13,12 +13,10 @@
}
.card-title {
margin: 0;
color: #4a68b9;
font-size: 10px;
margin-bottom: 4px;
font-size: 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@ -30,10 +28,7 @@
top: -2px;
}
}
&:last-child {
margin-bottom: 0;
}
.card-text span {
color: #ffffff66;
font-size: 12px;
@ -74,7 +69,7 @@
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
display: block;
margin-bottom: 10px;
margin-bottom: 4px;
text-decoration: none;
color: inherit;
}

View File

@ -1,9 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { concat, interval, Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service';
import { StateService } from '../../../services/state.service';
import { FederationAddress, FederationUtxo } from '../../../interfaces/node-api.interface';
import { Observable } from 'rxjs';
import { FederationUtxo } from '../../../interfaces/node-api.interface';
@Component({
selector: 'app-federation-utxos-stats',
@ -13,30 +10,11 @@ import { FederationAddress, FederationUtxo } from '../../../interfaces/node-api.
})
export class FederationUtxosStatsComponent implements OnInit {
@Input() federationUtxos$: Observable<FederationUtxo[]>;
@Input() federationAddresses$: Observable<FederationAddress[]>;
@Input() federationUtxosOneMonthAgo$: Observable<any>;
federationUtxosOneMonthAgo$: Observable<FederationUtxo[]>;
federationAddressesOneMonthAgo$: Observable<FederationAddress[]>;
constructor(private apiService: ApiService, private stateService: StateService) { }
constructor() { }
ngOnInit(): void {
// Calls this.apiService.federationUtxosOneMonthAgo$ at load and then every day
this.federationUtxosOneMonthAgo$ = concat(
this.apiService.federationUtxosOneMonthAgo$(),
interval(24 * 60 * 60 * 1000).pipe(
switchMap(() => this.apiService.federationUtxosOneMonthAgo$())
)
);
// Calls this.apiService.federationAddressesOneMonthAgo$ at load and then every day
this.federationAddressesOneMonthAgo$ = concat(
this.apiService.federationAddressesOneMonthAgo$(),
interval(24 * 60 * 60 * 1000).pipe(
switchMap(() => this.apiService.federationAddressesOneMonthAgo$())
)
);
}
}

View File

@ -3,9 +3,6 @@
<div class="row row-cols-1 row-cols-md-2">
<div class="col">
<div class="main-title">
<span i18n="liquid.bitcoin-reserves">BTC Reserves</span>
</div>
<div class="card">
<div class="card-body">
<app-reserves-supply-stats [currentPeg$]="currentPeg$" [currentReserves$]="currentReserves$"></app-reserves-supply-stats>
@ -14,30 +11,31 @@
</div>
</div>
<div class="col">
<div class="main-title">
<span i18n="liquid.federation-utxos">Liquid Federation UTXOs</span>
<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"></app-reserves-ratio-graph>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<app-federation-utxos-stats [federationUtxos$]="federationUtxos$" [federationAddresses$]="federationAddresses$"></app-federation-utxos-stats>
<app-federation-utxos-stats [federationUtxos$]="federationUtxos$" [federationUtxosOneMonthAgo$]="federationUtxosOneMonthAgo$"></app-federation-utxos-stats>
<app-federation-utxos-list [federationUtxos$]="federationUtxos$" [widget]="true"></app-federation-utxos-list>
</div>
</div>
</div>
<div class="col" style="margin-bottom: 1.47rem">
<div class="card graph-card">
<div class="card-body pl-2 pr-2">
<!-- The historical ratio chart -->
</div>
</div>
</div>
<div class="col" style="margin-bottom: 1.47rem">
<div class="card graph-card">
<div class="card-body pl-2 pr-2">
<!-- The addresses table -->
<div class="card">
<div class="card-body">
<app-federation-addresses-stats [federationAddresses$]="federationAddresses$" [federationAddressesOneMonthAgo$]="federationAddressesOneMonthAgo$"></app-federation-addresses-stats>
<app-federation-addresses-list [federationAddresses$]="federationAddresses$" [widget]="true"></app-federation-addresses-list>
</div>
</div>
</div>
@ -51,9 +49,6 @@
<div class="row row-cols-1 row-cols-md-2">
<div class="col">
<div class="main-title">
<span i18n="liquid.bitcoin-reserves">BTC Reserves</span>
</div>
<div class="card">
<div class="card-body">
<app-reserves-supply-stats></app-reserves-supply-stats>
@ -61,11 +56,19 @@
</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="main-title">
<span i18n="liquid.federation-utxos">Liquid Federation UTXOs</span>
</div>
<div class="card">
<div class="card-body">
<app-federation-utxos-stats></app-federation-utxos-stats>
@ -75,17 +78,10 @@
</div>
<div class="col" style="margin-bottom: 1.47rem">
<div class="card graph-card">
<div class="card-body pl-2 pr-2">
<!-- The historical ratio chart -->
</div>
</div>
</div>
<div class="col" style="margin-bottom: 1.47rem">
<div class="card graph-card">
<div class="card-body pl-2 pr-2">
<!-- The addresses table -->
<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>

View File

@ -10,19 +10,8 @@
background-color: #1d1f31;
}
.graph-card {
height: 100%;
@media (min-width: 992px) {
height: 385px;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.card-title > a {
color: #4a68b9;
padding-top: 20px;
}
.card-body.pool-ranking {
@ -48,17 +37,6 @@
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
}
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 11px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.in-progress-message {
position: relative;
color: #ffffff91;

View File

@ -2,9 +2,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { SeoService } from '../../../services/seo.service';
import { WebsocketService } from '../../../services/websocket.service';
import { StateService } from '../../../services/state.service';
import { Observable, concat, delay, filter, share, skip, switchMap, tap, throttleTime } from 'rxjs';
import { Observable, combineLatest, concat, delay, filter, interval, map, mergeMap, of, share, skip, startWith, switchMap, tap, throttleTime } from 'rxjs';
import { ApiService } from '../../../services/api.service';
import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo } from '../../../interfaces/node-api.interface';
import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo, LiquidPegs } from '../../../interfaces/node-api.interface';
@Component({
selector: 'app-reserves-audit-dashboard',
@ -14,10 +14,16 @@ import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo } from '../
})
export class ReservesAuditDashboardComponent implements OnInit {
auditStatus$: Observable<AuditStatus>;
auditUpdated$: Observable<boolean>;
currentPeg$: Observable<CurrentPegs>;
currentReserves$: Observable<CurrentPegs>;
federationUtxos$: Observable<FederationUtxo[]>;
federationUtxosOneMonthAgo$: Observable<any>;
federationAddresses$: Observable<FederationAddress[]>;
federationAddressesOneMonthAgo$: Observable<any>;
liquidPegsMonth$: Observable<any>;
liquidReservesMonth$: Observable<any>;
fullHistory$: Observable<any>;
private lastPegBlockUpdate: number = 0;
private lastReservesBlockUpdate: number = 0;
@ -45,8 +51,16 @@ export class ReservesAuditDashboardComponent implements OnInit {
)
);
this.currentReserves$ = this.auditStatus$.pipe(
this.auditUpdated$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
map(auditStatus => auditStatus.lastBlockAudit),
switchMap((lastBlockAudit) => {
return lastBlockAudit > this.lastReservesBlockUpdate ? of(true) : of(false);
}),
);
this.currentReserves$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
switchMap(_ =>
this.apiService.liquidReserves$().pipe(
filter((currentReserves) => currentReserves.lastBlockUpdate > this.lastReservesBlockUpdate),
@ -71,17 +85,84 @@ export class ReservesAuditDashboardComponent implements OnInit {
share()
);
this.federationUtxos$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
this.federationUtxos$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
switchMap(_ => this.apiService.federationUtxos$()),
share()
);
this.federationAddresses$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
this.federationAddresses$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
switchMap(_ => this.apiService.federationAddresses$()),
share()
);
this.federationUtxosOneMonthAgo$ = interval(60 * 60 * 1000)
.pipe(
startWith(0),
switchMap(() => this.apiService.federationUtxosOneMonthAgo$())
);
this.federationAddressesOneMonthAgo$ = interval(60 * 60 * 1000)
.pipe(
startWith(0),
switchMap(() => this.apiService.federationAddressesOneMonthAgo$())
);
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()
);
}
}

View File

@ -0,0 +1,42 @@
<div *ngIf="(unbackedMonths$ | async) as unbackedMonths; else loadingData">
<ng-container *ngIf="unbackedMonths.historyComplete; else loadingData">
<div class="fee-estimation-container">
<div class="item">
<h5 class="card-title" i18n="liquid.unpeg">Unpeg</h5>
<div class="card-text">
<div class="fee-text" [ngClass]="{'danger' : unbackedMonths.total > 0, 'correct': unbackedMonths.total === 0}">
{{ unbackedMonths.total }} <span i18n="liquid.unpeg-event">Unpeg Event</span>
</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="liquid.peg-ratio-history">Avg Peg Ratio</h5>
<div class="card-text">
<div class="fee-text" [ngClass]="{'danger' : unbackedMonths.avg < 1, 'correct': unbackedMonths.avg >= 1}">
{{ unbackedMonths.avg.toFixed(5) }}
</div>
</div>
</div>
</div>
</ng-container>
</div>
<ng-template #loadingData>
<div class="fee-estimation-container loading-container">
<div class="item">
<h5 class="card-title" i18n="liquid.peg-ratio-history">Peg Ratio History</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="liquid.peg-ratio-history">Avg Peg Ratio</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>

View File

@ -0,0 +1,63 @@
.fee-estimation-container {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
max-width: 300px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
.card-title {
margin-bottom: 4px;
color: #4a68b9;
font-size: 10px;
font-size: 1rem;
white-space: nowrap;
}
.card-text {
font-size: 22px;
span {
font-size: 11px;
position: relative;
top: -2px;
}
.danger {
color: #D81B60;
}
.correct {
color: #7CB342;
}
}
.card-text span {
color: #ffffff66;
font-size: 12px;
top: 0px;
}
.fee-text{
width: fit-content;
margin: auto;
line-height: 1.45;
padding: 0px 2px;
}
}
}
.loading-container{
min-height: 76px;
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
max-width: 90px;
margin: 15px auto 3px;
}
}

View File

@ -0,0 +1,51 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable, map } from 'rxjs';
@Component({
selector: 'app-reserves-ratio-stats',
templateUrl: './reserves-ratio-stats.component.html',
styleUrls: ['./reserves-ratio-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReservesRatioStatsComponent implements OnInit {
@Input() fullHistory$: Observable<any>;
unbackedMonths$: Observable<any>
constructor() { }
ngOnInit(): void {
if (!this.fullHistory$) {
return;
}
this.unbackedMonths$ = this.fullHistory$
.pipe(
map((fullHistory) => {
if (fullHistory.liquidPegs.series.length !== fullHistory.liquidReserves.series.length) {
return {
historyComplete: false,
total: null
};
}
// Only check the last 3 years
let ratioSeries = fullHistory.liquidReserves.series.map((value: number, index: number) => value / fullHistory.liquidPegs.series[index]);
ratioSeries = ratioSeries.slice(Math.max(ratioSeries.length - 36, 0));
let total = 0;
let avg = 0;
for (let i = 0; i < ratioSeries.length; i++) {
avg += ratioSeries[i];
if (ratioSeries[i] < 1) {
total++;
}
}
avg = avg / ratioSeries.length;
return {
historyComplete: true,
total: total,
avg: avg,
};
})
);
}
}

View File

@ -0,0 +1,4 @@
<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

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

View File

@ -0,0 +1,195 @@
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;
ratioHistoryChartOptions: EChartsOption;
ratioSeries: number[] = [];
height: number | string = '200';
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

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

View File

@ -12,7 +12,7 @@ import { CurrentPegs } from '../../../interfaces/node-api.interface';
export class ReservesRatioComponent implements OnInit, OnChanges {
@Input() currentPeg: CurrentPegs;
@Input() currentReserves: CurrentPegs;
pegsChartOptions: EChartsOption;
ratioChartOptions: EChartsOption;
height: number | string = '200';
right: number | string = '10';
@ -21,8 +21,7 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
template: ('widget' | 'advanced') = 'widget';
isLoading = true;
pegsChartOption: EChartsOption = {};
pegsChartInitOption = {
ratioChartInitOptions = {
renderer: 'svg'
};
@ -36,7 +35,7 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
if (!this.currentPeg || !this.currentReserves || this.currentPeg.amount === '0') {
return;
}
this.pegsChartOptions = this.createChartOptions(this.currentPeg, this.currentReserves);
this.ratioChartOptions = this.createChartOptions(this.currentPeg, this.currentReserves);
}
rendered() {

View File

@ -11,7 +11,7 @@
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">BTC Reserves</h5>
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">BTC Reserves<span class="badge badge-pill badge-warning beta" style="margin-left: 4px; font-size: 10px;">beta</span></h5>
<div class="card-text">
<div class="fee-text">{{ (+currentReserves.amount) / 100000000 | number: '1.2-2' }} <span style="color: #b86d12;">BTC</span></div>
<span class="fiat">

View File

@ -276,7 +276,7 @@
</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>
<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><span class="badge badge-pill badge-warning beta" style="margin-left: 4px; font-size: 10px;">beta</span></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>

View File

@ -49,6 +49,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
liquidPegsMonth$: Observable<any>;
currentPeg$: Observable<CurrentPegs>;
auditStatus$: Observable<AuditStatus>;
auditUpdated$: Observable<boolean>;
liquidReservesMonth$: Observable<any>;
currentReserves$: Observable<CurrentPegs>;
fullHistory$: Observable<any>;
@ -258,7 +259,15 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
share()
)
);
this.auditUpdated$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
map(auditStatus => auditStatus.lastBlockAudit),
switchMap((lastBlockAudit) => {
return lastBlockAudit > this.lastReservesBlockUpdate ? of(true) : of(false);
}),
);
this.liquidReservesMonth$ = interval(60 * 60 * 1000).pipe(
startWith(0),
mergeMap(() => this.apiService.federationAuditSynced$()),
@ -276,8 +285,8 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
share()
);
this.currentReserves$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
this.currentReserves$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
switchMap(_ =>
this.apiService.liquidReserves$().pipe(
filter((currentReserves) => currentReserves.lastBlockUpdate > this.lastReservesBlockUpdate),
@ -298,6 +307,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
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: [],
@ -309,7 +319,8 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
liquidPegs,
liquidReserves
};
})
}),
share()
);
}

View File

@ -83,7 +83,7 @@ export interface CurrentPegs {
}
export interface FederationAddress {
address: string;
bitcoinaddress: string;
balance: string;
}

View File

@ -19,7 +19,11 @@ import { ReservesAuditDashboardComponent } from '../components/liquid-reserves-a
import { ReservesSupplyStatsComponent } from '../components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component';
import { FederationUtxosStatsComponent } from '../components/liquid-reserves-audit/federation-utxos-stats/federation-utxos-stats.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 { 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 = [
{
@ -87,6 +91,10 @@ const routes: Routes = [
path: 'audit/utxos',
component: FederationUtxosListComponent,
},
{
path: 'audit/addresses',
component: FederationAddressesListComponent,
},
{
path: 'assets',
data: { networks: ['liquid'] },
@ -156,7 +164,11 @@ export class LiquidRoutingModule { }
ReservesSupplyStatsComponent,
FederationUtxosStatsComponent,
FederationUtxosListComponent,
FederationAddressesStatsComponent,
FederationAddressesListComponent,
ReservesRatioComponent,
ReservesRatioStatsComponent,
ReservesRatioGraphComponent,
]
})
export class LiquidMasterPageModule { }

View File

@ -206,11 +206,11 @@ export class ApiService {
return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos');
}
federationAddressesOneMonthAgo$(): Observable<FederationAddress[]> {
federationAddressesOneMonthAgo$(): Observable<any> {
return this.httpClient.get<FederationAddress[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses/previous-month');
}
federationUtxosOneMonthAgo$(): Observable<FederationUtxo[]> {
federationUtxosOneMonthAgo$(): Observable<any> {
return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos/previous-month');
}

View File

@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket } from '@fortawesome/free-solid-svg-icons';
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '../components/menu/menu.component';
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
@ -385,5 +385,6 @@ export class SharedModule {
library.addIcons(faUserCircle);
library.addIcons(faCheck);
library.addIcons(faRocket);
library.addIcons(faScaleBalanced);
}
}