Merge pull request #5621 from mempool/natsoni/tx-first-seen-fix

Show tx first seen time with audit disabled
This commit is contained in:
softsimon 2024-11-09 00:28:47 +07:00 committed by GitHub
commit c417470be2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 114 additions and 5 deletions

View File

@ -42,6 +42,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this)) .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', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) .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/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) .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) { private async getBlock(req: Request, res: Response) {
try { try {
const block = await blocks.$getBlock(req.params.hash); const block = await blocks.$getBlock(req.params.hash);

View File

@ -1224,6 +1224,11 @@ class Blocks {
return summary.transactions; 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 * Get 15 blocks
* *

View File

@ -406,6 +406,30 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
const auditAvailable = this.isAuditAvailable(height); const auditAvailable = this.isAuditAvailable(height);
const isCoinbase = this.tx.vin.some(v => v.is_coinbase); const isCoinbase = this.tx.vin.some(v => v.is_coinbase);
const fetchAudit = auditAvailable && !isCoinbase; 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 (fetchAudit) {
// If block audit is already cached, use it to get transaction audit // If block audit is already cached, use it to get transaction audit
const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash); const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash);
@ -428,24 +452,31 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
accelerated: isAccelerated, accelerated: isAccelerated,
firstSeen, firstSeen,
}; };
}),
switchMap(audit => addFirstSeen(audit, hash, height, txid, true)),
catchError(() => {
return of({ audit: null });
}) })
) )
} else { } else {
return this.apiService.getBlockTxAudit$(hash, txid).pipe( return this.apiService.getBlockTxAudit$(hash, txid).pipe(
retry({ count: 3, delay: 2000 }), retry({ count: 3, delay: 2000 }),
switchMap(audit => addFirstSeen(audit, hash, height, txid, false)),
catchError(() => { catchError(() => {
return of(null); return of({ audit: null });
}) })
) )
} }
} else { } 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 => { ).subscribe(auditStatus => {
this.auditStatus = auditStatus; this.auditStatus = auditStatus?.audit;
if (this.auditStatus?.firstSeen) { const firstSeen = this.auditStatus?.firstSeen || auditStatus['firstSeen'];
this.transactionTime = this.auditStatus.firstSeen; if (firstSeen) {
this.transactionTime = firstSeen;
} }
this.setIsAccelerated(); this.setIsAccelerated();
}); });
@ -922,6 +953,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return false; return false;
} }
break; break;
case 'testnet4':
if (blockHeight < this.stateService.env.TESTNET4_BLOCK_AUDIT_START_HEIGHT) {
return false;
}
break;
case 'signet': case 'signet':
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) { if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
return false; return false;
@ -935,6 +971,34 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return true; 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() { resetTransaction() {
this.firstLoad = false; this.firstLoad = false;
this.gotInitialPosition = false; this.gotInitialPosition = false;

View File

@ -18,6 +18,7 @@ export class ApiService {
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>; private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
public blockSummaryLoaded: { [hash: string]: boolean } = {};
public blockAuditLoaded: { [hash: string]: boolean } = {}; public blockAuditLoaded: { [hash: string]: boolean } = {};
constructor( constructor(
@ -318,9 +319,14 @@ export class ApiService {
} }
getStrippedBlockTransactions$(hash: string): Observable<TransactionStripped[]> { getStrippedBlockTransactions$(hash: string): Observable<TransactionStripped[]> {
this.setBlockSummaryLoaded(hash);
return this.httpClient.get<TransactionStripped[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary'); 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> { getDifficultyAdjustments$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>( return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustments` + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustments` +
@ -567,4 +573,12 @@ export class ApiService {
getBlockAuditLoaded(hash) { getBlockAuditLoaded(hash) {
return this.blockAuditLoaded[hash]; return this.blockAuditLoaded[hash];
} }
async setBlockSummaryLoaded(hash: string) {
this.blockSummaryLoaded[hash] = true;
}
getBlockSummaryLoaded(hash) {
return this.blockSummaryLoaded[hash];
}
} }

View File

@ -68,7 +68,12 @@ export interface Env {
AUDIT: boolean; AUDIT: boolean;
MAINNET_BLOCK_AUDIT_START_HEIGHT: number; MAINNET_BLOCK_AUDIT_START_HEIGHT: number;
TESTNET_BLOCK_AUDIT_START_HEIGHT: number; TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
TESTNET4_BLOCK_AUDIT_START_HEIGHT: number;
SIGNET_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; HISTORICAL_PRICE: boolean;
ACCELERATOR: boolean; ACCELERATOR: boolean;
ACCELERATOR_BUTTON: boolean; ACCELERATOR_BUTTON: boolean;
@ -107,7 +112,12 @@ const defaultEnv: Env = {
'AUDIT': false, 'AUDIT': false,
'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0, 'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0,
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0, 'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
'TESTNET4_BLOCK_AUDIT_START_HEIGHT': 0,
'SIGNET_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, 'HISTORICAL_PRICE': true,
'ACCELERATOR': false, 'ACCELERATOR': false,
'ACCELERATOR_BUTTON': true, 'ACCELERATOR_BUTTON': true,

View File

@ -13,6 +13,7 @@
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 773911, "MAINNET_BLOCK_AUDIT_START_HEIGHT": 773911,
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 2417829, "TESTNET_BLOCK_AUDIT_START_HEIGHT": 2417829,
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 127609, "SIGNET_BLOCK_AUDIT_START_HEIGHT": 127609,
"MAINNET_TX_FIRST_SEEN_START_HEIGHT": 838316,
"ITEMS_PER_PAGE": 25, "ITEMS_PER_PAGE": 25,
"LIGHTNING": true, "LIGHTNING": true,
"ACCELERATOR": true, "ACCELERATOR": true,