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
+
+
+
+
+
+
+
+
+
+ Allowed? |
+ TXID |
+ Effective fee rate |
+ Rejection 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,