mirror of
https://github.com/mempool/mempool.git
synced 2025-02-24 06:47:52 +01:00
Liquid audit: Add recent pegs widget and table
This commit is contained in:
parent
451a61e5fc
commit
639fc3dd5f
17 changed files with 428 additions and 69 deletions
|
@ -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;`;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,13 +1,4 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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">
|
||||||
|
‎{{ 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>
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>;
|
||||||
|
@ -72,7 +73,7 @@ export class ReservesAuditDashboardComponent implements OnInit {
|
||||||
map(([auditStatus, currentPeg]) => ({
|
map(([auditStatus, currentPeg]) => ({
|
||||||
lastBlockAudit: auditStatus.lastBlockAudit,
|
lastBlockAudit: auditStatus.lastBlockAudit,
|
||||||
currentPegAmount: currentPeg.amount
|
currentPegAmount: currentPeg.amount
|
||||||
})),
|
})),
|
||||||
switchMap(({ lastBlockAudit, currentPegAmount }) => {
|
switchMap(({ lastBlockAudit, currentPegAmount }) => {
|
||||||
const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate;
|
const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate;
|
||||||
const amountCheck = currentPegAmount !== this.lastPegAmount;
|
const amountCheck = currentPegAmount !== this.lastPegAmount;
|
||||||
|
@ -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),
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue