Avoid fetching full audit on tx page
This commit is contained in:
		
							parent
							
								
									2d03ab6346
								
							
						
					
					
						commit
						99ea1ad0a0
					
				@ -42,6 +42,7 @@ class BitcoinRoutes {
 | 
			
		||||
      .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/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)
 | 
			
		||||
      .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
 | 
			
		||||
@ -361,6 +362,20 @@ class BitcoinRoutes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getBlockTxAuditSummary(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
 | 
			
		||||
      if (auditSummary) {
 | 
			
		||||
        res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
 | 
			
		||||
        res.json(auditSummary);
 | 
			
		||||
      } else {
 | 
			
		||||
        return res.status(404).send(`transaction audit not available`);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getBlocks(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import config from '../config';
 | 
			
		||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import memPool from './mempool';
 | 
			
		||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
 | 
			
		||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit, TransactionAudit } from '../mempool.interfaces';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import diskCache from './disk-cache';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
@ -1359,6 +1359,14 @@ class Blocks {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> {
 | 
			
		||||
    if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
 | 
			
		||||
      return BlocksAuditsRepository.$getBlockTxAudit(hash, txid);
 | 
			
		||||
    } else {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getLastDifficultyAdjustmentTime(): number {
 | 
			
		||||
    return this.lastDifficultyAdjustmentTime;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -42,6 +42,19 @@ export interface BlockAudit {
 | 
			
		||||
  matchRate: number,
 | 
			
		||||
  expectedFees?: number,
 | 
			
		||||
  expectedWeight?: number,
 | 
			
		||||
  template?: any[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionAudit {
 | 
			
		||||
  seen?: boolean;
 | 
			
		||||
  expected?: boolean;
 | 
			
		||||
  added?: boolean;
 | 
			
		||||
  prioritized?: boolean;
 | 
			
		||||
  delayed?: number;
 | 
			
		||||
  accelerated?: boolean;
 | 
			
		||||
  conflict?: boolean;
 | 
			
		||||
  coinbase?: boolean;
 | 
			
		||||
  firstSeen?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuditScore {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import blocks from '../api/blocks';
 | 
			
		||||
import DB from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { BlockAudit, AuditScore } from '../mempool.interfaces';
 | 
			
		||||
import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces';
 | 
			
		||||
 | 
			
		||||
class BlocksAuditRepositories {
 | 
			
		||||
  public async $saveAudit(audit: BlockAudit): Promise<void> {
 | 
			
		||||
@ -98,6 +98,41 @@ class BlocksAuditRepositories {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
 | 
			
		||||
    try {
 | 
			
		||||
      const blockAudit = await this.$getBlockAudit(hash);
 | 
			
		||||
      
 | 
			
		||||
      if (blockAudit) {
 | 
			
		||||
        const isAdded = blockAudit.addedTxs.includes(txid);
 | 
			
		||||
        const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
 | 
			
		||||
        const isAccelerated = blockAudit.acceleratedTxs.includes(txid);
 | 
			
		||||
        const isConflict = blockAudit.fullrbfTxs.includes(txid);
 | 
			
		||||
        let isExpected = false;
 | 
			
		||||
        let firstSeen = undefined;
 | 
			
		||||
        blockAudit.template?.forEach(tx => {
 | 
			
		||||
          if (tx.txid === txid) {
 | 
			
		||||
            isExpected = true;
 | 
			
		||||
            firstSeen = tx.time;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          seen: isExpected || isPrioritized || isAccelerated,
 | 
			
		||||
          expected: isExpected,
 | 
			
		||||
          added: isAdded,
 | 
			
		||||
          prioritized: isPrioritized,
 | 
			
		||||
          conflict: isConflict,
 | 
			
		||||
          accelerated: isAccelerated,
 | 
			
		||||
          firstSeen,
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err(`Cannot fetch block transaction audit from db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(
 | 
			
		||||
 | 
			
		||||
@ -295,6 +295,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
            ),
 | 
			
		||||
          !this.isAuditAvailableFromBlockHeight(block.height) ? of(null) : this.apiService.getBlockAudit$(block.id)
 | 
			
		||||
            .pipe(
 | 
			
		||||
              tap(() => this.cacheService.setBlockAuditLoaded(block.id)),
 | 
			
		||||
              catchError((err) => {
 | 
			
		||||
                this.overviewError = err;
 | 
			
		||||
                return of(null);
 | 
			
		||||
 | 
			
		||||
@ -322,6 +322,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
 | 
			
		||||
            })
 | 
			
		||||
          ),
 | 
			
		||||
          fetchAudit ? this.apiService.getBlockAudit$(hash).pipe(
 | 
			
		||||
            tap((blockAudit) => this.cacheService.setBlockAuditLoaded(hash)),
 | 
			
		||||
            map(audit => {
 | 
			
		||||
              const isAdded = audit.addedTxs.includes(txid);
 | 
			
		||||
              const isPrioritized = audit.prioritizedTxs.includes(txid);
 | 
			
		||||
 | 
			
		||||
@ -42,7 +42,7 @@ interface Pool {
 | 
			
		||||
  slug: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface AuditStatus {
 | 
			
		||||
export interface TxAuditStatus {
 | 
			
		||||
  seen?: boolean;
 | 
			
		||||
  expected?: boolean;
 | 
			
		||||
  added?: boolean;
 | 
			
		||||
@ -100,7 +100,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
  sigops: number | null;
 | 
			
		||||
  adjustedVsize: number | null;
 | 
			
		||||
  pool: Pool | null;
 | 
			
		||||
  auditStatus: AuditStatus | null;
 | 
			
		||||
  auditStatus: TxAuditStatus | null;
 | 
			
		||||
  isAcceleration: boolean = false;
 | 
			
		||||
  filters: Filter[] = [];
 | 
			
		||||
  showCpfpDetails = false;
 | 
			
		||||
@ -364,33 +364,41 @@ 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;
 | 
			
		||||
        return fetchAudit ? this.apiService.getBlockAudit$(hash).pipe(
 | 
			
		||||
          map(audit => {
 | 
			
		||||
            const isAdded = audit.addedTxs.includes(txid);
 | 
			
		||||
            const isPrioritized = audit.prioritizedTxs.includes(txid);
 | 
			
		||||
            const isAccelerated = audit.acceleratedTxs.includes(txid);
 | 
			
		||||
            const isConflict = audit.fullrbfTxs.includes(txid);
 | 
			
		||||
            const isExpected = audit.template.some(tx => tx.txid === txid);
 | 
			
		||||
            const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
 | 
			
		||||
            return {
 | 
			
		||||
              seen: isExpected || isPrioritized || isAccelerated,
 | 
			
		||||
              expected: isExpected,
 | 
			
		||||
              added: isAdded,
 | 
			
		||||
              prioritized: isPrioritized,
 | 
			
		||||
              conflict: isConflict,
 | 
			
		||||
              accelerated: isAccelerated,
 | 
			
		||||
              firstSeen,
 | 
			
		||||
            };
 | 
			
		||||
          }),
 | 
			
		||||
          retry({ count: 3, delay: 2000 }),
 | 
			
		||||
          catchError(() => {
 | 
			
		||||
            return of(null);
 | 
			
		||||
          })
 | 
			
		||||
        ) : of(isCoinbase ? { coinbase: true } : null);
 | 
			
		||||
        if (fetchAudit) {
 | 
			
		||||
        // If block audit is already cached, use it to get transaction audit
 | 
			
		||||
          const blockAuditLoaded = this.cacheService.getBlockAuditLoaded(hash);
 | 
			
		||||
          if (blockAuditLoaded) {
 | 
			
		||||
            return this.apiService.getBlockAudit$(hash).pipe(
 | 
			
		||||
              map(audit => {
 | 
			
		||||
                const isAdded = audit.addedTxs.includes(txid);
 | 
			
		||||
                const isPrioritized = audit.prioritizedTxs.includes(txid);
 | 
			
		||||
                const isAccelerated = audit.acceleratedTxs.includes(txid);
 | 
			
		||||
                const isConflict = audit.fullrbfTxs.includes(txid);
 | 
			
		||||
                const isExpected = audit.template.some(tx => tx.txid === txid);
 | 
			
		||||
                const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
 | 
			
		||||
                return {
 | 
			
		||||
                  seen: isExpected || isPrioritized || isAccelerated,
 | 
			
		||||
                  expected: isExpected,
 | 
			
		||||
                  added: isAdded,
 | 
			
		||||
                  prioritized: isPrioritized,
 | 
			
		||||
                  conflict: isConflict,
 | 
			
		||||
                  accelerated: isAccelerated,
 | 
			
		||||
                  firstSeen,
 | 
			
		||||
                };
 | 
			
		||||
              })
 | 
			
		||||
            )
 | 
			
		||||
          } else {
 | 
			
		||||
            return this.apiService.getBlockTxAudit$(hash, txid).pipe(
 | 
			
		||||
              retry({ count: 3, delay: 2000 }),
 | 
			
		||||
              catchError(() => {
 | 
			
		||||
                return of(null);
 | 
			
		||||
              })
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          return of(isCoinbase ? { coinbase: true } : null);
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      catchError((e) => {
 | 
			
		||||
        return of(null);
 | 
			
		||||
      })
 | 
			
		||||
    ).subscribe(auditStatus => {
 | 
			
		||||
      this.auditStatus = auditStatus;
 | 
			
		||||
      if (this.auditStatus?.firstSeen) {
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import { Transaction } from '../interfaces/electrs.interface';
 | 
			
		||||
import { Conversion } from './price.service';
 | 
			
		||||
import { StorageService } from './storage.service';
 | 
			
		||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
 | 
			
		||||
import { TxAuditStatus } from '../components/transaction/transaction.component';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
@ -374,6 +375,12 @@ export class ApiService {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockTxAudit$(hash: string, txid: string) : Observable<TxAuditStatus> {
 | 
			
		||||
    return this.httpClient.get<TxAuditStatus>(
 | 
			
		||||
      this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/tx/${txid}/audit`
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockAuditScores$(from: number): Observable<AuditScore[]> {
 | 
			
		||||
    return this.httpClient.get<AuditScore[]>(
 | 
			
		||||
      this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/scores` +
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ export class CacheService {
 | 
			
		||||
  network: string;
 | 
			
		||||
  blockHashCache: { [hash: string]: BlockExtended } = {};
 | 
			
		||||
  blockCache: { [height: number]: BlockExtended } = {};
 | 
			
		||||
  blockAuditLoaded: { [hash: string]: boolean } = {};
 | 
			
		||||
  blockLoading: { [height: number]: boolean } = {};
 | 
			
		||||
  copiesInBlockQueue: { [height: number]: number } = {};
 | 
			
		||||
  blockPriorities: number[] = [];
 | 
			
		||||
@ -97,6 +98,10 @@ export class CacheService {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async setBlockAuditLoaded(hash: string) {
 | 
			
		||||
    this.blockAuditLoaded[hash] = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // increase the priority of a block, to delay removal
 | 
			
		||||
  bumpBlockPriority(height) {
 | 
			
		||||
    this.blockPriorities.push(height);
 | 
			
		||||
@ -124,6 +129,7 @@ export class CacheService {
 | 
			
		||||
  resetBlockCache() {
 | 
			
		||||
    this.blockHashCache = {};
 | 
			
		||||
    this.blockCache = {};
 | 
			
		||||
    this.blockAuditLoaded = {};
 | 
			
		||||
    this.blockLoading = {};
 | 
			
		||||
    this.copiesInBlockQueue = {};
 | 
			
		||||
    this.blockPriorities = [];
 | 
			
		||||
@ -132,4 +138,8 @@ export class CacheService {
 | 
			
		||||
  getCachedBlock(height) {
 | 
			
		||||
    return this.blockCache[height];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockAuditLoaded(hash) {
 | 
			
		||||
    return this.blockAuditLoaded[hash];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user