Merge pull request #5305 from mempool/natsoni/avoid-fetching-full-audit
Avoid fetching full audit on tx page
This commit is contained in:
		
						commit
						dca7df709b
					
				| @ -42,6 +42,7 @@ class BitcoinRoutes { | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) | ||||
|       .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) | ||||
| @ -361,6 +362,20 @@ class BitcoinRoutes { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $getBlockTxAuditSummary(req: Request, res: Response) { | ||||
|     try { | ||||
|       const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid); | ||||
|       if (auditSummary) { | ||||
|         res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); | ||||
|         res.json(auditSummary); | ||||
|       } else { | ||||
|         return res.status(404).send(`transaction audit not available`); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async getBlocks(req: Request, res: Response) { | ||||
|     try { | ||||
|       if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
 | ||||
|  | ||||
| @ -2,7 +2,7 @@ import config from '../config'; | ||||
| import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; | ||||
| import logger from '../logger'; | ||||
| import memPool from './mempool'; | ||||
| import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces'; | ||||
| import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit, TransactionAudit } from '../mempool.interfaces'; | ||||
| import { Common } from './common'; | ||||
| import diskCache from './disk-cache'; | ||||
| import transactionUtils from './transaction-utils'; | ||||
| @ -1359,6 +1359,14 @@ class Blocks { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> { | ||||
|     if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { | ||||
|       return BlocksAuditsRepository.$getBlockTxAudit(hash, txid); | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public getLastDifficultyAdjustmentTime(): number { | ||||
|     return this.lastDifficultyAdjustmentTime; | ||||
|   } | ||||
|  | ||||
| @ -42,6 +42,19 @@ export interface BlockAudit { | ||||
|   matchRate: number, | ||||
|   expectedFees?: number, | ||||
|   expectedWeight?: number, | ||||
|   template?: any[]; | ||||
| } | ||||
| 
 | ||||
| export interface TransactionAudit { | ||||
|   seen?: boolean; | ||||
|   expected?: boolean; | ||||
|   added?: boolean; | ||||
|   prioritized?: boolean; | ||||
|   delayed?: number; | ||||
|   accelerated?: boolean; | ||||
|   conflict?: boolean; | ||||
|   coinbase?: boolean; | ||||
|   firstSeen?: number; | ||||
| } | ||||
| 
 | ||||
| export interface AuditScore { | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import blocks from '../api/blocks'; | ||||
| import DB from '../database'; | ||||
| import logger from '../logger'; | ||||
| import { BlockAudit, AuditScore } from '../mempool.interfaces'; | ||||
| import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces'; | ||||
| 
 | ||||
| class BlocksAuditRepositories { | ||||
|   public async $saveAudit(audit: BlockAudit): Promise<void> { | ||||
| @ -98,6 +98,41 @@ class BlocksAuditRepositories { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> { | ||||
|     try { | ||||
|       const blockAudit = await this.$getBlockAudit(hash); | ||||
|        | ||||
|       if (blockAudit) { | ||||
|         const isAdded = blockAudit.addedTxs.includes(txid); | ||||
|         const isPrioritized = blockAudit.prioritizedTxs.includes(txid); | ||||
|         const isAccelerated = blockAudit.acceleratedTxs.includes(txid); | ||||
|         const isConflict = blockAudit.fullrbfTxs.includes(txid); | ||||
|         let isExpected = false; | ||||
|         let firstSeen = undefined; | ||||
|         blockAudit.template?.forEach(tx => { | ||||
|           if (tx.txid === txid) { | ||||
|             isExpected = true; | ||||
|             firstSeen = tx.time; | ||||
|           } | ||||
|         }); | ||||
| 
 | ||||
|         return { | ||||
|           seen: isExpected || isPrioritized || isAccelerated, | ||||
|           expected: isExpected, | ||||
|           added: isAdded, | ||||
|           prioritized: isPrioritized, | ||||
|           conflict: isConflict, | ||||
|           accelerated: isAccelerated, | ||||
|           firstSeen, | ||||
|         } | ||||
|       } | ||||
|       return null; | ||||
|     } catch (e: any) { | ||||
|       logger.err(`Cannot fetch block transaction audit from db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async $getBlockAuditScore(hash: string): Promise<AuditScore> { | ||||
|     try { | ||||
|       const [rows]: any[] = await DB.query( | ||||
|  | ||||
| @ -42,7 +42,7 @@ interface Pool { | ||||
|   slug: string; | ||||
| } | ||||
| 
 | ||||
| interface AuditStatus { | ||||
| export interface TxAuditStatus { | ||||
|   seen?: boolean; | ||||
|   expected?: boolean; | ||||
|   added?: boolean; | ||||
| @ -100,7 +100,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|   sigops: number | null; | ||||
|   adjustedVsize: number | null; | ||||
|   pool: Pool | null; | ||||
|   auditStatus: AuditStatus | null; | ||||
|   auditStatus: TxAuditStatus | null; | ||||
|   isAcceleration: boolean = false; | ||||
|   filters: Filter[] = []; | ||||
|   showCpfpDetails = false; | ||||
| @ -374,7 +374,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|         const auditAvailable = this.isAuditAvailable(height); | ||||
|         const isCoinbase = this.tx.vin.some(v => v.is_coinbase); | ||||
|         const fetchAudit = auditAvailable && !isCoinbase; | ||||
|         return fetchAudit ? this.apiService.getBlockAudit$(hash).pipe( | ||||
|         if (fetchAudit) { | ||||
|         // If block audit is already cached, use it to get transaction audit
 | ||||
|           const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash); | ||||
|           if (blockAuditLoaded) { | ||||
|             return this.apiService.getBlockAudit$(hash).pipe( | ||||
|               map(audit => { | ||||
|                 const isAdded = audit.addedTxs.includes(txid); | ||||
|                 const isPrioritized = audit.prioritizedTxs.includes(txid); | ||||
| @ -391,16 +395,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|                   accelerated: isAccelerated, | ||||
|                   firstSeen, | ||||
|                 }; | ||||
|           }), | ||||
|               }) | ||||
|             ) | ||||
|           } else { | ||||
|             return this.apiService.getBlockTxAudit$(hash, txid).pipe( | ||||
|               retry({ count: 3, delay: 2000 }), | ||||
|               catchError(() => { | ||||
|                 return of(null); | ||||
|               }) | ||||
|         ) : of(isCoinbase ? { coinbase: true } : null); | ||||
|             ) | ||||
|           } | ||||
|         } else { | ||||
|           return of(isCoinbase ? { coinbase: true } : null); | ||||
|         } | ||||
|       }), | ||||
|       catchError((e) => { | ||||
|         return of(null); | ||||
|       }) | ||||
|     ).subscribe(auditStatus => { | ||||
|       this.auditStatus = auditStatus; | ||||
|       if (this.auditStatus?.firstSeen) { | ||||
|  | ||||
| @ -8,6 +8,7 @@ import { Transaction } from '../interfaces/electrs.interface'; | ||||
| import { Conversion } from './price.service'; | ||||
| import { StorageService } from './storage.service'; | ||||
| import { WebsocketResponse } from '../interfaces/websocket.interface'; | ||||
| import { TxAuditStatus } from '../components/transaction/transaction.component'; | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| @ -17,6 +18,7 @@ export class ApiService { | ||||
|   private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
 | ||||
| 
 | ||||
|   private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>; | ||||
|   public blockAuditLoaded: { [hash: string]: boolean } = {}; | ||||
| 
 | ||||
|   constructor( | ||||
|     private httpClient: HttpClient, | ||||
| @ -369,11 +371,18 @@ export class ApiService { | ||||
|   } | ||||
| 
 | ||||
|   getBlockAudit$(hash: string) : Observable<BlockAudit> { | ||||
|     this.setBlockAuditLoaded(hash); | ||||
|     return this.httpClient.get<BlockAudit>( | ||||
|       this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary` | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   getBlockTxAudit$(hash: string, txid: string) : Observable<TxAuditStatus> { | ||||
|     return this.httpClient.get<TxAuditStatus>( | ||||
|       this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/tx/${txid}/audit` | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   getBlockAuditScores$(from: number): Observable<AuditScore[]> { | ||||
|     return this.httpClient.get<AuditScore[]>( | ||||
|       this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/scores` + | ||||
| @ -526,4 +535,13 @@ export class ApiService { | ||||
|       this.apiBaseUrl + this.apiBasePath + '/api/v1/accelerations/total' + (queryString?.length ? '?' + queryString : '') | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   // Cache methods
 | ||||
|   async setBlockAuditLoaded(hash: string) { | ||||
|     this.blockAuditLoaded[hash] = true; | ||||
|   } | ||||
| 
 | ||||
|   getBlockAuditLoaded(hash) { | ||||
|     return this.blockAuditLoaded[hash]; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -124,6 +124,7 @@ export class CacheService { | ||||
|   resetBlockCache() { | ||||
|     this.blockHashCache = {}; | ||||
|     this.blockCache = {}; | ||||
|     this.apiService.blockAuditLoaded = {}; | ||||
|     this.blockLoading = {}; | ||||
|     this.copiesInBlockQueue = {}; | ||||
|     this.blockPriorities = []; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user