Add submit package option to tx push page
This commit is contained in:
		
							parent
							
								
									9f0b3bd769
								
							
						
					
					
						commit
						d1741a51c9
					
				| @ -9,4 +9,66 @@ | ||||
|     <p class="red-color d-inline">{{ error }}</p> <a *ngIf="txId" [routerLink]="['/tx/' | relativeUrl, txId]">{{ txId }}</a> | ||||
|   </form> | ||||
| 
 | ||||
|   @if (network === '' || network === 'testnet' || network === 'testnet4' || network === 'signet') { | ||||
|     <br> | ||||
|     <h1 class="text-left" style="margin-top: 1rem;" i18n="shared.submit-transactions|Submit Package">Submit Package</h1> | ||||
| 
 | ||||
|     <form [formGroup]="submitTxsForm" (submit)="submitTxsForm.valid && submitTxs()" novalidate> | ||||
|       <div class="mb-3"> | ||||
|         <textarea formControlName="txs" class="form-control" rows="5" i18n-placeholder="transaction.test-transactions" placeholder="Comma-separated list of raw transactions"></textarea> | ||||
|       </div> | ||||
|       <label i18n="test.tx.max-fee-rate">Maximum fee rate (sat/vB)</label> | ||||
|       <input type="number" class="form-control input-dark" formControlName="maxfeerate" id="maxfeerate" | ||||
|           [value]="10000" placeholder="10,000 s/vb" [class]="{invalid: invalidMaxfeerate}"> | ||||
|       <label i18n="submitpackage.tx.max-burn-amount">Maximum burn amount (sats)</label> | ||||
|       <input type="number" class="form-control input-dark" formControlName="maxburnamount" id="maxburnamount" | ||||
|           [value]="0" placeholder="0 sat" [class]="{invalid: invalidMaxburnamount}"> | ||||
|       <br> | ||||
|       <button [disabled]="isLoadingPackage" type="submit" class="btn btn-primary mr-2" i18n="shared.submit-transactions|Submit Package">Submit Package</button> | ||||
|       <p *ngIf="errorPackage" class="red-color d-inline">{{ errorPackage }}</p> | ||||
|       <p *ngIf="packageMessage" class="d-inline">{{ packageMessage }}</p> | ||||
|        | ||||
|     </form> | ||||
| 
 | ||||
|     <br> | ||||
| 
 | ||||
|     <div class="box" *ngIf="results?.length"> | ||||
|       <table class="accept-results table table-fixed table-borderless table-striped"> | ||||
|         <tbody> | ||||
|           <tr> | ||||
|             <th class="allowed" i18n="test-tx.is-allowed">Allowed?</th> | ||||
|             <th class="txid" i18n="dashboard.latest-transactions.txid">TXID</th> | ||||
|             <th class="rate" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</th> | ||||
|             <th class="reason" i18n="test-tx.rejection-reason">Rejection reason</th> | ||||
|           </tr> | ||||
|           <ng-container *ngFor="let result of results;"> | ||||
|             <tr> | ||||
|               <td class="allowed"> | ||||
|                 @if (result.error == null) { | ||||
|                   <span>✅</span> | ||||
|                 } | ||||
|                 @else { | ||||
|                   <span>❌</span> | ||||
|                 } | ||||
|               </td> | ||||
|               <td class="txid"> | ||||
|                 @if (!result.error) { | ||||
|                   <a [routerLink]="['/tx/' | relativeUrl, result.txid]"><app-truncate [text]="result.txid"></app-truncate></a> | ||||
|                 } @else { | ||||
|                   <app-truncate [text]="result.txid"></app-truncate> | ||||
|                 } | ||||
|               </td> | ||||
|               <td class="rate"> | ||||
|                 <app-fee-rate *ngIf="result.fees?.['effective-feerate'] != null" [fee]="result.fees?.['effective-feerate'] * 100000"></app-fee-rate> | ||||
|                 <span *ngIf="result.fees?.['effective-feerate'] == null">-</span> | ||||
|               </td> | ||||
|               <td class="reason"> | ||||
|                 {{ result.error || '-' }} | ||||
|               </td> | ||||
|             </tr> | ||||
|           </ng-container> | ||||
|         </tbody> | ||||
|       </table> | ||||
|     </div> | ||||
|   } | ||||
| </div> | ||||
| @ -7,6 +7,7 @@ import { OpenGraphService } from '../../services/opengraph.service'; | ||||
| import { seoDescriptionNetwork } from '../../shared/common.utils'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | ||||
| import { TxResult } from '../../interfaces/node-api.interface'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-push-transaction', | ||||
| @ -19,6 +20,16 @@ export class PushTransactionComponent implements OnInit { | ||||
|   txId: string = ''; | ||||
|   isLoading = false; | ||||
| 
 | ||||
|   submitTxsForm: UntypedFormGroup; | ||||
|   errorPackage: string = ''; | ||||
|   packageMessage: string = ''; | ||||
|   results: TxResult[] = []; | ||||
|   invalidMaxfeerate = false; | ||||
|   invalidMaxburnamount = false; | ||||
|   isLoadingPackage = false; | ||||
| 
 | ||||
|   network = this.stateService.network; | ||||
| 
 | ||||
|   constructor( | ||||
|     private formBuilder: UntypedFormBuilder, | ||||
|     private apiService: ApiService, | ||||
| @ -35,6 +46,14 @@ export class PushTransactionComponent implements OnInit { | ||||
|       txHash: ['', Validators.required], | ||||
|     }); | ||||
| 
 | ||||
|     this.submitTxsForm = this.formBuilder.group({ | ||||
|       txs: ['', Validators.required], | ||||
|       maxfeerate: ['', Validators.min(0)], | ||||
|       maxburnamount: ['', Validators.min(0)], | ||||
|     }); | ||||
| 
 | ||||
|     this.stateService.networkChanged$.subscribe((network) => this.network = network); | ||||
| 
 | ||||
|     this.seoService.setTitle($localize`:@@meta.title.push-tx:Broadcast Transaction`); | ||||
|     this.seoService.setDescription($localize`:@@meta.description.push-tx:Broadcast a transaction to the ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} network using the transaction's hash.`); | ||||
|     this.ogService.setManualOgImage('tx-push.jpg'); | ||||
| @ -70,6 +89,67 @@ export class PushTransactionComponent implements OnInit { | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   submitTxs() { | ||||
|     let txs: string[] = []; | ||||
|     try { | ||||
|       txs = (this.submitTxsForm.get('txs')?.value as string).split(',').map(hex => hex.trim()); | ||||
|       if (txs?.length === 1) { | ||||
|         this.pushTxForm.get('txHash').setValue(txs[0]); | ||||
|         this.submitTxsForm.get('txs').setValue(''); | ||||
|         this.postTx(); | ||||
|         return; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       this.errorPackage = e?.message; | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     let maxfeerate; | ||||
|     let maxburnamount; | ||||
|     this.invalidMaxfeerate = false; | ||||
|     this.invalidMaxburnamount = false; | ||||
|     try { | ||||
|       const maxfeerateVal = this.submitTxsForm.get('maxfeerate')?.value; | ||||
|       if (maxfeerateVal != null && maxfeerateVal !== '') { | ||||
|         maxfeerate = parseFloat(maxfeerateVal) / 100_000; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       this.invalidMaxfeerate = true; | ||||
|     } | ||||
|     try { | ||||
|       const maxburnamountVal = this.submitTxsForm.get('maxburnamount')?.value; | ||||
|       if (maxburnamountVal != null && maxburnamountVal !== '') { | ||||
|         maxburnamount = parseInt(maxburnamountVal) / 100_000_000; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       this.invalidMaxburnamount = true; | ||||
|     } | ||||
| 
 | ||||
|     this.isLoadingPackage = true; | ||||
|     this.errorPackage = ''; | ||||
|     this.results = []; | ||||
|     this.apiService.submitPackage$(txs, maxfeerate === 0.1 ? null : maxfeerate, maxburnamount === 0 ? null : maxburnamount) | ||||
|       .subscribe((result) => { | ||||
|         this.isLoadingPackage = false; | ||||
| 
 | ||||
|         this.packageMessage = result['package_msg']; | ||||
|         for (let wtxid in result['tx-results']) { | ||||
|           this.results.push(result['tx-results'][wtxid]); | ||||
|         } | ||||
| 
 | ||||
|         this.submitTxsForm.reset(); | ||||
|       }, | ||||
|       (error) => { | ||||
|         if (typeof error.error?.error === 'string') { | ||||
|           const matchText = error.error.error.replace(/\\/g, '').match('"message":"(.*?)"'); | ||||
|           this.errorPackage = matchText && matchText[1] || error.error.error; | ||||
|         } else if (error.message) { | ||||
|           this.errorPackage = error.message; | ||||
|         } | ||||
|         this.isLoadingPackage = false; | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   private async handleColdcardPushTx(fragmentParams: URLSearchParams): Promise<boolean> { | ||||
|     // maybe conforms to Coldcard nfc-pushtx spec
 | ||||
|     if (fragmentParams && fragmentParams.get('t')) { | ||||
|  | ||||
| @ -453,3 +453,21 @@ export interface TestMempoolAcceptResult { | ||||
|   }, | ||||
|   ['reject-reason']?: string, | ||||
| } | ||||
| 
 | ||||
| export interface SubmitPackageResult { | ||||
|   package_msg: string; | ||||
|   "tx-results": { [wtxid: string]: TxResult }; | ||||
|   "replaced-transactions"?: string[]; | ||||
| } | ||||
| 
 | ||||
| export interface TxResult { | ||||
|   txid: string; | ||||
|   "other-wtxid"?: string; | ||||
|   vsize?: number; | ||||
|   fees?: { | ||||
|     base: number; | ||||
|     "effective-feerate"?: number; | ||||
|     "effective-includes"?: string[]; | ||||
|   }; | ||||
|   error?: string; | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; | ||||
| import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, | ||||
|   RbfTree, BlockAudit, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo, TestMempoolAcceptResult } from '../interfaces/node-api.interface'; | ||||
|   RbfTree, BlockAudit, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo, TestMempoolAcceptResult,  | ||||
|   SubmitPackageResult} from '../interfaces/node-api.interface'; | ||||
| import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, take, tap } from 'rxjs'; | ||||
| import { StateService } from './state.service'; | ||||
| import { Transaction } from '../interfaces/electrs.interface'; | ||||
| @ -244,6 +245,19 @@ export class ApiService { | ||||
|     return this.httpClient.post<TestMempoolAcceptResult[]>(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, rawTxs); | ||||
|   } | ||||
| 
 | ||||
|   submitPackage$(rawTxs: string[], maxfeerate?: number, maxburnamount?: number): Observable<SubmitPackageResult> { | ||||
|     const queryParams = []; | ||||
| 
 | ||||
|     if (maxfeerate) { | ||||
|       queryParams.push(`maxfeerate=${maxfeerate}`); | ||||
|     } | ||||
| 
 | ||||
|     if (maxburnamount) { | ||||
|       queryParams.push(`maxburnamount=${maxburnamount}`); | ||||
|     } | ||||
|     return this.httpClient.post<SubmitPackageResult>(this.apiBaseUrl + this.apiBasePath + '/api/txs/package' + (queryParams.length > 0 ? `?${queryParams.join('&')}` : ''), rawTxs); | ||||
|   } | ||||
| 
 | ||||
|   getTransactionStatus$(txid: string): Observable<any> { | ||||
|     return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txid + '/status'); | ||||
|   } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user