Merge branch 'master' into natsoni/federation-utxos-expiry

This commit is contained in:
natsoni
2024-03-07 10:27:44 +01:00
committed by GitHub
63 changed files with 1202 additions and 137 deletions

View File

@@ -0,0 +1,24 @@
<div class="frame {{ screenSize }}" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
<div class="heading">
<app-svg-images name="officialMempoolSpace" style="width: 144px; height: 36px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
<h3 i18n="addresses.balance">Balances</h3>
<div class="spacer"></div>
</div>
<table class="table table-borderless table-striped table-fixed">
<tr>
<th class="address" i18n="addresses.total">Total</th>
<th class="btc"><app-amount [satoshis]="balance" [digitsInfo]="digitsInfo" [noFiat]="true"></app-amount></th>
<th class="fiat"><app-fiat [value]="balance"></app-fiat></th>
</tr>
<tr *ngFor="let address of page">
<td class="address">
<app-truncate [text]="address" [lastChars]="8" [link]="['/address/' | relativeUrl, address]" [external]="true"></app-truncate>
</td>
<td class="btc"><app-amount [satoshis]="addresses[address]" [digitsInfo]="digitsInfo" [noFiat]="true"></app-amount></td>
<td class="fiat"><app-fiat [value]="addresses[address]"></app-fiat></td>
</tr>
</table>
<div *ngIf="addressStrings.length > itemsPerPage" class="pagination">
<ngb-pagination class="pagination-container float-right" [collectionSize]="addressStrings.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="pageIndex" (pageChange)="pageChange(pageIndex)" [boundaryLinks]="false" [ellipses]="false"></ngb-pagination>
</div>
</div>

View File

@@ -0,0 +1,101 @@
.frame {
position: relative;
background: #24273e;
padding: 0.5rem;
height: calc(100% + 60px);
}
.heading {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: start;
& > * {
flex-basis: 0;
flex-grow: 1;
}
h3 {
text-align: center;
margin: 0 1em;
}
}
.pagination {
position: absolute;
bottom: 0.5rem;
right: 0.5rem;
}
.table {
margin-top: 0.5em;
td, th {
padding: 0.15rem 0.5rem;
&.address {
width: auto;
}
&.btc {
width: 140px;
text-align: right;
}
&.fiat {
width: 142px;
text-align: right;
}
}
tr {
border-collapse: collapse;
&:first-child {
border-bottom: solid 1px white;
td, th {
padding-bottom: 0.3rem;
}
}
&:nth-child(2) {
td, th {
padding-top: 0.3rem;
}
}
&:nth-child(even) {
background: #181b2d;
}
}
@media (min-width: 528px) {
td, th {
&.btc {
width: 160px;
}
&.fiat {
width: 140px;
}
}
}
@media (min-width: 576px) {
td, th {
&.btc {
width: 170px;
}
&.fiat {
width: 140px;
}
}
}
@media (min-width: 992px) {
td, th {
&.btc {
width: 210px;
}
&.fiat {
width: 140px;
}
}
}
}

View File

