Merge branch 'master' into simon/ignore-existing-mining-pools

This commit is contained in:
softsimon
2023-06-12 14:29:40 +02:00
committed by GitHub
66 changed files with 869 additions and 603 deletions

View File

@@ -15,11 +15,9 @@
</span>
<span class="grow"></span>
<div class="container-buttons">
<button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right">
<ng-container *ngTemplateOutlet="latestBlock.height - bisqTx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - bisqTx.blockHeight + 1}"></ng-container>
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
</button>
<div *ngIf="(latestBlock$ | async) as latestBlock">
<app-confirmations [chainTip]="latestBlock?.height" [height]="bisqTx.blockHeight" [hideUnconfirmed]="true" buttonClass="float-right"></app-confirmations>
</div>
</div>
</div>

View File

@@ -70,11 +70,7 @@
<div class="btn-container">
<span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
<button type="button" class="btn btn-sm btn-success mt-2">
<ng-container *ngTemplateOutlet="latestBlock.height - tx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.blockHeight + 1}"></ng-container>
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
</button>
<app-confirmations [chainTip]="latestBlock?.height" [height]="tx.blockHeight" [hideUnconfirmed]="true" buttonClass="mt-2"></app-confirmations>
&nbsp;
</span>
<button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">

View File

@@ -1,7 +1,7 @@
<div class="container-xl about-page">
<div class="intro">
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">&trade;</span>
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">&reg;</span>
<img class="logo" src="/resources/mempool-logo-bigger.png" />
<div class="version">
v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]
@@ -13,7 +13,7 @@
<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>
<video src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
<video #promoVideo (click)="unmutePromoVideo()" (touchstart)="unmutePromoVideo()" src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
<track label="English" kind="captions" srclang="en" src="/resources/promo-video/en.vtt" [attr.default]="showSubtitles('en') ? '' : null">
<track label="日本語" kind="captions" srclang="ja" src="/resources/promo-video/ja.vtt" [attr.default]="showSubtitles('ja') ? '' : null">
<track label="中文" kind="captions" srclang="zh" src="/resources/promo-video/zh.vtt" [attr.default]="showSubtitles('zh') ? '' : null">
@@ -119,6 +119,18 @@
</svg>
<span>Gemini</span>
</a>
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#a)" fill-rule="evenodd" clip-rule="evenodd" fill="#e21924">
<path d="M21.92 14.59a1.18 1.18 0 0 0-1.18-1.18h-1.82v2.36h1.82a1.18 1.18 0 0 0 1.18-1.18ZM21 17.07h-2v2.45h2a1.23 1.23 0 1 0 0-2.45Z"/>
<path d="M36.43 0 35 5.59l-8 2.64-2.43-3.61-4.74 2.05-4.74-2.05-2.43 3.61-8-2.64L3.21 0 0 7.86l7.89 5.86-5.56 4 5.56 1.12 2.69-.49v3.17l3.59 4.38.68 3.19 5 2.87 5-2.87.68-3.19 3.59-4.38v-3.17l2.7.49 5.56-1.12-5.56-4 7.89-5.86zM24.69 18.45a2.5 2.5 0 0 1-2.5 2.5h-1.11v1.56h-1.26V21h-.9v1.56h-1.27V21H15.3v-1.42h.64a.9.9 0 0 0 .9-.9V14.3a.901.901 0 0 0-.9-.91h-.64V12h2.35v-1.5h1.27V12h.9v-1.5h1.26V12h.68A2.269 2.269 0 0 1 24 14.31a2.25 2.25 0 0 1-.92 1.82 2.52 2.52 0 0 1 1.58 2.32z"/>
</g>
<defs>
<clipPath id="a"><path fill="#fff" d="M0 0h160v32H0z"/></clipPath>
</defs>
</svg>
<span>Bull Bitcoin</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
@@ -205,9 +217,9 @@
<img class="image" src="/resources/profile/nix-bitcoin.png" />
<span>NixOS</span>
</a>
<a href="https://github.com/Start9Labs/embassy-os" target="_blank" title="EmbassyOS">
<a href="https://github.com/Start9Labs/start-os" target="_blank" title="StartOS">
<img class="image" src="/resources/profile/start9.png" />
<span>EmbassyOS</span>
<span>StartOS</span>
</a>
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server">
<img class="image not-rounded" src="/resources/profile/btcpayserver.svg" />
@@ -384,7 +396,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&trade;, mempool.space&trade;, the mempool logo&trade;, the mempool.space logos&trade;, the mempool square logo&trade;, and the mempool blocks logo&trade; are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&trade;, mempool.space&trade;, the mempool logo&reg;, the mempool.space logos&trade;, the mempool square logo&reg;, and the mempool blocks logo&trade; are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from '../../services/seo.service';
import { StateService } from '../../services/state.service';
@@ -17,6 +17,7 @@ import { DOCUMENT } from '@angular/common';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AboutComponent implements OnInit {
@ViewChild('promoVideo') promoVideo: ElementRef;
backendInfo$: Observable<IBackendInfo>;
sponsors$: Observable<any>;
translators$: Observable<ITranslators>;
@@ -91,7 +92,11 @@ export class AboutComponent implements OnInit {
}
}
showSubtitles(language) {
showSubtitles(language): boolean {
return ( this.locale.startsWith( language ) && !this.locale.startsWith('en') );
}
unmutePromoVideo(): void {
this.promoVideo.nativeElement.muted = false;
}
}

