Merge pull request #3962 from mempool/mononaut/latest-replacements
Switch "Latest blocks" to "Latest replacements"
This commit is contained in:
commit
f8fc0439f8
@ -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<string, string> = new Map();
|
||||
private replaces: Map<string, string[]> = 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();
|
||||
|
@ -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);
|
||||
|
@ -75,36 +75,31 @@
|
||||
<div class="col" style="max-height: 410px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
||||
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-rbf-replacements">Latest replacements</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
</a>
|
||||
<table class="table lastest-blocks-table">
|
||||
<table class="table lastest-replacements-table">
|
||||
<thead>
|
||||
<th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th>
|
||||
<th *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th>
|
||||
<th *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4" i18n="mining.pool-name">Pool</th>
|
||||
<th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th>
|
||||
<th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
|
||||
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<th class="table-cell-old-fee" i18n="dashboard.previous-transaction-fee">Previous fee</th>
|
||||
<th class="table-cell-new-fee" i18n="dashboard.new-transaction-fee">New fee</th>
|
||||
<th class="table-cell-badges" i18n="transaction.status|Transaction Status">Status</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock">
|
||||
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
||||
<td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
|
||||
<td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4">
|
||||
<a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'">
|
||||
<span class="pool-name">{{ block.extras.pool.name }}</span>
|
||||
<tr *ngFor="let replacement of replacements$ | async;">
|
||||
<td class="table-cell-txid">
|
||||
<a [routerLink]="['/tx' | relativeUrl, replacement.txid]">
|
||||
<app-truncate [text]="replacement.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
|
||||
<td class="table-cell-size">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"> </div>
|
||||
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
||||
</div>
|
||||
<td class="table-cell-old-fee"><app-fee-rate [fee]="replacement.oldFee" [weight]="replacement.oldVsize * 4"></app-fee-rate></td>
|
||||
<td class="table-cell-new-fee"><app-fee-rate [fee]="replacement.newFee" [weight]="replacement.newVsize * 4"></app-fee-rate></td>
|
||||
<td class="table-cell-badges">
|
||||
<span *ngIf="replacement.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="replacement.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
|
||||
<span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="transaction.rbf">RBF</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<MempoolInfoData>;
|
||||
mempoolLoadingStatus$: Observable<number>;
|
||||
vBytesPerSecondLimit = 1667;
|
||||
blocks$: Observable<BlockExtended[]>;
|
||||
transactions$: Observable<TransactionStripped[]>;
|
||||
replacements$: Observable<ReplacementInfo[]>;
|
||||
latestBlockHeight: number;
|
||||
mempoolTransactionsWeightPerSecondData: any;
|
||||
mempoolStats$: Observable<MempoolStatsData>;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<ReplacedTransaction>();
|
||||
txRbfInfo$ = new Subject<RbfTree>();
|
||||
rbfLatest$ = new Subject<RbfTree[]>();
|
||||
rbfLatestSummary$ = new Subject<ReplacementInfo[]>();
|
||||
utxoSpent$ = new Subject<object>();
|
||||
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
|
||||
mempoolTransactions$ = new Subject<Transaction>();
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user