@@ -0,0 +1,212 @@
import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, catchError } from 'rxjs/operators';
import { Address, Transaction } from '../../interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { of, Subscription, forkJoin } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { AddressInformation } from '../../interfaces/node-api.interface';
@Component({
selector: 'app-address-group',
templateUrl: './address-group.component.html',
styleUrls: ['./address-group.component.scss']
})
export class AddressGroupComponent implements OnInit, OnDestroy {
network = '';
balance = 0;
confirmed = 0;
mempool = 0;
addresses: { [address: string]: number | null };
addressStrings: string[] = [];
addressInfo: { [address: string]: AddressInformation | null };
seenTxs: { [txid: string ]: boolean } = {};
isLoadingAddress = true;
error: any;
mainSubscription: Subscription;
wsSubscription: Subscription;
page: string[] = [];
pageIndex: number = 1;
itemsPerPage: number = 10;
screenSize: 'lg' | 'md' | 'sm' = 'lg';
digitsInfo: string = '1.8-8';
constructor(
private route: ActivatedRoute,
private electrsApiService: ElectrsApiService,
private websocketService: WebsocketService,
private stateService: StateService,
private audioService: AudioService,
private apiService: ApiService,
private seoService: SeoService,
private cd: ChangeDetectorRef,
) { }
ngOnInit(): void {
this.onResize();
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.websocketService.want(['blocks']);
this.mainSubscription = this.route.queryParamMap
.pipe(
switchMap((params: ParamMap) => {
this.error = undefined;
this.isLoadingAddress = true;
this.addresses = {};
this.addressInfo = {};
this.balance = 0;
this.addressStrings = params.get('addresses').split(',').map(address => {
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) {
return address.toLowerCase();
} else {
return address;
}
});
return forkJoin(this.addressStrings.map(address => {
const getLiquidInfo = ((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));
return forkJoin([
of(address),
this.electrsApiService.getAddress$(address),
(getLiquidInfo ? this.apiService.validateAddress$(address) : of(null)),
]);
}));
}),
catchError(e => {
this.error = e;
return of([]);
})
).subscribe((addresses) => {
for (const addressData of addresses) {
const address = addressData[0];
const addressBalance = addressData[1] as Address;
if (addressBalance) {
this.addresses[address] = addressBalance.chain_stats.funded_txo_sum
+ addressBalance.mempool_stats.funded_txo_sum
- addressBalance.chain_stats.spent_txo_sum
- addressBalance.mempool_stats.spent_txo_sum;
this.balance += this.addresses[address];
this.confirmed += (addressBalance.chain_stats.funded_txo_sum - addressBalance.chain_stats.spent_txo_sum);
}
this.addressInfo[address] = addressData[2] ? addressData[2] as AddressInformation : null;
}
this.websocketService.startTrackAddresses(this.addressStrings);
this.isLoadingAddress = false;
this.pageChange(this.pageIndex);
});
this.wsSubscription = this.stateService.multiAddressTransactions$.subscribe(update => {
for (const address of Object.keys(update)) {
for (const tx of update[address].mempool) {
this.addTransaction(tx, false, false);
}
for (const tx of update[address].confirmed) {
this.addTransaction(tx, true, false);
}
for (const tx of update[address].removed) {
this.removeTransaction(tx, tx.status.confirmed);
}
}
});
}
pageChange(index): void {
this.page = this.addressStrings.slice((index - 1) * this.itemsPerPage, index * this.itemsPerPage);
}
addTransaction(transaction: Transaction, confirmed = false, playSound: boolean = true): boolean {
if (this.seenTxs[transaction.txid]) {
this.removeTransaction(transaction, false);
}
this.seenTxs[transaction.txid] = true;
let balance = 0;
transaction.vin.forEach((vin) => {
if (this.addressStrings.includes(vin?.prevout?.scriptpubkey_address)) {
this.addresses[vin?.prevout?.scriptpubkey_address] -= vin.prevout.value;
balance -= vin.prevout.value;
this.balance -= vin.prevout.value;
if (confirmed) {
this.confirmed -= vin.prevout.value;
}
}
});
transaction.vout.forEach((vout) => {
if (this.addressStrings.includes(vout?.scriptpubkey_address)) {
this.addresses[vout?.scriptpubkey_address] += vout.value;
balance += vout.value;
this.balance += vout.value;
if (confirmed) {
this.confirmed += vout.value;
}
}
});
if (playSound) {
if (balance > 0) {
this.audioService.playSound('cha-ching');
} else {
this.audioService.playSound('chime');
}
}
return true;
}
removeTransaction(transaction: Transaction, confirmed = false): boolean {
transaction.vin.forEach((vin) => {
if (this.addressStrings.includes(vin?.prevout?.scriptpubkey_address)) {
this.addresses[vin?.prevout?.scriptpubkey_address] += vin.prevout.value;
this.balance += vin.prevout.value;
if (confirmed) {
this.confirmed += vin.prevout.value;
}
}
});
transaction.vout.forEach((vout) => {
if (this.addressStrings.includes(vout?.scriptpubkey_address)) {
this.addresses[vout?.scriptpubkey_address] -= vout.value;
this.balance -= vout.value;
if (confirmed) {
this.confirmed -= vout.value;
}
}
});
return true;
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (window.innerWidth >= 992) {
this.screenSize = 'lg';
this.digitsInfo = '1.8-8';
} else if (window.innerWidth >= 528) {
this.screenSize = 'md';
this.digitsInfo = '1.4-4';
} else {
this.screenSize = 'sm';
this.digitsInfo = '1.2-2';
}
const newItemsPerPage = Math.floor((window.innerHeight - 150) / 30);
if (newItemsPerPage !== this.itemsPerPage) {
this.itemsPerPage = newItemsPerPage;
this.pageIndex = 1;
this.pageChange(this.pageIndex);
}
}
ngOnDestroy(): void {
this.mainSubscription.unsubscribe();
this.wsSubscription.unsubscribe();
this.websocketService.stopTrackingAddresses();
}
}

