Liquid audit: Add recent pegs widget and table
This commit is contained in:
		
							parent
							
								
									451a61e5fc
								
							
						
					
					
						commit
						639fc3dd5f
					
				@ -847,6 +847,7 @@ class DatabaseMigration {
 | 
			
		||||
      lasttimeupdate int(11) unsigned NOT NULL,
 | 
			
		||||
      pegtxid varchar(65) NOT NULL,
 | 
			
		||||
      pegindex int(11) NOT NULL,
 | 
			
		||||
      pegblocktime int(11) unsigned NOT NULL,
 | 
			
		||||
      PRIMARY KEY (txid, txindex), 
 | 
			
		||||
      FOREIGN KEY (bitcoinaddress) REFERENCES federation_addresses (bitcoinaddress)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
 | 
			
		||||
@ -96,8 +96,8 @@ class ElementsParser {
 | 
			
		||||
      logger.debug(`Saved new Federation address ${bitcoinaddress} to federation addresses.`);
 | 
			
		||||
 | 
			
		||||
      // 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 params_utxos: (string | number)[] = [bitcointxid, bitcoinindex, bitcoinaddress, amount, bitcoinblock, bitcoinBlockTime, 1, bitcoinblock - 1, 0, txid, txindex];
 | 
			
		||||
      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, blockTime];
 | 
			
		||||
      await DB.query(query_utxos, params_utxos);
 | 
			
		||||
      const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`)
 | 
			
		||||
      await this.$saveLastBlockAuditToDatabase(minBlockUpdate[0]['lastblockupdate']);
 | 
			
		||||
@ -148,7 +148,7 @@ class ElementsParser {
 | 
			
		||||
      while (auditProgress.lastBlockAudit <= auditProgress.confirmedTip) {
 | 
			
		||||
        // First, get the current UTXOs that need to be scanned in the block
 | 
			
		||||
        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
 | 
			
		||||
        let spentAsTip: any[];
 | 
			
		||||
@ -228,8 +228,8 @@ class ElementsParser {
 | 
			
		||||
          // 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[];
 | 
			
		||||
          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 params_utxos: (string | number)[] = [tx.txid, output.n, output.scriptPubKey.address, output.value * 100000000, block.height, block.time, 1, block.height, 0, '', 0];
 | 
			
		||||
            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, 0];
 | 
			
		||||
            await DB.query(query_utxos, params_utxos);
 | 
			
		||||
            // Add the UTXO to the utxo array
 | 
			
		||||
            spentAsTip.push({
 | 
			
		||||
@ -348,7 +348,7 @@ class ElementsParser {
 | 
			
		||||
 | 
			
		||||
  // Get all of the UTXOs held by the federation, most recent first
 | 
			
		||||
  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);
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
    <div class="fee-estimation-container">
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <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>
 | 
			
		||||
        <div class="card-text">
 | 
			
		||||
          <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="item">
 | 
			
		||||
      <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>
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,5 @@
 | 
			
		||||
<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 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="timestamp text-right" i18n="latest-blocks.date" [ngClass]="{'widget': widget}">Date</th>
 | 
			
		||||
      </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">
 | 
			
		||||
          <tr *ngFor="let utxo of utxos | slice:0:6">
 | 
			
		||||
            <td class="txid text-left widget">
 | 
			
		||||
@ -55,7 +46,7 @@
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="pegin text-left">
 | 
			
		||||
              <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>
 | 
			
		||||
                </a>
 | 
			
		||||
              </ng-container>
 | 
			
		||||
@ -106,7 +97,7 @@
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </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"
 | 
			
		||||
      (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
 | 
			
		||||
    </ngb-pagination>
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
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 { ApiService } from '../../../services/api.service';
 | 
			
		||||
import { Env, StateService } from '../../../services/state.service';
 | 
			
		||||
@ -22,12 +22,8 @@ export class FederationUtxosListComponent implements OnInit {
 | 
			
		||||
  pageSize = 15;
 | 
			
		||||
  maxSize = window.innerWidth <= 767.98 ? 3 : 5;
 | 
			
		||||
  skeletonLines: number[] = [];
 | 
			
		||||
  changeAddress: string = "bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4";
 | 
			
		||||
  auditStatus$: Observable<AuditStatus>;
 | 
			
		||||
  auditUpdated$: Observable<boolean>;
 | 
			
		||||
  showChangeUtxosToggleSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
 | 
			
		||||
  showChangeUtxosToggle$: Observable<boolean> = this.showChangeUtxosToggleSubject.asObservable();
 | 
			
		||||
  filteredFederationUtxos$: Observable<FederationUtxo[]>;
 | 
			
		||||
  lastReservesBlockUpdate: number = 0;
 | 
			
		||||
  currentPeg$: Observable<CurrentPegs>;
 | 
			
		||||
  lastPegBlockUpdate: number = 0;
 | 
			
		||||
@ -99,17 +95,6 @@ export class FederationUtxosListComponent implements OnInit {
 | 
			
		||||
        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 {
 | 
			
		||||
@ -121,7 +106,4 @@ export class FederationUtxosListComponent implements OnInit {
 | 
			
		||||
    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="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <app-federation-utxos-stats></app-federation-utxos-stats>
 | 
			
		||||
          <app-federation-utxos-list [federationUtxos$]="federationUtxos$" [widget]="true"></app-federation-utxos-list>
 | 
			
		||||
          <app-recent-pegs-stats></app-recent-pegs-stats>
 | 
			
		||||
          <app-recent-pegs-list [recentPegIns$]="recentPegIns$" [widget]="true"></app-recent-pegs-list>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@ -71,8 +71,8 @@
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <div class="card">
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <app-federation-utxos-stats></app-federation-utxos-stats>
 | 
			
		||||
            <app-federation-utxos-list [widget]="true"></app-federation-utxos-list>
 | 
			
		||||
            <app-recent-pegs-stats></app-recent-pegs-stats>
 | 
			
		||||
            <app-recent-pegs-list [widget]="true"></app-recent-pegs-list>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import { WebsocketService } from '../../../services/websocket.service';
 | 
			
		||||
import { StateService } from '../../../services/state.service';
 | 
			
		||||
import { Observable, Subject, combineLatest, delayWhen, filter, interval, map, of, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime, timer } from 'rxjs';
 | 
			
		||||
import { ApiService } from '../../../services/api.service';
 | 
			
		||||
import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo } from '../../../interfaces/node-api.interface';
 | 
			
		||||
import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo, RecentPeg } from '../../../interfaces/node-api.interface';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-reserves-audit-dashboard',
 | 
			
		||||
@ -18,6 +18,7 @@ export class ReservesAuditDashboardComponent implements OnInit {
 | 
			
		||||
  currentPeg$: Observable<CurrentPegs>;
 | 
			
		||||
  currentReserves$: Observable<CurrentPegs>;
 | 
			
		||||
  federationUtxos$: Observable<FederationUtxo[]>;
 | 
			
		||||
  recentPegIns$: Observable<RecentPeg[]>;
 | 
			
		||||
  federationAddresses$: Observable<FederationAddress[]>;
 | 
			
		||||
  federationAddressesOneMonthAgo$: Observable<any>;
 | 
			
		||||
  liquidPegsMonth$: Observable<any>;
 | 
			
		||||
@ -103,6 +104,20 @@ export class ReservesAuditDashboardComponent implements OnInit {
 | 
			
		||||
      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(
 | 
			
		||||
      filter(auditUpdated => auditUpdated === true),
 | 
			
		||||
      throttleTime(40000),
 | 
			
		||||
 | 
			
		||||
@ -96,6 +96,16 @@ export interface FederationUtxo {
 | 
			
		||||
  blocktime: number;
 | 
			
		||||
  pegtxid: string;
 | 
			
		||||
  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 {
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,8 @@ import { AssetComponent } from '../components/asset/asset.component';
 | 
			
		||||
import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component';
 | 
			
		||||
import { ReservesAuditDashboardComponent } from '../components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component';
 | 
			
		||||
import { ReservesSupplyStatsComponent } from '../components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component';
 | 
			
		||||
import { 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 { 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';
 | 
			
		||||
@ -109,6 +110,11 @@ const routes: Routes = [
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'audit/pegs',
 | 
			
		||||
        data: { networks: ['liquid'] },
 | 
			
		||||
        component: RecentPegsListComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'assets',
 | 
			
		||||
        data: { networks: ['liquid'] },
 | 
			
		||||
@ -176,7 +182,8 @@ export class LiquidRoutingModule { }
 | 
			
		||||
    LiquidMasterPageComponent,
 | 
			
		||||
    ReservesAuditDashboardComponent,
 | 
			
		||||
    ReservesSupplyStatsComponent,
 | 
			
		||||
    FederationUtxosStatsComponent,
 | 
			
		||||
    RecentPegsStatsComponent,
 | 
			
		||||
    RecentPegsListComponent,
 | 
			
		||||
    FederationWalletComponent,
 | 
			
		||||
    FederationUtxosListComponent,
 | 
			
		||||
    FederationAddressesStatsComponent,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user