Merge branch 'master' into fix-block-viz-resize
This commit is contained in:
@@ -41,10 +41,6 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
|
||||
<td>{{ blockAudit.tx_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="blockAudit.size">Size</td>
|
||||
<td [innerHTML]="'‎' + (blockAudit.size | bytes: 2)"></td>
|
||||
@@ -61,6 +57,10 @@
|
||||
<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>
|
||||
@@ -69,18 +69,10 @@
|
||||
<td i18n="block.missing-txs">Removed txs</td>
|
||||
<td>{{ blockAudit.missingTxs.length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.missing-txs">Omitted txs</td>
|
||||
<td>{{ numMissing }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.added-txs">Added txs</td>
|
||||
<td>{{ blockAudit.addedTxs.length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.missing-txs">Included txs</td>
|
||||
<td>{{ numUnexpected }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -97,21 +89,6 @@
|
||||
</div>
|
||||
|
||||
<ng-template [ngIf]="!error && isLoading">
|
||||
<div class="title-block" id="block">
|
||||
<h1>
|
||||
<span class="next-previous-blocks">
|
||||
<span i18n="shared.block-audit-title">Block Audit</span>
|
||||
|
||||
<a *ngIf="blockAudit" [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockAudit.height }}</a>
|
||||
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div class="grow"></div>
|
||||
|
||||
<button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- OVERVIEW -->
|
||||
<div class="box mb-3">
|
||||
<div class="row">
|
||||
@@ -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>
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -180,16 +155,16 @@
|
||||
<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"
|
||||
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
|
||||
[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"
|
||||
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
|
||||
</div>
|
||||
</div> <!-- row -->
|
||||
</div> <!-- box -->
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { Subscription, combineLatest } from 'rxjs';
|
||||
import { map, switchMap, startWith, catchError } from 'rxjs/operators';
|
||||
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';
|
||||
@@ -37,6 +38,7 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
isLoading = true;
|
||||
webGlEnabled = true;
|
||||
isMobile = window.innerWidth <= 767.98;
|
||||
hoverTx: string;
|
||||
|
||||
childChangeSubscription: Subscription;
|
||||
|
||||
@@ -51,7 +53,8 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private route: ActivatedRoute,
|
||||
public stateService: StateService,
|
||||
private router: Router,
|
||||
private apiService: ApiService
|
||||
private apiService: ApiService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
}
|
||||
@@ -76,69 +79,95 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.auditSubscription = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.blockHash = params.get('id') || null;
|
||||
if (!this.blockHash) {
|
||||
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)
|
||||
.pipe(
|
||||
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 (isAdded[tx.txid]) {
|
||||
tx.status = 'added';
|
||||
} else if (index === 0 || 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;
|
||||
})
|
||||
);
|
||||
}),
|
||||
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 null;
|
||||
return of(null);
|
||||
}),
|
||||
).subscribe((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}`);
|
||||
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() flip = true;
|
||||
@Input() disableSpinner = false;
|
||||
@Input() mirrorTxid: string | void;
|
||||
@Output() txClickEvent = new EventEmitter<TransactionStripped>();
|
||||
@Output() txHoverEvent = new EventEmitter<string>();
|
||||
@Output() readyEvent = new EventEmitter();
|
||||
|
||||
@ViewChild('blockCanvas')
|
||||
@@ -37,6 +39,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
scene: BlockScene;
|
||||
hoverTx: TxView | void;
|
||||
selectedTx: TxView | void;
|
||||
mirrorTx: TxView | void;
|
||||
tooltipPosition: Position;
|
||||
|
||||
readyNextFrame = false;
|
||||
@@ -63,6 +66,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.scene.setOrientation(this.orientation, this.flip);
|
||||
}
|
||||
}
|
||||
if (changes.mirrorTxid) {
|
||||
this.setMirror(this.mirrorTxid);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -76,6 +82,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.exit(direction);
|
||||
this.hoverTx = null;
|
||||
this.selectedTx = null;
|
||||
this.onTxHover(null);
|
||||
this.start();
|
||||
}
|
||||
|
||||
@@ -301,6 +308,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
this.hoverTx = null;
|
||||
this.selectedTx = null;
|
||||
this.onTxHover(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,17 +360,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.selectedTx = selected;
|
||||
} else {
|
||||
this.hoverTx = selected;
|
||||
this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
|
||||
}
|
||||
} else {
|
||||
if (clicked) {
|
||||
this.selectedTx = null;
|
||||
}
|
||||
this.hoverTx = null;
|
||||
this.onTxHover(null);
|
||||
}
|
||||
} else if (clicked) {
|
||||
if (selected === this.selectedTx) {
|
||||
this.hoverTx = this.selectedTx;
|
||||
this.selectedTx = null;
|
||||
this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
|
||||
} else {
|
||||
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) {
|
||||
const x = cssX * window.devicePixelRatio;
|
||||
const y = cssY * window.devicePixelRatio;
|
||||
@@ -378,6 +401,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.txClickEvent.emit(selected);
|
||||
}
|
||||
}
|
||||
|
||||
onTxHover(hoverId: string) {
|
||||
this.txHoverEvent.emit(hoverId);
|
||||
}
|
||||
}
|
||||
|
||||
// WebGL shader attributes
|
||||
|
||||
@@ -12,8 +12,8 @@ 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),
|
||||
added: hexToColor('0099ff'),
|
||||
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||
}
|
||||
|
||||
// convert from this class's update format to TxSprite's update format
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
<ng-container [ngSwitch]="tx?.status">
|
||||
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
|
||||
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
|
||||
<td *ngSwitchCase="'missing'" i18n="transaction.audit.missing">missing</td>
|
||||
<td *ngSwitchCase="'missing'" i18n="transaction.audit.omitted">omitted</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>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
<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>
|
||||
<span *ngIf="block.extras?.matchRate === null" i18n="unknown">Unknown</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators';
|
||||
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 { SeoService } from '../../services/seo.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
@@ -60,6 +60,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
nextBlockTxListSubscription: Subscription = undefined;
|
||||
timeLtrSubscription: Subscription;
|
||||
timeLtr: boolean;
|
||||
fetchAuditScore$ = new Subject<string>();
|
||||
fetchAuditScoreSubscription: Subscription;
|
||||
|
||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||
|
||||
@@ -105,12 +107,30 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (block.id === this.blockHash) {
|
||||
this.block = block;
|
||||
if (this.block.id && this.block?.extras?.matchRate == null) {
|
||||
this.fetchAuditScore$.next(this.block.id);
|
||||
}
|
||||
if (block?.extras?.reward != undefined) {
|
||||
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(
|
||||
switchMap((params: ParamMap) => {
|
||||
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.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.transactions = null;
|
||||
this.transactionsError = null;
|
||||
@@ -311,6 +334,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.networkChangedSubscription.unsubscribe();
|
||||
this.queryParamsSubscription.unsubscribe();
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
this.fetchAuditScoreSubscription?.unsubscribe();
|
||||
this.unsubscribeNextBlockSubscriptions();
|
||||
}
|
||||
|
||||
|
||||
@@ -46,22 +46,17 @@
|
||||
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<a *ngIf="block.extras?.matchRate != null" class="clear-link" [routerLink]="['/block-audit/' | relativeUrl, block.id]">
|
||||
<a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block-audit/' | relativeUrl, block.id] : null">
|
||||
<div class="progress progress-health">
|
||||
<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">
|
||||
<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>
|
||||
</a>
|
||||
<div *ngIf="block.extras?.matchRate == null" class="progress progress-health">
|
||||
<div class="progress-bar progress-bar-health" role="progressbar"
|
||||
[ngStyle]="{'width': '100%' }"></div>
|
||||
<div class="progress-text">
|
||||
<span>~</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<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) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-text .skeleton-loader {
|
||||
top: -8.5px;
|
||||
}
|
||||
}
|
||||
.health.widget {
|
||||
width: 25%;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest, concat, Observable, timer } from 'rxjs';
|
||||
import { delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest, concat, Observable, timer, EMPTY, Subscription, of } from 'rxjs';
|
||||
import { catchError, delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
@@ -12,10 +12,14 @@ import { WebsocketService } from '../../services/websocket.service';
|
||||
styleUrls: ['./blocks-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlocksList implements OnInit {
|
||||
export class BlocksList implements OnInit, OnDestroy {
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
blocks$: Observable<BlockExtended[]> = undefined;
|
||||
auditScores: { [hash: string]: number | void } = {};
|
||||
|
||||
auditScoreSubscription: Subscription;
|
||||
latestScoreSubscription: Subscription;
|
||||
|
||||
indexingAvailable = false;
|
||||
isLoading = true;
|
||||
@@ -105,6 +109,53 @@ export class BlocksList implements OnInit {
|
||||
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) {
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
<div class="d-flex">
|
||||
<div class="search-box-container mr-2">
|
||||
<input (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||
|
||||
<app-search-results #searchResults [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||
|
||||
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||
</div>
|
||||
<div>
|
||||
<button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener } from '@angular/core';
|
||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
@@ -23,6 +23,16 @@ export class SearchFormComponent implements OnInit {
|
||||
isTypeaheading$ = new BehaviorSubject<boolean>(false);
|
||||
typeAhead$: Observable<any>;
|
||||
searchForm: FormGroup;
|
||||
dropdownHidden = false;
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
onDocumentClick(event) {
|
||||
if (this.elementRef.nativeElement.contains(event.target)) {
|
||||
this.dropdownHidden = false;
|
||||
} else {
|
||||
this.dropdownHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
|
||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||
@@ -45,6 +55,7 @@ export class SearchFormComponent implements OnInit {
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private apiService: ApiService,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private elementRef: ElementRef,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -117,8 +117,9 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
switchMap(() => {
|
||||
let transactionObservable$: Observable<Transaction>;
|
||||
if (history.state.data && history.state.data.fee !== -1) {
|
||||
transactionObservable$ = of(history.state.data);
|
||||
const cached = this.stateService.getTxFromCache(this.txId);
|
||||
if (cached && cached.fee !== -1) {
|
||||
transactionObservable$ = of(cached);
|
||||
} else {
|
||||
transactionObservable$ = this.electrsApiService
|
||||
.getTransaction$(this.txId)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="title-block">
|
||||
<div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
|
||||
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
|
||||
<a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]" [state]="{ data: rbfTransaction.size ? rbfTransaction : null }">
|
||||
<a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]">
|
||||
<span class="d-inline d-lg-none">{{ rbfTransaction.txid | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span>
|
||||
</a>
|
||||
|
||||
@@ -183,8 +183,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}),
|
||||
switchMap(() => {
|
||||
let transactionObservable$: Observable<Transaction>;
|
||||
if (history.state.data && history.state.data.fee !== -1) {
|
||||
transactionObservable$ = of(history.state.data);
|
||||
const cached = this.stateService.getTxFromCache(this.txId);
|
||||
if (cached && cached.fee !== -1) {
|
||||
transactionObservable$ = of(cached);
|
||||
} else {
|
||||
transactionObservable$ = this.electrsApiService
|
||||
.getTransaction$(this.txId)
|
||||
@@ -279,6 +280,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.waitingForTransaction = false;
|
||||
}
|
||||
this.rbfTransaction = rbfTransaction;
|
||||
this.stateService.setTxCache([this.rbfTransaction]);
|
||||
});
|
||||
|
||||
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ng-container *ngFor="let tx of transactions; let i = index; trackBy: trackByFn">
|
||||
<div *ngIf="!transactionPage" class="header-bg box tx-page-container">
|
||||
<a class="float-left" [routerLink]="['/tx/' | relativeUrl, tx.txid]" [state]="{ data: tx }">
|
||||
<a class="float-left" [routerLink]="['/tx/' | relativeUrl, tx.txid]">
|
||||
<span style="float: left;" class="d-block d-md-none">{{ tx.txid | shortenString : 16 }}</span>
|
||||
<span style="float: left;" class="d-none d-md-block">{{ tx.txid }}</span>
|
||||
</a>
|
||||
|
||||
@@ -119,7 +119,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
this.transactionsLength = this.transactions.length;
|
||||
|
||||
this.stateService.setTxCache(this.transactions);
|
||||
|
||||
this.transactions.forEach((tx) => {
|
||||
tx['@voutLimit'] = true;
|
||||
|
||||
Reference in New Issue
Block a user