Add visualization to mined blocks
This commit is contained in:
parent
225decd286
commit
7f4c6352ba
@ -15,6 +15,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit {
|
|||||||
@Input() isLoading: boolean;
|
@Input() isLoading: boolean;
|
||||||
@Input() resolution: number;
|
@Input() resolution: number;
|
||||||
@Input() blockLimit: number;
|
@Input() blockLimit: number;
|
||||||
|
@Input() orientation = 'left';
|
||||||
|
@Input() flip = true;
|
||||||
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
||||||
|
|
||||||
@ViewChild('blockCanvas')
|
@ViewChild('blockCanvas')
|
||||||
@ -67,9 +69,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
replace(transactions: TransactionStripped[], direction: string): void {
|
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void {
|
||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.scene.replace(transactions, direction);
|
this.scene.replace(transactions || [], direction, sort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,8 +141,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit {
|
|||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
|
this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
|
||||||
} else {
|
} else {
|
||||||
this.scene = this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
||||||
blockLimit: this.blockLimit, vertexArray: this.vertexArray });
|
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray });
|
||||||
|
this.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ export default class BlockScene {
|
|||||||
scene: { count: number, offset: { x: number, y: number}};
|
scene: { count: number, offset: { x: number, y: number}};
|
||||||
vertexArray: FastVertexArray;
|
vertexArray: FastVertexArray;
|
||||||
txs: { [key: string]: TxView };
|
txs: { [key: string]: TxView };
|
||||||
|
orientation: string;
|
||||||
|
flip: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
gridWidth: number;
|
gridWidth: number;
|
||||||
@ -19,10 +21,11 @@ export default class BlockScene {
|
|||||||
layout: BlockLayout;
|
layout: BlockLayout;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
|
|
||||||
constructor({ width, height, resolution, blockLimit, vertexArray }:
|
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray }:
|
||||||
{ width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }
|
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||||
|
orientation: string, flip: boolean, vertexArray: FastVertexArray }
|
||||||
) {
|
) {
|
||||||
this.init({ width, height, resolution, blockLimit, vertexArray });
|
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray });
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
@ -61,7 +64,7 @@ export default class BlockScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset layout and replace with new set of transactions
|
// Reset layout and replace with new set of transactions
|
||||||
replace(txs: TransactionStripped[], direction: string = 'left'): void {
|
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true): void {
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const nextIds = {};
|
const nextIds = {};
|
||||||
const remove = [];
|
const remove = [];
|
||||||
@ -90,9 +93,15 @@ export default class BlockScene {
|
|||||||
|
|
||||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||||
|
|
||||||
Object.values(this.txs).sort(feeRateDescending).forEach(tx => {
|
if (sort) {
|
||||||
this.place(tx);
|
Object.values(this.txs).sort(feeRateDescending).forEach(tx => {
|
||||||
});
|
this.place(tx);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
txs.forEach(tx => {
|
||||||
|
this.place(this.txs[tx.txid]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.updateAll(startTime, direction);
|
this.updateAll(startTime, direction);
|
||||||
}
|
}
|
||||||
@ -143,9 +152,12 @@ export default class BlockScene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private init({ width, height, resolution, blockLimit, vertexArray }:
|
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray }:
|
||||||
{ width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }
|
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||||
|
orientation: string, flip: boolean, vertexArray: FastVertexArray }
|
||||||
): void {
|
): void {
|
||||||
|
this.orientation = orientation;
|
||||||
|
this.flip = flip;
|
||||||
this.vertexArray = vertexArray;
|
this.vertexArray = vertexArray;
|
||||||
|
|
||||||
this.scene = {
|
this.scene = {
|
||||||
@ -188,8 +200,8 @@ export default class BlockScene {
|
|||||||
tx.update({
|
tx.update({
|
||||||
display: {
|
display: {
|
||||||
position: {
|
position: {
|
||||||
x: tx.screenPosition.x + (direction === 'right' ? -this.width : this.width) * 1.4,
|
x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4,
|
||||||
y: tx.screenPosition.y,
|
y: tx.screenPosition.y + (direction === 'up' ? -this.height : (direction === 'down' ? this.height : 0)) * 1.4,
|
||||||
s: tx.screenPosition.s
|
s: tx.screenPosition.s
|
||||||
},
|
},
|
||||||
color: txColor,
|
color: txColor,
|
||||||
@ -237,8 +249,8 @@ export default class BlockScene {
|
|||||||
tx.update({
|
tx.update({
|
||||||
display: {
|
display: {
|
||||||
position: {
|
position: {
|
||||||
x: tx.screenPosition.x + (direction === 'right' ? this.width : -this.width) * 1.4,
|
x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4,
|
||||||
y: this.txs[id].screenPosition.y,
|
y: tx.screenPosition.y + (direction === 'up' ? this.height : (direction === 'down' ? -this.height : 0)) * 1.4,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
@ -264,18 +276,42 @@ export default class BlockScene {
|
|||||||
const slotSize = (position.s * this.gridSize);
|
const slotSize = (position.s * this.gridSize);
|
||||||
const squareSize = slotSize - (this.unitPadding * 2);
|
const squareSize = slotSize - (this.unitPadding * 2);
|
||||||
|
|
||||||
// The grid is laid out notionally left-to-right, bottom-to-top
|
// The grid is laid out notionally left-to-right, bottom-to-top,
|
||||||
// So we rotate 90deg counterclockwise then flip the y axis
|
// so we rotate and/or flip the y axis to match the target configuration.
|
||||||
|
//
|
||||||
|
// e.g. for flip = true, orientation = 'left':
|
||||||
//
|
//
|
||||||
// grid screen
|
// grid screen
|
||||||
// ________ ________ ________
|
// ________ ________ ________
|
||||||
// | | | b| | a|
|
// | | | | | a|
|
||||||
// | | rotate | | flip | c |
|
// | | flip | | rotate | c |
|
||||||
// | c | --> | c | --> | |
|
// | c | --> | c | --> | |
|
||||||
// |a______b| |_______a| |_______b|
|
// |a______b| |b______a| |_______b|
|
||||||
|
|
||||||
|
let x = (this.gridSize * position.x) + (slotSize / 2);
|
||||||
|
let y = (this.gridSize * position.y) + (slotSize / 2);
|
||||||
|
let t;
|
||||||
|
if (this.flip) {
|
||||||
|
x = this.width - x;
|
||||||
|
}
|
||||||
|
switch (this.orientation) {
|
||||||
|
case 'left':
|
||||||
|
t = x;
|
||||||
|
x = this.width - y;
|
||||||
|
y = t;
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
t = x;
|
||||||
|
x = y;
|
||||||
|
y = t;
|
||||||
|
break;
|
||||||
|
case 'bottom':
|
||||||
|
y = this.height - y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
x: this.width + (this.unitPadding * 2) - (this.gridSize * position.y) - slotSize,
|
x: x + this.unitPadding - (slotSize / 2),
|
||||||
y: this.height - ((this.gridSize * position.x) + (slotSize - this.unitPadding)),
|
y: y + this.unitPadding - (slotSize / 2),
|
||||||
s: squareSize
|
s: squareSize
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -284,11 +320,32 @@ export default class BlockScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
screenToGrid(position: Position): Position {
|
screenToGrid(position: Position): Position {
|
||||||
const grid = {
|
let x = position.x;
|
||||||
x: Math.floor((position.y - this.unitPadding) / this.gridSize),
|
let y = this.height - position.y;
|
||||||
y: Math.floor((this.width + (this.unitPadding * 2) - position.x) / this.gridSize)
|
let t;
|
||||||
|
|
||||||
|
switch (this.orientation) {
|
||||||
|
case 'left':
|
||||||
|
t = x;
|
||||||
|
x = y;
|
||||||
|
y = this.width - t;
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
t = x;
|
||||||
|
x = y;
|
||||||
|
y = t;
|
||||||
|
break;
|
||||||
|
case 'bottom':
|
||||||
|
y = this.height - y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (this.flip) {
|
||||||
|
x = this.width - x;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: Math.floor(x / this.gridSize),
|
||||||
|
y: Math.floor(y / this.gridSize)
|
||||||
};
|
};
|
||||||
return grid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculates and returns the size of the tx in multiples of the grid size
|
// calculates and returns the size of the tx in multiples of the grid size
|
||||||
|
@ -40,10 +40,11 @@
|
|||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
<ng-template [ngIf]="!isLoadingBlock && !error">
|
|
||||||
|
|
||||||
<div class="box">
|
|
||||||
<div class="row">
|
<div class="box" *ngIf="!error">
|
||||||
|
<div class="row">
|
||||||
|
<ng-template [ngIf]="!isLoadingBlock">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -68,73 +69,191 @@
|
|||||||
<td i18n="block.weight">Weight</td>
|
<td i18n="block.weight">Weight</td>
|
||||||
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<ng-template [ngIf]="webGlEnabled">
|
||||||
|
<tr *ngIf="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> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
||||||
|
</tr>
|
||||||
|
<ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
|
||||||
|
<tr>
|
||||||
|
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||||
|
<td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees">
|
||||||
|
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||||
|
<span class="fiat">
|
||||||
|
<app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<ng-template #liquidTotalFees>
|
||||||
|
<td>
|
||||||
|
<app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <app-fiat
|
||||||
|
[value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat>
|
||||||
|
</td>
|
||||||
|
</ng-template>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
|
||||||
|
<td>
|
||||||
|
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||||
|
<span class="fiat">
|
||||||
|
<app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #loadingFees>
|
||||||
|
<tr>
|
||||||
|
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||||
|
<td style="width: 75%;"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
|
||||||
|
<td><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td i18n="block.miner">Miner</td>
|
||||||
|
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||||
|
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
||||||
|
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
|
{{ block.extras.pool.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||||
|
<span placement="bottom" class="badge"
|
||||||
|
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
|
{{ block.extras.pool.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template [ngIf]="isLoadingBlock">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngIf="block?.extras?.medianFee != undefined">
|
<tr>
|
||||||
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
|
<tr>
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<ng-template [ngIf]="webGlEnabled">
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
<td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees">
|
|
||||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
|
||||||
<span class="fiat">
|
|
||||||
<app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<ng-template #liquidTotalFees>
|
|
||||||
<td>
|
|
||||||
<app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <app-fiat
|
|
||||||
[value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat>
|
|
||||||
</td>
|
|
||||||
</ng-template>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
<tr>
|
||||||
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
<td>
|
</tr>
|
||||||
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
<tr>
|
||||||
<span class="fiat">
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
<app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat>
|
</tr>
|
||||||
</span>
|
<tr>
|
||||||
</td>
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #loadingFees>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
|
||||||
<td style="width: 75%;"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
|
|
||||||
<td><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td i18n="block.miner">Miner</td>
|
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
|
||||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
|
||||||
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
|
||||||
{{ block.extras.pool.name }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
|
||||||
<span placement="bottom" class="badge"
|
|
||||||
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
|
||||||
{{ block.extras.pool.name }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<div class="col-sm" *ngIf="!webGlEnabled">
|
||||||
|
<table class="table table-borderless table-striped" *ngIf="!isLoadingBlock">
|
||||||
|
<tbody>
|
||||||
|
<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> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
||||||
|
</tr>
|
||||||
|
<ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
|
||||||
|
<tr>
|
||||||
|
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||||
|
<td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees">
|
||||||
|
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||||
|
<span class="fiat">
|
||||||
|
<app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<ng-template #liquidTotalFees>
|
||||||
|
<td>
|
||||||
|
<app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <app-fiat
|
||||||
|
[value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat>
|
||||||
|
</td>
|
||||||
|
</ng-template>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
|
||||||
|
<td>
|
||||||
|
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||||
|
<span class="fiat">
|
||||||
|
<app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #loadingFees>
|
||||||
|
<tr>
|
||||||
|
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||||
|
<td style="width: 75%;"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
|
||||||
|
<td><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td i18n="block.miner">Miner</td>
|
||||||
|
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||||
|
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
||||||
|
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
|
{{ block.extras.pool.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||||
|
<span placement="bottom" class="badge"
|
||||||
|
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
|
{{ block.extras.pool.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table class="table table-borderless table-striped" *ngIf="isLoadingBlock">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm chart-container" *ngIf="webGlEnabled">
|
||||||
|
<app-block-overview-graph
|
||||||
|
#blockGraph
|
||||||
|
[isLoading]="isLoadingOverview"
|
||||||
|
[resolution]="75"
|
||||||
|
[blockLimit]="stateService.blockVSize"
|
||||||
|
[orientation]="'top'"
|
||||||
|
[flip]="false"
|
||||||
|
></app-block-overview-graph>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-template [ngIf]="!isLoadingBlock && !error">
|
||||||
<div [hidden]="!showDetails" id="details">
|
<div [hidden]="!showDetails" id="details">
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@ -223,63 +342,17 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<span class="skeleton-loader"></span>
|
<span class="skeleton-loader"></span>
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<span class="skeleton-loader"></span>
|
<span class="skeleton-loader"></span>
|
||||||
<span class="skeleton-loader"></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ngb-pagination class="pagination-container float-right" [collectionSize]="block.tx_count" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page, blockTxTitle)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
<ngb-pagination class="pagination-container float-right" [collectionSize]="block.tx_count" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page, blockTxTitle)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template [ngIf]="isLoadingBlock && !error">
|
|
||||||
|
|
||||||
<div class="box">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm">
|
|
||||||
<table class="table table-borderless table-striped">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm">
|
|
||||||
<table class="table table-borderless table-striped">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template [ngIf]="error">
|
<ng-template [ngIf]="error">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<span i18n="error.general-loading-data">Error loading data.</span>
|
<span i18n="error.general-loading-data">Error loading data.</span>
|
||||||
|
@ -148,3 +148,10 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-container{
|
||||||
|
margin: 20px auto;
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,15 +2,16 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co
|
|||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { switchMap, tap, debounceTime, catchError, map } from 'rxjs/operators';
|
import { switchMap, tap, debounceTime, 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 } from 'rxjs';
|
import { Observable, of, Subscription } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
|
import { BlockExtended, TransactionStripped } from 'src/app/interfaces/node-api.interface';
|
||||||
import { ApiService } from 'src/app/services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
|
import { BlockOverviewGraphComponent } from 'src/app/components/block-overview-graph/block-overview-graph.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block',
|
selector: 'app-block',
|
||||||
@ -21,6 +22,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
network = '';
|
network = '';
|
||||||
block: BlockExtended;
|
block: BlockExtended;
|
||||||
blockHeight: number;
|
blockHeight: number;
|
||||||
|
lastBlockHeight: number;
|
||||||
nextBlockHeight: number;
|
nextBlockHeight: number;
|
||||||
blockHash: string;
|
blockHash: string;
|
||||||
isLoadingBlock = true;
|
isLoadingBlock = true;
|
||||||
@ -28,6 +30,10 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
latestBlocks: BlockExtended[] = [];
|
latestBlocks: BlockExtended[] = [];
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
isLoadingTransactions = true;
|
isLoadingTransactions = true;
|
||||||
|
strippedTransactions: TransactionStripped[];
|
||||||
|
overviewTransitionDirection: string;
|
||||||
|
isLoadingOverview = true;
|
||||||
|
isAwaitingOverview = true;
|
||||||
error: any;
|
error: any;
|
||||||
blockSubsidy: number;
|
blockSubsidy: number;
|
||||||
fees: number;
|
fees: number;
|
||||||
@ -39,13 +45,18 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
showPreviousBlocklink = true;
|
showPreviousBlocklink = true;
|
||||||
showNextBlocklink = true;
|
showNextBlocklink = true;
|
||||||
transactionsError: any = null;
|
transactionsError: any = null;
|
||||||
|
overviewError: any = null;
|
||||||
|
webGlEnabled = true;
|
||||||
|
|
||||||
subscription: Subscription;
|
transactionSubscription: Subscription;
|
||||||
|
overviewSubscription: Subscription;
|
||||||
keyNavigationSubscription: Subscription;
|
keyNavigationSubscription: Subscription;
|
||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
networkChangedSubscription: Subscription;
|
networkChangedSubscription: Subscription;
|
||||||
queryParamsSubscription: Subscription;
|
queryParamsSubscription: Subscription;
|
||||||
|
|
||||||
|
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
@ -56,7 +67,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private relativeUrlPipe: RelativeUrlPipe,
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
private apiService: ApiService
|
private apiService: ApiService
|
||||||
) { }
|
) {
|
||||||
|
this.webGlEnabled = detectWebGL();
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||||
@ -85,7 +98,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.subscription = 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') || '';
|
||||||
this.block = undefined;
|
this.block = undefined;
|
||||||
@ -141,6 +154,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
tap((block: BlockExtended) => {
|
tap((block: BlockExtended) => {
|
||||||
this.block = block;
|
this.block = block;
|
||||||
this.blockHeight = block.height;
|
this.blockHeight = block.height;
|
||||||
|
const direction = (this.lastBlockHeight < this.blockHeight) ? 'right' : 'left';
|
||||||
|
this.lastBlockHeight = this.blockHeight;
|
||||||
this.nextBlockHeight = block.height + 1;
|
this.nextBlockHeight = block.height + 1;
|
||||||
this.setNextAndPreviousBlockLink();
|
this.setNextAndPreviousBlockLink();
|
||||||
|
|
||||||
@ -154,8 +169,17 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
this.transactions = null;
|
this.transactions = null;
|
||||||
this.transactionsError = null;
|
this.transactionsError = null;
|
||||||
|
this.isLoadingOverview = true;
|
||||||
|
this.isAwaitingOverview = true;
|
||||||
|
this.overviewError = true;
|
||||||
|
if (this.blockGraph) {
|
||||||
|
this.blockGraph.exit(direction);
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
debounceTime(300),
|
debounceTime(300),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
this.transactionSubscription = block$.pipe(
|
||||||
switchMap((block) => this.electrsApiService.getBlockTransactions$(block.id)
|
switchMap((block) => this.electrsApiService.getBlockTransactions$(block.id)
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
@ -170,10 +194,51 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.transactions = transactions;
|
this.transactions = transactions;
|
||||||
this.isLoadingTransactions = false;
|
this.isLoadingTransactions = false;
|
||||||
|
|
||||||
|
if (!this.isAwaitingOverview && this.blockGraph && this.strippedTransactions && this.overviewTransitionDirection) {
|
||||||
|
this.isLoadingOverview = false;
|
||||||
|
this.blockGraph.replace(this.strippedTransactions, this.overviewTransitionDirection, false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
this.isLoadingBlock = false;
|
this.isLoadingBlock = false;
|
||||||
|
this.isLoadingOverview = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
console.log('overview loaded: ', prevBlock && prevBlock.height, block.height);
|
||||||
|
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.isAwaitingOverview = false;
|
||||||
|
this.strippedTransactions = transactions;
|
||||||
|
this.overviewTransitionDirection = direction;
|
||||||
|
if (!this.isLoadingTransactions && this.blockGraph) {
|
||||||
|
this.isLoadingOverview = false;
|
||||||
|
this.blockGraph.replace(this.strippedTransactions, this.overviewTransitionDirection, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.error = error;
|
||||||
|
this.isLoadingOverview = false;
|
||||||
|
this.isAwaitingOverview = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.networkChangedSubscription = this.stateService.networkChanged$
|
this.networkChangedSubscription = this.stateService.networkChanged$
|
||||||
@ -203,7 +268,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.stateService.markBlock$.next({});
|
this.stateService.markBlock$.next({});
|
||||||
this.subscription.unsubscribe();
|
this.transactionSubscription.unsubscribe();
|
||||||
|
this.overviewSubscription.unsubscribe();
|
||||||
this.keyNavigationSubscription.unsubscribe();
|
this.keyNavigationSubscription.unsubscribe();
|
||||||
this.blocksSubscription.unsubscribe();
|
this.blocksSubscription.unsubscribe();
|
||||||
this.networkChangedSubscription.unsubscribe();
|
this.networkChangedSubscription.unsubscribe();
|
||||||
@ -303,3 +369,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectWebGL() {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
||||||
|
return (gl && gl instanceof WebGLRenderingContext);
|
||||||
|
}
|
||||||
|
@ -3,5 +3,7 @@
|
|||||||
[isLoading]="isLoading$ | async"
|
[isLoading]="isLoading$ | async"
|
||||||
[resolution]="75"
|
[resolution]="75"
|
||||||
[blockLimit]="stateService.blockVSize"
|
[blockLimit]="stateService.blockVSize"
|
||||||
(txPreviewEvent)="onTxPreview($event)">
|
[orientation]="'left'"
|
||||||
</app-block-overview-graph>
|
[flip]="true"
|
||||||
|
(txPreviewEvent)="onTxPreview($event)"
|
||||||
|
></app-block-overview-graph>
|
||||||
|
@ -128,6 +128,13 @@ export interface BlockExtended extends Block {
|
|||||||
extras?: BlockExtension;
|
extras?: BlockExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TransactionStripped {
|
||||||
|
txid: string;
|
||||||
|
fee: number;
|
||||||
|
vsize: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RewardStats {
|
export interface RewardStats {
|
||||||
startBlock: number;
|
startBlock: number;
|
||||||
endBlock: number;
|
endBlock: number;
|
||||||
|
@ -1,6 +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, PoolsStats, PoolStat, BlockExtended, RewardStats } from '../interfaces/node-api.interface';
|
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
||||||
|
PoolsStats, PoolStat, BlockExtended, TransactionStripped, RewardStats } 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';
|
||||||
@ -158,6 +159,10 @@ export class ApiService {
|
|||||||
return this.httpClient.get<BlockExtended>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash);
|
return this.httpClient.get<BlockExtended>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStrippedBlockTransactions$(hash: string): Observable<TransactionStripped[]> {
|
||||||
|
return this.httpClient.get<TransactionStripped[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary');
|
||||||
|
}
|
||||||
|
|
||||||
getHistoricalHashrate$(interval: string | undefined): Observable<any> {
|
getHistoricalHashrate$(interval: string | undefined): Observable<any> {
|
||||||
return this.httpClient.get<any[]>(
|
return this.httpClient.get<any[]>(
|
||||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +
|
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +
|
||||||
|
Loading…
x
Reference in New Issue
Block a user