reduce latest rbf websocket data

This commit is contained in:
Mononaut 2023-07-14 16:08:57 +09:00
parent 240afbed95
commit fa48791c59
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
6 changed files with 99 additions and 46 deletions

View File

@ -6,6 +6,7 @@ import { Common } from "./common";
interface RbfTransaction extends TransactionStripped { interface RbfTransaction extends TransactionStripped {
rbf?: boolean; rbf?: boolean;
mined?: boolean; mined?: boolean;
fullRbf?: boolean;
} }
interface RbfTree { interface RbfTree {
@ -17,6 +18,16 @@ interface RbfTree {
replaces: RbfTree[]; replaces: RbfTree[];
} }
export interface ReplacementInfo {
mined: boolean;
fullRbf: boolean;
txid: string;
oldFee: number;
oldVsize: number;
newFee: number;
newVsize: number;
}
class RbfCache { class RbfCache {
private replacedBy: Map<string, string> = new Map(); private replacedBy: Map<string, string> = new Map();
private replaces: Map<string, string[]> = new Map(); private replaces: Map<string, string[]> = new Map();
@ -41,11 +52,15 @@ class RbfCache {
this.txs.set(newTx.txid, newTxExtended); this.txs.set(newTx.txid, newTxExtended);
// maintain rbf trees // maintain rbf trees
let fullRbf = false; let txFullRbf = false;
let treeFullRbf = false;
const replacedTrees: RbfTree[] = []; const replacedTrees: RbfTree[] = [];
for (const replacedTxExtended of replaced) { for (const replacedTxExtended of replaced) {
const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction; const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe); replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
if (!replacedTx.rbf) {
txFullRbf = true;
}
this.replacedBy.set(replacedTx.txid, newTx.txid); this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) { if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid); const treeId = this.treeMap.get(replacedTx.txid);
@ -55,7 +70,7 @@ class RbfCache {
if (tree) { if (tree) {
tree.interval = newTime - tree?.time; tree.interval = newTime - tree?.time;
replacedTrees.push(tree); replacedTrees.push(tree);
fullRbf = fullRbf || tree.fullRbf || !tree.tx.rbf; treeFullRbf = treeFullRbf || tree.fullRbf || !tree.tx.rbf;
} }
} }
} else { } else {
@ -67,15 +82,16 @@ class RbfCache {
fullRbf: !replacedTx.rbf, fullRbf: !replacedTx.rbf,
replaces: [], replaces: [],
}); });
fullRbf = fullRbf || !replacedTx.rbf; treeFullRbf = treeFullRbf || !replacedTx.rbf;
this.txs.set(replacedTx.txid, replacedTxExtended); this.txs.set(replacedTx.txid, replacedTxExtended);
} }
} }
newTx.fullRbf = txFullRbf;
const treeId = replacedTrees[0].tx.txid; const treeId = replacedTrees[0].tx.txid;
const newTree = { const newTree = {
tx: newTx, tx: newTx,
time: newTime, time: newTime,
fullRbf, fullRbf: treeFullRbf,
replaces: replacedTrees replaces: replacedTrees
}; };
this.rbfTrees.set(treeId, newTree); this.rbfTrees.set(treeId, newTree);
@ -349,6 +365,27 @@ class RbfCache {
} }
return tree; 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(); export default new RbfCache();

View File

