Merge pull request #4213 from mempool/mononaut/live-cpfp-updates
Send cpfp/effective fee rate changes to subscribed websocket clients
This commit is contained in:
		
						commit
						6780ba82d0
					
				| @ -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[] { |   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) { |     for (const [txid, rate] of rates) { | ||||||
|       if (txid in mempool) { |       if (txid in mempool) { | ||||||
|  |         mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize); | ||||||
|         mempool[txid].effectiveFeePerVsize = rate; |         mempool[txid].effectiveFeePerVsize = rate; | ||||||
|         mempool[txid].cpfpChecked = false; |         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}); |           Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true}); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @ -531,12 +535,21 @@ class MempoolBlocks { | |||||||
| 
 | 
 | ||||||
|           const acceleration = accelerations[txid]; |           const acceleration = accelerations[txid]; | ||||||
|           if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { |           if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { | ||||||
|  |             if (!mempoolTx.acceleration) { | ||||||
|  |               mempoolTx.cpfpDirty = true; | ||||||
|  |             } | ||||||
|             mempoolTx.acceleration = true; |             mempoolTx.acceleration = true; | ||||||
|             for (const ancestor of mempoolTx.ancestors || []) { |             for (const ancestor of mempoolTx.ancestors || []) { | ||||||
|  |               if (!mempool[ancestor.txid].acceleration) { | ||||||
|  |                 mempool[ancestor.txid].cpfpDirty = true; | ||||||
|  |               } | ||||||
|               mempool[ancestor.txid].acceleration = true; |               mempool[ancestor.txid].acceleration = true; | ||||||
|               isAccelerated[ancestor.txid] = true; |               isAccelerated[ancestor.txid] = true; | ||||||
|             } |             } | ||||||
|           } else { |           } else { | ||||||
|  |             if (mempoolTx.acceleration) { | ||||||
|  |               mempoolTx.cpfpDirty = true; | ||||||
|  |             } | ||||||
|             delete mempoolTx.acceleration; |             delete mempoolTx.acceleration; | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -586,13 +586,25 @@ class WebsocketHandler { | |||||||
| 
 | 
 | ||||||
|         const mempoolTx = newMempool[trackTxid]; |         const mempoolTx = newMempool[trackTxid]; | ||||||
|         if (mempoolTx && mempoolTx.position) { |         if (mempoolTx && mempoolTx.position) { | ||||||
|           response['txPosition'] = JSON.stringify({ |           const positionData = { | ||||||
|             txid: trackTxid, |             txid: trackTxid, | ||||||
|             position: { |             position: { | ||||||
|               ...mempoolTx.position, |               ...mempoolTx.position, | ||||||
|               accelerated: mempoolTx.acceleration || undefined, |               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); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -104,6 +104,7 @@ export interface MempoolTransactionExtended extends TransactionExtended { | |||||||
|   adjustedFeePerVsize: number; |   adjustedFeePerVsize: number; | ||||||
|   inputs?: number[]; |   inputs?: number[]; | ||||||
|   lastBoosted?: number; |   lastBoosted?: number; | ||||||
|  |   cpfpDirty?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface AuditTransaction { | export interface AuditTransaction { | ||||||
|  | |||||||
| @ -161,34 +161,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|         }) |         }) | ||||||
|       ) |       ) | ||||||
|       .subscribe((cpfpInfo) => { |       .subscribe((cpfpInfo) => { | ||||||
|         if (!cpfpInfo || !this.tx) { |         this.setCpfpInfo(cpfpInfo); | ||||||
|           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.fetchRbfSubscription = this.fetchRbfHistory$ |     this.fetchRbfSubscription = this.fetchRbfHistory$ | ||||||
| @ -259,6 +232,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|             mempoolPosition: this.mempoolPosition |             mempoolPosition: this.mempoolPosition | ||||||
|           }); |           }); | ||||||
|           this.txInBlockIndex = this.mempoolPosition.block; |           this.txInBlockIndex = this.mempoolPosition.block; | ||||||
|  | 
 | ||||||
|  |           if (txPosition.cpfp !== undefined) { | ||||||
|  |             this.setCpfpInfo(txPosition.cpfp); | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|         this.mempoolPosition = null; |         this.mempoolPosition = null; | ||||||
| @ -507,6 +484,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 { |   setFeatures(): void { | ||||||
|     if (this.tx) { |     if (this.tx) { | ||||||
|       this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit'); |       this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit'); | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ export interface Transaction { | |||||||
|   ancestors?: Ancestor[]; |   ancestors?: Ancestor[]; | ||||||
|   bestDescendant?: BestDescendant | null; |   bestDescendant?: BestDescendant | null; | ||||||
|   cpfpChecked?: boolean; |   cpfpChecked?: boolean; | ||||||
|   acceleration?: number; |   acceleration?: boolean; | ||||||
|   deleteAfter?: number; |   deleteAfter?: number; | ||||||
|   _unblinded?: any; |   _unblinded?: any; | ||||||
|   _deduced?: boolean; |   _deduced?: boolean; | ||||||
|  | |||||||
| @ -27,7 +27,7 @@ export interface CpfpInfo { | |||||||
|   effectiveFeePerVsize?: number; |   effectiveFeePerVsize?: number; | ||||||
|   sigops?: number; |   sigops?: number; | ||||||
|   adjustedVsize?: number; |   adjustedVsize?: number; | ||||||
|   acceleration?: number; |   acceleration?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface RbfInfo { | export interface RbfInfo { | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ 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, ReplacementInfo, 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, CpfpInfo, 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'; | ||||||
| import { filter, map, scan, shareReplay } from 'rxjs/operators'; | import { filter, map, scan, shareReplay } from 'rxjs/operators'; | ||||||
| @ -113,7 +113,7 @@ export class StateService { | |||||||
|   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>(); | ||||||
|   mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition}>(); |   mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null}>(); | ||||||
|   blockTransactions$ = new Subject<Transaction>(); |   blockTransactions$ = new Subject<Transaction>(); | ||||||
|   isLoadingWebSocket$ = new ReplaySubject<boolean>(1); |   isLoadingWebSocket$ = new ReplaySubject<boolean>(1); | ||||||
|   isLoadingMempool$ = new BehaviorSubject<boolean>(true); |   isLoadingMempool$ = new BehaviorSubject<boolean>(true); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user