diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index a3714406f..367ba1c0e 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -6,6 +6,7 @@ import { Common } from "./common"; interface RbfTransaction extends TransactionStripped { rbf?: boolean; mined?: boolean; + fullRbf?: boolean; } interface RbfTree { @@ -17,6 +18,16 @@ interface RbfTree { replaces: RbfTree[]; } +export interface ReplacementInfo { + mined: boolean; + fullRbf: boolean; + txid: string; + oldFee: number; + oldVsize: number; + newFee: number; + newVsize: number; +} + class RbfCache { private replacedBy: Map = new Map(); private replaces: Map = new Map(); @@ -41,11 +52,15 @@ class RbfCache { this.txs.set(newTx.txid, newTxExtended); // maintain rbf trees - let fullRbf = false; + let txFullRbf = false; + let treeFullRbf = false; const replacedTrees: RbfTree[] = []; for (const replacedTxExtended of replaced) { const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction; replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe); + if (!replacedTx.rbf) { + txFullRbf = true; + } this.replacedBy.set(replacedTx.txid, newTx.txid); if (this.treeMap.has(replacedTx.txid)) { const treeId = this.treeMap.get(replacedTx.txid); @@ -55,7 +70,7 @@ class RbfCache { if (tree) { tree.interval = newTime - tree?.time; replacedTrees.push(tree); - fullRbf = fullRbf || tree.fullRbf || !tree.tx.rbf; + treeFullRbf = treeFullRbf || tree.fullRbf || !tree.tx.rbf; } } } else { @@ -67,15 +82,16 @@ class RbfCache { fullRbf: !replacedTx.rbf, replaces: [], }); - fullRbf = fullRbf || !replacedTx.rbf; + treeFullRbf = treeFullRbf || !replacedTx.rbf; this.txs.set(replacedTx.txid, replacedTxExtended); } } + newTx.fullRbf = txFullRbf; const treeId = replacedTrees[0].tx.txid; const newTree = { tx: newTx, time: newTime, - fullRbf, + fullRbf: treeFullRbf, replaces: replacedTrees }; this.rbfTrees.set(treeId, newTree); @@ -349,6 +365,27 @@ class RbfCache { } return tree; } + + public getLatestRbfSummary(): ReplacementInfo[] { + const rbfList = this.getRbfTrees(false); + return rbfList.slice(0, 6).map(rbfTree => { + let oldFee = 0; + let oldVsize = 0; + for (const replaced of rbfTree.replaces) { + oldFee += replaced.tx.fee; + oldVsize += replaced.tx.vsize; + } + return { + txid: rbfTree.tx.txid, + mined: !!rbfTree.tx.mined, + fullRbf: !!rbfTree.tx.fullRbf, + oldFee, + oldVsize, + newFee: rbfTree.tx.fee, + newVsize: rbfTree.tx.vsize, + }; + }); + } } export default new RbfCache(); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index f91947dcb..48e9106f0 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -12,7 +12,7 @@ import { Common } from './common'; import loadingIndicators from './loading-indicators'; import config from '../config'; import transactionUtils from './transaction-utils'; -import rbfCache from './rbf-cache'; +import rbfCache, { ReplacementInfo } from './rbf-cache'; import difficultyAdjustment from './difficulty-adjustment'; import feeApi from './fee-api'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; @@ -40,6 +40,7 @@ class WebsocketHandler { private socketData: { [key: string]: string } = {}; private serializedInitData: string = '{}'; + private lastRbfSummary: ReplacementInfo | null = null; constructor() { } @@ -225,6 +226,15 @@ class WebsocketHandler { } } + if (parsedMessage && parsedMessage['track-rbf-summary'] != null) { + if (parsedMessage['track-rbf-summary']) { + client['track-rbf-summary'] = true; + response['rbfLatestSummary'] = this.socketData['rbfSummary']; + } else { + client['track-rbf-summary'] = false; + } + } + if (parsedMessage.action === 'init') { if (!this.socketData['blocks']?.length || !this.socketData['da']) { this.updateSocketData(); @@ -395,10 +405,13 @@ class WebsocketHandler { const rbfChanges = rbfCache.getRbfChanges(); let rbfReplacements; let fullRbfReplacements; + let rbfSummary; if (Object.keys(rbfChanges.trees).length) { rbfReplacements = rbfCache.getRbfTrees(false); fullRbfReplacements = rbfCache.getRbfTrees(true); + rbfSummary = rbfCache.getLatestRbfSummary(); } + for (const deletedTx of deletedTransactions) { rbfCache.evict(deletedTx.txid); } @@ -409,7 +422,7 @@ class WebsocketHandler { const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx)); // update init data - this.updateSocketDataFields({ + const socketDataFields = { 'mempoolInfo': mempoolInfo, 'vBytesPerSecond': vBytesPerSecond, 'mempool-blocks': mBlocks, @@ -417,7 +430,11 @@ class WebsocketHandler { 'loadingIndicators': loadingIndicators.getLoadingIndicators(), 'da': da?.previousTime ? da : undefined, 'fees': recommendedFees, - }); + }; + if (rbfSummary) { + socketDataFields['rbfSummary'] = rbfSummary; + } + this.updateSocketDataFields(socketDataFields); // cache serialized objects to avoid stringify-ing the same thing for every client const responseCache = { ...this.socketData }; @@ -601,6 +618,10 @@ class WebsocketHandler { response['rbfLatest'] = getCachedResponse('fullrbfLatest', fullRbfReplacements); } + if (client['track-rbf-summary'] && rbfSummary) { + response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary); + } + if (Object.keys(response).length) { const serializedResponse = this.serializeResponse(response); client.send(serializedResponse); diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 620678a28..3faef5a83 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -75,36 +75,31 @@
- -
Latest blocks
+
+
Latest replacements
 
- +
- - - - - + + + + - - - - + - - + + diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss index eb466fc16..f1e835d9c 100644 --- a/frontend/src/app/dashboard/dashboard.component.scss +++ b/frontend/src/app/dashboard/dashboard.component.scss @@ -175,39 +175,43 @@ height: 18px; } -.lastest-blocks-table { +.lastest-replacements-table { width: 100%; text-align: left; + table-layout:fixed; tr, td, th { border: 0px; - padding-top: 0.65rem !important; - padding-bottom: 0.7rem !important; + padding-top: 0.71rem !important; + padding-bottom: 0.75rem !important; } - .table-cell-height { - width: 15%; + td { + overflow:hidden; + width: 25%; } - .table-cell-mined { - width: 35%; - text-align: left; + .table-cell-txid { + width: 25%; + text-align: start; } - .table-cell-transaction-count { - display: none; - text-align: right; - width: 20%; - display: table-cell; - } - .table-cell-size { - display: none; - text-align: center; - width: 30%; - @media (min-width: 485px) { - display: table-cell; - } - @media (min-width: 768px) { + .table-cell-old-fee { + width: 25%; + text-align: end; + + @media(max-width: 1080px) { display: none; } - @media (min-width: 992px) { - display: table-cell; + } + .table-cell-new-fee { + width: 20%; + text-align: end; + } + .table-cell-badges { + width: 23%; + padding-right: 0; + padding-left: 5px; + text-align: end; + + .badge { + margin-left: 5px; } } } diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 6cf487be6..b1bc35eca 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { combineLatest, merge, Observable, of, Subscription } from 'rxjs'; -import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; -import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface'; -import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface'; +import { filter, map, scan, share, switchMap } from 'rxjs/operators'; +import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; +import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface'; import { ApiService } from '../services/api.service'; import { StateService } from '../services/state.service'; import { WebsocketService } from '../services/websocket.service'; @@ -38,8 +38,8 @@ export class DashboardComponent implements OnInit, OnDestroy { mempoolInfoData$: Observable; mempoolLoadingStatus$: Observable; vBytesPerSecondLimit = 1667; - blocks$: Observable; transactions$: Observable; + replacements$: Observable; latestBlockHeight: number; mempoolTransactionsWeightPerSecondData: any; mempoolStats$: Observable; @@ -58,12 +58,14 @@ export class DashboardComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.currencySubscription.unsubscribe(); + this.websocketService.stopTrackRbfSummary(); } ngOnInit(): void { this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$; this.seoService.resetTitle(); this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']); + this.websocketService.startTrackRbfSummary(); this.network$ = merge(of(''), this.stateService.networkChanged$); this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$ .pipe( @@ -130,23 +132,6 @@ export class DashboardComponent implements OnInit, OnDestroy { }), ); - this.blocks$ = this.stateService.blocks$ - .pipe( - tap((blocks) => { - this.latestBlockHeight = blocks[0].height; - }), - switchMap((blocks) => { - if (this.stateService.env.MINING_DASHBOARD === true) { - for (const block of blocks) { - // @ts-ignore: Need to add an extra field for the template - block.extras.pool.logo = `/resources/mining-pools/` + - block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; - } - } - return of(blocks.slice(0, 6)); - }) - ); - this.transactions$ = this.stateService.transactions$ .pipe( scan((acc, tx) => { @@ -159,6 +144,8 @@ export class DashboardComponent implements OnInit, OnDestroy { }, []), ); + this.replacements$ = this.stateService.rbfLatestSummary$; + this.mempoolStats$ = this.stateService.connectionState$ .pipe( filter((state) => state === 2), @@ -219,4 +206,16 @@ export class DashboardComponent implements OnInit, OnDestroy { trackByBlock(index: number, block: BlockExtended) { return block.height; } + + checkFullRbf(tree: RbfTree): void { + let fullRbf = false; + for (const replaced of tree.replaces) { + if (!replaced.tx.rbf) { + fullRbf = true; + } + replaced.replacedBy = tree.tx; + this.checkFullRbf(replaced); + } + tree.tx.fullRbf = fullRbf; + } } diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 20a114c72..991fe2680 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -18,6 +18,7 @@ export interface WebsocketResponse { txReplaced?: ReplacedTransaction; rbfInfo?: RbfTree; rbfLatest?: RbfTree[]; + rbfLatestSummary?: ReplacementInfo[]; utxoSpent?: object; transactions?: TransactionStripped[]; loadingIndicators?: ILoadingIndicators; @@ -29,6 +30,7 @@ export interface WebsocketResponse { 'track-asset'?: string; 'track-mempool-block'?: number; 'track-rbf'?: string; + 'track-rbf-summary'?: boolean; 'watch-mempool'?: boolean; 'track-bisq-market'?: string; 'refresh-blocks'?: boolean; @@ -37,6 +39,16 @@ export interface WebsocketResponse { export interface ReplacedTransaction extends Transaction { txid: string; } + +export interface ReplacementInfo { + mined: boolean; + fullRbf: boolean; + txid: string; + oldFee: number; + oldVsize: number; + newFee: number; + newVsize: number; +} export interface MempoolBlock { blink?: boolean; height?: number; diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 16252a9ec..2258d8440 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs'; import { Transaction } from '../interfaces/electrs.interface'; -import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface'; +import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface'; import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; @@ -108,6 +108,7 @@ export class StateService { txReplaced$ = new Subject(); txRbfInfo$ = new Subject(); rbfLatest$ = new Subject(); + rbfLatestSummary$ = new Subject(); utxoSpent$ = new Subject(); difficultyAdjustment$ = new ReplaySubject(1); mempoolTransactions$ = new Subject(); diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 7eed09e77..f32f772ac 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -29,6 +29,7 @@ export class WebsocketService { private trackingTxId: string; private isTrackingMempoolBlock = false; private isTrackingRbf = false; + private isTrackingRbfSummary = false; private trackingMempoolBlock: number; private latestGitCommit = ''; private onlineCheckTimeout: number; @@ -185,6 +186,16 @@ export class WebsocketService { this.isTrackingRbf = false; } + startTrackRbfSummary() { + this.websocketSubject.next({ 'track-rbf-summary': true }); + this.isTrackingRbfSummary = true; + } + + stopTrackRbfSummary() { + this.websocketSubject.next({ 'track-rbf-summary': false }); + this.isTrackingRbfSummary = false; + } + startTrackBisqMarket(market: string) { this.websocketSubject.next({ 'track-bisq-market': market }); } @@ -283,6 +294,10 @@ export class WebsocketService { this.stateService.rbfLatest$.next(response.rbfLatest); } + if (response.rbfLatestSummary) { + this.stateService.rbfLatestSummary$.next(response.rbfLatestSummary); + } + if (response.txReplaced) { this.stateService.txReplaced$.next(response.txReplaced); }
HeightMinedPoolTXsSizeTXIDPrevious feeNew feeStatus
{{ block.height }} - - - {{ block.extras.pool.name }} +
+ + {{ block.tx_count | number }} -
-
 
-
-
+
+ Mined + Full RBF + RBF