mirror of
https://github.com/Retropex/mempool.git
synced 2025-05-12 18:20:41 +02:00
Preview transaction from raw data
This commit is contained in:
parent
2de16322ae
commit
025b0585b4
@ -0,0 +1,190 @@
|
||||
<div class="container-xl">
|
||||
|
||||
@if (!transaction) {
|
||||
|
||||
<h1 style="margin-top: 19px;" i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</h1>
|
||||
|
||||
<form [formGroup]="pushTxForm" (submit)="decodeTransaction()" novalidate>
|
||||
<div class="mb-3">
|
||||
<textarea formControlName="txRaw" class="form-control" rows="5" i18n-placeholder="transaction.hex" placeholder="Transaction hex"></textarea>
|
||||
</div>
|
||||
<button [disabled]="isLoading" type="submit" class="btn btn-primary mr-2" i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</button>
|
||||
<input type="checkbox" [checked]="!offlineMode" id="offline-mode" (change)="onOfflineModeChange($event)">
|
||||
<label class="label" for="offline-mode">
|
||||
<span i18n="transaction.fetch-prevout-data">Fetch prevout data</span>
|
||||
</label>
|
||||
<p *ngIf="error" class="red-color d-inline">Error decoding transaction, reason: {{ error }}</p>
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (transaction && !error && !isLoading) {
|
||||
<div class="title-block">
|
||||
<h1 i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</h1>
|
||||
|
||||
<span class="tx-link">
|
||||
<span class="txid">
|
||||
<app-truncate [text]="transaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, transaction.txid]" [disabled]="!successBroadcast">
|
||||
<app-clipboard [text]="transaction.txid"></app-clipboard>
|
||||
</app-truncate>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="container-buttons">
|
||||
<button *ngIf="!successBroadcast" [disabled]="isLoadingBroadcast" type="button" class="btn btn-sm btn-primary" i18n="transaction.broadcast|Broadcast" (click)="postTx()">Broadcast</button>
|
||||
<button *ngIf="successBroadcast" type="button" class="btn btn-sm btn-success no-cursor" i18n="transaction.broadcasted|Broadcasted">Broadcasted</button>
|
||||
<button class="btn btn-sm" style="margin-left: 10px; padding: 0;" (click)="resetForm()">✕</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<p class="red-color d-inline">{{ errorBroadcast }}</p>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@if (!hasPrevouts) {
|
||||
<div class="alert alert-mempool">
|
||||
<span><strong>This transaction is missing prevouts data</strong>. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<app-transaction-details
|
||||
[network]="stateService.network"
|
||||
[tx]="transaction"
|
||||
[isLoadingTx]="false"
|
||||
[isMobile]="isMobile"
|
||||
[isLoadingFirstSeen]="false"
|
||||
[featuresEnabled]="true"
|
||||
[filters]="filters"
|
||||
[hasEffectiveFeeRate]="false"
|
||||
[cpfpInfo]="null"
|
||||
[hasCpfp]="false"
|
||||
[ETA$]="ETA$"
|
||||
(toggleCpfp$)="this.showCpfpDetails = !this.showCpfpDetails"
|
||||
></app-transaction-details>
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="flowEnabled; else flowPlaceholder">
|
||||
<div class="title float-left">
|
||||
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="toggleGraph()" i18n="hide-diagram">Hide diagram</button>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="box">
|
||||
<div class="graph-container" #graphContainer>
|
||||
<tx-bowtie-graph
|
||||
[tx]="transaction"
|
||||
[cached]="true"
|
||||
[width]="graphWidth"
|
||||
[height]="graphHeight"
|
||||
[lineLimit]="inOutLimit"
|
||||
[maxStrands]="graphExpanded ? maxInOut : 24"
|
||||
[network]="stateService.network"
|
||||
[tooltip]="true"
|
||||
[connectors]="true"
|
||||
[inputIndex]="null" [outputIndex]="null"
|
||||
>
|
||||
</tx-bowtie-graph>
|
||||
</div>
|
||||
<div class="toggle-wrapper" *ngIf="maxInOut > 24">
|
||||
<button class="btn btn-sm btn-primary graph-toggle" (click)="expandGraph();" *ngIf="!graphExpanded; else collapseBtn"><span i18n="show-more">Show more</span></button>
|
||||
<ng-template #collapseBtn>
|
||||
<button class="btn btn-sm btn-primary graph-toggle" (click)="collapseGraph();"><span i18n="show-less">Show less</span></button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
</ng-container>
|
||||
<ng-template #flowPlaceholder>
|
||||
<div class="box hidden">
|
||||
<div class="graph-container" #graphContainer>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div class="subtitle-block">
|
||||
<div class="title">
|
||||
<h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
|
||||
</div>
|
||||
|
||||
<div class="title-buttons">
|
||||
<button *ngIf="!flowEnabled" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show-diagram">Show diagram</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm" (click)="txList.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<app-transactions-list #txList [transactions]="[transaction]" [transactionPage]="true" [txPreview]="true"></app-transactions-list>
|
||||
|
||||
<div class="title text-left">
|
||||
<h2 i18n="transaction.details|Transaction Details">Details</h2>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="block.size">Size</td>
|
||||
<td [innerHTML]="'‎' + (transaction.size | bytes: 2)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||
<td [innerHTML]="'‎' + (transaction.weight / 4 | vbytes: 2)"></td>
|
||||
</tr>
|
||||
<tr *ngIf="adjustedVsize">
|
||||
<td><ng-container i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize</ng-container>
|
||||
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-is-adjusted-vsize">
|
||||
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
</td>
|
||||
<td [innerHTML]="'‎' + (adjustedVsize | vbytes: 2)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.weight">Weight</td>
|
||||
<td [innerHTML]="'‎' + (transaction.weight | wuBytes: 2)"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="transaction.version">Version</td>
|
||||
<td [innerHTML]="'‎' + (transaction.version | number)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.locktime">Locktime</td>
|
||||
<td [innerHTML]="'‎' + (transaction.locktime | number)"></td>
|
||||
</tr>
|
||||
<tr *ngIf="transaction.sigops >= 0">
|
||||
<td><ng-container i18n="transaction.sigops|Transaction Sigops">Sigops</ng-container>
|
||||
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-are-sigops">
|
||||
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
</td>
|
||||
<td [innerHTML]="'‎' + (transaction.sigops | number)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.hex">Transaction hex</td>
|
||||
<td><app-clipboard [text]="pushTxForm.get('txRaw').value" [leftPadding]="false"></app-clipboard></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isLoading) {
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-light mt-2 mb-2"></div>
|
||||
<h3 i18n="transaction.error.loading-prevouts">Loading transaction prevouts ({{ prevoutsLoadedCount }} / {{ prevoutsCount }})</h3>
|
||||
</div>
|
||||
}
|
||||
</div>
|
@ -0,0 +1,194 @@
|
||||
.label {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.container-buttons {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.title-block {
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
@media (min-width: 650px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
h1 {
|
||||
margin: 0rem;
|
||||
margin-right: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tx-link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: baseline;
|
||||
width: 0;
|
||||
max-width: 100%;
|
||||
margin-right: 0px;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 8px;
|
||||
@media (min-width: 651px) {
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0px;
|
||||
margin-right: 1em;
|
||||
top: 1px;
|
||||
position: relative;
|
||||
}
|
||||
@media (max-width: 650px) {
|
||||
width: 100%;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.txid {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: column;
|
||||
@media (min-width: 850px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.box.hidden {
|
||||
visibility: hidden;
|
||||
height: 0px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: var(--stat-box-bg);
|
||||
padding: 10px 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.toggle-wrapper {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 1.25em 0 0;
|
||||
}
|
||||
|
||||
.graph-toggle {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
tr td {
|
||||
padding: 0.75rem 0.5rem;
|
||||
@media (min-width: 576px) {
|
||||
padding: 0.75rem 0.75rem;
|
||||
}
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
@media (min-width: 850px) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.wrap-cell {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.effective-fee-container {
|
||||
display: block;
|
||||
@media (min-width: 768px){
|
||||
display: inline-block;
|
||||
}
|
||||
@media (max-width: 425px){
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.effective-fee-rating {
|
||||
@media (max-width: 767px){
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
h2 {
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline-info {
|
||||
margin-top: 5px;
|
||||
@media (min-width: 768px){
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.flow-toggle {
|
||||
margin-top: -5px;
|
||||
margin-left: 10px;
|
||||
@media (min-width: 768px){
|
||||
display: inline-block;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle-block {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title-buttons {
|
||||
flex-shrink: 1;
|
||||
text-align: right;
|
||||
.btn {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cpfp-details {
|
||||
.txids {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.txids {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.no-cursor {
|
||||
cursor: default !important;
|
||||
pointer-events: none;
|
||||
}
|
@ -0,0 +1,296 @@
|
||||
import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Transaction } from '@interfaces/electrs.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Filter, toFilters } from '../../shared/filters.utils';
|
||||
import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils';
|
||||
import { ETA, EtaService } from '../../services/eta.service';
|
||||
import { combineLatest, firstValueFrom, map, Observable, startWith, Subscription } from 'rxjs';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transaction-raw',
|
||||
templateUrl: './transaction-raw.component.html',
|
||||
styleUrls: ['./transaction-raw.component.scss'],
|
||||
})
|
||||
export class TransactionRawComponent implements OnInit, OnDestroy {
|
||||
|
||||
pushTxForm: UntypedFormGroup;
|
||||
isLoading: boolean;
|
||||
offlineMode: boolean = false;
|
||||
transaction: Transaction;
|
||||
error: string;
|
||||
errorPrevouts: string;
|
||||
hasPrevouts: boolean;
|
||||
prevoutsLoadedCount: number = 0;
|
||||
prevoutsCount: number;
|
||||
isLoadingBroadcast: boolean;
|
||||
errorBroadcast: string;
|
||||
successBroadcast: boolean;
|
||||
|
||||
isMobile: boolean;
|
||||
@ViewChild('graphContainer')
|
||||
graphContainer: ElementRef;
|
||||
graphExpanded: boolean = false;
|
||||
graphWidth: number = 1068;
|
||||
graphHeight: number = 360;
|
||||
inOutLimit: number = 150;
|
||||
maxInOut: number = 0;
|
||||
flowPrefSubscription: Subscription;
|
||||
hideFlow: boolean = this.stateService.hideFlow.value;
|
||||
flowEnabled: boolean;
|
||||
adjustedVsize: number;
|
||||
filters: Filter[] = [];
|
||||
showCpfpDetails = false;
|
||||
ETA$: Observable<ETA | null>;
|
||||
mempoolBlocksSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
public route: ActivatedRoute,
|
||||
public router: Router,
|
||||
public stateService: StateService,
|
||||
public etaService: EtaService,
|
||||
public electrsApi: ElectrsApiService,
|
||||
public websocketService: WebsocketService,
|
||||
public formBuilder: UntypedFormBuilder,
|
||||
public cd: ChangeDetectorRef,
|
||||
public seoService: SeoService,
|
||||
public apiService: ApiService,
|
||||
public relativeUrlPipe: RelativeUrlPipe,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@meta.title.preview-tx:Preview Transaction`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.preview-tx:Preview a transaction to the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network using the transaction's raw hex data.`);
|
||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||
this.pushTxForm = this.formBuilder.group({
|
||||
txRaw: ['', Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
async decodeTransaction(): Promise<void> {
|
||||
this.resetState();
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const tx = decodeRawTransaction(this.pushTxForm.get('txRaw').value, this.stateService.network);
|
||||
await this.fetchPrevouts(tx);
|
||||
this.processTransaction(tx);
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchPrevouts(transaction: Transaction): Promise<void> {
|
||||
if (this.offlineMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.prevoutsCount = transaction.vin.filter(input => !input.is_coinbase).length;
|
||||
if (this.prevoutsCount === 0) {
|
||||
this.hasPrevouts = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const txsToFetch: { [txid: string]: number } = transaction.vin.reduce((acc, input) => {
|
||||
if (!input.is_coinbase) {
|
||||
acc[input.txid] = (acc[input.txid] || 0) + 1;
|
||||
}
|
||||
return acc;
|
||||
}, {} as { [txid: string]: number });
|
||||
|
||||
try {
|
||||
|
||||
if (Object.keys(txsToFetch).length > 20) {
|
||||
throw new Error($localize`:@@transaction.too-many-prevouts:Too many transactions to fetch (${Object.keys(txsToFetch).length})`);
|
||||
}
|
||||
|
||||
const fetchedTransactions = await Promise.all(
|
||||
Object.keys(txsToFetch).map(txid =>
|
||||
firstValueFrom(this.electrsApi.getTransaction$(txid))
|
||||
.then(response => {
|
||||
this.prevoutsLoadedCount += txsToFetch[txid];
|
||||
this.cd.markForCheck();
|
||||
return response;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const transactionsMap = fetchedTransactions.reduce((acc, transaction) => {
|
||||
acc[transaction.txid] = transaction;
|
||||
return acc;
|
||||
}, {} as { [txid: string]: any });
|
||||
|
||||
const prevouts = transaction.vin.map((input, index) => ({ index, prevout: transactionsMap[input.txid]?.vout[input.vout] || null}));
|
||||
|
||||
transaction.vin = transaction.vin.map((input, index) => {
|
||||
if (!input.is_coinbase) {
|
||||
input.prevout = prevouts.find(p => p.index === index)?.prevout;
|
||||
addInnerScriptsToVin(input);
|
||||
}
|
||||
return input;
|
||||
});
|
||||
this.hasPrevouts = true;
|
||||
} catch (error) {
|
||||
this.errorPrevouts = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
processTransaction(tx: Transaction): void {
|
||||
this.transaction = tx;
|
||||
|
||||
if (this.hasPrevouts) {
|
||||
this.transaction.fee = this.transaction.vin.some(input => input.is_coinbase)
|
||||
? 0
|
||||
: this.transaction.vin.reduce((fee, input) => {
|
||||
return fee + (input.prevout?.value || 0);
|
||||
}, 0) - this.transaction.vout.reduce((sum, output) => sum + output.value, 0);
|
||||
this.transaction.feePerVsize = this.transaction.fee / (this.transaction.weight / 4);
|
||||
}
|
||||
|
||||
this.transaction.flags = getTransactionFlags(this.transaction, null, null, null, this.stateService.network);
|
||||
this.filters = this.transaction.flags ? toFilters(this.transaction.flags).filter(f => f.txPage) : [];
|
||||
this.transaction.sigops = countSigops(this.transaction);
|
||||
if (this.transaction.sigops >= 0) {
|
||||
this.adjustedVsize = Math.max(this.transaction.weight / 4, this.transaction.sigops * 5);
|
||||
}
|
||||
|
||||
this.setupGraph();
|
||||
this.setFlowEnabled();
|
||||
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
|
||||
this.hideFlow = !!hide;
|
||||
this.setFlowEnabled();
|
||||
});
|
||||
this.setGraphSize();
|
||||
|
||||
this.ETA$ = combineLatest([
|
||||
this.stateService.mempoolTxPosition$.pipe(startWith(null)),
|
||||
this.stateService.mempoolBlocks$.pipe(startWith(null)),
|
||||
this.stateService.difficultyAdjustment$.pipe(startWith(null)),
|
||||
]).pipe(
|
||||
map(([position, mempoolBlocks, da]) => {
|
||||
return this.etaService.calculateETA(
|
||||
this.stateService.network,
|
||||
this.transaction,
|
||||
mempoolBlocks,
|
||||
position,
|
||||
da,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe(() => {
|
||||
if (this.transaction) {
|
||||
this.stateService.markBlock$.next({
|
||||
txid: this.transaction.txid,
|
||||
txFeePerVSize: this.transaction.feePerVsize,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async postTx(): Promise<string> {
|
||||
this.isLoadingBroadcast = true;
|
||||
this.errorBroadcast = null;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.apiService.postTransaction$(this.pushTxForm.get('txRaw').value)
|
||||
.subscribe((result) => {
|
||||
this.isLoadingBroadcast = false;
|
||||
this.successBroadcast = true;
|
||||
resolve(result);
|
||||
},
|
||||
(error) => {
|
||||
if (typeof error.error === 'string') {
|
||||
const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"');
|
||||
this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error);
|
||||
} else if (error.message) {
|
||||
this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + error.message;
|
||||
}
|
||||
this.isLoadingBroadcast = false;
|
||||
reject(this.error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resetState() {
|
||||
this.transaction = null;
|
||||
this.error = null;
|
||||
this.errorPrevouts = null;
|
||||
this.errorBroadcast = null;
|
||||
this.successBroadcast = false;
|
||||
this.isLoading = false;
|
||||
this.adjustedVsize = null;
|
||||
this.filters = [];
|
||||
this.hasPrevouts = false;
|
||||
this.prevoutsLoadedCount = 0;
|
||||
this.prevoutsCount = 0;
|
||||
this.stateService.markBlock$.next({});
|
||||
this.mempoolBlocksSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
this.resetState();
|
||||
this.pushTxForm.reset();
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
setGraphSize(): void {
|
||||
this.isMobile = window.innerWidth < 850;
|
||||
if (this.graphContainer?.nativeElement && this.stateService.isBrowser) {
|
||||
setTimeout(() => {
|
||||
if (this.graphContainer?.nativeElement?.clientWidth) {
|
||||
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
|
||||
} else {
|
||||
setTimeout(() => { this.setGraphSize(); }, 1);
|
||||
}
|
||||
}, 1);
|
||||
} else {
|
||||
setTimeout(() => { this.setGraphSize(); }, 1);
|
||||
}
|
||||
}
|
||||
|
||||
setupGraph() {
|
||||
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.transaction?.vin?.length || 1, this.transaction?.vout?.length + 1 || 1));
|
||||
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);
|
||||
}
|
||||
|
||||
toggleGraph() {
|
||||
const showFlow = !this.flowEnabled;
|
||||
this.stateService.hideFlow.next(!showFlow);
|
||||
}
|
||||
|
||||
setFlowEnabled() {
|
||||
this.flowEnabled = !this.hideFlow;
|
||||
}
|
||||
|
||||
expandGraph() {
|
||||
this.graphExpanded = true;
|
||||
this.graphHeight = this.maxInOut * 15;
|
||||
}
|
||||
|
||||
collapseGraph() {
|
||||
this.graphExpanded = false;
|
||||
this.graphHeight = Math.min(360, this.maxInOut * 80);
|
||||
}
|
||||
|
||||
onOfflineModeChange(e): void {
|
||||
this.offlineMode = !e.target.checked;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.mempoolBlocksSubscription?.unsubscribe();
|
||||
this.flowPrefSubscription?.unsubscribe();
|
||||
this.stateService.markBlock$.next({});
|
||||
}
|
||||
|
||||
}
|
@ -9,6 +9,7 @@ import { TransactionExtrasModule } from '@components/transaction/transaction-ext
|
||||
import { GraphsModule } from '@app/graphs/graphs.module';
|
||||
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
|
||||
import { AccelerateFeeGraphComponent } from '@components/accelerate-checkout/accelerate-fee-graph.component';
|
||||
import { TransactionRawComponent } from '@components/transaction/transaction-raw.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -16,6 +17,10 @@ const routes: Routes = [
|
||||
redirectTo: '/',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
component: TransactionRawComponent,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: TransactionComponent,
|
||||
@ -49,6 +54,7 @@ export class TransactionRoutingModule { }
|
||||
TransactionDetailsComponent,
|
||||
AccelerateCheckout,
|
||||
AccelerateFeeGraphComponent,
|
||||
TransactionRawComponent,
|
||||
],
|
||||
exports: [
|
||||
TransactionComponent,
|
||||
|
@ -37,6 +37,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
@Input() addresses: string[] = [];
|
||||
@Input() rowLimit = 12;
|
||||
@Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block
|
||||
@Input() txPreview = false;
|
||||
|
||||
@Output() loadMore = new EventEmitter();
|
||||
|
||||
@ -81,7 +82,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
this.refreshOutspends$
|
||||
.pipe(
|
||||
switchMap((txIds) => {
|
||||
if (!this.cached) {
|
||||
if (!this.cached && !this.txPreview) {
|
||||
// break list into batches of 50 (maximum supported by esplora)
|
||||
const batches = [];
|
||||
for (let i = 0; i < txIds.length; i += 50) {
|
||||
@ -119,7 +120,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
),
|
||||
this.refreshChannels$
|
||||
.pipe(
|
||||
filter(() => this.stateService.networkSupportsLightning()),
|
||||
filter(() => this.stateService.networkSupportsLightning() && !this.txPreview),
|
||||
switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)),
|
||||
catchError((error) => {
|
||||
// handle 404
|
||||
@ -187,7 +188,10 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
this.transactionsLength = this.transactions.length;
|
||||
|
||||
if (!this.txPreview) {
|
||||
this.cacheService.setTxCache(this.transactions);
|
||||
}
|
||||
|
||||
const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
|
||||
this.transactions.forEach((tx) => {
|
||||
@ -347,7 +351,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
loadMoreInputs(tx: Transaction): void {
|
||||
if (!tx['@vinLoaded']) {
|
||||
if (!tx['@vinLoaded'] && !this.txPreview) {
|
||||
this.electrsApiService.getTransaction$(tx.txid)
|
||||
.subscribe((newTx) => {
|
||||
tx['@vinLoaded'] = true;
|
||||
|
@ -14,7 +14,7 @@ class GuardService {
|
||||
trackerGuard(route: Route, segments: UrlSegment[]): boolean {
|
||||
const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode;
|
||||
const path = this.router.getCurrentNavigation()?.extractedUrl.root.children.primary.segments;
|
||||
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test'].includes(path[1].path));
|
||||
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test', 'preview'].includes(path[1].path));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,6 +76,7 @@
|
||||
<p><a [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.recent-blocks">Recent Blocks</a></p>
|
||||
<p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
|
||||
<p><a [routerLink]="['/tx/test' | relativeUrl]" i18n="shared.test-transaction|Test Transaction">Test Transaction</a></p>
|
||||
<p><a [routerLink]="['/tx/preview' | relativeUrl]" i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</a></p>
|
||||
<p *ngIf="officialMempoolSpace"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
|
||||
<p><a [routerLink]="['/docs/api' | relativeUrl]" i18n="footer.api-documentation">API Documentation</a></p>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null" [style.justify-content]="textAlign" [class.inline]="inline">
|
||||
<ng-container *ngIf="link">
|
||||
<a [routerLink]="link" [queryParams]="queryParams" class="truncate-link" [target]="external ? '_blank' : '_self'">
|
||||
<a [routerLink]="link" [queryParams]="queryParams" class="truncate-link" [target]="external ? '_blank' : '_self'" [class.disabled]="disabled">
|
||||
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
@ -37,6 +37,12 @@
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 567px) {
|
||||
|
@ -15,6 +15,7 @@ export class TruncateComponent {
|
||||
@Input() maxWidth: number = null;
|
||||
@Input() inline: boolean = false;
|
||||
@Input() textAlign: 'start' | 'end' = 'start';
|
||||
@Input() disabled: boolean = false;
|
||||
rtl: boolean;
|
||||
|
||||
constructor(
|
||||
|
Loading…
Reference in New Issue
Block a user