diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html
index 2748b9ffc..188868b11 100644
--- a/frontend/src/app/components/transaction/transaction.component.html
+++ b/frontend/src/app/components/transaction/transaction.component.html
@@ -70,6 +70,26 @@
+
+ Mining |
+
+
+ {{ pool.name }}
+
+
+ Coinbase
+ Expected in Block
+ Seen in Mempool
+ Not seen in Mempool
+ Added
+ Conflict
+
+ |
+
+
+ |
+
@@ -509,7 +529,7 @@
-
+
Fee |
{{ tx.fee | number }} sat |
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..0167a3d43 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,22 @@ 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;
+ coinbase?: boolean;
+}
+
@Component({
selector: 'app-transaction',
templateUrl: './transaction.component.html',
@@ -58,6 +75,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 +85,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 +121,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 +288,54 @@ 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);
+ 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 => {
+ return block.extras.pool;
+ }),
+ catchError(() => {
+ return of(null);
+ })
+ ),
+ fetchAudit ? 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(isCoinbase ? { coinbase: true } : 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 +466,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 +524,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 +678,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 +720,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 +809,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.mempoolPositionSubscription.unsubscribe();
this.mempoolBlocksSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
+ this.miningSubscription?.unsubscribe();
this.leaveTransaction();
}
}