From c17b77fe31b3815d0d74855c9d5d5b5789cbc9a9 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 7 Mar 2024 23:21:27 +0000 Subject: [PATCH 1/2] Display basic mining info on transaction page --- .../transaction/transaction.component.html | 21 +++- .../transaction/transaction.component.scss | 4 + .../transaction/transaction.component.ts | 99 ++++++++++++++++++- 3 files changed, 121 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 2748b9ffc..211149b9d 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -70,6 +70,25 @@ + + Mining + + + {{ pool.name }} + + + Expected in Block + Seen in Mempool + Not seen in Mempool + Added + Conflict + + + + + + @@ -509,7 +528,7 @@ - + diff --git a/frontend/src/app/components/transaction/transaction.component.scss b/frontend/src/app/components/transaction/transaction.component.scss index d78edf85b..d24d57a93 100644 --- a/frontend/src/app/components/transaction/transaction.component.scss +++ b/frontend/src/app/components/transaction/transaction.component.scss @@ -149,6 +149,10 @@ .btn { display: block; } + + &.wrap-cell { + white-space: normal; + } } } diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 60797a9a1..eb9f49995 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -8,10 +8,11 @@ import { retryWhen, delay, mergeMap, - tap + tap, + map } from 'rxjs/operators'; import { Transaction } from '../../interfaces/electrs.interface'; -import { of, merge, Subscription, Observable, Subject, from, throwError } from 'rxjs'; +import { of, merge, Subscription, Observable, Subject, from, throwError, combineLatest } from 'rxjs'; import { StateService } from '../../services/state.service'; import { CacheService } from '../../services/cache.service'; import { WebsocketService } from '../../services/websocket.service'; @@ -28,6 +29,21 @@ import { isFeatureActive } from '../../bitcoin.utils'; import { ServicesApiServices } from '../../services/services-api.service'; import { EnterpriseService } from '../../services/enterprise.service'; +interface Pool { + id: number; + name: string; + slug: string; +} + +interface AuditStatus { + seen: boolean; + expected: boolean; + added: boolean; + delayed?: number; + accelerated: boolean; + conflict: boolean; +} + @Component({ selector: 'app-transaction', templateUrl: './transaction.component.html', @@ -58,6 +74,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { urlFragmentSubscription: Subscription; mempoolBlocksSubscription: Subscription; blocksSubscription: Subscription; + miningSubscription: Subscription; fragmentParams: URLSearchParams; rbfTransaction: undefined | Transaction; replaced: boolean = false; @@ -67,11 +84,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { accelerationInfo: Acceleration | null = null; sigops: number | null; adjustedVsize: number | null; + pool: Pool | null; + auditStatus: AuditStatus | null; showCpfpDetails = false; fetchCpfp$ = new Subject(); fetchRbfHistory$ = new Subject(); fetchCachedTx$ = new Subject(); fetchAcceleration$ = new Subject(); + fetchMiningInfo$ = new Subject<{ hash: string, height: number, txid: string }>(); isCached: boolean = false; now = Date.now(); da$: Observable; @@ -100,6 +120,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === ''; showAccelerationSummary = false; scrollIntoAccelPreview = false; + auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true; @ViewChild('graphContainer') graphContainer: ElementRef; @@ -266,6 +287,52 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } }); + this.miningSubscription = this.fetchMiningInfo$.pipe( + filter((target) => target.txid === this.txId), + tap(() => { + this.pool = null; + this.auditStatus = null; + }), + switchMap(({ hash, height, txid }) => { + const foundBlock = this.cacheService.getCachedBlock(height) || null; + const auditAvailable = this.isAuditAvailable(height); + return combineLatest([ + foundBlock ? of(foundBlock.extras.pool) : this.apiService.getBlock$(hash).pipe( + map(block => { + return block.extras.pool; + }), + catchError(() => { + return of(null); + }) + ), + auditAvailable ? this.apiService.getBlockAudit$(hash).pipe( + map(audit => { + const isAdded = audit.addedTxs.includes(txid); + const isAccelerated = audit.acceleratedTxs.includes(txid); + const isConflict = audit.fullrbfTxs.includes(txid); + const isExpected = audit.template.some(tx => tx.txid === txid); + return { + seen: isExpected || !(isAdded || isConflict), + expected: isExpected, + added: isAdded, + conflict: isConflict, + accelerated: isAccelerated, + }; + }), + catchError(() => { + return of(null); + }) + ) : of(null) + ]); + }), + catchError(() => { + return of(null); + }) + ).subscribe(([pool, auditStatus]) => { + this.pool = pool; + this.auditStatus = auditStatus; + }); + this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => { this.now = Date.now(); if (txPosition && txPosition.txid === this.txId && txPosition.position) { @@ -396,6 +463,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } } else { this.fetchAcceleration$.next(tx.status.block_hash); + this.fetchMiningInfo$.next({ hash: tx.status.block_hash, height: tx.status.block_height, txid: tx.txid }); this.transactionTime = 0; } @@ -453,6 +521,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.audioService.playSound('magic'); } this.fetchAcceleration$.next(block.id); + this.fetchMiningInfo$.next({ hash: block.id, height: block.height, txid: this.tx.txid }); } }); @@ -606,6 +675,29 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.featuresEnabled = this.segwitEnabled || this.taprootEnabled || this.rbfEnabled; } + isAuditAvailable(blockHeight: number): boolean { + if (!this.auditEnabled) { + return false; + } + switch (this.stateService.network) { + case 'testnet': + if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) { + return false; + } + break; + case 'signet': + if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) { + return false; + } + break; + default: + if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) { + return false; + } + } + return true; + } + resetTransaction() { this.error = undefined; this.tx = null; @@ -625,6 +717,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.accelerationInfo = null; this.txInBlockIndex = null; this.mempoolPosition = null; + this.pool = null; + this.auditStatus = null; document.body.scrollTo(0, 0); this.leaveTransaction(); } @@ -712,6 +806,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.mempoolPositionSubscription.unsubscribe(); this.mempoolBlocksSubscription.unsubscribe(); this.blocksSubscription.unsubscribe(); + this.miningSubscription?.unsubscribe(); this.leaveTransaction(); } } From 7af185a9197fedac093a8b2ebdba3170f5cdc14c Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 8 Mar 2024 15:21:37 +0000 Subject: [PATCH 2/2] Tx audit tags handle coinbase --- .../transaction/transaction.component.html | 3 ++- .../transaction/transaction.component.ts | 17 ++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 211149b9d..188868b11 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -78,7 +78,8 @@ {{ pool.name }} - Expected in Block + Coinbase + Expected in Block Seen in Mempool Not seen in Mempool Added diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index eb9f49995..0167a3d43 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -36,12 +36,13 @@ interface Pool { } interface AuditStatus { - seen: boolean; - expected: boolean; - added: boolean; + seen?: boolean; + expected?: boolean; + added?: boolean; delayed?: number; - accelerated: boolean; - conflict: boolean; + accelerated?: boolean; + conflict?: boolean; + coinbase?: boolean; } @Component({ @@ -296,6 +297,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { switchMap(({ hash, height, txid }) => { const foundBlock = this.cacheService.getCachedBlock(height) || null; const auditAvailable = this.isAuditAvailable(height); + const isCoinbase = this.tx.vin.some(v => v.is_coinbase); + const fetchAudit = auditAvailable && !isCoinbase; return combineLatest([ foundBlock ? of(foundBlock.extras.pool) : this.apiService.getBlock$(hash).pipe( map(block => { @@ -305,7 +308,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { return of(null); }) ), - auditAvailable ? this.apiService.getBlockAudit$(hash).pipe( + fetchAudit ? this.apiService.getBlockAudit$(hash).pipe( map(audit => { const isAdded = audit.addedTxs.includes(txid); const isAccelerated = audit.acceleratedTxs.includes(txid); @@ -322,7 +325,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { catchError(() => { return of(null); }) - ) : of(null) + ) : of(isCoinbase ? { coinbase: true } : null) ]); }), catchError(() => {
Fee {{ tx.fee | number }} sat