diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts
index d2d298e09..0dbd4fa27 100644
--- a/backend/src/api/bitcoin/bitcoin.routes.ts
+++ b/backend/src/api/bitcoin/bitcoin.routes.ts
@@ -12,7 +12,7 @@ import backendInfo from '../backend-info';
import transactionUtils from '../transaction-utils';
import { IEsploraApi } from './esplora-api.interface';
import loadingIndicators from '../loading-indicators';
-import { TransactionExtended } from '../../mempool.interfaces';
+import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces';
import logger from '../../logger';
import blocks from '../blocks';
import bitcoinClient from './bitcoin-client';
@@ -49,6 +49,7 @@ class BitcoinRoutes {
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
+ .post(config.MEMPOOL.API_URL_PREFIX + 'prevouts', this.$getPrevouts)
// Temporarily add txs/package endpoint for all backends until esplora supports it
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
;
@@ -824,6 +825,53 @@ class BitcoinRoutes {
}
}
+ private async $getPrevouts(req: Request, res: Response) {
+ try {
+ const outpoints = req.body;
+ if (!Array.isArray(outpoints) || outpoints.some((item) => !/^[a-fA-F0-9]{64}$/.test(item.txid) || typeof item.vout !== 'number')) {
+ return res.status(400).json({ error: 'Invalid input format' });
+ }
+
+ if (outpoints.length > 100) {
+ return res.status(400).json({ error: 'Too many prevouts requested' });
+ }
+
+ const result = Array(outpoints.length).fill(null);
+ const memPool = mempool.getMempool();
+
+ for (let i = 0; i < outpoints.length; i++) {
+ const outpoint = outpoints[i];
+ let prevout: IEsploraApi.Vout | null = null;
+ let tx: MempoolTransactionExtended | null = null;
+
+ const mempoolTx = memPool[outpoint.txid];
+ if (mempoolTx) {
+ prevout = mempoolTx.vout[outpoint.vout];
+ tx = mempoolTx;
+ } else {
+ const rawPrevout = await bitcoinClient.getTxOut(outpoint.txid, outpoint.vout, false);
+ if (rawPrevout) {
+ prevout = {
+ value: Math.round(rawPrevout.value * 100000000),
+ scriptpubkey: rawPrevout.scriptPubKey.hex,
+ scriptpubkey_asm: rawPrevout.scriptPubKey.asm ? transactionUtils.convertScriptSigAsm(rawPrevout.scriptPubKey.hex) : '',
+ scriptpubkey_type: transactionUtils.translateScriptPubKeyType(rawPrevout.scriptPubKey.type),
+ scriptpubkey_address: rawPrevout.scriptPubKey && rawPrevout.scriptPubKey.address ? rawPrevout.scriptPubKey.address : '',
+ };
+ }
+ }
+
+ if (prevout) {
+ result[i] = { prevout, tx };
+ }
+ }
+
+ res.json(result);
+
+ } catch (e) {
+ handleError(req, res, 500, e instanceof Error ? e.message : e);
+ }
+ }
}
export default new BitcoinRoutes();
diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts
index 28fa72bba..519527d5c 100644
--- a/backend/src/api/transaction-utils.ts
+++ b/backend/src/api/transaction-utils.ts
@@ -420,6 +420,29 @@ class TransactionUtils {
return { prioritized, deprioritized };
}
+
+ // Copied from https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/bitcoin/bitcoin-api.ts#L324
+ public translateScriptPubKeyType(outputType: string): string {
+ const map = {
+ 'pubkey': 'p2pk',
+ 'pubkeyhash': 'p2pkh',
+ 'scripthash': 'p2sh',
+ 'witness_v0_keyhash': 'v0_p2wpkh',
+ 'witness_v0_scripthash': 'v0_p2wsh',
+ 'witness_v1_taproot': 'v1_p2tr',
+ 'nonstandard': 'nonstandard',
+ 'multisig': 'multisig',
+ 'anchor': 'anchor',
+ 'nulldata': 'op_return'
+ };
+
+ if (map[outputType]) {
+ return map[outputType];
+ } else {
+ return 'unknown';
+ }
+ }
+
}
export default new TransactionUtils();
diff --git a/frontend/src/app/components/transaction/transaction-raw.component.html b/frontend/src/app/components/transaction/transaction-raw.component.html
index 461d77bc4..ca76d0e78 100644
--- a/frontend/src/app/components/transaction/transaction-raw.component.html
+++ b/frontend/src/app/components/transaction/transaction-raw.component.html
@@ -46,7 +46,7 @@
@if (offlineMode) {
Prevouts are not loaded, some fields like fee rate cannot be displayed.
} @else {
- Could not load prevouts. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }}
+ Error loading prevouts. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }}
}
}
@@ -188,7 +188,7 @@
@if (isLoading) {
-
Loading transaction prevouts ({{ prevoutsLoadedCount }} / {{ prevoutsCount }})
+
Loading transaction prevouts
}
\ No newline at end of file
diff --git a/frontend/src/app/components/transaction/transaction-raw.component.ts b/frontend/src/app/components/transaction/transaction-raw.component.ts
index cac7b595f..c9a0c2544 100644
--- a/frontend/src/app/components/transaction/transaction-raw.component.ts
+++ b/frontend/src/app/components/transaction/transaction-raw.component.ts
@@ -1,5 +1,5 @@
-import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy, ChangeDetectorRef } from '@angular/core';
-import { Transaction } from '@interfaces/electrs.interface';
+import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy } from '@angular/core';
+import { Transaction, Vout } from '@interfaces/electrs.interface';
import { StateService } from '../../services/state.service';
import { Filter, toFilters } from '../../shared/filters.utils';
import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils';
@@ -28,8 +28,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
error: string;
errorPrevouts: string;
hasPrevouts: boolean;
- prevoutsLoadedCount: number = 0;
- prevoutsCount: number;
+ missingPrevouts: string[];
isLoadingBroadcast: boolean;
errorBroadcast: string;
successBroadcast: boolean;
@@ -59,7 +58,6 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
public electrsApi: ElectrsApiService,
public websocketService: WebsocketService,
public formBuilder: UntypedFormBuilder,
- public cd: ChangeDetectorRef,
public seoService: SeoService,
public apiService: ApiService,
public relativeUrlPipe: RelativeUrlPipe,
@@ -93,52 +91,38 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
return;
}
- this.prevoutsCount = transaction.vin.filter(input => !input.is_coinbase).length;
- if (this.prevoutsCount === 0) {
+ const prevoutsToFetch = transaction.vin.map((input) => ({ txid: input.txid, vout: input.vout }));
+
+ if (!prevoutsToFetch.length || transaction.vin[0].is_coinbase) {
this.hasPrevouts = true;
return;
}
- const txsToFetch: { [txid: string]: number } = transaction.vin.reduce((acc, input) => {
- if (!input.is_coinbase) {
- acc[input.txid] = (acc[input.txid] || 0) + 1;
- }
- return acc;
- }, {} as { [txid: string]: number });
-
try {
+ this.missingPrevouts = [];
- if (Object.keys(txsToFetch).length > 20) {
- throw new Error($localize`:@@transaction.too-many-prevouts:Too many transactions to fetch (${Object.keys(txsToFetch).length})`);
+ const prevouts: { prevout: Vout, tx?: any }[] = await firstValueFrom(this.apiService.getPrevouts$(prevoutsToFetch));
+
+ if (prevouts?.length !== prevoutsToFetch.length) {
+ throw new Error();
}
- const fetchedTransactions = await Promise.all(
- Object.keys(txsToFetch).map(txid =>
- firstValueFrom(this.electrsApi.getTransaction$(txid))
- .then(response => {
- this.prevoutsLoadedCount += txsToFetch[txid];
- this.cd.markForCheck();
- return response;
- })
- )
- );
-
- const transactionsMap = fetchedTransactions.reduce((acc, transaction) => {
- acc[transaction.txid] = transaction;
- return acc;
- }, {} as { [txid: string]: any });
-
- const prevouts = transaction.vin.map((input, index) => ({ index, prevout: transactionsMap[input.txid]?.vout[input.vout] || null}));
-
transaction.vin = transaction.vin.map((input, index) => {
- if (!input.is_coinbase) {
- input.prevout = prevouts.find(p => p.index === index)?.prevout;
+ if (prevouts[index]) {
+ input.prevout = prevouts[index].prevout;
addInnerScriptsToVin(input);
+ } else {
+ this.missingPrevouts.push(`${input.txid}:${input.vout}`);
}
return input;
});
+
+ if (this.missingPrevouts.length) {
+ throw new Error(`Some prevouts do not exist or are already spent (${this.missingPrevouts.length})`);
+ }
+
this.hasPrevouts = true;
- } catch (error) {
+ } catch (error) {
this.errorPrevouts = error.message;
}
}
@@ -207,6 +191,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
.subscribe((result) => {
this.isLoadingBroadcast = false;
this.successBroadcast = true;
+ this.transaction.txid = result;
resolve(result);
},
(error) => {
@@ -232,8 +217,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
this.adjustedVsize = null;
this.filters = [];
this.hasPrevouts = false;
- this.prevoutsLoadedCount = 0;
- this.prevoutsCount = 0;
+ this.missingPrevouts = [];
this.stateService.markBlock$.next({});
this.mempoolBlocksSubscription?.unsubscribe();
}
diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts
index 3c8cf8807..ce0e67cbf 100644
--- a/frontend/src/app/services/api.service.ts
+++ b/frontend/src/app/services/api.service.ts
@@ -565,6 +565,10 @@ export class ApiService {
return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/acceleration/request/' + txid, '');
}
+ getPrevouts$(outpoints: {txid: string; vout: number}[]): Observable {
+ return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/prevouts', outpoints);
+ }
+
// Cache methods
async setBlockAuditLoaded(hash: string) {
this.blockAuditLoaded[hash] = true;