Merge branch 'master' into mononaut/preview-table-overflows

This commit is contained in:
wiz
2023-07-12 18:17:06 +09:00
committed by GitHub
243 changed files with 16344 additions and 2262 deletions

View File

@@ -34,7 +34,7 @@
</tr>
<tr *ngIf="block?.extras?.medianFee != undefined">
<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></td>
<td>~<app-fee-rate [fee]="block?.extras?.medianFee" rounding="1.0-0"></app-fee-rate></td>
</tr>
<ng-template [ngIf]="fees !== undefined">
<tr>

View File

@@ -1,6 +1,10 @@
<div class="container-xl" (window:resize)="onResize($event)">
<div class="title-block" [class.time-ltr]="timeLtr" id="block">
<div *ngIf="block?.stale" class="alert alert-mempool" role="alert">
<span i18n="block.reorged|Block reorg" class="alert-text">This block does not belong to the main chain, it has been replaced by:</span>
<app-truncate [text]="block.canonical" [lastChars]="12" [link]="['/block/' | relativeUrl, block.canonical]" [maxWidth]="480"></app-truncate>
</div>
<h1>
<ng-container *ngIf="blockHeight == null || blockHeight > 0; else genesis" i18n="shared.block-title">Block</ng-container>
<ng-template #genesis i18n="@@2303359202781425764">Genesis</ng-template>
@@ -23,6 +27,8 @@
<div class="grow"></div>
<button *ngIf="block?.stale" type="button" class="btn btn-sm btn-danger container-button" i18n="block.stale|Stale block state">Stale</button>
<button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">&#10005;</button>
</div>
@@ -41,7 +47,7 @@
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
<app-timestamp [unixTime]="block.timestamp"></app-timestamp>
<app-timestamp [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
</td>
</tr>
<tr>
@@ -63,7 +69,7 @@
*ngIf="blockAudit?.matchRate != null; else nullHealth"
>{{ blockAudit?.matchRate }}%</span>
<ng-template #nullHealth>
<ng-container *ngIf="!isLoadingAudit; else loadingHealth">
<ng-container *ngIf="!isLoadingOverview; else loadingHealth">
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-container>
</ng-template>
@@ -121,11 +127,11 @@
<ng-container *ngIf="!isLoadingBlock; else loadingRest">
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="mempool-block.fee-span">Fee span</td>
<td><span>{{ block.extras.feeRange[1] | 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>
<td><app-fee-rate [fee]="block?.extras?.minFee" [showUnit]="false" rounding="1.0-0"></app-fee-rate> - <app-fee-rate [fee]="block?.extras?.maxFee" rounding="1.0-0"></app-fee-rate></td>
</tr>
<tr *ngIf="block?.extras?.medianFee != undefined">
<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>
<td>~<app-fee-rate [fee]="block?.extras?.medianFee" rounding="1.0-0"></app-fee-rate>
<span class="fiat">
<app-fiat [blockConversion]="blockConversion" [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2"
i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes"
@@ -226,6 +232,9 @@
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
</div>
<ng-container *ngIf="network !== 'liquid'">
<ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
</ng-container>
</div>
<div class="col-sm" *ngIf="!isMobile">
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
@@ -235,6 +244,9 @@
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
</div>
<ng-container *ngIf="network !== 'liquid'">
<ng-container *ngTemplateOutlet="actualDetails"></ng-container>
</ng-container>
</div>
</div>
</div>
@@ -385,5 +397,60 @@
</a>
</ng-template>
<ng-template #expectedDetails>
<table *ngIf="block && blockAudit && blockAudit.expectedFees != null" class="table table-borderless table-striped audit-details-table">
<tbody>
<tr>
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
<td>
<app-amount [satoshis]="blockAudit.expectedFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</td>
</tr>
<tr>
<td i18n="block.weight">Weight</td>
<td [innerHTML]="'&lrm;' + (blockAudit.expectedWeight | wuBytes: 2)"></td>
</tr>
<tr>
<td i18n="mempool-block.transactions">Transactions</td>
<td>{{ blockAudit.template?.length || 0 }}</td>
</tr>
</tbody>
</table>
</ng-template>
<ng-template #actualDetails>
<table *ngIf="block && blockAudit && blockAudit.expectedFees != null" class="table table-borderless table-striped audit-details-table">
<tbody>
<tr>
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
<td>
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
<span *ngIf="blockAudit.feeDelta" class="difference" [class.positive]="blockAudit.feeDelta <= 0" [class.negative]="blockAudit.feeDelta > 0">
{{ blockAudit.feeDelta < 0 ? '+' : '' }}{{ (-blockAudit.feeDelta * 100) | amountShortener: 2 }}%
</span>
</td>
</tr>
<tr>
<td i18n="block.weight">Weight</td>
<td [innerHTML]>
<span [innerHTML]="'&lrm;' + (block.weight | wuBytes: 2)"></span>
<span *ngIf="blockAudit.weightDelta" class="difference" [class.positive]="blockAudit.weightDelta <= 0" [class.negative]="blockAudit.weightDelta > 0">
{{ blockAudit.weightDelta < 0 ? '+' : '' }}{{ (-blockAudit.weightDelta * 100) | amountShortener: 2 }}%
</span>
</td>
</tr>
<tr>
<td i18n="mempool-block.transactions">Transactions</td>
<td>
{{ block.tx_count }}
<span *ngIf="blockAudit.txDelta" class="difference" [class.positive]="blockAudit.txDelta <= 0" [class.negative]="blockAudit.txDelta > 0">
{{ blockAudit.txDelta < 0 ? '+' : '' }}{{ (-blockAudit.txDelta * 100) | amountShortener: 2 }}%
</span>
</td>
</tr>
</tbody>
</table>
</ng-template>
<br>
<br>

View File

@@ -1,3 +1,26 @@
.title-block {
flex-wrap: wrap;
align-items: baseline;
@media (min-width: 650px) {
flex-direction: row;
}
h1 {
margin: 0rem;
margin-right: 15px;
line-height: 1;
}
.alert-mempool {
flex-direction: row;
flex-wrap: wrap;
}
.container-button {
align-self: center;
margin-right: 1em;
}
}
.qr-wrapper {
background-color: #FFF;
padding: 10px;
@@ -38,6 +61,17 @@
color: rgba(255, 255, 255, 0.4);
margin-left: 5px;
}
.difference {
margin-left: 0.5em;
&.positive {
color: rgb(66, 183, 71);
}
&.negative {
color: rgb(183, 66, 66);
}
}
}
}
@@ -252,3 +286,10 @@ h1 {
top: 11px;
margin-left: 10px;
}
.audit-details-table {
margin-top: 1.25rem;
@media (max-width: 767.98px) {
margin-top: 0.75rem;
}
}

View File

@@ -2,9 +2,9 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/
import { Location } from '@angular/common';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith } from 'rxjs/operators';
import { Transaction, Vout } from '../../interfaces/electrs.interface';
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest } from 'rxjs';
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service';
@@ -44,7 +44,6 @@ export class BlockComponent implements OnInit, OnDestroy {
strippedTransactions: TransactionStripped[];
overviewTransitionDirection: string;
isLoadingOverview = true;
isLoadingAudit = true;
error: any;
blockSubsidy: number;
fees: number;
@@ -138,6 +137,8 @@ export class BlockComponent implements OnInit, OnDestroy {
if (block.id === this.blockHash) {
this.block = block;
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
if (block?.extras?.reward != undefined) {
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
}
@@ -234,6 +235,8 @@ export class BlockComponent implements OnInit, OnDestroy {
}
this.updateAuditAvailableFromBlockHeight(block.height);
this.block = block;
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
this.blockHeight = block.height;
this.lastBlockHeight = this.blockHeight;
this.nextBlockHeight = block.height + 1;
@@ -277,134 +280,125 @@ export class BlockComponent implements OnInit, OnDestroy {
this.isLoadingOverview = false;
});
if (!this.auditSupported) {
this.overviewSubscription = block$.pipe(
startWith(null),
pairwise(),
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
return of([]);
}),
switchMap((transactions) => {
if (prevBlock) {
return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
} else {
return of({ transactions, direction: 'down' });
}
})
)
),
)
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
this.strippedTransactions = transactions;
this.isLoadingOverview = false;
this.setupBlockGraphs();
},
(error) => {
this.error = error;
this.isLoadingOverview = false;
});
}
if (this.auditSupported) {
this.auditSubscription = block$.pipe(
startWith(null),
pairwise(),
switchMap(([prevBlock, block]) => {
this.isLoadingAudit = true;
this.blockAudit = null;
return this.apiService.getBlockAudit$(block.id)
this.overviewSubscription = block$.pipe(
switchMap((block) => {
return forkJoin([
this.apiService.getStrippedBlockTransactions$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
this.isLoadingAudit = false;
return of([]);
return of(null);
})
);
}
),
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;
),
!this.isAuditAvailableFromBlockHeight(block.height) ? of(null) : this.apiService.getBlockAudit$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
return of(null);
})
)
]);
})
)
.subscribe(([transactions, blockAudit]) => {
if (transactions) {
this.strippedTransactions = transactions;
} else {
this.strippedTransactions = [];
}
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.setAuditAvailable(true);
} else {
this.setAuditAvailable(false);
this.blockAudit = null;
if (transactions && blockAudit) {
const inTemplate = {};
const inBlock = {};
const isAdded = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
const isFresh = {};
const isSigop = {};
const isFullRbf = {};
this.numMissing = 0;
this.numUnexpected = 0;
if (blockAudit?.template) {
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
}
return blockAudit;
}),
catchError((err) => {
console.log(err);
this.error = err;
this.isLoadingOverview = false;
this.isLoadingAudit = false;
for (const tx of 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;
}
for (const txid of blockAudit.sigopTxs || []) {
isSigop[txid] = true;
}
for (const txid of blockAudit.fullrbfTxs || []) {
isFullRbf[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 {
if (isFresh[tx.txid]) {
tx.status = 'fresh';
} else if (isSigop[tx.txid]) {
tx.status = 'sigop';
} else if (isFullRbf[tx.txid]) {
tx.status = 'fullrbf';
} else {
tx.status = 'missing';
}
isMissing[tx.txid] = true;
this.numMissing++;
}
}
for (const [index, tx] of 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 if (isFullRbf[tx.txid]) {
tx.status = 'fullrbf';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
}
for (const tx of transactions) {
inBlock[tx.txid] = true;
}
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0;
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0;
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0;
this.blockAudit = blockAudit;
this.setAuditAvailable(true);
} else {
this.setAuditAvailable(false);
return of(null);
}),
).subscribe((blockAudit) => {
this.blockAudit = blockAudit;
this.setupBlockGraphs();
this.isLoadingOverview = false;
this.isLoadingAudit = false;
});
}
}
} else {
this.setAuditAvailable(false);
}
this.isLoadingOverview = false;
this.setupBlockGraphs();
});
this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
@@ -639,24 +633,50 @@ export class BlockComponent implements OnInit, OnDestroy {
}
updateAuditAvailableFromBlockHeight(blockHeight: number): void {
if (!this.auditSupported) {
if (!this.isAuditAvailableFromBlockHeight(blockHeight)) {
this.setAuditAvailable(false);
}
}
isAuditAvailableFromBlockHeight(blockHeight: number): boolean {
if (!this.auditSupported) {
return false;
}
switch (this.stateService.network) {
case 'testnet':
if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) {
this.setAuditAvailable(false);
return false;
}
break;
case 'signet':
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
this.setAuditAvailable(false);
return false;
}
break;
default:
if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) {
this.setAuditAvailable(false);
return false;
}
}
return true;
}
getMinBlockFee(block: BlockExtended): number {
if (block?.extras?.feeRange) {
// heuristic to check if feeRange is adjusted for effective rates
if (block.extras.medianFee === block.extras.feeRange[3]) {
return block.extras.feeRange[1];
} else {
return block.extras.feeRange[0];
}
}
return 0;
}
getMaxBlockFee(block: BlockExtended): number {
if (block?.extras?.feeRange) {
return block.extras.feeRange[block.extras.feeRange.length - 1];
}
return 0;
}
}