Add backend endpoint to fetch prevouts
This commit is contained in:
		
							parent
							
								
									e848d711fc
								
							
						
					
					
						commit
						727f22bc9d
					
				| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user