From f08fa034cc00c212e95f0d33793391f5e4bc8503 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 30 Oct 2024 15:19:46 +0100 Subject: [PATCH 1/3] Add missing frontend audit flag for testnet4 --- .../src/app/components/transaction/transaction.component.ts | 5 +++++ frontend/src/app/services/state.service.ts | 2 ++ 2 files changed, 7 insertions(+) diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 5f5ef4fa7..b8621ba0c 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -922,6 +922,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; diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 8dd17cf75..e6fed4cd2 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -68,6 +68,7 @@ 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; HISTORICAL_PRICE: boolean; ACCELERATOR: boolean; @@ -107,6 +108,7 @@ 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, 'HISTORICAL_PRICE': true, 'ACCELERATOR': false, From b6aeb5661f7f9137a3159c30b748d70c9a9c0749 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 30 Oct 2024 15:31:01 +0100 Subject: [PATCH 2/3] Add block/:hash/tx/:txid/summary endpoint --- backend/src/api/bitcoin/bitcoin.routes.ts | 15 +++++++++++++++ backend/src/api/blocks.ts | 5 +++++ frontend/src/app/services/api.service.ts | 14 ++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 3b33c1ead..d2d298e09 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -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); diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 3420d99c8..e621056ab 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -1224,6 +1224,11 @@ class Blocks { return summary.transactions; } + public async $getSingleTxFromSummary(hash: string, txid: string): Promise { + const txs = await this.$getStrippedBlockTransactions(hash); + return txs.find(tx => tx.txid === txid) || null; + } + /** * Get 15 blocks * diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 08251ddae..3c8cf8807 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -18,6 +18,7 @@ export class ApiService { private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet private requestCache = new Map, expiry: number }>; + public blockSummaryLoaded: { [hash: string]: boolean } = {}; public blockAuditLoaded: { [hash: string]: boolean } = {}; constructor( @@ -318,9 +319,14 @@ export class ApiService { } getStrippedBlockTransactions$(hash: string): Observable { + this.setBlockSummaryLoaded(hash); return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary'); } + getStrippedBlockTransaction$(hash: string, txid: string): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/tx/' + txid + '/summary'); + } + getDifficultyAdjustments$(interval: string | undefined): Observable { return this.httpClient.get( 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]; + } } From db321c3fa5ca257c31e83ac416e841b921eacc3d Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 30 Oct 2024 15:40:05 +0100 Subject: [PATCH 3/3] Get tx first seen from block summary if not available in audit --- .../transaction/transaction.component.ts | 69 +++++++++++++++++-- frontend/src/app/services/state.service.ts | 8 +++ .../mempool-frontend-config.mainnet.json | 1 + 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index b8621ba0c..f19a5bcbd 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -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(); }); @@ -940,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; diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index e6fed4cd2..2feb266d1 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -70,6 +70,10 @@ export interface Env { 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; @@ -110,6 +114,10 @@ const defaultEnv: Env = { '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, diff --git a/production/mempool-frontend-config.mainnet.json b/production/mempool-frontend-config.mainnet.json index 61a8c2c2a..79acaecc5 100644 --- a/production/mempool-frontend-config.mainnet.json +++ b/production/mempool-frontend-config.mainnet.json @@ -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,