Add backend endpoint to fetch prevouts

This commit is contained in:
natsoni 2024-12-09 23:24:11 +01:00
parent e848d711fc
commit 727f22bc9d
No known key found for this signature in database
GPG key ID: C65917583181743B
5 changed files with 101 additions and 42 deletions

View file

@ -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();

View file

@ -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();

View file

@ -46,7 +46,7 @@
@if (offlineMode) {
<span><strong>Prevouts are not loaded, some fields like fee rate cannot be displayed.</strong></span>
} @else {
<span><strong>Could not load prevouts</strong>. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }}</span>
<span><strong>Error loading prevouts</strong>. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }}</span>
}
</div>
}
@ -188,7 +188,7 @@
@if (isLoading) {
<div class="text-center">
<div class="spinner-border text-light mt-2 mb-2"></div>
<h3 i18n="transaction.error.loading-prevouts">Loading transaction prevouts ({{ prevoutsLoadedCount }} / {{ prevoutsCount }})</h3>
<h3 i18n="transaction.error.loading-prevouts">Loading transaction prevouts</h3>
</div>
}
</div>

View file

@ -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();
}

View file

@ -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<any> {
return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/prevouts', outpoints);
}
// Cache methods
async setBlockAuditLoaded(hash: string) {
this.blockAuditLoaded[hash] = true;