Merge pull request #5621 from mempool/natsoni/tx-first-seen-fix
Show tx first seen time with audit disabled
This commit is contained in:
		
						commit
						c417470be2
					
				| @ -42,6 +42,7 @@ class BitcoinRoutes { | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this)) | ||||
|       .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/tx/:txid/summary', this.getStrippedBlockTransaction) | ||||
|       .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) | ||||
| @ -321,6 +322,20 @@ class BitcoinRoutes { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async getStrippedBlockTransaction(req: Request, res: Response) { | ||||
|     try { | ||||
|       const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid); | ||||
|       if (!transaction) { | ||||
|         handleError(req, res, 404, `transaction not found in summary`); | ||||
|         return; | ||||
|       } | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); | ||||
|       res.json(transaction); | ||||
|     } catch (e) { | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async getBlock(req: Request, res: Response) { | ||||
|     try { | ||||
|       const block = await blocks.$getBlock(req.params.hash); | ||||
|  | ||||
| @ -1224,6 +1224,11 @@ class Blocks { | ||||
|     return summary.transactions; | ||||
|   } | ||||
| 
 | ||||
|   public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> { | ||||
|     const txs = await this.$getStrippedBlockTransactions(hash); | ||||
|     return txs.find(tx => tx.txid === txid) || null; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Get 15 blocks | ||||
|    *  | ||||
|  | ||||
| @ -406,6 +406,30 @@ 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; | ||||
| 
 | ||||
|         const addFirstSeen = (audit: TxAuditStatus | null, hash: string, height: number, txid: string, useFullSummary: boolean) => { | ||||
|           if ( | ||||
|             this.isFirstSeenAvailable(height) | ||||
|             && !audit?.firstSeen             // firstSeen is not already in audit
 | ||||
|             && (!audit || audit?.seen)       // audit is disabled or tx is already seen (meaning 'firstSeen' is in block summary)
 | ||||
|           ) { | ||||
|             return useFullSummary ? | ||||
|               this.apiService.getStrippedBlockTransactions$(hash).pipe( | ||||
|                 map(strippedTxs => { | ||||
|                   return { audit, firstSeen: strippedTxs.find(tx => tx.txid === txid)?.time }; | ||||
|                 }), | ||||
|                 catchError(() => of({ audit })) | ||||
|               ) : | ||||
|               this.apiService.getStrippedBlockTransaction$(hash, txid).pipe( | ||||
|                 map(strippedTx => { | ||||
|                   return { audit, firstSeen: strippedTx?.time }; | ||||
|                 }), | ||||
|                 catchError(() => of({ audit })) | ||||
|               ); | ||||
|           } | ||||
|           return of({ audit }); | ||||
|         }; | ||||
| 
 | ||||
|         if (fetchAudit) { | ||||
|         // If block audit is already cached, use it to get transaction audit
 | ||||
|           const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash); | ||||
| @ -428,24 +452,31 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|                   accelerated: isAccelerated, | ||||
|                   firstSeen, | ||||
|                 }; | ||||
|               }), | ||||
|               switchMap(audit => addFirstSeen(audit, hash, height, txid, true)), | ||||
|               catchError(() => { | ||||
|                 return of({ audit: null }); | ||||
|               }) | ||||
|             ) | ||||
|           } else { | ||||
|             return this.apiService.getBlockTxAudit$(hash, txid).pipe( | ||||
|               retry({ count: 3, delay: 2000 }), | ||||
|               switchMap(audit => addFirstSeen(audit, hash, height, txid, false)), | ||||
|               catchError(() => { | ||||
|                 return of(null); | ||||
|                 return of({ audit: null }); | ||||
|               }) | ||||
|             ) | ||||
|           } | ||||
|         } else { | ||||
|           return of(isCoinbase ? { coinbase: true } : null); | ||||
|           const audit = isCoinbase ? { coinbase: true } : null; | ||||
|           return addFirstSeen(audit, hash, height, txid, this.apiService.getBlockSummaryLoaded(hash)); | ||||
|         } | ||||
|       }), | ||||
|     ).subscribe(auditStatus => { | ||||
|       this.auditStatus = auditStatus; | ||||
|       if (this.auditStatus?.firstSeen) { | ||||
|         this.transactionTime = this.auditStatus.firstSeen; | ||||
|       this.auditStatus = auditStatus?.audit; | ||||
|       const firstSeen = this.auditStatus?.firstSeen || auditStatus['firstSeen']; | ||||
|       if (firstSeen) { | ||||
|         this.transactionTime = firstSeen; | ||||
|       } | ||||
|       this.setIsAccelerated(); | ||||
|     }); | ||||
| @ -922,6 +953,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|           return false; | ||||
|         } | ||||
|         break; | ||||
|       case 'testnet4': | ||||
|         if (blockHeight < this.stateService.env.TESTNET4_BLOCK_AUDIT_START_HEIGHT) { | ||||
|           return false; | ||||
|         } | ||||
|         break; | ||||
|       case 'signet': | ||||
|         if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) { | ||||
|           return false; | ||||
| @ -935,6 +971,34 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   isFirstSeenAvailable(blockHeight: number): boolean { | ||||
|     if (this.stateService.env.BASE_MODULE !== 'mempool') { | ||||
|       return false; | ||||
|     } | ||||
|     switch (this.stateService.network) { | ||||
|       case 'testnet': | ||||
|         if (this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT) { | ||||
|           return true; | ||||
|         } | ||||
|         break; | ||||
|       case 'testnet4': | ||||
|         if (this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT) { | ||||
|           return true; | ||||
|         } | ||||
|         break; | ||||
|       case 'signet': | ||||
|         if (this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT) { | ||||
|           return true; | ||||
|         } | ||||
|         break; | ||||
|       default: | ||||
|         if (this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT) { | ||||
|           return true; | ||||
|         } | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   resetTransaction() { | ||||
|     this.firstLoad = false; | ||||
|     this.gotInitialPosition = false; | ||||
|  | ||||
| @ -18,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 blockSummaryLoaded: { [hash: string]: boolean } = {}; | ||||
|   public blockAuditLoaded: { [hash: string]: boolean } = {}; | ||||
| 
 | ||||
|   constructor( | ||||
| @ -318,9 +319,14 @@ export class ApiService { | ||||
|   } | ||||
| 
 | ||||
|   getStrippedBlockTransactions$(hash: string): Observable<TransactionStripped[]> { | ||||
|     this.setBlockSummaryLoaded(hash); | ||||
|     return this.httpClient.get<TransactionStripped[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary'); | ||||
|   } | ||||
| 
 | ||||
|   getStrippedBlockTransaction$(hash: string, txid: string): Observable<TransactionStripped> { | ||||
|     return this.httpClient.get<TransactionStripped>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/tx/' + txid + '/summary'); | ||||
|   } | ||||
| 
 | ||||
|   getDifficultyAdjustments$(interval: string | undefined): Observable<any> { | ||||
|     return this.httpClient.get<any[]>( | ||||
|         this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustments` + | ||||
| @ -567,4 +573,12 @@ export class ApiService { | ||||
|   getBlockAuditLoaded(hash) { | ||||
|     return this.blockAuditLoaded[hash]; | ||||
|   } | ||||
| 
 | ||||
|   async setBlockSummaryLoaded(hash: string) { | ||||
|     this.blockSummaryLoaded[hash] = true; | ||||
|   } | ||||
| 
 | ||||
|   getBlockSummaryLoaded(hash) { | ||||
|     return this.blockSummaryLoaded[hash]; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -68,7 +68,12 @@ export interface Env { | ||||
|   AUDIT: boolean; | ||||
|   MAINNET_BLOCK_AUDIT_START_HEIGHT: number; | ||||
|   TESTNET_BLOCK_AUDIT_START_HEIGHT: number; | ||||
|   TESTNET4_BLOCK_AUDIT_START_HEIGHT: number; | ||||
|   SIGNET_BLOCK_AUDIT_START_HEIGHT: number; | ||||
|   MAINNET_TX_FIRST_SEEN_START_HEIGHT: number; | ||||
|   TESTNET_TX_FIRST_SEEN_START_HEIGHT: number; | ||||
|   TESTNET4_TX_FIRST_SEEN_START_HEIGHT: number; | ||||
|   SIGNET_TX_FIRST_SEEN_START_HEIGHT: number; | ||||
|   HISTORICAL_PRICE: boolean; | ||||
|   ACCELERATOR: boolean; | ||||
|   ACCELERATOR_BUTTON: boolean; | ||||
| @ -107,7 +112,12 @@ const defaultEnv: Env = { | ||||
|   'AUDIT': false, | ||||
|   'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0, | ||||
|   'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0, | ||||
|   'TESTNET4_BLOCK_AUDIT_START_HEIGHT': 0, | ||||
|   'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0, | ||||
|   'MAINNET_TX_FIRST_SEEN_START_HEIGHT': 0, | ||||
|   'TESTNET_TX_FIRST_SEEN_START_HEIGHT': 0, | ||||
|   'TESTNET4_TX_FIRST_SEEN_START_HEIGHT': 0, | ||||
|   'SIGNET_TX_FIRST_SEEN_START_HEIGHT': 0, | ||||
|   'HISTORICAL_PRICE': true, | ||||
|   'ACCELERATOR': false, | ||||
|   'ACCELERATOR_BUTTON': true, | ||||
|  | ||||
| @ -13,6 +13,7 @@ | ||||
|   "MAINNET_BLOCK_AUDIT_START_HEIGHT": 773911, | ||||
|   "TESTNET_BLOCK_AUDIT_START_HEIGHT": 2417829, | ||||
|   "SIGNET_BLOCK_AUDIT_START_HEIGHT": 127609, | ||||
|   "MAINNET_TX_FIRST_SEEN_START_HEIGHT": 838316, | ||||
|   "ITEMS_PER_PAGE": 25, | ||||
|   "LIGHTNING": true, | ||||
|   "ACCELERATOR": true, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user