Merge branch 'master' into nymkappa/fix-platinium-typo
This commit is contained in:
@@ -30,7 +30,7 @@ export class BisqDashboardComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project™. See Bisq market prices, trading activity, and more.`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See Bisq market prices, trading activity, and more.`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<div id="become-sponsor-container">
|
||||
<div class="become-sponsor community">
|
||||
<p style="font-weight: 700; font-size: 18px;">If you're an individual...</p>
|
||||
<a href="https://mempool.space/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button" (click)="onSponsorClick($event)">Become a Community Sponsor</a>
|
||||
<a [href]="host + '/sponsor'" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button" (click)="onSponsorClick($event)">Become a Community Sponsor</a>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Exclusive swag</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Your avatar on the About page</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> And more coming soon :)</p>
|
||||
</div>
|
||||
<div class="become-sponsor enterprise">
|
||||
<p style="font-weight: 700; font-size: 18px;">If you're a business...</p>
|
||||
<a href="https://mempool.space/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button" (click)="onEnterpriseClick($event)">Become an Enterprise Sponsor</a>
|
||||
<a [href]="host + '/enterprise'" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button" (click)="onEnterpriseClick($event)">Become an Enterprise Sponsor</a>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Increased API limits</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Co-branded instance</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
@@ -7,6 +7,8 @@ import { EnterpriseService } from '../../services/enterprise.service';
|
||||
styleUrls: ['./about-sponsors.component.scss'],
|
||||
})
|
||||
export class AboutSponsorsComponent {
|
||||
@Input() host = 'https://mempool.space';
|
||||
|
||||
constructor(private enterpriseService: EnterpriseService) {
|
||||
}
|
||||
|
||||
|
||||
@@ -405,7 +405,7 @@
|
||||
|
||||
<div class="copyright">
|
||||
<div class="title">
|
||||
Copyright © 2019-2023<br>
|
||||
Copyright © 2019-2024<br>
|
||||
Mempool Space K.K.<br>
|
||||
and other shadowy super-coders
|
||||
</div>
|
||||
@@ -422,7 +422,7 @@
|
||||
Trademark Notice<br>
|
||||
</div>
|
||||
<p>
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem™, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
</p>
|
||||
<p>
|
||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||
|
||||
@@ -5,6 +5,7 @@ import { StorageService } from '../../services/storage.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { nextRoundNumber } from '../../shared/common.utils';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
|
||||
export type AccelerationEstimate = {
|
||||
txSummary: TxSummary;
|
||||
@@ -64,6 +65,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
constructor(
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private storageService: StorageService,
|
||||
private audioService: AudioService,
|
||||
private cd: ChangeDetectorRef
|
||||
) { }
|
||||
|
||||
@@ -187,6 +189,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.userBid
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
this.showSuccess = true;
|
||||
this.scrollToPreviewWithTimeout('successAlert', 'center');
|
||||
this.estimateSubscription.unsubscribe();
|
||||
@@ -211,4 +214,4 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
onResize(): void {
|
||||
this.isMobile = window.innerWidth <= 767.98;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,6 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div *ngIf="widget">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="acceleration.block-fees">Out-of-band Fees Per Block</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
|
||||
@@ -53,11 +53,6 @@
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 290px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 10px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { Observable, Subscription, combineLatest } from 'rxjs';
|
||||
import { map, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { Observable, Subscription, combineLatest, fromEvent } from 'rxjs';
|
||||
import { map, max, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
@@ -29,6 +29,7 @@ import { ApiService } from '../../../services/api.service';
|
||||
})
|
||||
export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() height: number | string = '200';
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
@Input() accelerations$: Observable<Acceleration[]>;
|
||||
@@ -68,7 +69,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Acceleration Fees`);
|
||||
this.isLoading = true;
|
||||
if (this.widget) {
|
||||
this.miningWindowPreference = '1m';
|
||||
@@ -77,17 +77,19 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
this.statsObservable$ = combineLatest([
|
||||
(this.accelerations$ || this.servicesApiService.getAccelerationHistory$({ timeframe: this.miningWindowPreference })),
|
||||
this.apiService.getHistoricalBlockFees$(this.miningWindowPreference),
|
||||
fromEvent(window, 'resize').pipe(startWith(null)),
|
||||
]).pipe(
|
||||
tap(([accelerations, blockFeesResponse]) => {
|
||||
this.prepareChartOptions(accelerations, blockFeesResponse.body);
|
||||
}),
|
||||
map(([accelerations, blockFeesResponse]) => {
|
||||
return {
|
||||
avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + acc.feePaid, 0) / accelerations.length
|
||||
avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0) / accelerations.length
|
||||
};
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.seoService.setTitle($localize`:@@bcf34abc2d9ed8f45a2f65dd464c46694e9a181e:Acceleration Fees`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('1w');
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
@@ -153,7 +155,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
while (last <= val.avgHeight) {
|
||||
blockCount++;
|
||||
totalFeeDelta += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feeDelta, 0);
|
||||
totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feePaid, 0);
|
||||
totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0);
|
||||
totalCount += (blockAccelerations[last] || []).length;
|
||||
last++;
|
||||
}
|
||||
@@ -175,6 +177,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
],
|
||||
animation: false,
|
||||
grid: {
|
||||
height: this.height,
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
bottom: this.widget ? 30 : 80,
|
||||
@@ -248,7 +251,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Out-of-band fees per block',
|
||||
name: 'Total bid boost per block',
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@@ -258,7 +261,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
],
|
||||
selected: {
|
||||
'In-band fees per block': false,
|
||||
'Out-of-band fees per block': true,
|
||||
'Total bid boost per block': true,
|
||||
},
|
||||
show: !this.widget,
|
||||
},
|
||||
@@ -301,7 +304,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
{
|
||||
legendHoverLink: false,
|
||||
zlevel: 1,
|
||||
name: 'Out-of-band fees per block',
|
||||
name: 'Total bid boost per block',
|
||||
data: data.map(block => [block.timestamp * 1000, block.avgFeePaid, block.avgHeight]),
|
||||
stack: 'Total',
|
||||
type: 'bar',
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<div class="stats-wrapper" *ngIf="accelerationStats$ | async as stats; else loading">
|
||||
<div class="stats-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="address.transactions">Transactions</h5>
|
||||
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.count }}</div>
|
||||
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.out-of-band-fees">Out-of-band Fees</h5>
|
||||
<h5 class="card-title" i18n="accelerator.total-boost">Total Bid Boost</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.totalFeesPaid / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div>
|
||||
<span class="fiat">
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.success-rate">Success rate</h5>
|
||||
<h5 class="card-title" i18n="accelerator.success-rate">Success Rate</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.successRate.toFixed(2) }} %</div>
|
||||
<div class="symbol" i18n="accelerator.mined-next-block">mined</div>
|
||||
@@ -29,21 +29,21 @@
|
||||
<ng-template #loading>
|
||||
<div class="stats-container loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="address.transactions">Transactions</h5>
|
||||
<h5 class="card-title" i18n="accelerator.requests">Requests</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="accelerator.out-of-band-fees">Out-of-band Fees</h5>
|
||||
<h5 class="card-title" i18n="accelerator.total-boost">Total Bid Boost</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="accelerator.success-rate">Success rate</h5>
|
||||
<h5 class="card-title" i18n="accelerator.success-rate">Success Rate</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
|
||||
@@ -27,11 +27,11 @@ export class AccelerationStatsComponent implements OnInit {
|
||||
let totalFeesPaid = 0;
|
||||
let totalSucceeded = 0;
|
||||
let totalCanceled = 0;
|
||||
for (const acceleration of accelerations) {
|
||||
if (acceleration.status === 'completed') {
|
||||
for (const acc of accelerations) {
|
||||
if (acc.status === 'completed') {
|
||||
totalSucceeded++;
|
||||
totalFeesPaid += acceleration.feePaid || 0;
|
||||
} else if (acceleration.status === 'failed') {
|
||||
totalFeesPaid += (acc.feePaid - acc.baseFee - acc.vsizeFee) || 0;
|
||||
} else if (acc.status === 'failed') {
|
||||
totalCanceled++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="container-xl" style="min-height: 335px" [class.widget]="widget" [class.full-height]="!widget">
|
||||
<div class="container-xl widget-container" [class.widget]="widget" [class.full-height]="!widget">
|
||||
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1>
|
||||
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="min-height: 295px" *ngIf="accelerationList$ | async as accelerations">
|
||||
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations">
|
||||
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed">
|
||||
<thead>
|
||||
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
@@ -14,7 +14,7 @@
|
||||
<th class="time text-right" i18n="accelerator.block">Requested</th>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!pending">
|
||||
<th class="fee text-right" i18n="transaction.fee|Transaction fee">Out-of-band Fee</th>
|
||||
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
|
||||
<th class="block text-right" i18n="accelerator.block">Block</th>
|
||||
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
|
||||
</ng-container>
|
||||
@@ -39,7 +39,7 @@
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!pending">
|
||||
<td *ngIf="acceleration.feePaid" class="fee text-right">
|
||||
{{ (acceleration.feePaid) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
{{ (acceleration.boost) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
</td>
|
||||
<td *ngIf="!acceleration.feePaid" class="fee text-right">
|
||||
~
|
||||
@@ -48,7 +48,7 @@
|
||||
<a [routerLink]="['/block' | relativeUrl, acceleration.blockHeight]">{{ acceleration.blockHeight }}</a>
|
||||
</td>
|
||||
<td class="status text-right">
|
||||
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="transaction.rbf.mined">Pending</span>
|
||||
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
|
||||
<span *ngIf="acceleration.status === 'mined' || acceleration.status === 'completed'" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="acceleration.status === 'failed'" class="badge badge-danger" i18n="accelerator.canceled">Canceled</span>
|
||||
</td>
|
||||
|
||||
@@ -14,11 +14,24 @@
|
||||
.container-xl.legacy {
|
||||
max-width: 1140px;
|
||||
}
|
||||
.container-xl.widget-container {
|
||||
min-height: 335px;
|
||||
@media (max-width: 767px) {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.acceleration-list {
|
||||
min-height: 295px;
|
||||
@media (max-width: 767px) {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
@@ -50,37 +63,82 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.txid {
|
||||
width: 25%;
|
||||
@media (max-width: 1100px) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
@media (max-width: 875px) {
|
||||
@media (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
.fee {
|
||||
width: 35%;
|
||||
.fee, .block, .status {
|
||||
width: 15%;
|
||||
|
||||
@media (max-width: 720px) {
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
width: 20%;
|
||||
}
|
||||
.widget {
|
||||
.txid {
|
||||
width: 30%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 30%;
|
||||
@media (max-width: 1060px) and (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bid {
|
||||
width: 30%;
|
||||
}
|
||||
.fee-rate {
|
||||
width: 20%;
|
||||
@media (max-width: 1060px) and (min-width: 768px) {
|
||||
text-align: start !important;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
text-align: start !important;
|
||||
}
|
||||
@media (max-width: 840px) and (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 410px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
width: 25%;
|
||||
}
|
||||
.bid {
|
||||
width: 30%;
|
||||
min-width: 150px;
|
||||
@media (max-width: 840px) and (min-width: 768px) {
|
||||
text-align: start !important;
|
||||
}
|
||||
@media (max-width: 410px) {
|
||||
text-align: start !important;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 20%
|
||||
.time {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.fee {
|
||||
width: 30%;
|
||||
@media (max-width: 1060px) and (min-width: 768px) {
|
||||
text-align: start !important;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
text-align: start !important;
|
||||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 20%
|
||||
}
|
||||
}
|
||||
|
||||
/* Tooltip text */
|
||||
@@ -122,4 +180,7 @@ tr, td, th {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@media (max-width: 767px) {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ export class AccelerationsListComponent implements OnInit {
|
||||
acceleration.status = acceleration.status || 'accelerating';
|
||||
}
|
||||
}
|
||||
for (const acc of accelerations) {
|
||||
acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee;
|
||||
}
|
||||
if (this.widget) {
|
||||
return of(accelerations.slice(-6).reverse());
|
||||
} else {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<!-- pending stats -->
|
||||
<div class="col">
|
||||
<div class="main-title">
|
||||
<span [attr.data-cy]="'pending-accelerations'" i18n="accelerator.pending-accelerations">Active accelerations</span>
|
||||
<span [attr.data-cy]="'pending-accelerations'" i18n="accelerator.pending-accelerations">Active Accelerations</span>
|
||||
</div>
|
||||
<div class="card-wrapper">
|
||||
<div class="card">
|
||||
@@ -37,6 +37,11 @@
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
|
||||
<a class="title-link" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.mempool-goggles-accelerations">Mempool Goggles: Accelerations</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<div class="mempool-block-wrapper">
|
||||
<app-mempool-block-overview [index]="0" [overrideColors]="getAcceleratorColor"></app-mempool-block-overview>
|
||||
</div>
|
||||
@@ -48,7 +53,15 @@
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body pl-2 pr-2">
|
||||
<app-acceleration-fees-graph [attr.data-cy]="'acceleration-fees'" [widget]=true [accelerations$]="accelerations$"></app-acceleration-fees-graph>
|
||||
<h5 class="card-title" i18n="acceleration.total-bid-boost">Total Bid Boost</h5>
|
||||
<div class="mempool-graph">
|
||||
<app-acceleration-fees-graph
|
||||
[height]="graphHeight"
|
||||
[attr.data-cy]="'acceleration-fees'"
|
||||
[widget]=true
|
||||
[accelerations$]="accelerations$"
|
||||
></app-acceleration-fees-graph>
|
||||
</div>
|
||||
<div class="mt-1"><a [attr.data-cy]="'acceleration-fees-view-more'" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +82,7 @@
|
||||
<div class="card list-card">
|
||||
<div class="card-body">
|
||||
<div class="title-link">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Active Accelerations</h5>
|
||||
<h5 class="card-title d-inline" i18n="accelerator.pending-accelerations">Active Accelerations</h5>
|
||||
</div>
|
||||
<app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]="true" [accelerations$]="pendingAccelerations$"></app-accelerations-list>
|
||||
</div>
|
||||
@@ -80,7 +93,7 @@
|
||||
<div class="col">
|
||||
<div class="card list-card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/acceleration-list' | relativeUrl]">
|
||||
<a class="title-link" href="" [routerLink]="['/acceleration/list' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Recent Accelerations</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
|
||||
@@ -17,6 +17,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mempool-graph {
|
||||
height: 295px;
|
||||
@media (min-width: 768px) {
|
||||
height: 325px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
@@ -135,14 +145,31 @@
|
||||
}
|
||||
|
||||
.card {
|
||||
height: 385px;
|
||||
@media (min-width: 768px) {
|
||||
height: 420px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 510px;
|
||||
}
|
||||
}
|
||||
.list-card {
|
||||
height: 410px;
|
||||
@media (max-width: 767px) {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mempool-block-wrapper {
|
||||
max-height: 380px;
|
||||
max-width: 380px;
|
||||
max-height: 430px;
|
||||
max-width: 430px;
|
||||
margin: auto;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-height: 344px;
|
||||
max-width: 344px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
max-height: 430px;
|
||||
max-width: 430px;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
|
||||
@@ -30,6 +30,8 @@ export class AcceleratorDashboardComponent implements OnInit {
|
||||
minedAccelerations$: Observable<Acceleration[]>;
|
||||
loadingBlocks: boolean = true;
|
||||
|
||||
graphHeight: number = 300;
|
||||
|
||||
constructor(
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
@@ -40,33 +42,36 @@ export class AcceleratorDashboardComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.onResize();
|
||||
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
|
||||
|
||||
this.pendingAccelerations$ = interval(30000).pipe(
|
||||
startWith(true),
|
||||
switchMap(() => {
|
||||
return this.serviceApiServices.getAccelerations$();
|
||||
}),
|
||||
catchError((e) => {
|
||||
return of([]);
|
||||
return this.serviceApiServices.getAccelerations$().pipe(
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
this.accelerations$ = this.stateService.chainTip$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((chainTip) => {
|
||||
return this.serviceApiServices.getAccelerationHistory$({ timeframe: '1m' });
|
||||
}),
|
||||
catchError((e) => {
|
||||
return of([]);
|
||||
switchMap(() => {
|
||||
return this.serviceApiServices.getAccelerationHistory$({ timeframe: '1m' }).pipe(
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
this.minedAccelerations$ = this.accelerations$.pipe(
|
||||
map(accelerations => {
|
||||
return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status))
|
||||
return accelerations.filter(acc => ['mined', 'completed', 'failed'].includes(acc.status));
|
||||
})
|
||||
);
|
||||
|
||||
@@ -119,4 +124,15 @@ export class AcceleratorDashboardComponent implements OnInit {
|
||||
return normalColors[feeLevelIndex] || normalColors[mempoolFeeColors.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 992) {
|
||||
this.graphHeight = 330;
|
||||
} else if (window.innerWidth >= 768) {
|
||||
this.graphHeight = 245;
|
||||
} else {
|
||||
this.graphHeight = 210;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="stats-wrapper" *ngIf="accelerationStats$ | async as stats; else loading">
|
||||
<div class="stats-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="address.transactions">Transactions</h5>
|
||||
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.count }}</div>
|
||||
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
|
||||
<div class="symbol" i18n="accelerator.total-pending">pending</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.total-vsize">Total vsize</h5>
|
||||
<h5 class="card-title" i18n="accelerator.total-vsize">Total Vsize</h5>
|
||||
<div class="card-text">
|
||||
<div [innerHTML]="'‎' + (stats.totalVsize * 4 | vbytes: 2)"></div>
|
||||
<div class="symbol">{{ (stats.totalVsize / 1_000_000 * 100).toFixed(2) }}% <span i18n="accelerator.percent-of-next-block"> of next block</span></div>
|
||||
@@ -29,7 +29,7 @@
|
||||
<ng-template #loading>
|
||||
<div class="stats-container loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="address.transactions">Transactions</h5>
|
||||
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
@@ -43,7 +43,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.total-vsize">Total vsize</h5>
|
||||
<h5 class="card-title" i18n="accelerator.total-vsize">Total Vsize</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
|
||||
@@ -43,7 +43,7 @@ export class AddressLabelsComponent implements OnChanges {
|
||||
|
||||
handleVin() {
|
||||
if (this.vin.inner_witnessscript_asm) {
|
||||
if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0) {
|
||||
if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0 || this.vin.inner_witnessscript_asm.indexOf('OP_PUSHNUM_15 OP_CHECKMULTISIG OP_IFDUP OP_NOTIF OP_PUSHBYTES_2') === 1259) {
|
||||
if (this.vin.witness.length > 11) {
|
||||
this.label = 'Liquid Peg Out';
|
||||
} else {
|
||||
|
||||
@@ -31,8 +31,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
addressLoadingStatus$: Observable<number>;
|
||||
addressInfo: null | AddressInformation = null;
|
||||
|
||||
totalConfirmedTxCount = 0;
|
||||
loadedConfirmedTxCount = 0;
|
||||
fullyLoaded = false;
|
||||
txCount = 0;
|
||||
received = 0;
|
||||
sent = 0;
|
||||
@@ -66,7 +65,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
switchMap((params: ParamMap) => {
|
||||
this.error = undefined;
|
||||
this.isLoadingAddress = true;
|
||||
this.loadedConfirmedTxCount = 0;
|
||||
this.fullyLoaded = false;
|
||||
this.address = null;
|
||||
this.isLoadingTransactions = true;
|
||||
this.transactions = null;
|
||||
@@ -105,7 +104,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
.pipe(
|
||||
filter((address) => !!address),
|
||||
tap((address: Address) => {
|
||||
if ((this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') && /^([m-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address.address)) {
|
||||
if ((this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') && /^([a-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address.address)) {
|
||||
this.apiService.validateAddress$(address.address)
|
||||
.subscribe((addressInfo) => {
|
||||
this.addressInfo = addressInfo;
|
||||
@@ -128,7 +127,6 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.tempTransactions = transactions;
|
||||
if (transactions.length) {
|
||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||
this.loadedConfirmedTxCount += transactions.filter((tx) => tx.status.confirmed).length;
|
||||
}
|
||||
|
||||
const fetchTxs: string[] = [];
|
||||
@@ -191,8 +189,6 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.audioService.playSound('magic');
|
||||
}
|
||||
}
|
||||
this.totalConfirmedTxCount++;
|
||||
this.loadedConfirmedTxCount++;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -252,16 +248,19 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
|
||||
if (this.isLoadingTransactions || this.fullyLoaded) {
|
||||
return;
|
||||
}
|
||||
this.isLoadingTransactions = true;
|
||||
this.retryLoadMore = false;
|
||||
this.electrsApiService.getAddressTransactions$(this.address.address, this.lastTransactionTxId)
|
||||
.subscribe((transactions: Transaction[]) => {
|
||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||
this.loadedConfirmedTxCount += transactions.length;
|
||||
this.transactions = this.transactions.concat(transactions);
|
||||
if (transactions && transactions.length) {
|
||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||
this.transactions = this.transactions.concat(transactions);
|
||||
} else {
|
||||
this.fullyLoaded = true;
|
||||
}
|
||||
this.isLoadingTransactions = false;
|
||||
},
|
||||
(error) => {
|
||||
@@ -278,7 +277,6 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.received = this.address.chain_stats.funded_txo_sum + this.address.mempool_stats.funded_txo_sum;
|
||||
this.sent = this.address.chain_stats.spent_txo_sum + this.address.mempool_stats.spent_txo_sum;
|
||||
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
|
||||
this.totalConfirmedTxCount = this.address.chain_stats.tx_count;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</ng-template>
|
||||
<ng-template #default>
|
||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
|
||||
<span class="symbol"><ng-template [ngIf]="network === 'liquid'">L-</ng-template>
|
||||
<span class="symbol"><ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
|
||||
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
|
||||
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
|
||||
<ng-template [ngIf]="network === 'signet'">s</ng-template>BTC</span>
|
||||
|
||||
@@ -23,6 +23,7 @@ export class AmountComponent implements OnInit, OnDestroy {
|
||||
@Input() noFiat = false;
|
||||
@Input() addPlus = false;
|
||||
@Input() blockConversion: Price;
|
||||
@Input() forceBtc: boolean = false;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<ng-template #title>
|
||||
<div class="main-title">
|
||||
<h2>{{ group.name }}</h2>
|
||||
<h2>{{ group['group'].name }}</h2>
|
||||
<div class="sub-title" i18n>Group of {{ group.assets.length | number }} assets</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
|
||||
<a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles tooltip" ngbTooltip="select filter categories to highlight matching transactions">
|
||||
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
|
||||
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
|
||||
</a>
|
||||
<div class="filter-bar">
|
||||
<button class="menu-toggle" (click)="menuOpen = !menuOpen">
|
||||
<fa-icon [icon]="['fas', 'filter']"></fa-icon>
|
||||
<button class="menu-toggle" (click)="menuOpen = !menuOpen" title="Mempool Goggles">
|
||||
<app-svg-images name="goggles" width="100%" height="100%"></app-svg-images>
|
||||
</button>
|
||||
<div class="active-tags">
|
||||
<ng-container *ngFor="let filter of activeFilters;">
|
||||
@@ -14,7 +18,7 @@
|
||||
<h5>{{ group.label }}</h5>
|
||||
<div class="filter-group">
|
||||
<ng-container *ngFor="let filter of group.filters;">
|
||||
<button class="btn filter-tag" [class.selected]="filterFlags[filter.key]" (click)="toggleFilter(filter.key)">{{ filter.label }}</button>
|
||||
<button *ngIf="!disabledFilters[filter.key]" class="btn filter-tag" [class.selected]="filterFlags[filter.key]" (click)="toggleFilter(filter.key)">{{ filter.label }}</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -20,7 +20,21 @@
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.info-badges {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
float: right;
|
||||
|
||||
&:hover, &:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
padding: 0px 1px;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
|
||||
import { FilterGroups, TransactionFilters } from '../../shared/filters.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -7,24 +9,48 @@ import { FilterGroups, TransactionFilters } from '../../shared/filters.utils';
|
||||
templateUrl: './block-filters.component.html',
|
||||
styleUrls: ['./block-filters.component.scss'],
|
||||
})
|
||||
export class BlockFiltersComponent implements OnChanges {
|
||||
export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() cssWidth: number = 800;
|
||||
@Input() excludeFilters: string[] = [];
|
||||
@Output() onFilterChanged: EventEmitter<bigint | null> = new EventEmitter();
|
||||
|
||||
filterSubscription: Subscription;
|
||||
|
||||
filters = TransactionFilters;
|
||||
filterGroups = FilterGroups;
|
||||
disabledFilters: { [key: string]: boolean } = {};
|
||||
activeFilters: string[] = [];
|
||||
filterFlags: { [key: string]: boolean } = {};
|
||||
menuOpen: boolean = false;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.filterSubscription = this.stateService.activeGoggles$.subscribe((activeFilters: string[]) => {
|
||||
for (const key of Object.keys(this.filterFlags)) {
|
||||
this.filterFlags[key] = false;
|
||||
}
|
||||
for (const key of activeFilters) {
|
||||
this.filterFlags[key] = !this.disabledFilters[key];
|
||||
}
|
||||
this.activeFilters = [...activeFilters.filter(key => !this.disabledFilters[key])];
|
||||
this.onFilterChanged.emit(this.getBooleanFlags());
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.cssWidth) {
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
if (changes.excludeFilters) {
|
||||
this.disabledFilters = {};
|
||||
this.excludeFilters.forEach(filter => {
|
||||
this.disabledFilters[filter] = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toggleFilter(key): void {
|
||||
@@ -46,7 +72,9 @@ export class BlockFiltersComponent implements OnChanges {
|
||||
// remove active filter
|
||||
this.activeFilters = this.activeFilters.filter(f => f != key);
|
||||
}
|
||||
this.onFilterChanged.emit(this.getBooleanFlags());
|
||||
const booleanFlags = this.getBooleanFlags();
|
||||
this.onFilterChanged.emit(booleanFlags);
|
||||
this.stateService.activeGoggles$.next([...this.activeFilters]);
|
||||
}
|
||||
|
||||
getBooleanFlags(): bigint | null {
|
||||
@@ -67,4 +95,8 @@ export class BlockFiltersComponent implements OnChanges {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,6 @@
|
||||
[auditEnabled]="auditHighlighting"
|
||||
[blockConversion]="blockConversion"
|
||||
></app-block-overview-tooltip>
|
||||
<app-block-filters *ngIf="showFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
|
||||
<app-block-filters *ngIf="showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
@Input() unavailable: boolean = false;
|
||||
@Input() auditHighlighting: boolean = false;
|
||||
@Input() showFilters: boolean = false;
|
||||
@Input() excludeFilters: string[] = [];
|
||||
@Input() filterFlags: bigint | null = null;
|
||||
@Input() filterMode: 'and' | 'or' = 'and';
|
||||
@Input() blockConversion: Price;
|
||||
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
||||
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
|
||||
@@ -71,6 +73,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
|
||||
searchText: string;
|
||||
searchSubscription: Subscription;
|
||||
filtersAvailable: boolean = true;
|
||||
activeFilterFlags: bigint | null = null;
|
||||
|
||||
constructor(
|
||||
readonly ngZone: NgZone,
|
||||
@@ -110,17 +114,21 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
if (changes.overrideColor && this.scene) {
|
||||
this.scene.setColorFunction(this.overrideColors);
|
||||
}
|
||||
if ((changes.filterFlags || changes.showFilters) && this.scene) {
|
||||
this.setFilterFlags(this.filterFlags);
|
||||
if ((changes.filterFlags || changes.showFilters || changes.filterMode)) {
|
||||
this.setFilterFlags();
|
||||
}
|
||||
}
|
||||
|
||||
setFilterFlags(flags: bigint | null): void {
|
||||
if (flags != null) {
|
||||
this.scene.setColorFunction(this.getFilterColorFunction(flags));
|
||||
} else {
|
||||
this.scene.setColorFunction(this.overrideColors);
|
||||
setFilterFlags(flags?: bigint | null): void {
|
||||
this.activeFilterFlags = this.filterFlags || flags || null;
|
||||
if (this.scene) {
|
||||
if (this.activeFilterFlags != null) {
|
||||
this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags));
|
||||
} else {
|
||||
this.scene.setColorFunction(this.overrideColors);
|
||||
}
|
||||
}
|
||||
this.start();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -149,6 +157,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
|
||||
// initialize the scene without any entry transition
|
||||
setup(transactions: TransactionStripped[]): void {
|
||||
this.filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
|
||||
if (this.scene) {
|
||||
this.scene.setup(transactions);
|
||||
this.readyNextFrame = true;
|
||||
@@ -259,7 +268,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
||||
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
|
||||
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
|
||||
colorFunction: this.overrideColors });
|
||||
colorFunction: this.getColorFunction() });
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
@@ -503,9 +512,19 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.txHoverEvent.emit(hoverId);
|
||||
}
|
||||
|
||||
getColorFunction(): ((tx: TxView) => Color) {
|
||||
if (this.filterFlags) {
|
||||
return this.getFilterColorFunction(this.filterFlags);
|
||||
} else if (this.activeFilterFlags) {
|
||||
return this.getFilterColorFunction(this.activeFilterFlags);
|
||||
} else {
|
||||
return this.overrideColors;
|
||||
}
|
||||
}
|
||||
|
||||
getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) {
|
||||
return (tx: TxView) => {
|
||||
if ((tx.bigintFlags & flags) === flags) {
|
||||
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (tx.bigintFlags & flags) > 0n)) {
|
||||
return defaultColorFunction(tx);
|
||||
} else {
|
||||
return defaultColorFunction(
|
||||
|
||||
@@ -11,7 +11,7 @@ export default class BlockScene {
|
||||
getColor: ((tx: TxView) => Color) = defaultColorFunction;
|
||||
orientation: string;
|
||||
flip: boolean;
|
||||
animationDuration: number = 1000;
|
||||
animationDuration: number = 900;
|
||||
configAnimationOffset: number | null;
|
||||
animationOffset: number;
|
||||
highlightingEnabled: boolean;
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
<td *ngSwitchCase="'accelerated'"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
<tr *ngIf="!auditEnabled && tx && tx.status === 'accelerated'">
|
||||
<td class="td-width"></td>
|
||||
<td><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, tap, throttleTime, catchError, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
|
||||
import { of, Subscription, asyncScheduler } from 'rxjs';
|
||||
import { of, Subscription, asyncScheduler, forkJoin } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
@@ -10,6 +10,7 @@ import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.in
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-preview',
|
||||
@@ -42,7 +43,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
private openGraphService: OpenGraphService,
|
||||
private apiService: ApiService
|
||||
private apiService: ApiService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
@@ -121,21 +123,37 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
this.overviewSubscription = block$.pipe(
|
||||
startWith(null),
|
||||
pairwise(),
|
||||
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
this.overviewError = err;
|
||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
||||
return of([]);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
return of({ transactions, direction: 'down' });
|
||||
})
|
||||
)
|
||||
switchMap(([prevBlock, block]) => {
|
||||
return forkJoin([
|
||||
this.apiService.getStrippedBlockTransactions$(block.id)
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
this.overviewError = err;
|
||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
||||
return of([]);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
return of(transactions);
|
||||
})
|
||||
),
|
||||
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
|
||||
]);
|
||||
}
|
||||
),
|
||||
)
|
||||
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
|
||||
.subscribe(([transactions, accelerations]) => {
|
||||
this.strippedTransactions = transactions;
|
||||
|
||||
const acceleratedInBlock = {};
|
||||
for (const acc of accelerations) {
|
||||
acceleratedInBlock[acc.txid] = acc;
|
||||
}
|
||||
for (const tx of transactions) {
|
||||
if (acceleratedInBlock[tx.txid]) {
|
||||
tx.acc = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoadingOverview = false;
|
||||
if (this.blockGraph) {
|
||||
this.blockGraph.destroy();
|
||||
|
||||
@@ -42,12 +42,12 @@
|
||||
<ng-container *ngIf="!isLoadingBlock; else skeletonRows">
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.hash">Hash</td>
|
||||
<td>‎<a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.id"></app-clipboard></td>
|
||||
<td>‎<a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard [text]="block.id"></app-clipboard></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.timestamp">Timestamp</td>
|
||||
<td>
|
||||
<app-timestamp [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -59,7 +59,7 @@
|
||||
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
||||
</tr>
|
||||
<tr *ngIf="auditAvailable">
|
||||
<td><ng-container i18n="latest-blocks.health">Health</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
|
||||
<td><ng-container i18n="latest-blocks.health">Health</ng-container><a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
|
||||
<td>
|
||||
<span
|
||||
class="health-badge badge"
|
||||
@@ -115,6 +115,8 @@
|
||||
[orientation]="'top'"
|
||||
[flip]="false"
|
||||
[blockConversion]="blockConversion"
|
||||
[showFilters]="true"
|
||||
[excludeFilters]="['replacement']"
|
||||
(txClickEvent)="onTxClick($event)"
|
||||
></app-block-overview-graph>
|
||||
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||
@@ -229,23 +231,29 @@
|
||||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"
|
||||
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
|
||||
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="network !== 'liquid'">
|
||||
<ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
|
||||
<ng-template [ngIf]="!isLoadingOverview" [ngIfElse]="loadingDetailsSkeletons">
|
||||
<ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-sm" *ngIf="!isMobile">
|
||||
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
||||
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container><a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
||||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"
|
||||
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
|
||||
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="network !== 'liquid'">
|
||||
<ng-container *ngTemplateOutlet="actualDetails"></ng-container>
|
||||
<ng-template [ngIf]="!isLoadingOverview" [ngIfElse]="loadingDetailsSkeletons">
|
||||
<ng-container *ngTemplateOutlet="actualDetails"></ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -452,5 +460,24 @@
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingDetailsSkeletons>
|
||||
<table class="table table-borderless table-striped audit-details-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="w-50" i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.weight">Weight</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="mempool-block.transactions">Transactions</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
@@ -57,11 +57,6 @@
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.info-link {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.difference {
|
||||
margin-left: 0.5em;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { detectWebGL } from '../../shared/graphs.utils';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { PriceService, Price } from '../../services/price.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block',
|
||||
@@ -103,6 +104,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
private apiService: ApiService,
|
||||
private priceService: PriceService,
|
||||
private cacheService: CacheService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
}
|
||||
@@ -328,17 +330,28 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.overviewError = err;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
),
|
||||
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
|
||||
]);
|
||||
})
|
||||
)
|
||||
.subscribe(([transactions, blockAudit]) => {
|
||||
.subscribe(([transactions, blockAudit, accelerations]) => {
|
||||
if (transactions) {
|
||||
this.strippedTransactions = transactions;
|
||||
} else {
|
||||
this.strippedTransactions = [];
|
||||
}
|
||||
|
||||
const acceleratedInBlock = {};
|
||||
for (const acc of accelerations) {
|
||||
acceleratedInBlock[acc.txid] = acc;
|
||||
}
|
||||
for (const tx of transactions) {
|
||||
if (acceleratedInBlock[tx.txid]) {
|
||||
tx.acc = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.blockAudit = null;
|
||||
if (transactions && blockAudit) {
|
||||
const inTemplate = {};
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<ng-template #emptyfees>
|
||||
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
|
||||
|
||||
<app-fee-rate unitClass=""></app-fee-rate>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
|
||||
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
<ng-template #emptyfeespan>
|
||||
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fee-span">
|
||||
|
||||
<app-fee-rate unitClass=""></app-fee-rate>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo"
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<a
|
||||
@@ -92,21 +92,18 @@
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="isMempoolModule" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 125px"></span>
|
||||
<span class="skeleton-loader" style="max-width: 150px"></span>
|
||||
</td>
|
||||
<td class="timestamp" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 150px"></span>
|
||||
</td>
|
||||
<td class="mined" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 125px"></span>
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="isMempoolModule" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="isMempoolModule && !widget" class="fees text-right" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<td *ngIf="isMempoolModule && !auditAvailable || isMempoolModule && !widget" class="fees text-right" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="auditAvailable && !widget" class="fee-delta" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
|
||||
@@ -47,20 +47,30 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" *ngIf="showHalving">
|
||||
<h5 class="card-title" i18n="difficulty-box.next-halving" i18n-ngbTooltip="difficulty-box.next-halving"
|
||||
ngbTooltip="Next Halving" placement="bottom" #averagefee [disableTooltip]="!isEllipsisActive(averagefee)">Next Halving</h5>
|
||||
<div class="card-text">
|
||||
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.average-fee" [ngbTooltip]="halvingBlocksLeft" [tooltipContext]="{ epochData: epochData }" placement="bottom">
|
||||
<span>{{ timeUntilHalving | date }}</span>
|
||||
<div class="symbol" *ngIf="blocksUntilHalving === 1; else approxTime">
|
||||
<app-time kind="until" [time]="epochData.timeAvg + now" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
|
||||
</div>
|
||||
<ng-template #approxTime>
|
||||
<div class="symbol">
|
||||
<app-time kind="until" [time]="timeUntilHalving" [fastRender]="false" [fixedRender]="true" [precision]="0" [numUnits]="2" [units]="['year', 'day', 'hour', 'minute']"></app-time>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true" [precision]="1"></app-time></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #halvingBlocksLeft let-epochData="epochData">
|
||||
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingDifficulty>
|
||||
<div class="difficulty-skeleton loading-container">
|
||||
<div class="item">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { combineLatest, Observable, timer } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
interface EpochProgress {
|
||||
@@ -15,6 +15,7 @@ interface EpochProgress {
|
||||
previousRetarget: number;
|
||||
blocksUntilHalving: number;
|
||||
timeUntilHalving: number;
|
||||
timeAvg: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -26,6 +27,9 @@ interface EpochProgress {
|
||||
export class DifficultyMiningComponent implements OnInit {
|
||||
isLoadingWebSocket$: Observable<boolean>;
|
||||
difficultyEpoch$: Observable<EpochProgress>;
|
||||
blocksUntilHalving: number | null = null;
|
||||
timeUntilHalving = 0;
|
||||
now = new Date().getTime();
|
||||
|
||||
@Input() showProgress = true;
|
||||
@Input() showHalving = false;
|
||||
@@ -64,8 +68,9 @@ export class DifficultyMiningComponent implements OnInit {
|
||||
colorPreviousAdjustments = '#ffffff66';
|
||||
}
|
||||
|
||||
const blocksUntilHalving = 210000 - (maxHeight % 210000);
|
||||
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
||||
this.blocksUntilHalving = 210000 - (maxHeight % 210000);
|
||||
this.timeUntilHalving = new Date().getTime() + (this.blocksUntilHalving * 600000);
|
||||
this.now = new Date().getTime();
|
||||
|
||||
const data = {
|
||||
base: `${da.progressPercent.toFixed(2)}%`,
|
||||
@@ -77,8 +82,9 @@ export class DifficultyMiningComponent implements OnInit {
|
||||
newDifficultyHeight: da.nextRetargetHeight,
|
||||
estimatedRetargetDate: da.estimatedRetargetDate,
|
||||
previousRetarget: da.previousRetarget,
|
||||
blocksUntilHalving,
|
||||
timeUntilHalving,
|
||||
blocksUntilHalving: this.blocksUntilHalving,
|
||||
timeUntilHalving: this.timeUntilHalving,
|
||||
timeAvg: da.timeAvg,
|
||||
};
|
||||
return data;
|
||||
})
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
|
||||
@@ -57,8 +57,6 @@
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.pool-distribution {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { merge, Observable, of } from 'rxjs';
|
||||
import { combineLatest, fromEvent, merge, Observable, of } from 'rxjs';
|
||||
import { map, mergeMap, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
@@ -31,6 +31,7 @@ import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
export class HashrateChartComponent implements OnInit {
|
||||
@Input() tableOnly = false;
|
||||
@Input() widget = false;
|
||||
@Input() height: number = 300;
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
|
||||
@@ -86,28 +87,32 @@ export class HashrateChartComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
|
||||
this.hashrateObservable$ = merge(
|
||||
this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith(this.radioGroupForm.controls.dateSpan.value),
|
||||
switchMap((timespan) => {
|
||||
if (!this.widget && !firstRun) {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
}
|
||||
this.timespan = timespan;
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalHashrate$(this.timespan);
|
||||
})
|
||||
),
|
||||
this.stateService.chainTip$
|
||||
this.hashrateObservable$ = combineLatest(
|
||||
merge(
|
||||
this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
startWith(this.radioGroupForm.controls.dateSpan.value),
|
||||
switchMap((timespan) => {
|
||||
if (!this.widget && !firstRun) {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
}
|
||||
this.timespan = timespan;
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalHashrate$(this.timespan);
|
||||
})
|
||||
)
|
||||
),
|
||||
this.stateService.chainTip$
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
return this.apiService.getHistoricalHashrate$(this.timespan);
|
||||
})
|
||||
)
|
||||
),
|
||||
fromEvent(window, 'resize').pipe(startWith(null)),
|
||||
).pipe(
|
||||
map(([response, _]) => response),
|
||||
tap((response: any) => {
|
||||
const data = response.body;
|
||||
|
||||
@@ -221,6 +226,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
]),
|
||||
],
|
||||
grid: {
|
||||
height: (this.widget && this.height) ? this.height - 30 : undefined,
|
||||
top: this.widget ? 20 : 40,
|
||||
bottom: this.widget ? 30 : 70,
|
||||
right: this.right,
|
||||
|
||||
@@ -11,6 +11,13 @@ import { MiningService } from '../../services/mining.service';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
interface Hashrate {
|
||||
timestamp: number;
|
||||
avgHashRate: number;
|
||||
share: number;
|
||||
poolName: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-hashrate-chart-pools',
|
||||
templateUrl: './hashrate-chart-pools.component.html',
|
||||
@@ -32,6 +39,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
miningWindowPreference: string;
|
||||
radioGroupForm: UntypedFormGroup;
|
||||
|
||||
hashrates: Hashrate[];
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
@@ -87,56 +95,9 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
return this.apiService.getHistoricalPoolsHashrate$(timespan)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
const hashrates = response.body;
|
||||
this.hashrates = response.body;
|
||||
// Prepare series (group all hashrates data point by pool)
|
||||
const grouped = {};
|
||||
for (const hashrate of hashrates) {
|
||||
if (!grouped.hasOwnProperty(hashrate.poolName)) {
|
||||
grouped[hashrate.poolName] = [];
|
||||
}
|
||||
grouped[hashrate.poolName].push(hashrate);
|
||||
}
|
||||
|
||||
const series = [];
|
||||
const legends = [];
|
||||
for (const name in grouped) {
|
||||
series.push({
|
||||
zlevel: 0,
|
||||
stack: 'Total',
|
||||
name: name,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: grouped[name].map((val) => [val.timestamp * 1000, val.share * 100]),
|
||||
type: 'line',
|
||||
lineStyle: { width: 0 },
|
||||
areaStyle: { opacity: 1 },
|
||||
smooth: true,
|
||||
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()],
|
||||
emphasis: {
|
||||
disabled: true,
|
||||
scale: false,
|
||||
},
|
||||
});
|
||||
|
||||
legends.push({
|
||||
name: name,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
itemStyle: {
|
||||
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.prepareChartOptions({
|
||||
legends: legends,
|
||||
series: series,
|
||||
});
|
||||
this.isLoading = false;
|
||||
|
||||
const series = this.applyHashrates();
|
||||
if (series.length === 0) {
|
||||
this.cd.markForCheck();
|
||||
throw new Error();
|
||||
@@ -156,6 +117,77 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
applyHashrates(): any[] {
|
||||
const times: { [time: number]: { hashrates: { [pool: string]: Hashrate } } } = {};
|
||||
const pools = {};
|
||||
for (const hashrate of this.hashrates) {
|
||||
if (!times[hashrate.timestamp]) {
|
||||
times[hashrate.timestamp] = { hashrates: {} };
|
||||
}
|
||||
times[hashrate.timestamp].hashrates[hashrate.poolName] = hashrate;
|
||||
if (!pools[hashrate.poolName]) {
|
||||
pools[hashrate.poolName] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const sortedTimes = Object.keys(times).sort((a,b) => parseInt(a) - parseInt(b)).map(time => ({ time: parseInt(time), hashrates: times[time].hashrates }));
|
||||
const lastHashrates = sortedTimes[sortedTimes.length - 1].hashrates;
|
||||
const sortedPools = Object.keys(pools).sort((a,b) => {
|
||||
if (lastHashrates[b]?.share ?? lastHashrates[a]?.share ?? false) {
|
||||
// sort by descending share of hashrate in latest period
|
||||
return (lastHashrates[b]?.share || 0) - (lastHashrates[a]?.share || 0);
|
||||
} else {
|
||||
// tiebreak by pool name
|
||||
b < a;
|
||||
}
|
||||
});
|
||||
|
||||
const series = [];
|
||||
const legends = [];
|
||||
for (const name of sortedPools) {
|
||||
const data = sortedTimes.map(({ time, hashrates }) => {
|
||||
return [time * 1000, (hashrates[name]?.share || 0) * 100];
|
||||
});
|
||||
series.push({
|
||||
zlevel: 0,
|
||||
stack: 'Total',
|
||||
name: name,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data,
|
||||
type: 'line',
|
||||
lineStyle: { width: 0 },
|
||||
areaStyle: { opacity: 1 },
|
||||
smooth: true,
|
||||
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()],
|
||||
emphasis: {
|
||||
disabled: true,
|
||||
scale: false,
|
||||
},
|
||||
});
|
||||
|
||||
legends.push({
|
||||
name: name,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
itemStyle: {
|
||||
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.prepareChartOptions({
|
||||
legends: legends,
|
||||
series: series,
|
||||
});
|
||||
this.isLoading = false;
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
prepareChartOptions(data) {
|
||||
let title: object;
|
||||
if (data.series.length === 0) {
|
||||
@@ -256,6 +288,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
},
|
||||
}],
|
||||
};
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
|
||||
@@ -20,14 +20,13 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
|
||||
@Input() data: any;
|
||||
pegsChartOptions: EChartsOption;
|
||||
|
||||
height: number | string = '200';
|
||||
height: number | string = '320';
|
||||
right: number | string = '10';
|
||||
top: number | string = '20';
|
||||
left: number | string = '50';
|
||||
template: ('widget' | 'advanced') = 'widget';
|
||||
isLoading = true;
|
||||
|
||||
pegsChartOption: EChartsOption = {};
|
||||
pegsChartInitOption = {
|
||||
renderer: 'svg'
|
||||
};
|
||||
@@ -41,20 +40,24 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (!this.data) {
|
||||
if (!this.data?.liquidPegs) {
|
||||
return;
|
||||
}
|
||||
this.pegsChartOptions = this.createChartOptions(this.data.series, this.data.labels);
|
||||
if (!this.data.liquidReserves) {
|
||||
this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels);
|
||||
} else {
|
||||
this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels, this.data.liquidReserves.series);
|
||||
}
|
||||
}
|
||||
|
||||
rendered() {
|
||||
if (!this.data) {
|
||||
if (!this.data.liquidPegs) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
createChartOptions(series: number[], labels: string[]): EChartsOption {
|
||||
createChartOptions(pegSeries: number[], labels: string[], reservesSeries?: number[],): EChartsOption {
|
||||
return {
|
||||
grid: {
|
||||
height: this.height,
|
||||
@@ -99,17 +102,18 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
|
||||
type: 'line',
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const colorSpan = (color: string) => `<span class="indicator" style="background-color: #116761;"></span>`;
|
||||
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ${color};"></span>`;
|
||||
let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
|
||||
params.map((item: any, index: number) => {
|
||||
for (let index = params.length - 1; index >= 0; index--) {
|
||||
const item = params[index];
|
||||
if (index < 26) {
|
||||
itemFormatted += `<div class="item">
|
||||
<div class="indicator-container">${colorSpan(item.color)}</div>
|
||||
<div class="grow"></div>
|
||||
<div class="value">${formatNumber(item.value, this.locale, '1.2-2')} <span class="symbol">L-BTC</span></div>
|
||||
<div style="margin-right: 5px"></div>
|
||||
<div class="value">${formatNumber(item.value, this.locale, '1.2-2')} <span class="symbol">${item.seriesName}</span></div>
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
|
||||
}
|
||||
},
|
||||
@@ -138,20 +142,34 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: series,
|
||||
data: pegSeries,
|
||||
name: 'L-BTC',
|
||||
color: '#116761',
|
||||
type: 'line',
|
||||
stack: 'total',
|
||||
smooth: false,
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.2,
|
||||
color: '#116761',
|
||||
},
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
width: 2,
|
||||
color: '#116761',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: reservesSeries,
|
||||
name: 'BTC',
|
||||
color: '#EA983B',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#EA983B',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
|
||||
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
||||
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
||||
<a ngbDropdownItem *ngIf="env.BISQ_ENABLED" [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
||||
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||
</div>
|
||||
@@ -78,6 +78,9 @@
|
||||
<li class="nav-item" routerLinkActive="active" id="btn-assets">
|
||||
<a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-audit">
|
||||
<a class="nav-link" [routerLink]="['/audit']" (click)="collapse()"><fa-icon [icon]="['fas', 'scale-balanced']" [fixedWidth]="true" i18n-title="master-page.btc-reserves-audit" title="BTC Reserves Audit"></fa-icon></a>
|
||||
</li>
|
||||
<li [hidden]="isMobile" class="nav-item mr-2" routerLinkActive="active" id="btn-docs">
|
||||
<a class="nav-link" [routerLink]="['/docs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'book']" [fixedWidth]="true" i18n-title="master-page.docs" title="Docs"></fa-icon></a>
|
||||
</li>
|
||||
@@ -98,4 +101,4 @@
|
||||
|
||||
<app-global-footer *ngIf="footerVisible"></app-global-footer>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<div [ngClass]="{'widget': widget}">
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="min-height: 295px">
|
||||
<table class="table table-borderless">
|
||||
<thead style="vertical-align: middle;">
|
||||
<th class="address text-left" [ngClass]="{'widget': widget}" i18n="shared.address">Address</th>
|
||||
<th class="amount text-right" [ngClass]="{'widget': widget}" i18n="address.balance">Balance</th>
|
||||
</thead>
|
||||
<tbody *ngIf="federationAddresses$ | async as addresses; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<ng-container *ngIf="widget; else regularRows">
|
||||
<tr *ngFor="let address of addresses | slice:0:5">
|
||||
<td class="address text-left widget">
|
||||
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + address.bitcoinaddress }}" target="_blank" style="color:#b86d12">
|
||||
<app-truncate [text]="address.bitcoinaddress" [lastChars]="6"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="amount text-right widget">
|
||||
<app-amount [satoshis]="+address.balance" [noFiat]="true" [forceBtc]="true"></app-amount>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template #regularRows>
|
||||
<tr *ngFor="let address of addresses | slice:(page - 1) * pageSize:page * pageSize">
|
||||
<td class="address text-left">
|
||||
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + address.bitcoinaddress }}" target="_blank" style="color:#b86d12">
|
||||
<app-truncate [text]="address.bitcoinaddress" [lastChars]="6"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="amount text-right">
|
||||
<app-amount [satoshis]="+address.balance" [noFiat]="true" [forceBtc]="true"></app-amount>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</tbody>
|
||||
<ng-template #skeleton>
|
||||
<tbody *ngIf="widget; else regularRowsSkeleton">
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="address text-left widget">
|
||||
<span class="skeleton-loader" style="max-width: 400px"></span>
|
||||
</td>
|
||||
<td class="amount text-right widget">
|
||||
<span class="skeleton-loader" style="max-width: 350px"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<ng-template #regularRowsSkeleton>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="address text-left">
|
||||
<span class="skeleton-loader" style="max-width: 600px"></span>
|
||||
</td>
|
||||
<td class="amount text-right">
|
||||
<span class="skeleton-loader" style="max-width: 400px"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</table>
|
||||
|
||||
<ngb-pagination *ngIf="!widget && federationAddresses$ | async as addresses" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
|
||||
[collectionSize]="addresses.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
|
||||
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination>
|
||||
|
||||
<ng-template [ngIf]="!widget">
|
||||
<div class="clearfix"></div>
|
||||
<br>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.address {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
}
|
||||
.address.widget {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.amount {
|
||||
width: 25%;
|
||||
}
|
||||
.amount.widget {
|
||||
width: 40%;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
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, FederationAddress } from '../../../interfaces/node-api.interface';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-federation-addresses-list',
|
||||
templateUrl: './federation-addresses-list.component.html',
|
||||
styleUrls: ['./federation-addresses-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FederationAddressesListComponent implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() federationAddresses$: Observable<FederationAddress[]>;
|
||||
|
||||
env: Env;
|
||||
isLoading = true;
|
||||
page = 1;
|
||||
pageSize = 15;
|
||||
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
|
||||
skeletonLines: number[] = [];
|
||||
auditStatus$: Observable<AuditStatus>;
|
||||
auditUpdated$: Observable<boolean>;
|
||||
lastReservesBlockUpdate: number = 0;
|
||||
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(5).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.federationAddresses$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.federationAddresses$()),
|
||||
tap(_ => this.isLoading = false),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next(1);
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<div *ngIf="(federationAddresses$ | async) as federationAddresses; else loadingData">
|
||||
|
||||
<div class="fee-estimation-container">
|
||||
<div class="item">
|
||||
<a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]">
|
||||
<h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container> <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="shared.addresses">addresses</span></div>
|
||||
<span class="fiat" *ngIf="(federationAddressesOneMonthAgo$ | async) as federationAddressesOneMonthAgo; else loadingSkeleton" i18n-ngbTooltip="liquid.percentage-change-last-month" ngbTooltip="Percentage change past month" placement="bottom">
|
||||
<app-change [current]="federationAddresses.length" [previous]="federationAddressesOneMonthAgo.addresses_count_one_month"></app-change>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingData>
|
||||
<div class="fee-estimation-container loading-container">
|
||||
<div class="item">
|
||||
<a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]">
|
||||
<h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||
</a>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingSkeleton>
|
||||
<div class="skeleton-loader skeleton-loader-transactions" style="margin-top: 2px; margin-bottom: 5px;"></div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,75 @@
|
||||
.fee-estimation-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item {
|
||||
max-width: 300px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
color: #4a68b9;
|
||||
font-size: 10px;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
span {
|
||||
font-size: 11px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-text span {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
top: 0px;
|
||||
}
|
||||
.fee-text{
|
||||
border-bottom: 1px solid #ffffff1c;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
line-height: 1.45;
|
||||
padding: 0px 2px;
|
||||
}
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container{
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
max-width: 90px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 3px;
|
||||
max-width: 55px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { FederationAddress } from '../../../interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-federation-addresses-stats',
|
||||
templateUrl: './federation-addresses-stats.component.html',
|
||||
styleUrls: ['./federation-addresses-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FederationAddressesStatsComponent implements OnInit {
|
||||
@Input() federationAddresses$: Observable<FederationAddress[]>;
|
||||
@Input() federationAddressesOneMonthAgo$: Observable<any>;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<div [ngClass]="{'widget': widget}">
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="min-height: 295px">
|
||||
<table class="table table-borderless">
|
||||
<thead style="vertical-align: middle;">
|
||||
<th class="txid text-left" [ngClass]="{'widget': widget}" i18n="transaction.output">Output</th>
|
||||
<th class="address text-left" *ngIf="!widget" i18n="shared.address">Address</th>
|
||||
<th class="amount text-right" [ngClass]="{'widget': widget}" i18n="shared.amount">Amount</th>
|
||||
<th class="pegin text-left" *ngIf="!widget" i18n="liquid.related-peg-in">Related Peg-In</th>
|
||||
<th class="timestamp text-left" i18n="shared.date" [ngClass]="{'widget': widget}">Date</th>
|
||||
</thead>
|
||||
<tbody *ngIf="federationUtxos$ | async as utxos; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<ng-container *ngIf="widget; else regularRows">
|
||||
<tr *ngFor="let utxo of utxos | slice:0:6">
|
||||
<td class="txid text-left widget">
|
||||
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + utxo.txid + ':' + utxo.txindex }}" target="_blank" style="color:#b86d12">
|
||||
<app-truncate [text]="utxo.txid + ':' + utxo.txindex" [lastChars]="6"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="amount text-right widget">
|
||||
<app-amount [satoshis]="utxo.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
|
||||
</td>
|
||||
<td class="timestamp text-left widget">
|
||||
<app-time kind="since" [time]="utxo.blocktime"></app-time>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template #regularRows>
|
||||
<tr *ngFor="let utxo of utxos | slice:(page - 1) * pageSize:page * pageSize">
|
||||
<td class="txid text-left">
|
||||
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + utxo.txid + ':' + utxo.txindex }}" target="_blank" style="color:#b86d12">
|
||||
<app-truncate [text]="utxo.txid + ':' + utxo.txindex" [lastChars]="6"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="address text-left">
|
||||
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + utxo.bitcoinaddress }}" target="_blank" style="color:#b86d12">
|
||||
<app-truncate [text]="utxo.bitcoinaddress" [lastChars]="6"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="amount text-right">
|
||||
<app-amount [satoshis]="utxo.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
|
||||
</td>
|
||||
<td class="pegin text-left">
|
||||
<ng-container *ngIf="utxo.pegtxid; else noPeginMessage">
|
||||
<a [routerLink]="['/tx' | relativeUrl, utxo.pegtxid]" [fragment]="'vin=' + utxo.pegindex">
|
||||
<app-truncate [text]="utxo.pegtxid + ':' + utxo.pegindex" [lastChars]="6"></app-truncate>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-template #noPeginMessage>
|
||||
<i><span class="text-muted" i18n="liquid.change-output">Change output</span></i>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td class="timestamp text-left">
|
||||
‎{{ utxo.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="utxo.blocktime"></app-time>)</i></div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</tbody>
|
||||
<ng-template #skeleton>
|
||||
<tbody *ngIf="widget; else regularRowsSkeleton">
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="txid 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-left widget">
|
||||
<span class="skeleton-loader" style="max-width: 300px"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<ng-template #regularRowsSkeleton>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="txid text-left">
|
||||
<span class="skeleton-loader" style="max-width: 300px"></span>
|
||||
</td>
|
||||
<td class="address 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="pegin text-left">
|
||||
<span class="skeleton-loader" style="max-width: 300px"></span>
|
||||
</td>
|
||||
<td class="timestamp text-left">
|
||||
<span class="skeleton-loader" style="max-width: 140px"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</table>
|
||||
|
||||
<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>
|
||||
|
||||
<ng-template [ngIf]="!widget">
|
||||
<div class="clearfix"></div>
|
||||
<br>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,94 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.txid {
|
||||
width: 25%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
}
|
||||
.txid.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%;
|
||||
}
|
||||
|
||||
.pegin {
|
||||
width: 25%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
@media (max-width: 872px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
width: 18%;
|
||||
@media (max-width: 800px) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
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 } from '../../../interfaces/node-api.interface';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-federation-utxos-list',
|
||||
templateUrl: './federation-utxos-list.component.html',
|
||||
styleUrls: ['./federation-utxos-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FederationUtxosListComponent implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() federationUtxos$: Observable<FederationUtxo[]>;
|
||||
|
||||
env: Env;
|
||||
isLoading = true;
|
||||
page = 1;
|
||||
pageSize = 15;
|
||||
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
|
||||
skeletonLines: number[] = [];
|
||||
auditStatus$: Observable<AuditStatus>;
|
||||
auditUpdated$: Observable<boolean>;
|
||||
lastReservesBlockUpdate: number = 0;
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next(1);
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<div class="container-xl">
|
||||
<div>
|
||||
<h1 i18n="liquid.federation-wallet">Liquid Federation Wallet</h1>
|
||||
</div>
|
||||
|
||||
<div class="nav-container">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="['/audit/wallet/utxos' | relativeUrl]" routerLinkActive="active">UTXOs</a>
|
||||
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]" routerLinkActive="active"><ng-container i18n="mining.addresses">Addresses</ng-container></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -0,0 +1,13 @@
|
||||
ul {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.nav-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-federation-wallet',
|
||||
templateUrl: './federation-wallet.component.html',
|
||||
styleUrls: ['./federation-wallet.component.scss']
|
||||
})
|
||||
export class FederationWalletComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private seoService: SeoService
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@993e5bc509c26db81d93018e24a6afe6e50cae52:Liquid Federation Wallet`);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<div class="container-xl">
|
||||
<div [ngClass]="{'widget': widget}">
|
||||
|
||||
<div *ngIf="!widget">
|
||||
<h1 i18n="liquid.recent-pegs">Recent Peg-In / Out's</h1>
|
||||
</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="timestamp text-left" i18n="shared.date" [ngClass]="{'widget': widget}">Date</th>
|
||||
<th class="amount text-right" [ngClass]="{'widget': widget}" i18n="shared.amount">Amount</th>
|
||||
<th class="output text-left" *ngIf="!widget" i18n="liquid.fund-redemption-tx">Fund / Redemption Tx</th>
|
||||
<th class="address text-left" *ngIf="!widget" i18n="liquid.bitcoin-address">BTC Address</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:5">
|
||||
<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"></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"></app-truncate>
|
||||
</a>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td class="timestamp text-left widget">
|
||||
<app-time kind="since" [time]="peg.blocktime"></app-time>
|
||||
</td>
|
||||
<td class="amount text-right widget" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}">
|
||||
<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
|
||||
</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="timestamp text-left">
|
||||
‎{{ 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>
|
||||
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}">
|
||||
<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
|
||||
</td>
|
||||
<td class="output text-left">
|
||||
<ng-container *ngIf="peg.bitcointxid; else redeemInProgress">
|
||||
<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>
|
||||
<ng-template #redeemInProgress>
|
||||
<ng-container *ngIf="peg.bitcoinaddress; else noRedeem">
|
||||
<i><span class="text-muted" i18n="liquid.redemption-in-progress">Peg out in progress...</span></i>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td class="address text-left">
|
||||
<ng-container *ngIf="peg.bitcoinaddress; else noRedeem">
|
||||
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + peg.bitcoinaddress }}" target="_blank" style="color:#b86d12">
|
||||
<app-truncate [text]="peg.bitcoinaddress" [lastChars]="6"></app-truncate>
|
||||
</a>
|
||||
</ng-container>
|
||||
</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="timestamp text-left widget">
|
||||
<span class="skeleton-loader" style="max-width: 300px"></span>
|
||||
</td>
|
||||
<td class="amount 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="timestamp text-left">
|
||||
<span class="skeleton-loader" style="max-width: 140px"></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="address text-left">
|
||||
<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 #noRedeem>
|
||||
<span class="text-muted">-</span>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,120 @@
|
||||
.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: 20%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 120px;
|
||||
}
|
||||
.transaction.widget {
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.address {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
@media (max-width: 527px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.amount {
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.output {
|
||||
width: 20%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
@media (max-width: 800px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.address {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
@media (max-width: 960px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
width: 0%;
|
||||
@media (max-width: 650px) {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 1000px) {
|
||||
.relative-time {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.timestamp.widget {
|
||||
@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;
|
||||
}
|
||||
|
||||
.glow-effect {
|
||||
animation: color-oscillation 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes color-oscillation {
|
||||
0% {
|
||||
color: #777983;
|
||||
}
|
||||
100% {
|
||||
color: #D81B60;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
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';
|
||||
import { SeoService } from '../../../services/seo.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[]> = of([]);
|
||||
@Input() recentPegOuts$: Observable<RecentPeg[]> = of([]);
|
||||
|
||||
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,
|
||||
private seoService: SeoService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLoading = !this.widget;
|
||||
this.env = this.stateService.env;
|
||||
this.skeletonLines = this.widget === true ? [...Array(5).keys()] : [...Array(15).keys()];
|
||||
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`:@@a8b0889ea1b41888f1e247f2731cc9322198ca04:Recent Peg-In / Out's`);
|
||||
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$()),
|
||||
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,
|
||||
bitcoinaddress: utxo.bitcoinaddress,
|
||||
bitcointxid: utxo.txid,
|
||||
bitcoinindex: utxo.txindex,
|
||||
blocktime: utxo.pegblocktime,
|
||||
}
|
||||
})),
|
||||
share()
|
||||
);
|
||||
|
||||
this.recentPegOuts$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.recentPegOuts$()),
|
||||
share()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
this.recentPegs$ = combineLatest([
|
||||
this.recentPegIns$,
|
||||
this.recentPegOuts$
|
||||
]).pipe(
|
||||
map(([recentPegIns, recentPegOuts]) => {
|
||||
return [
|
||||
...recentPegIns,
|
||||
...recentPegOuts
|
||||
].sort((a, b) => {
|
||||
return b.blocktime - a.blocktime;
|
||||
});
|
||||
}),
|
||||
filter(recentPegs => recentPegs.length > 0),
|
||||
tap(_ => this.isLoading = false),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next(1);
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<div *ngIf="(pegsVolume$ | async) as pegsVolume; else loadingData">
|
||||
<div class="fee-estimation-container">
|
||||
<div class="item">
|
||||
<a class="title-link" [routerLink]="['/audit/pegs' | relativeUrl]">
|
||||
<h5 class="card-title"><ng-container i18n="liquid.recent-pegs">Recent Peg-In / Out's</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fee-estimation-container">
|
||||
<div class="item">
|
||||
<div class="card-text">
|
||||
<div class="fee-text credit" i18n-ngbTooltip="liquid.peg-ins-volume-day" ngbTooltip="24h Peg-In Volume" placement="top">+{{ (+pegsVolume[0].volume) / 100000000 | number: '1.2-2' }} <span i18n="shared.addresses">BTC</span></div>
|
||||
<div class="fiat">{{ (+pegsVolume[0].number) }} <span i18n="liquid.peg-ins">Peg-Ins</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="card-text">
|
||||
<div class="fee-text debit" i18n-ngbTooltip="liquid.peg-out-volume-day" ngbTooltip="24h Peg-Out Volume" placement="top">{{ (+pegsVolume[1].volume) / 100000000 | number: '1.2-2' }} <span i18n="shared.addresses">BTC</span></div>
|
||||
<div class="fiat">{{ (+pegsVolume[1].number) }} <span i18n="liquid.peg-outs">Peg-Outs</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingData>
|
||||
<div class="fee-estimation-container loading-container">
|
||||
<div class="item">
|
||||
<a class="title-link" [routerLink]="['/audit/pegs' | relativeUrl]">
|
||||
<h5 class="card-title"><ng-container i18n="liquid.recent-pegs">Recent Peg-In / Out's</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fee-estimation-container">
|
||||
<div class="item">
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,79 @@
|
||||
.fee-estimation-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item {
|
||||
max-width: 300px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
color: #4a68b9;
|
||||
font-size: 10px;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
span {
|
||||
font-size: 11px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-text span {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
top: 0px;
|
||||
}
|
||||
.fee-text{
|
||||
border-bottom: 1px solid #ffffff1c;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
line-height: 1.45;
|
||||
padding: 0px 2px;
|
||||
}
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-text {
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
max-width: 90px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 3px;
|
||||
max-width: 55px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.credit {
|
||||
color: #7CB342;
|
||||
}
|
||||
|
||||
.debit {
|
||||
color: #D81B60;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PegsVolume } from '../../../interfaces/node-api.interface';
|
||||
|
||||
@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 {
|
||||
@Input() pegsVolume$: Observable<PegsVolume[]>;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<div class="container-xl dashboard-container" *ngIf="(auditStatus$ | async)?.isAuditSynced; else auditInProgress">
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-reserves-supply-stats [currentPeg$]="currentPeg$" [currentReserves$]="currentReserves$"></app-reserves-supply-stats>
|
||||
<app-reserves-ratio [currentPeg]="currentPeg$ | async" [currentReserves]="currentReserves$ | async"></app-reserves-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<app-reserves-ratio-stats [fullHistory$]="fullHistory$"></app-reserves-ratio-stats>
|
||||
</div>
|
||||
<div class="card-body pl-0" style="padding-top: 10px;">
|
||||
<app-reserves-ratio-graph [data]="fullHistory$ | async"></app-reserves-ratio-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-recent-pegs-stats [pegsVolume$]="pegsVolume$"></app-recent-pegs-stats>
|
||||
<app-recent-pegs-list [recentPegIns$]="recentPegIns$" [recentPegOuts$]="recentPegOuts$"[widget]="true"></app-recent-pegs-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-federation-addresses-stats [federationAddresses$]="federationAddresses$" [federationAddressesOneMonthAgo$]="federationAddressesOneMonthAgo$"></app-federation-addresses-stats>
|
||||
<app-federation-addresses-list [federationAddresses$]="federationAddresses$" [widget]="true"></app-federation-addresses-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-template #loadingSkeleton>
|
||||
<div class="container-xl dashboard-container">
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-reserves-supply-stats></app-reserves-supply-stats>
|
||||
<app-reserves-ratio></app-reserves-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<app-reserves-ratio-stats></app-reserves-ratio-stats>
|
||||
</div>
|
||||
<div class="card-body pl-0" style="padding-top: 10px;">
|
||||
<app-reserves-ratio-graph></app-reserves-ratio-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-recent-pegs-stats></app-recent-pegs-stats>
|
||||
<app-recent-pegs-list [widget]="true"></app-recent-pegs-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-federation-addresses-stats></app-federation-addresses-stats>
|
||||
<app-federation-addresses-list [widget]="true"></app-federation-addresses-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #auditInProgress>
|
||||
<ng-container *ngIf="(auditStatus$ | async) as auditStatus; else loadingSkeleton">
|
||||
<div class="in-progress-message" *ngIf="auditStatus.lastBlockAudit && auditStatus.bitcoinHeaders; else loadingSkeleton">
|
||||
<span i18n="liquid.audit-in-progress">Audit in progress: Bitcoin block height #{{ auditStatus.lastBlockAudit }} / #{{ auditStatus.bitcoinHeaders }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,138 @@
|
||||
.dashboard-container {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
.col {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #1d1f31;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.card-body.pool-ranking {
|
||||
padding: 1.25rem 0.25rem 0.75rem 0.25rem;
|
||||
}
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
#blockchain-container {
|
||||
position: relative;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
#blockchain-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fade-border {
|
||||
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
|
||||
}
|
||||
|
||||
.in-progress-message {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 24px 20px !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
max-width: 90px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 3px;
|
||||
max-width: 55px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.lastest-blocks-table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
padding-bottom: 0.8rem !important;
|
||||
}
|
||||
.table-cell-height {
|
||||
width: 25%;
|
||||
}
|
||||
.table-cell-fee {
|
||||
width: 25%;
|
||||
text-align: right;
|
||||
}
|
||||
.table-cell-pool {
|
||||
text-align: left;
|
||||
width: 30%;
|
||||
|
||||
@media (max-width: 875px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pool-name {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
.table-cell-acceleration-count {
|
||||
text-align: right;
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
height: 385px;
|
||||
}
|
||||
.list-card {
|
||||
height: 410px;
|
||||
@media (max-width: 767px) {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mempool-block-wrapper {
|
||||
max-height: 380px;
|
||||
max-width: 380px;
|
||||
margin: auto;
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { Observable, 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, PegsVolume, RecentPeg } from '../../../interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reserves-audit-dashboard',
|
||||
templateUrl: './reserves-audit-dashboard.component.html',
|
||||
styleUrls: ['./reserves-audit-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReservesAuditDashboardComponent implements OnInit {
|
||||
auditStatus$: Observable<AuditStatus>;
|
||||
auditUpdated$: Observable<boolean>;
|
||||
currentPeg$: Observable<CurrentPegs>;
|
||||
currentReserves$: Observable<CurrentPegs>;
|
||||
federationUtxos$: Observable<FederationUtxo[]>;
|
||||
recentPegIns$: Observable<RecentPeg[]>;
|
||||
recentPegOuts$: Observable<RecentPeg[]>;
|
||||
pegsVolume$: Observable<PegsVolume[]>;
|
||||
federationAddresses$: Observable<FederationAddress[]>;
|
||||
federationAddressesOneMonthAgo$: Observable<any>;
|
||||
liquidPegsMonth$: Observable<any>;
|
||||
liquidReservesMonth$: Observable<any>;
|
||||
fullHistory$: Observable<any>;
|
||||
isLoad: boolean = true;
|
||||
private lastPegBlockUpdate: number = 0;
|
||||
private lastPegAmount: string = '';
|
||||
private lastReservesBlockUpdate: number = 0;
|
||||
|
||||
private destroy$ = new Subject();
|
||||
|
||||
constructor(
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
private apiService: ApiService,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@liquid.reserves-audit:Reserves Audit Dashboard`);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks', 'mempool-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.lastPegAmount = currentPegAmount;
|
||||
return of(blockAuditCheck || amountCheck);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
|
||||
this.currentReserves$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ =>
|
||||
this.apiService.liquidReserves$().pipe(
|
||||
filter((currentReserves) => currentReserves.lastBlockUpdate >= this.lastReservesBlockUpdate),
|
||||
tap((currentReserves) => {
|
||||
this.lastReservesBlockUpdate = currentReserves.lastBlockUpdate;
|
||||
})
|
||||
)
|
||||
),
|
||||
share()
|
||||
);
|
||||
|
||||
this.federationUtxos$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.federationUtxos$()),
|
||||
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,
|
||||
bitcoinaddress: utxo.bitcoinaddress,
|
||||
bitcointxid: utxo.txid,
|
||||
bitcoinindex: utxo.txindex,
|
||||
blocktime: utxo.pegblocktime,
|
||||
}
|
||||
})),
|
||||
share()
|
||||
);
|
||||
|
||||
this.recentPegOuts$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.recentPegOuts$()),
|
||||
share()
|
||||
);
|
||||
|
||||
this.pegsVolume$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.pegsVolume$()),
|
||||
share()
|
||||
);
|
||||
|
||||
this.federationAddresses$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.federationAddresses$()),
|
||||
share()
|
||||
);
|
||||
|
||||
this.federationAddressesOneMonthAgo$ = interval(60 * 60 * 1000)
|
||||
.pipe(
|
||||
startWith(0),
|
||||
switchMap(() => this.apiService.federationAddressesOneMonthAgo$())
|
||||
);
|
||||
|
||||
this.liquidPegsMonth$ = interval(60 * 60 * 1000)
|
||||
.pipe(
|
||||
startWith(0),
|
||||
switchMap(() => this.apiService.listLiquidPegsMonth$()),
|
||||
map((pegs) => {
|
||||
const labels = pegs.map(stats => stats.date);
|
||||
const series = pegs.map(stats => parseFloat(stats.amount) / 100000000);
|
||||
series.reduce((prev, curr, i) => series[i] = prev + curr, 0);
|
||||
return {
|
||||
series,
|
||||
labels
|
||||
};
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
this.liquidReservesMonth$ = interval(60 * 60 * 1000).pipe(
|
||||
startWith(0),
|
||||
switchMap(() => this.apiService.listLiquidReservesMonth$()),
|
||||
map(reserves => {
|
||||
const labels = reserves.map(stats => stats.date);
|
||||
const series = reserves.map(stats => parseFloat(stats.amount) / 100000000);
|
||||
return {
|
||||
series,
|
||||
labels
|
||||
};
|
||||
}),
|
||||
share()
|
||||
);
|
||||
|
||||
this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$, this.currentReserves$])
|
||||
.pipe(
|
||||
map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => {
|
||||
liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000;
|
||||
|
||||
if (liquidPegs.series.length === liquidReserves?.series.length) {
|
||||
liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000;
|
||||
} else if (liquidPegs.series.length === liquidReserves?.series.length + 1) {
|
||||
liquidReserves.series.push(parseFloat(currentReserves?.amount) / 100000000);
|
||||
liquidReserves.labels.push(liquidPegs.labels[liquidPegs.labels.length - 1]);
|
||||
} else {
|
||||
liquidReserves = {
|
||||
series: [],
|
||||
labels: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
liquidPegs,
|
||||
liquidReserves
|
||||
};
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next(1);
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<div *ngIf="(unbackedMonths$ | async) as unbackedMonths; else loadingData">
|
||||
<ng-container *ngIf="unbackedMonths.historyComplete; else loadingData">
|
||||
<div class="fee-estimation-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="liquid.unpeg">Unpeg</h5>
|
||||
<div class="card-text">
|
||||
<div class="fee-text" [ngClass]="{'danger' : unbackedMonths.total > 0, 'correct': unbackedMonths.total === 0}">
|
||||
{{ unbackedMonths.total }} <span i18n="liquid.unpeg-event">Unpeg Event</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="liquid.avg-peg-ratio">Avg Peg Ratio</h5>
|
||||
<div class="card-text">
|
||||
<div class="fee-text" [ngClass]="{'danger' : unbackedMonths.avg < 1, 'correct': unbackedMonths.avg >= 1}">
|
||||
{{ unbackedMonths.avg.toFixed(5) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingData>
|
||||
<div class="fee-estimation-container loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="liquid.unpeg">Unpeg</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="liquid.avg-peg-ratio">Avg Peg Ratio</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
.fee-estimation-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item {
|
||||
max-width: 300px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin-bottom: 4px;
|
||||
color: #4a68b9;
|
||||
font-size: 10px;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
span {
|
||||
font-size: 11px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
.danger {
|
||||
color: #D81B60;
|
||||
}
|
||||
.correct {
|
||||
color: #7CB342;
|
||||
}
|
||||
}
|
||||
|
||||
.card-text span {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
top: 0px;
|
||||
}
|
||||
.fee-text{
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
line-height: 1.45;
|
||||
padding: 0px 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container{
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
max-width: 90px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable, map } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reserves-ratio-stats',
|
||||
templateUrl: './reserves-ratio-stats.component.html',
|
||||
styleUrls: ['./reserves-ratio-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReservesRatioStatsComponent implements OnInit {
|
||||
@Input() fullHistory$: Observable<any>;
|
||||
unbackedMonths$: Observable<any>
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.fullHistory$) {
|
||||
return;
|
||||
}
|
||||
this.unbackedMonths$ = this.fullHistory$
|
||||
.pipe(
|
||||
map((fullHistory) => {
|
||||
if (fullHistory.liquidPegs.series.length !== fullHistory.liquidReserves.series.length) {
|
||||
return {
|
||||
historyComplete: false,
|
||||
total: null
|
||||
};
|
||||
}
|
||||
// Only check the last 3 years
|
||||
let ratioSeries = fullHistory.liquidReserves.series.map((value: number, index: number) => value / fullHistory.liquidPegs.series[index]);
|
||||
ratioSeries = ratioSeries.slice(Math.max(ratioSeries.length - 36, 0));
|
||||
let total = 0;
|
||||
let avg = 0;
|
||||
for (let i = 0; i < ratioSeries.length; i++) {
|
||||
avg += ratioSeries[i];
|
||||
if (ratioSeries[i] < 1) {
|
||||
total++;
|
||||
}
|
||||
}
|
||||
avg = avg / ratioSeries.length;
|
||||
return {
|
||||
historyComplete: true,
|
||||
total: total,
|
||||
avg: avg,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="echarts" echarts [initOpts]="ratioHistoryChartInitOptions" [options]="ratioHistoryChartOptions" (chartRendered)="rendered()"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 100;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { formatDate, formatNumber } from '@angular/common';
|
||||
import { EChartsOption } from '../../../graphs/echarts';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reserves-ratio-graph',
|
||||
templateUrl: './reserves-ratio-graph.component.html',
|
||||
styleUrls: ['./reserves-ratio-graph.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReservesRatioGraphComponent implements OnInit, OnChanges {
|
||||
@Input() data: any;
|
||||
ratioHistoryChartOptions: EChartsOption;
|
||||
ratioSeries: number[] = [];
|
||||
|
||||
height: number | string = '200';
|
||||
right: number | string = '10';
|
||||
top: number | string = '20';
|
||||
left: number | string = '50';
|
||||
template: ('widget' | 'advanced') = 'widget';
|
||||
isLoading = true;
|
||||
|
||||
ratioHistoryChartInitOptions = {
|
||||
renderer: 'svg'
|
||||
};
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.isLoading = true;
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (!this.data) {
|
||||
return;
|
||||
}
|
||||
// Compute the ratio series: the ratio of the reserves to the pegs
|
||||
this.ratioSeries = this.data.liquidReserves.series.map((value: number, index: number) => value / this.data.liquidPegs.series[index]);
|
||||
// Truncate the ratio series and labels series to last 3 years
|
||||
this.ratioSeries = this.ratioSeries.slice(Math.max(this.ratioSeries.length - 36, 0));
|
||||
this.data.liquidPegs.labels = this.data.liquidPegs.labels.slice(Math.max(this.data.liquidPegs.labels.length - 36, 0));
|
||||
// Cut the values that are too high or too low
|
||||
this.ratioSeries = this.ratioSeries.map((value: number) => Math.min(Math.max(value, 0.995), 1.005));
|
||||
this.ratioHistoryChartOptions = this.createChartOptions(this.ratioSeries, this.data.liquidPegs.labels);
|
||||
}
|
||||
|
||||
rendered() {
|
||||
if (!this.data) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
createChartOptions(ratioSeries: number[], labels: string[]): EChartsOption {
|
||||
return {
|
||||
grid: {
|
||||
height: this.height,
|
||||
right: this.right,
|
||||
top: this.top,
|
||||
left: this.left,
|
||||
},
|
||||
animation: false,
|
||||
dataZoom: [{
|
||||
type: 'inside',
|
||||
realtime: true,
|
||||
zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
|
||||
maxSpan: 100,
|
||||
minSpan: 10,
|
||||
}, {
|
||||
show: (this.template === 'advanced') ? true : false,
|
||||
type: 'slider',
|
||||
brushSelect: false,
|
||||
realtime: true,
|
||||
selectedDataBackground: {
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 0.45,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0,
|
||||
}
|
||||
}
|
||||
}],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
position: (pos, params, el, elRect, size) => {
|
||||
const obj = { top: -20 };
|
||||
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
|
||||
return obj;
|
||||
},
|
||||
extraCssText: `width: ${(this.template === 'widget') ? '125px' : '135px'};
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;`,
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ${color};"></span>`;
|
||||
let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
|
||||
const item = params[0];
|
||||
const formattedValue = formatNumber(item.value, this.locale, '1.5-5');
|
||||
const symbol = (item.value === 1.005) ? '≥ ' : (item.value === 0.995) ? '≤ ' : '';
|
||||
itemFormatted += `<div class="item">
|
||||
<div class="indicator-container">${colorSpan(item.color)}</div>
|
||||
<div style="margin-right: 5px"></div>
|
||||
<div class="value">${symbol}${formattedValue}</div>
|
||||
</div>`;
|
||||
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
axisLabel: {
|
||||
align: 'center',
|
||||
fontSize: 11,
|
||||
lineHeight: 12
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: labels.map((value: any) => `${formatDate(value, 'MMM\ny', this.locale)}`),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dotted',
|
||||
color: '#ffffff66',
|
||||
opacity: 0.25,
|
||||
}
|
||||
},
|
||||
min: 0.995,
|
||||
max: 1.005,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: ratioSeries,
|
||||
name: '',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
|
||||
},
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 1,
|
||||
width: 1,
|
||||
},
|
||||
data: [{
|
||||
yAxis: 1,
|
||||
label: {
|
||||
show: false,
|
||||
color: '#ffffff',
|
||||
}
|
||||
}],
|
||||
},
|
||||
},
|
||||
],
|
||||
visualMap: {
|
||||
show: false,
|
||||
top: 50,
|
||||
right: 10,
|
||||
pieces: [{
|
||||
gt: 0,
|
||||
lte: 0.999,
|
||||
color: '#D81B60'
|
||||
},
|
||||
{
|
||||
gt: 0.999,
|
||||
lte: 1.001,
|
||||
color: '#FDD835'
|
||||
},
|
||||
{
|
||||
gt: 1.001,
|
||||
lte: 2,
|
||||
color: '#7CB342'
|
||||
}
|
||||
],
|
||||
outOfRange: {
|
||||
color: '#999'
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="echarts" echarts [initOpts]="ratioChartInitOptions" [options]="ratioChartOptions" (chartRendered)="rendered()"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 100;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { EChartsOption } from '../../../graphs/echarts';
|
||||
import { CurrentPegs } from '../../../interfaces/node-api.interface';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-reserves-ratio',
|
||||
templateUrl: './reserves-ratio.component.html',
|
||||
styleUrls: ['./reserves-ratio.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReservesRatioComponent implements OnInit, OnChanges {
|
||||
@Input() currentPeg: CurrentPegs;
|
||||
@Input() currentReserves: CurrentPegs;
|
||||
ratioChartOptions: EChartsOption;
|
||||
|
||||
height: number | string = '200';
|
||||
right: number | string = '10';
|
||||
top: number | string = '20';
|
||||
left: number | string = '50';
|
||||
template: ('widget' | 'advanced') = 'widget';
|
||||
isLoading = true;
|
||||
|
||||
ratioChartInitOptions = {
|
||||
renderer: 'svg'
|
||||
};
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
this.isLoading = true;
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (!this.currentPeg || !this.currentReserves || this.currentPeg.amount === '0') {
|
||||
return;
|
||||
}
|
||||
this.ratioChartOptions = this.createChartOptions(this.currentPeg, this.currentReserves);
|
||||
}
|
||||
|
||||
rendered() {
|
||||
if (!this.currentPeg || !this.currentReserves) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
createChartOptions(currentPeg: CurrentPegs, currentReserves: CurrentPegs): EChartsOption {
|
||||
return {
|
||||
series: [
|
||||
{
|
||||
type: 'gauge',
|
||||
startAngle: 180,
|
||||
endAngle: 0,
|
||||
center: ['50%', '70%'],
|
||||
radius: '100%',
|
||||
min: 0.999,
|
||||
max: 1.001,
|
||||
splitNumber: 2,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 6,
|
||||
color: [
|
||||
[0.49, '#D81B60'],
|
||||
[1, '#7CB342']
|
||||
]
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'inherit',
|
||||
fontFamily: 'inherit',
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z',
|
||||
length: '50%',
|
||||
width: 16,
|
||||
offsetCenter: [0, '-27%'],
|
||||
itemStyle: {
|
||||
color: 'auto'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
length: 12,
|
||||
lineStyle: {
|
||||
color: 'auto',
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
length: 20,
|
||||
lineStyle: {
|
||||
color: 'auto',
|
||||
width: 5
|
||||
}
|
||||
},
|
||||
title: {
|
||||
show: true,
|
||||
offsetCenter: [0, '-117.5%'],
|
||||
fontSize: 18,
|
||||
color: '#4a68b9',
|
||||
fontFamily: 'inherit',
|
||||
fontWeight: 500,
|
||||
},
|
||||
detail: {
|
||||
fontSize: 25,
|
||||
offsetCenter: [0, '-0%'],
|
||||
valueAnimation: true,
|
||||
fontFamily: 'inherit',
|
||||
fontWeight: 500,
|
||||
formatter: function (value) {
|
||||
return (value).toFixed(5);
|
||||
},
|
||||
color: 'inherit'
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: parseFloat(currentReserves.amount) / parseFloat(currentPeg.amount),
|
||||
name: 'Peg-O-Meter'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<div *ngIf="(currentPeg$ | async) as currentPeg; else loadingData">
|
||||
<div *ngIf="(currentReserves$ | async) as currentReserves; else loadingData">
|
||||
<div class="fee-estimation-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
|
||||
<div class="card-text">
|
||||
<div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div>
|
||||
<span class="fiat">
|
||||
<span>As of block <a [routerLink]="['/block', currentPeg.hash]">{{ currentPeg.lastBlockUpdate }}</a></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.btc-reserves">BTC Reserves</h5>
|
||||
<div class="card-text">
|
||||
<div class="fee-text">{{ (+currentReserves.amount) / 100000000 | number: '1.2-2' }} <span style="color: #b86d12;">BTC</span></div>
|
||||
<span class="fiat">
|
||||
<span>As of block <a href="{{ env.MEMPOOL_WEBSITE_URL + '/block/' + currentReserves.hash }}" target="_blank" style="color:#b86d12">{{ currentReserves.lastBlockUpdate }}</a></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingData>
|
||||
<div class="fee-estimation-container loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</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.btc-reserves">BTC Reserves</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
.fee-estimation-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item {
|
||||
max-width: 150px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #4a68b9;
|
||||
font-size: 10px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 1rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
span {
|
||||
font-size: 11px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.card-text span {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
top: 0px;
|
||||
}
|
||||
.fee-text{
|
||||
border-bottom: 1px solid #ffffff1c;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
line-height: 1.45;
|
||||
padding: 0px 2px;
|
||||
}
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container{
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
max-width: 90px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 3px;
|
||||
max-width: 55px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Env, StateService } from '../../../services/state.service';
|
||||
import { CurrentPegs } from '../../../interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reserves-supply-stats',
|
||||
templateUrl: './reserves-supply-stats.component.html',
|
||||
styleUrls: ['./reserves-supply-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReservesSupplyStatsComponent implements OnInit {
|
||||
@Input() currentReserves$: Observable<CurrentPegs>;
|
||||
@Input() currentPeg$: Observable<CurrentPegs>;
|
||||
|
||||
env: Env;
|
||||
|
||||
constructor(private stateService: StateService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.env = this.stateService.env;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -48,9 +48,16 @@
|
||||
|
||||
<div class="navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav {{ network.val }}">
|
||||
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home">
|
||||
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home" *ngIf="stateService.env.ACCELERATOR">
|
||||
<a class="nav-link" [routerLink]="['/acceleration' | relativeUrl]" (click)="collapse()">
|
||||
<fa-icon [icon]="['fas', 'rocket']" [fixedWidth]="true" i18n-title="master-page.accelerator-dashboard" title="Accelerator Dashboard"></fa-icon>
|
||||
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="mining.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
|
||||
</li>
|
||||
|
||||
@@ -29,6 +29,16 @@ li.nav-item {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
@media (max-width: 429px) {
|
||||
margin: auto 5px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
@media (max-width: 369px) {
|
||||
margin: auto 3px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
@@ -64,7 +74,10 @@ li.nav-item {
|
||||
}
|
||||
a {
|
||||
font-size: 0.8em;
|
||||
@media (min-width: 375px) {
|
||||
@media (min-width: 370px) {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@media (min-width: 430px) {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
@@ -198,7 +211,15 @@ nav {
|
||||
margin: 24px 0px 0px -15px;
|
||||
font-size: 8px;
|
||||
@media (max-width: 767.98px) {
|
||||
margin: 33px 0px 0px -19px;
|
||||
margin: 30px 0px 0px -19px;
|
||||
font-size: 7px;
|
||||
}
|
||||
@media (max-width: 3429px) {
|
||||
margin: 25px 0px 0px -19px;
|
||||
font-size: 7px;
|
||||
}
|
||||
@media (max-width: 369px) {
|
||||
margin: 20px 0px 0px -19px;
|
||||
font-size: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<app-block-overview-graph
|
||||
#blockGraph
|
||||
[isLoading]="isLoading$ | async"
|
||||
[resolution]="86"
|
||||
[resolution]="resolution"
|
||||
[blockLimit]="stateService.blockVSize"
|
||||
[orientation]="timeLtr ? 'right' : 'left'"
|
||||
[flip]="true"
|
||||
[showFilters]="showFilters"
|
||||
[filterFlags]="filterFlags"
|
||||
[filterMode]="filterMode"
|
||||
[overrideColors]="overrideColors"
|
||||
(txClickEvent)="onTxClick($event)"
|
||||
></app-block-overview-graph>
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventE
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { MempoolBlockDelta, TransactionStripped } from '../../interfaces/websocket.interface';
|
||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
||||
import { Subscription, BehaviorSubject, merge, of } from 'rxjs';
|
||||
import { switchMap, filter } from 'rxjs/operators';
|
||||
import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs';
|
||||
import { switchMap, filter, concatMap, map } from 'rxjs/operators';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { Router } from '@angular/router';
|
||||
@@ -18,8 +18,11 @@ import TxView from '../block-overview-graph/tx-view';
|
||||
})
|
||||
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
|
||||
@Input() index: number;
|
||||
@Input() resolution = 86;
|
||||
@Input() showFilters: boolean = false;
|
||||
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
||||
@Input() filterFlags: bigint | undefined = undefined;
|
||||
@Input() filterMode: 'and' | 'or' = 'and';
|
||||
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
||||
|
||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||
@@ -33,7 +36,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
poolDirection: string = 'left';
|
||||
|
||||
blockSub: Subscription;
|
||||
deltaSub: Subscription;
|
||||
rateLimit = 1000;
|
||||
private lastEventTime = Date.now() - this.rateLimit;
|
||||
private subId = 0;
|
||||
|
||||
firstLoad: boolean = true;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
@@ -53,20 +60,81 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.blockSub = merge(
|
||||
of(true),
|
||||
this.stateService.connectionState$.pipe(filter((state) => state === 2))
|
||||
)
|
||||
.pipe(switchMap(() => this.stateService.mempoolBlockTransactions$))
|
||||
.subscribe((transactionsStripped) => {
|
||||
this.replaceBlock(transactionsStripped);
|
||||
});
|
||||
this.deltaSub = this.stateService.mempoolBlockDelta$.subscribe((delta) => {
|
||||
this.updateBlock(delta);
|
||||
this.stateService.mempoolBlockTransactions$,
|
||||
this.stateService.mempoolBlockDelta$,
|
||||
).pipe(
|
||||
concatMap(update => {
|
||||
const now = Date.now();
|
||||
const timeSinceLastEvent = now - this.lastEventTime;
|
||||
this.lastEventTime = Math.max(now, this.lastEventTime + this.rateLimit);
|
||||
|
||||
const subId = this.subId;
|
||||
|
||||
// If time since last event is less than X seconds, delay this event
|
||||
if (timeSinceLastEvent < this.rateLimit) {
|
||||
return timer(this.rateLimit - timeSinceLastEvent).pipe(
|
||||
// Emit the event after the timer
|
||||
map(() => ({ update, subId }))
|
||||
);
|
||||
} else {
|
||||
// If enough time has passed, emit the event immediately
|
||||
return of({ update, subId });
|
||||
}
|
||||
})
|
||||
).subscribe(({ update, subId }) => {
|
||||
// discard stale updates after a block transition
|
||||
if (subId !== this.subId) {
|
||||
return;
|
||||
}
|
||||
// process update
|
||||
if (update['added']) {
|
||||
// delta
|
||||
this.updateBlock(update as MempoolBlockDelta);
|
||||
} else {
|
||||
const transactionsStripped = update as TransactionStripped[];
|
||||
// new transactions
|
||||
if (this.firstLoad) {
|
||||
this.replaceBlock(transactionsStripped);
|
||||
} else {
|
||||
const inOldBlock = {};
|
||||
const inNewBlock = {};
|
||||
const added: TransactionStripped[] = [];
|
||||
const changed: { txid: string, rate: number | undefined, acc: boolean | undefined }[] = [];
|
||||
const removed: string[] = [];
|
||||
for (const tx of transactionsStripped) {
|
||||
inNewBlock[tx.txid] = true;
|
||||
}
|
||||
for (const txid of Object.keys(this.blockGraph?.scene?.txs || {})) {
|
||||
inOldBlock[txid] = true;
|
||||
if (!inNewBlock[txid]) {
|
||||
removed.push(txid);
|
||||
}
|
||||
}
|
||||
for (const tx of transactionsStripped) {
|
||||
if (!inOldBlock[tx.txid]) {
|
||||
added.push(tx);
|
||||
} else {
|
||||
changed.push({
|
||||
txid: tx.txid,
|
||||
rate: tx.rate,
|
||||
acc: tx.acc
|
||||
});
|
||||
}
|
||||
}
|
||||
this.updateBlock({
|
||||
removed,
|
||||
changed,
|
||||
added
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
if (changes.index) {
|
||||
this.subId++;
|
||||
this.firstLoad = true;
|
||||
if (this.blockGraph) {
|
||||
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
|
||||
}
|
||||
@@ -77,7 +145,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.blockSub.unsubscribe();
|
||||
this.deltaSub.unsubscribe();
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
this.websocketService.stopTrackMempoolBlock();
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
tap(() => {
|
||||
this.stateService.markBlock$.next({ mempoolBlockIndex: this.mempoolBlockIndex });
|
||||
this.websocketService.startTrackMempoolBlock(this.mempoolBlockIndex);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -74,6 +75,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stateService.markBlock$.next({});
|
||||
this.websocketService.stopTrackMempoolBlock();
|
||||
}
|
||||
|
||||
getOrdinal(mempoolBlock: MempoolBlock): string {
|
||||
|
||||
@@ -26,9 +26,11 @@
|
||||
|
||||
<!-- pool distribution -->
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card graph-card">
|
||||
<div class="card">
|
||||
<div class="card-body pl-2 pr-2">
|
||||
<app-pool-ranking [attr.data-cy]="'pool-distribution'" [widget]=true></app-pool-ranking>
|
||||
<div class="mempool-graph">
|
||||
<app-pool-ranking [height]="graphHeight" [attr.data-cy]="'pool-distribution'" [widget]=true></app-pool-ranking>
|
||||
</div>
|
||||
<div class="mt-1"><a [attr.data-cy]="'pool-distribution-view-more'" [routerLink]="['/graphs/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,7 +40,9 @@
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
|
||||
<app-hashrate-chart [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-hashrate-chart>
|
||||
<div class="fixed-mempool-graph">
|
||||
<app-hashrate-chart [height]="graphHeight" [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-hashrate-chart>
|
||||
</div>
|
||||
<div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" fragment="1y" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-mempool-graph {
|
||||
height: 330px;
|
||||
}
|
||||
|
||||
.mempool-graph, .fixed-mempool-graph {
|
||||
@media (min-width: 768px) {
|
||||
height: 345px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 472px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
@@ -11,6 +11,8 @@ import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||
graphHeight = 375;
|
||||
|
||||
constructor(
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
@@ -22,6 +24,7 @@ export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.onResize();
|
||||
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
|
||||
}
|
||||
|
||||
@@ -35,4 +38,15 @@ export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 992) {
|
||||
this.graphHeight = 375;
|
||||
} else if (window.innerWidth >= 768) {
|
||||
this.graphHeight = 245;
|
||||
} else {
|
||||
this.graphHeight = 240;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? '' : 'pb-0'" class="container pb-lg-0">
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
<th class="" i18n="mining.pool-name">Pool</th>
|
||||
<th class="" *ngIf="this.miningWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th>
|
||||
<th class="" i18n="master-page.blocks">Blocks</th>
|
||||
<th *ngIf="auditAvailable" class="health text-right widget" i18n="latest-blocks.avg_health"
|
||||
<th *ngIf="auditAvailable" class="health text-right widget" [ngClass]="{'health-column': this.miningWindowPreference === '24h'}" i18n="latest-blocks.avg_health"
|
||||
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
|
||||
<th *ngIf="auditAvailable" class="d-none d-sm-table-cell" i18n="mining.fees-per-block">Avg Block Fees</th>
|
||||
<th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty Blocks</th>
|
||||
@@ -104,13 +104,13 @@
|
||||
<td class="text-right">
|
||||
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.src = '/resources/mining-pools/default.svg'">
|
||||
</td>
|
||||
<td class=""><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
|
||||
<td class="pool-name"><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
|
||||
<td class="" *ngIf="this.miningWindowPreference === '24h'">{{ pool.lastEstimatedHashrate }} {{
|
||||
miningStats.miningUnits.hashrateUnit }}</td>
|
||||
<td class="d-flex justify-content-center">
|
||||
{{ pool.blockCount }}<span class="d-none d-md-table-cell"> ({{ pool.share }}%)</span>
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable, 'health-column': this.miningWindowPreference === '24h'}">
|
||||
<a
|
||||
class="health-badge badge"
|
||||
[class.badge-success]="pool.avgMatchRate >= 99"
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 240px;
|
||||
@media (max-width: 767px) {
|
||||
max-height: 240px;
|
||||
}
|
||||
@media (max-width: 485px) {
|
||||
max-height: 200px;
|
||||
}
|
||||
@@ -41,6 +43,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.pool-name {
|
||||
max-width: 110px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.health-column {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { isMobile } from '../../shared/common.utils';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PoolRankingComponent implements OnInit {
|
||||
@Input() height: number = 300;
|
||||
@Input() widget = false;
|
||||
|
||||
miningWindowPreference: string;
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
<a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a>
|
||||
</td>
|
||||
<td class="timestamp">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
||||
</td>
|
||||
<td class="mined">
|
||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time>
|
||||
|
||||
@@ -115,13 +115,13 @@ export class PoolComponent implements OnInit {
|
||||
|
||||
prepareChartOptions(data) {
|
||||
let title: object;
|
||||
if (data.length === 0) {
|
||||
if (data.length <= 1) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`,
|
||||
text: $localize`Not enough data yet`,
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
@@ -172,14 +172,14 @@ export class PoolComponent implements OnInit {
|
||||
`;
|
||||
}.bind(this)
|
||||
},
|
||||
xAxis: data.length === 0 ? undefined : {
|
||||
xAxis: data.length <= 1 ? undefined : {
|
||||
type: 'time',
|
||||
splitNumber: (this.isMobile()) ? 5 : 10,
|
||||
axisLabel: {
|
||||
hideOverlap: true,
|
||||
}
|
||||
},
|
||||
yAxis: data.length === 0 ? undefined : [
|
||||
yAxis: data.length <= 1 ? undefined : [
|
||||
{
|
||||
min: (value) => {
|
||||
return value.min * 0.9;
|
||||
@@ -198,7 +198,7 @@ export class PoolComponent implements OnInit {
|
||||
}
|
||||
},
|
||||
],
|
||||
series: data.length === 0 ? undefined : [
|
||||
series: data.length <= 1 ? undefined : [
|
||||
{
|
||||
zlevel: 0,
|
||||
name: 'Hashrate',
|
||||
@@ -211,7 +211,7 @@ export class PoolComponent implements OnInit {
|
||||
},
|
||||
},
|
||||
],
|
||||
dataZoom: data.length === 0 ? undefined : [{
|
||||
dataZoom: data.length <= 1 ? undefined : [{
|
||||
type: 'inside',
|
||||
realtime: true,
|
||||
zoomLock: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="rbf-timeline box" [class.mined]="replacements.mined">
|
||||
<div class="timeline-wrapper">
|
||||
<div class="timeline" *ngFor="let timeline of rows">
|
||||
<div class="intervals">
|
||||
<div class="timeline" *ngFor="let timeline of rows; let j = index">
|
||||
<div class="intervals" *ngIf="j < rowLimit || timelineExpanded">
|
||||
<ng-container *ngFor="let cell of timeline; let i = index;">
|
||||
<div class="node-spacer"></div>
|
||||
<ng-container *ngIf="i < timeline.length - 1">
|
||||
@@ -13,7 +13,7 @@
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<div class="nodes" *ngIf="j < rowLimit || timelineExpanded">
|
||||
<ng-container *ngFor="let cell of timeline; let i = index;">
|
||||
<ng-container *ngIf="cell.replacement?.tx; else nonNode">
|
||||
<div class="node"
|
||||
@@ -37,7 +37,7 @@
|
||||
</ng-container>
|
||||
<ng-template #nonNode>
|
||||
<ng-container [ngSwitch]="cell.connector">
|
||||
<div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf"></div></div>
|
||||
<div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf" [class.last-pipe]="!timelineExpanded && j === rowLimit - 1"></div></div>
|
||||
<div class="connector" *ngSwitchCase="'corner'"><div class="corner" [class.fullrbf]="cell.fullRbf"></div></div>
|
||||
<div class="node-spacer" *ngSwitchDefault></div>
|
||||
</ng-container>
|
||||
@@ -51,6 +51,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [class.fade-out]="!timelineExpanded && rows.length > rowLimit"></div>
|
||||
<div class="toggle-wrapper" *ngIf="rows.length > rowLimit && rowLimit !== 0">
|
||||
<button class="btn btn-sm btn-primary graph-toggle" (click)="toggleTimeline(true);" *ngIf="!timelineExpanded; else collapseBtn">
|
||||
<span i18n="show-all">Show all</span>
|
||||
(<ng-container *ngTemplateOutlet="xRemaining; context: {$implicit: rows.length - rowLimit}"></ng-container>)
|
||||
</button>
|
||||
<ng-template #collapseBtn>
|
||||
<button class="btn btn-sm btn-primary graph-toggle" (click)="toggleTimeline(false);"><span i18n="show-less">Show less</span></button>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<ng-template #nodeSpacer>
|
||||
<div class="node-spacer"></div>
|
||||
@@ -72,3 +82,5 @@
|
||||
[isConnector]="hoverConnector"
|
||||
></app-rbf-timeline-tooltip> -->
|
||||
</div>
|
||||
|
||||
<ng-template #xRemaining let-x i18n="x-remaining">{{ x }} remaining</ng-template>
|
||||
@@ -30,12 +30,32 @@
|
||||
overflow-x: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
top: -70px;
|
||||
background: linear-gradient(to bottom, rgba(36, 39, 62, 0) 0%, rgba(36, 39, 62, 1) 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-wrapper {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 1.25em 0 0;
|
||||
}
|
||||
|
||||
.intervals, .nodes {
|
||||
min-width: 100%;
|
||||
display: flex;
|
||||
@@ -191,6 +211,10 @@
|
||||
&.fullrbf {
|
||||
border-right: solid 10px #1bd8f4;
|
||||
}
|
||||
&.last-pipe {
|
||||
height: 150px;
|
||||
bottom: -42px;
|
||||
}
|
||||
}
|
||||
|
||||
.corner {
|
||||
|
||||
@@ -25,7 +25,9 @@ function isTimelineCell(val: RbfTree | TimelineCell): boolean {
|
||||
export class RbfTimelineComponent implements OnInit, OnChanges {
|
||||
@Input() replacements: RbfTree;
|
||||
@Input() txid: string;
|
||||
@Input() rowLimit: number = 5; // If explicitly set to 0, all timelines rows will be displayed by default
|
||||
rows: TimelineCell[][] = [];
|
||||
timelineExpanded: boolean = this.rowLimit === 0;
|
||||
|
||||
hoverInfo: RbfTree | null = null;
|
||||
tooltipPosition = null;
|
||||
@@ -191,6 +193,10 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
||||
return rows;
|
||||
}
|
||||
|
||||
toggleTimeline(expand: boolean): void {
|
||||
this.timelineExpanded = expand;
|
||||
}
|
||||
|
||||
scrollToSelected() {
|
||||
const node = document.getElementById('node-' + this.txid);
|
||||
if (node) {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
:host ::ng-deep {
|
||||
.dropdown-item {
|
||||
white-space: nowrap;
|
||||
width: calc(100% - 34px);
|
||||
}
|
||||
.dropdown-menu {
|
||||
width: calc(100% - 34px);
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.dropdown-item {
|
||||
width: 410px;
|
||||
}
|
||||
.dropdown-menu {
|
||||
width: 410px;
|
||||
}
|
||||
@@ -46,7 +42,7 @@ form {
|
||||
min-width: 400px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
min-width: 200px;
|
||||
min-width: 142px;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
min-width: 300px;
|
||||
|
||||
@@ -2,13 +2,14 @@ import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewC
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Env, StateService } from '../../services/state.service';
|
||||
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith, tap } from 'rxjs/operators';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SearchResultsComponent } from './search-results/search-results.component';
|
||||
import { Network, findOtherNetworks, getRegex, getTargetUrl, needBaseModuleChange } from '../../shared/regex.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-form',
|
||||
@@ -18,7 +19,7 @@ import { SearchResultsComponent } from './search-results/search-results.componen
|
||||
})
|
||||
export class SearchFormComponent implements OnInit {
|
||||
@Input() hamburgerOpen = false;
|
||||
|
||||
env: Env;
|
||||
network = '';
|
||||
assets: object = {};
|
||||
isSearching = false;
|
||||
@@ -36,12 +37,13 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/;
|
||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||
regexBlockheight = /^[0-9]{1,9}$/;
|
||||
regexDate = /^(?:\d{4}[-/]\d{1,2}[-/]\d{1,2}(?: \d{1,2}:\d{2})?)$/;
|
||||
regexUnixTimestamp = /^\d{10}$/;
|
||||
regexAddress = getRegex('address', 'mainnet'); // Default to mainnet
|
||||
regexBlockhash = getRegex('blockhash', 'mainnet');
|
||||
regexTransaction = getRegex('transaction');
|
||||
regexBlockheight = getRegex('blockheight');
|
||||
regexDate = getRegex('date');
|
||||
regexUnixTimestamp = getRegex('timestamp');
|
||||
|
||||
focus$ = new Subject<string>();
|
||||
click$ = new Subject<string>();
|
||||
|
||||
@@ -66,8 +68,14 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
|
||||
this.env = this.stateService.env;
|
||||
this.stateService.networkChanged$.subscribe((network) => {
|
||||
this.network = network;
|
||||
// TODO: Eventually change network type here from string to enum of consts
|
||||
this.regexAddress = getRegex('address', network as any || 'mainnet');
|
||||
this.regexBlockhash = getRegex('blockhash', network as any || 'mainnet');
|
||||
});
|
||||
|
||||
this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
|
||||
if (this.searchInput && e.type === EventType.NavigationStart) {
|
||||
this.searchInput.nativeElement.blur();
|
||||
@@ -96,9 +104,6 @@ export class SearchFormComponent implements OnInit {
|
||||
const searchText$ = this.searchForm.get('searchText').valueChanges
|
||||
.pipe(
|
||||
map((text) => {
|
||||
if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) {
|
||||
return text.substr(1);
|
||||
}
|
||||
return text.trim();
|
||||
}),
|
||||
tap((text) => {
|
||||
@@ -132,9 +137,6 @@ export class SearchFormComponent implements OnInit {
|
||||
);
|
||||
}),
|
||||
map((result: any[]) => {
|
||||
if (this.network === 'bisq') {
|
||||
result[0] = result[0].map((address: string) => 'B' + address);
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
tap(() => {
|
||||
@@ -164,9 +166,11 @@ export class SearchFormComponent implements OnInit {
|
||||
blockHeight: false,
|
||||
txId: false,
|
||||
address: false,
|
||||
otherNetworks: [],
|
||||
addresses: [],
|
||||
nodes: [],
|
||||
channels: [],
|
||||
liquidAsset: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -174,15 +178,22 @@ export class SearchFormComponent implements OnInit {
|
||||
const addressPrefixSearchResults = result[0];
|
||||
const lightningResults = result[1];
|
||||
|
||||
const matchesBlockHeight = this.regexBlockheight.test(searchText);
|
||||
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date';
|
||||
const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText);
|
||||
// Do not show date and timestamp results for liquid and bisq
|
||||
const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'signet';
|
||||
|
||||
const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight;
|
||||
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date' && new Date(searchText).getTime() <= Date.now() && isNetworkBitcoin;
|
||||
const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText) && parseInt(searchText) <= Math.floor(Date.now() / 1000) && isNetworkBitcoin;
|
||||
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
|
||||
const matchesBlockHash = this.regexBlockhash.test(searchText);
|
||||
const matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
|
||||
let matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
|
||||
const otherNetworks = findOtherNetworks(searchText, this.network as any || 'mainnet', this.env);
|
||||
const liquidAsset = this.assets ? (this.assets[searchText] || []) : [];
|
||||
|
||||
if (matchesAddress && this.network === 'bisq') {
|
||||
searchText = 'B' + searchText;
|
||||
// Add B prefix to addresses in Bisq network
|
||||
if (!matchesAddress && this.network === 'bisq' && getRegex('address', 'mainnet').test(searchText)) {
|
||||
searchText = 'B' + searchText;
|
||||
matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
|
||||
}
|
||||
|
||||
if (matchesDateTime && searchText.indexOf('/') !== -1) {
|
||||
@@ -198,9 +209,11 @@ export class SearchFormComponent implements OnInit {
|
||||
txId: matchesTxId,
|
||||
blockHash: matchesBlockHash,
|
||||
address: matchesAddress,
|
||||
addresses: addressPrefixSearchResults,
|
||||
addresses: matchesAddress && addressPrefixSearchResults.length === 1 && searchText === addressPrefixSearchResults[0] ? [] : addressPrefixSearchResults, // If there is only one address and it matches the search text, don't show it in the dropdown
|
||||
otherNetworks: otherNetworks,
|
||||
nodes: lightningResults.nodes,
|
||||
channels: lightningResults.channels,
|
||||
liquidAsset: liquidAsset,
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -217,12 +230,21 @@ export class SearchFormComponent implements OnInit {
|
||||
selectedResult(result: any): void {
|
||||
if (typeof result === 'string') {
|
||||
this.search(result);
|
||||
} else if (typeof result === 'number') {
|
||||
} else if (typeof result === 'number' && result <= this.stateService.latestBlockHeight) {
|
||||
this.navigate('/block/', result.toString());
|
||||
} else if (result.alias) {
|
||||
this.navigate('/lightning/node/', result.public_key);
|
||||
} else if (result.short_id) {
|
||||
this.navigate('/lightning/channel/', result.id);
|
||||
} else if (result.network) {
|
||||
if (result.isNetworkAvailable) {
|
||||
this.navigate('/address/', result.address, undefined, result.network);
|
||||
} else {
|
||||
this.searchForm.setValue({
|
||||
searchText: '',
|
||||
});
|
||||
this.isSearching = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,23 +252,26 @@ export class SearchFormComponent implements OnInit {
|
||||
const searchText = result || this.searchForm.value.searchText.trim();
|
||||
if (searchText) {
|
||||
this.isSearching = true;
|
||||
|
||||
if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) {
|
||||
this.navigate('/address/', searchText);
|
||||
} else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) {
|
||||
} else if (this.regexBlockhash.test(searchText)) {
|
||||
this.navigate('/block/', searchText);
|
||||
} else if (this.regexBlockheight.test(searchText)) {
|
||||
parseInt(searchText) <= this.stateService.latestBlockHeight ? this.navigate('/block/', searchText) : this.isSearching = false;
|
||||
} else if (this.regexTransaction.test(searchText)) {
|
||||
const matches = this.regexTransaction.exec(searchText);
|
||||
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
||||
if (this.assets[matches[1]]) {
|
||||
this.navigate('/assets/asset/', matches[1]);
|
||||
if (this.assets[matches[0]]) {
|
||||
this.navigate('/assets/asset/', matches[0]);
|
||||
}
|
||||
this.electrsApiService.getAsset$(matches[1])
|
||||
this.electrsApiService.getAsset$(matches[0])
|
||||
.subscribe(
|
||||
() => { this.navigate('/assets/asset/', matches[1]); },
|
||||
() => { this.navigate('/assets/asset/', matches[0]); },
|
||||
() => {
|
||||
this.electrsApiService.getBlock$(matches[1])
|
||||
this.electrsApiService.getBlock$(matches[0])
|
||||
.subscribe(
|
||||
(block) => { this.navigate('/block/', matches[1], { state: { data: { block } } }); },
|
||||
(block) => { this.navigate('/block/', matches[0], { state: { data: { block } } }); },
|
||||
() => { this.navigate('/tx/', matches[0]); });
|
||||
}
|
||||
);
|
||||
@@ -256,6 +281,11 @@ export class SearchFormComponent implements OnInit {
|
||||
} else if (this.regexDate.test(searchText) || this.regexUnixTimestamp.test(searchText)) {
|
||||
let timestamp: number;
|
||||
this.regexDate.test(searchText) ? timestamp = Math.floor(new Date(searchText).getTime() / 1000) : timestamp = searchText;
|
||||
// Check if timestamp is too far in the future or before the genesis block
|
||||
if (timestamp > Math.floor(Date.now() / 1000)) {
|
||||
this.isSearching = false;
|
||||
return;
|
||||
}
|
||||
this.apiService.getBlockDataFromTimestamp$(timestamp).subscribe(
|
||||
(data) => { this.navigate('/block/', data.hash); },
|
||||
(error) => { console.log(error); this.isSearching = false; }
|
||||
@@ -267,12 +297,17 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
navigate(url: string, searchText: string, extras?: any): void {
|
||||
this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras);
|
||||
this.searchTriggered.emit();
|
||||
this.searchForm.setValue({
|
||||
searchText: '',
|
||||
});
|
||||
this.isSearching = false;
|
||||
|
||||
navigate(url: string, searchText: string, extras?: any, swapNetwork?: string) {
|
||||
if (needBaseModuleChange(this.env.BASE_MODULE as 'liquid' | 'bisq' | 'mempool', swapNetwork as Network)) {
|
||||
window.location.href = getTargetUrl(swapNetwork as Network, searchText, this.env);
|
||||
} else {
|
||||
this.router.navigate([this.relativeUrlPipe.transform(url, swapNetwork), searchText], extras);
|
||||
this.searchTriggered.emit();
|
||||
this.searchForm.setValue({
|
||||
searchText: '',
|
||||
});
|
||||
this.isSearching = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.addresses.length && !results.nodes.length && !results.channels.length">
|
||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.otherNetworks.length && !results.addresses.length && !results.nodes.length && !results.channels.length && !results.liquidAsset.length">
|
||||
<ng-template [ngIf]="results.blockHeight">
|
||||
<div class="card-title" i18n="search.bitcoin-block-height">Bitcoin Block Height</div>
|
||||
<div class="card-title" i18n="search.bitcoin-block-height">{{ networkName }} Block Height</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText }"></ng-container>
|
||||
</button>
|
||||
@@ -17,28 +17,36 @@
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.txId">
|
||||
<div class="card-title" i18n="search.bitcoin-transaction">Bitcoin Transaction</div>
|
||||
<ng-template [ngIf]="results.txId && !results.liquidAsset.length">
|
||||
<div class="card-title" i18n="search.bitcoin-transaction">{{ networkName }} Transaction</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 13 }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.address">
|
||||
<div class="card-title" i18n="search.bitcoin-address">Bitcoin Address</div>
|
||||
<div class="card-title" i18n="search.bitcoin-address">{{ networkName }} Address</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : isMobile ? 20 : 30 }"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : isMobile ? 17 : 30 }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.blockHash">
|
||||
<div class="card-title" i18n="search.bitcoin-block">Bitcoin Block</div>
|
||||
<div class="card-title" i18n="search.bitcoin-block">{{ networkName }} Block</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 13 }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.otherNetworks.length">
|
||||
<div class="card-title danger" i18n="search.other-networks">Other Network Address</div>
|
||||
<ng-template ngFor [ngForOf]="results.otherNetworks" let-otherNetwork let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" [class.inactive]="!otherNetwork.isNetworkAvailable" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: otherNetwork.address| shortenString : isMobile ? 12 : 20 }"></ng-container> <b>({{ otherNetwork.network.charAt(0).toUpperCase() + otherNetwork.network.slice(1) }})</b>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.addresses.length">
|
||||
<div class="card-title" i18n="search.bitcoin-addresses">Bitcoin Addresses</div>
|
||||
<div class="card-title" i18n="search.bitcoin-addresses">{{ networkName }} Addresses</div>
|
||||
<ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + i)" [class.active]="(results.hashQuickMatch + results.otherNetworks.length + i) === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight>
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -46,7 +54,7 @@
|
||||
<ng-template [ngIf]="results.nodes.length">
|
||||
<div class="card-title" i18n="search.lightning-nodes">Lightning Nodes</div>
|
||||
<ng-template ngFor [ngForOf]="results.nodes" let-node let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="node.alias" [term]="results.searchText"></ngb-highlight> <span class="symbol">{{ node.public_key | shortenString : 10 }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -54,11 +62,17 @@
|
||||
<ng-template [ngIf]="results.channels.length">
|
||||
<div class="card-title" i18n="search.lightning-channels">Lightning Channels</div>
|
||||
<ng-template ngFor [ngForOf]="results.channels" let-channel let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="channel.short_id" [term]="results.searchText"></ngb-highlight> <span class="symbol">{{ channel.id }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.liquidAsset.length">
|
||||
<div class="card-title" i18n="search.liquid-asset">Liquid Asset</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 11 }"></ng-container> <b>({{ results.liquidAsset[1] }})</b>
|
||||
</button>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<ng-template #goTo let-x i18n="search.go-to">Go to "{{ x }}"</ng-template>
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 42px;
|
||||
|
||||
@@ -10,19 +10,24 @@ export class SearchResultsComponent implements OnChanges {
|
||||
@Input() results: any = {};
|
||||
@Output() selectedResult = new EventEmitter();
|
||||
|
||||
isMobile = (window.innerWidth <= 767.98);
|
||||
isMobile = (window.innerWidth <= 1150);
|
||||
resultsFlattened = [];
|
||||
activeIdx = 0;
|
||||
focusFirst = true;
|
||||
networkName = '';
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.networkName = this.stateService.network.charAt(0).toUpperCase() + this.stateService.network.slice(1);
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.activeIdx = 0;
|
||||
if (this.results) {
|
||||
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
|
||||
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.otherNetworks, ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +50,9 @@ export class SearchResultsComponent implements OnChanges {
|
||||
break;
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
if (this.resultsFlattened[this.activeIdx]?.isNetworkAvailable === false) {
|
||||
return;
|
||||
}
|
||||
if (this.resultsFlattened[this.activeIdx]) {
|
||||
this.selectedResult.emit(this.resultsFlattened[this.activeIdx]);
|
||||
} else {
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
<ng-container *ngIf="specialEvent">
|
||||
<div class="pyro">
|
||||
<div class="before"></div>
|
||||
<div class="after"></div>
|
||||
<div class="inner a"></div>
|
||||
<div class="inner b"></div>
|
||||
<div class="inner c"></div>
|
||||
</div>
|
||||
<div class="warning-label">{{ eventName }}</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -144,52 +144,86 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pyro > .before, .pyro > .after {
|
||||
.pyro > .inner {
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
@include animation((1s bang ease-out infinite backwards, 1s gravity ease-in infinite backwards, 5s position linear infinite backwards));
|
||||
@include animation((1.25s ease-out infinite bang, 1.25s ease-in infinite gravity, 10s linear infinite position));
|
||||
|
||||
&.b {
|
||||
@include animation-delay((-4.35s, -4.35s, -4.35s));
|
||||
@include animation-duration((1.45s, 1.45s, 11.6s));
|
||||
}
|
||||
&.c {
|
||||
@include animation-delay((-3.2s, -3.2s, -3.2s));
|
||||
@include animation-duration((1.6s, 1.6s, 12.8s));
|
||||
}
|
||||
}
|
||||
|
||||
.pyro > .after {
|
||||
@include animation-delay((1.25s, 1.25s, 1.25s));
|
||||
@include animation-duration((1.25s, 1.25s, 6.25s));
|
||||
}
|
||||
|
||||
@keyframes bang{
|
||||
to{
|
||||
box-shadow:-314.6666666667px -362.6666666667px red,-51.6666666667px 32.3333333333px #ff3700,-354.6666666667px -264.6666666667px #7b00ff,-319.6666666667px -73.6666666667px #00f7ff,-135.6666666667px -154.6666666667px #00ff48,57.3333333333px -402.6666666667px #0d00ff,-126.6666666667px -121.6666666667px #00ff7b,-335.6666666667px -5.6666666667px #00fff2,-291.6666666667px -.6666666667px #4f0,-126.6666666667px -187.6666666667px #7f0,-413.6666666667px -224.6666666667px #00ffbf,-283.6666666667px -391.6666666667px #00ff3c,-340.6666666667px -345.6666666667px #02f,-168.6666666667px -179.6666666667px #eaff00,7.3333333333px -153.6666666667px #26ff00,-175.6666666667px -234.6666666667px #8400ff,-324.6666666667px -254.6666666667px #0048ff,-335.6666666667px -9.6666666667px #00ff59,-304.6666666667px -8.6666666667px #001eff,-331.6666666667px -44.6666666667px #3f0,.3333333333px -49.6666666667px #0fc,-370.6666666667px -60.6666666667px #0015ff,29.3333333333px -13.6666666667px #8cff00,-168.6666666667px -281.6666666667px #f80,-48.6666666667px -61.6666666667px #f0b,33.3333333333px -113.6666666667px #ff00e1,-193.6666666667px -196.6666666667px #ff7b00,-14.6666666667px -24.6666666667px #ff0037,-149.6666666667px -273.6666666667px #0fa,-19.6666666667px -63.6666666667px #ff0004,13.3333333333px -227.6666666667px #7f0,-265.6666666667px -43.6666666667px #ff4800,-121.6666666667px -95.6666666667px #bfff00,-241.6666666667px -90.6666666667px #6200ff,-307.6666666667px -231.6666666667px #ff0062,78.3333333333px -128.6666666667px #ffbf00,27.3333333333px 44.3333333333px #95ff00,-81.6666666667px 6.3333333333px #ffc800,-343.6666666667px -247.6666666667px #2f0,-225.6666666667px -250.6666666667px #08f,-9.6666666667px -243.6666666667px #ff1a00,83.3333333333px -409.6666666667px #04f,-380.6666666667px -331.6666666667px #84ff00,-103.6666666667px -51.6666666667px #f02,-174.6666666667px -169.6666666667px #ffc800,20.3333333333px -191.6666666667px #ff0059,-40.6666666667px -55.6666666667px #0400ff,-199.6666666667px -66.6666666667px #ffd500,-358.6666666667px -5.6666666667px #0051ff,-84.6666666667px -289.6666666667px #f7ff00,-193.6666666667px -184.6666666667px #80f
|
||||
@keyframes bang {
|
||||
0%, 15% {
|
||||
box-sizing: none;
|
||||
}
|
||||
84% {
|
||||
box-shadow: -30.55vw -35.21vw red,-5.02vw 3.14vw #ff3700,-34.43vw -25.70vw #7b00ff,-31.04vw -7.15vw #00f7ff,-13.17vw -15.02vw #00ff48,5.57vw -39.09vw #0d00ff,-12.30vw -11.81vw #00ff7b,-32.59vw -0.55vw #00fff2,-28.32vw -.6666666667px #4f0,-12.30vw -18.22vw #7f0,-40.16vw -21.81vw #00ffbf,-27.54vw -38.03vw #00ff3c,-33.07vw -33.56vw #02f,-16.38vw -17.44vw #eaff00,0.71vw -14.92vw #26ff00,-17.06vw -22.78vw #8400ff,-31.52vw -24.72vw #0048ff,-32.59vw -0.94vw #00ff59,-29.58vw -0.84vw #001eff,-32.20vw -4.34vw #3f0,.3333333333px -4.82vw #0fc,-35.99vw -5.89vw #0015ff,2.85vw -1.33vw #8cff00,-16.38vw -27.35vw #f80,-4.72vw -5.99vw #f0b,3.24vw -11.04vw #ff00e1,-18.80vw -19.09vw #ff7b00,-1.42vw -2.39vw #ff0037,-14.53vw -26.57vw #0fa,-1.91vw -6.18vw #ff0004,1.29vw -22.10vw #7f0,-25.79vw -4.24vw #ff4800,-11.81vw -9.29vw #bfff00,-23.46vw -8.80vw #6200ff,-29.87vw -22.49vw #ff0062,7.61vw -12.49vw #ffbf00,2.65vw 4.30vw #95ff00,-7.93vw 0.61vw #ffc800,-33.37vw -24.05vw #2f0,-21.91vw -24.34vw #08f,-0.94vw -23.66vw #ff1a00,8.09vw -39.77vw #04f,-36.96vw -32.20vw #84ff00,-10.06vw -5.02vw #f02,-16.96vw -16.47vw #ffc800,1.97vw -18.61vw #ff0059,-3.95vw -5.40vw #0400ff,-19.39vw -6.47vw #ffd500,-34.82vw -0.55vw #0051ff,-8.22vw -28.12vw #f7ff00,-18.80vw -17.93vw #80f;
|
||||
}
|
||||
85%, 100% {
|
||||
box-sizing: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include keyframes(gravity) {
|
||||
to {
|
||||
0% {
|
||||
@include transform(translateY(0px));
|
||||
opacity: 0;
|
||||
}
|
||||
15% {
|
||||
@include transform(translateY(0px));
|
||||
opacity: 1;
|
||||
}
|
||||
84% {
|
||||
@include transform(translateY(400px));
|
||||
opacity: 0;
|
||||
}
|
||||
85%, 100% {
|
||||
@include transform(translateY(200px));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include keyframes(position) {
|
||||
0%, 19.9% {
|
||||
margin-top: 10%;
|
||||
margin-left: 40%;
|
||||
0%, 12.4% {
|
||||
margin-top: 10vw;
|
||||
margin-left: 50%;
|
||||
}
|
||||
20%, 39.9% {
|
||||
margin-top: 40%;
|
||||
margin-left: 30%;
|
||||
12.5%, 24.9% {
|
||||
margin-top: 22vw;
|
||||
margin-left: 35%;
|
||||
}
|
||||
40%, 59.9% {
|
||||
margin-top: 20%;
|
||||
margin-left: 70%
|
||||
25%, 37.4% {
|
||||
margin-top: 15vw;
|
||||
margin-left: 80%
|
||||
}
|
||||
60%, 79.9% {
|
||||
margin-top: 30%;
|
||||
margin-left: 20%;
|
||||
37.5%, 49.9% {
|
||||
margin-top: 28vw;
|
||||
margin-left: 72%;
|
||||
}
|
||||
80%, 99.9% {
|
||||
margin-top: 30%;
|
||||
margin-left: 80%;
|
||||
50%, 62.4% {
|
||||
margin-top: 22vw;
|
||||
margin-left: 37%;
|
||||
}
|
||||
62.5%, 74.9% {
|
||||
margin-top: 10vw;
|
||||
margin-left: 66%;
|
||||
}
|
||||
75%, 87.4% {
|
||||
margin-top: 25vw;
|
||||
margin-left: 49%;
|
||||
}
|
||||
87.5%, 99.9% {
|
||||
margin-top: 18vw;
|
||||
margin-left: 58%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,12 +147,15 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (specialBlocks[block.height] && specialBlocks[block.height].networks.includes(this.stateService.network || 'mainnet')) {
|
||||
this.specialEvent = true;
|
||||
this.eventName = specialBlocks[block.height].labelEventCompleted;
|
||||
setTimeout(() => {
|
||||
for (const block of blocks) {
|
||||
if (specialBlocks[block.height] && specialBlocks[block.height].networks.includes(this.stateService.network || 'mainnet')) {
|
||||
this.specialEvent = true;
|
||||
this.eventName = specialBlocks[block.height].labelEventCompleted;
|
||||
}
|
||||
if (specialBlocks[block.height - 8] && specialBlocks[block.height - 8].networks.includes(this.stateService.network || 'mainnet')) {
|
||||
this.specialEvent = false;
|
||||
}, 60 * 60 * 1000);
|
||||
this.eventName = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
this.resetScrollSubscription = this.stateService.resetScroll$.subscribe(reset => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user