Merge pull request #2662 from mononaut/block-audit-feature
Block audit feature
This commit is contained in:
commit
057456504c
@ -20,6 +20,7 @@ import indexer from '../indexer';
|
|||||||
import fiatConversion from './fiat-conversion';
|
import fiatConversion from './fiat-conversion';
|
||||||
import poolsParser from './pools-parser';
|
import poolsParser from './pools-parser';
|
||||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
|
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
import mining from './mining/mining';
|
import mining from './mining/mining';
|
||||||
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
||||||
import PricesRepository from '../repositories/PricesRepository';
|
import PricesRepository from '../repositories/PricesRepository';
|
||||||
@ -186,14 +187,18 @@ class Blocks {
|
|||||||
if (!pool) { // We should never have this situation in practise
|
if (!pool) { // We should never have this situation in practise
|
||||||
logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. ` +
|
logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. ` +
|
||||||
`Check your "pools" table entries`);
|
`Check your "pools" table entries`);
|
||||||
return blockExtended;
|
} else {
|
||||||
|
blockExtended.extras.pool = {
|
||||||
|
id: pool.id,
|
||||||
|
name: pool.name,
|
||||||
|
slug: pool.slug,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
blockExtended.extras.pool = {
|
const auditSummary = await BlocksAuditsRepository.$getShortBlockAudit(block.id);
|
||||||
id: pool.id,
|
if (auditSummary) {
|
||||||
name: pool.name,
|
blockExtended.extras.matchRate = auditSummary.matchRate;
|
||||||
slug: pool.slug,
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return blockExtended;
|
return blockExtended;
|
||||||
|
@ -238,6 +238,12 @@ class MiningRoutes {
|
|||||||
public async $getBlockAudit(req: Request, res: Response) {
|
public async $getBlockAudit(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
|
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('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||||
|
@ -413,7 +413,7 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
let mBlocks: undefined | MempoolBlock[];
|
let mBlocks: undefined | MempoolBlock[];
|
||||||
let mBlockDeltas: undefined | MempoolBlockDelta[];
|
let mBlockDeltas: undefined | MempoolBlockDelta[];
|
||||||
let matchRate = 0;
|
let matchRate;
|
||||||
const _memPool = memPool.getMempool();
|
const _memPool = memPool.getMempool();
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
|
@ -58,10 +58,12 @@ class BlocksAuditRepositories {
|
|||||||
WHERE blocks_audits.hash = "${hash}"
|
WHERE blocks_audits.hash = "${hash}"
|
||||||
`);
|
`);
|
||||||
|
|
||||||
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
if (rows.length) {
|
||||||
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
||||||
rows[0].transactions = JSON.parse(rows[0].transactions);
|
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
||||||
rows[0].template = JSON.parse(rows[0].template);
|
rows[0].transactions = JSON.parse(rows[0].transactions);
|
||||||
|
rows[0].template = JSON.parse(rows[0].template);
|
||||||
|
}
|
||||||
|
|
||||||
return rows[0];
|
return rows[0];
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -69,6 +71,20 @@ class BlocksAuditRepositories {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getShortBlockAudit(hash: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const [rows]: any[] = await DB.query(
|
||||||
|
`SELECT hash as id, match_rate as matchRate
|
||||||
|
FROM blocks_audits
|
||||||
|
WHERE blocks_audits.hash = "${hash}"
|
||||||
|
`);
|
||||||
|
return rows[0];
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new BlocksAuditRepositories();
|
export default new BlocksAuditRepositories();
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
<div class="container-xl" (window:resize)="onResize($event)">
|
<div class="container-xl" (window:resize)="onResize($event)">
|
||||||
|
|
||||||
<div *ngIf="(auditObservable$ | async) as blockAudit; else skeleton">
|
<div class="title-block" id="block">
|
||||||
<div class="title-block" id="block">
|
<h1>
|
||||||
<h1>
|
<span class="next-previous-blocks">
|
||||||
<span class="next-previous-blocks">
|
<span i18n="shared.block-audit-title">Block Audit</span>
|
||||||
<span i18n="shared.block-title">Block </span>
|
|
||||||
|
<a *ngIf="blockAudit" [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockAudit.height }}</a>
|
||||||
<a [routerLink]="['/block/' | relativeUrl, blockAudit.id]">{{ blockAudit.height }}</a>
|
|
||||||
|
</span>
|
||||||
<span i18n="shared.template-vs-mined">Template vs Mined</span>
|
</h1>
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
|
|
||||||
<button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">✕</button>
|
<button [routerLink]="['/block/' | relativeUrl, blockHash]" class="btn btn-sm">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!error && !isLoading">
|
||||||
|
|
||||||
|
|
||||||
<!-- OVERVIEW -->
|
<!-- OVERVIEW -->
|
||||||
<div class="box mb-3">
|
<div class="box mb-3">
|
||||||
@ -26,8 +27,8 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="block.hash">Hash</td>
|
<td class="td-width" i18n="block.hash">Hash</td>
|
||||||
<td><a [routerLink]="['/block/' | relativeUrl, blockAudit.id]" title="{{ blockAudit.id }}">{{ blockAudit.id | shortenString : 13 }}</a>
|
<td><a [routerLink]="['/block/' | relativeUrl, blockHash]" title="{{ blockHash }}">{{ blockHash | shortenString : 13 }}</a>
|
||||||
<app-clipboard class="d-none d-sm-inline-block" [text]="blockAudit.id"></app-clipboard>
|
<app-clipboard class="d-none d-sm-inline-block" [text]="blockHash"></app-clipboard>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@ -40,6 +41,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
|
||||||
|
<td>{{ blockAudit.tx_count }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="blockAudit.size">Size</td>
|
<td i18n="blockAudit.size">Size</td>
|
||||||
<td [innerHTML]="'‎' + (blockAudit.size | bytes: 2)"></td>
|
<td [innerHTML]="'‎' + (blockAudit.size | bytes: 2)"></td>
|
||||||
@ -57,21 +62,25 @@
|
|||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
|
<td i18n="block.health">Block health</td>
|
||||||
<td>{{ blockAudit.tx_count }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.match-rate">Match rate</td>
|
|
||||||
<td>{{ blockAudit.matchRate }}%</td>
|
<td>{{ blockAudit.matchRate }}%</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="block.missing-txs">Missing txs</td>
|
<td i18n="block.missing-txs">Removed txs</td>
|
||||||
<td>{{ blockAudit.missingTxs.length }}</td>
|
<td>{{ blockAudit.missingTxs.length }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="block.missing-txs">Omitted txs</td>
|
||||||
|
<td>{{ numMissing }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="block.added-txs">Added txs</td>
|
<td i18n="block.added-txs">Added txs</td>
|
||||||
<td>{{ blockAudit.addedTxs.length }}</td>
|
<td>{{ blockAudit.addedTxs.length }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="block.missing-txs">Included txs</td>
|
||||||
|
<td>{{ numUnexpected }}</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -79,33 +88,110 @@
|
|||||||
</div> <!-- box -->
|
</div> <!-- box -->
|
||||||
|
|
||||||
<!-- ADDED vs MISSING button -->
|
<!-- ADDED vs MISSING button -->
|
||||||
<div class="d-flex justify-content-center menu mt-3" *ngIf="isMobile">
|
<div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
|
||||||
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1 ml-1 menu-button" i18n="block.missing-txs"
|
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
|
||||||
fragment="missing" (click)="changeMode('missing')">Missing</a>
|
fragment="projected" (click)="changeMode('projected')">Projected</a>
|
||||||
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1 ml-1 menu-button" i18n="block.added-txs"
|
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
|
||||||
fragment="added" (click)="changeMode('added')">Added</a>
|
fragment="actual" (click)="changeMode('actual')">Actual</a>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
|
<a *ngIf="blockAudit" [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockAudit.height }}</a>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="grow"></div>
|
||||||
|
|
||||||
|
<button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">✕</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 -->
|
<!-- VISUALIZATIONS -->
|
||||||
<div class="box">
|
<div class="box" *ngIf="!error">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- MISSING TX RENDERING -->
|
<!-- MISSING TX RENDERING -->
|
||||||
<div class="col-sm" *ngIf="webGlEnabled">
|
<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"
|
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false"
|
||||||
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
|
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ADDED TX RENDERING -->
|
<!-- ADDED TX RENDERING -->
|
||||||
<div class="col-sm" *ngIf="webGlEnabled && !isMobile">
|
<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"
|
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false"
|
||||||
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
|
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- row -->
|
</div> <!-- row -->
|
||||||
</div> <!-- box -->
|
</div> <!-- box -->
|
||||||
|
|
||||||
<ng-template #skeleton></ng-template>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
@ -37,4 +37,8 @@
|
|||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-subtitle {
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
@ -1,12 +1,12 @@
|
|||||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
|
||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subscription, combineLatest } from 'rxjs';
|
||||||
import { map, share, switchMap, tap } from 'rxjs/operators';
|
import { map, switchMap, startWith, catchError } from 'rxjs/operators';
|
||||||
import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
|
import { BlockAudit, TransactionStripped } from 'src/app/interfaces/node-api.interface';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from 'src/app/services/state.service';
|
||||||
import { detectWebGL } from '../../shared/graphs.utils';
|
import { detectWebGL } from 'src/app/shared/graphs.utils';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
|
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -22,22 +22,30 @@ import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overv
|
|||||||
}
|
}
|
||||||
`],
|
`],
|
||||||
})
|
})
|
||||||
export class BlockAuditComponent implements OnInit, OnDestroy {
|
export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
blockAudit: BlockAudit = undefined;
|
blockAudit: BlockAudit = undefined;
|
||||||
transactions: string[];
|
transactions: string[];
|
||||||
auditObservable$: Observable<BlockAudit>;
|
auditSubscription: Subscription;
|
||||||
|
urlFragmentSubscription: Subscription;
|
||||||
|
|
||||||
paginationMaxSize: number;
|
paginationMaxSize: number;
|
||||||
page = 1;
|
page = 1;
|
||||||
itemsPerPage: number;
|
itemsPerPage: number;
|
||||||
|
|
||||||
mode: 'missing' | 'added' = 'missing';
|
mode: 'projected' | 'actual' = 'projected';
|
||||||
|
error: any;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
webGlEnabled = true;
|
webGlEnabled = true;
|
||||||
isMobile = window.innerWidth <= 767.98;
|
isMobile = window.innerWidth <= 767.98;
|
||||||
|
|
||||||
@ViewChild('blockGraphTemplate') blockGraphTemplate: BlockOverviewGraphComponent;
|
childChangeSubscription: Subscription;
|
||||||
@ViewChild('blockGraphMined') blockGraphMined: BlockOverviewGraphComponent;
|
|
||||||
|
blockHash: string;
|
||||||
|
numMissing: number = 0;
|
||||||
|
numUnexpected: number = 0;
|
||||||
|
|
||||||
|
@ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
|
||||||
|
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@ -48,73 +56,137 @@ export class BlockAuditComponent implements OnInit, OnDestroy {
|
|||||||
this.webGlEnabled = detectWebGL();
|
this.webGlEnabled = detectWebGL();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy() {
|
||||||
|
this.childChangeSubscription.unsubscribe();
|
||||||
|
this.urlFragmentSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
|
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
|
||||||
this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE;
|
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) => {
|
switchMap((params: ParamMap) => {
|
||||||
const blockHash: string = params.get('id') || '';
|
this.blockHash = params.get('id') || null;
|
||||||
return this.apiService.getBlockAudit$(blockHash)
|
if (!this.blockHash) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.apiService.getBlockAudit$(this.blockHash)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((response) => {
|
map((response) => {
|
||||||
const blockAudit = response.body;
|
const blockAudit = response.body;
|
||||||
for (let i = 0; i < blockAudit.template.length; ++i) {
|
const inTemplate = {};
|
||||||
if (blockAudit.missingTxs.includes(blockAudit.template[i].txid)) {
|
const inBlock = {};
|
||||||
blockAudit.template[i].status = 'missing';
|
const isAdded = {};
|
||||||
} else if (blockAudit.addedTxs.includes(blockAudit.template[i].txid)) {
|
const isCensored = {};
|
||||||
blockAudit.template[i].status = 'added';
|
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 {
|
} else {
|
||||||
blockAudit.template[i].status = 'found';
|
tx.status = 'missing';
|
||||||
|
isMissing[tx.txid] = true;
|
||||||
|
this.numMissing++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let i = 0; i < blockAudit.transactions.length; ++i) {
|
for (const [index, tx] of blockAudit.transactions.entries()) {
|
||||||
if (blockAudit.missingTxs.includes(blockAudit.transactions[i].txid)) {
|
if (isAdded[tx.txid]) {
|
||||||
blockAudit.transactions[i].status = 'missing';
|
tx.status = 'added';
|
||||||
} else if (blockAudit.addedTxs.includes(blockAudit.transactions[i].txid)) {
|
} else if (index === 0 || inTemplate[tx.txid]) {
|
||||||
blockAudit.transactions[i].status = 'added';
|
tx.status = 'found';
|
||||||
} else {
|
} else {
|
||||||
blockAudit.transactions[i].status = 'found';
|
tx.status = 'selected';
|
||||||
|
isSelected[tx.txid] = true;
|
||||||
|
this.numUnexpected++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const tx of blockAudit.transactions) {
|
||||||
|
inBlock[tx.txid] = true;
|
||||||
|
}
|
||||||
return blockAudit;
|
return blockAudit;
|
||||||
}),
|
})
|
||||||
tap((blockAudit) => {
|
|
||||||
this.changeMode(this.mode);
|
|
||||||
if (this.blockGraphTemplate) {
|
|
||||||
this.blockGraphTemplate.destroy();
|
|
||||||
this.blockGraphTemplate.setup(blockAudit.template);
|
|
||||||
}
|
|
||||||
if (this.blockGraphMined) {
|
|
||||||
this.blockGraphMined.destroy();
|
|
||||||
this.blockGraphMined.setup(blockAudit.transactions);
|
|
||||||
}
|
|
||||||
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.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) {
|
onResize(event: any) {
|
||||||
this.isMobile = event.target.innerWidth <= 767.98;
|
const isMobile = event.target.innerWidth <= 767.98;
|
||||||
|
const changed = isMobile !== this.isMobile;
|
||||||
|
this.isMobile = isMobile;
|
||||||
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.changeMode(this.mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changeMode(mode: 'missing' | 'added') {
|
changeMode(mode: 'projected' | 'actual') {
|
||||||
this.router.navigate([], { fragment: mode });
|
this.router.navigate([], { fragment: mode });
|
||||||
this.mode = mode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pageChange(page: number, target: HTMLElement) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,15 @@ import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
|||||||
const hoverTransitionTime = 300;
|
const hoverTransitionTime = 300;
|
||||||
const defaultHoverColor = hexToColor('1bd8f4');
|
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
|
// convert from this class's update format to TxSprite's update format
|
||||||
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
|
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
|
||||||
return {
|
return {
|
||||||
@ -25,7 +34,7 @@ export default class TxView implements TransactionStripped {
|
|||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
feerate: number;
|
feerate: number;
|
||||||
status?: 'found' | 'missing' | 'added';
|
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
|
||||||
|
|
||||||
initialised: boolean;
|
initialised: boolean;
|
||||||
vertexArray: FastVertexArray;
|
vertexArray: FastVertexArray;
|
||||||
@ -142,16 +151,23 @@ export default class TxView implements TransactionStripped {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getColor(): Color {
|
getColor(): Color {
|
||||||
// Block audit
|
|
||||||
if (this.status === 'missing') {
|
|
||||||
return hexToColor('039BE5');
|
|
||||||
} else if (this.status === 'added') {
|
|
||||||
return hexToColor('D81B60');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block component
|
|
||||||
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
|
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
|
||||||
return hexToColor(mempoolFeeColors[feeLevelIndex] || mempoolFeeColors[mempoolFeeColors.length - 1]);
|
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
||||||
|
// Block audit
|
||||||
|
switch(this.status) {
|
||||||
|
case 'censored':
|
||||||
|
return auditColors.censored;
|
||||||
|
case 'missing':
|
||||||
|
return auditColors.missing;
|
||||||
|
case 'added':
|
||||||
|
return auditColors.added;
|
||||||
|
case 'selected':
|
||||||
|
return auditColors.selected;
|
||||||
|
case 'found':
|
||||||
|
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||||
|
default:
|
||||||
|
return feeLevelColor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,3 +179,22 @@ function hexToColor(hex: string): Color {
|
|||||||
a: 1
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -32,6 +32,16 @@
|
|||||||
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||||
<td [innerHTML]="'‎' + (vsize | vbytes: 2)"></td>
|
<td [innerHTML]="'‎' + (vsize | vbytes: 2)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="tx && tx.status && tx.status.length">
|
||||||
|
<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.removed">removed</td>
|
||||||
|
<td *ngSwitchCase="'missing'" i18n="transaction.audit.missing">missing</td>
|
||||||
|
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
|
||||||
|
<td *ngSwitchCase="'selected'" i18n="transaction.audit.included">included</td>
|
||||||
|
</ng-container>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -110,6 +110,13 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="indexingAvailable">
|
||||||
|
<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>
|
</ng-template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -47,6 +47,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
transactionsError: any = null;
|
transactionsError: any = null;
|
||||||
overviewError: any = null;
|
overviewError: any = null;
|
||||||
webGlEnabled = true;
|
webGlEnabled = true;
|
||||||
|
indexingAvailable = false;
|
||||||
|
|
||||||
transactionSubscription: Subscription;
|
transactionSubscription: Subscription;
|
||||||
overviewSubscription: Subscription;
|
overviewSubscription: Subscription;
|
||||||
@ -86,6 +87,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.timeLtr = !!ltr;
|
this.timeLtr = !!ltr;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' &&
|
||||||
|
this.stateService.env.MINING_DASHBOARD === true);
|
||||||
|
|
||||||
this.txsLoadingStatus$ = this.route.paramMap
|
this.txsLoadingStatus$ = this.route.paramMap
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => this.stateService.loadingIndicators$),
|
switchMap(() => this.stateService.loadingIndicators$),
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
i18n-ngbTooltip="mining.pool-name" ngbTooltip="Pool" placement="bottom" #miningpool [disableTooltip]="!isEllipsisActive(miningpool)">Pool</th>
|
i18n-ngbTooltip="mining.pool-name" ngbTooltip="Pool" placement="bottom" #miningpool [disableTooltip]="!isEllipsisActive(miningpool)">Pool</th>
|
||||||
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Timestamp</th>
|
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Timestamp</th>
|
||||||
<th class="mined" i18n="latest-blocks.mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Mined</th>
|
<th class="mined" i18n="latest-blocks.mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Mined</th>
|
||||||
|
<th *ngIf="indexingAvailable" class="health text-left" i18n="latest-blocks.health" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||||
|
i18n-ngbTooltip="latest-blocks.health" ngbTooltip="Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Health</th>
|
||||||
<th *ngIf="indexingAvailable" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
<th *ngIf="indexingAvailable" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||||
i18n-ngbTooltip="latest-blocks.reward" ngbTooltip="Reward" placement="bottom" #reward [disableTooltip]="!isEllipsisActive(reward)">Reward</th>
|
i18n-ngbTooltip="latest-blocks.reward" ngbTooltip="Reward" placement="bottom" #reward [disableTooltip]="!isEllipsisActive(reward)">Reward</th>
|
||||||
<th *ngIf="indexingAvailable && !widget" class="fees text-right" i18n="latest-blocks.fees" [class]="indexingAvailable ? '' : 'legacy'">Fees</th>
|
<th *ngIf="indexingAvailable && !widget" class="fees text-right" i18n="latest-blocks.fees" [class]="indexingAvailable ? '' : 'legacy'">Fees</th>
|
||||||
@ -37,12 +39,30 @@
|
|||||||
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
|
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="timestamp" *ngIf="!widget">
|
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||||
</td>
|
</td>
|
||||||
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||||
<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}">
|
||||||
|
<a *ngIf="block.extras?.matchRate != null" class="clear-link" [routerLink]="['/block-audit/' | relativeUrl, block.id]">
|
||||||
|
<div class="progress progress-health">
|
||||||
|
<div class="progress-bar progress-bar-health" role="progressbar"
|
||||||
|
[ngStyle]="{'width': (100 - (block.extras?.matchRate || 0)) + '%' }"></div>
|
||||||
|
<div class="progress-text">
|
||||||
|
<span>{{ block.extras.matchRate }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div *ngIf="block.extras?.matchRate == null" class="progress progress-health">
|
||||||
|
<div class="progress-bar progress-bar-health" role="progressbar"
|
||||||
|
[ngStyle]="{'width': '100%' }"></div>
|
||||||
|
<div class="progress-text">
|
||||||
|
<span>~</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||||
<app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
|
<app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
|
||||||
</td>
|
</td>
|
||||||
@ -77,6 +97,9 @@
|
|||||||
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||||
<span class="skeleton-loader" style="max-width: 125px"></span>
|
<span class="skeleton-loader" style="max-width: 125px"></span>
|
||||||
</td>
|
</td>
|
||||||
|
<td *ngIf="indexingAvailable" class="health text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||||
|
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||||
|
</td>
|
||||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -63,7 +63,7 @@ tr, td, th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.height {
|
.height {
|
||||||
width: 10%;
|
width: 8%;
|
||||||
}
|
}
|
||||||
.height.widget {
|
.height.widget {
|
||||||
width: 15%;
|
width: 15%;
|
||||||
@ -77,12 +77,18 @@ tr, td, th {
|
|||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
width: 18%;
|
width: 18%;
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 1100px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.timestamp.legacy {
|
.timestamp.legacy {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
@media (max-width: 850px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mined {
|
.mined {
|
||||||
@ -93,6 +99,10 @@ tr, td, th {
|
|||||||
}
|
}
|
||||||
.mined.legacy {
|
.mined.legacy {
|
||||||
width: 15%;
|
width: 15%;
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
padding-right: 20px;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
}
|
}
|
||||||
@ -100,6 +110,7 @@ tr, td, th {
|
|||||||
|
|
||||||
.txs {
|
.txs {
|
||||||
padding-right: 40px;
|
padding-right: 40px;
|
||||||
|
width: 8%;
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
@ -113,17 +124,21 @@ tr, td, th {
|
|||||||
}
|
}
|
||||||
.txs.widget {
|
.txs.widget {
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
display: none;
|
||||||
@media (max-width: 650px) {
|
@media (max-width: 650px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.txs.legacy {
|
.txs.legacy {
|
||||||
padding-right: 80px;
|
width: 18%;
|
||||||
width: 10%;
|
display: table-cell;
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fees {
|
.fees {
|
||||||
width: 10%;
|
width: 8%;
|
||||||
@media (max-width: 650px) {
|
@media (max-width: 650px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -133,7 +148,7 @@ tr, td, th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reward {
|
.reward {
|
||||||
width: 10%;
|
width: 8%;
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
width: 7%;
|
width: 7%;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
@ -152,8 +167,11 @@ tr, td, th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.size {
|
.size {
|
||||||
width: 12%;
|
width: 10%;
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
|
width: 13%;
|
||||||
|
}
|
||||||
|
@media (max-width: 950px) {
|
||||||
width: 15%;
|
width: 15%;
|
||||||
}
|
}
|
||||||
@media (max-width: 650px) {
|
@media (max-width: 650px) {
|
||||||
@ -164,12 +182,34 @@ tr, td, th {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.size.legacy {
|
.size.legacy {
|
||||||
width: 20%;
|
width: 30%;
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.health {
|
||||||
|
width: 10%;
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
width: 13%;
|
||||||
|
}
|
||||||
|
@media (max-width: 950px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.health.widget {
|
||||||
|
width: 25%;
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Tooltip text */
|
/* Tooltip text */
|
||||||
.tooltip-custom {
|
.tooltip-custom {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -141,7 +141,7 @@ export interface TransactionStripped {
|
|||||||
fee: number;
|
fee: number;
|
||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
status?: 'found' | 'missing' | 'added';
|
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RewardStats {
|
export interface RewardStats {
|
||||||
|
@ -70,7 +70,7 @@ export interface TransactionStripped {
|
|||||||
fee: number;
|
fee: number;
|
||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
status?: 'found' | 'missing' | 'added';
|
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBackendInfo {
|
export interface IBackendInfo {
|
||||||
|
@ -668,6 +668,15 @@ h1, h2, h3 {
|
|||||||
background-color: #2e324e;
|
background-color: #2e324e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress.progress-health {
|
||||||
|
background: repeating-linear-gradient(to right, #2d3348, #2d3348 0%, #105fb0 0%, #1a9436 100%);
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar.progress-bar-health {
|
||||||
|
background: #2d3348;
|
||||||
|
}
|
||||||
|
|
||||||
.mt-2-5, .my-2-5 {
|
.mt-2-5, .my-2-5 {
|
||||||
margin-top: 0.75rem !important;
|
margin-top: 0.75rem !important;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user