Clean up block audit page & tweak color scheme

This commit is contained in:
Mononaut 2022-10-27 18:39:26 -06:00
parent d86f045150
commit b6343ddc2d
No known key found for this signature in database
GPG key ID: 61B952CAF4838F94
8 changed files with 222 additions and 75 deletions

View file

@ -238,6 +238,12 @@ class MiningRoutes {
public async $getBlockAudit(req: Request, res: Response) {
try {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) {
res.status(404).send(`This block has not been audited.`);
return;
}
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());

View file

@ -413,7 +413,7 @@ class WebsocketHandler {
let mBlocks: undefined | MempoolBlock[];
let mBlockDeltas: undefined | MempoolBlockDelta[];
let matchRate = 0;
let matchRate;
const _memPool = memPool.getMempool();
if (Common.indexingEnabled()) {

View file

@ -58,10 +58,12 @@ class BlocksAuditRepositories {
WHERE blocks_audits.hash = "${hash}"
`);
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].transactions = JSON.parse(rows[0].transactions);
rows[0].template = JSON.parse(rows[0].template);
if (rows.length) {
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].transactions = JSON.parse(rows[0].transactions);
rows[0].template = JSON.parse(rows[0].template);
}
return rows[0];
} catch (e: any) {

View file

@ -1,21 +1,22 @@
<div class="container-xl" (window:resize)="onResize($event)">
<div *ngIf="(auditObservable$ | async) as blockAudit; else skeleton">
<div class="title-block" id="block">
<h1>
<span class="next-previous-blocks">
<span i18n="shared.block-title">Block </span>
&nbsp;
<a [routerLink]="['/block/' | relativeUrl, blockAudit.id]">{{ blockAudit.height }}</a>
&nbsp;
<span i18n="shared.template-vs-mined">Template vs Mined</span>
</span>
</h1>
<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>
<div class="grow"></div>
<button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">&#10005;</button>
</div>
<button [routerLink]="['/block/' | relativeUrl, blockHash]" class="btn btn-sm">&#10005;</button>
</div>
<div *ngIf="!error && !isLoading">
<!-- OVERVIEW -->
<div class="box mb-3">
@ -26,8 +27,8 @@
<tbody>
<tr>
<td class="td-width" i18n="block.hash">Hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, blockAudit.id]" title="{{ blockAudit.id }}">{{ blockAudit.id | shortenString : 13 }}</a>
<app-clipboard class="d-none d-sm-inline-block" [text]="blockAudit.id"></app-clipboard>
<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>
@ -40,6 +41,10 @@
</div>
</td>
</tr>
<tr>
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
<td>{{ blockAudit.tx_count }}</td>
</tr>
<tr>
<td i18n="blockAudit.size">Size</td>
<td [innerHTML]="'&lrm;' + (blockAudit.size | bytes: 2)"></td>
@ -57,21 +62,25 @@
<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.match-rate">Match rate</td>
<td i18n="block.health">Block health</td>
<td>{{ blockAudit.matchRate }}%</td>
</tr>
<tr>
<td i18n="block.missing-txs">Missing txs</td>
<td i18n="block.missing-txs">Removed txs</td>
<td>{{ blockAudit.missingTxs.length }}</td>
</tr>
<tr>
<td i18n="block.missing-txs">Omitted txs</td>
<td>{{ numMissing }}</td>
</tr>
<tr>
<td i18n="block.added-txs">Added txs</td>
<td>{{ blockAudit.addedTxs.length }}</td>
</tr>
<tr>
<td i18n="block.missing-txs">Included txs</td>
<td>{{ numUnexpected }}</td>
</tr>
</tbody>
</table>
</div>
@ -79,33 +88,110 @@
</div> <!-- box -->
<!-- ADDED vs MISSING button -->
<div class="d-flex justify-content-center menu mt-3" *ngIf="isMobile">
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1 ml-1 menu-button" i18n="block.missing-txs"
fragment="missing" (click)="changeMode('missing')">Missing</a>
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1 ml-1 menu-button" i18n="block.added-txs"
fragment="added" (click)="changeMode('added')">Added</a>
<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">
<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]="['/' | relativeUrl]" class="btn btn-sm">&#10005;</button>
</div>
<!-- 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>
<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>
<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">
<div class="box" *ngIf="!error">
<div class="row">
<!-- MISSING TX RENDERING -->
<div class="col-sm" *ngIf="webGlEnabled">
<app-block-overview-graph #blockGraphTemplate [isLoading]="isLoading" [resolution]="75"
<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"
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
</div>
<!-- ADDED TX RENDERING -->
<div class="col-sm" *ngIf="webGlEnabled && !isMobile">
<app-block-overview-graph #blockGraphMined [isLoading]="isLoading" [resolution]="75"
<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"
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
</div>
</div> <!-- row -->
</div> <!-- box -->
<ng-template #skeleton></ng-template>
</div>

View file

@ -37,4 +37,8 @@
@media (min-width: 768px) {
max-width: 150px;
}
}
.block-subtitle {
text-align: center;
}

View file

@ -1,7 +1,7 @@
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Observable, Subscription, combineLatest } from 'rxjs';
import { map, share, switchMap, tap, startWith } from 'rxjs/operators';
import { map, switchMap, startWith, catchError } from 'rxjs/operators';
import { BlockAudit, TransactionStripped } from 'src/app/interfaces/node-api.interface';
import { ApiService } from 'src/app/services/api.service';
import { StateService } from 'src/app/services/state.service';
@ -25,21 +25,27 @@ import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overv
export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
blockAudit: BlockAudit = undefined;
transactions: string[];
auditObservable$: Observable<BlockAudit>;
auditSubscription: Subscription;
urlFragmentSubscription: Subscription;
paginationMaxSize: number;
page = 1;
itemsPerPage: number;
mode: 'missing' | 'added' = 'missing';
mode: 'projected' | 'actual' = 'projected';
error: any;
isLoading = true;
webGlEnabled = true;
isMobile = window.innerWidth <= 767.98;
childChangeSubscription: Subscription;
@ViewChildren('blockGraphTemplate') blockGraphTemplate: QueryList<BlockOverviewGraphComponent>;
@ViewChildren('blockGraphMined') blockGraphMined: QueryList<BlockOverviewGraphComponent>;
blockHash: string;
numMissing: number = 0;
numUnexpected: number = 0;
@ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
constructor(
private route: ActivatedRoute,
@ -50,18 +56,31 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
this.webGlEnabled = detectWebGL();
}
ngOnDestroy(): void {
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.auditObservable$ = this.route.paramMap.pipe(
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: string = params.get('id') || '';
return this.apiService.getBlockAudit$(blockHash)
this.blockHash = params.get('id') || null;
if (!this.blockHash) {
return null;
}
return this.apiService.getBlockAudit$(this.blockHash)
.pipe(
map((response) => {
const blockAudit = response.body;
@ -71,6 +90,8 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
const isCensored = {};
const isMissing = {};
const isSelected = {};
this.numMissing = 0;
this.numUnexpected = 0;
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
}
@ -92,6 +113,7 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
} else {
tx.status = 'missing';
isMissing[tx.txid] = true;
this.numMissing++;
}
}
for (const [index, tx] of blockAudit.transactions.entries()) {
@ -102,43 +124,46 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
return blockAudit;
}),
tap((blockAudit) => {
this.blockAudit = blockAudit;
this.changeMode(this.mode);
this.isLoading = false;
}),
})
);
}),
share()
);
catchError((err) => {
console.log(err);
this.error = err;
this.isLoading = false;
return null;
}),
).subscribe((blockAudit) => {
this.blockAudit = blockAudit;
this.setupBlockGraphs();
this.isLoading = false;
});
}
ngAfterViewInit() {
this.childChangeSubscription = combineLatest([this.blockGraphTemplate.changes.pipe(startWith(null)), this.blockGraphMined.changes.pipe(startWith(null))]).subscribe(() => {
console.log('changed!');
this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
this.setupBlockGraphs();
})
}
setupBlockGraphs() {
console.log('setting up block graphs')
if (this.blockAudit) {
this.blockGraphTemplate.forEach(graph => {
this.blockGraphProjected.forEach(graph => {
graph.destroy();
if (this.isMobile && this.mode === 'added') {
if (this.isMobile && this.mode === 'actual') {
graph.setup(this.blockAudit.transactions);
} else {
graph.setup(this.blockAudit.template);
}
})
this.blockGraphMined.forEach(graph => {
this.blockGraphActual.forEach(graph => {
graph.destroy();
graph.setup(this.blockAudit.transactions);
})
@ -156,18 +181,12 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
changeMode(mode: 'missing' | 'added') {
changeMode(mode: 'projected' | 'actual') {
this.router.navigate([], { fragment: mode });
this.mode = mode;
this.setupBlockGraphs();
}
onTxClick(event: TransactionStripped): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
this.router.navigate([url]);
}
pageChange(page: number, target: HTMLElement) {
}
}

View file

@ -7,6 +7,15 @@ import { feeLevels, mempoolFeeColors } from '../../app.constants';
const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4');
const feeColors = mempoolFeeColors.map(hexToColor);
const auditFeeColors = feeColors.map((color) => desaturate(color, 0.3));
const auditColors = {
censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('03E1E5'),
selected: darken(desaturate(hexToColor('039BE5'), 0.3), 0.7),
}
// convert from this class's update format to TxSprite's update format
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
return {
@ -143,17 +152,19 @@ export default class TxView implements TransactionStripped {
getColor(): Color {
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
const feeLevelColor = hexToColor(mempoolFeeColors[feeLevelIndex] || mempoolFeeColors[mempoolFeeColors.length - 1]);
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
// Block audit
switch(this.status) {
case 'censored':
return hexToColor('D81BC2');
return auditColors.censored;
case 'missing':
return hexToColor('8C1BD8');
return auditColors.missing;
case 'added':
return hexToColor('03E1E5');
return auditColors.added;
case 'selected':
return hexToColor('039BE5');
return auditColors.selected;
case 'found':
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
default:
return feeLevelColor;
}
@ -168,3 +179,22 @@ function hexToColor(hex: string): Color {
a: 1
};
}
function desaturate(color: Color, amount: number): Color {
const gray = (color.r + color.g + color.b) / 6;
return {
r: color.r + ((gray - color.r) * amount),
g: color.g + ((gray - color.g) * amount),
b: color.b + ((gray - color.b) * amount),
a: color.a,
};
}
function darken(color: Color, amount: number): Color {
return {
r: color.r * amount,
g: color.g * amount,
b: color.b * amount,
a: color.a,
}
}

View file

@ -36,10 +36,10 @@
<td class="td-width" i18n="transaction.audit-status">Audit status</td>
<ng-container [ngSwitch]="tx?.status">
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
<td *ngSwitchCase="'censored'" i18n="transaction.audit.censored">censored</td>
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
<td *ngSwitchCase="'missing'" i18n="transaction.audit.missing">missing</td>
<td *ngSwitchCase="'added'" i18n="transaction.audit.prioritized">prioritized</td>
<td *ngSwitchCase="'selected'" i18n="transaction.audit.unexpected">unexpected</td>
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
<td *ngSwitchCase="'selected'" i18n="transaction.audit.included">included</td>
</ng-container>
</tr>
</tbody>