From c7cb7d1ac4e17c3902fdda350e08038595df7890 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 23 Mar 2024 11:27:28 +0000 Subject: [PATCH 1/4] Add testmempoolaccept API endpoint --- .../bitcoin/bitcoin-api-abstract-factory.ts | 3 ++- .../src/api/bitcoin/bitcoin-api.interface.ts | 13 +++++++++++ backend/src/api/bitcoin/bitcoin-api.ts | 10 +++++++- backend/src/api/bitcoin/bitcoin.routes.ts | 14 +++++++++++ backend/src/api/bitcoin/esplora-api.ts | 5 ++++ backend/src/api/common.ts | 23 +++++++++++++++++++ .../src/app/interfaces/node-api.interface.ts | 13 +++++++++++ frontend/src/app/services/api.service.ts | 8 +++++-- 8 files changed, 85 insertions(+), 4 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index cc0c801b5..4ec50f4b3 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -1,4 +1,4 @@ -import { IBitcoinApi } from './bitcoin-api.interface'; +import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IEsploraApi } from './esplora-api.interface'; export interface AbstractBitcoinApi { @@ -22,6 +22,7 @@ export interface AbstractBitcoinApi { $getScriptHash(scripthash: string): Promise; $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise; $sendRawTransaction(rawTransaction: string): Promise; + $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise; $getOutspend(txId: string, vout: number): Promise; $getOutspends(txId: string): Promise; $getBatchedOutspends(txId: string[]): Promise; diff --git a/backend/src/api/bitcoin/bitcoin-api.interface.ts b/backend/src/api/bitcoin/bitcoin-api.interface.ts index e176566d7..6e8583f6f 100644 --- a/backend/src/api/bitcoin/bitcoin-api.interface.ts +++ b/backend/src/api/bitcoin/bitcoin-api.interface.ts @@ -205,3 +205,16 @@ export namespace IBitcoinApi { "utxo_size_inc": number; } } + +export interface TestMempoolAcceptResult { + txid: string, + wtxid: string, + allowed?: boolean, + vsize?: number, + fees?: { + base: number, + "effective-feerate": number, + "effective-includes": string[], + }, + ['reject-reason']?: string, +} diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index d19eb06ac..c3304b432 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -1,6 +1,6 @@ import * as bitcoinjs from 'bitcoinjs-lib'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; -import { IBitcoinApi } from './bitcoin-api.interface'; +import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IEsploraApi } from './esplora-api.interface'; import blocks from '../blocks'; import mempool from '../mempool'; @@ -174,6 +174,14 @@ class BitcoinApi implements AbstractBitcoinApi { return this.bitcoindClient.sendRawTransaction(rawTransaction); } + async $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise { + if (rawTransactions.length) { + return this.bitcoindClient.testMempoolAccept(rawTransactions, maxfeerate ?? undefined); + } else { + return []; + } + } + async $getOutspend(txId: string, vout: number): Promise { const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); return { diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index a82cda3f0..ab5b0a73a 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -55,6 +55,7 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction) .post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction) + .post(config.MEMPOOL.API_URL_PREFIX + 'txs/test', this.$testTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends) @@ -749,6 +750,19 @@ class BitcoinRoutes { } } + private async $testTransactions(req: Request, res: Response) { + res.setHeader('content-type', 'text/plain'); + try { + const rawTxs = Common.getTransactionsFromRequest(req); + const maxfeerate = parseFloat(req.query.maxfeerate as string); + const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); + res.send(result); + } catch (e: any) { + res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + : (e.message || 'Error')); + } + } + } export default new BitcoinRoutes(); diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index a9dadf4a0..90e50d4c2 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -5,6 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact import { IEsploraApi } from './esplora-api.interface'; import logger from '../../logger'; import { Common } from '../common'; +import { TestMempoolAcceptResult } from './bitcoin-api.interface'; interface FailoverHost { host: string, @@ -327,6 +328,10 @@ class ElectrsApi implements AbstractBitcoinApi { throw new Error('Method not implemented.'); } + $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise { + throw new Error('Method not implemented.'); + } + $getOutspend(txId: string, vout: number): Promise { return this.failoverRouter.$get('/tx/' + txId + '/outspend/' + vout); } diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 92dfceb52..ddbe98ec5 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -946,6 +946,29 @@ export class Common { return this.validateTransactionHex(matches[1].toLowerCase()); } + static getTransactionsFromRequest(req: Request): string[] { + if (typeof req.body !== 'string') { + throw Object.assign(new Error('Non-string request body'), { code: -1 }); + } + + const txs = req.body.split(','); + + return txs.map(rawTx => { + // Support both upper and lower case hex + // Support both txHash= Form and direct API POST + const reg = /^((?:[a-fA-F0-9]{2})+)$/; + const matches = reg.exec(rawTx); + if (!matches || !matches[1]) { + throw Object.assign(new Error('Invalid hex string'), { code: -2 }); + } + + // Guaranteed to be a hex string of multiple of 2 + // Guaranteed to be lower case + // Guaranteed to pass validation (see function below) + return this.validateTransactionHex(matches[1].toLowerCase()); + }); + } + private static validateTransactionHex(txhex: string): string { // Do not mutate txhex diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 343d43a2e..ed7f5d9d4 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -423,4 +423,17 @@ export interface AccelerationInfo { effective_fee: number, boost_rate: number, boost_cost: number, +} + +export interface TestMempoolAcceptResult { + txid: string, + wtxid: string, + allowed?: boolean, + vsize?: number, + fees?: { + base: number, + "effective-feerate": number, + "effective-includes": string[], + }, + ['reject-reason']?: string, } \ No newline at end of file diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 145a8705e..f5e59d0c9 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -1,7 +1,7 @@ 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, Acceleration, AccelerationHistoryParams, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo } from '../interfaces/node-api.interface'; +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'; import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, take, tap } from 'rxjs'; import { StateService } from './state.service'; import { Transaction } from '../interfaces/electrs.interface'; @@ -238,6 +238,10 @@ 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'}); + } + getTransactionStatus$(txid: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txid + '/status'); } From f3232b2d5c9d6b5ccf48fdd24295f85a6fe43d9f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 24 Mar 2024 09:02:19 +0000 Subject: [PATCH 2/4] Add Test Transactions page --- .../test-transactions.component.html | 53 ++++++++++++++ .../test-transactions.component.scss | 33 +++++++++ .../test-transactions.component.ts | 71 +++++++++++++++++++ frontend/src/app/master-page.module.ts | 5 ++ frontend/src/app/services/api.service.ts | 4 +- frontend/src/app/shared/shared.module.ts | 3 + 6 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/components/test-transactions/test-transactions.component.html create mode 100644 frontend/src/app/components/test-transactions/test-transactions.component.scss create mode 100644 frontend/src/app/components/test-transactions/test-transactions.component.ts 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, From 2a4325580248cc252652b074baf085b89dc5ea41 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 25 Mar 2024 05:52:03 +0000 Subject: [PATCH 3/4] testmempool accept more validation & switch to JSON array format --- backend/src/api/bitcoin/bitcoin.routes.ts | 2 +- backend/src/api/common.ts | 12 ++++++++---- backend/src/index.ts | 1 + .../test-transactions.component.scss | 1 + .../test-transactions.component.ts | 17 ++++++++++++++++- frontend/src/app/services/api.service.ts | 4 ++-- 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index ab5b0a73a..ec63f4ff8 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -751,13 +751,13 @@ class BitcoinRoutes { } private async $testTransactions(req: Request, res: Response) { - res.setHeader('content-type', 'text/plain'); try { const rawTxs = Common.getTransactionsFromRequest(req); const maxfeerate = parseFloat(req.query.maxfeerate as string); const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); res.send(result); } catch (e: any) { + res.setHeader('content-type', 'text/plain'); res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) : (e.message || 'Error')); } diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index ddbe98ec5..2c9338814 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -946,12 +946,16 @@ export class Common { return this.validateTransactionHex(matches[1].toLowerCase()); } - static getTransactionsFromRequest(req: Request): string[] { - if (typeof req.body !== 'string') { - throw Object.assign(new Error('Non-string request body'), { code: -1 }); + static getTransactionsFromRequest(req: Request, limit: number = 25): string[] { + if (!Array.isArray(req.body) || req.body.some(hex => typeof hex !== 'string')) { + throw Object.assign(new Error('Invalid request body (should be an array of hexadecimal strings)'), { code: -1 }); } - const txs = req.body.split(','); + if (limit && req.body.length > limit) { + throw Object.assign(new Error('Exceeded maximum of 25 transactions'), { code: -1 }); + } + + const txs = req.body; return txs.map(rawTx => { // Support both upper and lower case hex diff --git a/backend/src/index.ts b/backend/src/index.ts index b088155ea..df9f7dc65 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -131,6 +131,7 @@ class Server { }) .use(express.urlencoded({ extended: true })) .use(express.text({ type: ['text/plain', 'application/base64'] })) + .use(express.json()) ; if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) { diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.scss b/frontend/src/app/components/test-transactions/test-transactions.component.scss index 399575b8e..ffdd5811b 100644 --- a/frontend/src/app/components/test-transactions/test-transactions.component.scss +++ b/frontend/src/app/components/test-transactions/test-transactions.component.scss @@ -2,6 +2,7 @@ td, th { &.allowed { width: 10%; + text-align: center; } &.txid { width: 50%; diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.ts b/frontend/src/app/components/test-transactions/test-transactions.component.ts index c959cb2b0..7291dae20 100644 --- a/frontend/src/app/components/test-transactions/test-transactions.component.ts +++ b/frontend/src/app/components/test-transactions/test-transactions.component.ts @@ -37,6 +37,21 @@ export class TestTransactionsComponent implements OnInit { } testTxs() { + let txs: string[] = []; + try { + txs = (this.testTxsForm.get('txs')?.value as string).split(',').map(hex => hex.trim()); + if (!txs?.length) { + this.error = 'At least one transaction is required'; + return; + } else if (txs.length > 25) { + this.error = 'Exceeded maximum of 25 transactions'; + return; + } + } catch (e) { + this.error = e?.message; + return; + } + let maxfeerate; this.invalidMaxfeerate = false; try { @@ -51,7 +66,7 @@ export class TestTransactionsComponent implements OnInit { 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) + this.apiService.testTransactions$(txs, maxfeerate === 0.1 ? null : maxfeerate) .subscribe((result) => { this.isLoading = false; this.results = result || []; diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index e41f8eb9e..0531916a0 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, maxfeerate?: number): Observable { - return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, hexPayload); + testTransactions$(rawTxs: string[], maxfeerate?: number): Observable { + return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, rawTxs); } getTransactionStatus$(txid: string): Observable { From e45a03729b6cbb6d01b898d273f7c59a53f0a390 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 4 May 2024 14:19:26 +0700 Subject: [PATCH 4/4] Adding footer link --- .../test-transactions/test-transactions.component.html | 2 +- .../components/global-footer/global-footer.component.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.html b/frontend/src/app/components/test-transactions/test-transactions.component.html index 7f15da2f2..20ba5c4bd 100644 --- a/frontend/src/app/components/test-transactions/test-transactions.component.html +++ b/frontend/src/app/components/test-transactions/test-transactions.component.html @@ -6,7 +6,7 @@
- +
diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index 7e3d44b06..b1ce52e5e 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -49,6 +49,7 @@

Lightning Explorer

Recent Blocks

Broadcast Transaction

+

Test Transaction

Connect to our Nodes

API Documentation