Merge branch 'master' into nymkappa/tx-overflow

This commit is contained in:
softsimon
2023-11-10 12:47:52 +09:00
committed by GitHub
114 changed files with 1869 additions and 910 deletions

View File

@@ -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'"> &trade;</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'"> &reg;</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>

View File

@@ -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(

View File

@@ -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;

View File

@@ -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';

View File

@@ -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' },
]),

View File

@@ -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';

View File

@@ -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' },
]),

View File

@@ -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';

View File

@@ -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>

View File

@@ -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;
}
}

View 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();
}
}
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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();
}
}

View File

@@ -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}">
&lrm;{{ 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>

View File

@@ -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);
})
);

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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';

View File

@@ -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,

View File

@@ -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',

View File

@@ -0,0 +1,5 @@
<div class="block-wrapper">
<div class="block-container">
<app-mempool-block-overview [index]="index"></app-mempool-block-overview>
</div>
</div>

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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) {

View File

@@ -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],
};
}

View File

@@ -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';

View File

@@ -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' },

View File

@@ -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' },

View File

@@ -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®.');
}
}

View File

@@ -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)"

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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>

View File

@@ -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,

View File

@@ -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.');
}
}