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 + '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);
|
||||||
|
@ -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
|
||||||
*
|
*
|
||||||
|
@ -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;
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user