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 transactionUtils from '../transaction-utils'; | ||||||
| import { IEsploraApi } from './esplora-api.interface'; | import { IEsploraApi } from './esplora-api.interface'; | ||||||
| import loadingIndicators from '../loading-indicators'; | import loadingIndicators from '../loading-indicators'; | ||||||
| import { TransactionExtended } from '../../mempool.interfaces'; | import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces'; | ||||||
| import logger from '../../logger'; | import logger from '../../logger'; | ||||||
| import blocks from '../blocks'; | import blocks from '../blocks'; | ||||||
| import bitcoinClient from './bitcoin-client'; | import bitcoinClient from './bitcoin-client'; | ||||||
| @ -49,6 +49,7 @@ class BitcoinRoutes { | |||||||
|       .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) |       .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', this.getBlocksByBulk.bind(this)) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', 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
 |       // Temporarily add txs/package endpoint for all backends until esplora supports it
 | ||||||
|       .post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage) |       .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(); | export default new BitcoinRoutes(); | ||||||
|  | |||||||
| @ -420,6 +420,29 @@ class TransactionUtils { | |||||||
| 
 | 
 | ||||||
|     return { prioritized, deprioritized }; |     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(); | export default new TransactionUtils(); | ||||||
|  | |||||||
| @ -46,7 +46,7 @@ | |||||||
|         @if (offlineMode) { |         @if (offlineMode) { | ||||||
|           <span><strong>Prevouts are not loaded, some fields like fee rate cannot be displayed.</strong></span> |           <span><strong>Prevouts are not loaded, some fields like fee rate cannot be displayed.</strong></span> | ||||||
|         } @else { |         } @else { | ||||||
|           <span><strong>Could not load prevouts</strong>. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }}</span> |           <span><strong>Error loading prevouts</strong>. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }}</span> | ||||||
|         } |         } | ||||||
|       </div> |       </div> | ||||||
|     } |     } | ||||||
| @ -188,7 +188,7 @@ | |||||||
|   @if (isLoading) { |   @if (isLoading) { | ||||||
|     <div class="text-center"> |     <div class="text-center"> | ||||||
|       <div class="spinner-border text-light mt-2 mb-2"></div> |       <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> | ||||||
|   } |   } | ||||||
| </div> | </div> | ||||||
| @ -1,5 +1,5 @@ | |||||||
| import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy, ChangeDetectorRef } from '@angular/core'; | import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy } from '@angular/core'; | ||||||
| import { Transaction } from '@interfaces/electrs.interface'; | import { Transaction, Vout } from '@interfaces/electrs.interface'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
| import { Filter, toFilters } from '../../shared/filters.utils'; | import { Filter, toFilters } from '../../shared/filters.utils'; | ||||||
| import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils'; | import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils'; | ||||||
| @ -28,8 +28,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { | |||||||
|   error: string; |   error: string; | ||||||
|   errorPrevouts: string; |   errorPrevouts: string; | ||||||
|   hasPrevouts: boolean; |   hasPrevouts: boolean; | ||||||
|   prevoutsLoadedCount: number = 0; |   missingPrevouts: string[]; | ||||||
|   prevoutsCount: number; |  | ||||||
|   isLoadingBroadcast: boolean; |   isLoadingBroadcast: boolean; | ||||||
|   errorBroadcast: string; |   errorBroadcast: string; | ||||||
|   successBroadcast: boolean; |   successBroadcast: boolean; | ||||||
| @ -59,7 +58,6 @@ export class TransactionRawComponent implements OnInit, OnDestroy { | |||||||
|     public electrsApi: ElectrsApiService, |     public electrsApi: ElectrsApiService, | ||||||
|     public websocketService: WebsocketService, |     public websocketService: WebsocketService, | ||||||
|     public formBuilder: UntypedFormBuilder, |     public formBuilder: UntypedFormBuilder, | ||||||
|     public cd: ChangeDetectorRef, |  | ||||||
|     public seoService: SeoService, |     public seoService: SeoService, | ||||||
|     public apiService: ApiService, |     public apiService: ApiService, | ||||||
|     public relativeUrlPipe: RelativeUrlPipe, |     public relativeUrlPipe: RelativeUrlPipe, | ||||||
| @ -93,50 +91,36 @@ export class TransactionRawComponent implements OnInit, OnDestroy { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.prevoutsCount = transaction.vin.filter(input => !input.is_coinbase).length; |     const prevoutsToFetch = transaction.vin.map((input) => ({ txid: input.txid, vout: input.vout })); | ||||||
|     if (this.prevoutsCount === 0) { | 
 | ||||||
|  |     if (!prevoutsToFetch.length || transaction.vin[0].is_coinbase) { | ||||||
|       this.hasPrevouts = true; |       this.hasPrevouts = true; | ||||||
|       return; |       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 { |     try { | ||||||
|  |       this.missingPrevouts = []; | ||||||
| 
 | 
 | ||||||
|       if (Object.keys(txsToFetch).length > 20) { |       const prevouts: { prevout: Vout, tx?: any }[] = await firstValueFrom(this.apiService.getPrevouts$(prevoutsToFetch)); | ||||||
|         throw new Error($localize`:@@transaction.too-many-prevouts:Too many transactions to fetch (${Object.keys(txsToFetch).length})`); | 
 | ||||||
|  |       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) => { |       transaction.vin = transaction.vin.map((input, index) => { | ||||||
|         if (!input.is_coinbase) { |         if (prevouts[index]) { | ||||||
|           input.prevout = prevouts.find(p => p.index === index)?.prevout; |           input.prevout = prevouts[index].prevout; | ||||||
|           addInnerScriptsToVin(input); |           addInnerScriptsToVin(input); | ||||||
|  |         } else { | ||||||
|  |           this.missingPrevouts.push(`${input.txid}:${input.vout}`); | ||||||
|         } |         } | ||||||
|         return input; |         return input; | ||||||
|       }); |       }); | ||||||
|  | 
 | ||||||
|  |       if (this.missingPrevouts.length) { | ||||||
|  |         throw new Error(`Some prevouts do not exist or are already spent (${this.missingPrevouts.length})`); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       this.hasPrevouts = true; |       this.hasPrevouts = true; | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|       this.errorPrevouts = error.message; |       this.errorPrevouts = error.message; | ||||||
| @ -207,6 +191,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { | |||||||
|       .subscribe((result) => { |       .subscribe((result) => { | ||||||
|         this.isLoadingBroadcast = false; |         this.isLoadingBroadcast = false; | ||||||
|         this.successBroadcast = true; |         this.successBroadcast = true; | ||||||
|  |         this.transaction.txid = result; | ||||||
|         resolve(result); |         resolve(result); | ||||||
|       }, |       }, | ||||||
|       (error) => { |       (error) => { | ||||||
| @ -232,8 +217,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { | |||||||
|     this.adjustedVsize = null; |     this.adjustedVsize = null; | ||||||
|     this.filters = []; |     this.filters = []; | ||||||
|     this.hasPrevouts = false; |     this.hasPrevouts = false; | ||||||
|     this.prevoutsLoadedCount = 0; |     this.missingPrevouts = []; | ||||||
|     this.prevoutsCount = 0; |  | ||||||
|     this.stateService.markBlock$.next({}); |     this.stateService.markBlock$.next({}); | ||||||
|     this.mempoolBlocksSubscription?.unsubscribe(); |     this.mempoolBlocksSubscription?.unsubscribe(); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -565,6 +565,10 @@ export class ApiService { | |||||||
|     return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/acceleration/request/' + txid, ''); |     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
 |   // Cache methods
 | ||||||
|   async setBlockAuditLoaded(hash: string) { |   async setBlockAuditLoaded(hash: string) { | ||||||
|     this.blockAuditLoaded[hash] = true; |     this.blockAuditLoaded[hash] = true; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user