mirror of
https://github.com/mempool/mempool.git
synced 2025-02-24 06:47:52 +01:00
Add backend endpoint to fetch prevouts
This commit is contained in:
parent
e848d711fc
commit
727f22bc9d
5 changed files with 101 additions and 42 deletions
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
|
@ -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,50 +91,36 @@ 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) {
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue