Merge pull request #2663 from mononaut/block-audit-tweaks
Block audit tweaks
This commit is contained in:
commit
a7c511fc1c
@ -1,5 +1,10 @@
|
|||||||
import logger from '../logger';
|
import config from '../config';
|
||||||
import { BlockExtended, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
|
import { Common } from './common';
|
||||||
|
import { TransactionExtended, MempoolBlockWithTransactions, AuditScore } from '../mempool.interfaces';
|
||||||
|
import blocksRepository from '../repositories/BlocksRepository';
|
||||||
|
import blocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
|
import blocks from '../api/blocks';
|
||||||
|
|
||||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||||
|
|
||||||
@ -44,8 +49,6 @@ class Audit {
|
|||||||
|
|
||||||
displacedWeight += (4000 - transactions[0].weight);
|
displacedWeight += (4000 - transactions[0].weight);
|
||||||
|
|
||||||
logger.warn(`${fresh.length} fresh, ${Object.keys(isCensored).length} possibly censored, ${displacedWeight} displaced weight`);
|
|
||||||
|
|
||||||
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
|
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
|
||||||
// these displaced transactions should occupy the first N weight units of the next projected block
|
// these displaced transactions should occupy the first N weight units of the next projected block
|
||||||
let displacedWeightRemaining = displacedWeight;
|
let displacedWeightRemaining = displacedWeight;
|
||||||
@ -73,6 +76,7 @@ class Audit {
|
|||||||
|
|
||||||
// mark unexpected transactions in the mined block as 'added'
|
// mark unexpected transactions in the mined block as 'added'
|
||||||
let overflowWeight = 0;
|
let overflowWeight = 0;
|
||||||
|
let totalWeight = 0;
|
||||||
for (const tx of transactions) {
|
for (const tx of transactions) {
|
||||||
if (inTemplate[tx.txid]) {
|
if (inTemplate[tx.txid]) {
|
||||||
matches.push(tx.txid);
|
matches.push(tx.txid);
|
||||||
@ -82,11 +86,13 @@ class Audit {
|
|||||||
}
|
}
|
||||||
overflowWeight += tx.weight;
|
overflowWeight += tx.weight;
|
||||||
}
|
}
|
||||||
|
totalWeight += tx.weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// transactions missing from near the end of our template are probably not being censored
|
// transactions missing from near the end of our template are probably not being censored
|
||||||
let overflowWeightRemaining = overflowWeight;
|
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
||||||
let lastOverflowRate = 1.00;
|
let maxOverflowRate = 0;
|
||||||
|
let rateThreshold = 0;
|
||||||
index = projectedBlocks[0].transactionIds.length - 1;
|
index = projectedBlocks[0].transactionIds.length - 1;
|
||||||
while (index >= 0) {
|
while (index >= 0) {
|
||||||
const txid = projectedBlocks[0].transactionIds[index];
|
const txid = projectedBlocks[0].transactionIds[index];
|
||||||
@ -94,8 +100,11 @@ class Audit {
|
|||||||
if (isCensored[txid]) {
|
if (isCensored[txid]) {
|
||||||
delete isCensored[txid];
|
delete isCensored[txid];
|
||||||
}
|
}
|
||||||
lastOverflowRate = mempool[txid].effectiveFeePerVsize;
|
if (mempool[txid].effectiveFeePerVsize > maxOverflowRate) {
|
||||||
} else if (Math.floor(mempool[txid].effectiveFeePerVsize * 100) <= Math.ceil(lastOverflowRate * 100)) { // tolerance of 0.01 sat/vb
|
maxOverflowRate = mempool[txid].effectiveFeePerVsize;
|
||||||
|
rateThreshold = (Math.ceil(maxOverflowRate * 100) / 100) + 0.005;
|
||||||
|
}
|
||||||
|
} else if (mempool[txid].effectiveFeePerVsize <= rateThreshold) { // tolerance of 0.01 sat/vb + rounding
|
||||||
if (isCensored[txid]) {
|
if (isCensored[txid]) {
|
||||||
delete isCensored[txid];
|
delete isCensored[txid];
|
||||||
}
|
}
|
||||||
@ -113,6 +122,45 @@ class Audit {
|
|||||||
score
|
score
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getBlockAuditScores(fromHeight?: number, limit: number = 15): Promise<AuditScore[]> {
|
||||||
|
let currentHeight = fromHeight !== undefined ? fromHeight : await blocksRepository.$mostRecentBlockHeight();
|
||||||
|
const returnScores: AuditScore[] = [];
|
||||||
|
|
||||||
|
if (currentHeight < 0) {
|
||||||
|
return returnScores;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < limit && currentHeight >= 0; i++) {
|
||||||
|
const block = blocks.getBlocks().find((b) => b.height === currentHeight);
|
||||||
|
if (block?.extras?.matchRate != null) {
|
||||||
|
returnScores.push({
|
||||||
|
hash: block.id,
|
||||||
|
matchRate: block.extras.matchRate
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let currentHash;
|
||||||
|
if (!currentHash && Common.indexingEnabled()) {
|
||||||
|
const dbBlock = await blocksRepository.$getBlockByHeight(currentHeight);
|
||||||
|
if (dbBlock && dbBlock['id']) {
|
||||||
|
currentHash = dbBlock['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!currentHash) {
|
||||||
|
currentHash = await bitcoinApi.$getBlockHash(currentHeight);
|
||||||
|
}
|
||||||
|
if (currentHash) {
|
||||||
|
const auditScore = await blocksAuditsRepository.$getBlockAuditScore(currentHash);
|
||||||
|
returnScores.push({
|
||||||
|
hash: currentHash,
|
||||||
|
matchRate: auditScore?.matchRate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentHeight--;
|
||||||
|
}
|
||||||
|
return returnScores;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Audit();
|
export default new Audit();
|
@ -195,9 +195,9 @@ class Blocks {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const auditSummary = await BlocksAuditsRepository.$getShortBlockAudit(block.id);
|
const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(block.id);
|
||||||
if (auditSummary) {
|
if (auditScore != null) {
|
||||||
blockExtended.extras.matchRate = auditSummary.matchRate;
|
blockExtended.extras.matchRate = auditScore.matchRate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Application, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import config from "../../config";
|
import config from "../../config";
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
|
import audits from '../audit';
|
||||||
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
|
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
|
||||||
import BlocksRepository from '../../repositories/BlocksRepository';
|
import BlocksRepository from '../../repositories/BlocksRepository';
|
||||||
import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository';
|
import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository';
|
||||||
@ -26,6 +27,9 @@ class MiningRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlockPrediction)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlockPrediction)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores', this.$getBlockAuditScores)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores/:height', this.$getBlockAuditScores)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/score/:hash', this.$getBlockAuditScore)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp)
|
||||||
;
|
;
|
||||||
@ -276,6 +280,29 @@ class MiningRoutes {
|
|||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $getBlockAuditScores(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
|
res.json(await audits.$getBlockAuditScores(height, 15));
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getBlockAuditScore(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const audit = await BlocksAuditsRepository.$getBlockAuditScore(req.params.hash);
|
||||||
|
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||||
|
res.json(audit || 'null');
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new MiningRoutes();
|
export default new MiningRoutes();
|
||||||
|
@ -32,6 +32,11 @@ export interface BlockAudit {
|
|||||||
matchRate: number,
|
matchRate: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuditScore {
|
||||||
|
hash: string,
|
||||||
|
matchRate?: number,
|
||||||
|
}
|
||||||
|
|
||||||
export interface MempoolBlock {
|
export interface MempoolBlock {
|
||||||
blockSize: number;
|
blockSize: number;
|
||||||
blockVSize: number;
|
blockVSize: number;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { BlockAudit } from '../mempool.interfaces';
|
import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
||||||
|
|
||||||
class BlocksAuditRepositories {
|
class BlocksAuditRepositories {
|
||||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||||
@ -72,10 +72,10 @@ class BlocksAuditRepositories {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getShortBlockAudit(hash: string): Promise<any> {
|
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
|
||||||
try {
|
try {
|
||||||
const [rows]: any[] = await DB.query(
|
const [rows]: any[] = await DB.query(
|
||||||
`SELECT hash as id, match_rate as matchRate
|
`SELECT hash, match_rate as matchRate
|
||||||
FROM blocks_audits
|
FROM blocks_audits
|
||||||
WHERE blocks_audits.hash = "${hash}"
|
WHERE blocks_audits.hash = "${hash}"
|
||||||
`);
|
`);
|
||||||
|
@ -41,10 +41,6 @@
|
|||||||
</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>
|
||||||
@ -61,6 +57,10 @@
|
|||||||
<div class="col-sm" *ngIf="blockAudit">
|
<div class="col-sm" *ngIf="blockAudit">
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
|
||||||
|
<td>{{ blockAudit.tx_count }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="block.health">Block health</td>
|
<td i18n="block.health">Block health</td>
|
||||||
<td>{{ blockAudit.matchRate }}%</td>
|
<td>{{ blockAudit.matchRate }}%</td>
|
||||||
@ -69,18 +69,10 @@
|
|||||||
<td i18n="block.missing-txs">Removed 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>
|
||||||
@ -97,21 +89,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template [ngIf]="!error && isLoading">
|
<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 -->
|
<!-- OVERVIEW -->
|
||||||
<div class="box mb-3">
|
<div class="box mb-3">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -123,7 +100,6 @@
|
|||||||
<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>
|
<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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -136,7 +112,6 @@
|
|||||||
<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>
|
<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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -180,16 +155,16 @@
|
|||||||
<div class="col-sm" *ngIf="webGlEnabled">
|
<div class="col-sm" *ngIf="webGlEnabled">
|
||||||
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
|
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
|
||||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoading" [resolution]="75"
|
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoading" [resolution]="75"
|
||||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false"
|
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
|
||||||
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
|
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($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">
|
||||||
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
|
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
|
||||||
<app-block-overview-graph #blockGraphActual [isLoading]="isLoading" [resolution]="75"
|
<app-block-overview-graph #blockGraphActual [isLoading]="isLoading" [resolution]="75"
|
||||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false"
|
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
|
||||||
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
|
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- row -->
|
</div> <!-- row -->
|
||||||
</div> <!-- box -->
|
</div> <!-- box -->
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } 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 { Subscription, combineLatest } from 'rxjs';
|
import { Subscription, combineLatest, of } from 'rxjs';
|
||||||
import { map, switchMap, startWith, catchError } from 'rxjs/operators';
|
import { map, switchMap, startWith, catchError, filter } from 'rxjs/operators';
|
||||||
import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
|
import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { detectWebGL } from '../../shared/graphs.utils';
|
import { detectWebGL } from '../../shared/graphs.utils';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
@ -37,6 +38,7 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
webGlEnabled = true;
|
webGlEnabled = true;
|
||||||
isMobile = window.innerWidth <= 767.98;
|
isMobile = window.innerWidth <= 767.98;
|
||||||
|
hoverTx: string;
|
||||||
|
|
||||||
childChangeSubscription: Subscription;
|
childChangeSubscription: Subscription;
|
||||||
|
|
||||||
@ -51,7 +53,8 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private apiService: ApiService
|
private apiService: ApiService,
|
||||||
|
private electrsApiService: ElectrsApiService,
|
||||||
) {
|
) {
|
||||||
this.webGlEnabled = detectWebGL();
|
this.webGlEnabled = detectWebGL();
|
||||||
}
|
}
|
||||||
@ -76,69 +79,95 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.auditSubscription = this.route.paramMap.pipe(
|
this.auditSubscription = this.route.paramMap.pipe(
|
||||||
switchMap((params: ParamMap) => {
|
switchMap((params: ParamMap) => {
|
||||||
this.blockHash = params.get('id') || null;
|
const blockHash = params.get('id') || null;
|
||||||
if (!this.blockHash) {
|
if (!blockHash) {
|
||||||
return null;
|
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)
|
return this.apiService.getBlockAudit$(this.blockHash)
|
||||||
.pipe(
|
}),
|
||||||
map((response) => {
|
filter((response) => response != null),
|
||||||
const blockAudit = response.body;
|
map((response) => {
|
||||||
const inTemplate = {};
|
const blockAudit = response.body;
|
||||||
const inBlock = {};
|
const inTemplate = {};
|
||||||
const isAdded = {};
|
const inBlock = {};
|
||||||
const isCensored = {};
|
const isAdded = {};
|
||||||
const isMissing = {};
|
const isCensored = {};
|
||||||
const isSelected = {};
|
const isMissing = {};
|
||||||
this.numMissing = 0;
|
const isSelected = {};
|
||||||
this.numUnexpected = 0;
|
this.numMissing = 0;
|
||||||
for (const tx of blockAudit.template) {
|
this.numUnexpected = 0;
|
||||||
inTemplate[tx.txid] = true;
|
for (const tx of blockAudit.template) {
|
||||||
}
|
inTemplate[tx.txid] = true;
|
||||||
for (const tx of blockAudit.transactions) {
|
}
|
||||||
inBlock[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.addedTxs) {
|
||||||
}
|
isAdded[txid] = true;
|
||||||
for (const txid of blockAudit.missingTxs) {
|
}
|
||||||
isCensored[txid] = true;
|
for (const txid of blockAudit.missingTxs) {
|
||||||
}
|
isCensored[txid] = true;
|
||||||
// set transaction statuses
|
}
|
||||||
for (const tx of blockAudit.template) {
|
// set transaction statuses
|
||||||
if (isCensored[tx.txid]) {
|
for (const tx of blockAudit.template) {
|
||||||
tx.status = 'censored';
|
if (isCensored[tx.txid]) {
|
||||||
} else if (inBlock[tx.txid]) {
|
tx.status = 'censored';
|
||||||
tx.status = 'found';
|
} else if (inBlock[tx.txid]) {
|
||||||
} else {
|
tx.status = 'found';
|
||||||
tx.status = 'missing';
|
} else {
|
||||||
isMissing[tx.txid] = true;
|
tx.status = 'missing';
|
||||||
this.numMissing++;
|
isMissing[tx.txid] = true;
|
||||||
}
|
this.numMissing++;
|
||||||
}
|
}
|
||||||
for (const [index, tx] of blockAudit.transactions.entries()) {
|
}
|
||||||
if (isAdded[tx.txid]) {
|
for (const [index, tx] of blockAudit.transactions.entries()) {
|
||||||
tx.status = 'added';
|
if (index === 0) {
|
||||||
} else if (index === 0 || inTemplate[tx.txid]) {
|
tx.status = null;
|
||||||
tx.status = 'found';
|
} else if (isAdded[tx.txid]) {
|
||||||
} else {
|
tx.status = 'added';
|
||||||
tx.status = 'selected';
|
} else if (inTemplate[tx.txid]) {
|
||||||
isSelected[tx.txid] = true;
|
tx.status = 'found';
|
||||||
this.numUnexpected++;
|
} else {
|
||||||
}
|
tx.status = 'selected';
|
||||||
}
|
isSelected[tx.txid] = true;
|
||||||
for (const tx of blockAudit.transactions) {
|
this.numUnexpected++;
|
||||||
inBlock[tx.txid] = true;
|
}
|
||||||
}
|
}
|
||||||
return blockAudit;
|
for (const tx of blockAudit.transactions) {
|
||||||
})
|
inBlock[tx.txid] = true;
|
||||||
);
|
}
|
||||||
|
return blockAudit;
|
||||||
}),
|
}),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
this.error = err;
|
this.error = err;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
return null;
|
return of(null);
|
||||||
}),
|
}),
|
||||||
).subscribe((blockAudit) => {
|
).subscribe((blockAudit) => {
|
||||||
this.blockAudit = blockAudit;
|
this.blockAudit = blockAudit;
|
||||||
@ -189,4 +218,12 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
@Input() orientation = 'left';
|
@Input() orientation = 'left';
|
||||||
@Input() flip = true;
|
@Input() flip = true;
|
||||||
@Input() disableSpinner = false;
|
@Input() disableSpinner = false;
|
||||||
|
@Input() mirrorTxid: string | void;
|
||||||
@Output() txClickEvent = new EventEmitter<TransactionStripped>();
|
@Output() txClickEvent = new EventEmitter<TransactionStripped>();
|
||||||
|
@Output() txHoverEvent = new EventEmitter<string>();
|
||||||
@Output() readyEvent = new EventEmitter();
|
@Output() readyEvent = new EventEmitter();
|
||||||
|
|
||||||
@ViewChild('blockCanvas')
|
@ViewChild('blockCanvas')
|
||||||
@ -37,6 +39,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
scene: BlockScene;
|
scene: BlockScene;
|
||||||
hoverTx: TxView | void;
|
hoverTx: TxView | void;
|
||||||
selectedTx: TxView | void;
|
selectedTx: TxView | void;
|
||||||
|
mirrorTx: TxView | void;
|
||||||
tooltipPosition: Position;
|
tooltipPosition: Position;
|
||||||
|
|
||||||
readyNextFrame = false;
|
readyNextFrame = false;
|
||||||
@ -63,6 +66,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
this.scene.setOrientation(this.orientation, this.flip);
|
this.scene.setOrientation(this.orientation, this.flip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (changes.mirrorTxid) {
|
||||||
|
this.setMirror(this.mirrorTxid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -76,6 +82,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
this.exit(direction);
|
this.exit(direction);
|
||||||
this.hoverTx = null;
|
this.hoverTx = null;
|
||||||
this.selectedTx = null;
|
this.selectedTx = null;
|
||||||
|
this.onTxHover(null);
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,6 +308,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
}
|
}
|
||||||
this.hoverTx = null;
|
this.hoverTx = null;
|
||||||
this.selectedTx = null;
|
this.selectedTx = null;
|
||||||
|
this.onTxHover(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,17 +360,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
this.selectedTx = selected;
|
this.selectedTx = selected;
|
||||||
} else {
|
} else {
|
||||||
this.hoverTx = selected;
|
this.hoverTx = selected;
|
||||||
|
this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (clicked) {
|
if (clicked) {
|
||||||
this.selectedTx = null;
|
this.selectedTx = null;
|
||||||
}
|
}
|
||||||
this.hoverTx = null;
|
this.hoverTx = null;
|
||||||
|
this.onTxHover(null);
|
||||||
}
|
}
|
||||||
} else if (clicked) {
|
} else if (clicked) {
|
||||||
if (selected === this.selectedTx) {
|
if (selected === this.selectedTx) {
|
||||||
this.hoverTx = this.selectedTx;
|
this.hoverTx = this.selectedTx;
|
||||||
this.selectedTx = null;
|
this.selectedTx = null;
|
||||||
|
this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
|
||||||
} else {
|
} else {
|
||||||
this.selectedTx = selected;
|
this.selectedTx = selected;
|
||||||
}
|
}
|
||||||
@ -370,6 +381,18 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMirror(txid: string | void) {
|
||||||
|
if (this.mirrorTx) {
|
||||||
|
this.scene.setHover(this.mirrorTx, false);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
if (txid && this.scene.txs[txid]) {
|
||||||
|
this.mirrorTx = this.scene.txs[txid];
|
||||||
|
this.scene.setHover(this.mirrorTx, true);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onTxClick(cssX: number, cssY: number) {
|
onTxClick(cssX: number, cssY: number) {
|
||||||
const x = cssX * window.devicePixelRatio;
|
const x = cssX * window.devicePixelRatio;
|
||||||
const y = cssY * window.devicePixelRatio;
|
const y = cssY * window.devicePixelRatio;
|
||||||
@ -378,6 +401,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
this.txClickEvent.emit(selected);
|
this.txClickEvent.emit(selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTxHover(hoverId: string) {
|
||||||
|
this.txHoverEvent.emit(hoverId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebGL shader attributes
|
// WebGL shader attributes
|
||||||
|
@ -12,8 +12,8 @@ const auditFeeColors = feeColors.map((color) => desaturate(color, 0.3));
|
|||||||
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),
|
||||||
added: hexToColor('03E1E5'),
|
added: hexToColor('0099ff'),
|
||||||
selected: darken(desaturate(hexToColor('039BE5'), 0.3), 0.7),
|
selected: darken(desaturate(hexToColor('0099ff'), 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
|
||||||
|
@ -37,9 +37,9 @@
|
|||||||
<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.missing">missing</td>
|
<td *ngSwitchCase="'missing'" i18n="transaction.audit.omitted">omitted</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.included">included</td>
|
<td *ngSwitchCase="'selected'" i18n="transaction.audit.extra">extra</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -114,7 +114,7 @@
|
|||||||
<td i18n="block.health">Block health</td>
|
<td i18n="block.health">Block health</td>
|
||||||
<td>
|
<td>
|
||||||
<a *ngIf="block.extras?.matchRate != null" [routerLink]="['/block-audit/' | relativeUrl, blockHash]">{{ block.extras.matchRate }}%</a>
|
<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>
|
<span *ngIf="block.extras?.matchRate === null" i18n="unknown">Unknown</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -4,7 +4,7 @@ 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 } from 'rxjs/operators';
|
||||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||||
import { Observable, of, Subscription, asyncScheduler, EMPTY } from 'rxjs';
|
import { Observable, of, Subscription, asyncScheduler, EMPTY, Subject } 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';
|
||||||
@ -60,6 +60,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
nextBlockTxListSubscription: Subscription = undefined;
|
nextBlockTxListSubscription: Subscription = undefined;
|
||||||
timeLtrSubscription: Subscription;
|
timeLtrSubscription: Subscription;
|
||||||
timeLtr: boolean;
|
timeLtr: boolean;
|
||||||
|
fetchAuditScore$ = new Subject<string>();
|
||||||
|
fetchAuditScoreSubscription: Subscription;
|
||||||
|
|
||||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||||
|
|
||||||
@ -105,12 +107,30 @@ 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') || '';
|
||||||
@ -209,6 +229,9 @@ 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;
|
||||||
@ -311,6 +334,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,22 +46,17 @@
|
|||||||
<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 *ngIf="block.extras?.matchRate != null" class="clear-link" [routerLink]="['/block-audit/' | relativeUrl, block.id]">
|
<a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block-audit/' | 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 - (block.extras?.matchRate || 0)) + '%' }"></div>
|
[ngStyle]="{'width': (100 - (auditScores[block.id] || 0)) + '%' }"></div>
|
||||||
<div class="progress-text">
|
<div class="progress-text">
|
||||||
<span>{{ block.extras.matchRate }}%</span>
|
<span *ngIf="auditScores[block.id] != null;">{{ auditScores[block.id] }}%</span>
|
||||||
|
<span *ngIf="auditScores[block.id] === undefined" class="skeleton-loader"></span>
|
||||||
|
<span *ngIf="auditScores[block.id] === null">~</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</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>
|
||||||
<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>
|
||||||
|
@ -196,6 +196,10 @@ tr, td, th {
|
|||||||
@media (max-width: 950px) {
|
@media (max-width: 950px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-text .skeleton-loader {
|
||||||
|
top: -8.5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.health.widget {
|
.health.widget {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||||
import { BehaviorSubject, combineLatest, concat, Observable, timer } from 'rxjs';
|
import { BehaviorSubject, combineLatest, concat, Observable, timer, EMPTY, Subscription, of } from 'rxjs';
|
||||||
import { delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
|
import { catchError, delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
|
||||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
@ -12,10 +12,14 @@ import { WebsocketService } from '../../services/websocket.service';
|
|||||||
styleUrls: ['./blocks-list.component.scss'],
|
styleUrls: ['./blocks-list.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class BlocksList implements OnInit {
|
export class BlocksList implements OnInit, OnDestroy {
|
||||||
@Input() widget: boolean = false;
|
@Input() widget: boolean = false;
|
||||||
|
|
||||||
blocks$: Observable<BlockExtended[]> = undefined;
|
blocks$: Observable<BlockExtended[]> = undefined;
|
||||||
|
auditScores: { [hash: string]: number | void } = {};
|
||||||
|
|
||||||
|
auditScoreSubscription: Subscription;
|
||||||
|
latestScoreSubscription: Subscription;
|
||||||
|
|
||||||
indexingAvailable = false;
|
indexingAvailable = false;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
@ -105,6 +109,53 @@ export class BlocksList implements OnInit {
|
|||||||
return acc;
|
return acc;
|
||||||
}, [])
|
}, [])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.indexingAvailable) {
|
||||||
|
this.auditScoreSubscription = this.fromHeightSubject.pipe(
|
||||||
|
switchMap((fromBlockHeight) => {
|
||||||
|
return this.apiService.getBlockAuditScores$(this.page === 1 ? undefined : fromBlockHeight)
|
||||||
|
.pipe(
|
||||||
|
catchError(() => {
|
||||||
|
return EMPTY;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
).subscribe((scores) => {
|
||||||
|
Object.values(scores).forEach(score => {
|
||||||
|
this.auditScores[score.hash] = score?.matchRate != null ? score.matchRate : null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.latestScoreSubscription = this.stateService.blocks$.pipe(
|
||||||
|
switchMap((block) => {
|
||||||
|
if (block[0]?.extras?.matchRate != null) {
|
||||||
|
return of({
|
||||||
|
hash: block[0].id,
|
||||||
|
matchRate: block[0]?.extras?.matchRate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (block[0]?.id && this.auditScores[block[0].id] === undefined) {
|
||||||
|
return this.apiService.getBlockAuditScore$(block[0].id)
|
||||||
|
.pipe(
|
||||||
|
catchError(() => {
|
||||||
|
return EMPTY;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
).subscribe((score) => {
|
||||||
|
if (score && score.hash) {
|
||||||
|
this.auditScores[score.hash] = score?.matchRate != null ? score.matchRate : null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.auditScoreSubscription?.unsubscribe();
|
||||||
|
this.latestScoreSubscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
pageChange(page: number) {
|
pageChange(page: number) {
|
||||||
|
@ -152,6 +152,11 @@ export interface RewardStats {
|
|||||||
totalTx: number;
|
totalTx: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuditScore {
|
||||||
|
hash: string;
|
||||||
|
matchRate?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ITopNodesPerChannels {
|
export interface ITopNodesPerChannels {
|
||||||
publicKey: string,
|
publicKey: string,
|
||||||
alias: string,
|
alias: string,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
||||||
PoolStat, BlockExtended, TransactionStripped, RewardStats } from '../interfaces/node-api.interface';
|
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore } from '../interfaces/node-api.interface';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { StateService } from './state.service';
|
import { StateService } from './state.service';
|
||||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||||
@ -234,6 +234,19 @@ export class ApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBlockAuditScores$(from: number): Observable<AuditScore[]> {
|
||||||
|
return this.httpClient.get<AuditScore[]>(
|
||||||
|
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/scores` +
|
||||||
|
(from !== undefined ? `/${from}` : ``)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlockAuditScore$(hash: string) : Observable<any> {
|
||||||
|
return this.httpClient.get<any>(
|
||||||
|
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/score/` + hash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getRewardStats$(blockCount: number = 144): Observable<RewardStats> {
|
getRewardStats$(blockCount: number = 144): Observable<RewardStats> {
|
||||||
return this.httpClient.get<RewardStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`);
|
return this.httpClient.get<RewardStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user