From 4fbab08586d6ae1eb4c4aabb9b6dbd237ede1625 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 20 Jun 2023 14:54:25 -0400 Subject: [PATCH] Separate summary and audit-summary API endpoints --- backend/src/api/bitcoin/bitcoin.routes.ts | 10 +- backend/src/api/blocks.ts | 14 +- .../repositories/BlocksAuditsRepository.ts | 7 +- .../app/components/block/block.component.html | 2 +- .../app/components/block/block.component.ts | 246 ++++++++---------- .../src/app/interfaces/node-api.interface.ts | 3 + frontend/src/app/services/api.service.ts | 8 +- 7 files changed, 129 insertions(+), 161 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 0a343c376..f4176e67b 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -394,9 +394,13 @@ class BitcoinRoutes { private async getBlockAuditSummary(req: Request, res: Response) { try { - const transactions = await blocks.$getBlockAuditSummary(req.params.hash); - res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); - res.json(transactions); + const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash); + if (auditSummary) { + res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); + res.json(auditSummary); + } else { + return res.status(404).send(`audit not available`); + } } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index fae1d453b..12203068f 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -1007,19 +1007,11 @@ class Blocks { } public async $getBlockAuditSummary(hash: string): Promise { - let summary; if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { - summary = await BlocksAuditsRepository.$getBlockAudit(hash); + return BlocksAuditsRepository.$getBlockAudit(hash); + } else { + return null; } - - // fallback to non-audited transaction summary - if (!summary?.transactions?.length) { - const strippedTransactions = await this.$getStrippedBlockTransactions(hash); - summary = { - transactions: strippedTransactions - }; - } - return summary; } public getLastDifficultyAdjustmentTime(): number { diff --git a/backend/src/repositories/BlocksAuditsRepository.ts b/backend/src/repositories/BlocksAuditsRepository.ts index 1fa2b0209..8ad035f32 100644 --- a/backend/src/repositories/BlocksAuditsRepository.ts +++ b/backend/src/repositories/BlocksAuditsRepository.ts @@ -64,7 +64,6 @@ class BlocksAuditRepositories { const [rows]: any[] = await DB.query( `SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size, blocks.weight, blocks.tx_count, - transactions, template, missing_txs as missingTxs, added_txs as addedTxs, @@ -76,7 +75,6 @@ class BlocksAuditRepositories { FROM blocks_audits JOIN blocks ON blocks.hash = blocks_audits.hash JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash - JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash WHERE blocks_audits.hash = "${hash}" `); @@ -85,12 +83,9 @@ class BlocksAuditRepositories { rows[0].addedTxs = JSON.parse(rows[0].addedTxs); rows[0].freshTxs = JSON.parse(rows[0].freshTxs); rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs); - rows[0].transactions = JSON.parse(rows[0].transactions); rows[0].template = JSON.parse(rows[0].template); - if (rows[0].transactions.length) { - return rows[0]; - } + return rows[0]; } return null; } catch (e: any) { diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 1a0a81026..00cb2fcb1 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -63,7 +63,7 @@ *ngIf="blockAudit?.matchRate != null; else nullHealth" >{{ blockAudit?.matchRate }}% - + Unknown diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 17e6e9b7f..927222dbc 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -2,9 +2,9 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/ import { Location } from '@angular/common'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ElectrsApiService } from '../../services/electrs-api.service'; -import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise, filter } from 'rxjs/operators'; +import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith } from 'rxjs/operators'; import { Transaction, Vout } from '../../interfaces/electrs.interface'; -import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest } from 'rxjs'; +import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs'; import { StateService } from '../../services/state.service'; import { SeoService } from '../../services/seo.service'; import { WebsocketService } from '../../services/websocket.service'; @@ -44,7 +44,6 @@ export class BlockComponent implements OnInit, OnDestroy { strippedTransactions: TransactionStripped[]; overviewTransitionDirection: string; isLoadingOverview = true; - isLoadingAudit = true; error: any; blockSubsidy: number; fees: number; @@ -281,143 +280,111 @@ export class BlockComponent implements OnInit, OnDestroy { this.isLoadingOverview = false; }); - if (!this.auditSupported) { - this.overviewSubscription = block$.pipe( - startWith(null), - pairwise(), - switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id) - .pipe( - catchError((err) => { - this.overviewError = err; - return of([]); - }), - switchMap((transactions) => { - if (prevBlock) { - return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' }); - } else { - return of({ transactions, direction: 'down' }); - } - }) - ) - ), - ) - .subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => { - this.strippedTransactions = transactions; - this.isLoadingOverview = false; - this.setupBlockGraphs(); - }, - (error) => { - this.error = error; - this.isLoadingOverview = false; - }); - } - - if (this.auditSupported) { - this.auditSubscription = block$.pipe( - startWith(null), - pairwise(), - switchMap(([prevBlock, block]) => { - this.isLoadingAudit = true; - this.blockAudit = null; - return this.apiService.getBlockAudit$(block.id) + this.overviewSubscription = block$.pipe( + switchMap((block) => { + return forkJoin([ + this.apiService.getStrippedBlockTransactions$(block.id) .pipe( catchError((err) => { this.overviewError = err; - this.isLoadingAudit = false; - return of([]); + return of(null); }) - ); - } - ), - filter((response) => response != null), - map((response) => { - const blockAudit = response.body; - const inTemplate = {}; - const inBlock = {}; - const isAdded = {}; - const isCensored = {}; - const isMissing = {}; - const isSelected = {}; - const isFresh = {}; - const isSigop = {}; - this.numMissing = 0; - this.numUnexpected = 0; + ), + !this.isAuditAvailableFromBlockHeight(block.height) ? of(null) : this.apiService.getBlockAudit$(block.id) + .pipe( + catchError((err) => { + this.overviewError = err; + return of(null); + }) + ) + ]); + }) + ) + .subscribe(([transactions, blockAudit]) => { + if (transactions) { + this.strippedTransactions = transactions; + } else { + this.strippedTransactions = []; + } - if (blockAudit?.template) { - for (const tx of blockAudit.template) { - inTemplate[tx.txid] = true; - } - for (const tx of blockAudit.transactions) { - inBlock[tx.txid] = true; - } - for (const txid of blockAudit.addedTxs) { - isAdded[txid] = true; - } - for (const txid of blockAudit.missingTxs) { - isCensored[txid] = true; - } - for (const txid of blockAudit.freshTxs || []) { - isFresh[txid] = true; - } - for (const txid of blockAudit.sigopTxs || []) { - isSigop[txid] = true; - } - // set transaction statuses - for (const tx of blockAudit.template) { - tx.context = 'projected'; - if (isCensored[tx.txid]) { - tx.status = 'censored'; - } else if (inBlock[tx.txid]) { - tx.status = 'found'; - } else { - tx.status = isFresh[tx.txid] ? 'fresh' : (isSigop[tx.txid] ? 'sigop' : 'missing'); - isMissing[tx.txid] = true; - this.numMissing++; - } - } - for (const [index, tx] of blockAudit.transactions.entries()) { - tx.context = 'actual'; - if (index === 0) { - tx.status = null; - } else if (isAdded[tx.txid]) { - tx.status = 'added'; - } else if (inTemplate[tx.txid]) { - tx.status = 'found'; - } else { - tx.status = 'selected'; - isSelected[tx.txid] = true; - this.numUnexpected++; - } - } - for (const tx of blockAudit.transactions) { - inBlock[tx.txid] = true; - } + this.blockAudit = null; + if (transactions && blockAudit) { + const inTemplate = {}; + const inBlock = {}; + const isAdded = {}; + const isCensored = {}; + const isMissing = {}; + const isSelected = {}; + const isFresh = {}; + const isSigop = {}; + this.numMissing = 0; + this.numUnexpected = 0; - blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0; - blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0; - blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0; - - this.setAuditAvailable(true); - } else { - this.setAuditAvailable(false); + if (blockAudit?.template) { + for (const tx of blockAudit.template) { + inTemplate[tx.txid] = true; } - return blockAudit; - }), - catchError((err) => { - console.log(err); - this.error = err; - this.isLoadingOverview = false; - this.isLoadingAudit = false; + for (const tx of transactions) { + inBlock[tx.txid] = true; + } + for (const txid of blockAudit.addedTxs) { + isAdded[txid] = true; + } + for (const txid of blockAudit.missingTxs) { + isCensored[txid] = true; + } + for (const txid of blockAudit.freshTxs || []) { + isFresh[txid] = true; + } + for (const txid of blockAudit.sigopTxs || []) { + isSigop[txid] = true; + } + // set transaction statuses + for (const tx of blockAudit.template) { + tx.context = 'projected'; + if (isCensored[tx.txid]) { + tx.status = 'censored'; + } else if (inBlock[tx.txid]) { + tx.status = 'found'; + } else { + tx.status = isFresh[tx.txid] ? 'fresh' : (isSigop[tx.txid] ? 'sigop' : 'missing'); + isMissing[tx.txid] = true; + this.numMissing++; + } + } + for (const [index, tx] of transactions.entries()) { + tx.context = 'actual'; + if (index === 0) { + tx.status = null; + } else if (isAdded[tx.txid]) { + tx.status = 'added'; + } else if (inTemplate[tx.txid]) { + tx.status = 'found'; + } else { + tx.status = 'selected'; + isSelected[tx.txid] = true; + this.numUnexpected++; + } + } + for (const tx of transactions) { + inBlock[tx.txid] = true; + } + + blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0; + blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0; + blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0; + this.blockAudit = blockAudit; + this.setAuditAvailable(true); + } else { this.setAuditAvailable(false); - return of(null); - }), - ).subscribe((blockAudit) => { - this.blockAudit = blockAudit; - this.setupBlockGraphs(); - this.isLoadingOverview = false; - this.isLoadingAudit = false; - }); - } + } + } else { + this.setAuditAvailable(false); + } + + this.isLoadingOverview = false; + this.setupBlockGraphs(); + }); this.networkChangedSubscription = this.stateService.networkChanged$ .subscribe((network) => this.network = network); @@ -652,25 +619,32 @@ export class BlockComponent implements OnInit, OnDestroy { } updateAuditAvailableFromBlockHeight(blockHeight: number): void { - if (!this.auditSupported) { + if (!this.isAuditAvailableFromBlockHeight(blockHeight)) { this.setAuditAvailable(false); } + } + + isAuditAvailableFromBlockHeight(blockHeight: number): boolean { + if (!this.auditSupported) { + return false; + } switch (this.stateService.network) { case 'testnet': if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) { - this.setAuditAvailable(false); + return false; } break; case 'signet': if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) { - this.setAuditAvailable(false); + return false; } break; default: if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) { - this.setAuditAvailable(false); + return false; } } + return true; } getMinBlockFee(block: BlockExtended): number { diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 2f9b95ab1..648eb38ea 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -153,6 +153,8 @@ export interface BlockExtended extends Block { export interface BlockAudit extends BlockExtended { missingTxs: string[], addedTxs: string[], + freshTxs: string[], + sigopTxs: string[], matchRate: number, expectedFees: number, expectedWeight: number, @@ -169,6 +171,7 @@ export interface TransactionStripped { vsize: number; value: number; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; + context?: 'projected' | 'actual'; } interface RbfTransaction extends TransactionStripped { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 8521ddc83..45fa7e9fc 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, - PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree } from '../interfaces/node-api.interface'; + PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit } from '../interfaces/node-api.interface'; import { Observable } from 'rxjs'; import { StateService } from './state.service'; import { WebsocketResponse } from '../interfaces/websocket.interface'; @@ -245,9 +245,9 @@ export class ApiService { ); } - getBlockAudit$(hash: string) : Observable { - return this.httpClient.get( - this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`, { observe: 'response' } + getBlockAudit$(hash: string) : Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary` ); }