View File

@@ -56,7 +56,7 @@
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [precision]="1" minUnit="minute"></app-time></div>
</ng-container>
</div>
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
<div class="animated" [class]="markHeight === block.height ? 'hide' : 'show'" *ngIf="block.extras?.pool != undefined">
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
[routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
{{ block.extras.pool.name}}</a>

View File

@@ -166,7 +166,7 @@
opacity: 1;
}
.hide {
opacity: 0;
opacity: 0.4;
pointer-events : none;
}

View File

@@ -1,4 +1,4 @@
<footer class="footer">
<footer class="footer" [class.inline-footer]="inline">
<div class="container-xl">
<div class="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData">
<div class="col d-none d-sm-block">

View File

@@ -6,6 +6,12 @@
background-color: #1d1f31;
box-shadow: 15px 15px 15px 15px #000;
z-index: 10;
&.inline-footer {
position: relative;
bottom: unset;
top: -44px;
}
}
.sub-text {

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Observable, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
@@ -23,6 +23,8 @@ interface MempoolInfoData {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FooterComponent implements OnInit {
@Input() inline = false;
mempoolBlocksData$: Observable<MempoolBlocksData>;
mempoolInfoData$: Observable<MempoolInfoData>;
vBytesPerSecondLimit = 1667;

View File

@@ -1,7 +1,7 @@
<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks" [class.minimal]="minimal">
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'" *ngIf="(difficultyAdjustments$ | async) as da;">
<div class="flashing">
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
<div class="flashing" *ngIf="(mempoolBlocks$ | async) as mempoolBlocks">
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks" let-i="index" [ngForTrackBy]="trackByFn">
<div
*ngIf="minimal && spotlight > 0 && spotlight === i + 1"
class="spotlight-bottom"

View File

@@ -1,9 +1,9 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
import { Subscription, Observable, of, combineLatest } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
import { Router } from '@angular/router';
import { take, map, switchMap, tap } from 'rxjs/operators';
import { map, switchMap, tap } from 'rxjs/operators';
import { feeLevels, mempoolFeeColors } from '../../app.constants';
import { specialBlocks } from '../../app.constants';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@@ -86,7 +86,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
public stateService: StateService,
private cd: ChangeDetectorRef,
private relativeUrlPipe: RelativeUrlPipe,
private location: Location
private location: Location,
) { }
enabledMiningInfoIfNeeded(url) {
@@ -129,50 +129,44 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
})
);
this.mempoolBlocks$ = merge(
of(true),
fromEvent(window, 'resize')
)
.pipe(
switchMap(() => combineLatest([
this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
this.stateService.mempoolBlocks$
.pipe(
map((mempoolBlocks) => {
if (!mempoolBlocks.length) {
return [{ index: 0, blockSize: 0, blockVSize: 0, feeRange: [0, 0], medianFee: 0, nTx: 0, totalFees: 0 }];
}
return mempoolBlocks;
}),
)
])),
map(([lastBlock, mempoolBlocks]) => {
mempoolBlocks.forEach((block, i) => {
block.index = this.blockIndex + i;
block.height = lastBlock.height + i + 1;
block.blink = specialBlocks[block.height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
});
this.mempoolBlocks$ = combineLatest([
this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
this.stateService.mempoolBlocks$
.pipe(
map((mempoolBlocks) => {
if (!mempoolBlocks.length) {
return [{ index: 0, blockSize: 0, blockVSize: 0, feeRange: [0, 0], medianFee: 0, nTx: 0, totalFees: 0 }];
}
return mempoolBlocks;
}),
)
]).pipe(
map(([lastBlock, mempoolBlocks]) => {
mempoolBlocks.forEach((block, i) => {
block.index = this.blockIndex + i;
block.height = lastBlock.height + i + 1;
block.blink = specialBlocks[block.height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
});
const stringifiedBlocks = JSON.stringify(mempoolBlocks);
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
const stringifiedBlocks = JSON.stringify(mempoolBlocks);
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
this.now = Date.now();
this.now = Date.now();
this.updateMempoolBlockStyles();
this.calculateTransactionPosition();
return this.mempoolBlocks;
}),
tap(() => {
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
if (this.mempoolWidth !== width) {
this.mempoolWidth = width;
this.widthChange.emit(this.mempoolWidth);
this.cd.markForCheck();
}
})
);
this.updateMempoolBlockStyles();
this.calculateTransactionPosition();
return this.mempoolBlocks;
}),
tap(() => {
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
if (this.mempoolWidth !== width) {
this.mempoolWidth = width;
this.widthChange.emit(this.mempoolWidth);
}
})
);
this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$
.pipe(

View File

@@ -0,0 +1,36 @@
<div class="tomahawk">
<div class="links">
<span>Monitoring</span>
<a [routerLink]='"/nodes"'>Nodes</a>
</div>
<app-start [showLoadingIndicator]="true"></app-start>
<app-footer [inline]="true"></app-footer>
<ng-container *ngIf="(hosts$ | async) as hosts">
<div class="status-panel">
<table class="status-table table table-fixed table-borderless table-striped" *ngIf="(tip$ | async) as tip">
<tbody>
<tr>
<th class="rank"></th>
<th class="flag"></th>
<th class="host">Host</th>
<th class="updated">Last checked</th>
<th class="rtt only-small">RTT</th>
<th class="rtt only-large">RTT</th>
<th class="height">Height</th>
</tr>
<tr *ngFor="let host of hosts; let i = index; trackBy: trackByFn">
<td class="rank">{{ i + 1 }}</td>
<td class="flag">{{ host.active ? '⭐️' : host.flag }}</td>
<td class="host">{{ host.link }}</td>
<td class="updated">{{ getLastUpdateSeconds(host) }}</td>
<td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < tip ? '🟧' : '')) }}</td>
</tr>
</tbody>
</table>
</div>
</ng-container>
</div>

