Replacing footer and latest blocks with a stats dashboard.
This commit is contained in:
parent
8146939f0f
commit
6c1d28a9ac
@ -7,6 +7,7 @@ import { Common } from './common';
|
|||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: Block[] = [];
|
private blocks: Block[] = [];
|
||||||
private currentBlockHeight = 0;
|
private currentBlockHeight = 0;
|
||||||
|
private lastDifficultyAdjustmentTime = 0;
|
||||||
private newBlockCallback: ((block: Block, txIds: string[], transactions: TransactionExtended[]) => void) | undefined;
|
private newBlockCallback: ((block: Block, txIds: string[], transactions: TransactionExtended[]) => void) | undefined;
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -38,6 +39,13 @@ class Blocks {
|
|||||||
this.currentBlockHeight = blockHeightTip - config.INITIAL_BLOCK_AMOUNT;
|
this.currentBlockHeight = blockHeightTip - config.INITIAL_BLOCK_AMOUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.lastDifficultyAdjustmentTime) {
|
||||||
|
const heightDiff = blockHeightTip % 2016;
|
||||||
|
const blockHash = await bitcoinApi.getBlockHash(blockHeightTip - heightDiff);
|
||||||
|
const block = await bitcoinApi.getBlock(blockHash);
|
||||||
|
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
while (this.currentBlockHeight < blockHeightTip) {
|
while (this.currentBlockHeight < blockHeightTip) {
|
||||||
if (this.currentBlockHeight === 0) {
|
if (this.currentBlockHeight === 0) {
|
||||||
this.currentBlockHeight = blockHeightTip;
|
this.currentBlockHeight = blockHeightTip;
|
||||||
@ -78,6 +86,10 @@ class Blocks {
|
|||||||
block.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0;
|
block.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0;
|
||||||
block.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions.slice(0, transactions.length - 1), 8) : [0, 0];
|
block.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions.slice(0, transactions.length - 1), 8) : [0, 0];
|
||||||
|
|
||||||
|
if (block.height % 2016 === 0) {
|
||||||
|
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
this.blocks.push(block);
|
this.blocks.push(block);
|
||||||
if (this.blocks.length > config.KEEP_BLOCK_AMOUNT) {
|
if (this.blocks.length > config.KEEP_BLOCK_AMOUNT) {
|
||||||
this.blocks.shift();
|
this.blocks.shift();
|
||||||
@ -93,6 +105,10 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getLastDifficultyAdjustmentTime(): number {
|
||||||
|
return this.lastDifficultyAdjustmentTime;
|
||||||
|
}
|
||||||
|
|
||||||
private stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo {
|
private stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo {
|
||||||
return {
|
return {
|
||||||
vin: [{
|
vin: [{
|
||||||
|
@ -84,6 +84,7 @@ class WebsocketHandler {
|
|||||||
client.send(JSON.stringify({
|
client.send(JSON.stringify({
|
||||||
'mempoolInfo': memPool.getMempoolInfo(),
|
'mempoolInfo': memPool.getMempoolInfo(),
|
||||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||||
|
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
|
||||||
'blocks': _blocks.slice(Math.max(_blocks.length - config.INITIAL_BLOCK_AMOUNT, 0)),
|
'blocks': _blocks.slice(Math.max(_blocks.length - config.INITIAL_BLOCK_AMOUNT, 0)),
|
||||||
'conversions': fiatConversion.getTickers()['BTCUSD'],
|
'conversions': fiatConversion.getTickers()['BTCUSD'],
|
||||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
||||||
@ -270,6 +271,7 @@ class WebsocketHandler {
|
|||||||
const response = {
|
const response = {
|
||||||
'block': block,
|
'block': block,
|
||||||
'mempoolInfo': memPool.getMempoolInfo(),
|
'mempoolInfo': memPool.getMempoolInfo(),
|
||||||
|
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mBlocks && client['want-mempool-blocks']) {
|
if (mBlocks && client['want-mempool-blocks']) {
|
||||||
|
@ -9,10 +9,10 @@ import { AboutComponent } from './components/about/about.component';
|
|||||||
import { TelevisionComponent } from './components/television/television.component';
|
import { TelevisionComponent } from './components/television/television.component';
|
||||||
import { StatisticsComponent } from './components/statistics/statistics.component';
|
import { StatisticsComponent } from './components/statistics/statistics.component';
|
||||||
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
|
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
|
||||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
|
|
||||||
import { AssetComponent } from './components/asset/asset.component';
|
import { AssetComponent } from './components/asset/asset.component';
|
||||||
import { AssetsComponent } from './assets/assets.component';
|
import { AssetsComponent } from './assets/assets.component';
|
||||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||||
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -25,7 +25,7 @@ const routes: Routes = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: LatestBlocksComponent
|
component: DashboardComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tx/:id',
|
path: 'tx/:id',
|
||||||
@ -69,7 +69,7 @@ const routes: Routes = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: LatestBlocksComponent
|
component: DashboardComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tx/:id',
|
path: 'tx/:id',
|
||||||
@ -134,7 +134,7 @@ const routes: Routes = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: LatestBlocksComponent
|
component: DashboardComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tx/:id',
|
path: 'tx/:id',
|
||||||
|
@ -16,7 +16,6 @@ import { StateService } from './services/state.service';
|
|||||||
import { BlockComponent } from './components/block/block.component';
|
import { BlockComponent } from './components/block/block.component';
|
||||||
import { AddressComponent } from './components/address/address.component';
|
import { AddressComponent } from './components/address/address.component';
|
||||||
import { SearchFormComponent } from './components/search-form/search-form.component';
|
import { SearchFormComponent } from './components/search-form/search-form.component';
|
||||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
|
|
||||||
import { WebsocketService } from './services/websocket.service';
|
import { WebsocketService } from './services/websocket.service';
|
||||||
import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
|
import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
|
||||||
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
|
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
|
||||||
@ -41,6 +40,7 @@ import { MinerComponent } from './components/miner/miner.component';
|
|||||||
import { SharedModule } from './shared/shared.module';
|
import { SharedModule } from './shared/shared.module';
|
||||||
import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
|
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
|
||||||
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -58,7 +58,6 @@ import { FeesBoxComponent } from './components/fees-box/fees-box.component';
|
|||||||
AddressComponent,
|
AddressComponent,
|
||||||
AmountComponent,
|
AmountComponent,
|
||||||
SearchFormComponent,
|
SearchFormComponent,
|
||||||
LatestBlocksComponent,
|
|
||||||
TimespanComponent,
|
TimespanComponent,
|
||||||
AddressLabelsComponent,
|
AddressLabelsComponent,
|
||||||
MempoolBlocksComponent,
|
MempoolBlocksComponent,
|
||||||
@ -72,6 +71,7 @@ import { FeesBoxComponent } from './components/fees-box/fees-box.component';
|
|||||||
MinerComponent,
|
MinerComponent,
|
||||||
StatusViewComponent,
|
StatusViewComponent,
|
||||||
FeesBoxComponent,
|
FeesBoxComponent,
|
||||||
|
DashboardComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@ -48,7 +48,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
|
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||||
|
|
||||||
this.mainSubscription = this.route.paramMap
|
this.mainSubscription = this.route.paramMap
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@ -53,7 +53,7 @@ export class AssetComponent implements OnInit, OnDestroy {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
|
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||||
|
|
||||||
this.mainSubscription = this.route.paramMap
|
this.mainSubscription = this.route.paramMap
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
<app-fees-box *ngIf="(network$ | async) === ''" class="d-block mr-2 ml-2 mb-4"></app-fees-box>
|
|
||||||
|
|
||||||
<div class="container-xl">
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<table class="table table-borderless" [alwaysCallback]="true" [fromRoot]="true" [infiniteScrollContainer]="'body'" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
|
|
||||||
<thead>
|
|
||||||
<th style="width: 15%;">Height</th>
|
|
||||||
<th class="d-none d-md-block" style="width: 20%;">Timestamp</th>
|
|
||||||
<th style="width: 20%;">Mined</th>
|
|
||||||
<th class="d-none d-lg-block" style="width: 15%;">Transactions</th>
|
|
||||||
<th style="width: 20%;">Filled</th>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
|
|
||||||
<td><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
|
||||||
<td class="d-none d-md-block">{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
|
|
||||||
<td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> ago</td>
|
|
||||||
<td class="d-none d-lg-block">{{ block.tx_count | number }}</td>
|
|
||||||
<td>
|
|
||||||
<div class="progress position-relative">
|
|
||||||
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / 4000000)*100 + '%' }"></div>
|
|
||||||
<div class="progress-text">{{ block.size | bytes: 2 }}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<ng-template [ngIf]="isLoading">
|
|
||||||
<tr *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
|
|
||||||
<td><span class="skeleton-loader"></span></td>
|
|
||||||
<td class="d-none d-md-block"><span class="skeleton-loader"></span></td>
|
|
||||||
<td><span class="skeleton-loader"></span></td>
|
|
||||||
<td class="d-none d-lg-block"><span class="skeleton-loader"></span></td>
|
|
||||||
<td><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
@ -1,14 +0,0 @@
|
|||||||
.progress {
|
|
||||||
background-color: #2d3348;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.d-md-block {
|
|
||||||
display: table-cell !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.d-lg-block {
|
|
||||||
display: table-cell !important;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { LatestBlocksComponent } from './latest-blocks.component';
|
|
||||||
|
|
||||||
describe('LatestBlocksComponent', () => {
|
|
||||||
let component: LatestBlocksComponent;
|
|
||||||
let fixture: ComponentFixture<LatestBlocksComponent>;
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ LatestBlocksComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(LatestBlocksComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,111 +0,0 @@
|
|||||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
|
||||||
import { StateService } from '../../services/state.service';
|
|
||||||
import { Block } from '../../interfaces/electrs.interface';
|
|
||||||
import { Subscription, Observable, merge, of } from 'rxjs';
|
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-latest-blocks',
|
|
||||||
templateUrl: './latest-blocks.component.html',
|
|
||||||
styleUrls: ['./latest-blocks.component.scss'],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class LatestBlocksComponent implements OnInit, OnDestroy {
|
|
||||||
network$: Observable<string>;
|
|
||||||
|
|
||||||
blocks: any[] = [];
|
|
||||||
blockSubscription: Subscription;
|
|
||||||
isLoading = true;
|
|
||||||
interval: any;
|
|
||||||
|
|
||||||
latestBlockHeight: number;
|
|
||||||
|
|
||||||
heightOfPageUntilBlocks = 430;
|
|
||||||
heightOfBlocksTableChunk = 470;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private electrsApiService: ElectrsApiService,
|
|
||||||
private stateService: StateService,
|
|
||||||
private seoService: SeoService,
|
|
||||||
private cd: ChangeDetectorRef,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.seoService.resetTitle();
|
|
||||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
|
||||||
|
|
||||||
this.blockSubscription = this.stateService.blocks$
|
|
||||||
.subscribe(([block]) => {
|
|
||||||
if (block === null || !this.blocks.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.latestBlockHeight = block.height;
|
|
||||||
|
|
||||||
if (block.height === this.blocks[0].height) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are out of sync, reload the blocks instead
|
|
||||||
if (block.height > this.blocks[0].height + 1) {
|
|
||||||
this.loadInitialBlocks();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (block.height <= this.blocks[0].height) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.blocks.pop();
|
|
||||||
this.blocks.unshift(block);
|
|
||||||
this.cd.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loadInitialBlocks();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
clearInterval(this.interval);
|
|
||||||
this.blockSubscription.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadInitialBlocks() {
|
|
||||||
this.electrsApiService.listBlocks$()
|
|
||||||
.subscribe((blocks) => {
|
|
||||||
this.blocks = blocks;
|
|
||||||
this.isLoading = false;
|
|
||||||
|
|
||||||
this.latestBlockHeight = blocks[0].height;
|
|
||||||
|
|
||||||
const spaceForBlocks = window.innerHeight - this.heightOfPageUntilBlocks;
|
|
||||||
const chunks = Math.ceil(spaceForBlocks / this.heightOfBlocksTableChunk) - 1;
|
|
||||||
if (chunks > 0) {
|
|
||||||
this.loadMore(chunks);
|
|
||||||
}
|
|
||||||
this.cd.markForCheck();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMore(chunks = 0) {
|
|
||||||
if (this.isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.isLoading = true;
|
|
||||||
this.electrsApiService.listBlocks$(this.blocks[this.blocks.length - 1].height - 1)
|
|
||||||
.subscribe((blocks) => {
|
|
||||||
this.blocks = this.blocks.concat(blocks);
|
|
||||||
this.isLoading = false;
|
|
||||||
|
|
||||||
const chunksLeft = chunks - 1;
|
|
||||||
if (chunksLeft > 0) {
|
|
||||||
this.loadMore(chunksLeft);
|
|
||||||
}
|
|
||||||
this.cd.markForCheck();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
trackByBlock(index: number, block: Block) {
|
|
||||||
return block.height;
|
|
||||||
}
|
|
||||||
}
|
|
@ -66,10 +66,4 @@
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<ng-container *ngIf="network.val !== 'bisq'">
|
|
||||||
<br><br>
|
|
||||||
|
|
||||||
<app-footer></app-footer>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
</ng-container>
|
|
@ -108,10 +108,10 @@ export class StatisticsComponent implements OnInit {
|
|||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
this.spinnerLoading = true;
|
this.spinnerLoading = true;
|
||||||
if (this.radioGroupForm.controls.dateSpan.value === '2h') {
|
if (this.radioGroupForm.controls.dateSpan.value === '2h') {
|
||||||
this.websocketService.want(['blocks', 'stats', 'live-2h-chart']);
|
this.websocketService.want(['blocks', 'live-2h-chart']);
|
||||||
return this.apiService.list2HStatistics$();
|
return this.apiService.list2HStatistics$();
|
||||||
}
|
}
|
||||||
this.websocketService.want(['blocks', 'stats']);
|
this.websocketService.want(['blocks']);
|
||||||
if (this.radioGroupForm.controls.dateSpan.value === '24h') {
|
if (this.radioGroupForm.controls.dateSpan.value === '24h') {
|
||||||
return this.apiService.list24HStatistics$();
|
return this.apiService.list24HStatistics$();
|
||||||
}
|
}
|
||||||
|
51
frontend/src/app/dashboard/dashboard.component.html
Normal file
51
frontend/src/app/dashboard/dashboard.component.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<app-fees-box *ngIf="(network$ | async) === ''" class="d-block mr-2 ml-2 mb-5"></app-fees-box>
|
||||||
|
|
||||||
|
<div class="container-xl">
|
||||||
|
|
||||||
|
<div class="row row-cols-1 row-cols-md-2" *ngIf="mempoolInfoData$ | async as mempoolInfoData">
|
||||||
|
<div class="col mb-4">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mempoolSize">Mempool size</h5>
|
||||||
|
<p class="card-text" *ngIf="(mempoolBlocksData$ | async) as mempoolBlocksData">{{ mempoolBlocksData.size | bytes }} ({{ mempoolBlocksData.blocks }} block<span [hidden]="mempoolBlocksData.blocks <= 1">s</span>)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col mb-4">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title unconfirmedTx">Unconfirmed transactions</h5>
|
||||||
|
<p class="card-text">{{ mempoolInfoData.memPoolInfo.size | number }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col mb-4">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title txWeightPerSecond">Tx weight per second</h5>
|
||||||
|
<span *ngIf="mempoolInfoData.vBytesPerSecond === 0; else inSync">
|
||||||
|
<span class="badge badge-pill badge-warning">Backend is synchronizing</span>
|
||||||
|
</span>
|
||||||
|
<ng-template #inSync>
|
||||||
|
<div class="progress sub-text">
|
||||||
|
<div class="progress-bar {{ mempoolInfoData.progressClass }}" style="padding: 4px;" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth}">{{ mempoolInfoData.vBytesPerSecond | ceil | number }} vB/s</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col mb-4">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title txPerSecond">Difficulty Epoch</h5>
|
||||||
|
<div class="progress" *ngIf="(difficultyEpoch$ | async) as epochData">
|
||||||
|
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}"></div>
|
||||||
|
<div class="progress-bar bg-success" role="progressbar" style="width: 0%" [ngStyle]="{'width': epochData.green}"></div>
|
||||||
|
<div class="progress-bar bg-danger" role="progressbar" style="width: 1%; background-color: #f14d80;" [ngStyle]="{'width': epochData.red}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
36
frontend/src/app/dashboard/dashboard.component.scss
Normal file
36
frontend/src/app/dashboard/dashboard.component.scss
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
.card {
|
||||||
|
background-color: #1d1f31;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txWeightPerSecond {
|
||||||
|
color: #4a9ff4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mempoolSize {
|
||||||
|
color: #4a68b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txPerSecond {
|
||||||
|
color: #f4bb4a;;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unconfirmedTx {
|
||||||
|
color: #f14d80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-block {
|
||||||
|
float: left;
|
||||||
|
width: 350px;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 250px;
|
||||||
|
background-color: #2d3348;
|
||||||
|
height: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-warning {
|
||||||
|
background-color: #b58800 !important;
|
||||||
|
}
|
114
frontend/src/app/dashboard/dashboard.component.ts
Normal file
114
frontend/src/app/dashboard/dashboard.component.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
|
import { combineLatest, merge, Observable, of } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { MempoolInfo } from '../interfaces/websocket.interface';
|
||||||
|
import { StateService } from '../services/state.service';
|
||||||
|
|
||||||
|
interface MempoolBlocksData {
|
||||||
|
blocks: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EpochProgress {
|
||||||
|
base: string;
|
||||||
|
green: string;
|
||||||
|
red: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MempoolInfoData {
|
||||||
|
memPoolInfo: MempoolInfo;
|
||||||
|
vBytesPerSecond: number;
|
||||||
|
progressWidth: string;
|
||||||
|
progressClass: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dashboard',
|
||||||
|
templateUrl: './dashboard.component.html',
|
||||||
|
styleUrls: ['./dashboard.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class DashboardComponent implements OnInit {
|
||||||
|
network$: Observable<string>;
|
||||||
|
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||||
|
latestBlockHeight$: Observable<number>;
|
||||||
|
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||||
|
difficultyEpoch$: Observable<EpochProgress>;
|
||||||
|
vBytesPerSecondLimit = 1667;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private stateService: StateService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||||
|
|
||||||
|
this.mempoolInfoData$ = combineLatest([
|
||||||
|
this.stateService.mempoolInfo$,
|
||||||
|
this.stateService.vbytesPerSecond$
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
map(([mempoolInfo, vbytesPerSecond]) => {
|
||||||
|
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
|
||||||
|
|
||||||
|
let progressClass = 'bg-danger';
|
||||||
|
if (percent <= 75) {
|
||||||
|
progressClass = 'bg-success';
|
||||||
|
} else if (percent <= 99) {
|
||||||
|
progressClass = 'bg-warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
memPoolInfo: mempoolInfo,
|
||||||
|
vBytesPerSecond: vbytesPerSecond,
|
||||||
|
progressWidth: percent + '%',
|
||||||
|
progressClass: progressClass,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.difficultyEpoch$ = combineLatest([
|
||||||
|
this.stateService.blocks$.pipe(map(([block]) => block)),
|
||||||
|
this.stateService.lastDifficultyAdjustment$
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
map(([block, DATime]) => {
|
||||||
|
const now = new Date().getTime() / 1000;
|
||||||
|
const diff = now - DATime;
|
||||||
|
const blocksInEpoch = block.height % 2016;
|
||||||
|
const estimatedBlocks = Math.round(diff / 60 / 10);
|
||||||
|
|
||||||
|
let base = 0;
|
||||||
|
let green = 0;
|
||||||
|
let red = 0;
|
||||||
|
|
||||||
|
if (blocksInEpoch >= estimatedBlocks) {
|
||||||
|
base = estimatedBlocks / 2016 * 100;
|
||||||
|
green = (blocksInEpoch - estimatedBlocks) / 2016 * 100;
|
||||||
|
} else {
|
||||||
|
base = blocksInEpoch / 2016 * 100;
|
||||||
|
red = (estimatedBlocks - blocksInEpoch) / 2016 * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
base: base + '%',
|
||||||
|
green: green + '%',
|
||||||
|
red: red + '%',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
|
||||||
|
.pipe(
|
||||||
|
map((mempoolBlocks) => {
|
||||||
|
const size = mempoolBlocks.map((m) => m.blockSize).reduce((a, b) => a + b, 0);
|
||||||
|
const vsize = mempoolBlocks.map((m) => m.blockVSize).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
size: size,
|
||||||
|
blocks: Math.ceil(vsize / 1000000)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ export interface WebsocketResponse {
|
|||||||
historicalDate?: string;
|
historicalDate?: string;
|
||||||
mempoolInfo?: MempoolInfo;
|
mempoolInfo?: MempoolInfo;
|
||||||
vBytesPerSecond?: number;
|
vBytesPerSecond?: number;
|
||||||
|
lastDifficultyAdjustment?: number;
|
||||||
action?: string;
|
action?: string;
|
||||||
data?: string[];
|
data?: string[];
|
||||||
tx?: Transaction;
|
tx?: Transaction;
|
||||||
@ -31,8 +32,4 @@ export interface MempoolBlock {
|
|||||||
export interface MempoolInfo {
|
export interface MempoolInfo {
|
||||||
size: number;
|
size: number;
|
||||||
bytes: number;
|
bytes: number;
|
||||||
usage?: number;
|
|
||||||
maxmempool?: number;
|
|
||||||
mempoolminfee?: number;
|
|
||||||
minrelaytxfee?: number;
|
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ export class StateService {
|
|||||||
blockTransactions$ = new Subject<Transaction>();
|
blockTransactions$ = new Subject<Transaction>();
|
||||||
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
|
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
|
||||||
vbytesPerSecond$ = new ReplaySubject<number>(1);
|
vbytesPerSecond$ = new ReplaySubject<number>(1);
|
||||||
|
lastDifficultyAdjustment$ = new ReplaySubject<number>(1);
|
||||||
gitCommit$ = new ReplaySubject<string>(1);
|
gitCommit$ = new ReplaySubject<string>(1);
|
||||||
|
|
||||||
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
||||||
|
@ -141,6 +141,10 @@ export class WebsocketService {
|
|||||||
this.stateService.vbytesPerSecond$.next(response.vBytesPerSecond);
|
this.stateService.vbytesPerSecond$.next(response.vBytesPerSecond);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.lastDifficultyAdjustment !== undefined) {
|
||||||
|
this.stateService.lastDifficultyAdjustment$.next(response.lastDifficultyAdjustment);
|
||||||
|
}
|
||||||
|
|
||||||
if (response['git-commit']) {
|
if (response['git-commit']) {
|
||||||
this.stateService.gitCommit$.next(response['git-commit']);
|
this.stateService.gitCommit$.next(response['git-commit']);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user