diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts
index 0b158efe2..971f00514 100644
--- a/backend/src/api/blocks.ts
+++ b/backend/src/api/blocks.ts
@@ -7,6 +7,7 @@ import { Common } from './common';
class Blocks {
private blocks: Block[] = [];
private currentBlockHeight = 0;
+ private lastDifficultyAdjustmentTime = 0;
private newBlockCallback: ((block: Block, txIds: string[], transactions: TransactionExtended[]) => void) | undefined;
constructor() { }
@@ -38,6 +39,13 @@ class Blocks {
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) {
if (this.currentBlockHeight === 0) {
this.currentBlockHeight = blockHeightTip;
@@ -78,6 +86,10 @@ class Blocks {
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];
+ if (block.height % 2016 === 0) {
+ this.lastDifficultyAdjustmentTime = block.timestamp;
+ }
+
this.blocks.push(block);
if (this.blocks.length > config.KEEP_BLOCK_AMOUNT) {
this.blocks.shift();
@@ -93,6 +105,10 @@ class Blocks {
}
}
+ public getLastDifficultyAdjustmentTime(): number {
+ return this.lastDifficultyAdjustmentTime;
+ }
+
private stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo {
return {
vin: [{
diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts
index a20e075f5..e36271f7f 100644
--- a/backend/src/api/websocket-handler.ts
+++ b/backend/src/api/websocket-handler.ts
@@ -84,6 +84,7 @@ class WebsocketHandler {
client.send(JSON.stringify({
'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(),
+ 'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
'blocks': _blocks.slice(Math.max(_blocks.length - config.INITIAL_BLOCK_AMOUNT, 0)),
'conversions': fiatConversion.getTickers()['BTCUSD'],
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
@@ -270,6 +271,7 @@ class WebsocketHandler {
const response = {
'block': block,
'mempoolInfo': memPool.getMempoolInfo(),
+ 'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
};
if (mBlocks && client['want-mempool-blocks']) {
diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts
index b401bbd4f..77c397ed0 100644
--- a/frontend/src/app/app-routing.module.ts
+++ b/frontend/src/app/app-routing.module.ts
@@ -9,10 +9,10 @@ import { AboutComponent } from './components/about/about.component';
import { TelevisionComponent } from './components/television/television.component';
import { StatisticsComponent } from './components/statistics/statistics.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 { AssetsComponent } from './assets/assets.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
+import { DashboardComponent } from './dashboard/dashboard.component';
const routes: Routes = [
{
@@ -25,7 +25,7 @@ const routes: Routes = [
children: [
{
path: '',
- component: LatestBlocksComponent
+ component: DashboardComponent,
},
{
path: 'tx/:id',
@@ -69,7 +69,7 @@ const routes: Routes = [
children: [
{
path: '',
- component: LatestBlocksComponent
+ component: DashboardComponent
},
{
path: 'tx/:id',
@@ -134,7 +134,7 @@ const routes: Routes = [
children: [
{
path: '',
- component: LatestBlocksComponent
+ component: DashboardComponent
},
{
path: 'tx/:id',
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index 8f8afd176..fe5952f4f 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -16,7 +16,6 @@ import { StateService } from './services/state.service';
import { BlockComponent } from './components/block/block.component';
import { AddressComponent } from './components/address/address.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 { AddressLabelsComponent } from './components/address-labels/address-labels.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 { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
+import { DashboardComponent } from './dashboard/dashboard.component';
@NgModule({
declarations: [
@@ -58,7 +58,6 @@ import { FeesBoxComponent } from './components/fees-box/fees-box.component';
AddressComponent,
AmountComponent,
SearchFormComponent,
- LatestBlocksComponent,
TimespanComponent,
AddressLabelsComponent,
MempoolBlocksComponent,
@@ -72,6 +71,7 @@ import { FeesBoxComponent } from './components/fees-box/fees-box.component';
MinerComponent,
StatusViewComponent,
FeesBoxComponent,
+ DashboardComponent,
],
imports: [
BrowserModule,
diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts
index a152437aa..bb596e6f8 100644
--- a/frontend/src/app/components/address/address.component.ts
+++ b/frontend/src/app/components/address/address.component.ts
@@ -48,7 +48,7 @@ export class AddressComponent implements OnInit, OnDestroy {
ngOnInit() {
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
.pipe(
diff --git a/frontend/src/app/components/asset/asset.component.ts b/frontend/src/app/components/asset/asset.component.ts
index eea87a859..f13d72b18 100644
--- a/frontend/src/app/components/asset/asset.component.ts
+++ b/frontend/src/app/components/asset/asset.component.ts
@@ -53,7 +53,7 @@ export class AssetComponent implements OnInit, OnDestroy {
) { }
ngOnInit() {
- this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
+ this.websocketService.want(['blocks', 'mempool-blocks']);
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.mainSubscription = this.route.paramMap
diff --git a/frontend/src/app/components/latest-blocks/latest-blocks.component.html b/frontend/src/app/components/latest-blocks/latest-blocks.component.html
deleted file mode 100644
index 9a3108617..000000000
--- a/frontend/src/app/components/latest-blocks/latest-blocks.component.html
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
- Height |
- Timestamp |
- Mined |
- Transactions |
- Filled |
-
-
-
- {{ block.height }} |
- {{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} |
- ago |
- {{ block.tx_count | number }} |
-
-
-
- {{ block.size | bytes: 2 }}
-
- |
-
-
-
- |
- |
- |
- |
- |
-
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/src/app/components/latest-blocks/latest-blocks.component.scss b/frontend/src/app/components/latest-blocks/latest-blocks.component.scss
deleted file mode 100644
index 0f2246c99..000000000
--- a/frontend/src/app/components/latest-blocks/latest-blocks.component.scss
+++ /dev/null
@@ -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;
- }
-}
diff --git a/frontend/src/app/components/latest-blocks/latest-blocks.component.spec.ts b/frontend/src/app/components/latest-blocks/latest-blocks.component.spec.ts
deleted file mode 100644
index e8c5b3b5a..000000000
--- a/frontend/src/app/components/latest-blocks/latest-blocks.component.spec.ts
+++ /dev/null
@@ -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;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ LatestBlocksComponent ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(LatestBlocksComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/frontend/src/app/components/latest-blocks/latest-blocks.component.ts b/frontend/src/app/components/latest-blocks/latest-blocks.component.ts
deleted file mode 100644
index 6cccfb81f..000000000
--- a/frontend/src/app/components/latest-blocks/latest-blocks.component.ts
+++ /dev/null
@@ -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;
-
- 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;
- }
-}
diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html
index eab40d088..b51c5da5b 100644
--- a/frontend/src/app/components/master-page/master-page.component.html
+++ b/frontend/src/app/components/master-page/master-page.component.html
@@ -66,10 +66,4 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/src/app/components/statistics/statistics.component.ts b/frontend/src/app/components/statistics/statistics.component.ts
index dce1c4cf1..d5dc1a587 100644
--- a/frontend/src/app/components/statistics/statistics.component.ts
+++ b/frontend/src/app/components/statistics/statistics.component.ts
@@ -108,10 +108,10 @@ export class StatisticsComponent implements OnInit {
switchMap(() => {
this.spinnerLoading = true;
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$();
}
- this.websocketService.want(['blocks', 'stats']);
+ this.websocketService.want(['blocks']);
if (this.radioGroupForm.controls.dateSpan.value === '24h') {
return this.apiService.list24HStatistics$();
}
diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html
new file mode 100644
index 000000000..133b7b798
--- /dev/null
+++ b/frontend/src/app/dashboard/dashboard.component.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
Mempool size
+
{{ mempoolBlocksData.size | bytes }} ({{ mempoolBlocksData.blocks }} blocks)
+
+
+
+
+
+
+
Unconfirmed transactions
+
{{ mempoolInfoData.memPoolInfo.size | number }}
+
+
+
+
+
+
+
Tx weight per second
+
+ Backend is synchronizing
+
+
+
+
{{ mempoolInfoData.vBytesPerSecond | ceil | number }} vB/s
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss
new file mode 100644
index 000000000..efd762c7a
--- /dev/null
+++ b/frontend/src/app/dashboard/dashboard.component.scss
@@ -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;
+}
diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts
new file mode 100644
index 000000000..12fb8153e
--- /dev/null
+++ b/frontend/src/app/dashboard/dashboard.component.ts
@@ -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;
+ mempoolBlocksData$: Observable;
+ latestBlockHeight$: Observable;
+ mempoolInfoData$: Observable;
+ difficultyEpoch$: Observable;
+ 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)
+ };
+ })
+ );
+ }
+}
diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts
index 9da76da0f..f0190c318 100644
--- a/frontend/src/app/interfaces/websocket.interface.ts
+++ b/frontend/src/app/interfaces/websocket.interface.ts
@@ -8,6 +8,7 @@ export interface WebsocketResponse {
historicalDate?: string;
mempoolInfo?: MempoolInfo;
vBytesPerSecond?: number;
+ lastDifficultyAdjustment?: number;
action?: string;
data?: string[];
tx?: Transaction;
@@ -31,8 +32,4 @@ export interface MempoolBlock {
export interface MempoolInfo {
size: number;
bytes: number;
- usage?: number;
- maxmempool?: number;
- mempoolminfee?: number;
- minrelaytxfee?: number;
}
diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts
index 9f6af5772..39537797c 100644
--- a/frontend/src/app/services/state.service.ts
+++ b/frontend/src/app/services/state.service.ts
@@ -31,6 +31,7 @@ export class StateService {
blockTransactions$ = new Subject();
isLoadingWebSocket$ = new ReplaySubject(1);
vbytesPerSecond$ = new ReplaySubject(1);
+ lastDifficultyAdjustment$ = new ReplaySubject(1);
gitCommit$ = new ReplaySubject(1);
live2Chart$ = new Subject();
diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts
index 997db4ee4..5e926f59d 100644
--- a/frontend/src/app/services/websocket.service.ts
+++ b/frontend/src/app/services/websocket.service.ts
@@ -141,6 +141,10 @@ export class WebsocketService {
this.stateService.vbytesPerSecond$.next(response.vBytesPerSecond);
}
+ if (response.lastDifficultyAdjustment !== undefined) {
+ this.stateService.lastDifficultyAdjustment$.next(response.lastDifficultyAdjustment);
+ }
+
if (response['git-commit']) {
this.stateService.gitCommit$.next(response['git-commit']);
}