Merge pull request #4815 from mempool/mononaut/testmempoolaccept
testmempoolaccept
This commit is contained in:
		
						commit
						89363bfbff
					
				@ -1,4 +1,4 @@
 | 
				
			|||||||
import { IBitcoinApi } from './bitcoin-api.interface';
 | 
					import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
 | 
				
			||||||
import { IEsploraApi } from './esplora-api.interface';
 | 
					import { IEsploraApi } from './esplora-api.interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface AbstractBitcoinApi {
 | 
					export interface AbstractBitcoinApi {
 | 
				
			||||||
@ -22,6 +22,7 @@ export interface AbstractBitcoinApi {
 | 
				
			|||||||
  $getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
 | 
					  $getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
 | 
				
			||||||
  $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
 | 
					  $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
 | 
				
			||||||
  $sendRawTransaction(rawTransaction: string): Promise<string>;
 | 
					  $sendRawTransaction(rawTransaction: string): Promise<string>;
 | 
				
			||||||
 | 
					  $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
 | 
				
			||||||
  $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
 | 
					  $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
 | 
				
			||||||
  $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
 | 
					  $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
 | 
				
			||||||
  $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
 | 
					  $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
 | 
				
			||||||
 | 
				
			|||||||
@ -205,3 +205,16 @@ export namespace IBitcoinApi {
 | 
				
			|||||||
    "utxo_size_inc": number;
 | 
					    "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,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import * as bitcoinjs from 'bitcoinjs-lib';
 | 
					import * as bitcoinjs from 'bitcoinjs-lib';
 | 
				
			||||||
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
 | 
					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 { IEsploraApi } from './esplora-api.interface';
 | 
				
			||||||
import blocks from '../blocks';
 | 
					import blocks from '../blocks';
 | 
				
			||||||
import mempool from '../mempool';
 | 
					import mempool from '../mempool';
 | 
				
			||||||
@ -174,6 +174,14 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
				
			|||||||
    return this.bitcoindClient.sendRawTransaction(rawTransaction);
 | 
					    return this.bitcoindClient.sendRawTransaction(rawTransaction);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> {
 | 
				
			||||||
 | 
					    if (rawTransactions.length) {
 | 
				
			||||||
 | 
					      return this.bitcoindClient.testMempoolAccept(rawTransactions, maxfeerate ?? undefined);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
 | 
					  async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
 | 
				
			||||||
    const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
 | 
					    const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 | 
				
			|||||||
@ -55,6 +55,7 @@ class BitcoinRoutes {
 | 
				
			|||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions)
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions)
 | 
				
			||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction)
 | 
					          .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 + '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/hex', this.getRawTransaction)
 | 
				
			||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
 | 
				
			||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
 | 
				
			||||||
@ -749,6 +750,19 @@ class BitcoinRoutes {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async $testTransactions(req: Request, res: Response) {
 | 
				
			||||||
 | 
					    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'));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default new BitcoinRoutes();
 | 
					export default new BitcoinRoutes();
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact
 | 
				
			|||||||
import { IEsploraApi } from './esplora-api.interface';
 | 
					import { IEsploraApi } from './esplora-api.interface';
 | 
				
			||||||
import logger from '../../logger';
 | 
					import logger from '../../logger';
 | 
				
			||||||
import { Common } from '../common';
 | 
					import { Common } from '../common';
 | 
				
			||||||
 | 
					import { TestMempoolAcceptResult } from './bitcoin-api.interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface FailoverHost {
 | 
					interface FailoverHost {
 | 
				
			||||||
  host: string,
 | 
					  host: string,
 | 
				
			||||||
@ -327,6 +328,10 @@ class ElectrsApi implements AbstractBitcoinApi {
 | 
				
			|||||||
    throw new Error('Method not implemented.');
 | 
					    throw new Error('Method not implemented.');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> {
 | 
				
			||||||
 | 
					    throw new Error('Method not implemented.');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
 | 
					  $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
 | 
				
			||||||
    return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
 | 
					    return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -946,6 +946,33 @@ export class Common {
 | 
				
			|||||||
    return this.validateTransactionHex(matches[1].toLowerCase());
 | 
					    return this.validateTransactionHex(matches[1].toLowerCase());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					      // 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 {
 | 
					  private static validateTransactionHex(txhex: string): string {
 | 
				
			||||||
    // Do not mutate txhex
 | 
					    // Do not mutate txhex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -131,6 +131,7 @@ class Server {
 | 
				
			|||||||
      })
 | 
					      })
 | 
				
			||||||
      .use(express.urlencoded({ extended: true }))
 | 
					      .use(express.urlencoded({ extended: true }))
 | 
				
			||||||
      .use(express.text({ type: ['text/plain', 'application/base64'] }))
 | 
					      .use(express.text({ type: ['text/plain', 'application/base64'] }))
 | 
				
			||||||
 | 
					      .use(express.json())
 | 
				
			||||||
      ;
 | 
					      ;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) {
 | 
					    if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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 (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}">
 | 
				
			||||||
 | 
					    <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,34 @@
 | 
				
			|||||||
 | 
					.accept-results {
 | 
				
			||||||
 | 
					  td, th {
 | 
				
			||||||
 | 
					    &.allowed {
 | 
				
			||||||
 | 
					      width: 10%;
 | 
				
			||||||
 | 
					      text-align: center;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    &.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,86 @@
 | 
				
			|||||||
 | 
					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 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 {
 | 
				
			||||||
 | 
					      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$(txs, 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;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -423,4 +423,17 @@ export interface AccelerationInfo {
 | 
				
			|||||||
  effective_fee: number,
 | 
					  effective_fee: number,
 | 
				
			||||||
  boost_rate: number,
 | 
					  boost_rate: number,
 | 
				
			||||||
  boost_cost: 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,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -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),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { Injectable } from '@angular/core';
 | 
					import { Injectable } from '@angular/core';
 | 
				
			||||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
 | 
					import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
 | 
				
			||||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
 | 
					import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights,
 | 
				
			||||||
  PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo } from '../interfaces/node-api.interface';
 | 
					  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 { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, take, tap } from 'rxjs';
 | 
				
			||||||
import { StateService } from './state.service';
 | 
					import { StateService } from './state.service';
 | 
				
			||||||
import { Transaction } from '../interfaces/electrs.interface';
 | 
					import { Transaction } from '../interfaces/electrs.interface';
 | 
				
			||||||
@ -238,6 +238,10 @@ 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$(rawTxs: string[], maxfeerate?: number): Observable<TestMempoolAcceptResult[]> {
 | 
				
			||||||
 | 
					    return this.httpClient.post<TestMempoolAcceptResult[]>(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, rawTxs);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getTransactionStatus$(txid: string): Observable<any> {
 | 
					  getTransactionStatus$(txid: string): Observable<any> {
 | 
				
			||||||
    return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txid + '/status');
 | 
					    return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txid + '/status');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -49,6 +49,7 @@
 | 
				
			|||||||
          <p><a *ngIf="env.LIGHTNING" [routerLink]="['/lightning' | relativeUrl]" i18n="master-page.lightning">Lightning Explorer</a></p>
 | 
					          <p><a *ngIf="env.LIGHTNING" [routerLink]="['/lightning' | relativeUrl]" i18n="master-page.lightning">Lightning Explorer</a></p>
 | 
				
			||||||
          <p><a [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.recent-blocks">Recent Blocks</a></p>
 | 
					          <p><a [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.recent-blocks">Recent Blocks</a></p>
 | 
				
			||||||
          <p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
 | 
					          <p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
 | 
				
			||||||
 | 
					          <p><a [routerLink]="['/tx/test' | relativeUrl]" i18n="shared.test-transaction|Test Transaction">Test Transaction</a></p>
 | 
				
			||||||
          <p *ngIf="officialMempoolSpace"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
 | 
					          <p *ngIf="officialMempoolSpace"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
 | 
				
			||||||
          <p><a [routerLink]="['/docs/api' | relativeUrl]" i18n="footer.api-documentation">API Documentation</a></p>
 | 
					          <p><a [routerLink]="['/docs/api' | relativeUrl]" i18n="footer.api-documentation">API Documentation</a></p>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -70,6 +70,7 @@ import { AddressTransactionsWidgetComponent } from '../components/address-transa
 | 
				
			|||||||
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';
 | 
				
			||||||
@ -180,6 +181,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
 | 
				
			|||||||
    RbfTimelineComponent,
 | 
					    RbfTimelineComponent,
 | 
				
			||||||
    RbfTimelineTooltipComponent,
 | 
					    RbfTimelineTooltipComponent,
 | 
				
			||||||
    PushTransactionComponent,
 | 
					    PushTransactionComponent,
 | 
				
			||||||
 | 
					    TestTransactionsComponent,
 | 
				
			||||||
    AssetsNavComponent,
 | 
					    AssetsNavComponent,
 | 
				
			||||||
    AssetsFeaturedComponent,
 | 
					    AssetsFeaturedComponent,
 | 
				
			||||||
    AssetGroupComponent,
 | 
					    AssetGroupComponent,
 | 
				
			||||||
@ -318,6 +320,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