diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.html b/frontend/src/app/components/test-transactions/test-transactions.component.html new file mode 100644 index 000000000..7f15da2f2 --- /dev/null +++ b/frontend/src/app/components/test-transactions/test-transactions.component.html @@ -0,0 +1,53 @@ +
+

Test Transactions

+ +
+ +
+ +
+ + +
+ +

{{ error }}

+
+ +
+ +
+ + + + + + + + + + + + + + + + + +
Allowed?TXIDEffective fee rateRejection reason
+ + + + - + + + + + + - + + {{ result['reject-reason'] || '-' }} +
+
+ +
\ No newline at end of file diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.scss b/frontend/src/app/components/test-transactions/test-transactions.component.scss new file mode 100644 index 000000000..399575b8e --- /dev/null +++ b/frontend/src/app/components/test-transactions/test-transactions.component.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.ts b/frontend/src/app/components/test-transactions/test-transactions.component.ts new file mode 100644 index 000000000..c959cb2b0 --- /dev/null +++ b/frontend/src/app/components/test-transactions/test-transactions.component.ts @@ -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; + }); + } + +} diff --git a/frontend/src/app/master-page.module.ts b/frontend/src/app/master-page.module.ts index 018809d59..2d3c34a56 100644 --- a/frontend/src/app/master-page.module.ts +++ b/frontend/src/app/master-page.module.ts @@ -6,6 +6,7 @@ import { SharedModule } from './shared/shared.module'; import { StartComponent } from './components/start/start.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 { BlocksList } from './components/blocks-list/blocks-list.component'; import { RbfList } from './components/rbf-list/rbf-list.component'; @@ -30,6 +31,10 @@ const routes: Routes = [ path: 'tx/push', component: PushTransactionComponent, }, + { + path: 'tx/test', + component: TestTransactionsComponent, + }, { path: 'about', loadChildren: () => import('./components/about/about.module').then(m => m.AboutModule), diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index f5e59d0c9..e41f8eb9e 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -238,8 +238,8 @@ export class ApiService { return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'}); } - testTransactions$(hexPayload: string): Observable { - return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/txs/test', hexPayload, { responseType: 'text' as 'json'}); + testTransactions$(hexPayload: string, maxfeerate?: number): Observable { + return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, hexPayload); } getTransactionStatus$(txid: string): Observable { diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 50268029b..d018dd1e8 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -68,6 +68,7 @@ import { DifficultyMiningComponent } from '../components/difficulty-mining/diffi import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component'; import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.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 { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component'; import { AssetCirculationComponent } from '../components/asset-circulation/asset-circulation.component'; @@ -176,6 +177,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir RbfTimelineComponent, RbfTimelineTooltipComponent, PushTransactionComponent, + TestTransactionsComponent, AssetsNavComponent, AssetsFeaturedComponent, AssetGroupComponent, @@ -312,6 +314,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir RbfTimelineComponent, RbfTimelineTooltipComponent, PushTransactionComponent, + TestTransactionsComponent, AssetsNavComponent, AssetsFeaturedComponent, AssetGroupComponent,