View File

@@ -0,0 +1,75 @@
.tomahawk {
.links {
text-align: right;
margin-inline-end: 1em;
a, span {
margin-left: 1em;
}
}
.status-panel {
max-width: 720px;
margin: auto;
padding: 1em;
background: #24273e;
}
.status-table {
width: 100%;
td, th {
padding: 0.25em;
&.rank, &.flag {
width: 28px;
text-align: right;
}
&.updated {
display: none;
width: 130px;
text-align: right;
white-space: pre-wrap;
}
&.rtt, &.height {
width: 92px;
text-align: right;
}
&.only-small {
display: table-cell;
&.rtt {
width: 60px;
}
}
&.only-large {
display: none;
}
&.height {
padding-right: 0.5em;
}
&.host {
width: auto;
overflow: hidden;
text-overflow: ellipsis;
}
@media (min-width: 576px) {
&.rank, &.flag {
width: 32px;
}
&.updated {
display: table-cell;
}
&.rtt, &.height {
width: 96px;
}
&.only-small {
display: none;
}
&.only-large {
display: table-cell;
}
}
}
}
}

View File

@@ -0,0 +1,84 @@
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, ChangeDetectorRef } from '@angular/core';
import { WebsocketService } from '../../services/websocket.service';
import { Observable, Subject, map } from 'rxjs';
import { StateService } from '../../services/state.service';
import { HealthCheckHost } from '../../interfaces/websocket.interface';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-server-health',
templateUrl: './server-health.component.html',
styleUrls: ['./server-health.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ServerHealthComponent implements OnInit {
hosts$: Observable<HealthCheckHost[]>;
tip$: Subject<number>;
interval: number;
now: number = Date.now();
constructor(
private websocketService: WebsocketService,
private stateService: StateService,
private cd: ChangeDetectorRef,
public sanitizer: DomSanitizer,
) {}
ngOnInit(): void {
this.hosts$ = this.stateService.serverHealth$.pipe(
map((hosts) => {
const subpath = window.location.pathname.slice(0, -11);
for (const host of hosts) {
let statusUrl = '';
let linkHost = '';
if (host.socket) {
statusUrl = 'https://' + window.location.hostname + subpath + '/status';
linkHost = window.location.hostname + subpath;
} else {
const hostUrl = new URL(host.host);
statusUrl = 'https://' + hostUrl.hostname + subpath + '/status';
linkHost = hostUrl.hostname + subpath;
}
host.statusPage = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, statusUrl));
host.link = linkHost;
host.flag = this.parseFlag(host.host);
}
return hosts;
})
);
this.tip$ = this.stateService.chainTip$;
this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']);
this.interval = window.setInterval(() => {
this.now = Date.now();
this.cd.markForCheck();
}, 1000);
}
trackByFn(index: number, host: HealthCheckHost): string {
return host.host;
}
getLastUpdateSeconds(host: HealthCheckHost): string {
if (host.lastChecked) {
const seconds = Math.ceil((this.now - host.lastChecked) / 1000);
return `${seconds} second${seconds > 1 ? 's' : ' '} ago`;
} else {
return '~';
}
}
private parseFlag(host: string): string {
if (host.includes('.fra.')) {
return '🇩🇪';
} else if (host.includes('.tk7.')) {
return '🇯🇵';
} else if (host.includes('.fmt.')) {
return '🇺🇸';
} else if (host.includes('.va1.')) {
return '🇺🇸';
} else {
return '';
}
}
}

