Add custom dashboard component
This commit is contained in:
		
							parent
							
								
									ac0f56325b
								
							
						
					
					
						commit
						4f4215577a
					
				@ -1,14 +1,14 @@
 | 
			
		||||
<app-indexing-progress></app-indexing-progress>
 | 
			
		||||
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
 | 
			
		||||
 | 
			
		||||
<div class="full-container">
 | 
			
		||||
  <div class="card-header mb-0 mb-md-2">
 | 
			
		||||
<div [class.full-container]="!widget">
 | 
			
		||||
  <div *ngIf="!widget" class="card-header mb-0 mb-md-2">
 | 
			
		||||
    <div class="d-flex d-md-block align-items-baseline">
 | 
			
		||||
      <span i18n="address.balance-history">Balance History</span>
 | 
			
		||||
    </div>  
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngIf="!error">
 | 
			
		||||
    <div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
 | 
			
		||||
    <div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
 | 
			
		||||
      (chartInit)="onChartInit($event)">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
@ -20,4 +20,8 @@
 | 
			
		||||
      <p class="error">{{ error }}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
  <div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
 | 
			
		||||
    <div class="spinner-border text-light"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-address-graph',
 | 
			
		||||
@ -26,8 +27,10 @@ export class AddressGraphComponent implements OnChanges {
 | 
			
		||||
  @Input() address: string;
 | 
			
		||||
  @Input() isPubkey: boolean = false;
 | 
			
		||||
  @Input() stats: ChainStats;
 | 
			
		||||
  @Input() height: number = 200;
 | 
			
		||||
  @Input() right: number | string = 10;
 | 
			
		||||
  @Input() left: number | string = 70;
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
 | 
			
		||||
  data: any[] = [];
 | 
			
		||||
  hoverData: any[] = [];
 | 
			
		||||
@ -43,6 +46,7 @@ export class AddressGraphComponent implements OnChanges {
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private amountShortenerPipe: AmountShortenerPipe,
 | 
			
		||||
@ -52,6 +56,9 @@ export class AddressGraphComponent implements OnChanges {
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(changes: SimpleChanges): void {
 | 
			
		||||
    this.isLoading = true;
 | 
			
		||||
    if (!this.address || !this.stats) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    (this.isPubkey
 | 
			
		||||
      ? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
 | 
			
		||||
      : this.electrsApiService.getAddressSummary$(this.address)).pipe(
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,59 @@
 | 
			
		||||
<div class="card">
 | 
			
		||||
  <div class="card-body more-padding">
 | 
			
		||||
    <div class="balance-container" *ngIf="!isLoading; else loading">
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
 | 
			
		||||
        <div class="card-text">
 | 
			
		||||
          {{ ((addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="symbol">
 | 
			
		||||
          <app-fiat [value]="(addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum)"></app-fiat>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <h5 class="card-title" i18n="dashboard.7d-change">Change (7d)</h5>
 | 
			
		||||
        <div class="card-text">
 | 
			
		||||
          {{ delta7d > 0 ? '+' : ''}}{{ ((delta7d) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="symbol">
 | 
			
		||||
          <app-fiat [value]="delta7d"></app-fiat>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <h5 class="card-title" i18n="dashboard.30d-change">Change (30d)</h5>
 | 
			
		||||
        <div class="card-text">
 | 
			
		||||
          {{ delta30d > 0 ? '+' : ''}}{{ ((delta30d) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="symbol">
 | 
			
		||||
          <app-fiat [value]="delta30d"></app-fiat>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #loading>
 | 
			
		||||
  <div class="balance-skeleton">
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="dashboard.7d-change">Change (7d)</h5>
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="dashboard.30d-change">Change (30d)</h5>
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,160 @@
 | 
			
		||||
.balance-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  justify-content: space-around;
 | 
			
		||||
  height: 76px;
 | 
			
		||||
  .shared-block {
 | 
			
		||||
    color: var(--transparent-fg);
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
  }
 | 
			
		||||
  .item {
 | 
			
		||||
    padding: 0 5px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      display: none;
 | 
			
		||||
      @media (min-width: 485px) {
 | 
			
		||||
        display: table-cell;
 | 
			
		||||
      }
 | 
			
		||||
      @media (min-width: 768px) {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
      @media (min-width: 992px) {
 | 
			
		||||
        display: table-cell;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .card-text {
 | 
			
		||||
    font-size: 22px;
 | 
			
		||||
    margin-top: -9px;
 | 
			
		||||
    position: relative;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.balance-skeleton {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  @media (min-width: 376px) {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
  .item {
 | 
			
		||||
    min-width: 120px;
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    width: -webkit-fill-available;
 | 
			
		||||
    @media (min-width: 376px) {
 | 
			
		||||
      margin: 0 auto 0px;
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child{
 | 
			
		||||
      display: none;
 | 
			
		||||
      @media (min-width: 485px) {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
      @media (min-width: 768px) {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
      @media (min-width: 992px) {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .card-text {
 | 
			
		||||
    .skeleton-loader {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      display: block;
 | 
			
		||||
      &:first-child {
 | 
			
		||||
        margin: 14px auto 0;
 | 
			
		||||
        max-width: 80px;
 | 
			
		||||
      }
 | 
			
		||||
      &:last-child {
 | 
			
		||||
        margin: 10px auto 0;
 | 
			
		||||
        max-width: 120px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card {
 | 
			
		||||
  background-color: var(--bg);
 | 
			
		||||
  height: 126px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-title {
 | 
			
		||||
  color: var(--title-fg);
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progress {
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  background-color: var(--secondary);
 | 
			
		||||
  height: 1.1rem;
 | 
			
		||||
  max-width: 180px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.skeleton-loader {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.more-padding {
 | 
			
		||||
  padding: 24px 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.small-bar {
 | 
			
		||||
  height: 8px;
 | 
			
		||||
  top: -4px;
 | 
			
		||||
  max-width: 120px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loading-container {
 | 
			
		||||
  min-height: 76px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main-title {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: #ffffff91;
 | 
			
		||||
  margin-top: -13px;
 | 
			
		||||
  font-size: 10px;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding-bottom: 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-wrapper {
 | 
			
		||||
  .card {
 | 
			
		||||
    height: auto !important;
 | 
			
		||||
  }
 | 
			
		||||
  .card-body {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex: inherit;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    justify-content: space-around;
 | 
			
		||||
    padding: 24px 20px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.retarget-sign {
 | 
			
		||||
  margin-right: -3px;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  top: -2px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.previous-retarget-sign {
 | 
			
		||||
  margin-right: -2px;
 | 
			
		||||
  font-size: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.symbol {
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,72 @@
 | 
			
		||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { catchError, of } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-balance-widget',
 | 
			
		||||
  templateUrl: './balance-widget.component.html',
 | 
			
		||||
  styleUrls: ['./balance-widget.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class BalanceWidgetComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() address: string;
 | 
			
		||||
  @Input() addressInfo: Address;
 | 
			
		||||
  @Input() isPubkey: boolean = false;
 | 
			
		||||
 | 
			
		||||
  isLoading: boolean = true;
 | 
			
		||||
  error: any;
 | 
			
		||||
 | 
			
		||||
  delta7d: number = 0;
 | 
			
		||||
  delta30d: number = 0;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private cd: ChangeDetectorRef,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(changes: SimpleChanges): void {
 | 
			
		||||
    this.isLoading = true;
 | 
			
		||||
    if (!this.address || !this.addressInfo) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    (this.isPubkey
 | 
			
		||||
      ? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
 | 
			
		||||
      : this.electrsApiService.getAddressSummary$(this.address)).pipe(
 | 
			
		||||
      catchError(e => {
 | 
			
		||||
        this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
 | 
			
		||||
        return of(null);
 | 
			
		||||
      }),
 | 
			
		||||
    ).subscribe(addressSummary => {
 | 
			
		||||
      if (addressSummary) {
 | 
			
		||||
        console.log('got address summary!');
 | 
			
		||||
        this.error = null;
 | 
			
		||||
        this.calculateStats(addressSummary);
 | 
			
		||||
      }
 | 
			
		||||
      this.isLoading = false;
 | 
			
		||||
      this.cd.markForCheck();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  calculateStats(summary: AddressTxSummary[]): void {
 | 
			
		||||
    let weekTotal = 0;
 | 
			
		||||
    let monthTotal = 0;
 | 
			
		||||
    const weekAgo = (Date.now() / 1000) - (60 * 60 * 24 * 7);
 | 
			
		||||
    const monthAgo = (Date.now() / 1000) - (60 * 60 * 24 * 30);
 | 
			
		||||
    for (let i = 0; i < summary.length && summary[i].time >= monthAgo; i++) {
 | 
			
		||||
      monthTotal += summary[i].value;
 | 
			
		||||
      if (summary[i].time >= weekAgo) {
 | 
			
		||||
        weekTotal += summary[i].value;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.delta7d = weekTotal;
 | 
			
		||||
    this.delta30d = monthTotal;
 | 
			
		||||
    console.log('calculated address stats: ', weekTotal, monthTotal);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,252 @@
 | 
			
		||||
 | 
			
		||||
<div class="container-xl dashboard-container">
 | 
			
		||||
  <div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
 | 
			
		||||
    @for (widget of widgets; track widget.component) {
 | 
			
		||||
      @switch (widget.component) {
 | 
			
		||||
        @case ('fees') {
 | 
			
		||||
          <div class="col card-wrapper">
 | 
			
		||||
            <div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
 | 
			
		||||
            <div class="card">
 | 
			
		||||
              <div class="card-body less-padding">
 | 
			
		||||
                <app-fees-box class="d-block"></app-fees-box>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @case ('difficulty') {
 | 
			
		||||
          <div class="col">
 | 
			
		||||
            <app-difficulty></app-difficulty>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @case ('goggles') {
 | 
			
		||||
          <div class="col">
 | 
			
		||||
            <div class="card graph-card">
 | 
			
		||||
              <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
 | 
			
		||||
                <a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
 | 
			
		||||
                  <h5 class="card-title d-inline"><span>Mempool Goggles™</span> : {{ goggleCycle[goggleIndex].name }}</h5>
 | 
			
		||||
                  <span> </span>
 | 
			
		||||
                  <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
 | 
			
		||||
                </a>
 | 
			
		||||
                <div class="quick-filter">
 | 
			
		||||
                  <div class="btn-group btn-group-toggle">
 | 
			
		||||
                    <label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex"  *ngFor="let filter of goggleCycle">
 | 
			
		||||
                      <input type="radio" [value]="'3m'" fragment="3m" (click)="setFilter(filter.index)" [attr.data-cy]="'3m'"> {{ filter.name }}
 | 
			
		||||
                    </label>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="mempool-block-wrapper" *ngIf="webGlEnabled">
 | 
			
		||||
                  <app-mempool-block-overview
 | 
			
		||||
                    [index]="0"
 | 
			
		||||
                    [resolution]="goggleResolution"
 | 
			
		||||
                    [filterFlags]="goggleFlags"
 | 
			
		||||
                    [filterMode]="goggleMode"
 | 
			
		||||
                    [gradientMode]="gradientMode"
 | 
			
		||||
                  ></app-mempool-block-overview>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @case ('incoming') {
 | 
			
		||||
          <div class="col">
 | 
			
		||||
            <div class="card graph-card">
 | 
			
		||||
              <div class="card-body">
 | 
			
		||||
                <ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
 | 
			
		||||
                  <h5 class="card-title mt-3" i18n="dashboard.incoming-transactions">Incoming Transactions</h5>
 | 
			
		||||
                  <div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats">
 | 
			
		||||
                    <app-incoming-transactions-graph
 | 
			
		||||
                      [height]="incomingGraphHeight"
 | 
			
		||||
                      [left]="50"
 | 
			
		||||
                      [right]="20"
 | 
			
		||||
                      [data]="mempoolStats?.weightPerSecond"
 | 
			
		||||
                      [windowPreferenceOverride]="'2h'"
 | 
			
		||||
                      ></app-incoming-transactions-graph>
 | 
			
		||||
                  </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <ng-template #mempoolTable let-mempoolInfoData>
 | 
			
		||||
            <div class="mempool-info-data">
 | 
			
		||||
              <div class="item">
 | 
			
		||||
                <h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === mempoolInfoData.value.memPoolInfo.minrelaytxfee || (stateService.env.BASE_MODULE === 'liquid' && mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.000001) else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
 | 
			
		||||
                <ng-template #purgingText><h5 class="card-title" i18n="dashboard.purging|Purgin below fee">Purging</h5></ng-template>
 | 
			
		||||
                <p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
 | 
			
		||||
                  <ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee !== mempoolInfoData.value.memPoolInfo.minrelaytxfee">< </ng-template><app-fee-rate [fee]="mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000"></app-fee-rate>
 | 
			
		||||
                </p>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="item">
 | 
			
		||||
                <h5 class="card-title" i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</h5>
 | 
			
		||||
                <p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
 | 
			
		||||
                  {{ mempoolInfoData.value.memPoolInfo.size | number }} <span i18n="dashboard.txs">TXs</span>
 | 
			
		||||
                </p>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="item bar">
 | 
			
		||||
                <h5 class="card-title" i18n="dashboard.memory-usage|Memory usage">Memory Usage</h5>
 | 
			
		||||
                <div class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loadingbig">
 | 
			
		||||
                  <div class="progress">
 | 
			
		||||
                    <div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }"> </div>
 | 
			
		||||
                    <div class="progress-text">‎<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes : 2 : 'B' : null : false : 3"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
        }
 | 
			
		||||
        @case ('replacements') {
 | 
			
		||||
          <div class="col" style="max-height: 410px">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
              <div class="card-body">
 | 
			
		||||
                <a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
 | 
			
		||||
                  <h5 class="card-title d-inline" i18n="dashboard.recent-rbf-replacements">Recent Replacements</h5>
 | 
			
		||||
                  <span> </span>
 | 
			
		||||
                  <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
 | 
			
		||||
                </a>
 | 
			
		||||
                <table class="table lastest-replacements-table">
 | 
			
		||||
                  <thead>
 | 
			
		||||
                    <th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
 | 
			
		||||
                    <th class="table-cell-old-fee" i18n="dashboard.previous-transaction-fee">Previous fee</th>
 | 
			
		||||
                    <th class="table-cell-new-fee" i18n="dashboard.new-transaction-fee">New fee</th>
 | 
			
		||||
                    <th class="table-cell-badges" i18n="transaction.status|Transaction Status">Status</th>
 | 
			
		||||
                  </thead>
 | 
			
		||||
                  <tbody *ngIf="replacements$ | async as replacements; else replacementsSkeleton">
 | 
			
		||||
                    <tr *ngFor="let replacement of replacements">
 | 
			
		||||
                      <td class="table-cell-txid">
 | 
			
		||||
                        <a [routerLink]="['/tx' | relativeUrl, replacement.txid]">
 | 
			
		||||
                          <app-truncate [text]="replacement.txid" [lastChars]="5"></app-truncate>
 | 
			
		||||
                        </a>
 | 
			
		||||
                      </td>
 | 
			
		||||
                      <td class="table-cell-old-fee"><app-fee-rate [fee]="replacement.oldFee" [weight]="replacement.oldVsize * 4"></app-fee-rate></td>
 | 
			
		||||
                      <td class="table-cell-new-fee"><app-fee-rate [fee]="replacement.newFee" [weight]="replacement.newVsize * 4"></app-fee-rate></td>
 | 
			
		||||
                      <td class="table-cell-badges">
 | 
			
		||||
                        <span *ngIf="replacement.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
 | 
			
		||||
                        <span *ngIf="replacement.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
 | 
			
		||||
                        <span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="tx-features.tag.rbf|RBF">RBF</span>
 | 
			
		||||
                      </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                  </tbody>
 | 
			
		||||
                </table>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <ng-template #replacementsSkeleton>
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr *ngFor="let i of [1,2,3,4,5,6]">
 | 
			
		||||
                <td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
 | 
			
		||||
                <td class="table-cell-old-fee"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
 | 
			
		||||
                <td class="table-cell-new-fee"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
 | 
			
		||||
                <td class="table-cell-badges"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
        }
 | 
			
		||||
        @case ('blocks') {
 | 
			
		||||
          <div class="col" style="max-height: 410px">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
              <div class="card-body">
 | 
			
		||||
                <a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
 | 
			
		||||
                  <h5 class="card-title d-inline" i18n="dashboard.recent-blocks">Recent Blocks</h5>
 | 
			
		||||
                  <span> </span>
 | 
			
		||||
                  <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
 | 
			
		||||
                </a>
 | 
			
		||||
                <table class="table lastest-blocks-table">
 | 
			
		||||
                  <thead>
 | 
			
		||||
                    <th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th>
 | 
			
		||||
                    <th class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th>
 | 
			
		||||
                    <th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th>
 | 
			
		||||
                    <th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
 | 
			
		||||
                  </thead>
 | 
			
		||||
                  <tbody *ngIf="blocks$ | async as blocks; else blocksSkeleton">
 | 
			
		||||
                    <tr *ngFor="let block of blocks; let i = index; trackBy: trackByBlock">
 | 
			
		||||
                      <td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
 | 
			
		||||
                      <td class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
 | 
			
		||||
                      <td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
 | 
			
		||||
                      <td class="table-cell-size">
 | 
			
		||||
                        <div class="progress">
 | 
			
		||||
                          <div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"> </div>
 | 
			
		||||
                          <div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                  </tbody>
 | 
			
		||||
                </table>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <ng-template #blocksSkeleton>
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr *ngFor="let i of [1,2,3,4,5,6]">
 | 
			
		||||
                <td class="table-cell-height"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
 | 
			
		||||
                <td class="table-cell-mined"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
 | 
			
		||||
                <td class="table-cell-transaction-count"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
 | 
			
		||||
                <td class="table-cell-size"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
        }
 | 
			
		||||
        @case ('transactions') {
 | 
			
		||||
          <div class="col" style="max-height: 410px">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
              <div class="card-body">
 | 
			
		||||
                <h5 class="card-title" i18n="dashboard.recent-transactions">Recent Transactions</h5>
 | 
			
		||||
                <table class="table latest-transactions">
 | 
			
		||||
                  <thead>
 | 
			
		||||
                    <th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
 | 
			
		||||
                    <th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
 | 
			
		||||
                    <th class="table-cell-fiat" *ngIf="(network$ | async) === ''">{{ currency }}</th>
 | 
			
		||||
                    <th class="table-cell-fees" i18n="transaction.fee|Transaction fee">Fee</th>
 | 
			
		||||
                  </thead>
 | 
			
		||||
                  <tbody *ngIf="transactions$ | async as transactions else recentTransactionsSkeleton">
 | 
			
		||||
                    <tr *ngFor="let transaction of transactions; let i = index;">
 | 
			
		||||
                      <td class="table-cell-txid">
 | 
			
		||||
                        <a [routerLink]="['/tx' | relativeUrl, transaction.txid]">
 | 
			
		||||
                          <app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
 | 
			
		||||
                        </a>
 | 
			
		||||
                      </td>
 | 
			
		||||
                      <td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquidtestnet'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td>
 | 
			
		||||
                      <td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
 | 
			
		||||
                      <td class="table-cell-fees"><app-fee-rate [fee]="transaction.fee" [weight]="transaction.vsize * 4"></app-fee-rate></td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                  </tbody>
 | 
			
		||||
                </table>
 | 
			
		||||
                <div class=""> </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <ng-template #recentTransactionsSkeleton>
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr *ngFor="let i of [1,2,3,4,5,6]">
 | 
			
		||||
                <td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
 | 
			
		||||
                <td class="table-cell-satoshis"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
 | 
			
		||||
                <td class="table-cell-fiat" *ngIf="(network$ | async) === ''"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
 | 
			
		||||
                <td class="table-cell-fees"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
        }
 | 
			
		||||
        @case ('balance') {
 | 
			
		||||
          <div class="col card-wrapper">
 | 
			
		||||
            <div class="main-title" i18n="dashboard.treasury">Treasury</div>
 | 
			
		||||
            <app-balance-widget [address]="widget.props.address" [addressInfo]="address"></app-balance-widget>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @case ('address') {
 | 
			
		||||
          <div class="col" style="max-height: 410px">
 | 
			
		||||
            <div class="card graph-card">
 | 
			
		||||
              <div class="card-body">
 | 
			
		||||
                <h5 class="card-title mb-4" i18n="dashboard.balance-history">Balance History</h5>
 | 
			
		||||
                <app-address-graph [address]="widget.props.address" [stats]="address?.chain_stats" [widget]="true" [height]="graphHeight"></app-address-graph>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #loading>
 | 
			
		||||
  <div class="skeleton-loader"></div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
<ng-template #loadingbig>
 | 
			
		||||
  <span class="skeleton-loader skeleton-loader-big" ></span>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,490 @@
 | 
			
		||||
.dashboard-container {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  margin-top: 0.5rem;
 | 
			
		||||
  .col {
 | 
			
		||||
    margin-bottom: 1.5rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card {
 | 
			
		||||
  background-color: var(--bg);
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-title {
 | 
			
		||||
  color: var(--title-fg);
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.info-block {
 | 
			
		||||
  float: left;
 | 
			
		||||
  width: 350px;
 | 
			
		||||
  line-height: 25px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progress {
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  background-color: var(--secondary);
 | 
			
		||||
  height: 1.1rem;
 | 
			
		||||
  max-width: 180px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bg-warning {
 | 
			
		||||
  background-color: #b58800 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.skeleton-loader {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.more-padding {
 | 
			
		||||
  padding: 18px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.graph-card {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  @media (min-width: 768px) {
 | 
			
		||||
    height: 415px;
 | 
			
		||||
  }
 | 
			
		||||
  @media (min-width: 992px) {
 | 
			
		||||
    height: 510px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mempool-info-data {
 | 
			
		||||
  min-height: 56px;
 | 
			
		||||
  display: block;
 | 
			
		||||
  @media (min-width: 485px) {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
  &.lbtc-pegs-stats { 
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
  h5 {
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
  }
 | 
			
		||||
  .item {
 | 
			
		||||
    width: 50%;
 | 
			
		||||
    margin: 0px auto 20px;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    @media (min-width: 485px) {
 | 
			
		||||
      margin: 0px auto 10px;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 768px) {
 | 
			
		||||
      margin: 0px auto 0px;
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin: 0px auto 0px;
 | 
			
		||||
    }
 | 
			
		||||
    &:nth-child(2) {
 | 
			
		||||
      order: 2;
 | 
			
		||||
      @media (min-width: 485px) {
 | 
			
		||||
        order: 3;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &:nth-child(3) {
 | 
			
		||||
      order: 3;
 | 
			
		||||
      @media (min-width: 485px) {
 | 
			
		||||
        order: 2;
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
      @media (min-width: 768px) {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
      @media (min-width: 992px) {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .card-text {
 | 
			
		||||
      font-size: 18px;
 | 
			
		||||
      span {
 | 
			
		||||
        color: var(--transparent-fg);
 | 
			
		||||
        font-size: 12px;
 | 
			
		||||
      }
 | 
			
		||||
      .bitcoin-color {
 | 
			
		||||
        color: var(--orange);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .progress {
 | 
			
		||||
      width: 90%;
 | 
			
		||||
      @media (min-width: 768px) {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .bar {
 | 
			
		||||
    width: 93%;
 | 
			
		||||
    margin: 0px 5px 20px;
 | 
			
		||||
    @media (min-width: 485px) {
 | 
			
		||||
      max-width: 200px;
 | 
			
		||||
      margin: 0px auto 0px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .skeleton-loader {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    max-width: 100px;
 | 
			
		||||
    display: block;
 | 
			
		||||
    margin: 18px auto 0;
 | 
			
		||||
  }
 | 
			
		||||
  .skeleton-loader-big {
 | 
			
		||||
    max-width: 180px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.latest-transactions {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  table-layout:fixed;
 | 
			
		||||
  tr, td, th {
 | 
			
		||||
    border: 0px;
 | 
			
		||||
    padding-top: 0.71rem !important;
 | 
			
		||||
    padding-bottom: 0.75rem !important;
 | 
			
		||||
  }
 | 
			
		||||
  td {
 | 
			
		||||
    overflow:hidden;
 | 
			
		||||
    width: 25%;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-satoshis {
 | 
			
		||||
    display: none;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
    @media (min-width: 576px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 768px) {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 1100px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-fiat {
 | 
			
		||||
    display: none;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
    @media (min-width: 485px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 768px) {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 992px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-fees {
 | 
			
		||||
    text-align: right;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.skeleton-loader-transactions {
 | 
			
		||||
  max-width: 250px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  top: 2px;
 | 
			
		||||
  margin-bottom: -3px;
 | 
			
		||||
  height: 18px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.lastest-blocks-table {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  tr, td, th {
 | 
			
		||||
    border: 0px;
 | 
			
		||||
    padding-top: 0.65rem !important;
 | 
			
		||||
    padding-bottom: 0.7rem !important;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-height {
 | 
			
		||||
    width: 15%;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-mined {
 | 
			
		||||
    width: 35%;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-transaction-count {
 | 
			
		||||
    display: none;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
    width: 20%;
 | 
			
		||||
    display: table-cell;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-size {
 | 
			
		||||
    display: none;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    width: 30%;
 | 
			
		||||
    @media (min-width: 485px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 768px) {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 992px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.lastest-replacements-table {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  table-layout:fixed;
 | 
			
		||||
  tr, td, th {
 | 
			
		||||
    border: 0px;
 | 
			
		||||
    padding-top: 0.71rem !important;
 | 
			
		||||
    padding-bottom: 0.75rem !important;
 | 
			
		||||
  }
 | 
			
		||||
  td {
 | 
			
		||||
    overflow:hidden;
 | 
			
		||||
    width: 25%;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-txid {
 | 
			
		||||
    width: 25%;
 | 
			
		||||
    text-align: start;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-old-fee {
 | 
			
		||||
    width: 25%;
 | 
			
		||||
    text-align: end;
 | 
			
		||||
 | 
			
		||||
    @media(max-width: 1080px) {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-new-fee {
 | 
			
		||||
    width: 20%;
 | 
			
		||||
    text-align: end;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-badges {
 | 
			
		||||
    width: 23%;
 | 
			
		||||
    padding-right: 0;
 | 
			
		||||
    padding-left: 5px;
 | 
			
		||||
    text-align: end;
 | 
			
		||||
 | 
			
		||||
    .badge {
 | 
			
		||||
      margin-left: 5px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mempool-graph {
 | 
			
		||||
  height: 255px;
 | 
			
		||||
  @media (min-width: 768px) {
 | 
			
		||||
    height: 285px;
 | 
			
		||||
  }
 | 
			
		||||
  @media (min-width: 992px) {
 | 
			
		||||
    height: 370px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.loadingGraphs{
 | 
			
		||||
  height: 250px;
 | 
			
		||||
  display: grid;
 | 
			
		||||
  place-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.inc-tx-progress-bar {
 | 
			
		||||
  max-width: 250px;
 | 
			
		||||
  .progress-bar {
 | 
			
		||||
    padding: 4px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.terms-of-service {
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.small-bar {
 | 
			
		||||
  height: 8px;
 | 
			
		||||
  top: -4px;
 | 
			
		||||
  max-width: 120px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loading-container {
 | 
			
		||||
  min-height: 76px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main-title {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: #ffffff91;
 | 
			
		||||
  margin-top: -13px;
 | 
			
		||||
  font-size: 10px;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding-bottom: 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-wrapper {
 | 
			
		||||
  .card {
 | 
			
		||||
    height: auto !important;
 | 
			
		||||
  }
 | 
			
		||||
  .card-body {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex: inherit;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    justify-content: space-around;
 | 
			
		||||
    padding: 22px 20px;
 | 
			
		||||
    &.liquid {
 | 
			
		||||
      height: 124.5px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .less-padding {
 | 
			
		||||
    padding: 20px 20px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.retarget-sign {
 | 
			
		||||
  margin-right: -3px;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  top: -2px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.previous-retarget-sign {
 | 
			
		||||
  margin-right: -2px;
 | 
			
		||||
  font-size: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.assetIcon {
 | 
			
		||||
  width: 40px;
 | 
			
		||||
  height: 40px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.asset-title {
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.asset-icon {
 | 
			
		||||
  width: 65px;
 | 
			
		||||
  height: 65px;
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.circulating-amount {
 | 
			
		||||
  text-align: right;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clear-link {
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pool-name {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  vertical-align: text-top;
 | 
			
		||||
  padding-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
 | 
			
		||||
  display: block;
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mempool-block-wrapper {
 | 
			
		||||
  max-height: 410px;
 | 
			
		||||
  max-width: 410px;
 | 
			
		||||
  margin: auto;
 | 
			
		||||
 | 
			
		||||
  @media (min-width: 768px) {
 | 
			
		||||
    max-height: 344px;
 | 
			
		||||
    max-width: 344px;
 | 
			
		||||
  }
 | 
			
		||||
  @media (min-width: 992px) {
 | 
			
		||||
    max-height: 410px;
 | 
			
		||||
    max-width: 410px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.goggle-badge {
 | 
			
		||||
  margin: 6px 5px 8px;
 | 
			
		||||
  background: none;
 | 
			
		||||
  border: solid 2px var(--primary);
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  &.active {
 | 
			
		||||
    background: var(--primary);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-xs {
 | 
			
		||||
  padding: 0.35rem 0.5rem;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.quick-filter {
 | 
			
		||||
  margin-top: 5px;
 | 
			
		||||
  margin-bottom: 6px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-liquid {
 | 
			
		||||
  background-color: var(--bg);
 | 
			
		||||
  height: 418px;
 | 
			
		||||
  @media (min-width: 992px) {
 | 
			
		||||
    height: 512px;
 | 
			
		||||
  }
 | 
			
		||||
  &.smaller {
 | 
			
		||||
    height: 408px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-title-liquid {
 | 
			
		||||
  padding-top: 20px;
 | 
			
		||||
  margin-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.in-progress-message {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: #ffffff91;
 | 
			
		||||
  margin-top: 20px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding-bottom: 3px;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stats-card {
 | 
			
		||||
  min-height: 56px;
 | 
			
		||||
  display: block;
 | 
			
		||||
  @media (min-width: 485px) {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
  h5 {
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
  }
 | 
			
		||||
  .item {
 | 
			
		||||
    width: 50%;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    margin: 0px auto 20px;
 | 
			
		||||
    &:nth-child(2) {
 | 
			
		||||
      order: 2;
 | 
			
		||||
      @media (min-width: 485px) {
 | 
			
		||||
        order: 3;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &:nth-child(3) {
 | 
			
		||||
      order: 3;
 | 
			
		||||
      @media (min-width: 485px) {
 | 
			
		||||
        order: 2;
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
      @media (min-width: 768px) {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
      @media (min-width: 992px) {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .card-title {
 | 
			
		||||
      font-size: 1rem;
 | 
			
		||||
      color: var(--title-fg);
 | 
			
		||||
    }
 | 
			
		||||
    .card-text {
 | 
			
		||||
      font-size: 18px;
 | 
			
		||||
      span {
 | 
			
		||||
        color: var(--transparent-fg);
 | 
			
		||||
        font-size: 12px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,323 @@
 | 
			
		||||
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
 | 
			
		||||
import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs';
 | 
			
		||||
import { catchError, filter, map, scan, shareReplay, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { MempoolInfo, ReplacementInfo } from '../../interfaces/websocket.interface';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
import { ActiveFilter, FilterMode, GradientMode, toFlags } from '../../shared/filters.utils';
 | 
			
		||||
import { detectWebGL } from '../../shared/graphs.utils';
 | 
			
		||||
import { Address } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
 | 
			
		||||
interface MempoolBlocksData {
 | 
			
		||||
  blocks: number;
 | 
			
		||||
  size: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface MempoolInfoData {
 | 
			
		||||
  memPoolInfo: MempoolInfo;
 | 
			
		||||
  vBytesPerSecond: number;
 | 
			
		||||
  progressWidth: string;
 | 
			
		||||
  progressColor: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface MempoolStatsData {
 | 
			
		||||
  mempool: OptimizedMempoolStats[];
 | 
			
		||||
  weightPerSecond: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-custom-dashboard',
 | 
			
		||||
  templateUrl: './custom-dashboard.component.html',
 | 
			
		||||
  styleUrls: ['./custom-dashboard.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush
 | 
			
		||||
})
 | 
			
		||||
export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewInit {
 | 
			
		||||
  network$: Observable<string>;
 | 
			
		||||
  mempoolBlocksData$: Observable<MempoolBlocksData>;
 | 
			
		||||
  mempoolInfoData$: Observable<MempoolInfoData>;
 | 
			
		||||
  mempoolLoadingStatus$: Observable<number>;
 | 
			
		||||
  vBytesPerSecondLimit = 1667;
 | 
			
		||||
  transactions$: Observable<TransactionStripped[]>;
 | 
			
		||||
  blocks$: Observable<BlockExtended[]>;
 | 
			
		||||
  replacements$: Observable<ReplacementInfo[]>;
 | 
			
		||||
  latestBlockHeight: number;
 | 
			
		||||
  mempoolTransactionsWeightPerSecondData: any;
 | 
			
		||||
  mempoolStats$: Observable<MempoolStatsData>;
 | 
			
		||||
  transactionsWeightPerSecondOptions: any;
 | 
			
		||||
  isLoadingWebSocket$: Observable<boolean>;
 | 
			
		||||
  isLoad: boolean = true;
 | 
			
		||||
  filterSubscription: Subscription;
 | 
			
		||||
  mempoolInfoSubscription: Subscription;
 | 
			
		||||
  currencySubscription: Subscription;
 | 
			
		||||
  currency: string;
 | 
			
		||||
  incomingGraphHeight: number = 300;
 | 
			
		||||
  graphHeight: number = 300;
 | 
			
		||||
  webGlEnabled = true;
 | 
			
		||||
 | 
			
		||||
  widgets;
 | 
			
		||||
 | 
			
		||||
  addressSubscription: Subscription;
 | 
			
		||||
  address: Address;
 | 
			
		||||
 | 
			
		||||
  goggleResolution = 82;
 | 
			
		||||
  goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[], gradient: GradientMode }[] = [
 | 
			
		||||
    { index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
 | 
			
		||||
    { index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
 | 
			
		||||
    { index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
 | 
			
		||||
    { index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
 | 
			
		||||
  ];
 | 
			
		||||
  goggleFlags = 0n;
 | 
			
		||||
  goggleMode: FilterMode = 'and';
 | 
			
		||||
  gradientMode: GradientMode = 'age';
 | 
			
		||||
  goggleIndex = 0;
 | 
			
		||||
 | 
			
		||||
  private destroy$ = new Subject();
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    @Inject(PLATFORM_ID) private platformId: Object,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
 | 
			
		||||
    this.widgets = this.stateService.env.customize?.dashboard.widgets || [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit(): void {
 | 
			
		||||
    this.stateService.focusSearchInputDesktop();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.filterSubscription.unsubscribe();
 | 
			
		||||
    this.mempoolInfoSubscription.unsubscribe();
 | 
			
		||||
    this.currencySubscription.unsubscribe();
 | 
			
		||||
    this.websocketService.stopTrackRbfSummary();
 | 
			
		||||
    if (this.addressSubscription) {
 | 
			
		||||
      this.addressSubscription.unsubscribe();
 | 
			
		||||
      this.websocketService.stopTrackingAddress();
 | 
			
		||||
      this.address = null;
 | 
			
		||||
    }
 | 
			
		||||
    this.destroy$.next(1);
 | 
			
		||||
    this.destroy$.complete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.onResize();
 | 
			
		||||
    this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
 | 
			
		||||
    this.seoService.resetTitle();
 | 
			
		||||
    this.seoService.resetDescription();
 | 
			
		||||
    this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
 | 
			
		||||
    this.websocketService.startTrackRbfSummary();
 | 
			
		||||
    this.network$ = merge(of(''), this.stateService.networkChanged$);
 | 
			
		||||
    this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
 | 
			
		||||
      const activeFilters = active.filters.sort().join(',');
 | 
			
		||||
      for (const goggle of this.goggleCycle) {
 | 
			
		||||
        if (goggle.mode === active.mode) {
 | 
			
		||||
          const goggleFilters = goggle.filters.sort().join(',');
 | 
			
		||||
          if (goggleFilters === activeFilters) {
 | 
			
		||||
            this.goggleIndex = goggle.index;
 | 
			
		||||
            this.goggleFlags = toFlags(goggle.filters);
 | 
			
		||||
            this.goggleMode = goggle.mode;
 | 
			
		||||
            this.gradientMode = active.gradient;
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      this.goggleCycle.push({
 | 
			
		||||
        index: this.goggleCycle.length,
 | 
			
		||||
        name: 'Custom',
 | 
			
		||||
        mode: active.mode,
 | 
			
		||||
        filters: active.filters,
 | 
			
		||||
        gradient: active.gradient,
 | 
			
		||||
      });
 | 
			
		||||
      this.goggleIndex = this.goggleCycle.length - 1;
 | 
			
		||||
      this.goggleFlags = toFlags(active.filters);
 | 
			
		||||
      this.goggleMode = active.mode;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.mempoolInfoData$ = combineLatest([
 | 
			
		||||
      this.stateService.mempoolInfo$,
 | 
			
		||||
      this.stateService.vbytesPerSecond$
 | 
			
		||||
    ]).pipe(
 | 
			
		||||
      map(([mempoolInfo, vbytesPerSecond]) => {
 | 
			
		||||
        const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
 | 
			
		||||
 | 
			
		||||
        let progressColor = 'bg-success';
 | 
			
		||||
        if (vbytesPerSecond > 1667) {
 | 
			
		||||
          progressColor = 'bg-warning';
 | 
			
		||||
        }
 | 
			
		||||
        if (vbytesPerSecond > 3000) {
 | 
			
		||||
          progressColor = 'bg-danger';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
 | 
			
		||||
        let mempoolSizeProgress = 'bg-danger';
 | 
			
		||||
        if (mempoolSizePercentage <= 50) {
 | 
			
		||||
          mempoolSizeProgress = 'bg-success';
 | 
			
		||||
        } else if (mempoolSizePercentage <= 75) {
 | 
			
		||||
          mempoolSizeProgress = 'bg-warning';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          memPoolInfo: mempoolInfo,
 | 
			
		||||
          vBytesPerSecond: vbytesPerSecond,
 | 
			
		||||
          progressWidth: percent + '%',
 | 
			
		||||
          progressColor: progressColor,
 | 
			
		||||
          mempoolSizeProgress: mempoolSizeProgress,
 | 
			
		||||
        };
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.mempoolInfoSubscription = this.mempoolInfoData$.subscribe();
 | 
			
		||||
 | 
			
		||||
    this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map((mempoolBlocks) => {
 | 
			
		||||
          const size = mempoolBlocks.map((m) => m.blockSize).reduce((a, b) => a + b, 0);
 | 
			
		||||
          const vsize = mempoolBlocks.map((m) => m.blockVSize).reduce((a, b) => a + b, 0);
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            size: size,
 | 
			
		||||
            blocks: Math.ceil(vsize / this.stateService.blockVSize)
 | 
			
		||||
          };
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.transactions$ = this.stateService.transactions$;
 | 
			
		||||
 | 
			
		||||
    this.blocks$ = this.stateService.blocks$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        tap((blocks) => {
 | 
			
		||||
          this.latestBlockHeight = blocks[0].height;
 | 
			
		||||
        }),
 | 
			
		||||
        switchMap((blocks) => {
 | 
			
		||||
          if (this.stateService.env.MINING_DASHBOARD === true) {
 | 
			
		||||
            for (const block of blocks) {
 | 
			
		||||
              // @ts-ignore: Need to add an extra field for the template
 | 
			
		||||
              block.extras.pool.logo = `/resources/mining-pools/` +
 | 
			
		||||
                block.extras.pool.slug + '.svg';
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return of(blocks.slice(0, 6));
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.replacements$ = this.stateService.rbfLatestSummary$;
 | 
			
		||||
 | 
			
		||||
    this.mempoolStats$ = this.stateService.connectionState$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        filter((state) => state === 2),
 | 
			
		||||
        switchMap(() => this.apiService.list2HStatistics$().pipe(
 | 
			
		||||
          catchError((e) => {
 | 
			
		||||
            return of(null);
 | 
			
		||||
          })
 | 
			
		||||
        )),
 | 
			
		||||
        switchMap((mempoolStats) => {
 | 
			
		||||
          return merge(
 | 
			
		||||
            this.stateService.live2Chart$
 | 
			
		||||
              .pipe(
 | 
			
		||||
                scan((acc, stats) => {
 | 
			
		||||
                  acc.unshift(stats);
 | 
			
		||||
                  acc = acc.slice(0, 120);
 | 
			
		||||
                  return acc;
 | 
			
		||||
                }, (mempoolStats || []))
 | 
			
		||||
              ),
 | 
			
		||||
            of(mempoolStats)
 | 
			
		||||
          );
 | 
			
		||||
        }),
 | 
			
		||||
        map((mempoolStats) => {
 | 
			
		||||
          if (mempoolStats) {
 | 
			
		||||
            return {
 | 
			
		||||
              mempool: mempoolStats,
 | 
			
		||||
              weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
 | 
			
		||||
            };
 | 
			
		||||
          } else {
 | 
			
		||||
            return null;
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
        shareReplay(1),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
 | 
			
		||||
      this.currency = fiat;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.startAddressSubscription();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
 | 
			
		||||
    mempoolStats.reverse();
 | 
			
		||||
    const labels = mempoolStats.map(stats => stats.added);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      labels: labels,
 | 
			
		||||
      series: [mempoolStats.map((stats) => [stats.added * 1000, stats.vbytes_per_second])],
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackByBlock(index: number, block: BlockExtended) {
 | 
			
		||||
    return block.height;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getArrayFromNumber(num: number): number[] {
 | 
			
		||||
    return Array.from({ length: num }, (_, i) => i + 1);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  setFilter(index): void {
 | 
			
		||||
    const selected = this.goggleCycle[index];
 | 
			
		||||
    this.stateService.activeGoggles$.next(selected);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  startAddressSubscription(): void {
 | 
			
		||||
    if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.address)) {
 | 
			
		||||
      const address = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.address).props.address;
 | 
			
		||||
      const addressString = (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) ? address.toLowerCase() : address;
 | 
			
		||||
      
 | 
			
		||||
      this.addressSubscription = (
 | 
			
		||||
        addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
 | 
			
		||||
        ? this.electrsApiService.getPubKeyAddress$(addressString)
 | 
			
		||||
        : this.electrsApiService.getAddress$(addressString)
 | 
			
		||||
      ).pipe(
 | 
			
		||||
          catchError((err) => {
 | 
			
		||||
            // this.isLoadingAddress = false;
 | 
			
		||||
            // this.error = err;
 | 
			
		||||
            // this.seoService.logSoft404();
 | 
			
		||||
            console.log(err);
 | 
			
		||||
            return of(null);
 | 
			
		||||
          }),
 | 
			
		||||
          filter((address) => !!address)
 | 
			
		||||
        ).subscribe((address: Address) => {
 | 
			
		||||
          this.websocketService.startTrackAddress(address.address);
 | 
			
		||||
          this.address = address;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @HostListener('window:resize', ['$event'])
 | 
			
		||||
  onResize(): void {
 | 
			
		||||
    if (window.innerWidth >= 992) {
 | 
			
		||||
      this.incomingGraphHeight = 300;
 | 
			
		||||
      this.goggleResolution = 82;
 | 
			
		||||
      this.graphHeight = 400;
 | 
			
		||||
    } else if (window.innerWidth >= 768) {
 | 
			
		||||
      this.incomingGraphHeight = 215;
 | 
			
		||||
      this.goggleResolution = 80;
 | 
			
		||||
      this.graphHeight = 310;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.incomingGraphHeight = 180;
 | 
			
		||||
      this.goggleResolution = 86;
 | 
			
		||||
      this.graphHeight = 310;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -27,6 +27,7 @@ import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.co
 | 
			
		||||
import { PoolComponent } from '../components/pool/pool.component';
 | 
			
		||||
import { TelevisionComponent } from '../components/television/television.component';
 | 
			
		||||
import { DashboardComponent } from '../dashboard/dashboard.component';
 | 
			
		||||
import { CustomDashboardComponent } from '../components/custom-dashboard/custom-dashboard.component';
 | 
			
		||||
import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component';
 | 
			
		||||
import { AcceleratorDashboardComponent } from '../components/acceleration/accelerator-dashboard/accelerator-dashboard.component';
 | 
			
		||||
import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component';
 | 
			
		||||
@ -39,6 +40,7 @@ import { CommonModule } from '@angular/common';
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
    DashboardComponent,
 | 
			
		||||
    CustomDashboardComponent,
 | 
			
		||||
    MempoolBlockComponent,
 | 
			
		||||
    AddressComponent,
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,10 +17,16 @@ import { StartComponent } from '../components/start/start.component';
 | 
			
		||||
import { StatisticsComponent } from '../components/statistics/statistics.component';
 | 
			
		||||
import { TelevisionComponent } from '../components/television/television.component';
 | 
			
		||||
import { DashboardComponent } from '../dashboard/dashboard.component';
 | 
			
		||||
import { CustomDashboardComponent } from '../components/custom-dashboard/custom-dashboard.component';
 | 
			
		||||
import { AccelerationFeesGraphComponent } from '../components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component';
 | 
			
		||||
import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component';
 | 
			
		||||
import { AddressComponent } from '../components/address/address.component';
 | 
			
		||||
 | 
			
		||||
const browserWindow = window || {};
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
const browserWindowEnv = browserWindow.__env || {};
 | 
			
		||||
const isCustomized = browserWindowEnv?.customize;
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: '',
 | 
			
		||||
@ -149,7 +155,7 @@ const routes: Routes = [
 | 
			
		||||
        component: StartComponent,
 | 
			
		||||
        children: [{
 | 
			
		||||
          path: '',
 | 
			
		||||
          component: DashboardComponent,
 | 
			
		||||
          component: isCustomized ? CustomDashboardComponent : DashboardComponent,
 | 
			
		||||
        }]
 | 
			
		||||
      },
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -65,6 +65,7 @@ import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
 | 
			
		||||
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
 | 
			
		||||
import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
 | 
			
		||||
import { DifficultyMiningComponent } from '../components/difficulty-mining/difficulty-mining.component';
 | 
			
		||||
import { BalanceWidgetComponent } from '../components/balance-widget/balance-widget.component';
 | 
			
		||||
import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component';
 | 
			
		||||
import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.component';
 | 
			
		||||
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
 | 
			
		||||
@ -173,6 +174,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
 | 
			
		||||
    DifficultyComponent,
 | 
			
		||||
    DifficultyMiningComponent,
 | 
			
		||||
    DifficultyTooltipComponent,
 | 
			
		||||
    BalanceWidgetComponent,
 | 
			
		||||
    RbfTimelineComponent,
 | 
			
		||||
    RbfTimelineTooltipComponent,
 | 
			
		||||
    PushTransactionComponent,
 | 
			
		||||
@ -309,6 +311,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
 | 
			
		||||
    DifficultyComponent,
 | 
			
		||||
    DifficultyMiningComponent,
 | 
			
		||||
    DifficultyTooltipComponent,
 | 
			
		||||
    BalanceWidgetComponent,
 | 
			
		||||
    RbfTimelineComponent,
 | 
			
		||||
    RbfTimelineTooltipComponent,
 | 
			
		||||
    PushTransactionComponent,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user