mirror of
https://github.com/mempool/mempool.git
synced 2025-04-22 14:34:47 +02:00
Liquid: Federation Audit Dashboard
This commit is contained in:
parent
b7feb0d43d
commit
a6b584d964
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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%;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
@ -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">
|
||||
‎{{ 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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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$())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -0,0 +1,6 @@
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 100;
|
||||
}
|
@ -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'
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ export interface CurrentPegs {
|
||||
}
|
||||
|
||||
export interface FederationAddress {
|
||||
address: string;
|
||||
bitcoinaddress: string;
|
||||
balance: string;
|
||||
}
|
||||
|
||||
|
@ -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 { }
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user