Liquid audit: Add recent pegs widget and table

This commit is contained in:
natsee 2024-01-29 17:06:25 +01:00
parent 451a61e5fc
commit 639fc3dd5f
No known key found for this signature in database
GPG key ID: 233CF3150A89BED8
17 changed files with 428 additions and 69 deletions

View file

@ -847,6 +847,7 @@ class DatabaseMigration {
lasttimeupdate int(11) unsigned NOT NULL, lasttimeupdate int(11) unsigned NOT NULL,
pegtxid varchar(65) NOT NULL, pegtxid varchar(65) NOT NULL,
pegindex int(11) NOT NULL, pegindex int(11) NOT NULL,
pegblocktime int(11) unsigned NOT NULL,
PRIMARY KEY (txid, txindex), PRIMARY KEY (txid, txindex),
FOREIGN KEY (bitcoinaddress) REFERENCES federation_addresses (bitcoinaddress) FOREIGN KEY (bitcoinaddress) REFERENCES federation_addresses (bitcoinaddress)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;

View file

@ -96,8 +96,8 @@ class ElementsParser {
logger.debug(`Saved new Federation address ${bitcoinaddress} to federation addresses.`); logger.debug(`Saved new Federation address ${bitcoinaddress} to federation addresses.`);
// Add the UTXO to the federation txos table // Add the UTXO to the federation txos table
const query_utxos = `INSERT IGNORE INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, pegtxid, pegindex) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; const query_utxos = `INSERT IGNORE INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, pegtxid, pegindex, pegblocktime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
const params_utxos: (string | number)[] = [bitcointxid, bitcoinindex, bitcoinaddress, amount, bitcoinblock, bitcoinBlockTime, 1, bitcoinblock - 1, 0, txid, txindex]; const params_utxos: (string | number)[] = [bitcointxid, bitcoinindex, bitcoinaddress, amount, bitcoinblock, bitcoinBlockTime, 1, bitcoinblock - 1, 0, txid, txindex, blockTime];
await DB.query(query_utxos, params_utxos); await DB.query(query_utxos, params_utxos);
const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`) const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`)
await this.$saveLastBlockAuditToDatabase(minBlockUpdate[0]['lastblockupdate']); await this.$saveLastBlockAuditToDatabase(minBlockUpdate[0]['lastblockupdate']);
@ -148,7 +148,7 @@ class ElementsParser {
while (auditProgress.lastBlockAudit <= auditProgress.confirmedTip) { while (auditProgress.lastBlockAudit <= auditProgress.confirmedTip) {
// First, get the current UTXOs that need to be scanned in the block // First, get the current UTXOs that need to be scanned in the block
const utxos = await this.$getFederationUtxosToScan(auditProgress.lastBlockAudit); const utxos = await this.$getFederationUtxosToScan(auditProgress.lastBlockAudit);
logger.debug(`Found ${utxos.length} Federation UTXOs to scan in block ${auditProgress.lastBlockAudit} / ${auditProgress.confirmedTip}`); logger.debug(`Found ${utxos.length} Federation UTXOs to scan in Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip}`);
// The fast way: check if these UTXOs are still unspent as of the current block with gettxout // The fast way: check if these UTXOs are still unspent as of the current block with gettxout
let spentAsTip: any[]; let spentAsTip: any[];
@ -228,8 +228,8 @@ class ElementsParser {
// Check that the UTXO was not already added in the DB by previous scans // Check that the UTXO was not already added in the DB by previous scans
const [rows_check] = await DB.query(`SELECT txid FROM federation_txos WHERE txid = ? AND txindex = ?`, [tx.txid, output.n]) as any[]; const [rows_check] = await DB.query(`SELECT txid FROM federation_txos WHERE txid = ? AND txindex = ?`, [tx.txid, output.n]) as any[];
if (rows_check.length === 0) { if (rows_check.length === 0) {
const query_utxos = `INSERT INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, pegtxid, pegindex) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; const query_utxos = `INSERT INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, pegtxid, pegindex, pegblocktime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
const params_utxos: (string | number)[] = [tx.txid, output.n, output.scriptPubKey.address, output.value * 100000000, block.height, block.time, 1, block.height, 0, '', 0]; const params_utxos: (string | number)[] = [tx.txid, output.n, output.scriptPubKey.address, output.value * 100000000, block.height, block.time, 1, block.height, 0, '', 0, 0];
await DB.query(query_utxos, params_utxos); await DB.query(query_utxos, params_utxos);
// Add the UTXO to the utxo array // Add the UTXO to the utxo array
spentAsTip.push({ spentAsTip.push({
@ -348,7 +348,7 @@ class ElementsParser {
// Get all of the UTXOs held by the federation, most recent first // Get all of the UTXOs held by the federation, most recent first
public async $getFederationUtxos(): Promise<any> { public async $getFederationUtxos(): Promise<any> {
const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, pegtxid, pegindex FROM federation_txos WHERE unspent = 1 ORDER BY blocktime DESC;`; const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, pegtxid, pegindex, pegblocktime FROM federation_txos WHERE unspent = 1 ORDER BY blocktime DESC;`;
const [rows] = await DB.query(query); const [rows] = await DB.query(query);
return rows; return rows;
} }

View file

@ -3,7 +3,7 @@
<div class="fee-estimation-container"> <div class="fee-estimation-container">
<div class="item"> <div class="item">
<a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]"> <a class="title-link" [routerLink]="['/audit/wallet/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> <h5 class="card-title" i18n="liquid.federation-wallet">Liquid Federation Wallet <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a> </a>
<div class="card-text"> <div class="card-text">
<div class="fee-text">{{ federationAddresses.length }} <span i18n="liquid.addresses">addresses</span></div> <div class="fee-text">{{ federationAddresses.length }} <span i18n="liquid.addresses">addresses</span></div>
@ -19,7 +19,7 @@
<div class="fee-estimation-container loading-container"> <div class="fee-estimation-container loading-container">
<div class="item"> <div class="item">
<a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]"> <a class="title-link" [routerLink]="['/audit/wallet/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> <h5 class="card-title" i18n="liquid.federation-wallet">Liquid Federation Wallet <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a> </a>
<div class="card-text"> <div class="card-text">
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>

View file

@ -1,14 +1,5 @@
<div [ngClass]="{'widget': widget}"> <div [ngClass]="{'widget': widget}">
<div *ngIf="!widget" class="form-check">
<div style="padding-left: 0.75rem;">
<input style="margin-top: 6px" class="form-check-input" type="checkbox" [checked]="showChangeUtxosToggle$ | async" id="show-change-utxos" (change)="onShowChangeUtxosToggleChange($event)">
<label class="form-check-label" for="show-change-utxos">
<small i18n="liquid.include-change-utxos">Include Change UTXOs</small>
</label>
</div>
</div>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div> <div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div> <div class="clearfix"></div>
@ -22,7 +13,7 @@
<th class="pegin text-left" *ngIf="!widget" i18n="liquid.related-peg-in">Related Peg-In</th> <th class="pegin text-left" *ngIf="!widget" i18n="liquid.related-peg-in">Related Peg-In</th>
<th class="timestamp text-right" i18n="latest-blocks.date" [ngClass]="{'widget': widget}">Date</th> <th class="timestamp text-right" i18n="latest-blocks.date" [ngClass]="{'widget': widget}">Date</th>
</thead> </thead>
<tbody *ngIf="filteredFederationUtxos$ | async as utxos; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> <tbody *ngIf="federationUtxos$ | async as utxos; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<ng-container *ngIf="widget; else regularRows"> <ng-container *ngIf="widget; else regularRows">
<tr *ngFor="let utxo of utxos | slice:0:6"> <tr *ngFor="let utxo of utxos | slice:0:6">
<td class="txid text-left widget"> <td class="txid text-left widget">
@ -55,7 +46,7 @@
</td> </td>
<td class="pegin text-left"> <td class="pegin text-left">
<ng-container *ngIf="utxo.pegtxid; else noPeginMessage"> <ng-container *ngIf="utxo.pegtxid; else noPeginMessage">
<a [routerLink]="['/tx' | relativeUrl, utxo.pegtxid + ':' + utxo.pegindex]"> <a [routerLink]="['/tx' | relativeUrl, utxo.pegtxid]" [fragment]="'vin=' + utxo.pegindex">
<app-truncate [text]="utxo.pegtxid + ':' + utxo.pegindex" [lastChars]="6"></app-truncate> <app-truncate [text]="utxo.pegtxid + ':' + utxo.pegindex" [lastChars]="6"></app-truncate>
</a> </a>
</ng-container> </ng-container>
@ -106,7 +97,7 @@
</ng-template> </ng-template>
</table> </table>
<ngb-pagination *ngIf="!widget && filteredFederationUtxos$ | async as utxos" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''" <ngb-pagination *ngIf="!widget && federationUtxos$ | async as utxos" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
[collectionSize]="utxos.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page" [collectionSize]="utxos.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false"> (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
</ngb-pagination> </ngb-pagination>

View file

@ -1,5 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { BehaviorSubject, Observable, Subject, combineLatest, of, timer } from 'rxjs'; import { Observable, Subject, combineLatest, of, timer } from 'rxjs';
import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service'; import { ApiService } from '../../../services/api.service';
import { Env, StateService } from '../../../services/state.service'; import { Env, StateService } from '../../../services/state.service';
@ -22,12 +22,8 @@ export class FederationUtxosListComponent implements OnInit {
pageSize = 15; pageSize = 15;
maxSize = window.innerWidth <= 767.98 ? 3 : 5; maxSize = window.innerWidth <= 767.98 ? 3 : 5;
skeletonLines: number[] = []; skeletonLines: number[] = [];
changeAddress: string = "bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4";
auditStatus$: Observable<AuditStatus>; auditStatus$: Observable<AuditStatus>;
auditUpdated$: Observable<boolean>; auditUpdated$: Observable<boolean>;
showChangeUtxosToggleSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
showChangeUtxosToggle$: Observable<boolean> = this.showChangeUtxosToggleSubject.asObservable();
filteredFederationUtxos$: Observable<FederationUtxo[]>;
lastReservesBlockUpdate: number = 0; lastReservesBlockUpdate: number = 0;
currentPeg$: Observable<CurrentPegs>; currentPeg$: Observable<CurrentPegs>;
lastPegBlockUpdate: number = 0; lastPegBlockUpdate: number = 0;
@ -99,17 +95,6 @@ export class FederationUtxosListComponent implements OnInit {
share() share()
); );
} }
if (this.federationUtxos$) {
this.filteredFederationUtxos$ = combineLatest([
this.federationUtxos$,
this.showChangeUtxosToggle$
]).pipe(
switchMap(([federationUtxos, showChangeUtxosToggle]) => showChangeUtxosToggle ? of(federationUtxos) : of(federationUtxos.filter(utxo => utxo.bitcoinaddress !== this.changeAddress))),
share()
);
}
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -121,7 +106,4 @@ export class FederationUtxosListComponent implements OnInit {
this.page = page; this.page = page;
} }
onShowChangeUtxosToggleChange(e): void {
this.showChangeUtxosToggleSubject.next(e.target.checked);
}
} }

View file

@ -1,7 +0,0 @@
<div class="fee-estimation-container">
<div class="item">
<a class="title-link" [routerLink]="['/audit/wallet/utxos' | relativeUrl]">
<h5 class="card-title" i18n="liquid.recent-peg-ins">Recent Peg-Ins <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
</div>
</div>

View file

@ -1,15 +0,0 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
@Component({
selector: 'app-federation-utxos-stats',
templateUrl: './federation-utxos-stats.component.html',
styleUrls: ['./federation-utxos-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FederationUtxosStatsComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View file

@ -0,0 +1,125 @@
<div class="container-xl">
<div [ngClass]="{'widget': widget}">
<div *ngIf="!widget">
<h1 i18n="liquid.recent-pegs">Recent Peg-In / Out's</h1>
</div>
<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="transaction text-left" [ngClass]="{'widget': widget}" i18n="shared.transaction">Transaction</th>
<th class="amount text-right" [ngClass]="{'widget': widget}" i18n="liquid.amount">Amount</th>
<th class="output text-left" *ngIf="!widget" i18n="liquid.bitcoin-funding-redeem">BTC Funding / Redeem</th>
<th class="timestamp text-right" i18n="latest-blocks.date" [ngClass]="{'widget': widget}">Date</th>
</thead>
<tbody *ngIf="recentPegs$ | async as pegs; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<ng-container *ngIf="widget; else regularRows">
<tr *ngFor="let peg of pegs | slice:0:6">
<td class="transaction text-left widget">
<ng-container *ngIf="peg.amount > 0">
<a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex">
<app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate>
</a>
</ng-container>
<ng-container *ngIf="peg.amount < 0">
<a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex">
<app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate>
</a>
</ng-container>
</td>
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0}">
{{ peg.amount > 0 ? '+' : '-' }}<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
</td>
<td class="timestamp text-right widget">
<app-time kind="since" [time]="peg.blocktime"></app-time>
</td>
</tr>
</ng-container>
<ng-template #regularRows>
<tr *ngFor="let peg of pegs | slice:(page - 1) * pageSize:page * pageSize">
<td class="transaction text-left">
<ng-container *ngIf="peg.amount > 0">
<a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex">
<app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate>
</a>
</ng-container>
<ng-container *ngIf="peg.amount < 0">
<a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex">
<app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate>
</a>
</ng-container>
</td>
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0}">
{{ peg.amount > 0 ? '+' : '-' }}<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
</td>
<td class="output text-left">
<ng-container *ngIf="peg.bitcointxid; else noPeginMessage">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + peg.bitcointxid + ':' + peg.bitcoinindex }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="peg.bitcointxid + ':' + peg.bitcoinindex" [lastChars]="6"></app-truncate>
</a>
</ng-container>
</td>
<td class="timestamp text-right">
&lrm;{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="peg.blocktime"></app-time>)</i></div>
</td>
</tr>
</ng-template>
</tbody>
<ng-template #skeleton>
<tbody *ngIf="widget; else regularRowsSkeleton">
<tr *ngFor="let item of skeletonLines">
<td class="transaction 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: 300px"></span>
</td>
<td class="timestamp text-right widget">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
</tr>
</tbody>
<ng-template #regularRowsSkeleton>
<tr *ngFor="let item of skeletonLines">
<td class="transaction text-left">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="amount text-right">
<span class="skeleton-loader" style="max-width: 140px"></span>
</td>
<td class="output text-left">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="timestamp text-right">
<span class="skeleton-loader" style="max-width: 140px"></span>
</td>
</tr>
</ng-template>
</ng-template>
</table>
<ngb-pagination *ngIf="!widget && recentPegs$ | async as pegs" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
[collectionSize]="pegs.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>
</div>
<br>
<ng-template #noPeginMessage>
<span class="text-muted">BTC Redeem in progress...</span>
</ng-template>

View file

@ -0,0 +1,101 @@
.spinner-border {
height: 25px;
width: 25px;
margin-top: 13px;
}
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;
}
.transaction {
width: 25%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
}
.transaction.widget {
width: 40%;
}
.address {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
@media (max-width: 527px) {
display: none;
}
}
.amount {
width: 12%;
}
.amount.widget {
width: 30%;
}
.output {
width: 25%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
@media (max-width: 840px) {
display: none;
}
}
.timestamp {
width: 18%;
@media (max-width: 650px) {
display: none;
}
@media (max-width: 1000px) {
.relative-time {
display: none;
}
}
}
.timestamp.widget {
width: 100%;
@media (min-width: 768px) AND (max-width: 1050px) {
display: none;
}
@media (max-width: 767px) {
display: block;
}
@media (max-width: 500px) {
display: none;
}
}
.credit {
color: #7CB342;
}
.debit {
color: #D81B60;
}

View file

@ -0,0 +1,127 @@
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { Observable, Subject, combineLatest, of, timer } from 'rxjs';
import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service';
import { Env, StateService } from '../../../services/state.service';
import { AuditStatus, CurrentPegs, FederationUtxo, RecentPeg } from '../../../interfaces/node-api.interface';
import { WebsocketService } from '../../../services/websocket.service';
@Component({
selector: 'app-recent-pegs-list',
templateUrl: './recent-pegs-list.component.html',
styleUrls: ['./recent-pegs-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RecentPegsListComponent implements OnInit {
@Input() widget: boolean = false;
@Input() recentPegIns$: Observable<RecentPeg[]>;
env: Env;
isLoading = true;
page = 1;
pageSize = 15;
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
skeletonLines: number[] = [];
auditStatus$: Observable<AuditStatus>;
auditUpdated$: Observable<boolean>;
federationUtxos$: Observable<FederationUtxo[]>;
recentPegs$: Observable<RecentPeg[]>;
lastReservesBlockUpdate: number = 0;
currentPeg$: Observable<CurrentPegs>;
lastPegBlockUpdate: number = 0;
lastPegAmount: string = '';
isLoad: boolean = true;
private destroy$ = new Subject();
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(6).keys()] : [...Array(15).keys()];
if (!this.widget) {
this.websocketService.want(['blocks']);
this.auditStatus$ = this.stateService.blocks$.pipe(
takeUntil(this.destroy$),
throttleTime(40000),
delayWhen(_ => this.isLoad ? timer(0) : timer(2000)),
tap(() => this.isLoad = false),
switchMap(() => this.apiService.federationAuditSynced$()),
shareReplay(1)
);
this.currentPeg$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
switchMap(_ =>
this.apiService.liquidPegs$().pipe(
filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate),
tap((currentPegs) => {
this.lastPegBlockUpdate = currentPegs.lastBlockUpdate;
})
)
),
share()
);
this.auditUpdated$ = combineLatest([
this.auditStatus$,
this.currentPeg$
]).pipe(
filter(([auditStatus, _]) => auditStatus.isAuditSynced === true),
map(([auditStatus, currentPeg]) => ({
lastBlockAudit: auditStatus.lastBlockAudit,
currentPegAmount: currentPeg.amount
})),
switchMap(({ lastBlockAudit, currentPegAmount }) => {
const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate;
const amountCheck = currentPegAmount !== this.lastPegAmount;
this.lastReservesBlockUpdate = lastBlockAudit;
this.lastPegAmount = currentPegAmount;
return of(blockAuditCheck || amountCheck);
}),
share()
);
this.federationUtxos$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.federationUtxos$()),
tap(_ => this.isLoading = false),
share()
);
this.recentPegIns$ = this.federationUtxos$.pipe(
map(federationUtxos => federationUtxos.filter(utxo => utxo.pegtxid).map(utxo => {
return {
txid: utxo.pegtxid,
txindex: utxo.pegindex,
amount: utxo.amount,
bitcointxid: utxo.txid,
bitcoinindex: utxo.txindex,
blocktime: utxo.pegblocktime,
}
})),
share()
);
}
this.recentPegs$ = this.recentPegIns$;
}
ngOnDestroy(): void {
this.destroy$.next(1);
this.destroy$.complete();
}
pageChange(page: number): void {
this.page = page;
}
}

View file

@ -0,0 +1,7 @@
<div class="fee-estimation-container">
<div class="item">
<a class="title-link" [routerLink]="['/audit/pegs' | relativeUrl]">
<h5 class="card-title" i18n="liquid.recent-peg-in-out">Recent Peg-In / Out's <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
</div>
</div>

View file

@ -0,0 +1,15 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
@Component({
selector: 'app-recent-pegs-stats',
templateUrl: './recent-pegs-stats.component.html',
styleUrls: ['./recent-pegs-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RecentPegsStatsComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View file

@ -25,8 +25,8 @@
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<app-federation-utxos-stats></app-federation-utxos-stats> <app-recent-pegs-stats></app-recent-pegs-stats>
<app-federation-utxos-list [federationUtxos$]="federationUtxos$" [widget]="true"></app-federation-utxos-list> <app-recent-pegs-list [recentPegIns$]="recentPegIns$" [widget]="true"></app-recent-pegs-list>
</div> </div>
</div> </div>
</div> </div>
@ -71,8 +71,8 @@
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<app-federation-utxos-stats></app-federation-utxos-stats> <app-recent-pegs-stats></app-recent-pegs-stats>
<app-federation-utxos-list [widget]="true"></app-federation-utxos-list> <app-recent-pegs-list [widget]="true"></app-recent-pegs-list>
</div> </div>
</div> </div>
</div> </div>

View file

@ -4,7 +4,7 @@ import { WebsocketService } from '../../../services/websocket.service';
import { StateService } from '../../../services/state.service'; import { StateService } from '../../../services/state.service';
import { Observable, Subject, combineLatest, delayWhen, filter, interval, map, of, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime, timer } from 'rxjs'; import { Observable, Subject, combineLatest, delayWhen, filter, interval, map, of, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime, timer } from 'rxjs';
import { ApiService } from '../../../services/api.service'; import { ApiService } from '../../../services/api.service';
import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo } from '../../../interfaces/node-api.interface'; import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo, RecentPeg } from '../../../interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-reserves-audit-dashboard', selector: 'app-reserves-audit-dashboard',
@ -18,6 +18,7 @@ export class ReservesAuditDashboardComponent implements OnInit {
currentPeg$: Observable<CurrentPegs>; currentPeg$: Observable<CurrentPegs>;
currentReserves$: Observable<CurrentPegs>; currentReserves$: Observable<CurrentPegs>;
federationUtxos$: Observable<FederationUtxo[]>; federationUtxos$: Observable<FederationUtxo[]>;
recentPegIns$: Observable<RecentPeg[]>;
federationAddresses$: Observable<FederationAddress[]>; federationAddresses$: Observable<FederationAddress[]>;
federationAddressesOneMonthAgo$: Observable<any>; federationAddressesOneMonthAgo$: Observable<any>;
liquidPegsMonth$: Observable<any>; liquidPegsMonth$: Observable<any>;
@ -103,6 +104,20 @@ export class ReservesAuditDashboardComponent implements OnInit {
share() share()
); );
this.recentPegIns$ = this.federationUtxos$.pipe(
map(federationUtxos => federationUtxos.filter(utxo => utxo.pegtxid).map(utxo => {
return {
txid: utxo.pegtxid,
txindex: utxo.pegindex,
amount: utxo.amount,
bitcointxid: utxo.txid,
bitcoinindex: utxo.txindex,
blocktime: utxo.pegblocktime,
}
})),
share()
);
this.federationAddresses$ = this.auditUpdated$.pipe( this.federationAddresses$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true), filter(auditUpdated => auditUpdated === true),
throttleTime(40000), throttleTime(40000),

View file

@ -96,6 +96,16 @@ export interface FederationUtxo {
blocktime: number; blocktime: number;
pegtxid: string; pegtxid: string;
pegindex: number; pegindex: number;
pegblocktime: number;
}
export interface RecentPeg {
txid: string;
txindex: number; // input #0 for peg-ins
amount: number;
bitcointxid: string;
bitcoinindex: number;
blocktime: number;
} }
export interface AuditStatus { export interface AuditStatus {

View file

@ -17,7 +17,8 @@ import { AssetComponent } from '../components/asset/asset.component';
import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component'; import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component';
import { ReservesAuditDashboardComponent } from '../components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component'; import { ReservesAuditDashboardComponent } from '../components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component';
import { ReservesSupplyStatsComponent } from '../components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component'; import { 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 { RecentPegsStatsComponent } from '../components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component';
import { RecentPegsListComponent } from '../components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component';
import { FederationWalletComponent } from '../components/liquid-reserves-audit/federation-wallet/federation-wallet.component'; import { FederationWalletComponent } from '../components/liquid-reserves-audit/federation-wallet/federation-wallet.component';
import { FederationUtxosListComponent } from '../components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component'; import { FederationUtxosListComponent } from '../components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component';
import { FederationAddressesStatsComponent } from '../components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component'; import { FederationAddressesStatsComponent } from '../components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component';
@ -109,6 +110,11 @@ const routes: Routes = [
} }
] ]
}, },
{
path: 'audit/pegs',
data: { networks: ['liquid'] },
component: RecentPegsListComponent,
},
{ {
path: 'assets', path: 'assets',
data: { networks: ['liquid'] }, data: { networks: ['liquid'] },
@ -176,7 +182,8 @@ export class LiquidRoutingModule { }
LiquidMasterPageComponent, LiquidMasterPageComponent,
ReservesAuditDashboardComponent, ReservesAuditDashboardComponent,
ReservesSupplyStatsComponent, ReservesSupplyStatsComponent,
FederationUtxosStatsComponent, RecentPegsStatsComponent,
RecentPegsListComponent,
FederationWalletComponent, FederationWalletComponent,
FederationUtxosListComponent, FederationUtxosListComponent,
FederationAddressesStatsComponent, FederationAddressesStatsComponent,