diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 1de4bbee7..15f9b6cf7 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -451,6 +451,7 @@ class MempoolBlocks { private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] { for (const [txid, rate] of rates) { if (txid in mempool) { + mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize); mempool[txid].effectiveFeePerVsize = rate; mempool[txid].cpfpChecked = false; } @@ -494,6 +495,9 @@ class MempoolBlocks { } } }); + if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) { + mempoolTx.cpfpDirty = true; + } Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true}); } } @@ -531,12 +535,21 @@ class MempoolBlocks { const acceleration = accelerations[txid]; if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { + if (!mempoolTx.acceleration) { + mempoolTx.cpfpDirty = true; + } mempoolTx.acceleration = true; for (const ancestor of mempoolTx.ancestors || []) { + if (!mempool[ancestor.txid].acceleration) { + mempool[ancestor.txid].cpfpDirty = true; + } mempool[ancestor.txid].acceleration = true; isAccelerated[ancestor.txid] = true; } } else { + if (mempoolTx.acceleration) { + mempoolTx.cpfpDirty = true; + } delete mempoolTx.acceleration; } diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 9cb24df10..41cb6b99c 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -586,13 +586,25 @@ class WebsocketHandler { const mempoolTx = newMempool[trackTxid]; if (mempoolTx && mempoolTx.position) { - response['txPosition'] = JSON.stringify({ + const positionData = { txid: trackTxid, position: { ...mempoolTx.position, accelerated: mempoolTx.acceleration || undefined, } - }); + }; + if (mempoolTx.cpfpDirty) { + positionData['cpfp'] = { + ancestors: mempoolTx.ancestors, + bestDescendant: mempoolTx.bestDescendant || null, + descendants: mempoolTx.descendants || null, + effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null, + sigops: mempoolTx.sigops, + adjustedVsize: mempoolTx.adjustedVsize, + acceleration: mempoolTx.acceleration + }; + } + response['txPosition'] = JSON.stringify(positionData); } } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index c08846191..b013f2f26 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -104,6 +104,7 @@ export interface MempoolTransactionExtended extends TransactionExtended { adjustedFeePerVsize: number; inputs?: number[]; lastBoosted?: number; + cpfpDirty?: boolean; } export interface AuditTransaction { diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index ac1452835..a0476df3f 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -174,34 +174,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { }) ) .subscribe((cpfpInfo) => { - if (!cpfpInfo || !this.tx) { - this.cpfpInfo = null; - this.hasEffectiveFeeRate = false; - return; - } - // merge ancestors/descendants - const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])]; - if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) { - relatives.push(cpfpInfo.bestDescendant); - } - const hasRelatives = !!relatives.length; - if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) { - let totalWeight = - this.tx.weight + - relatives.reduce((prev, val) => prev + val.weight, 0); - let totalFees = - this.tx.fee + - relatives.reduce((prev, val) => prev + val.fee, 0); - this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4); - } else { - this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize; - } - if (cpfpInfo.acceleration) { - this.tx.acceleration = cpfpInfo.acceleration; - } - - this.cpfpInfo = cpfpInfo; - this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01)); + this.setCpfpInfo(cpfpInfo); }); this.fetchRbfSubscription = this.fetchRbfHistory$ @@ -272,6 +245,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { mempoolPosition: this.mempoolPosition }); this.txInBlockIndex = this.mempoolPosition.block; + + if (txPosition.cpfp !== undefined) { + this.setCpfpInfo(txPosition.cpfp); + } } } else { this.mempoolPosition = null; @@ -534,6 +511,37 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { }); } + setCpfpInfo(cpfpInfo: CpfpInfo): void { + if (!cpfpInfo || !this.tx) { + this.cpfpInfo = null; + this.hasEffectiveFeeRate = false; + return; + } + // merge ancestors/descendants + const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])]; + if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) { + relatives.push(cpfpInfo.bestDescendant); + } + const hasRelatives = !!relatives.length; + if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) { + const totalWeight = + this.tx.weight + + relatives.reduce((prev, val) => prev + val.weight, 0); + const totalFees = + this.tx.fee + + relatives.reduce((prev, val) => prev + val.fee, 0); + this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4); + } else { + this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize; + } + if (cpfpInfo.acceleration) { + this.tx.acceleration = cpfpInfo.acceleration; + } + + this.cpfpInfo = cpfpInfo; + this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01)); + } + setFeatures(): void { if (this.tx) { this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit'); diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index 5c15b0ae4..2d604a9de 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -19,7 +19,7 @@ export interface Transaction { ancestors?: Ancestor[]; bestDescendant?: BestDescendant | null; cpfpChecked?: boolean; - acceleration?: number; + acceleration?: boolean; deleteAfter?: number; _unblinded?: any; _deduced?: boolean; diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index fbf86aeb4..a9f069b56 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -27,7 +27,7 @@ export interface CpfpInfo { effectiveFeePerVsize?: number; sigops?: number; adjustedVsize?: number; - acceleration?: number; + acceleration?: boolean; } export interface RbfInfo { diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 0e4163159..878edf359 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -2,7 +2,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, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface'; -import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; +import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; import { filter, map, scan, shareReplay } from 'rxjs/operators'; @@ -116,7 +116,7 @@ export class StateService { utxoSpent$ = new Subject(); difficultyAdjustment$ = new ReplaySubject(1); mempoolTransactions$ = new Subject(); - mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition}>(); + mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null}>(); blockTransactions$ = new Subject(); isLoadingWebSocket$ = new ReplaySubject(1); isLoadingMempool$ = new BehaviorSubject(true);