diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 04fd6e2ce..63256be9b 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -1,4 +1,4 @@ -import { TransactionExtended } from '../interfaces'; +import { Transaction, TransactionExtended, TransactionStripped } from '../interfaces'; export class Common { static median(numbers: number[]) { @@ -47,4 +47,13 @@ export class Common { }); return matches; } + + static stripTransaction(tx: TransactionExtended): TransactionStripped { + return { + txid: tx.txid, + fee: tx.fee, + weight: tx.weight, + value: tx.vin.reduce((acc, vin) => acc + (vin.prevout ? vin.prevout.value : 0), 0), + }; + } } diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index c8febcd5b..feba09f36 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -1,6 +1,7 @@ const config = require('../../mempool-config.json'); import bitcoinApi from './bitcoin/electrs-api'; import { MempoolInfo, TransactionExtended, Transaction, VbytesPerSecond } from '../interfaces'; +import { Common } from './common'; class Mempool { private inSync: boolean = false; @@ -15,6 +16,7 @@ class Mempool { private vBytesPerSecondArray: VbytesPerSecond[] = []; private vBytesPerSecond: number = 0; private mempoolProtection = 0; + private latestTransactions: any[] = []; constructor() { setInterval(this.updateTxPerSecond.bind(this), 1000); @@ -24,6 +26,10 @@ class Mempool { return this.inSync; } + public getLatestTransactions() { + return this.latestTransactions; + } + public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => void) { this.mempoolChangedCallback = fn; @@ -159,6 +165,9 @@ class Mempool { newMempool = this.mempoolCache; } + const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx)); + this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6); + if (!this.inSync && transactions.length === Object.keys(newMempool).length) { this.inSync = true; console.log('The mempool is now in sync!'); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index e36271f7f..3a2edc466 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -88,6 +88,7 @@ class WebsocketHandler { 'blocks': _blocks.slice(Math.max(_blocks.length - config.INITIAL_BLOCK_AMOUNT, 0)), 'conversions': fiatConversion.getTickers()['BTCUSD'], 'mempool-blocks': mempoolBlocks.getMempoolBlocks(), + 'transactions': memPool.getLatestTransactions(), 'git-commit': backendInfo.gitCommitHash, 'hostname': backendInfo.hostname, ...this.extraInitProperties @@ -150,6 +151,7 @@ class WebsocketHandler { if (client['want-stats']) { response['mempoolInfo'] = mempoolInfo; response['vBytesPerSecond'] = vBytesPerSecond; + response['transactions'] = newTransactions.splice(0, 6).map((tx) => Common.stripTransaction(tx)); } if (client['want-mempool-blocks']) { diff --git a/backend/src/interfaces.ts b/backend/src/interfaces.ts index 019168ae5..159ad868d 100644 --- a/backend/src/interfaces.ts +++ b/backend/src/interfaces.ts @@ -52,6 +52,13 @@ export interface TransactionExtended extends Transaction { firstSeen: number; } +export interface TransactionStripped { + txid: string; + fee: number; + weight: number; + value: number; +} + export interface Vin { txid: string; vout: number; diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 21950b1fd..fefd2cf05 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -28,7 +28,7 @@  Backend is synchronizing -
+
{{ mempoolInfoData.value.vBytesPerSecond | ceil | number }} vB/s
@@ -36,11 +36,11 @@
-
+
Difficulty adjustment
-
+
+{{ epochData.change | number: '1.0-2' }}%
@@ -48,10 +48,67 @@
+
+
+
+
Latest blocks
+ + + + + + + + + + + + + + + +
HeightMinedTxsFilled
{{ block.height }} ago{{ block.tx_count | number }} +
+
+
{{ block.size | bytes: 2 }}
+
+
+ +
+
+
+
+
+
+
Latest transactions
+ + + + + + + + + + + + + +
TXIDAmount (BTC)Fee
{{ transaction.txid | shortenString : 12 }} (){{ transaction.fee / (transaction.weight / 4) | number : '1.1-1' }} sat/vB
+
 
+
+
+
+
+ + +
+ +
\ No newline at end of file diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss index a6d7039ac..31d31feed 100644 --- a/frontend/src/app/dashboard/dashboard.component.scss +++ b/frontend/src/app/dashboard/dashboard.component.scss @@ -26,7 +26,7 @@ .progress { display: inline-flex; - width: 250px; + width: 100%; background-color: #2d3348; height: 1.1rem; } diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 642cfbc45..bfe513c59 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -1,7 +1,8 @@ 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 { map, reduce, scan, tap } from 'rxjs/operators'; +import { Block } from '../interfaces/electrs.interface'; +import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface'; import { StateService } from '../services/state.service'; interface MempoolBlocksData { @@ -32,10 +33,12 @@ interface MempoolInfoData { export class DashboardComponent implements OnInit { network$: Observable; mempoolBlocksData$: Observable; - latestBlockHeight$: Observable; mempoolInfoData$: Observable; difficultyEpoch$: Observable; vBytesPerSecondLimit = 1667; + blocks$: Observable; + transactions$: Observable; + latestBlockHeight: number; constructor( private stateService: StateService, @@ -113,5 +116,30 @@ export class DashboardComponent implements OnInit { }; }) ); + + this.blocks$ = this.stateService.blocks$ + .pipe( + tap(([block]) => { + this.latestBlockHeight = block.height; + }), + scan((acc, [block]) => { + acc.unshift(block); + return acc; + }, []), + map((blocks) => blocks.slice(0, 6)), + ); + + this.transactions$ = this.stateService.transactions$ + .pipe( + scan((acc, tx) => { + acc.unshift(tx); + return acc; + }, []), + map((txs) => txs.slice(0, 6)), + ); + } + + trackByBlock(index: number, block: Block) { + return block.height; } } diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index f0190c318..bc40e00e1 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -13,6 +13,7 @@ export interface WebsocketResponse { data?: string[]; tx?: Transaction; rbfTransaction?: Transaction; + transactions?: TransactionStripped[]; 'track-tx'?: string; 'track-address'?: string; 'track-asset'?: string; @@ -33,3 +34,11 @@ export interface MempoolInfo { size: number; bytes: number; } + +export interface TransactionStripped { + txid: string; + fee: number; + weight: number; + value: number; +} + diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 39537797c..7c28ca773 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent } from 'rxjs'; import { Block, Transaction } from '../interfaces/electrs.interface'; -import { MempoolBlock, MempoolInfo } from '../interfaces/websocket.interface'; +import { MempoolBlock, MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface'; import { OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; import { env } from '../app.constants'; @@ -22,6 +22,7 @@ export class StateService { networkChanged$ = new ReplaySubject(1); blocks$ = new ReplaySubject<[Block, boolean]>(env.KEEP_BLOCKS_AMOUNT); + transactions$ = new ReplaySubject(6); conversions$ = new ReplaySubject(1); bsqPrice$ = new ReplaySubject(1); mempoolInfo$ = new ReplaySubject(1); diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 5e926f59d..a85efcc84 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -101,6 +101,10 @@ export class WebsocketService { this.stateService.mempoolBlocks$.next(response['mempool-blocks']); } + if (response.transactions) { + response.transactions.forEach((tx) => this.stateService.transactions$.next(tx)); + } + if (response['bsq-price']) { this.stateService.bsqPrice$.next(response['bsq-price']); }