Merge pull request #4748 from mempool/mononaut/transaction-mining-info
Display basic mining info on transaction page
This commit is contained in:
commit
ac971d17c7
@ -70,6 +70,26 @@
|
|||||||
<app-tx-features [tx]="tx"></app-tx-features>
|
<app-tx-features [tx]="tx"></app-tx-features>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="network === ''">
|
||||||
|
<td class="td-width" i18n="transaction.mining">Mining</td>
|
||||||
|
<td *ngIf="pool" class="wrap-cell">
|
||||||
|
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge mr-1"
|
||||||
|
[class]="pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
|
{{ pool.name }}
|
||||||
|
</a>
|
||||||
|
<ng-container *ngIf="auditStatus">
|
||||||
|
<span *ngIf="auditStatus.coinbase; else expected" class="badge badge-primary mr-1" i18n="tx-features.tag.coinbase|Coinbase">Coinbase</span>
|
||||||
|
<ng-template #expected><span *ngIf="auditStatus.expected; else seen" class="badge badge-success mr-1" i18n-ngbTooltip="Expected in block tooltip" ngbTooltip="This transaction was projected to be included in the block" placement="bottom" i18n="tx-features.tag.expected|Expected in Block">Expected in Block</span></ng-template>
|
||||||
|
<ng-template #seen><span *ngIf="auditStatus.seen; else notSeen" class="badge badge-success mr-1" i18n-ngbTooltip="Seen in mempool tooltip" ngbTooltip="This transaction was seen in the mempool prior to mining" placement="bottom" i18n="tx-features.tag.seen|Seen in Mempool">Seen in Mempool</span></ng-template>
|
||||||
|
<ng-template #notSeen><span class="badge badge-warning mr-1" i18n-ngbTooltip="Not seen in mempool tooltip" ngbTooltip="This transaction was missing from our mempool prior to mining" placement="bottom" i18n="tx-features.tag.not-seen|Not seen in Mempool">Not seen in Mempool</span></ng-template>
|
||||||
|
<span *ngIf="auditStatus.added" class="badge badge-primary mr-1" i18n-ngbTooltip="Added transaction tooltip" ngbTooltip="This transaction may have been added or prioritized out-of-band" placement="bottom" i18n="tx-features.tag.added|Added">Added</span>
|
||||||
|
<span *ngIf="auditStatus.conflict" class="badge badge-warning mr-1" i18n-ngbTooltip="Conflict in mempool tooltip" ngbTooltip="This transaction conflicted with another version in our mempool" placement="bottom" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!pool">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -509,7 +529,7 @@
|
|||||||
<ng-template #feeTable>
|
<ng-template #feeTable>
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngIf="isMobile && (network === 'liquid' || network === 'liquidtestnet' || !featuresEnabled)"></tr>
|
<tr *ngIf="isMobile && (network === 'liquid' || network === 'liquidtestnet' || !featuresEnabled || network === '')"></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||||
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="tx.fee"></app-fiat></span></td>
|
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="tx.fee"></app-fiat></span></td>
|
||||||
|
@ -149,6 +149,10 @@
|
|||||||
.btn {
|
.btn {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.wrap-cell {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,10 +8,11 @@ import {
|
|||||||
retryWhen,
|
retryWhen,
|
||||||
delay,
|
delay,
|
||||||
mergeMap,
|
mergeMap,
|
||||||
tap
|
tap,
|
||||||
|
map
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
import { Transaction } from '../../interfaces/electrs.interface';
|
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 { StateService } from '../../services/state.service';
|
||||||
import { CacheService } from '../../services/cache.service';
|
import { CacheService } from '../../services/cache.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
@ -28,6 +29,22 @@ import { isFeatureActive } from '../../bitcoin.utils';
|
|||||||
import { ServicesApiServices } from '../../services/services-api.service';
|
import { ServicesApiServices } from '../../services/services-api.service';
|
||||||
import { EnterpriseService } from '../../services/enterprise.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({
|
@Component({
|
||||||
selector: 'app-transaction',
|
selector: 'app-transaction',
|
||||||
templateUrl: './transaction.component.html',
|
templateUrl: './transaction.component.html',
|
||||||
@ -58,6 +75,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
urlFragmentSubscription: Subscription;
|
urlFragmentSubscription: Subscription;
|
||||||
mempoolBlocksSubscription: Subscription;
|
mempoolBlocksSubscription: Subscription;
|
||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
|
miningSubscription: Subscription;
|
||||||
fragmentParams: URLSearchParams;
|
fragmentParams: URLSearchParams;
|
||||||
rbfTransaction: undefined | Transaction;
|
rbfTransaction: undefined | Transaction;
|
||||||
replaced: boolean = false;
|
replaced: boolean = false;
|
||||||
@ -67,11 +85,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
accelerationInfo: Acceleration | null = null;
|
accelerationInfo: Acceleration | null = null;
|
||||||
sigops: number | null;
|
sigops: number | null;
|
||||||
adjustedVsize: number | null;
|
adjustedVsize: number | null;
|
||||||
|
pool: Pool | null;
|
||||||
|
auditStatus: AuditStatus | null;
|
||||||
showCpfpDetails = false;
|
showCpfpDetails = false;
|
||||||
fetchCpfp$ = new Subject<string>();
|
fetchCpfp$ = new Subject<string>();
|
||||||
fetchRbfHistory$ = new Subject<string>();
|
fetchRbfHistory$ = new Subject<string>();
|
||||||
fetchCachedTx$ = new Subject<string>();
|
fetchCachedTx$ = new Subject<string>();
|
||||||
fetchAcceleration$ = new Subject<string>();
|
fetchAcceleration$ = new Subject<string>();
|
||||||
|
fetchMiningInfo$ = new Subject<{ hash: string, height: number, txid: string }>();
|
||||||
isCached: boolean = false;
|
isCached: boolean = false;
|
||||||
now = Date.now();
|
now = Date.now();
|
||||||
da$: Observable<DifficultyAdjustment>;
|
da$: Observable<DifficultyAdjustment>;
|
||||||
@ -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 === '';
|
acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
|
||||||
showAccelerationSummary = false;
|
showAccelerationSummary = false;
|
||||||
scrollIntoAccelPreview = false;
|
scrollIntoAccelPreview = false;
|
||||||
|
auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true;
|
||||||
|
|
||||||
@ViewChild('graphContainer')
|
@ViewChild('graphContainer')
|
||||||
graphContainer: ElementRef;
|
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.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
|
||||||
this.now = Date.now();
|
this.now = Date.now();
|
||||||
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
|
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
|
||||||
@ -396,6 +466,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.fetchAcceleration$.next(tx.status.block_hash);
|
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;
|
this.transactionTime = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -453,6 +524,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.audioService.playSound('magic');
|
this.audioService.playSound('magic');
|
||||||
}
|
}
|
||||||
this.fetchAcceleration$.next(block.id);
|
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;
|
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() {
|
resetTransaction() {
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
this.tx = null;
|
this.tx = null;
|
||||||
@ -625,6 +720,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.accelerationInfo = null;
|
this.accelerationInfo = null;
|
||||||
this.txInBlockIndex = null;
|
this.txInBlockIndex = null;
|
||||||
this.mempoolPosition = null;
|
this.mempoolPosition = null;
|
||||||
|
this.pool = null;
|
||||||
|
this.auditStatus = null;
|
||||||
document.body.scrollTo(0, 0);
|
document.body.scrollTo(0, 0);
|
||||||
this.leaveTransaction();
|
this.leaveTransaction();
|
||||||
}
|
}
|
||||||
@ -712,6 +809,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.mempoolPositionSubscription.unsubscribe();
|
this.mempoolPositionSubscription.unsubscribe();
|
||||||
this.mempoolBlocksSubscription.unsubscribe();
|
this.mempoolBlocksSubscription.unsubscribe();
|
||||||
this.blocksSubscription.unsubscribe();
|
this.blocksSubscription.unsubscribe();
|
||||||
|
this.miningSubscription?.unsubscribe();
|
||||||
this.leaveTransaction();
|
this.leaveTransaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user