Merge pull request #5815 from mempool/natsoni/tx-preview-feedback

Tx preview: trim input and redirect to tx page
This commit is contained in:
mononaut
2025-03-29 16:30:27 +08:00
committed by GitHub
3 changed files with 35 additions and 17 deletions

View File

@@ -30,7 +30,6 @@
</span> </span>
<div class="container-buttons"> <div class="container-buttons">
<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()">&#10005;</button> <button class="btn btn-sm" style="margin-left: 10px; padding: 0;" (click)="resetForm()">&#10005;</button>
</div> </div>
@@ -40,14 +39,18 @@
<div class="clearfix"></div> <div class="clearfix"></div>
<div *ngIf="!successBroadcast" class="alert alert-mempool" style="align-items: center;"> <div class="alert alert-mempool" style="align-items: center;">
<span> <span>
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
<ng-container i18n="transaction.local-tx|This transaction is stored locally in your browser."> <ng-container *ngIf="!successBroadcast" i18n="transaction.local-tx|This transaction is stored locally in your browser.">
This transaction is stored locally in your browser. Broadcast it to add it to the mempool. This transaction is stored locally in your browser. Broadcast it to add it to the mempool.
</ng-container> </ng-container>
<ng-container *ngIf="successBroadcast" i18n="transaction.redirecting|Redirecting to transaction page...">
Redirecting to transaction page...
</ng-container>
</span> </span>
<button [disabled]="isLoadingBroadcast" type="button" class="btn btn-sm btn-primary" i18n="transaction.broadcast|Broadcast" (click)="postTx()">Broadcast</button> <button *ngIf="!successBroadcast" [disabled]="isLoadingBroadcast" type="button" class="btn btn-sm btn-primary btn-broadcast" i18n="transaction.broadcast|Broadcast" (click)="postTx()">Broadcast</button>
<button *ngIf="successBroadcast" type="button" class="btn btn-sm btn-success no-cursor btn-broadcast" i18n="transaction.broadcasted|Broadcasted">Broadcasted</button>
</div> </div>
@if (!hasPrevouts) { @if (!hasPrevouts) {

View File

@@ -192,3 +192,11 @@
cursor: default !important; cursor: default !important;
pointer-events: none; pointer-events: none;
} }
.btn-broadcast {
margin-left: 5px;
@media (max-width: 567px) {
margin-left: 0;
margin-top: 5px;
}
}

View File

@@ -3,7 +3,7 @@ import { Transaction, Vout } from '@interfaces/electrs.interface';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { Filter, toFilters } from '../../shared/filters.utils'; import { Filter, toFilters } from '../../shared/filters.utils';
import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils'; import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils';
import { firstValueFrom, Subscription } from 'rxjs'; import { catchError, firstValueFrom, Subscription, switchMap, tap, throwError, timer } from 'rxjs';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
@@ -36,6 +36,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
isLoadingBroadcast: boolean; isLoadingBroadcast: boolean;
errorBroadcast: string; errorBroadcast: string;
successBroadcast: boolean; successBroadcast: boolean;
broadcastSubscription: Subscription;
isMobile: boolean; isMobile: boolean;
@ViewChild('graphContainer') @ViewChild('graphContainer')
@@ -82,7 +83,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
this.resetState(); this.resetState();
this.isLoading = true; this.isLoading = true;
try { try {
const { tx, hex } = decodeRawTransaction(this.pushTxForm.get('txRaw').value, this.stateService.network); const { tx, hex } = decodeRawTransaction(this.pushTxForm.get('txRaw').value.trim(), this.stateService.network);
await this.fetchPrevouts(tx); await this.fetchPrevouts(tx);
await this.fetchCpfpInfo(tx); await this.fetchCpfpInfo(tx);
this.processTransaction(tx, hex); this.processTransaction(tx, hex);
@@ -207,18 +208,22 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
}); });
} }
async postTx(): Promise<string> { postTx(): void {
this.isLoadingBroadcast = true; this.isLoadingBroadcast = true;
this.errorBroadcast = null; this.errorBroadcast = null;
return new Promise((resolve, reject) => {
this.apiService.postTransaction$(this.rawHexTransaction) this.broadcastSubscription = this.apiService.postTransaction$(this.rawHexTransaction).pipe(
.subscribe((result) => { tap((txid: string) => {
this.isLoadingBroadcast = false; this.isLoadingBroadcast = false;
this.successBroadcast = true; this.successBroadcast = true;
this.transaction.txid = result; this.transaction.txid = txid;
resolve(result); }),
}, switchMap((txid: string) =>
(error) => { timer(2000).pipe(
tap(() => this.router.navigate([this.relativeUrlPipe.transform('/tx/' + txid)])),
)
),
catchError((error) => {
if (typeof error.error === 'string') { if (typeof error.error === 'string') {
const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"'); const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"');
this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error); this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error);
@@ -226,9 +231,9 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + error.message; this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + error.message;
} }
this.isLoadingBroadcast = false; this.isLoadingBroadcast = false;
reject(this.error); return throwError(() => error);
}); })
}); ).subscribe();
} }
resetState() { resetState() {
@@ -253,6 +258,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
this.missingPrevouts = []; this.missingPrevouts = [];
this.stateService.markBlock$.next({}); this.stateService.markBlock$.next({});
this.mempoolBlocksSubscription?.unsubscribe(); this.mempoolBlocksSubscription?.unsubscribe();
this.broadcastSubscription?.unsubscribe();
} }
resetForm() { resetForm() {
@@ -308,6 +314,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
this.mempoolBlocksSubscription?.unsubscribe(); this.mempoolBlocksSubscription?.unsubscribe();
this.flowPrefSubscription?.unsubscribe(); this.flowPrefSubscription?.unsubscribe();
this.stateService.markBlock$.next({}); this.stateService.markBlock$.next({});
this.broadcastSubscription?.unsubscribe();
} }
} }