merge block-audit and block pages

This commit is contained in:
Mononaut 2022-11-23 19:07:17 +09:00
parent 276470474d
commit 5fc3b8b70c
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
16 changed files with 353 additions and 590 deletions

View File

@ -1,3 +1,4 @@
import blocks from '../api/blocks';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { BlockAudit, AuditScore } from '../mempool.interfaces'; import { BlockAudit, AuditScore } from '../mempool.interfaces';
@ -64,6 +65,12 @@ class BlocksAuditRepositories {
rows[0].freshTxs = JSON.parse(rows[0].freshTxs); rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
rows[0].transactions = JSON.parse(rows[0].transactions); rows[0].transactions = JSON.parse(rows[0].transactions);
rows[0].template = JSON.parse(rows[0].template); rows[0].template = JSON.parse(rows[0].template);
} else {
// fallback to non-audited transaction summary
const strippedTransactions = await blocks.$getStrippedBlockTransactions(hash);
return {
transactions: strippedTransactions
}
} }
return rows[0]; return rows[0];

View File

@ -4,7 +4,6 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
import { StartComponent } from './components/start/start.component'; import { StartComponent } from './components/start/start.component';
import { TransactionComponent } from './components/transaction/transaction.component'; import { TransactionComponent } from './components/transaction/transaction.component';
import { BlockComponent } from './components/block/block.component'; import { BlockComponent } from './components/block/block.component';
import { BlockAuditComponent } from './components/block-audit/block-audit.component';
import { AddressComponent } from './components/address/address.component'; import { AddressComponent } from './components/address/address.component';
import { MasterPageComponent } from './components/master-page/master-page.component'; import { MasterPageComponent } from './components/master-page/master-page.component';
import { AboutComponent } from './components/about/about.component'; import { AboutComponent } from './components/about/about.component';
@ -103,16 +102,6 @@ let routes: Routes = [
}, },
], ],
}, },
{
path: 'block-audit',
data: { networkSpecific: true },
children: [
{
path: ':id',
component: BlockAuditComponent,
},
],
},
{ {
path: 'docs', path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule), loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
@ -219,16 +208,6 @@ let routes: Routes = [
}, },
], ],
}, },
{
path: 'block-audit',
data: { networkSpecific: true },
children: [
{
path: ':id',
component: BlockAuditComponent,
},
],
},
{ {
path: 'docs', path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
@ -331,16 +310,6 @@ let routes: Routes = [
}, },
], ],
}, },
{
path: 'block-audit',
data: { networkSpecific: true },
children: [
{
path: ':id',
component: BlockAuditComponent
},
],
},
{ {
path: 'docs', path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)

View File

@ -1,172 +0,0 @@
<div class="container-xl" (window:resize)="onResize($event)">
<div class="title-block" id="block">
<h1>
<span class="next-previous-blocks">
<span i18n="shared.block-audit-title">Block Audit</span>
&nbsp;
<a *ngIf="blockAudit" [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockAudit.height }}</a>
&nbsp;
</span>
</h1>
<div class="grow"></div>
<button [routerLink]="['/block/' | relativeUrl, blockHash]" class="btn btn-sm">&#10005;</button>
</div>
<div *ngIf="!error && !isLoading">
<!-- OVERVIEW -->
<div class="box mb-3">
<div class="row">
<!-- LEFT COLUMN -->
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="block.hash">Hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, blockHash]" title="{{ blockHash }}">{{ blockHash | shortenString : 13 }}</a>
<app-clipboard class="d-none d-sm-inline-block" [text]="blockHash"></app-clipboard>
</td>
</tr>
<tr>
<td i18n="blockAudit.timestamp">Timestamp</td>
<td>
&lrm;{{ blockAudit.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i class="symbol">(<app-time-since [time]="blockAudit.timestamp" [fastRender]="true">
</app-time-since>)</i>
</div>
</td>
</tr>
<tr>
<td i18n="blockAudit.size">Size</td>
<td [innerHTML]="'&lrm;' + (blockAudit.size | bytes: 2)"></td>
</tr>
<tr>
<td i18n="block.weight">Weight</td>
<td [innerHTML]="'&lrm;' + (blockAudit.weight | wuBytes: 2)"></td>
</tr>
</tbody>
</table>
</div>
<!-- RIGHT COLUMN -->
<div class="col-sm" *ngIf="blockAudit">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
<td>{{ blockAudit.tx_count }}</td>
</tr>
<tr>
<td i18n="block.health">Block health</td>
<td>{{ blockAudit.matchRate }}%</td>
</tr>
<tr>
<td i18n="block.missing-txs">Removed txs</td>
<td>{{ blockAudit.missingTxs.length }}</td>
</tr>
<tr>
<td i18n="block.added-txs">Added txs</td>
<td>{{ blockAudit.addedTxs.length }}</td>
</tr>
</tbody>
</table>
</div>
</div> <!-- row -->
</div> <!-- box -->
<!-- ADDED vs MISSING button -->
<div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
fragment="projected" (click)="changeMode('projected')">Projected</a>
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
fragment="actual" (click)="changeMode('actual')">Actual</a>
</div>
</div>
<ng-template [ngIf]="!error && isLoading">
<!-- OVERVIEW -->
<div class="box mb-3">
<div class="row">
<!-- LEFT COLUMN -->
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
</tbody>
</table>
</div>
<!-- RIGHT COLUMN -->
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
</tbody>
</table>
</div>
</div> <!-- row -->
</div> <!-- box -->
<!-- ADDED vs MISSING button -->
<div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
fragment="projected" (click)="changeMode('projected')">Projected</a>
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
fragment="actual" (click)="changeMode('actual')">Actual</a>
</div>
</ng-template>
<ng-template [ngIf]="error">
<div *ngIf="error && error.status === 404; else generalError" class="text-center">
<br>
<b i18n="error.audit-unavailable">audit unavailable</b>
<br><br>
<i>{{ error.error }}</i>
<br>
<br>
</div>
<ng-template #generalError>
<div class="text-center">
<br>
<span i18n="error.general-loading-data">Error loading data.</span>
<br><br>
<i>{{ error }}</i>
<br>
<br>
</div>
</ng-template>
</ng-template>
<!-- VISUALIZATIONS -->
<div class="box" *ngIf="!error">
<div class="row">
<!-- MISSING TX RENDERING -->
<div class="col-sm" *ngIf="webGlEnabled">
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoading" [resolution]="75"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
</div>
<!-- ADDED TX RENDERING -->
<div class="col-sm" *ngIf="webGlEnabled && !isMobile">
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
<app-block-overview-graph #blockGraphActual [isLoading]="isLoading" [resolution]="75"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
</div>
</div> <!-- row -->
</div> <!-- box -->
</div>

View File

@ -1,44 +0,0 @@
.title-block {
border-top: none;
}
.table {
tr td {
&:last-child {
text-align: right;
@media (min-width: 768px) {
text-align: left;
}
}
}
}
.block-tx-title {
display: flex;
justify-content: space-between;
flex-direction: column;
position: relative;
@media (min-width: 550px) {
flex-direction: row;
}
h2 {
line-height: 1;
margin: 0;
position: relative;
padding-bottom: 10px;
@media (min-width: 550px) {
padding-bottom: 0px;
align-self: end;
}
}
}
.menu-button {
@media (min-width: 768px) {
max-width: 150px;
}
}
.block-subtitle {
text-align: center;
}

View File

@ -1,229 +0,0 @@
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription, combineLatest, of } from 'rxjs';
import { map, switchMap, startWith, catchError, filter } from 'rxjs/operators';
import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { StateService } from '../../services/state.service';
import { detectWebGL } from '../../shared/graphs.utils';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
@Component({
selector: 'app-block-audit',
templateUrl: './block-audit.component.html',
styleUrls: ['./block-audit.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
}
`],
})
export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
blockAudit: BlockAudit = undefined;
transactions: string[];
auditSubscription: Subscription;
urlFragmentSubscription: Subscription;
paginationMaxSize: number;
page = 1;
itemsPerPage: number;
mode: 'projected' | 'actual' = 'projected';
error: any;
isLoading = true;
webGlEnabled = true;
isMobile = window.innerWidth <= 767.98;
hoverTx: string;
childChangeSubscription: Subscription;
blockHash: string;
numMissing: number = 0;
numUnexpected: number = 0;
@ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
constructor(
private route: ActivatedRoute,
public stateService: StateService,
private router: Router,
private apiService: ApiService,
private electrsApiService: ElectrsApiService,
) {
this.webGlEnabled = detectWebGL();
}
ngOnDestroy() {
this.childChangeSubscription.unsubscribe();
this.urlFragmentSubscription.unsubscribe();
}
ngOnInit(): void {
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE;
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
if (fragment === 'actual') {
this.mode = 'actual';
} else {
this.mode = 'projected'
}
this.setupBlockGraphs();
});
this.auditSubscription = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
const blockHash = params.get('id') || null;
if (!blockHash) {
return null;
}
let isBlockHeight = false;
if (/^[0-9]+$/.test(blockHash)) {
isBlockHeight = true;
} else {
this.blockHash = blockHash;
}
if (isBlockHeight) {
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
.pipe(
switchMap((hash: string) => {
if (hash) {
this.blockHash = hash;
return this.apiService.getBlockAudit$(this.blockHash)
} else {
return null;
}
}),
catchError((err) => {
this.error = err;
return of(null);
}),
);
}
return this.apiService.getBlockAudit$(this.blockHash)
}),
filter((response) => response != null),
map((response) => {
const blockAudit = response.body;
const inTemplate = {};
const inBlock = {};
const isAdded = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
this.numMissing = 0;
this.numUnexpected = 0;
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
// set transaction statuses
for (const tx of blockAudit.template) {
if (isCensored[tx.txid]) {
tx.status = 'censored';
} else if (inBlock[tx.txid]) {
tx.status = 'found';
} else {
tx.status = 'missing';
isMissing[tx.txid] = true;
this.numMissing++;
}
}
for (const [index, tx] of blockAudit.transactions.entries()) {
if (index === 0) {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
return blockAudit;
}),
catchError((err) => {
console.log(err);
this.error = err;
this.isLoading = false;
return of(null);
}),
).subscribe((blockAudit) => {
this.blockAudit = blockAudit;
this.setupBlockGraphs();
this.isLoading = false;
});
}
ngAfterViewInit() {
this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
this.setupBlockGraphs();
})
}
setupBlockGraphs() {
if (this.blockAudit) {
this.blockGraphProjected.forEach(graph => {
graph.destroy();
if (this.isMobile && this.mode === 'actual') {
graph.setup(this.blockAudit.transactions);
} else {
graph.setup(this.blockAudit.template);
}
})
this.blockGraphActual.forEach(graph => {
graph.destroy();
graph.setup(this.blockAudit.transactions);
})
}
}
onResize(event: any) {
const isMobile = event.target.innerWidth <= 767.98;
const changed = isMobile !== this.isMobile;
this.isMobile = isMobile;
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
if (changed) {
this.changeMode(this.mode);
}
}
changeMode(mode: 'projected' | 'actual') {
this.router.navigate([], { fragment: mode });
}
onTxClick(event: TransactionStripped): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
this.router.navigate([url]);
}
onTxHover(txid: string): void {
if (txid && txid.length) {
this.hoverTx = txid;
} else {
this.hoverTx = null;
}
}
}

View File

@ -1,7 +1,8 @@
<div class="block-overview-graph"> <div class="block-overview-graph">
<canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas> <canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
<div class="loader-wrapper" [class.hidden]="!isLoading || disableSpinner"> <div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
<div class="spinner-border ml-3 loading" role="status"></div> <div *ngIf="isLoading" class="spinner-border ml-3 loading" role="status"></div>
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
</div> </div>
<app-block-overview-tooltip <app-block-overview-tooltip

View File

@ -19,6 +19,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() flip = true; @Input() flip = true;
@Input() disableSpinner = false; @Input() disableSpinner = false;
@Input() mirrorTxid: string | void; @Input() mirrorTxid: string | void;
@Input() unavailable: boolean = false;
@Output() txClickEvent = new EventEmitter<TransactionStripped>(); @Output() txClickEvent = new EventEmitter<TransactionStripped>();
@Output() txHoverEvent = new EventEmitter<string>(); @Output() txHoverEvent = new EventEmitter<string>();
@Output() readyEvent = new EventEmitter(); @Output() readyEvent = new EventEmitter();

View File

@ -3,12 +3,13 @@ import { FastVertexArray } from './fast-vertex-array';
import { TransactionStripped } from '../../interfaces/websocket.interface'; import { TransactionStripped } from '../../interfaces/websocket.interface';
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types'; import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
import { feeLevels, mempoolFeeColors } from '../../app.constants'; import { feeLevels, mempoolFeeColors } from '../../app.constants';
import BlockScene from './block-scene';
const hoverTransitionTime = 300; const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4'); const defaultHoverColor = hexToColor('1bd8f4');
const feeColors = mempoolFeeColors.map(hexToColor); const feeColors = mempoolFeeColors.map(hexToColor);
const auditFeeColors = feeColors.map((color) => desaturate(color, 0.3)); const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
const auditColors = { const auditColors = {
censored: hexToColor('f344df'), censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
@ -34,7 +35,8 @@ export default class TxView implements TransactionStripped {
vsize: number; vsize: number;
value: number; value: number;
feerate: number; feerate: number;
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
context?: 'projected' | 'actual';
initialised: boolean; initialised: boolean;
vertexArray: FastVertexArray; vertexArray: FastVertexArray;
@ -48,6 +50,7 @@ export default class TxView implements TransactionStripped {
dirty: boolean; dirty: boolean;
constructor(tx: TransactionStripped, vertexArray: FastVertexArray) { constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
this.context = tx.context;
this.txid = tx.txid; this.txid = tx.txid;
this.fee = tx.fee; this.fee = tx.fee;
this.vsize = tx.vsize; this.vsize = tx.vsize;
@ -159,12 +162,18 @@ export default class TxView implements TransactionStripped {
return auditColors.censored; return auditColors.censored;
case 'missing': case 'missing':
return auditColors.missing; return auditColors.missing;
case 'fresh':
return auditColors.missing;
case 'added': case 'added':
return auditColors.added; return auditColors.added;
case 'selected': case 'selected':
return auditColors.selected; return auditColors.selected;
case 'found': case 'found':
if (this.context === 'projected') {
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
} else {
return feeLevelColor;
}
default: default:
return feeLevelColor; return feeLevelColor;
} }

View File

@ -37,9 +37,10 @@
<ng-container [ngSwitch]="tx?.status"> <ng-container [ngSwitch]="tx?.status">
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td> <td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td> <td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
<td *ngSwitchCase="'missing'" i18n="transaction.audit.omitted">omitted</td> <td *ngSwitchCase="'missing'" i18n="transaction.audit.marginal">marginal fee rate</td>
<td *ngSwitchCase="'fresh'" i18n="transaction.audit.recently-broadcast">recently broadcast</td>
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td> <td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
<td *ngSwitchCase="'selected'" i18n="transaction.audit.extra">extra</td> <td *ngSwitchCase="'selected'" i18n="transaction.audit.marginal">marginal fee rate</td>
</ng-container> </ng-container>
</tr> </tr>
</tbody> </tbody>

View File

@ -54,7 +54,19 @@
<td i18n="block.weight">Weight</td> <td i18n="block.weight">Weight</td>
<td [innerHTML]="'&lrm;' + (block.weight | wuBytes: 2)"></td> <td [innerHTML]="'&lrm;' + (block.weight | wuBytes: 2)"></td>
</tr> </tr>
<ng-template [ngIf]="webGlEnabled"> <tr *ngIf="auditEnabled">
<td i18n="block.health">Block health</td>
<td>
<span *ngIf="blockAudit?.matchRate != null">{{ blockAudit.matchRate }}%</span>
<span *ngIf="blockAudit?.matchRate === null" i18n="unknown">Unknown</span>
</td>
</tr>
<ng-container *ngIf="!indexingAvailable && webGlEnabled">
<tr *ngIf="isMobile && auditEnabled"></tr>
<tr>
<td i18n="mempool-block.fee-span">Fee span</td>
<td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
</tr>
<tr *ngIf="block?.extras?.medianFee != undefined"> <tr *ngIf="block?.extras?.medianFee != undefined">
<td class="td-width" i18n="block.median-fee">Median fee</td> <td class="td-width" i18n="block.median-fee">Median fee</td>
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td> <td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
@ -98,26 +110,19 @@
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="block.miner">Miner</td> <td i18n="block.miner">Miner</td>
<td *ngIf="stateService.env.MINING_DASHBOARD"> <td *ngIf="stateService.env.MINING_DASHBOARD">
<a [attr.data-cy]="'block-details-miner-badge'" placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" <a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'"> [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block.extras.pool.name }} {{ block.extras.pool.name }}
</a> </a>
</td> </td>
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'"> <td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
<span [attr.data-cy]="'block-details-miner-badge'" placement="bottom" class="badge" <span placement="bottom" class="badge"
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'"> [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block.extras.pool.name }} {{ block.extras.pool.name }}
</span> </span>
</td> </td>
</tr> </tr>
<tr *ngIf="indexingAvailable"> </ng-container>
<td i18n="block.health">Block health</td>
<td>
<a *ngIf="block.extras?.matchRate != null" [routerLink]="['/block-audit/' | relativeUrl, blockHash]">{{ block.extras.matchRate }}%</a>
<span *ngIf="block.extras?.matchRate === null" i18n="unknown">Unknown</span>
</td>
</tr>
</ng-template>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -138,7 +143,11 @@
<tr> <tr>
<td colspan="2"><span class="skeleton-loader"></span></td> <td colspan="2"><span class="skeleton-loader"></span></td>
</tr> </tr>
<ng-template [ngIf]="webGlEnabled"> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<ng-container *ngIf="!indexingAvailable && webGlEnabled">
<tr *ngIf="isMobile && !auditEnabled"></tr>
<tr> <tr>
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td> <td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
</tr> </tr>
@ -148,17 +157,25 @@
<tr> <tr>
<td colspan="2"><span class="skeleton-loader"></span></td> <td colspan="2"><span class="skeleton-loader"></span></td>
</tr> </tr>
<tr> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td colspan="2"><span class="skeleton-loader"></span></td> <td colspan="2"><span class="skeleton-loader"></span></td>
</tr> </tr>
</ng-template> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</ng-container>
</tbody> </tbody>
</table> </table>
</div> </div>
</ng-template> </ng-template>
<div class="col-sm" *ngIf="!webGlEnabled"> <div class="col-sm">
<table class="table table-borderless table-striped" *ngIf="!isLoadingBlock"> <table class="table table-borderless table-striped" *ngIf="!isLoadingBlock && (indexingAvailable || !webGlEnabled)">
<tbody> <tbody>
<tr *ngIf="isMobile && auditEnabled"></tr>
<tr>
<td i18n="mempool-block.fee-span">Fee span</td>
<td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
</tr>
<tr *ngIf="block?.extras?.medianFee != undefined"> <tr *ngIf="block?.extras?.medianFee != undefined">
<td class="td-width" i18n="block.median-fee">Median fee</td> <td class="td-width" i18n="block.median-fee">Median fee</td>
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td> <td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
@ -216,8 +233,9 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="table table-borderless table-striped" *ngIf="isLoadingBlock"> <table class="table table-borderless table-striped" *ngIf="isLoadingBlock && (indexingAvailable || !webGlEnabled)">
<tbody> <tbody>
<tr *ngIf="isMobile && !auditEnabled"></tr>
<tr> <tr>
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td> <td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
</tr> </tr>
@ -230,12 +248,14 @@
<tr> <tr>
<td colspan="2"><span class="skeleton-loader"></span></td> <td colspan="2"><span class="skeleton-loader"></span></td>
</tr> </tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</tbody> </tbody>
</table> </table>
</div> <div class="col-sm chart-container" *ngIf="webGlEnabled && !indexingAvailable">
<div class="col-sm chart-container" *ngIf="webGlEnabled">
<app-block-overview-graph <app-block-overview-graph
#blockGraph #blockGraphActual
[isLoading]="isLoadingOverview" [isLoading]="isLoadingOverview"
[resolution]="75" [resolution]="75"
[blockLimit]="stateService.blockVSize" [blockLimit]="stateService.blockVSize"
@ -246,6 +266,36 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<span id="overview"></span>
<br>
<!-- VISUALIZATIONS -->
<div class="box" *ngIf="!error && webGlEnabled && indexingAvailable">
<div class="nav nav-tabs" *ngIf="isMobile && auditEnabled">
<a class="nav-link" [class.active]="mode === 'projected'" i18n="block.projected"
fragment="projected" (click)="changeMode('projected')">Projected</a>
<a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual"
fragment="actual" (click)="changeMode('actual')">Actual</a>
</div>
<div class="row">
<div class="col-sm">
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!auditEnabled"></app-block-overview-graph>
</div>
<div class="col-sm" *ngIf="!isMobile">
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
</div>
</div>
</div>
<ng-template [ngIf]="!isLoadingBlock && !error"> <ng-template [ngIf]="!isLoadingBlock && !error">
<div [hidden]="!showDetails" id="details"> <div [hidden]="!showDetails" id="details">
<br> <br>
@ -273,6 +323,7 @@
<div class="col-sm" *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> <div class="col-sm" *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<table class="table table-borderless table-striped"> <table class="table table-borderless table-striped">
<tbody> <tbody>
<tr *ngIf="isMobile"></tr>
<tr> <tr>
<td class="td-width" i18n="block.difficulty">Difficulty</td> <td class="td-width" i18n="block.difficulty">Difficulty</td>
<td>{{ block.difficulty }}</td> <td>{{ block.difficulty }}</td>

View File

@ -171,3 +171,35 @@ h1 {
margin: auto; margin: auto;
} }
} }
.menu-button {
@media (min-width: 768px) {
max-width: 150px;
}
}
.block-subtitle {
text-align: center;
}
.nav-tabs {
border-color: white;
border-width: 1px;
}
.nav-tabs .nav-link {
background: inherit;
border-width: 1px;
border-bottom: none;
border-color: transparent;
margin-bottom: -1px;
cursor: pointer;
&.active {
background: #24273e;
}
&.active, &:hover {
border-color: white;
}
}

View File

@ -1,15 +1,15 @@
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators'; import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
import { Transaction, Vout } from '../../interfaces/electrs.interface'; import { Transaction, Vout } from '../../interfaces/electrs.interface';
import { Observable, of, Subscription, asyncScheduler, EMPTY, Subject } from 'rxjs'; import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest } from 'rxjs';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '../../shared/graphs.utils'; import { detectWebGL } from '../../shared/graphs.utils';
@ -17,11 +17,20 @@ import { detectWebGL } from '../../shared/graphs.utils';
@Component({ @Component({
selector: 'app-block', selector: 'app-block',
templateUrl: './block.component.html', templateUrl: './block.component.html',
styleUrls: ['./block.component.scss'] styleUrls: ['./block.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
}
`],
}) })
export class BlockComponent implements OnInit, OnDestroy { export class BlockComponent implements OnInit, OnDestroy {
network = ''; network = '';
block: BlockExtended; block: BlockExtended;
blockAudit: BlockAudit = undefined;
blockHeight: number; blockHeight: number;
lastBlockHeight: number; lastBlockHeight: number;
nextBlockHeight: number; nextBlockHeight: number;
@ -48,9 +57,16 @@ export class BlockComponent implements OnInit, OnDestroy {
overviewError: any = null; overviewError: any = null;
webGlEnabled = true; webGlEnabled = true;
indexingAvailable = false; indexingAvailable = false;
auditEnabled = true;
isMobile = window.innerWidth <= 767.98;
hoverTx: string;
numMissing: number = 0;
numUnexpected: number = 0;
mode: 'projected' | 'actual' = 'projected';
transactionSubscription: Subscription; transactionSubscription: Subscription;
overviewSubscription: Subscription; overviewSubscription: Subscription;
auditSubscription: Subscription;
keyNavigationSubscription: Subscription; keyNavigationSubscription: Subscription;
blocksSubscription: Subscription; blocksSubscription: Subscription;
networkChangedSubscription: Subscription; networkChangedSubscription: Subscription;
@ -60,10 +76,10 @@ export class BlockComponent implements OnInit, OnDestroy {
nextBlockTxListSubscription: Subscription = undefined; nextBlockTxListSubscription: Subscription = undefined;
timeLtrSubscription: Subscription; timeLtrSubscription: Subscription;
timeLtr: boolean; timeLtr: boolean;
fetchAuditScore$ = new Subject<string>(); childChangeSubscription: Subscription;
fetchAuditScoreSubscription: Subscription;
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; @ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -89,8 +105,8 @@ export class BlockComponent implements OnInit, OnDestroy {
this.timeLtr = !!ltr; this.timeLtr = !!ltr;
}); });
this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' && this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true);
this.stateService.env.MINING_DASHBOARD === true); this.auditEnabled = this.indexingAvailable;
this.txsLoadingStatus$ = this.route.paramMap this.txsLoadingStatus$ = this.route.paramMap
.pipe( .pipe(
@ -107,30 +123,12 @@ export class BlockComponent implements OnInit, OnDestroy {
if (block.id === this.blockHash) { if (block.id === this.blockHash) {
this.block = block; this.block = block;
if (this.block.id && this.block?.extras?.matchRate == null) {
this.fetchAuditScore$.next(this.block.id);
}
if (block?.extras?.reward != undefined) { if (block?.extras?.reward != undefined) {
this.fees = block.extras.reward / 100000000 - this.blockSubsidy; this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
} }
} }
}); });
if (this.indexingAvailable) {
this.fetchAuditScoreSubscription = this.fetchAuditScore$
.pipe(
switchMap((hash) => this.apiService.getBlockAuditScore$(hash)),
catchError(() => EMPTY),
)
.subscribe((score) => {
if (score && score.hash === this.block.id) {
this.block.extras.matchRate = score.matchRate || null;
} else {
this.block.extras.matchRate = null;
}
});
}
const block$ = this.route.paramMap.pipe( const block$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
const blockHash: string = params.get('id') || ''; const blockHash: string = params.get('id') || '';
@ -212,7 +210,11 @@ export class BlockComponent implements OnInit, OnDestroy {
setTimeout(() => { setTimeout(() => {
this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe(); this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe();
this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe(); this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe();
if (this.indexingAvailable) {
this.apiService.getBlockAudit$(block.previousblockhash);
} else {
this.nextBlockSummarySubscription = this.apiService.getStrippedBlockTransactions$(block.previousblockhash).subscribe(); this.nextBlockSummarySubscription = this.apiService.getStrippedBlockTransactions$(block.previousblockhash).subscribe();
}
}, 100); }, 100);
} }
@ -229,9 +231,6 @@ export class BlockComponent implements OnInit, OnDestroy {
this.fees = block.extras.reward / 100000000 - this.blockSubsidy; this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
} }
this.stateService.markBlock$.next({ blockHeight: this.blockHeight }); this.stateService.markBlock$.next({ blockHeight: this.blockHeight });
if (this.block.id && this.block?.extras?.matchRate == null) {
this.fetchAuditScore$.next(this.block.id);
}
this.isLoadingTransactions = true; this.isLoadingTransactions = true;
this.transactions = null; this.transactions = null;
this.transactionsError = null; this.transactionsError = null;
@ -263,6 +262,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.isLoadingOverview = false; this.isLoadingOverview = false;
}); });
if (!this.indexingAvailable) {
this.overviewSubscription = block$.pipe( this.overviewSubscription = block$.pipe(
startWith(null), startWith(null),
pairwise(), pairwise(),
@ -285,18 +285,103 @@ export class BlockComponent implements OnInit, OnDestroy {
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => { .subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
this.strippedTransactions = transactions; this.strippedTransactions = transactions;
this.isLoadingOverview = false; this.isLoadingOverview = false;
if (this.blockGraph) { this.setupBlockGraphs();
this.blockGraph.destroy();
this.blockGraph.setup(this.strippedTransactions);
}
}, },
(error) => { (error) => {
this.error = error; this.error = error;
this.isLoadingOverview = false; this.isLoadingOverview = false;
if (this.blockGraph) {
this.blockGraph.destroy();
}
}); });
}
if (this.indexingAvailable) {
this.auditSubscription = block$.pipe(
startWith(null),
pairwise(),
switchMap(([prevBlock, block]) => this.apiService.getBlockAudit$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
return of([]);
})
)
),
filter((response) => response != null),
map((response) => {
const blockAudit = response.body;
const inTemplate = {};
const inBlock = {};
const isAdded = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
const isFresh = {};
this.numMissing = 0;
this.numUnexpected = 0;
if (blockAudit.template) {
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
for (const txid of blockAudit.freshTxs || []) {
isFresh[txid] = true;
}
// set transaction statuses
for (const tx of blockAudit.template) {
tx.context = 'projected';
if (isCensored[tx.txid]) {
tx.status = 'censored';
} else if (inBlock[tx.txid]) {
tx.status = 'found';
} else {
tx.status = isFresh[tx.txid] ? 'fresh' : 'missing';
isMissing[tx.txid] = true;
this.numMissing++;
}
}
for (const [index, tx] of blockAudit.transactions.entries()) {
tx.context = 'actual';
if (index === 0) {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
this.auditEnabled = true;
} else {
this.auditEnabled = false;
}
return blockAudit;
}),
catchError((err) => {
console.log(err);
this.error = err;
this.isLoadingOverview = false;
return of(null);
}),
).subscribe((blockAudit) => {
this.blockAudit = blockAudit;
this.setupBlockGraphs();
this.isLoadingOverview = false;
});
}
this.networkChangedSubscription = this.stateService.networkChanged$ this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network); .subscribe((network) => this.network = network);
@ -307,6 +392,12 @@ export class BlockComponent implements OnInit, OnDestroy {
} else { } else {
this.showDetails = false; this.showDetails = false;
} }
if (params.view === 'projected') {
this.mode = 'projected';
} else {
this.mode = 'actual';
}
this.setupBlockGraphs();
}); });
this.keyNavigationSubscription = this.stateService.keyNavigation$.subscribe((event) => { this.keyNavigationSubscription = this.stateService.keyNavigation$.subscribe((event) => {
@ -325,17 +416,24 @@ export class BlockComponent implements OnInit, OnDestroy {
}); });
} }
ngAfterViewInit(): void {
this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
this.setupBlockGraphs();
});
}
ngOnDestroy() { ngOnDestroy() {
this.stateService.markBlock$.next({}); this.stateService.markBlock$.next({});
this.transactionSubscription.unsubscribe(); this.transactionSubscription.unsubscribe();
this.overviewSubscription.unsubscribe(); this.overviewSubscription?.unsubscribe();
this.auditSubscription?.unsubscribe();
this.keyNavigationSubscription.unsubscribe(); this.keyNavigationSubscription.unsubscribe();
this.blocksSubscription.unsubscribe(); this.blocksSubscription.unsubscribe();
this.networkChangedSubscription.unsubscribe(); this.networkChangedSubscription.unsubscribe();
this.queryParamsSubscription.unsubscribe(); this.queryParamsSubscription.unsubscribe();
this.timeLtrSubscription.unsubscribe(); this.timeLtrSubscription.unsubscribe();
this.fetchAuditScoreSubscription?.unsubscribe();
this.unsubscribeNextBlockSubscriptions(); this.unsubscribeNextBlockSubscriptions();
this.childChangeSubscription.unsubscribe();
} }
unsubscribeNextBlockSubscriptions() { unsubscribeNextBlockSubscriptions() {
@ -382,7 +480,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.showDetails = false; this.showDetails = false;
this.router.navigate([], { this.router.navigate([], {
relativeTo: this.route, relativeTo: this.route,
queryParams: { showDetails: false }, queryParams: { showDetails: false, view: this.mode },
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
fragment: 'block' fragment: 'block'
}); });
@ -390,7 +488,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.showDetails = true; this.showDetails = true;
this.router.navigate([], { this.router.navigate([], {
relativeTo: this.route, relativeTo: this.route,
queryParams: { showDetails: true }, queryParams: { showDetails: true, view: this.mode },
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
fragment: 'details' fragment: 'details'
}); });
@ -409,10 +507,6 @@ export class BlockComponent implements OnInit, OnDestroy {
return this.block && this.block.height > 681393 && (new Date().getTime() / 1000) < 1628640000; return this.block && this.block.height > 681393 && (new Date().getTime() / 1000) < 1628640000;
} }
onResize(event: any) {
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
}
navigateToPreviousBlock() { navigateToPreviousBlock() {
if (!this.block) { if (!this.block) {
return; return;
@ -443,8 +537,53 @@ export class BlockComponent implements OnInit, OnDestroy {
} }
} }
setupBlockGraphs(): void {
if (this.blockAudit || this.strippedTransactions) {
this.blockGraphProjected.forEach(graph => {
graph.destroy();
if (this.isMobile && this.mode === 'actual') {
graph.setup(this.blockAudit?.transactions || this.strippedTransactions || []);
} else {
graph.setup(this.blockAudit?.template || []);
}
});
this.blockGraphActual.forEach(graph => {
graph.destroy();
graph.setup(this.blockAudit?.transactions || this.strippedTransactions || []);
});
}
}
onResize(event: any): void {
const isMobile = event.target.innerWidth <= 767.98;
const changed = isMobile !== this.isMobile;
this.isMobile = isMobile;
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
if (changed) {
this.changeMode(this.mode);
}
}
changeMode(mode: 'projected' | 'actual'): void {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { showDetails: this.showDetails, view: mode },
queryParamsHandling: 'merge',
fragment: 'overview'
});
}
onTxClick(event: TransactionStripped): void { onTxClick(event: TransactionStripped): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`); const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
this.router.navigate([url]); this.router.navigate([url]);
} }
onTxHover(txid: string): void {
if (txid && txid.length) {
this.hoverTx = txid;
} else {
this.hoverTx = null;
}
}
} }

View File

@ -46,7 +46,7 @@
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> <app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
</td> </td>
<td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block-audit/' | relativeUrl, block.id] : null"> <a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block/' | relativeUrl, block.id] : null">
<div class="progress progress-health"> <div class="progress progress-health">
<div class="progress-bar progress-bar-health" role="progressbar" <div class="progress-bar progress-bar-health" role="progressbar"
[ngStyle]="{'width': (100 - (auditScores[block.id] || 0)) + '%' }"></div> [ngStyle]="{'width': (100 - (auditScores[block.id] || 0)) + '%' }"></div>

View File

@ -141,7 +141,7 @@ export interface TransactionStripped {
fee: number; fee: number;
vsize: number; vsize: number;
value: number; value: number;
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
} }
export interface RewardStats { export interface RewardStats {

View File

@ -70,7 +70,8 @@ export interface TransactionStripped {
fee: number; fee: number;
vsize: number; vsize: number;
value: number; value: number;
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
context?: 'projected' | 'actual';
} }
export interface IBackendInfo { export interface IBackendInfo {

View File

@ -45,7 +45,6 @@ import { StartComponent } from '../components/start/start.component';
import { TransactionComponent } from '../components/transaction/transaction.component'; import { TransactionComponent } from '../components/transaction/transaction.component';
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component'; import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
import { BlockComponent } from '../components/block/block.component'; import { BlockComponent } from '../components/block/block.component';
import { BlockAuditComponent } from '../components/block-audit/block-audit.component';
import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component'; import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
import { AddressComponent } from '../components/address/address.component'; import { AddressComponent } from '../components/address/address.component';
@ -120,7 +119,6 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
StartComponent, StartComponent,
TransactionComponent, TransactionComponent,
BlockComponent, BlockComponent,
BlockAuditComponent,
BlockOverviewGraphComponent, BlockOverviewGraphComponent,
BlockOverviewTooltipComponent, BlockOverviewTooltipComponent,
TransactionsListComponent, TransactionsListComponent,
@ -223,7 +221,6 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
StartComponent, StartComponent,
TransactionComponent, TransactionComponent,
BlockComponent, BlockComponent,
BlockAuditComponent,
BlockOverviewGraphComponent, BlockOverviewGraphComponent,
BlockOverviewTooltipComponent, BlockOverviewTooltipComponent,
TransactionsListComponent, TransactionsListComponent,