diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts
index f52d42d1f..47704f993 100644
--- a/backend/src/api/mining/mining-routes.ts
+++ b/backend/src/api/mining/mining-routes.ts
@@ -238,6 +238,12 @@ class MiningRoutes {
public async $getBlockAudit(req: Request, res: Response) {
try {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
+
+ if (!audit) {
+ res.status(404).send(`This block has not been audited.`);
+ return;
+ }
+
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts
index 60560b93c..4bd7cfc8d 100644
--- a/backend/src/api/websocket-handler.ts
+++ b/backend/src/api/websocket-handler.ts
@@ -413,7 +413,7 @@ class WebsocketHandler {
let mBlocks: undefined | MempoolBlock[];
let mBlockDeltas: undefined | MempoolBlockDelta[];
- let matchRate = 0;
+ let matchRate;
const _memPool = memPool.getMempool();
if (Common.indexingEnabled()) {
diff --git a/backend/src/repositories/BlocksAuditsRepository.ts b/backend/src/repositories/BlocksAuditsRepository.ts
index be85b22b9..4ddd7d761 100644
--- a/backend/src/repositories/BlocksAuditsRepository.ts
+++ b/backend/src/repositories/BlocksAuditsRepository.ts
@@ -58,10 +58,12 @@ class BlocksAuditRepositories {
WHERE blocks_audits.hash = "${hash}"
`);
- rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
- rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
- rows[0].transactions = JSON.parse(rows[0].transactions);
- rows[0].template = JSON.parse(rows[0].template);
+ if (rows.length) {
+ rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
+ rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
+ rows[0].transactions = JSON.parse(rows[0].transactions);
+ rows[0].template = JSON.parse(rows[0].template);
+ }
return rows[0];
} catch (e: any) {
diff --git a/frontend/src/app/components/block-audit/block-audit.component.html b/frontend/src/app/components/block-audit/block-audit.component.html
index 0ee6bef44..543dbb705 100644
--- a/frontend/src/app/components/block-audit/block-audit.component.html
+++ b/frontend/src/app/components/block-audit/block-audit.component.html
@@ -1,21 +1,22 @@
-
-
+
+
+
@@ -26,8 +27,8 @@
Hash |
- {{ blockAudit.id | shortenString : 13 }}
-
+ | {{ blockHash | shortenString : 13 }}
+
|
@@ -40,6 +41,10 @@
+
+ Transactions |
+ {{ blockAudit.tx_count }} |
+
Size |
|
@@ -57,21 +62,25 @@
- Transactions |
- {{ blockAudit.tx_count }} |
-
-
- Match rate |
+ Block health |
{{ blockAudit.matchRate }}% |
- Missing txs |
+ Removed txs |
{{ blockAudit.missingTxs.length }} |
+
+ Omitted txs |
+ {{ numMissing }} |
+
Added txs |
{{ blockAudit.addedTxs.length }} |
+
+ Included txs |
+ {{ numUnexpected }} |
+
@@ -79,33 +88,110 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ audit unavailable
+
+ {{ error.error }}
+
+
+
+
+
+
+ Error loading data.
+
+ {{ error }}
+
+
+
+
+
+
-
\ No newline at end of file
diff --git a/frontend/src/app/components/block-audit/block-audit.component.scss b/frontend/src/app/components/block-audit/block-audit.component.scss
index 7ec503891..1e35b7c63 100644
--- a/frontend/src/app/components/block-audit/block-audit.component.scss
+++ b/frontend/src/app/components/block-audit/block-audit.component.scss
@@ -37,4 +37,8 @@
@media (min-width: 768px) {
max-width: 150px;
}
+}
+
+.block-subtitle {
+ text-align: center;
}
\ No newline at end of file
diff --git a/frontend/src/app/components/block-audit/block-audit.component.ts b/frontend/src/app/components/block-audit/block-audit.component.ts
index ed884e728..a7eb879b4 100644
--- a/frontend/src/app/components/block-audit/block-audit.component.ts
+++ b/frontend/src/app/components/block-audit/block-audit.component.ts
@@ -1,7 +1,7 @@
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Observable, Subscription, combineLatest } from 'rxjs';
-import { map, share, switchMap, tap, startWith } from 'rxjs/operators';
+import { map, switchMap, startWith, catchError } from 'rxjs/operators';
import { BlockAudit, TransactionStripped } from 'src/app/interfaces/node-api.interface';
import { ApiService } from 'src/app/services/api.service';
import { StateService } from 'src/app/services/state.service';
@@ -25,21 +25,27 @@ import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overv
export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
blockAudit: BlockAudit = undefined;
transactions: string[];
- auditObservable$: Observable;
+ auditSubscription: Subscription;
+ urlFragmentSubscription: Subscription;
paginationMaxSize: number;
page = 1;
itemsPerPage: number;
- mode: 'missing' | 'added' = 'missing';
+ mode: 'projected' | 'actual' = 'projected';
+ error: any;
isLoading = true;
webGlEnabled = true;
isMobile = window.innerWidth <= 767.98;
childChangeSubscription: Subscription;
- @ViewChildren('blockGraphTemplate') blockGraphTemplate: QueryList;
- @ViewChildren('blockGraphMined') blockGraphMined: QueryList;
+ blockHash: string;
+ numMissing: number = 0;
+ numUnexpected: number = 0;
+
+ @ViewChildren('blockGraphProjected') blockGraphProjected: QueryList;
+ @ViewChildren('blockGraphActual') blockGraphActual: QueryList;
constructor(
private route: ActivatedRoute,
@@ -50,18 +56,31 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
this.webGlEnabled = detectWebGL();
}
- ngOnDestroy(): void {
+ ngOnDestroy() {
this.childChangeSubscription.unsubscribe();
+ this.urlFragmentSubscription.unsubscribe();
}
ngOnInit(): void {
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE;
- this.auditObservable$ = this.route.paramMap.pipe(
+ this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
+ if (fragment === 'actual') {
+ this.mode = 'actual';
+ } else {
+ this.mode = 'projected'
+ }
+ this.setupBlockGraphs();
+ });
+
+ this.auditSubscription = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
- const blockHash: string = params.get('id') || '';
- return this.apiService.getBlockAudit$(blockHash)
+ this.blockHash = params.get('id') || null;
+ if (!this.blockHash) {
+ return null;
+ }
+ return this.apiService.getBlockAudit$(this.blockHash)
.pipe(
map((response) => {
const blockAudit = response.body;
@@ -71,6 +90,8 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
const isCensored = {};
const isMissing = {};
const isSelected = {};
+ this.numMissing = 0;
+ this.numUnexpected = 0;
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
}
@@ -92,6 +113,7 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
} else {
tx.status = 'missing';
isMissing[tx.txid] = true;
+ this.numMissing++;
}
}
for (const [index, tx] of blockAudit.transactions.entries()) {
@@ -102,43 +124,46 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
+ this.numUnexpected++;
}
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
return blockAudit;
- }),
- tap((blockAudit) => {
- this.blockAudit = blockAudit;
- this.changeMode(this.mode);
- this.isLoading = false;
- }),
+ })
);
}),
- share()
- );
+ catchError((err) => {
+ console.log(err);
+ this.error = err;
+ this.isLoading = false;
+ return null;
+ }),
+ ).subscribe((blockAudit) => {
+ this.blockAudit = blockAudit;
+ this.setupBlockGraphs();
+ this.isLoading = false;
+ });
}
ngAfterViewInit() {
- this.childChangeSubscription = combineLatest([this.blockGraphTemplate.changes.pipe(startWith(null)), this.blockGraphMined.changes.pipe(startWith(null))]).subscribe(() => {
- console.log('changed!');
+ this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
this.setupBlockGraphs();
})
}
setupBlockGraphs() {
- console.log('setting up block graphs')
if (this.blockAudit) {
- this.blockGraphTemplate.forEach(graph => {
+ this.blockGraphProjected.forEach(graph => {
graph.destroy();
- if (this.isMobile && this.mode === 'added') {
+ if (this.isMobile && this.mode === 'actual') {
graph.setup(this.blockAudit.transactions);
} else {
graph.setup(this.blockAudit.template);
}
})
- this.blockGraphMined.forEach(graph => {
+ this.blockGraphActual.forEach(graph => {
graph.destroy();
graph.setup(this.blockAudit.transactions);
})
@@ -156,18 +181,12 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
- changeMode(mode: 'missing' | 'added') {
+ changeMode(mode: 'projected' | 'actual') {
this.router.navigate([], { fragment: mode });
- this.mode = mode;
-
- this.setupBlockGraphs();
}
onTxClick(event: TransactionStripped): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
this.router.navigate([url]);
}
-
- pageChange(page: number, target: HTMLElement) {
- }
}
diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts
index 1ddc55630..ac2a4655a 100644
--- a/frontend/src/app/components/block-overview-graph/tx-view.ts
+++ b/frontend/src/app/components/block-overview-graph/tx-view.ts
@@ -7,6 +7,15 @@ import { feeLevels, mempoolFeeColors } from '../../app.constants';
const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4');
+const feeColors = mempoolFeeColors.map(hexToColor);
+const auditFeeColors = feeColors.map((color) => desaturate(color, 0.3));
+const auditColors = {
+ censored: hexToColor('f344df'),
+ missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
+ added: hexToColor('03E1E5'),
+ selected: darken(desaturate(hexToColor('039BE5'), 0.3), 0.7),
+}
+
// convert from this class's update format to TxSprite's update format
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
return {
@@ -143,17 +152,19 @@ export default class TxView implements TransactionStripped {
getColor(): Color {
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
- const feeLevelColor = hexToColor(mempoolFeeColors[feeLevelIndex] || mempoolFeeColors[mempoolFeeColors.length - 1]);
+ const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
// Block audit
switch(this.status) {
case 'censored':
- return hexToColor('D81BC2');
+ return auditColors.censored;
case 'missing':
- return hexToColor('8C1BD8');
+ return auditColors.missing;
case 'added':
- return hexToColor('03E1E5');
+ return auditColors.added;
case 'selected':
- return hexToColor('039BE5');
+ return auditColors.selected;
+ case 'found':
+ return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
default:
return feeLevelColor;
}
@@ -168,3 +179,22 @@ function hexToColor(hex: string): Color {
a: 1
};
}
+
+function desaturate(color: Color, amount: number): Color {
+ const gray = (color.r + color.g + color.b) / 6;
+ return {
+ r: color.r + ((gray - color.r) * amount),
+ g: color.g + ((gray - color.g) * amount),
+ b: color.b + ((gray - color.b) * amount),
+ a: color.a,
+ };
+}
+
+function darken(color: Color, amount: number): Color {
+ return {
+ r: color.r * amount,
+ g: color.g * amount,
+ b: color.b * amount,
+ a: color.a,
+ }
+}
diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html
index 83fc627be..b19b67b06 100644
--- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html
+++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html
@@ -36,10 +36,10 @@
Audit status |
match |
- censored |
+ removed |
missing |
- prioritized |
- unexpected |
+ added |
+ included |