View File

@@ -0,0 +1,16 @@
<div class="tomahawk">
<div class="links">
<a [routerLink]='"/monitoring"'>Monitoring</a>
<span>Nodes</span>
</div>
<app-start [showLoadingIndicator]="true"></app-start>
<app-footer [inline]="true"></app-footer>
<ng-container *ngFor="let host of hosts; trackBy: trackByFn">
<h5 [id]="host.host" class="hostLink">
<a [href]="'https://' + host.link">{{ host.link }}</a>
</h5>
<iframe class="mempoolStatus" [src]="host.statusPage"></iframe>
</ng-container>
</div>

View File

@@ -0,0 +1,22 @@
.tomahawk {
.links {
text-align: right;
margin-inline-end: 1em;
a, span {
margin-left: 1em;
}
}
.mempoolStatus {
width: 100%;
height: 270px;
border: none;
}
.hostLink {
text-align: center;
margin: auto;
margin-top: 1em;
}
}

View File

@@ -0,0 +1,80 @@
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { WebsocketService } from '../../services/websocket.service';
import { Observable, Subject, Subscription, map, tap } from 'rxjs';
import { StateService } from '../../services/state.service';
import { HealthCheckHost } from '../../interfaces/websocket.interface';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-server-status',
templateUrl: './server-status.component.html',
styleUrls: ['./server-status.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ServerStatusComponent implements OnInit, OnDestroy {
tip$: Subject<number>;
hosts: HealthCheckHost[] = [];
hostSubscription: Subscription;
constructor(
private websocketService: WebsocketService,
private stateService: StateService,
private cd: ChangeDetectorRef,
public sanitizer: DomSanitizer,
) {}
ngOnInit(): void {
this.hostSubscription = this.stateService.serverHealth$.pipe(
map((hosts) => {
const subpath = window.location.pathname.slice(0, -6);
for (const host of hosts) {
let statusUrl = '';
let linkHost = '';
if (host.socket) {
statusUrl = 'https://' + window.location.hostname + subpath + '/status';
linkHost = window.location.hostname + subpath;
} else {
const hostUrl = new URL(host.host);
statusUrl = 'https://' + hostUrl.hostname + subpath + '/status';
linkHost = hostUrl.hostname + subpath;
}
host.statusPage = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, statusUrl));
host.link = linkHost;
}
return hosts;
}),
tap((hosts) => {
if (this.hosts.length !== hosts.length) {
this.hosts = hosts.sort((a,b) => {
const aParts = (a.host?.split('.') || []).reverse();
const bParts = (b.host?.split('.') || []).reverse();
let i = 0;
while (i < Math.max(aParts.length, bParts.length)) {
if (aParts[i] && !bParts[i]) {
return 1;
} else if (bParts[i] && !aParts[i]) {
return -1;
} else if (aParts[i] !== bParts[i]) {
return aParts[i].localeCompare(bParts[i]);
}
i++;
}
return 0;
});
}
this.cd.markForCheck();
})
).subscribe();
this.tip$ = this.stateService.chainTip$;
this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']);
}
trackByFn(index: number, host: HealthCheckHost): string {
return host.host;
}
ngOnDestroy(): void {
this.hosts = [];
this.hostSubscription.unsubscribe();
}
}