@ -12,7 +12,7 @@ import { Common } from './common';
import loadingIndicators from './loading-indicators'; import loadingIndicators from './loading-indicators';
import config from '../config'; import config from '../config';
import transactionUtils from './transaction-utils'; import transactionUtils from './transaction-utils';
import rbfCache from './rbf-cache'; import rbfCache, { ReplacementInfo } from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment'; import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api'; import feeApi from './fee-api';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
@ -40,6 +40,7 @@ class WebsocketHandler {
private socketData: { [key: string]: string } = {}; private socketData: { [key: string]: string } = {};
private serializedInitData: string = '{}'; private serializedInitData: string = '{}';
private lastRbfSummary: ReplacementInfo | null = null;
constructor() { } 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 (parsedMessage.action === 'init') {
if (!this.socketData['blocks']?.length || !this.socketData['da']) { if (!this.socketData['blocks']?.length || !this.socketData['da']) {
this.updateSocketData(); this.updateSocketData();
@ -395,10 +405,13 @@ class WebsocketHandler {
const rbfChanges = rbfCache.getRbfChanges(); const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements; let rbfReplacements;
let fullRbfReplacements; let fullRbfReplacements;
let rbfSummary;
if (Object.keys(rbfChanges.trees).length) { if (Object.keys(rbfChanges.trees).length) {
rbfReplacements = rbfCache.getRbfTrees(false); rbfReplacements = rbfCache.getRbfTrees(false);
fullRbfReplacements = rbfCache.getRbfTrees(true); fullRbfReplacements = rbfCache.getRbfTrees(true);
rbfSummary = rbfCache.getLatestRbfSummary();
} }
for (const deletedTx of deletedTransactions) { for (const deletedTx of deletedTransactions) {
rbfCache.evict(deletedTx.txid); rbfCache.evict(deletedTx.txid);
} }
@ -409,7 +422,7 @@ class WebsocketHandler {
const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx)); const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
// update init data // update init data
this.updateSocketDataFields({ const socketDataFields = {
'mempoolInfo': mempoolInfo, 'mempoolInfo': mempoolInfo,
'vBytesPerSecond': vBytesPerSecond, 'vBytesPerSecond': vBytesPerSecond,
'mempool-blocks': mBlocks, 'mempool-blocks': mBlocks,
@ -417,7 +430,11 @@ class WebsocketHandler {
'loadingIndicators': loadingIndicators.getLoadingIndicators(), 'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': da?.previousTime ? da : undefined, 'da': da?.previousTime ? da : undefined,
'fees': recommendedFees, 'fees': recommendedFees,
}); };
if (rbfSummary) {
socketDataFields['rbfSummary'] = rbfSummary;
}
this.updateSocketDataFields(socketDataFields);
// cache serialized objects to avoid stringify-ing the same thing for every client // cache serialized objects to avoid stringify-ing the same thing for every client
const responseCache = { ...this.socketData }; const responseCache = { ...this.socketData };
@ -601,6 +618,10 @@ class WebsocketHandler {
response['rbfLatest'] = getCachedResponse('fullrbfLatest', fullRbfReplacements); response['rbfLatest'] = getCachedResponse('fullrbfLatest', fullRbfReplacements);
} }
if (client['track-rbf-summary'] && rbfSummary) {
response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary);
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
const serializedResponse = this.serializeResponse(response); const serializedResponse = this.serializeResponse(response);
client.send(serializedResponse); client.send(serializedResponse);

View File

@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs'; import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; import { filter, map, scan, share, switchMap } from 'rxjs/operators';
import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface'; import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
import { ApiService } from '../services/api.service'; import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service'; import { StateService } from '../services/state.service';
import { WebsocketService } from '../services/websocket.service'; import { WebsocketService } from '../services/websocket.service';
@ -25,17 +25,6 @@ interface MempoolStatsData {
weightPerSecond: any; weightPerSecond: any;
} }
interface ReplacementInfo {
tree: RbfTree;
mined: boolean;
fullRbf: boolean;
txid: string;
oldFee: number;
oldVsize: number;
newFee: number;
newVsize: number;
}
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
templateUrl: './dashboard.component.html', templateUrl: './dashboard.component.html',
@ -69,13 +58,14 @@ export class DashboardComponent implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.currencySubscription.unsubscribe(); this.currencySubscription.unsubscribe();
this.websocketService.stopTrackRbfSummary();
} }
ngOnInit(): void { ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$; this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.seoService.resetTitle(); this.seoService.resetTitle();
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']); this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
this.websocketService.startTrackRbf('all'); this.websocketService.startTrackRbfSummary();
this.network$ = merge(of(''), this.stateService.networkChanged$); this.network$ = merge(of(''), this.stateService.networkChanged$);
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$ this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
.pipe( .pipe(
@ -154,30 +144,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
}, []), }, []),
); );
this.replacements$ = this.stateService.rbfLatest$.pipe( this.replacements$ = this.stateService.rbfLatestSummary$;
switchMap((rbfList) => {
const replacements = 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;
}
this.checkFullRbf(rbfTree);
return {
tree: rbfTree,
txid: rbfTree.tx.txid,
mined: rbfTree.tx.mined,
fullRbf: rbfTree.tx.fullRbf,
oldFee,
oldVsize,
newFee: rbfTree.tx.fee,
newVsize: rbfTree.tx.vsize,
};
});
return of(replacements);
})
);
this.mempoolStats$ = this.stateService.connectionState$ this.mempoolStats$ = this.stateService.connectionState$
.pipe( .pipe(

View File

@ -18,6 +18,7 @@ export interface WebsocketResponse {
txReplaced?: ReplacedTransaction; txReplaced?: ReplacedTransaction;
rbfInfo?: RbfTree; rbfInfo?: RbfTree;
rbfLatest?: RbfTree[]; rbfLatest?: RbfTree[];
rbfLatestSummary?: ReplacementInfo[];
utxoSpent?: object; utxoSpent?: object;
transactions?: TransactionStripped[]; transactions?: TransactionStripped[];
loadingIndicators?: ILoadingIndicators; loadingIndicators?: ILoadingIndicators;
@ -29,6 +30,7 @@ export interface WebsocketResponse {
'track-asset'?: string; 'track-asset'?: string;
'track-mempool-block'?: number; 'track-mempool-block'?: number;
'track-rbf'?: string; 'track-rbf'?: string;
'track-rbf-summary'?: boolean;
'watch-mempool'?: boolean; 'watch-mempool'?: boolean;
'track-bisq-market'?: string; 'track-bisq-market'?: string;
'refresh-blocks'?: boolean; 'refresh-blocks'?: boolean;
@ -37,6 +39,16 @@ export interface WebsocketResponse {
export interface ReplacedTransaction extends Transaction { export interface ReplacedTransaction extends Transaction {
txid: string; txid: string;
} }
export interface ReplacementInfo {
mined: boolean;
fullRbf: boolean;
txid: string;
oldFee: number;
oldVsize: number;
newFee: number;
newVsize: number;
}
export interface MempoolBlock { export interface MempoolBlock {
blink?: boolean; blink?: boolean;
height?: number; height?: number;

View File

@ -1,7 +1,7 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface'; 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 { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
@ -108,6 +108,7 @@ export class StateService {
txReplaced$ = new Subject<ReplacedTransaction>(); txReplaced$ = new Subject<ReplacedTransaction>();
txRbfInfo$ = new Subject<RbfTree>(); txRbfInfo$ = new Subject<RbfTree>();
rbfLatest$ = new Subject<RbfTree[]>(); rbfLatest$ = new Subject<RbfTree[]>();
rbfLatestSummary$ = new Subject<ReplacementInfo[]>();
utxoSpent$ = new Subject<object>(); utxoSpent$ = new Subject<object>();
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1); difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
mempoolTransactions$ = new Subject<Transaction>(); mempoolTransactions$ = new Subject<Transaction>();

View File

@ -29,6 +29,7 @@ export class WebsocketService {
private trackingTxId: string; private trackingTxId: string;
private isTrackingMempoolBlock = false; private isTrackingMempoolBlock = false;
private isTrackingRbf = false; private isTrackingRbf = false;
private isTrackingRbfSummary = false;
private trackingMempoolBlock: number; private trackingMempoolBlock: number;
private latestGitCommit = ''; private latestGitCommit = '';
private onlineCheckTimeout: number; private onlineCheckTimeout: number;
@ -185,6 +186,16 @@ export class WebsocketService {
this.isTrackingRbf = false; 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) { startTrackBisqMarket(market: string) {
this.websocketSubject.next({ 'track-bisq-market': market }); this.websocketSubject.next({ 'track-bisq-market': market });
} }
@ -283,6 +294,10 @@ export class WebsocketService {
this.stateService.rbfLatest$.next(response.rbfLatest); this.stateService.rbfLatest$.next(response.rbfLatest);
} }
if (response.rbfLatestSummary) {
this.stateService.rbfLatestSummary$.next(response.rbfLatestSummary);
}
if (response.txReplaced) { if (response.txReplaced) {
this.stateService.txReplaced$.next(response.txReplaced); this.stateService.txReplaced$.next(response.txReplaced);
} }