Add Test Transactions page
This commit is contained in:
parent
c7cb7d1ac4
commit
f3232b2d5c
@ -0,0 +1,53 @@
|
|||||||
|
<div class="container-xl">
|
||||||
|
<h1 class="text-left" i18n="shared.test-transactions|Test Transactions">Test Transactions</h1>
|
||||||
|
|
||||||
|
<form [formGroup]="testTxsForm" (submit)="testTxsForm.valid && testTxs()" novalidate>
|
||||||
|
<label for="maxfeerate" i18n="test.tx.raw-hex">Raw hex</label>
|
||||||
|
<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 for="maxfeerate" i18n="test.tx.max-fee-rate">Maximum fee rate (s/vb)</label>
|
||||||
|
<input type="number" class="form-control input-dark" formControlName="maxfeerate" id="maxfeerate"
|
||||||
|
[value]="10000" placeholder="10,000 s/vb" [class]="{invalid: invalidMaxfeerate}">
|
||||||
|
<br>
|
||||||
|
<button [disabled]="isLoading" type="submit" class="btn btn-primary mr-2" i18n="shared.test-transactions|Test Transactions">Test Transactions</button>
|
||||||
|
<p class="red-color d-inline">{{ error }}</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">
|
||||||
|
<ng-container [ngSwitch]="result.allowed">
|
||||||
|
<span *ngSwitchCase="true">✅</span>
|
||||||
|
<span *ngSwitchCase="false">❌</span>
|
||||||
|
<span *ngSwitchDefault>-</span>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
<td class="txid">
|
||||||
|
<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['reject-reason'] || '-' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@ -0,0 +1,33 @@
|
|||||||
|
.accept-results {
|
||||||
|
td, th {
|
||||||
|
&.allowed {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
&.txid {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
&.rate {
|
||||||
|
width: 20%;
|
||||||
|
text-align: right;
|
||||||
|
white-space: wrap;
|
||||||
|
}
|
||||||
|
&.reason {
|
||||||
|
width: 20%;
|
||||||
|
text-align: right;
|
||||||
|
white-space: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 950px) {
|
||||||
|
table-layout: auto;
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
&.allowed {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
&.txid {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||||
|
import { ApiService } from '../../services/api.service';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { SeoService } from '../../services/seo.service';
|
||||||
|
import { OpenGraphService } from '../../services/opengraph.service';
|
||||||
|
import { TestMempoolAcceptResult } from '../../interfaces/node-api.interface';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-test-transactions',
|
||||||
|
templateUrl: './test-transactions.component.html',
|
||||||
|
styleUrls: ['./test-transactions.component.scss']
|
||||||
|
})
|
||||||
|
export class TestTransactionsComponent implements OnInit {
|
||||||
|
testTxsForm: UntypedFormGroup;
|
||||||
|
error: string = '';
|
||||||
|
results: TestMempoolAcceptResult[] = [];
|
||||||
|
isLoading = false;
|
||||||
|
invalidMaxfeerate = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: UntypedFormBuilder,
|
||||||
|
private apiService: ApiService,
|
||||||
|
public stateService: StateService,
|
||||||
|
private seoService: SeoService,
|
||||||
|
private ogService: OpenGraphService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.testTxsForm = this.formBuilder.group({
|
||||||
|
txs: ['', Validators.required],
|
||||||
|
maxfeerate: ['', Validators.min(0)]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.seoService.setTitle($localize`:@@meta.title.test-txs:Test Transactions`);
|
||||||
|
this.ogService.setManualOgImage('tx-push.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
testTxs() {
|
||||||
|
let maxfeerate;
|
||||||
|
this.invalidMaxfeerate = false;
|
||||||
|
try {
|
||||||
|
const maxfeerateVal = this.testTxsForm.get('maxfeerate')?.value;
|
||||||
|
if (maxfeerateVal != null && maxfeerateVal !== '') {
|
||||||
|
maxfeerate = parseFloat(maxfeerateVal) / 100_000;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.invalidMaxfeerate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.error = '';
|
||||||
|
this.results = [];
|
||||||
|
this.apiService.testTransactions$((this.testTxsForm.get('txs')?.value as string).split(',').map(hex => hex.trim()).join(','), maxfeerate === 0.1 ? null : maxfeerate)
|
||||||
|
.subscribe((result) => {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.results = result || [];
|
||||||
|
this.testTxsForm.reset();
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (typeof error.error === 'string') {
|
||||||
|
const matchText = error.error.match('"message":"(.*?)"');
|
||||||
|
this.error = matchText && matchText[1] || error.error;
|
||||||
|
} else if (error.message) {
|
||||||
|
this.error = error.message;
|
||||||
|
}
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -6,6 +6,7 @@ import { SharedModule } from './shared/shared.module';
|
|||||||
|
|
||||||
import { StartComponent } from './components/start/start.component';
|
import { StartComponent } from './components/start/start.component';
|
||||||
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
|
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
|
||||||
|
import { TestTransactionsComponent } from './components/test-transactions/test-transactions.component';
|
||||||
import { CalculatorComponent } from './components/calculator/calculator.component';
|
import { CalculatorComponent } from './components/calculator/calculator.component';
|
||||||
import { BlocksList } from './components/blocks-list/blocks-list.component';
|
import { BlocksList } from './components/blocks-list/blocks-list.component';
|
||||||
import { RbfList } from './components/rbf-list/rbf-list.component';
|
import { RbfList } from './components/rbf-list/rbf-list.component';
|
||||||
@ -30,6 +31,10 @@ const routes: Routes = [
|
|||||||
path: 'tx/push',
|
path: 'tx/push',
|
||||||
component: PushTransactionComponent,
|
component: PushTransactionComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'tx/test',
|
||||||
|
component: TestTransactionsComponent,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'about',
|
path: 'about',
|
||||||
loadChildren: () => import('./components/about/about.module').then(m => m.AboutModule),
|
loadChildren: () => import('./components/about/about.module').then(m => m.AboutModule),
|
||||||
|
@ -238,8 +238,8 @@ export class ApiService {
|
|||||||
return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'});
|
return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'});
|
||||||
}
|
}
|
||||||
|
|
||||||
testTransactions$(hexPayload: string): Observable<TestMempoolAcceptResult[]> {
|
testTransactions$(hexPayload: string, maxfeerate?: number): Observable<TestMempoolAcceptResult[]> {
|
||||||
return this.httpClient.post<TestMempoolAcceptResult[]>(this.apiBaseUrl + this.apiBasePath + '/api/txs/test', hexPayload, { responseType: 'text' as 'json'});
|
return this.httpClient.post<TestMempoolAcceptResult[]>(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, hexPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTransactionStatus$(txid: string): Observable<any> {
|
getTransactionStatus$(txid: string): Observable<any> {
|
||||||
|
@ -68,6 +68,7 @@ import { DifficultyMiningComponent } from '../components/difficulty-mining/diffi
|
|||||||
import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component';
|
import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component';
|
||||||
import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.component';
|
import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.component';
|
||||||
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
|
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
|
||||||
|
import { TestTransactionsComponent } from '../components/test-transactions/test-transactions.component';
|
||||||
import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component';
|
import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component';
|
||||||
import { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component';
|
import { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component';
|
||||||
import { AssetCirculationComponent } from '../components/asset-circulation/asset-circulation.component';
|
import { AssetCirculationComponent } from '../components/asset-circulation/asset-circulation.component';
|
||||||
@ -176,6 +177,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
RbfTimelineComponent,
|
RbfTimelineComponent,
|
||||||
RbfTimelineTooltipComponent,
|
RbfTimelineTooltipComponent,
|
||||||
PushTransactionComponent,
|
PushTransactionComponent,
|
||||||
|
TestTransactionsComponent,
|
||||||
AssetsNavComponent,
|
AssetsNavComponent,
|
||||||
AssetsFeaturedComponent,
|
AssetsFeaturedComponent,
|
||||||
AssetGroupComponent,
|
AssetGroupComponent,
|
||||||
@ -312,6 +314,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
RbfTimelineComponent,
|
RbfTimelineComponent,
|
||||||
RbfTimelineTooltipComponent,
|
RbfTimelineTooltipComponent,
|
||||||
PushTransactionComponent,
|
PushTransactionComponent,
|
||||||
|
TestTransactionsComponent,
|
||||||
AssetsNavComponent,
|
AssetsNavComponent,
|
||||||
AssetsFeaturedComponent,
|
AssetsFeaturedComponent,
|
||||||
AssetGroupComponent,
|
AssetGroupComponent,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user