merge block-audit and block pages
This commit is contained in:
parent
276470474d
commit
5fc3b8b70c
@ -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];
|
||||||
|
@ -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)
|
||||||
|
@ -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>
|
|
||||||
|
|
||||||
<a *ngIf="blockAudit" [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockAudit.height }}</a>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="grow"></div>
|
|
||||||
|
|
||||||
<button [routerLink]="['/block/' | relativeUrl, blockHash]" class="btn btn-sm">✕</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>
|
|
||||||
‎{{ 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]="'‎' + (blockAudit.size | bytes: 2)"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.weight">Weight</td>
|
|
||||||
<td [innerHTML]="'‎' + (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>
|
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -54,7 +54,19 @@
|
|||||||
<td i18n="block.weight">Weight</td>
|
<td i18n="block.weight">Weight</td>
|
||||||
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
<td [innerHTML]="'‎' + (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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user