mempool/frontend/src/app/components/block-view/block-view.component.ts

181 lines
6.1 KiB
TypeScript

import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, tap, catchError, shareReplay, filter } from 'rxjs/operators';
import { of, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
let bestScore = Infinity;
let best = null;
for (let i = min; i <= max; i++) {
const remainder = (n % i);
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
bestScore = remainder;
best = i;
}
}
return best;
}
@Component({
selector: 'app-block-view',
templateUrl: './block-view.component.html',
styleUrls: ['./block-view.component.scss']
})
export class BlockViewComponent implements OnInit, OnDestroy {
network = '';
block: BlockExtended;
blockHeight: number;
blockHash: string;
rawId: string;
isLoadingBlock = true;
strippedTransactions: TransactionStripped[];
isLoadingOverview = true;
autofit: boolean = false;
resolution: number = 80;
overviewSubscription: Subscription;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
constructor(
private route: ActivatedRoute,
private router: Router,
private electrsApiService: ElectrsApiService,
public stateService: StateService,
private seoService: SeoService,
private apiService: ApiService
) { }
ngOnInit(): void {
this.network = this.stateService.network;
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.autofit = params.autofit === 'true';
if (this.autofit) {
this.onResize();
}
});
const block$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
this.rawId = params.get('id') || '';
const blockHash: string = params.get('id') || '';
this.block = undefined;
let isBlockHeight = false;
if (/^[0-9]+$/.test(blockHash)) {
isBlockHeight = true;
} else {
this.blockHash = blockHash;
}
this.isLoadingBlock = true;
this.isLoadingOverview = true;
if (isBlockHeight) {
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
.pipe(
switchMap((hash) => {
if (hash) {
this.blockHash = hash;
return this.apiService.getBlock$(hash);
} else {
return null;
}
}),
catchError(() => {
return of(null);
}),
);
}
return this.apiService.getBlock$(blockHash);
}),
filter((block: BlockExtended | void) => block != null),
tap((block: BlockExtended) => {
this.block = block;
this.blockHeight = block.height;
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
this.seoService.setDescription($localize`:@@meta.description.liquid.block:See size, weight, fee range, included transactions, and more for Liquid${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
} else {
this.seoService.setDescription($localize`:@@meta.description.bitcoin.block:See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
}
this.isLoadingBlock = false;
this.isLoadingOverview = true;
}),
shareReplay(1)
);
this.overviewSubscription = block$.pipe(
switchMap((block) => this.apiService.getStrippedBlockTransactions$(block.id)
.pipe(
catchError(() => {
return of([]);
}),
switchMap((transactions) => {
return of(transactions);
})
)
),
)
.subscribe((transactions: TransactionStripped[]) => {
this.strippedTransactions = transactions;
this.isLoadingOverview = false;
if (this.blockGraph) {
this.blockGraph.destroy();
this.blockGraph.setup(this.strippedTransactions);
}
},
() => {
this.isLoadingOverview = false;
if (this.blockGraph) {
this.blockGraph.destroy();
}
});
this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {
this.router.navigate([url]);
} else {
window.open(url, '_blank');
}
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (this.autofit) {
this.resolution = bestFitResolution(64, 96, Math.min(window.innerWidth, window.innerHeight));
}
}
ngOnDestroy(): void {
if (this.overviewSubscription) {
this.overviewSubscription.unsubscribe();
}
if (this.networkChangedSubscription) {
this.networkChangedSubscription.unsubscribe();
}
if (this.queryParamsSubscription) {
this.queryParamsSubscription.unsubscribe();
}
}
}