Merge branch 'master' into nymkappa/tx-overflow
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
|
||||
<div class="about-text">
|
||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></h5>
|
||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ®</ng-template></h5>
|
||||
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export class AboutComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
this.backendInfo$ = this.stateService.backendInfo$;
|
||||
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.about:Learn more about The Mempool Open Source Project™\: enterprise sponsors, individual sponsors, integrations, who contributes, FOSS licensing, and more.`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.about:Learn more about The Mempool Open Source Project®\: enterprise sponsors, individual sponsors, integrations, who contributes, FOSS licensing, and more.`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(
|
||||
|
||||
@@ -174,6 +174,11 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.addTransaction(tx);
|
||||
});
|
||||
|
||||
this.stateService.mempoolRemovedTransactions$
|
||||
.subscribe(tx => {
|
||||
this.removeTransaction(tx);
|
||||
});
|
||||
|
||||
this.stateService.blockTransactions$
|
||||
.subscribe((transaction) => {
|
||||
const tx = this.transactions.find((t) => t.txid === transaction.txid);
|
||||
@@ -222,6 +227,30 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
return true;
|
||||
}
|
||||
|
||||
removeTransaction(transaction: Transaction): boolean {
|
||||
const index = this.transactions.findIndex(((tx) => tx.txid === transaction.txid));
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.transactions.splice(index, 1);
|
||||
this.transactions = this.transactions.slice();
|
||||
this.txCount--;
|
||||
|
||||
transaction.vin.forEach((vin) => {
|
||||
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
|
||||
this.sent -= vin.prevout.value;
|
||||
}
|
||||
});
|
||||
transaction.vout.forEach((vout) => {
|
||||
if (vout?.scriptpubkey_address === this.address.address) {
|
||||
this.received -= vout.value;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable, Subscription, combineLatest } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
@@ -123,11 +123,11 @@ export class BlockFeesGraphComponent implements OnInit {
|
||||
this.chartOptions = {
|
||||
title: title,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#FDD835' },
|
||||
{ offset: 1, color: '#FB8C00' },
|
||||
]),
|
||||
new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#C0CA33' },
|
||||
{ offset: 1, color: '#1B5E20' },
|
||||
]),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
@@ -123,11 +123,11 @@ export class BlockRewardsGraphComponent implements OnInit {
|
||||
title: title,
|
||||
animation: false,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#FDD835' },
|
||||
{ offset: 1, color: '#FB8C00' },
|
||||
]),
|
||||
new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#C0CA33' },
|
||||
{ offset: 1, color: '#1B5E20' },
|
||||
]),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption} from 'echarts';
|
||||
import { EChartsOption} from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="block-wrapper">
|
||||
<div class="block-container">
|
||||
<app-block-overview-graph
|
||||
#blockGraph
|
||||
[isLoading]="false"
|
||||
[resolution]="resolution"
|
||||
[blockLimit]="stateService.blockVSize"
|
||||
[orientation]="'top'"
|
||||
[flip]="false"
|
||||
[disableSpinner]="true"
|
||||
(txClickEvent)="onTxClick($event)"
|
||||
></app-block-overview-graph>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
.block-wrapper {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #181b2d;
|
||||
}
|
||||
|
||||
.block-container {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
width: 100vw;
|
||||
max-width: 100vh;
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
* {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
180
frontend/src/app/components/block-view/block-view.component.ts
Normal file
180
frontend/src/app/components/block-view/block-view.component.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.page = 1;
|
||||
this.error = undefined;
|
||||
this.fees = undefined;
|
||||
this.stateService.markBlock$.next({});
|
||||
|
||||
if (history.state.data && history.state.data.blockHeight) {
|
||||
this.blockHeight = history.state.data.blockHeight;
|
||||
@@ -176,6 +175,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
let isBlockHeight = false;
|
||||
if (/^[0-9]+$/.test(blockHash)) {
|
||||
isBlockHeight = true;
|
||||
this.stateService.markBlock$.next({ blockHeight: parseInt(blockHash, 10)});
|
||||
} else {
|
||||
this.blockHash = blockHash;
|
||||
}
|
||||
@@ -202,6 +202,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.location.replaceState(
|
||||
this.router.createUrlTree([(this.network ? '/' + this.network : '') + '/block/', hash]).toString()
|
||||
);
|
||||
this.seoService.updateCanonical(this.location.path());
|
||||
return this.apiService.getBlock$(hash).pipe(
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="text-center" class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.ltr-transition]="ltrTransitionEnabled" #container>
|
||||
<div class="position-container" [ngClass]="network ? network : ''" [style.--divider-offset]="dividerOffset + 'px'" [style.--mempool-offset]="mempoolOffset + 'px'">
|
||||
<div #positionContainer class="position-container" [ngClass]="network ? network : ''" [style]="positionStyle">
|
||||
<span>
|
||||
<div class="blocks-wrapper">
|
||||
<div class="scroll-spacer" *ngIf="minScrollWidth" [style.left]="minScrollWidth + 'px'"></div>
|
||||
|
||||
@@ -26,15 +26,7 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 75px;
|
||||
--divider-offset: 50vw;
|
||||
--mempool-offset: 0px;
|
||||
transform: translateX(calc(var(--divider-offset) + var(--mempool-offset)));
|
||||
}
|
||||
|
||||
.blockchain-wrapper.time-ltr {
|
||||
.position-container {
|
||||
transform: translateX(calc(100vw - var(--divider-offset) - var(--mempool-offset)));
|
||||
}
|
||||
transform: translateX(1280px);
|
||||
}
|
||||
|
||||
.black-background {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, HostListener, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { firstValueFrom, Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@@ -27,8 +27,11 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
||||
loadingTip: boolean = true;
|
||||
connected: boolean = true;
|
||||
|
||||
dividerOffset: number = 0;
|
||||
mempoolOffset: number = 0;
|
||||
dividerOffset: number | null = null;
|
||||
mempoolOffset: number | null = null;
|
||||
positionStyle = {
|
||||
transform: "translateX(1280px)",
|
||||
};
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
@@ -40,6 +43,7 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
||||
this.network = this.stateService.network;
|
||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||
this.timeLtr = !!ltr;
|
||||
this.updateStyle();
|
||||
});
|
||||
this.connectionStateSubscription = this.stateService.connectionState$.subscribe(state => {
|
||||
this.connected = (state === 2);
|
||||
@@ -63,29 +67,47 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
||||
const prevOffset = this.mempoolOffset;
|
||||
this.mempoolOffset = 0;
|
||||
this.mempoolOffsetChange.emit(0);
|
||||
this.updateStyle();
|
||||
setTimeout(() => {
|
||||
this.ltrTransitionEnabled = true;
|
||||
this.flipping = true;
|
||||
this.stateService.timeLtr.next(!this.timeLtr);
|
||||
this.cd.markForCheck();
|
||||
setTimeout(() => {
|
||||
this.ltrTransitionEnabled = false;
|
||||
this.flipping = false;
|
||||
this.mempoolOffset = prevOffset;
|
||||
this.mempoolOffsetChange.emit(this.mempoolOffset);
|
||||
this.mempoolOffsetChange.emit((this.mempoolOffset || 0));
|
||||
this.updateStyle();
|
||||
this.cd.markForCheck();
|
||||
}, 1000);
|
||||
}, 0);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onMempoolWidthChange(width): void {
|
||||
if (this.flipping) {
|
||||
return;
|
||||
}
|
||||
this.mempoolOffset = Math.max(0, width - this.dividerOffset);
|
||||
this.cd.markForCheck();
|
||||
this.mempoolOffset = Math.max(0, width - (this.dividerOffset || 0));
|
||||
this.updateStyle();
|
||||
this.mempoolOffsetChange.emit(this.mempoolOffset);
|
||||
}
|
||||
|
||||
updateStyle(): void {
|
||||
if (this.dividerOffset == null || this.mempoolOffset == null) {
|
||||
return;
|
||||
}
|
||||
const oldTransform = this.positionStyle.transform;
|
||||
this.positionStyle = this.timeLtr ? {
|
||||
transform: `translateX(calc(100vw - ${this.dividerOffset + this.mempoolOffset}px)`,
|
||||
} : {
|
||||
transform: `translateX(${this.dividerOffset + this.mempoolOffset}px)`,
|
||||
};
|
||||
if (oldTransform !== this.positionStyle.transform) {
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.containerWidth) {
|
||||
this.onResize();
|
||||
@@ -107,6 +129,6 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
||||
this.dividerOffset = width * 0.95;
|
||||
}
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
this.updateStyle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
|
||||
|
||||
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !indexingAvailable}">
|
||||
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !isMempoolModule}">
|
||||
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Blocks</h1>
|
||||
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
|
||||
|
||||
@@ -9,28 +9,28 @@
|
||||
<div style="min-height: 295px">
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th class="height text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}" i18n="latest-blocks.height">Height</th>
|
||||
<th *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}" i18n="mining.pool-name"
|
||||
<th class="height text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}" i18n="latest-blocks.height">Height</th>
|
||||
<th *ngIf="isMempoolModule" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}" i18n="mining.pool-name"
|
||||
i18n-ngbTooltip="mining.pool-name" ngbTooltip="Pool" placement="bottom" #miningpool [disableTooltip]="!isEllipsisActive(miningpool)">Pool</th>
|
||||
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Timestamp</th>
|
||||
<th *ngIf="auditAvailable" class="health text-right" i18n="latest-blocks.health" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">Timestamp</th>
|
||||
<th *ngIf="auditAvailable" class="health text-right" i18n="latest-blocks.health" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"
|
||||
i18n-ngbTooltip="latest-blocks.health" ngbTooltip="Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Health</th>
|
||||
<th *ngIf="indexingAvailable" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
<th *ngIf="isMempoolModule" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"
|
||||
i18n-ngbTooltip="latest-blocks.reward" ngbTooltip="Reward" placement="bottom" #reward [disableTooltip]="!isEllipsisActive(reward)">Reward</th>
|
||||
<th *ngIf="indexingAvailable && !widget" class="fees text-right" i18n="latest-blocks.fees" [class]="indexingAvailable ? '' : 'legacy'">Fees</th>
|
||||
<th *ngIf="auditAvailable && !widget" class="fee-delta" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"></th>
|
||||
<th *ngIf="indexingAvailable" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
<th *ngIf="isMempoolModule && !auditAvailable || isMempoolModule && !widget" class="fees text-right" i18n="latest-blocks.fees" [class]="isMempoolModule ? '' : 'legacy'">Fees</th>
|
||||
<th *ngIf="auditAvailable && !widget" class="fee-delta" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"></th>
|
||||
<th *ngIf="isMempoolModule" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"
|
||||
i18n-ngbTooltip="dashboard.txs" ngbTooltip="TXs" placement="bottom" #txs [disableTooltip]="!isEllipsisActive(txs)">TXs</th>
|
||||
<th *ngIf="!indexingAvailable" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">Transactions</th>
|
||||
<th class="size" i18n="latest-blocks.size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Size</th>
|
||||
<th *ngIf="!isMempoolModule" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">Transactions</th>
|
||||
<th class="size" i18n="latest-blocks.size" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">Size</th>
|
||||
</thead>
|
||||
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
|
||||
<td class="height text-left" [class]="widget ? 'widget' : ''">
|
||||
<a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<div class="tooltip-custom">
|
||||
<td *ngIf="isMempoolModule" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<div *ngIf="indexingAvailable" class="tooltip-custom">
|
||||
<a class="clear-link" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]">
|
||||
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
@@ -38,11 +38,17 @@
|
||||
</a>
|
||||
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
|
||||
</div>
|
||||
<div *ngIf="!indexingAvailable" class="tooltip-custom">
|
||||
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
<span class="pool-name">{{ block.extras.pool.name }}</span>
|
||||
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<a
|
||||
*ngIf="block?.extras?.matchRate != null; else nullHealth"
|
||||
class="health-badge badge"
|
||||
@@ -56,21 +62,21 @@
|
||||
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="isMempoolModule" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable && !widget" class="fees text-right" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<td *ngIf="isMempoolModule && !auditAvailable || isMempoolModule && !widget" class="fees text-right" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<app-amount [satoshis]="block.extras.totalFees" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="fee-delta" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="auditAvailable" class="fee-delta" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span *ngIf="block.extras.feeDelta" class="difference" [class.positive]="block.extras.feeDelta >= 0" [class.negative]="block.extras.feeDelta < 0">
|
||||
{{ block.extras.feeDelta > 0 ? '+' : '' }}{{ (block.extras.feeDelta * 100) | amountShortener: 2 }}%
|
||||
</span>
|
||||
</td>
|
||||
<td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
{{ block.tx_count | number }}
|
||||
</td>
|
||||
<td class="size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<td class="size" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool" role="progressbar"
|
||||
[ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div>
|
||||
@@ -82,34 +88,34 @@
|
||||
<ng-template #skeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="height text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td class="height text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="isMempoolModule" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 125px"></span>
|
||||
</td>
|
||||
<td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<td class="timestamp" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 150px"></span>
|
||||
</td>
|
||||
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<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': !indexingAvailable}">
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="isMempoolModule" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable && !widget" class="fees text-right" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<td *ngIf="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]="indexingAvailable ? '' : 'legacy'">
|
||||
<td *ngIf="auditAvailable && !widget" class="fee-delta" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<td class="size" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -19,6 +19,7 @@ export class BlocksList implements OnInit {
|
||||
|
||||
blocks$: Observable<BlockExtended[]> = undefined;
|
||||
|
||||
isMempoolModule = false;
|
||||
indexingAvailable = false;
|
||||
auditAvailable = false;
|
||||
isLoading = true;
|
||||
@@ -39,6 +40,7 @@ export class BlocksList implements OnInit {
|
||||
private cd: ChangeDetectorRef,
|
||||
private seoService: SeoService,
|
||||
) {
|
||||
this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool';
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -75,11 +77,10 @@ export class BlocksList implements OnInit {
|
||||
this.lastBlockHeight = Math.max(...blocks.map(o => o.height));
|
||||
}),
|
||||
map(blocks => {
|
||||
if (this.indexingAvailable) {
|
||||
if (this.stateService.env.BASE_MODULE === 'mempool') {
|
||||
for (const block of blocks) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||
block.extras.pool.slug + '.svg';
|
||||
block.extras.pool.logo = `/resources/mining-pools/` + block.extras.pool.slug + '.svg';
|
||||
}
|
||||
}
|
||||
if (this.widget) {
|
||||
@@ -110,7 +111,7 @@ export class BlocksList implements OnInit {
|
||||
}
|
||||
if (blocks[1]) {
|
||||
this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1;
|
||||
if (this.stateService.env.MINING_DASHBOARD) {
|
||||
if (this.isMempoolModule) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
|
||||
blocks[1][0].extras.pool.slug + '.svg';
|
||||
@@ -121,9 +122,11 @@ export class BlocksList implements OnInit {
|
||||
return acc;
|
||||
}, []),
|
||||
switchMap((blocks) => {
|
||||
blocks.forEach(block => {
|
||||
block.extras.feeDelta = block.extras.expectedFees ? (block.extras.totalFees - block.extras.expectedFees) / block.extras.expectedFees : 0;
|
||||
});
|
||||
if (this.isMempoolModule && this.auditAvailable) {
|
||||
blocks.forEach(block => {
|
||||
block.extras.feeDelta = block.extras.expectedFees ? (block.extras.totalFees - block.extras.expectedFees) / block.extras.expectedFees : 0;
|
||||
});
|
||||
}
|
||||
return of(blocks);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -194,7 +194,7 @@ export class DifficultyComponent implements OnInit {
|
||||
|
||||
@HostListener('pointerdown', ['$event'])
|
||||
onPointerDown(event): void {
|
||||
if (this.epochSvgElement.nativeElement?.contains(event.target)) {
|
||||
if (this.epochSvgElement?.nativeElement?.contains(event.target)) {
|
||||
this.onPointerMove(event);
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -202,7 +202,7 @@ export class DifficultyComponent implements OnInit {
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event): void {
|
||||
if (this.epochSvgElement.nativeElement?.contains(event.target)) {
|
||||
if (this.epochSvgElement?.nativeElement?.contains(event.target)) {
|
||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { merge, Observable, of } from 'rxjs';
|
||||
import { map, mergeMap, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
@@ -204,7 +204,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
title: title,
|
||||
animation: false,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
{ offset: 0, color: '#F4511E99' },
|
||||
{ offset: 0.25, color: '#FB8C0099' },
|
||||
{ offset: 0.5, color: '#FFB30099' },
|
||||
@@ -212,7 +212,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
{ offset: 1, color: '#7CB34299' }
|
||||
]),
|
||||
'#D81B60',
|
||||
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
{ offset: 0, color: '#F4511E' },
|
||||
{ offset: 0.25, color: '#FB8C00' },
|
||||
{ offset: 0.5, color: '#FFB300' },
|
||||
@@ -342,7 +342,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val) => {
|
||||
formatter: (val): string => {
|
||||
const selectedPowerOfTen: any = selectPowerOfTen(val);
|
||||
const newVal = Math.round(val / selectedPowerOfTen.divider);
|
||||
return `${newVal} ${selectedPowerOfTen.unit}H/s`;
|
||||
@@ -364,9 +364,9 @@ export class HashrateChartComponent implements OnInit {
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val) => {
|
||||
formatter: (val): string => {
|
||||
if (this.stateService.network === 'signet') {
|
||||
return val;
|
||||
return `${val}`;
|
||||
}
|
||||
const selectedPowerOfTen: any = selectPowerOfTen(val);
|
||||
const newVal = Math.round(val / selectedPowerOfTen.divider);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { OnChanges } from '@angular/core';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/graphs.utils';
|
||||
@@ -37,6 +37,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
};
|
||||
windowPreference: string;
|
||||
chartInstance: any = undefined;
|
||||
MA: number[][] = [];
|
||||
weightMode: boolean = false;
|
||||
rateUnitSub: Subscription;
|
||||
|
||||
@@ -62,6 +63,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
return;
|
||||
}
|
||||
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
|
||||
this.MA = this.calculateMA(this.data.series[0]);
|
||||
this.mountChart();
|
||||
}
|
||||
|
||||
@@ -72,7 +74,101 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
/// calculate the moving average of maData
|
||||
calculateMA(maData): number[][] {
|
||||
//update const variables that are not changed
|
||||
const ma: number[][] = [];
|
||||
let sum = 0;
|
||||
let i = 0;
|
||||
const len = maData.length;
|
||||
|
||||
//Adjust window length based on the length of the data
|
||||
//5% appeared as a good amount from tests
|
||||
//TODO: make this a text box in the UI
|
||||
const maWindowLen = Math.ceil(len * 0.05);
|
||||
|
||||
//calculate the center of the moving average window
|
||||
const center = Math.floor(maWindowLen / 2);
|
||||
|
||||
//calculate the centered moving average
|
||||
for (i = center; i < len - center; i++) {
|
||||
sum = 0;
|
||||
//build out ma as we loop through the data
|
||||
ma[i] = [];
|
||||
ma[i].push(maData[i][0]);
|
||||
for (let j = i - center; j <= i + center; j++) {
|
||||
sum += maData[j][1];
|
||||
}
|
||||
|
||||
ma[i].push(sum / maWindowLen);
|
||||
}
|
||||
|
||||
//return the moving average array
|
||||
return ma;
|
||||
}
|
||||
|
||||
mountChart(): void {
|
||||
//create an array for the echart series
|
||||
//similar to how it is done in mempool-graph.component.ts
|
||||
const seriesGraph = [];
|
||||
seriesGraph.push({
|
||||
zlevel: 0,
|
||||
name: 'data',
|
||||
data: this.data.series[0],
|
||||
type: 'line',
|
||||
smooth: false,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 1,
|
||||
width: 2,
|
||||
},
|
||||
data: [{
|
||||
yAxis: 1667,
|
||||
label: {
|
||||
show: false,
|
||||
color: '#ffffff',
|
||||
}
|
||||
}],
|
||||
}
|
||||
},
|
||||
{
|
||||
zlevel: 0,
|
||||
name: 'MA',
|
||||
data: this.MA,
|
||||
type: 'line',
|
||||
smooth: false,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
color: "white",
|
||||
},
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 1,
|
||||
width: 2,
|
||||
},
|
||||
data: [{
|
||||
yAxis: 1667,
|
||||
label: {
|
||||
show: false,
|
||||
color: '#ffffff',
|
||||
}
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
this.mempoolStatsChartOption = {
|
||||
grid: {
|
||||
height: this.height,
|
||||
@@ -122,16 +218,20 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
type: 'line',
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
|
||||
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
|
||||
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ` + color + `"></span>`;
|
||||
let itemFormatted = '<div class="title">' + axisValueLabel + '</div>';
|
||||
params.map((item: any, index: number) => {
|
||||
if (index < 26) {
|
||||
itemFormatted += `<div class="item">
|
||||
<div class="indicator-container">${colorSpan(item.color)}</div>
|
||||
<div class="grow"></div>
|
||||
<div class="value">${formatNumber(this.weightMode ? item.value[1] * 4 : item.value[1], this.locale, '1.0-0')} <span class="symbol">${this.weightMode ? 'WU' : 'vB'}/s</span></div>
|
||||
</div>`;
|
||||
|
||||
//Do no include MA in tooltip legend!
|
||||
if (item.seriesName !== 'MA') {
|
||||
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[1], this.locale, '1.0-0')}<span class="symbol">vB/s</span></div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
|
||||
@@ -171,35 +271,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
zlevel: 0,
|
||||
data: this.data.series[0],
|
||||
type: 'line',
|
||||
smooth: false,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 1,
|
||||
width: 2,
|
||||
},
|
||||
data: [{
|
||||
yAxis: 1667,
|
||||
label: {
|
||||
show: false,
|
||||
color: '#ffffff',
|
||||
}
|
||||
}],
|
||||
}
|
||||
},
|
||||
],
|
||||
series: seriesGraph,
|
||||
visualMap: {
|
||||
show: false,
|
||||
top: 50,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { formatDate, formatNumber } from '@angular/common';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lbtc-pegs-graph',
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="block-wrapper">
|
||||
<div class="block-container">
|
||||
<app-mempool-block-overview [index]="index"></app-mempool-block-overview>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
.block-wrapper {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #181b2d;
|
||||
}
|
||||
|
||||
.block-container {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
width: 100vw;
|
||||
max-width: 100vh;
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
* {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { Subscription, filter, map, switchMap, tap } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
|
||||
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-mempool-block-view',
|
||||
templateUrl: './mempool-block-view.component.html',
|
||||
styleUrls: ['./mempool-block-view.component.scss']
|
||||
})
|
||||
export class MempoolBlockViewComponent implements OnInit, OnDestroy {
|
||||
autofit: boolean = false;
|
||||
resolution: number = 80;
|
||||
index: number = 0;
|
||||
|
||||
routeParamsSubscription: Subscription;
|
||||
queryParamsSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private websocketService: WebsocketService,
|
||||
public stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||
|
||||
this.routeParamsSubscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.index = parseInt(params.get('index'), 10) || 0;
|
||||
return this.stateService.mempoolBlocks$
|
||||
.pipe(
|
||||
map((blocks) => {
|
||||
if (!blocks.length) {
|
||||
return [{ index: 0, blockSize: 0, blockVSize: 0, feeRange: [0, 0], medianFee: 0, nTx: 0, totalFees: 0 }];
|
||||
}
|
||||
return blocks;
|
||||
}),
|
||||
filter((mempoolBlocks) => mempoolBlocks.length > 0),
|
||||
tap((mempoolBlocks) => {
|
||||
while (!mempoolBlocks[this.index]) {
|
||||
this.index--;
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
).subscribe();
|
||||
|
||||
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
|
||||
this.autofit = params.autofit === 'true';
|
||||
if (this.autofit) {
|
||||
this.onResize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (this.autofit) {
|
||||
this.resolution = bestFitResolution(64, 96, Math.min(window.innerWidth, window.innerHeight));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.routeParamsSubscription.unsubscribe();
|
||||
this.queryParamsSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,10 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
ngOnInit() {
|
||||
this.chainTip = this.stateService.latestBlockHeight;
|
||||
|
||||
const width = this.containerOffset + (this.stateService.env.MEMPOOL_BLOCKS_AMOUNT) * this.blockOffset;
|
||||
this.mempoolWidth = width;
|
||||
this.widthChange.emit(this.mempoolWidth);
|
||||
|
||||
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
||||
this.enabledMiningInfoIfNeeded(this.location.path());
|
||||
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
|
||||
@@ -161,11 +165,11 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return this.mempoolBlocks;
|
||||
}),
|
||||
tap(() => {
|
||||
this.cd.markForCheck();
|
||||
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
|
||||
if (this.mempoolWidth !== width) {
|
||||
this.mempoolWidth = width;
|
||||
this.widthChange.emit(this.mempoolWidth);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -215,11 +219,13 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (isNewBlock && (block?.extras?.similarity == null || block?.extras?.similarity > 0.5) && !this.tabHidden) {
|
||||
this.blockIndex++;
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.chainTipSubscription = this.stateService.chainTip$.subscribe((height) => {
|
||||
if (this.chainTip === -1) {
|
||||
this.chainTip = height;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -257,6 +263,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.blockPadding = 0.24 * this.blockWidth;
|
||||
this.containerOffset = 0.32 * this.blockWidth;
|
||||
this.blockOffset = this.blockWidth + this.blockPadding;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +282,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
onResize(): void {
|
||||
this.animateEntry = false;
|
||||
this.reduceEmptyBlocksToFitScreen(this.mempoolEmptyBlocks);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
trackByFn(index: number, block: MempoolBlock) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnChanges } from '@angular/core';
|
||||
import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe';
|
||||
import { WuBytesPipe } from '../../shared/pipes/bytes-pipe/wubytes.pipe';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { OptimizedMempoolStats } from '../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { feeLevels, chartColors } from '../../app.constants';
|
||||
import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/graphs.utils';
|
||||
|
||||
@@ -26,6 +27,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
@Input() data: any[];
|
||||
@Input() filterSize = 100000;
|
||||
@Input() limitFilterFee = 1;
|
||||
@Input() hideCount: boolean = true;
|
||||
@Input() height: number | string = 200;
|
||||
@Input() top: number | string = 20;
|
||||
@Input() right: number | string = 10;
|
||||
@@ -50,10 +52,13 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
inverted: boolean;
|
||||
chartInstance: any = undefined;
|
||||
weightMode: boolean = false;
|
||||
isWidget: boolean = false;
|
||||
showCount: boolean = false;
|
||||
|
||||
constructor(
|
||||
private vbytesPipe: VbytesPipe,
|
||||
private wubytesPipe: WuBytesPipe,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
private stateService: StateService,
|
||||
private storageService: StorageService,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
@@ -62,12 +67,16 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
ngOnInit(): void {
|
||||
this.isLoading = true;
|
||||
this.inverted = this.storageService.getValue('inverted-graph') === 'true';
|
||||
this.isWidget = this.template === 'widget';
|
||||
this.showCount = !this.isWidget && !this.hideCount;
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
ngOnChanges(changes) {
|
||||
if (!this.data) {
|
||||
return;
|
||||
}
|
||||
this.isWidget = this.template === 'widget';
|
||||
this.showCount = !this.isWidget && !this.hideCount;
|
||||
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
|
||||
this.mempoolVsizeFeesData = this.handleNewMempoolData(this.data.concat([]));
|
||||
this.mountFeeChart();
|
||||
@@ -96,10 +105,12 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
mempoolStats.reverse();
|
||||
const labels = mempoolStats.map(stats => stats.added);
|
||||
const finalArrayVByte = this.generateArray(mempoolStats);
|
||||
const finalArrayCount = this.generateCountArray(mempoolStats);
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
series: finalArrayVByte
|
||||
series: finalArrayVByte,
|
||||
countSeries: finalArrayCount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -124,9 +135,13 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
return finalArray;
|
||||
}
|
||||
|
||||
generateCountArray(mempoolStats: OptimizedMempoolStats[]) {
|
||||
return mempoolStats.filter(stats => stats.count > 0).map(stats => [stats.added * 1000, stats.count]);
|
||||
}
|
||||
|
||||
mountFeeChart() {
|
||||
this.orderLevels();
|
||||
const { series } = this.mempoolVsizeFeesData;
|
||||
const { series, countSeries } = this.mempoolVsizeFeesData;
|
||||
|
||||
const seriesGraph = [];
|
||||
const newColors = [];
|
||||
@@ -178,6 +193,29 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.showCount) {
|
||||
newColors.push('white');
|
||||
seriesGraph.push({
|
||||
zlevel: 1,
|
||||
yAxisIndex: 1,
|
||||
name: 'count',
|
||||
type: 'line',
|
||||
stack: 'count',
|
||||
smooth: false,
|
||||
markPoint: false,
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
opacity: 1,
|
||||
},
|
||||
symbol: 'none',
|
||||
silent: true,
|
||||
areaStyle: {
|
||||
color: null,
|
||||
opacity: 0,
|
||||
},
|
||||
data: countSeries,
|
||||
});
|
||||
}
|
||||
|
||||
this.mempoolVsizeFeesOptions = {
|
||||
series: this.inverted ? [...seriesGraph].reverse() : seriesGraph,
|
||||
@@ -201,7 +239,11 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
label: {
|
||||
formatter: (params: any) => {
|
||||
if (params.axisDimension === 'y') {
|
||||
return this.vbytesPipe.transform(params.value, 2, 'vB', 'MvB', true)
|
||||
if (params.axisIndex === 0) {
|
||||
return this.vbytesPipe.transform(params.value, 2, 'vB', 'MvB', true);
|
||||
} else {
|
||||
return this.amountShortenerPipe.transform(params.value, 2, undefined, true);
|
||||
}
|
||||
} else {
|
||||
return formatterXAxis(this.locale, this.windowPreference, params.value);
|
||||
}
|
||||
@@ -214,7 +256,11 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
const itemFormatted = [];
|
||||
let totalParcial = 0;
|
||||
let progressPercentageText = '';
|
||||
const items = this.inverted ? [...params].reverse() : params;
|
||||
let countItem;
|
||||
let items = this.inverted ? [...params].reverse() : params;
|
||||
if (items[items.length - 1].seriesName === 'count') {
|
||||
countItem = items.pop();
|
||||
}
|
||||
items.map((item: any, index: number) => {
|
||||
totalParcial += item.value[1];
|
||||
const progressPercentage = (item.value[1] / totalValue) * 100;
|
||||
@@ -276,6 +322,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
</tr>`);
|
||||
});
|
||||
const classActive = (this.template === 'advanced') ? 'fees-wrapper-tooltip-chart-advanced' : '';
|
||||
const titleCount = $localize`Count`;
|
||||
const titleRange = $localize`Range`;
|
||||
const titleSize = $localize`:@@7faaaa08f56427999f3be41df1093ce4089bbd75:Size`;
|
||||
const titleSum = $localize`Sum`;
|
||||
@@ -286,6 +333,25 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
${this.vbytesPipe.transform(totalValue, 2, 'vB', 'MvB', false)}
|
||||
</span>
|
||||
</div>
|
||||
` +
|
||||
(this.showCount && countItem ? `
|
||||
<table class="count">
|
||||
<tbody>
|
||||
<tr class="item">
|
||||
<td class="indicator-container">
|
||||
<span class="indicator" style="background-color: white"></span>
|
||||
<span>
|
||||
${titleCount}
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: right;">
|
||||
<span>${this.amountShortenerPipe.transform(countItem.value[1], 2, undefined, true)}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
` : '')
|
||||
+ `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -305,12 +371,12 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
</div>`;
|
||||
}
|
||||
},
|
||||
dataZoom: (this.template === 'widget' && this.isMobile()) ? null : [{
|
||||
dataZoom: (this.isWidget && this.isMobile()) ? null : [{
|
||||
type: 'inside',
|
||||
realtime: true,
|
||||
zoomLock: (this.template === 'widget') ? true : false,
|
||||
zoomLock: (this.isWidget) ? true : false,
|
||||
zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
|
||||
moveOnMouseMove: (this.template === 'widget') ? true : false,
|
||||
moveOnMouseMove: (this.isWidget) ? true : false,
|
||||
maxSpan: 100,
|
||||
minSpan: 10,
|
||||
}, {
|
||||
@@ -339,7 +405,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
name: this.template === 'widget' ? '' : formatterXAxisLabel(this.locale, this.windowPreference),
|
||||
name: this.isWidget ? '' : formatterXAxisLabel(this.locale, this.windowPreference),
|
||||
nameLocation: 'middle',
|
||||
nameTextStyle: {
|
||||
padding: [20, 0, 0, 0],
|
||||
@@ -357,7 +423,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
},
|
||||
}
|
||||
],
|
||||
yAxis: {
|
||||
yAxis: [{
|
||||
type: 'value',
|
||||
axisLine: { onZero: false },
|
||||
axisLabel: {
|
||||
@@ -371,7 +437,17 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
opacity: 0.25,
|
||||
}
|
||||
}
|
||||
},
|
||||
}, this.showCount ? {
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLine: { onZero: false },
|
||||
axisLabel: {
|
||||
formatter: (value: number) => (`${this.amountShortenerPipe.transform(value, 2, undefined, true)}`),
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
}
|
||||
} : null],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
import { EChartsOption, PieSeriesOption } from '../../graphs/echarts';
|
||||
import { merge, Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map, switchMap, catchError } from 'rxjs/operators';
|
||||
import { PoolStat } from '../../interfaces/node-api.interface';
|
||||
@@ -127,7 +127,7 @@ export class PoolPreviewComponent implements OnInit {
|
||||
title: title,
|
||||
animation: false,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
{ offset: 0, color: '#F4511E' },
|
||||
{ offset: 0.25, color: '#FB8C00' },
|
||||
{ offset: 0.5, color: '#FFB300' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { BehaviorSubject, Observable, of, timer } from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, PoolStat } from '../../interfaces/node-api.interface';
|
||||
@@ -131,7 +131,7 @@ export class PoolComponent implements OnInit {
|
||||
title: title,
|
||||
animation: false,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
{ offset: 0, color: '#F4511E' },
|
||||
{ offset: 0.25, color: '#FB8C00' },
|
||||
{ offset: 0.5, color: '#FFB300' },
|
||||
|
||||
@@ -17,6 +17,6 @@ export class PrivacyPolicyComponent {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle('Privacy Policy');
|
||||
this.seoService.setDescription('Trusted third parties are security holes, as are trusted first parties...you should only trust your own self-hosted instance of The Mempool Open Source Project™.');
|
||||
this.seoService.setDescription('Trusted third parties are security holes, as are trusted first parties...you should only trust your own self-hosted instance of The Mempool Open Source Project®.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer
|
||||
[class.menu-open]="menuOpen"
|
||||
[class.menu-closing]="menuSliding && !menuOpen"
|
||||
[class.with-menu]="hasMenu"
|
||||
(mousedown)="onMouseDown($event)"
|
||||
(pointerdown)="onPointerDown($event)"
|
||||
(touchmove)="onTouchMove($event)"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
width: calc(100% + 120px);
|
||||
width: 100%;
|
||||
|
||||
transform: translateX(0px);
|
||||
transition: transform 0;
|
||||
@@ -20,6 +20,10 @@
|
||||
transform: translateX(0px);
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
|
||||
&.with-menu {
|
||||
width: calc(100% + 120px);
|
||||
}
|
||||
}
|
||||
|
||||
#blockchain-container::-webkit-scrollbar {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input, DoCheck } from '@angular/core';
|
||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input, ChangeDetectorRef, ChangeDetectionStrategy, AfterViewChecked } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { MarkBlockState, StateService } from '../../services/state.service';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
@@ -8,8 +8,9 @@ import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
selector: 'app-start',
|
||||
templateUrl: './start.component.html',
|
||||
styleUrls: ['./start.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
@Input() showLoadingIndicator = false;
|
||||
|
||||
interval = 60;
|
||||
@@ -23,7 +24,7 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
timeLtrSubscription: Subscription;
|
||||
timeLtr: boolean = this.stateService.timeLtr.value;
|
||||
chainTipSubscription: Subscription;
|
||||
chainTip: number = -1;
|
||||
chainTip: number = 100;
|
||||
tipIsSet: boolean = false;
|
||||
lastMark: MarkBlockState;
|
||||
markBlockSubscription: Subscription;
|
||||
@@ -41,7 +42,8 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
blocksPerPage: number = 1;
|
||||
pageWidth: number;
|
||||
firstPageWidth: number;
|
||||
minScrollWidth: number;
|
||||
minScrollWidth: number = 40 + (155 * (8 + (2 * Math.ceil(window.innerWidth / 155))));
|
||||
currentScrollWidth: number = null;
|
||||
pageIndex: number = 0;
|
||||
pages: any[] = [];
|
||||
pendingMark: number | null = null;
|
||||
@@ -49,25 +51,24 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
lastUpdate: number = 0;
|
||||
lastMouseX: number;
|
||||
velocity: number = 0;
|
||||
mempoolOffset: number = 0;
|
||||
mempoolOffset: number = null;
|
||||
mempoolWidth: number = 0;
|
||||
scrollLeft: number = null;
|
||||
|
||||
private resizeObserver: ResizeObserver;
|
||||
chainWidth: number = window.innerWidth;
|
||||
menuOpen: boolean = false;
|
||||
menuSliding: boolean = false;
|
||||
menuTimeout: number;
|
||||
|
||||
hasMenu = false;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.isiOS = ['iPhone','iPod','iPad'].includes((navigator as any)?.userAgentData?.platform || navigator.platform);
|
||||
}
|
||||
|
||||
ngDoCheck(): void {
|
||||
if (this.pendingOffset != null) {
|
||||
const offset = this.pendingOffset;
|
||||
this.pendingOffset = null;
|
||||
this.addConvertedScrollOffset(offset);
|
||||
if (this.stateService.network === '') {
|
||||
this.hasMenu = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +78,7 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
this.blockCount = blocks.length;
|
||||
this.dynamicBlocksAmount = Math.min(this.blockCount, this.stateService.env.KEEP_BLOCKS_AMOUNT, 8);
|
||||
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
||||
this.minScrollWidth = 40 + (8 * this.blockWidth) + (this.pageWidth * 2);
|
||||
if (this.blockCount <= Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT)) {
|
||||
this.onResize();
|
||||
}
|
||||
@@ -122,7 +124,7 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
this.scrollToBlock(scrollToHeight);
|
||||
}
|
||||
}
|
||||
if (!this.tipIsSet || (blockHeight < 0 && !this.mempoolOffset)) {
|
||||
if (!this.tipIsSet || (blockHeight < 0 && this.mempoolOffset == null)) {
|
||||
this.pendingMark = blockHeight;
|
||||
}
|
||||
}
|
||||
@@ -168,15 +170,47 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewChecked(): void {
|
||||
if (this.currentScrollWidth !== this.blockchainContainer?.nativeElement?.scrollWidth) {
|
||||
this.currentScrollWidth = this.blockchainContainer?.nativeElement?.scrollWidth;
|
||||
if (this.pendingOffset != null) {
|
||||
const delta = this.pendingOffset - (this.mempoolOffset || 0);
|
||||
this.mempoolOffset = this.pendingOffset;
|
||||
this.currentScrollWidth = this.blockchainContainer?.nativeElement?.scrollWidth;
|
||||
this.pendingOffset = null;
|
||||
this.addConvertedScrollOffset(delta);
|
||||
this.applyPendingMarkArrow();
|
||||
} else {
|
||||
this.applyScrollLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMempoolOffsetChange(offset): void {
|
||||
const delta = offset - this.mempoolOffset;
|
||||
this.addConvertedScrollOffset(delta);
|
||||
this.mempoolOffset = offset;
|
||||
this.applyPendingMarkArrow();
|
||||
if (offset !== this.mempoolOffset) {
|
||||
this.pendingOffset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
applyScrollLeft(): void {
|
||||
if (this.blockchainContainer?.nativeElement?.scrollWidth) {
|
||||
let lastScrollLeft = null;
|
||||
while (this.scrollLeft < 0 && this.shiftPagesForward() && lastScrollLeft !== this.scrollLeft) {
|
||||
lastScrollLeft = this.scrollLeft;
|
||||
this.scrollLeft += this.pageWidth;
|
||||
}
|
||||
lastScrollLeft = null;
|
||||
while (this.scrollLeft > this.blockchainContainer.nativeElement.scrollWidth && this.shiftPagesBack() && lastScrollLeft !== this.scrollLeft) {
|
||||
lastScrollLeft = this.scrollLeft;
|
||||
this.scrollLeft -= this.pageWidth;
|
||||
}
|
||||
this.blockchainContainer.nativeElement.scrollLeft = this.scrollLeft;
|
||||
}
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
|
||||
applyPendingMarkArrow(): void {
|
||||
if (this.pendingMark != null) {
|
||||
if (this.pendingMark != null && this.pendingMark <= this.chainTip) {
|
||||
if (this.pendingMark < 0) {
|
||||
this.scrollToBlock(this.chainTip - this.pendingMark);
|
||||
} else {
|
||||
@@ -191,6 +225,7 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
window.clearTimeout(this.menuTimeout);
|
||||
this.menuTimeout = window.setTimeout(() => {
|
||||
this.menuSliding = false;
|
||||
this.cd.markForCheck();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@@ -200,34 +235,33 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
this.isMobile = this.chainWidth <= 767.98;
|
||||
let firstVisibleBlock;
|
||||
let offset;
|
||||
if (this.blockchainContainer?.nativeElement != null) {
|
||||
this.pages.forEach(page => {
|
||||
const left = page.offset - this.getConvertedScrollOffset();
|
||||
const right = left + this.pageWidth;
|
||||
if (left <= 0 && right > 0) {
|
||||
const blockIndex = Math.max(0, Math.floor(left / -this.blockWidth));
|
||||
firstVisibleBlock = page.height - blockIndex;
|
||||
offset = left + (blockIndex * this.blockWidth);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.pages.forEach(page => {
|
||||
const left = page.offset - this.getConvertedScrollOffset(this.scrollLeft);
|
||||
const right = left + this.pageWidth;
|
||||
if (left <= 0 && right > 0) {
|
||||
const blockIndex = Math.max(0, Math.floor(left / -this.blockWidth));
|
||||
firstVisibleBlock = page.height - blockIndex;
|
||||
offset = left + (blockIndex * this.blockWidth);
|
||||
}
|
||||
});
|
||||
|
||||
this.blocksPerPage = Math.ceil(this.chainWidth / this.blockWidth);
|
||||
this.pageWidth = this.blocksPerPage * this.blockWidth;
|
||||
this.minScrollWidth = this.firstPageWidth + (this.pageWidth * 2);
|
||||
this.minScrollWidth = 40 + (8 * this.blockWidth) + (this.pageWidth * 2);
|
||||
|
||||
if (firstVisibleBlock != null) {
|
||||
this.scrollToBlock(firstVisibleBlock, offset + (this.isMobile ? this.blockWidth : 0));
|
||||
this.scrollToBlock(firstVisibleBlock, offset);
|
||||
} else {
|
||||
this.updatePages();
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onMouseDown(event: MouseEvent) {
|
||||
if (!(event.which > 1 || event.button > 0)) {
|
||||
this.mouseDragStartX = event.clientX;
|
||||
this.resetMomentum(event.clientX);
|
||||
this.blockchainScrollLeftInit = this.blockchainContainer.nativeElement.scrollLeft;
|
||||
this.blockchainScrollLeftInit = this.scrollLeft;
|
||||
}
|
||||
}
|
||||
onPointerDown(event: PointerEvent) {
|
||||
@@ -253,8 +287,8 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
if (this.mouseDragStartX != null) {
|
||||
this.updateVelocity(event.clientX);
|
||||
this.stateService.setBlockScrollingInProgress(true);
|
||||
this.blockchainContainer.nativeElement.scrollLeft =
|
||||
this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX;
|
||||
this.scrollLeft = this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX;
|
||||
this.applyScrollLeft();
|
||||
}
|
||||
}
|
||||
@HostListener('document:mouseup', [])
|
||||
@@ -310,25 +344,31 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
} else {
|
||||
this.velocity += dv;
|
||||
}
|
||||
this.blockchainContainer.nativeElement.scrollLeft -= displacement;
|
||||
this.scrollLeft -= displacement;
|
||||
this.applyScrollLeft();
|
||||
this.animateMomentum();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onScroll(e) {
|
||||
if (this.blockchainContainer?.nativeElement?.scrollLeft == null) {
|
||||
return;
|
||||
}
|
||||
this.scrollLeft = this.blockchainContainer?.nativeElement?.scrollLeft;
|
||||
const middlePage = this.pageIndex === 0 ? this.pages[0] : this.pages[1];
|
||||
// compensate for css transform
|
||||
const translation = (this.isMobile ? this.chainWidth * 0.95 : this.chainWidth * 0.5);
|
||||
const backThreshold = middlePage.offset + (this.pageWidth * 0.5) + translation;
|
||||
const forwardThreshold = middlePage.offset - (this.pageWidth * 0.5) + translation;
|
||||
const scrollLeft = this.getConvertedScrollOffset();
|
||||
if (scrollLeft > backThreshold) {
|
||||
this.scrollLeft = this.blockchainContainer.nativeElement.scrollLeft;
|
||||
const offsetScroll = this.getConvertedScrollOffset(this.scrollLeft);
|
||||
if (offsetScroll > backThreshold) {
|
||||
if (this.shiftPagesBack()) {
|
||||
this.addConvertedScrollOffset(-this.pageWidth);
|
||||
this.blockchainScrollLeftInit -= this.pageWidth;
|
||||
}
|
||||
} else if (scrollLeft < forwardThreshold) {
|
||||
} else if (offsetScroll < forwardThreshold) {
|
||||
if (this.shiftPagesForward()) {
|
||||
this.addConvertedScrollOffset(this.pageWidth);
|
||||
this.blockchainScrollLeftInit += this.pageWidth;
|
||||
@@ -337,10 +377,6 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
}
|
||||
|
||||
scrollToBlock(height, blockOffset = 0) {
|
||||
if (!this.blockchainContainer?.nativeElement) {
|
||||
setTimeout(() => { this.scrollToBlock(height, blockOffset); }, 50);
|
||||
return;
|
||||
}
|
||||
if (this.isMobile) {
|
||||
blockOffset -= this.blockWidth;
|
||||
}
|
||||
@@ -348,15 +384,15 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
const pages = [];
|
||||
this.pageIndex = Math.max(viewingPageIndex - 1, 0);
|
||||
let viewingPage = this.getPageAt(viewingPageIndex);
|
||||
const isLastPage = viewingPage.height < this.blocksPerPage;
|
||||
const isLastPage = viewingPage.height <= 0;
|
||||
if (isLastPage) {
|
||||
this.pageIndex = Math.max(viewingPageIndex - 2, 0);
|
||||
viewingPage = this.getPageAt(viewingPageIndex);
|
||||
}
|
||||
const left = viewingPage.offset - this.getConvertedScrollOffset();
|
||||
const left = viewingPage.offset - this.getConvertedScrollOffset(this.scrollLeft);
|
||||
const blockIndex = viewingPage.height - height;
|
||||
const targetOffset = (this.blockWidth * blockIndex) + left;
|
||||
let deltaOffset = targetOffset - blockOffset;
|
||||
const deltaOffset = targetOffset - blockOffset;
|
||||
|
||||
if (isLastPage) {
|
||||
pages.push(this.getPageAt(viewingPageIndex - 2));
|
||||
@@ -386,6 +422,7 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
pages.push(this.getPageAt(this.pageIndex + 1));
|
||||
pages.push(this.getPageAt(this.pageIndex + 2));
|
||||
this.pages = pages;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
shiftPagesBack(): boolean {
|
||||
@@ -439,44 +476,40 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
blockInViewport(height: number): boolean {
|
||||
const firstHeight = this.pages[0].height;
|
||||
const translation = (this.isMobile ? this.chainWidth * 0.95 : this.chainWidth * 0.5);
|
||||
const firstX = this.pages[0].offset - this.getConvertedScrollOffset() + translation;
|
||||
const firstX = this.pages[0].offset - this.getConvertedScrollOffset(this.scrollLeft) + translation;
|
||||
const xPos = firstX + ((firstHeight - height) * 155);
|
||||
return xPos > -55 && xPos < (this.chainWidth - 100);
|
||||
}
|
||||
|
||||
getConvertedScrollOffset(): number {
|
||||
getConvertedScrollOffset(scrollLeft): number {
|
||||
if (this.timeLtr) {
|
||||
return -(this.blockchainContainer?.nativeElement?.scrollLeft || 0) - this.mempoolOffset;
|
||||
return -(scrollLeft || 0) - (this.mempoolOffset || 0);
|
||||
} else {
|
||||
return (this.blockchainContainer?.nativeElement?.scrollLeft || 0) - this.mempoolOffset;
|
||||
return (scrollLeft || 0) - (this.mempoolOffset || 0);
|
||||
}
|
||||
}
|
||||
|
||||
setScrollLeft(offset: number): void {
|
||||
if (this.timeLtr) {
|
||||
this.blockchainContainer.nativeElement.scrollLeft = offset - this.mempoolOffset;
|
||||
this.scrollLeft = offset - (this.mempoolOffset || 0);
|
||||
} else {
|
||||
this.blockchainContainer.nativeElement.scrollLeft = offset + this.mempoolOffset;
|
||||
this.scrollLeft = offset + (this.mempoolOffset || 0);
|
||||
}
|
||||
this.applyScrollLeft();
|
||||
}
|
||||
|
||||
addConvertedScrollOffset(offset: number): void {
|
||||
if (!this.blockchainContainer?.nativeElement) {
|
||||
this.pendingOffset = offset;
|
||||
return;
|
||||
}
|
||||
if (this.timeLtr) {
|
||||
this.blockchainContainer.nativeElement.scrollLeft -= offset;
|
||||
this.scrollLeft -= offset;
|
||||
} else {
|
||||
this.blockchainContainer.nativeElement.scrollLeft += offset;
|
||||
this.scrollLeft += offset;
|
||||
}
|
||||
this.applyScrollLeft();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.blockchainContainer?.nativeElement) {
|
||||
// clean up scroll position to prevent caching wrong scroll in Firefox
|
||||
this.setScrollLeft(0);
|
||||
}
|
||||
// clean up scroll position to prevent caching wrong scroll in Firefox
|
||||
this.setScrollLeft(0);
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
this.chainTipSubscription.unsubscribe();
|
||||
this.markBlockSubscription.unsubscribe();
|
||||
|
||||
@@ -69,6 +69,12 @@
|
||||
</button>
|
||||
<div class="dropdown-fees" ngbDropdownMenu aria-labelledby="dropdownFees">
|
||||
<ul>
|
||||
<li (click)="this.showCount = !this.showCount"
|
||||
[class]="this.showCount ? '' : 'inactive'">
|
||||
<span class="square" [ngStyle]="{'backgroundColor': 'white'}"></span>
|
||||
<span class="fee-text">{{ titleCount }}</span>
|
||||
</li>
|
||||
<hr style="margin: 4px;">
|
||||
<ng-template ngFor let-feeData let-i="index" [ngForOf]="feeLevelDropdownData">
|
||||
<ng-template [ngIf]="feeData.fee <= (feeLevels[maxFeeIndex])">
|
||||
<li (click)="filterFeeIndex = feeData.fee"
|
||||
@@ -92,8 +98,8 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="incoming-transactions-graph">
|
||||
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'"
|
||||
[limitFilterFee]="filterFeeIndex" [height]="500" [left]="65" [right]="10"
|
||||
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'" [hideCount]="!showCount"
|
||||
[limitFilterFee]="filterFeeIndex" [height]="500" [left]="65" [right]="showCount ? 50 : 10"
|
||||
[data]="mempoolStats && mempoolStats.length ? mempoolStats : null"></app-mempool-graph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ export class StatisticsComponent implements OnInit {
|
||||
chartColors = chartColors;
|
||||
filterSize = 100000;
|
||||
filterFeeIndex = 1;
|
||||
showCount = false;
|
||||
maxFeeIndex: number;
|
||||
dropDownOpen = false;
|
||||
|
||||
@@ -46,6 +47,7 @@ export class StatisticsComponent implements OnInit {
|
||||
inverted: boolean;
|
||||
feeLevelDropdownData = [];
|
||||
timespan = '';
|
||||
titleCount = $localize`Count`;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
|
||||
@@ -17,6 +17,6 @@ export class TrademarkPolicyComponent {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle('Trademark Policy');
|
||||
this.seoService.setDescription('An overview of the trademarks registered by Mempool Space K.K. and The Mempool Open Source Project™ and what we consider to be lawful usage of those trademarks.');
|
||||
this.seoService.setDescription('An overview of the trademarks registered by Mempool Space K.K. and The Mempool Open Source Project® and what we consider to be lawful usage of those trademarks.');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user