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')) {
|
||||
|
@ -452,4 +452,22 @@ export interface TestMempoolAcceptResult {
|
||||
"effective-includes": string[],
|
||||
},
|
||||
['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