From e2a5b90b38dcdd438b2e2f99e72a45a16a2955b3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 2 Feb 2024 20:49:32 +0000 Subject: [PATCH] Display multiple arrows for accelerated transactions --- backend/src/api/mempool-blocks.ts | 66 +++++++++++++---- backend/src/api/mempool.ts | 6 +- .../mempool-blocks.component.html | 5 +- .../mempool-blocks.component.scss | 15 +++- .../mempool-blocks.component.ts | 70 +++++++++++++++++-- .../transaction/transaction.component.ts | 29 +++++++- .../src/app/interfaces/node-api.interface.ts | 5 ++ frontend/src/app/services/state.service.ts | 5 +- 8 files changed, 170 insertions(+), 31 deletions(-) diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 022246c71..233dba43e 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -7,6 +7,7 @@ import { Worker } from 'worker_threads'; import path from 'path'; import mempool from './mempool'; import { Acceleration } from './services/acceleration'; +import PoolsRepository from '../repositories/PoolsRepository'; const MAX_UINT32 = Math.pow(2, 32) - 1; @@ -20,6 +21,17 @@ class MempoolBlocks { private nextUid: number = 1; private uidMap: Map = new Map(); // map short numerical uids to full txids + private pools: { [id: number]: PoolTag } = {}; + + constructor() { + PoolsRepository.$getPools().then(allPools => { + this.pools = {}; + for (const pool of allPools) { + this.pools[pool.uniqueId] = pool; + } + }); + } + public getMempoolBlocks(): MempoolBlock[] { return this.mempoolBlocks.map((block) => { return { @@ -695,10 +707,10 @@ class MempoolBlocks { // estimates and saves positions of accelerations in mining partner mempools private updateAccelerationPositions(mempoolCache: { [txid: string]: MempoolTransactionExtended }, accelerations: { [txid: string]: Acceleration }, mempoolBlocks: MempoolBlockWithTransactions[]): void { - const accelerationPositions: { [txid: string]: { [pool: string]: { block: number, vbytes: number } } } = {}; + const accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {}; // keep track of simulated mempool blocks for each active pool const pools: { - [pool: string]: { block: number, vsize: number, accelerations: string[] }; + [pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean }; } = {}; // prepare a list of accelerations in ascending order (we'll pop items off the end of the list) const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => { @@ -714,21 +726,31 @@ class MempoolBlocks { }).sort((a, b) => a.rate - b.rate); // initialize the pool tracker for (const { acceleration } of accQueue) { - accelerationPositions[acceleration.txid] = {}; + accelerationPositions[acceleration.txid] = []; for (const pool of acceleration.pools) { if (!pools[pool]) { pools[pool] = { + name: this.pools[pool]?.name || 'unknown', block: 0, vsize: 0, accelerations: [], + complete: false, }; } pools[pool].accelerations.push(acceleration.txid); } for (const ancestor of mempoolCache[acceleration.txid].ancestors || []) { - accelerationPositions[ancestor.txid] = {}; + accelerationPositions[ancestor.txid] = []; } } + + for (const pool of Object.keys(pools)) { + // if any pools accepted *every* acceleration, we can just use the GBT result positions directly + if (pools[pool].accelerations.length === Object.keys(accelerations).length) { + pools[pool].complete = true; + } + } + let block = 0; let index = 0; let next = accQueue.pop(); @@ -746,16 +768,36 @@ class MempoolBlocks { pools[pool].vsize = next.vsize; } // insert the acceleration into matching pool's blocks - accelerationPositions[next.acceleration.txid][pool] = { - block: pools[pool].block, - vbytes: pools[pool].vsize - (next.vsize / 2), - }; + if (pools[pool].complete && mempoolCache[next.acceleration.txid]?.position !== undefined) { + accelerationPositions[next.acceleration.txid].push({ + ...mempoolCache[next.acceleration.txid].position as { block: number, vsize: number }, + poolId: pool, + pool: pools[pool].name + }); + } else { + accelerationPositions[next.acceleration.txid].push({ + poolId: pool, + pool: pools[pool].name, + block: pools[pool].block, + vsize: pools[pool].vsize - (next.vsize / 2), + }); + } // and any accelerated ancestors for (const ancestor of mempoolCache[next.acceleration.txid].ancestors || []) { - accelerationPositions[ancestor.txid][pool] = { - block: pools[pool].block, - vbytes: pools[pool].vsize - (next.vsize / 2), - }; + if (pools[pool].complete && mempoolCache[ancestor.txid]?.position !== undefined) { + accelerationPositions[ancestor.txid].push({ + ...mempoolCache[ancestor.txid].position as { block: number, vsize: number }, + poolId: pool, + pool: pools[pool].name, + }); + } else { + accelerationPositions[ancestor.txid].push({ + poolId: pool, + pool: pools[pool].name, + block: pools[pool].block, + vsize: pools[pool].vsize - (next.vsize / 2), + }); + } } } next = accQueue.pop(); diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 8fc975c52..cf5234811 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -25,7 +25,7 @@ class Mempool { deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise) | undefined; private accelerations: { [txId: string]: Acceleration } = {}; - private accelerationPositions: { [txid: string]: { [pool: number]: { block: number, vbytes: number } } } = {}; + private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {}; private txPerSecondArray: number[] = []; private txPerSecond: number = 0; @@ -432,11 +432,11 @@ class Mempool { } } - setAccelerationPositions(positions: { [txid: string]: { [pool: number]: { block: number, vbytes: number } } }): void { + setAccelerationPositions(positions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] }): void { this.accelerationPositions = positions; } - getAccelerationPositions(txid: string): { [pool: number]: { block: number, vbytes: number } } | undefined { + getAccelerationPositions(txid: string): { [pool: number]: { poolId: number, pool: string, block: number, vsize: number } } | undefined { return this.accelerationPositions[txid]; } diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html index 59d35c91e..e8644549f 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html @@ -49,7 +49,10 @@ -
+
+ + + diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss index 606699d93..60f9d44d8 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss @@ -122,6 +122,17 @@ border-bottom: 35px solid #FFF; } +.acceleration-arrow { + position: relative; + right: 75px; + top: 105px; + width: 0; + height: 0; + border-left: 35px solid transparent; + border-right: 35px solid transparent; + border-bottom: 35px solid #FFF; +} + .blockLink { width: 100%; height: 100%; @@ -154,7 +165,7 @@ } :host-context(.rtl-layout) { - #arrow-up { + #arrow-up, .acceleration-arrow { transform: translateX(70px); } } @@ -172,8 +183,6 @@ } .blink{ - width:400px; - height:400px; border-bottom: 35px solid #FFF; animation: blink 0.2s infinite; } diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index 61e62f642..511338a84 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -8,7 +8,7 @@ import { feeLevels, mempoolFeeColors } from '../../app.constants'; import { specialBlocks } from '../../app.constants'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { Location } from '@angular/common'; -import { DifficultyAdjustment, MempoolPosition } from '../../interfaces/node-api.interface'; +import { AccelerationPosition, DifficultyAdjustment, MempoolPosition } from '../../interfaces/node-api.interface'; import { animate, style, transition, trigger } from '@angular/animations'; @Component({ @@ -66,13 +66,19 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { blockPadding: number = 30; containerOffset: number = 40; arrowVisible = false; + accelerationArrow = true; tabHidden = false; feeRounding = '1.0-0'; rightPosition = 0; transition = 'background 2s, right 2s, transform 1s'; + accelerationPositions: AccelerationPosition[] = []; + accTransition = 'background 2s, right 2s, transform 1s'; + animatingAcceleration: boolean = false; markIndex: number; + markedTxid: string; + txPosition: MempoolPosition; txFeePerVSize: number; @@ -160,7 +166,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { this.now = Date.now(); this.updateMempoolBlockStyles(); - this.calculateTransactionPosition(); return this.mempoolBlocks; }), @@ -188,6 +193,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { this.markIndex = undefined; this.txPosition = undefined; this.txFeePerVSize = undefined; + this.accelerationPositions = []; if (state.mempoolBlockIndex !== undefined) { this.markIndex = state.mempoolBlockIndex; } @@ -197,7 +203,20 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { if (state.txFeePerVSize) { this.txFeePerVSize = state.txFeePerVSize; } - this.calculateTransactionPosition(); + if (state.accelerationPositions) { + this.accelerationPositions = state.accelerationPositions; + } + if (this.txPosition && this.txPosition.accelerated) { + const newlyAccelerated = (!this.accelerationArrow && state.txid === this.markedTxid); + this.calculateTransactionPosition(true); + if (newlyAccelerated || !this.animatingAcceleration) { + this.calculateAccelerationPositions(newlyAccelerated); + } + } else { + this.accelerationArrow = false; + this.calculateTransactionPosition(); + } + this.markedTxid = state.txid; this.cd.markForCheck(); }); @@ -289,6 +308,10 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { return (block.isStack) ? `stack-${block.index}` : block.index; } + accTrackByFn(index: number, pool: AccelerationPosition) { + return pool.pool; + } + reduceEmptyBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] { const innerWidth = this.containerWidth || (this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2); let blocksAmount = this.stateService.env.MEMPOOL_BLOCKS_AMOUNT; @@ -389,7 +412,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { }; } - calculateTransactionPosition() { + calculateTransactionPosition(fromFee: boolean = false) { if ((!this.txPosition && !this.txFeePerVSize && (this.markIndex === undefined || this.markIndex === -1)) || !this.mempoolBlocks) { this.arrowVisible = false; return; @@ -408,7 +431,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { this.arrowVisible = true; - if (this.txPosition) { + if (this.txPosition && !fromFee) { if (this.txPosition.block >= this.mempoolBlocks.length) { this.rightPosition = ((this.mempoolBlocks.length - 1) * (this.blockWidth + this.blockPadding)) + this.blockWidth; } else { @@ -418,9 +441,9 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { } } else { let found = false; - for (let txInBlockIndex = 0; txInBlockIndex < this.mempoolBlocks.length && !found; txInBlockIndex++) { + for (let txInBlockIndex = this.mempoolBlocks.length - 1; txInBlockIndex >= 0 && !found; txInBlockIndex--) { const block = this.mempoolBlocks[txInBlockIndex]; - for (let i = 0; i < block.feeRange.length - 1 && !found; i++) { + for (let i = block.feeRange.length - 2; i >= 0 && !found; i--) { if (this.txFeePerVSize < block.feeRange[i + 1] && this.txFeePerVSize >= block.feeRange[i]) { const feeRangeIndex = i; const feeRangeChunkSize = 1 / (block.feeRange.length - 1); @@ -448,6 +471,39 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { } } + calculateAccelerationPositions(animate: boolean = false) { + if (!this.accelerationPositions || !this.mempoolBlocks) { + this.accelerationArrow = false; + return; + } + + this.accelerationArrow = true; + + const applyPositions = () => { + for (const accelerationPosition of this.accelerationPositions) { + if (accelerationPosition.block >= this.mempoolBlocks.length) { + accelerationPosition.offset = ((this.mempoolBlocks.length - 1) * (this.blockWidth + this.blockPadding)) + this.blockWidth; + } else { + const positionInBlock = Math.min(1, this.txPosition.vsize / this.stateService.blockVSize) * this.blockWidth; + const positionOfBlock = accelerationPosition.block * (this.blockWidth + this.blockPadding); + accelerationPosition.offset = positionOfBlock + positionInBlock; + } + } + }; + if (animate) { + this.animatingAcceleration = true; + for (const accelerationPosition of this.accelerationPositions) { + accelerationPosition.offset = this.rightPosition; + } + setTimeout(applyPositions, 100); + setTimeout(() => { + this.animatingAcceleration = false; + }, 200); + } else { + applyPositions(); + } + } + mountEmptyBlocks() { const emptyBlocks = []; const numberOfBlocks = this.stateService.env.MEMPOOL_BLOCKS_AMOUNT; diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index ace4ded37..469475873 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -21,7 +21,7 @@ import { ApiService } from '../../services/api.service'; import { SeoService } from '../../services/seo.service'; import { StorageService } from '../../services/storage.service'; import { seoDescriptionNetwork } from '../../shared/common.utils'; -import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration } from '../../interfaces/node-api.interface'; +import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition } from '../../interfaces/node-api.interface'; import { LiquidUnblinding } from './liquid-ublinding'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { Price, PriceService } from '../../services/price.service'; @@ -38,6 +38,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { txId: string; txInBlockIndex: number; mempoolPosition: MempoolPosition; + accelerationPositions: AccelerationPosition[]; isLoadingTx = true; error: any = undefined; errorUnblinded: any = undefined; @@ -265,10 +266,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.now = Date.now(); if (txPosition && txPosition.txid === this.txId && txPosition.position) { this.mempoolPosition = txPosition.position; + this.accelerationPositions = txPosition.accelerationPositions; if (this.tx && !this.tx.status.confirmed) { + const txFeePerVSize = this.getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated); this.stateService.markBlock$.next({ txid: txPosition.txid, - mempoolPosition: this.mempoolPosition + txFeePerVSize, + mempoolPosition: this.mempoolPosition, + accelerationPositions: this.accelerationPositions, }); this.txInBlockIndex = this.mempoolPosition.block; @@ -278,6 +283,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } } else { this.mempoolPosition = null; + this.accelerationPositions = null; } }); @@ -400,11 +406,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { }); this.fetchCpfp$.next(this.tx.txid); } else { + const txFeePerVSize = this.getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated); if (tx.cpfpChecked) { this.stateService.markBlock$.next({ txid: tx.txid, - txFeePerVSize: tx.effectiveFeePerVsize, + txFeePerVSize, mempoolPosition: this.mempoolPosition, + accelerationPositions: this.accelerationPositions, }); this.cpfpInfo = { ancestors: tx.ancestors, @@ -619,6 +627,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.accelerationInfo = null; this.txInBlockIndex = null; this.mempoolPosition = null; + this.accelerationPositions = null; document.body.scrollTo(0, 0); this.leaveTransaction(); } @@ -632,6 +641,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1); } + getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean): number { + if (accelerated) { + let ancestorVsize = tx.weight / 4; + let ancestorFee = tx.fee; + for (const ancestor of tx.ancestors || []) { + ancestorVsize += (ancestor.weight / 4); + ancestorFee += ancestor.fee; + } + return Math.min(tx.fee / (tx.weight / 4), (ancestorFee / ancestorVsize)); + } else { + return tx.effectiveFeePerVsize; + } + } + setupGraph() { this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1)); this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80); diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 9d936722d..cd2a3774c 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -196,6 +196,11 @@ export interface MempoolPosition { accelerated?: boolean } +export interface AccelerationPosition extends MempoolPosition { + pool: string; + offset?: number; +} + export interface RewardStats { startBlock: number; endBlock: number; diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index f87a3dc31..1d10dbd11 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, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; +import { AccelerationPosition, 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'; @@ -16,6 +16,7 @@ export interface MarkBlockState { mempoolBlockIndex?: number; txFeePerVSize?: number; mempoolPosition?: MempoolPosition; + accelerationPositions?: AccelerationPosition[]; } export interface ILoadingIndicators { [name: string]: number; } @@ -116,7 +117,7 @@ export class StateService { utxoSpent$ = new Subject(); difficultyAdjustment$ = new ReplaySubject(1); mempoolTransactions$ = new Subject(); - mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null}>(); + mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null, accelerationPositions?: AccelerationPosition[] }>(); mempoolRemovedTransactions$ = new Subject(); blockTransactions$ = new Subject(); isLoadingWebSocket$ = new ReplaySubject(1);