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 @@ - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
HeightTimestampMinedTransactionsFilled
{{ 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
+
+
+
+
+
+
+
+
+
Difficulty Epoch
+
+
+
+
+
+
+
+
+
+ +
\ 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']); }