View File

@@ -3,7 +3,7 @@
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<div class="d-flex d-md-block align-items-baseline">
<span i18n="mining.block-prediction-accuracy">Block Prediction Accuracy</span>
<span i18n="mining.blocks-health">Block Health</span>
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
@@ -12,34 +12,34 @@
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}">
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 24h
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 24h
</label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3D
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 3D
</label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1W
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 1W
</label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1M
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 1M
</label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3M
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 3M
</label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 6M
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 6M
</label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1Y
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 1Y
</label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 2Y
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 2Y
</label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3Y
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 3Y
</label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount > 157680" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> ALL
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> ALL
</label>
</div>
</form>

View File

@@ -13,9 +13,9 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-block-prediction-graph',
templateUrl: './block-prediction-graph.component.html',
styleUrls: ['./block-prediction-graph.component.scss'],
selector: 'app-block-health-graph',
templateUrl: './block-health-graph.component.html',
styleUrls: ['./block-health-graph.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
@@ -26,7 +26,7 @@ import { StateService } from '../../services/state.service';
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockPredictionGraphComponent implements OnInit {
export class BlockHealthGraphComponent implements OnInit {
@Input() right: number | string = 45;
@Input() left: number | string = 75;
@@ -60,7 +60,7 @@ export class BlockPredictionGraphComponent implements OnInit {
}
ngOnInit(): void {
this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Prediction Accuracy`);
this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Health`);
this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -80,7 +80,7 @@ export class BlockPredictionGraphComponent implements OnInit {
this.storageService.setValue('miningWindowPreference', timespan);
this.timespan = timespan;
this.isLoading = true;
return this.apiService.getHistoricalBlockPrediction$(timespan)
return this.apiService.getHistoricalBlocksHealth$(timespan)
.pipe(
tap((response) => {
this.prepareChartOptions(response.body);
@@ -163,7 +163,7 @@ export class BlockPredictionGraphComponent implements OnInit {
hideOverlap: true,
padding: [0, 5],
},
data: data.map(prediction => prediction[0])
data: data.map(health => health[0])
},
yAxis: data.length === 0 ? undefined : [
{
@@ -186,12 +186,12 @@ export class BlockPredictionGraphComponent implements OnInit {
series: data.length === 0 ? undefined : [
{
zlevel: 0,
name: $localize`Match rate`,
data: data.map(prediction => ({
value: prediction[2],
block: prediction[1],
name: $localize`Health`,
data: data.map(health => ({
value: health[2],
block: health[1],
itemStyle: {
color: this.getPredictionColor(prediction[2])
color: this.getHealthColor(health[2])
}
})),
type: 'bar',
@@ -257,7 +257,7 @@ export class BlockPredictionGraphComponent implements OnInit {
return 'rgb(' + gradient.red + ',' + gradient.green + ',' + gradient.blue + ')';
}
getPredictionColor(matchRate) {
getHealthColor(matchRate) {
return this.colorGradient(
Math.pow((100 - matchRate) / 100, 0.5),
{red: 67, green: 171, blue: 71},
@@ -294,7 +294,7 @@ export class BlockPredictionGraphComponent implements OnInit {
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `block-fees-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
}), `block-health-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none';

View File

@@ -37,7 +37,7 @@ export default class TxView implements TransactionStripped {
value: number;
feerate: number;
rate?: number;
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected';
context?: 'projected' | 'actual';
scene?: BlockScene;
@@ -171,6 +171,7 @@ export default class TxView implements TransactionStripped {
case 'censored':
return auditColors.censored;
case 'missing':
case 'sigop':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'fresh':
return auditColors.missing;

View File

@@ -44,6 +44,7 @@
<td *ngSwitchCase="'found'"><span class="badge badge-success" i18n="transaction.audit.match">Match</span></td>
<td *ngSwitchCase="'censored'"><span class="badge badge-danger" i18n="transaction.audit.removed">Removed</span></td>
<td *ngSwitchCase="'missing'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
<td *ngSwitchCase="'sigop'"><span class="badge badge-warning" i18n="transaction.audit.sigop">High sigop count</span></td>
<td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span></td>
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>

View File

@@ -335,6 +335,7 @@ export class BlockComponent implements OnInit, OnDestroy {
const isMissing = {};
const isSelected = {};
const isFresh = {};
const isSigop = {};
this.numMissing = 0;
this.numUnexpected = 0;
@@ -354,6 +355,9 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const txid of blockAudit.freshTxs || []) {
isFresh[txid] = true;
}
for (const txid of blockAudit.sigopTxs || []) {
isSigop[txid] = true;
}
// set transaction statuses
for (const tx of blockAudit.template) {
tx.context = 'projected';
@@ -362,7 +366,7 @@ export class BlockComponent implements OnInit, OnDestroy {
} else if (inBlock[tx.txid]) {
tx.status = 'found';
} else {
tx.status = isFresh[tx.txid] ? 'fresh' : 'missing';
tx.status = isFresh[tx.txid] ? 'fresh' : (isSigop[tx.txid] ? 'sigop' : 'missing');
isMissing[tx.txid] = true;
this.numMissing++;
}

View File

@@ -1,4 +1,4 @@
<div class="fee-estimation-wrapper" *ngIf="(isLoadingWebSocket$ | async) === false && (recommendedFees$ | async) as recommendedFees; else loadingFees">
<div class="fee-estimation-wrapper" *ngIf="(isLoading$ | async) === false && (recommendedFees$ | async) as recommendedFees; else loadingFees">
<div class="d-flex">
<div class="fee-progress-bar" [style.background]="noPriority">
<span class="fee-label" i18n="fees-box.no-priority" i18n-ngbTooltip="Transaction feerate tooltip (economy)" ngbTooltip="Either 2x the minimum, or the Low Priority rate (whichever is lower)" placement="top">No Priority</span>

View File

@@ -1,9 +1,9 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Observable } from 'rxjs';
import { Observable, combineLatest } from 'rxjs';
import { Recommendedfees } from '../../interfaces/websocket.interface';
import { feeLevels, mempoolFeeColors } from '../../app.constants';
import { tap } from 'rxjs/operators';
import { map, startWith, tap } from 'rxjs/operators';
@Component({
selector: 'app-fees-box',
@@ -12,7 +12,7 @@ import { tap } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeesBoxComponent implements OnInit {
isLoadingWebSocket$: Observable<boolean>;
isLoading$: Observable<boolean>;
recommendedFees$: Observable<Recommendedfees>;
gradient = 'linear-gradient(to right, #2e324e, #2e324e)';
noPriority = '#2e324e';
@@ -22,7 +22,12 @@ export class FeesBoxComponent implements OnInit {
) { }
ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.isLoading$ = combineLatest(
this.stateService.isLoadingWebSocket$.pipe(startWith(false)),
this.stateService.loadingIndicators$.pipe(startWith({ mempool: 0 })),
).pipe(map(([socket, indicators]) => {
return socket || (indicators.mempool != null && indicators.mempool !== 100);
}));
this.recommendedFees$ = this.stateService.recommendedFees$
.pipe(
tap((fees) => {

View File

@@ -22,7 +22,7 @@
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a>
<a *ngIf="stateService.env.AUDIT" class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" i18n="mining.block-prediction-accuracy">Block Prediction Accuracy</a>
[routerLink]="['/graphs/mining/block-health' | relativeUrl]" i18n="mining.block-health">Block Health</a>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<ng-container *ngIf="{ val: network$ | async } as network">
<header>
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]">
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
<ng-template [ngIf]="subdomain">
<div class="subdomain_container">
<img [src]="'/api/v1/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">

View File

@@ -53,4 +53,8 @@ export class MasterPageComponent implements OnInit {
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
brandClick(e): void {
this.stateService.resetScroll$.next(true);
}
}

View File

@@ -104,7 +104,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.mempoolEmptyBlocks.forEach((b) => {
this.mempoolEmptyBlockStyles.push(this.getStyleForMempoolEmptyBlock(b.index));
});
this.reduceMempoolBlocksToFitScreen(this.mempoolEmptyBlocks);
this.reduceEmptyBlocksToFitScreen(this.mempoolEmptyBlocks);
this.mempoolBlocks.map(() => {
this.updateMempoolBlockStyles();
@@ -244,12 +244,33 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
@HostListener('window:resize', ['$event'])
onResize(): void {
this.animateEntry = false;
this.reduceEmptyBlocksToFitScreen(this.mempoolEmptyBlocks);
}
trackByFn(index: number, block: MempoolBlock) {
return (block.isStack) ? `stack-${block.index}` : block.index;
}
reduceEmptyBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
while (blocks.length < blocksAmount) {
blocks.push({
blockSize: 0,
blockVSize: 0,
feeRange: [],
index: blocks.length,
medianFee: 0,
nTx: 0,
totalFees: 0
});
}
while (blocks.length > blocksAmount) {
blocks.pop();
}
return blocks;
}
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
let blocksAmount;

View File

@@ -24,6 +24,7 @@ import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/grap
export class MempoolGraphComponent implements OnInit, OnChanges {
@Input() data: any[];
@Input() filterSize = 100000;
@Input() limitFilterFee = 1;
@Input() height: number | string = 200;
@Input() top: number | string = 20;
@Input() right: number | string = 10;
@@ -40,7 +41,9 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
};
windowPreference: string;
hoverIndexSerie = 0;
maxFee: number;
feeLimitIndex: number;
maxFeeIndex: number;
feeLevelsOrdered = [];
chartColorsOrdered = chartColors;
inverted: boolean;
@@ -98,8 +101,9 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
}
generateArray(mempoolStats: OptimizedMempoolStats[]) {
let finalArray: number[][][] = [];
const finalArray: number[][][] = [];
let feesArray: number[][] = [];
let maxTier = 0;
for (let index = 37; index > -1; index--) {
feesArray = [];
@@ -111,7 +115,8 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
});
finalArray.push(feesArray);
}
this.feeLimitIndex = maxTier;
this.maxFeeIndex = maxTier;
finalArray.reverse();
return finalArray;
}
@@ -124,7 +129,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
const newColors = [];
for (let index = 0; index < series.length; index++) {
const value = series[index];
if (index < this.feeLimitIndex) {
if (index >= this.feeLimitIndex && index <= this.maxFeeIndex) {
newColors.push(this.chartColorsOrdered[index]);
seriesGraph.push({
zlevel: 0,
@@ -383,21 +388,26 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
orderLevels() {
this.feeLevelsOrdered = [];
let maxIndex = Math.min(feeLevels.length, this.feeLimitIndex);
for (let i = 0; i < maxIndex; i++) {
const maxIndex = Math.min(feeLevels.length, this.maxFeeIndex);
for (let i = 0; i < feeLevels.length; i++) {
if (feeLevels[i] === this.limitFilterFee) {
this.feeLimitIndex = i;
}
if (feeLevels[i] <= feeLevels[this.maxFeeIndex]) {
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
if (i === maxIndex - 1) {
if (i === maxIndex || feeLevels[i] == null) {
this.feeLevelsOrdered.push(`${(feeLevels[i] / 10).toFixed(1)}+`);
} else {
this.feeLevelsOrdered.push(`${(feeLevels[i] / 10).toFixed(1)} - ${(feeLevels[i + 1] / 10).toFixed(1)}`);
}
} else {
if (i === maxIndex - 1) {
if (i === maxIndex || feeLevels[i] == null) {
this.feeLevelsOrdered.push(`${feeLevels[i]}+`);
} else {
this.feeLevelsOrdered.push(`${feeLevels[i]} - ${feeLevels[i + 1]}`);
}
}
}
}
this.chartColorsOrdered = chartColors.slice(0, this.feeLevelsOrdered.length);
}

View File

@@ -25,6 +25,7 @@ export class StartComponent implements OnInit, OnDestroy {
markBlockSubscription: Subscription;
blockCounterSubscription: Subscription;
@ViewChild('blockchainContainer') blockchainContainer: ElementRef;
resetScrollSubscription: Subscription;
isMobile: boolean = false;
isiOS: boolean = false;
@@ -106,6 +107,12 @@ export class StartComponent implements OnInit, OnDestroy {
}, 60 * 60 * 1000);
}
});
this.resetScrollSubscription = this.stateService.resetScroll$.subscribe(reset => {
if (reset) {
this.resetScroll();
this.stateService.resetScroll$.next(false);
}
});
}
@HostListener('window:resize', ['$event'])
@@ -385,5 +392,6 @@ export class StartComponent implements OnInit, OnDestroy {
this.chainTipSubscription.unsubscribe();
this.markBlockSubscription.unsubscribe();
this.blockCounterSubscription.unsubscribe();
this.resetScrollSubscription.unsubscribe();
}
}

View File

@@ -62,7 +62,7 @@
<div class="dropdown-fees" ngbDropdownMenu aria-labelledby="dropdownFees">
<ul>
<ng-template ngFor let-feeData let-i="index" [ngForOf]="feeLevelDropdownData">
<ng-template [ngIf]="feeData.fee <= 400">
<ng-template [ngIf]="feeData.fee <= (feeLevels[maxFeeIndex])">
<li (click)="filterFeeIndex = feeData.fee"
[class]="filterFeeIndex > feeData.fee ? 'inactive' : ''">
<span class="square" [ngStyle]="{'backgroundColor': feeData.color}"></span>
@@ -84,7 +84,8 @@
</div>
<div class="card-body">
<div class="incoming-transactions-graph">
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'" [height]="500" [left]="65" [right]="10"
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'"
[limitFilterFee]="filterFeeIndex" [height]="500" [left]="65" [right]="10"
[data]="mempoolStats && mempoolStats.length ? mempoolStats : null"></app-mempool-graph>
</div>
</div>

View File

@@ -30,7 +30,9 @@ export class StatisticsComponent implements OnInit {
spinnerLoading = false;
feeLevels = feeLevels;
chartColors = chartColors;
filterSize = 100000;
filterFeeIndex = 1;
maxFeeIndex: number;
dropDownOpen = false;
mempoolStats: OptimizedMempoolStats[] = [];
@@ -134,6 +136,16 @@ export class StatisticsComponent implements OnInit {
mempoolStats.reverse();
const labels = mempoolStats.map(stats => stats.added);
let maxTier = 0;
for (let index = 37; index > -1; index--) {
mempoolStats.forEach((stats) => {
if (stats.vsizes[index] >= this.filterSize) {
maxTier = Math.max(maxTier, index);
}
});
}
this.maxFeeIndex = maxTier;
this.capExtremeVbytesValues();
this.mempoolTransactionsWeightPerSecondData = {
@@ -152,27 +164,42 @@ export class StatisticsComponent implements OnInit {
}
setFeeLevelDropdownData() {
let _feeLevels = feeLevels
let _feeLevels = feeLevels;
let _chartColors = chartColors;
if (!this.inverted) {
_feeLevels = [...feeLevels].reverse();
_chartColors = [...chartColors].reverse();
}
_feeLevels.forEach((fee, i) => {
let range;
const nextIndex = this.inverted ? i + 1 : i - 1;
if (this.stateService.isLiquid()) {
if (_feeLevels[nextIndex] == null) {
range = `${(_feeLevels[i] / 10).toFixed(1)}+`;
} else {
range = `${(_feeLevels[i] / 10).toFixed(1)} - ${(_feeLevels[nextIndex] / 10).toFixed(1)}`;
}
} else {
if (_feeLevels[nextIndex] == null) {
range = `${_feeLevels[i]}+`;
} else {
range = `${_feeLevels[i]} - ${_feeLevels[nextIndex]}`;
}
}
if (this.inverted) {
this.feeLevelDropdownData.push({
fee: fee,
range: this.stateService.isLiquid() ? `${(_feeLevels[i] / 10).toFixed(1)} - ${(_feeLevels[i + 1] / 10).toFixed(1)}` : `${_feeLevels[i]} - ${_feeLevels[i + 1]}`,
range,
color: _chartColors[i],
});
} else {
this.feeLevelDropdownData.push({
fee: fee,
range: this.stateService.isLiquid() ? `${(_feeLevels[i] / 10).toFixed(1)} - ${(_feeLevels[i - 1] / 10).toFixed(1)}` : `${_feeLevels[i]} - ${_feeLevels[i - 1]}`,
color: _chartColors[i - 1],
range,
color: _chartColors[i],
});
}
})
});
}
/**

View File

@@ -304,7 +304,7 @@
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
<p>“The Mempool Space K.K.&trade;, The Mempool Open Source Project&trade;, mempool.space&trade;, the mempool logo&trade;, the mempool.space logos&trade;, the mempool square logo&trade;, and the mempool blocks logo&trade; are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”</p>
<p>“The Mempool Space K.K.&trade;, The Mempool Open Source Project&trade;, mempool.space&trade;, the mempool logo&reg;, the mempool.space logos&trade;, the mempool square logo&reg;, and the mempool blocks logo&trade; are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”</p>
<li>What to Do When You See Abuse</li>

View File

@@ -18,19 +18,7 @@
</span>
<div class="container-buttons">
<ng-template [ngIf]="tx?.status?.confirmed">
<button *ngIf="latestBlock" type="button" class="btn btn-sm btn-success">
<ng-container *ngTemplateOutlet="latestBlock.height - tx.status.block_height + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.status.block_height + 1}"></ng-container>
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
</button>
</ng-template>
<ng-template [ngIf]="tx && !tx?.status?.confirmed && replaced">
<button type="button" class="btn btn-sm btn-danger" i18n="transaction.unconfirmed|Transaction unconfirmed state">Replaced</button>
</ng-template>
<ng-template [ngIf]="tx && !tx?.status?.confirmed && !replaced">
<button type="button" class="btn btn-sm btn-danger" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
</ng-template>
<app-confirmations [chainTip]="latestBlock?.height" [height]="tx?.status?.block_height" [replaced]="replaced"></app-confirmations>
</div>
</ng-container>
</div>
@@ -271,6 +259,10 @@
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td [innerHTML]="'&lrm;' + (tx.weight / 4 | vbytes: 2)"></td>
</tr>
<tr *ngIf="cpfpInfo && cpfpInfo.adjustedVsize && cpfpInfo.adjustedVsize > (tx.weight / 4)">
<td i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize</td>
<td [innerHTML]="'&lrm;' + (cpfpInfo.adjustedVsize | vbytes: 2)"></td>
</tr>
<tr>
<td i18n="block.weight">Weight</td>
<td [innerHTML]="'&lrm;' + (tx.weight | wuBytes: 2)"></td>
@@ -289,6 +281,10 @@
<td i18n="transaction.locktime">Locktime</td>
<td [innerHTML]="'&lrm;' + (tx.locktime | number)"></td>
</tr>
<tr *ngIf="cpfpInfo && cpfpInfo.adjustedVsize && cpfpInfo.adjustedVsize > (tx.weight / 4)">
<td i18n="transaction.sigops|Transaction Sigops">Sigops</td>
<td [innerHTML]="'&lrm;' + (cpfpInfo.sigops | number)"></td>
</tr>
<tr>
<td i18n="transaction.hex">Transaction hex</td>
<td><a target="_blank" href="{{ network === '' ? '' : '/' + network }}/api/tx/{{ txId }}/hex"><fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"></fa-icon></a></td>
@@ -477,11 +473,11 @@
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
<ng-template [ngIf]="tx?.status?.confirmed">
&nbsp;
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo?.descendants?.length && !cpfpInfo?.bestDescendant && !cpfpInfo?.ancestors?.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
<app-tx-fee-rating *ngIf="tx.fee && !hasEffectiveFeeRate" [tx]="tx"></app-tx-fee-rating>
</ng-template>
</td>
</tr>
<tr *ngIf="cpfpInfo && (cpfpInfo?.bestDescendant || cpfpInfo?.descendants?.length || cpfpInfo?.ancestors?.length)">
<tr *ngIf="cpfpInfo && hasEffectiveFeeRate">
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td>
<div class="effective-fee-container">
@@ -490,7 +486,7 @@
<app-tx-fee-rating class="ml-2 mr-2" *ngIf="tx.fee || tx.effectiveFeePerVsize" [tx]="tx"></app-tx-fee-rating>
</ng-template>
</div>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
<button *ngIf="cpfpInfo.bestDescendant || cpfpInfo.descendants?.length || cpfpInfo.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
</td>
</tr>
</tbody>

View File

@@ -86,6 +86,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
segwitEnabled: boolean;
rbfEnabled: boolean;
taprootEnabled: boolean;
hasEffectiveFeeRate: boolean;
@ViewChild('graphContainer')
graphContainer: ElementRef;
@@ -157,6 +158,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
.subscribe((cpfpInfo) => {
if (!cpfpInfo || !this.tx) {
this.cpfpInfo = null;
this.hasEffectiveFeeRate = false;
return;
}
// merge ancestors/descendants
@@ -164,16 +166,21 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
relatives.push(cpfpInfo.bestDescendant);
}
let totalWeight =
this.tx.weight +
relatives.reduce((prev, val) => prev + val.weight, 0);
let totalFees =
this.tx.fee +
relatives.reduce((prev, val) => prev + val.fee, 0);
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
const hasRelatives = !!relatives.length;
if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) {
let totalWeight =
this.tx.weight +
relatives.reduce((prev, val) => prev + val.weight, 0);
let totalFees =
this.tx.fee +
relatives.reduce((prev, val) => prev + val.fee, 0);
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
} else {
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
}
this.cpfpInfo = cpfpInfo;
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
});
this.fetchRbfSubscription = this.fetchRbfHistory$
@@ -359,6 +366,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant,
};
const hasRelatives = !!(tx.ancestors.length || tx.bestDescendant);
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
} else {
this.fetchCpfp$.next(this.tx.txid);
}
@@ -382,7 +391,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
this.latestBlock = block;
if (txConfirmed && this.tx && !this.tx.status.confirmed) {
if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
this.tx.status = {
confirmed: true,
block_height: block.height,
@@ -500,6 +509,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.replaced = false;
this.transactionTime = -1;
this.cpfpInfo = null;
this.hasEffectiveFeeRate = false;
this.rbfInfo = null;
this.rbfReplaces = [];
this.showCpfpDetails = false;

View File

@@ -298,14 +298,7 @@
<div class="float-right">
<ng-container *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
<button *ngIf="tx.status.confirmed; else unconfirmedButton" type="button" class="btn btn-sm btn-success mt-2">
<ng-container *ngTemplateOutlet="latestBlock.height - tx.status.block_height + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.status.block_height + 1}"></ng-container>
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
</button>
<ng-template #unconfirmedButton>
<button type="button" class="btn btn-sm btn-danger mt-2" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
</ng-template>
<app-confirmations [chainTip]="latestBlock?.height" [height]="tx?.status?.block_height" buttonClass="mt-2"></app-confirmations>
</ng-container>
<button *ngIf="address === ''; else viewingAddress" type="button" class="btn btn-sm btn-primary mt-2 ml-2" (click)="switchCurrency()">
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>

View File

@@ -26,7 +26,6 @@
<div class="mempool-graph">
<app-mempool-graph
[template]="'widget'"
[filterSize]="1000000"
[data]="mempoolStats.value?.mempool"
[windowPreferenceOverride]="'2h'"
></app-mempool-graph>

View File

@@ -21,7 +21,7 @@ import { DashboardComponent } from '../dashboard/dashboard.component';
import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component';
import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component';
import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component';
import { BlockPredictionGraphComponent } from '../components/block-prediction-graph/block-prediction-graph.component';
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
import { CommonModule } from '@angular/common';
@NgModule({
@@ -46,7 +46,7 @@ import { CommonModule } from '@angular/common';
LbtcPegsGraphComponent,
HashrateChartComponent,
HashrateChartPoolsComponent,
BlockPredictionGraphComponent,
BlockHealthGraphComponent,
],
imports: [
CommonModule,

View File

@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BlockPredictionGraphComponent } from '../components/block-prediction-graph/block-prediction-graph.component';
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
import { BlockFeeRatesGraphComponent } from '../components/block-fee-rates-graph/block-fee-rates-graph.component';
import { BlockFeesGraphComponent } from '../components/block-fees-graph/block-fees-graph.component';
import { BlockRewardsGraphComponent } from '../components/block-rewards-graph/block-rewards-graph.component';
@@ -143,9 +143,9 @@ const routes: Routes = [
redirectTo: 'mempool',
},
{
path: 'mining/block-prediction',
path: 'mining/block-health',
data: { networks: ['bitcoin'] },
component: BlockPredictionGraphComponent,
component: BlockHealthGraphComponent,
},
]
},

View File

@@ -24,6 +24,9 @@ export interface CpfpInfo {
ancestors: Ancestor[];
descendants?: Ancestor[];
bestDescendant?: BestDescendant | null;
effectiveFeePerVsize?: number;
sigops?: number;
adjustedVsize?: number;
}
export interface RbfInfo {
@@ -155,7 +158,7 @@ export interface TransactionStripped {
fee: number;
vsize: number;
value: number;
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected';
}
interface RbfTransaction extends TransactionStripped {

View File

@@ -6,7 +6,7 @@ export interface WebsocketResponse {
block?: BlockExtended;
blocks?: BlockExtended[];
conversions?: any;
txConfirmed?: boolean;
txConfirmed?: string;
historicalDate?: string;
mempoolInfo?: MempoolInfo;
vBytesPerSecond?: number;
@@ -76,7 +76,7 @@ export interface TransactionStripped {
vsize: number;
value: number;
rate?: number; // effective fee rate
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected';
context?: 'projected' | 'actual';
}

View File

@@ -253,20 +253,4 @@ export class NodeFeeChartComponent implements OnInit {
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
// @ts-ignore
this.chartOptions.grid.bottom = 40;
this.chartOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.chartOptions);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
}), `node-fee-chart.svg`);
// @ts-ignore
this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none';
this.chartInstance.setOption(this.chartOptions);
}
}

View File

@@ -252,21 +252,4 @@ export class NodeStatisticsChartComponent implements OnInit {
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();
// @ts-ignore
this.chartOptions.grid.bottom = 40;
this.chartOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.chartOptions);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
}), `block-sizes-weights-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none';
this.chartInstance.setOption(this.chartOptions);
}
}

View File

@@ -442,7 +442,7 @@ export class NodesNetworksChartComponent implements OnInit {
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `block-sizes-weights-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
}), `lightning-nodes-per-network-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none';

View File

@@ -238,7 +238,7 @@ export class ApiService {
);
}
getHistoricalBlockPrediction$(interval: string | undefined) : Observable<any> {
getHistoricalBlocksHealth$(interval: string | undefined) : Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/predictions` +
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }

View File

@@ -92,7 +92,7 @@ export class StateService {
networkChanged$ = new ReplaySubject<string>(1);
lightningChanged$ = new ReplaySubject<boolean>(1);
blocks$: ReplaySubject<[BlockExtended, boolean]>;
blocks$: ReplaySubject<[BlockExtended, string]>;
transactions$ = new ReplaySubject<TransactionStripped>(6);
conversions$ = new ReplaySubject<any>(1);
bsqPrice$ = new ReplaySubject<number>(1);
@@ -126,6 +126,7 @@ export class StateService {
keyNavigation$ = new Subject<KeyboardEvent>();
blockScrolling$: Subject<boolean> = new Subject<boolean>();
resetScroll$: Subject<boolean> = new Subject<boolean>();
timeLtr: BehaviorSubject<boolean>;
hideFlow: BehaviorSubject<boolean>;
hideAudit: BehaviorSubject<boolean>;
@@ -163,7 +164,7 @@ export class StateService {
}
});
this.blocks$ = new ReplaySubject<[BlockExtended, boolean]>(this.env.KEEP_BLOCKS_AMOUNT);
this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT);
if (this.env.BASE_MODULE === 'bisq') {
this.network = this.env.BASE_MODULE;

View File

@@ -241,7 +241,7 @@ export class WebsocketService {
blocks.forEach((block: BlockExtended) => {
if (block.height > this.stateService.latestBlockHeight) {
maxHeight = Math.max(maxHeight, block.height);
this.stateService.blocks$.next([block, false]);
this.stateService.blocks$.next([block, '']);
}
});
this.stateService.updateChainTip(maxHeight);
@@ -258,7 +258,7 @@ export class WebsocketService {
if (response.block) {
if (response.block.height > this.stateService.latestBlockHeight) {
this.stateService.updateChainTip(response.block.height);
this.stateService.blocks$.next([response.block, !!response.txConfirmed]);
this.stateService.blocks$.next([response.block, response.txConfirmed || '']);
}
if (response.txConfirmed) {

View File

@@ -0,0 +1,13 @@
<ng-template [ngIf]="confirmations">
<button type="button" class="btn btn-sm btn-success {{buttonClass}}">
<ng-container *ngTemplateOutlet="confirmations == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: confirmations}"></ng-container>
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
</button>
</ng-template>
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
<button type="button" class="btn btn-sm btn-danger {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Replaced</button>
</ng-template>
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced">
<button type="button" class="btn btn-sm btn-danger {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
</ng-template>

View File

@@ -0,0 +1,26 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
@Component({
selector: 'app-confirmations',
templateUrl: './confirmations.component.html',
styleUrls: ['./confirmations.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfirmationsComponent implements OnChanges {
@Input() chainTip: number;
@Input() height: number;
@Input() replaced: boolean = false;
@Input() hideUnconfirmed: boolean = false;
@Input() buttonClass: string = '';
confirmations: number = 0;
ngOnChanges(): void {
if (this.chainTip != null && this.height != null) {
this.confirmations = Math.max(1, this.chainTip - this.height + 1);
} else {
this.confirmations = 0;
}
}
}

View File

@@ -85,6 +85,7 @@ import { SatsComponent } from './components/sats/sats.component';
import { TruncateComponent } from './components/truncate/truncate.component';
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
import { TimestampComponent } from './components/timestamp/timestamp.component';
import { ConfirmationsComponent } from './components/confirmations/confirmations.component';
import { ToggleComponent } from './components/toggle/toggle.component';
import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component';
import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component';
@@ -175,6 +176,7 @@ import { ClockMempoolComponent } from '../components/clock/clock-mempool.compone
TruncateComponent,
SearchResultsComponent,
TimestampComponent,
ConfirmationsComponent,
ToggleComponent,
GeolocationComponent,
TestnetAlertComponent,
@@ -289,6 +291,7 @@ import { ClockMempoolComponent } from '../components/clock/clock-mempool.compone
TruncateComponent,
SearchResultsComponent,
TimestampComponent,
ConfirmationsComponent,
ToggleComponent,
GeolocationComponent,
PreviewTitleComponent,