Merge pull request #5305 from mempool/natsoni/avoid-fetching-full-audit
Avoid fetching full audit on tx page
This commit is contained in:
		
						commit
						dca7df709b
					
				@ -42,6 +42,7 @@ class BitcoinRoutes {
 | 
				
			|||||||
      .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/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 + 'blocks/tip/height', this.getBlockTipHeight)
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
 | 
				
			||||||
      .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
 | 
					      .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
 | 
				
			||||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
 | 
					      .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) {
 | 
					  private async getBlocks(req: Request, res: Response) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
 | 
					      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 bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
 | 
				
			||||||
import logger from '../logger';
 | 
					import logger from '../logger';
 | 
				
			||||||
import memPool from './mempool';
 | 
					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 { Common } from './common';
 | 
				
			||||||
import diskCache from './disk-cache';
 | 
					import diskCache from './disk-cache';
 | 
				
			||||||
import transactionUtils from './transaction-utils';
 | 
					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 {
 | 
					  public getLastDifficultyAdjustmentTime(): number {
 | 
				
			||||||
    return this.lastDifficultyAdjustmentTime;
 | 
					    return this.lastDifficultyAdjustmentTime;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -42,6 +42,19 @@ export interface BlockAudit {
 | 
				
			|||||||
  matchRate: number,
 | 
					  matchRate: number,
 | 
				
			||||||
  expectedFees?: number,
 | 
					  expectedFees?: number,
 | 
				
			||||||
  expectedWeight?: 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 {
 | 
					export interface AuditScore {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import blocks from '../api/blocks';
 | 
					import blocks from '../api/blocks';
 | 
				
			||||||
import DB from '../database';
 | 
					import DB from '../database';
 | 
				
			||||||
import logger from '../logger';
 | 
					import logger from '../logger';
 | 
				
			||||||
import { BlockAudit, AuditScore } from '../mempool.interfaces';
 | 
					import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BlocksAuditRepositories {
 | 
					class BlocksAuditRepositories {
 | 
				
			||||||
  public async $saveAudit(audit: BlockAudit): Promise<void> {
 | 
					  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> {
 | 
					  public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const [rows]: any[] = await DB.query(
 | 
					      const [rows]: any[] = await DB.query(
 | 
				
			||||||
 | 
				
			|||||||
@ -42,7 +42,7 @@ interface Pool {
 | 
				
			|||||||
  slug: string;
 | 
					  slug: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface AuditStatus {
 | 
					export interface TxAuditStatus {
 | 
				
			||||||
  seen?: boolean;
 | 
					  seen?: boolean;
 | 
				
			||||||
  expected?: boolean;
 | 
					  expected?: boolean;
 | 
				
			||||||
  added?: boolean;
 | 
					  added?: boolean;
 | 
				
			||||||
@ -100,7 +100,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
  sigops: number | null;
 | 
					  sigops: number | null;
 | 
				
			||||||
  adjustedVsize: number | null;
 | 
					  adjustedVsize: number | null;
 | 
				
			||||||
  pool: Pool | null;
 | 
					  pool: Pool | null;
 | 
				
			||||||
  auditStatus: AuditStatus | null;
 | 
					  auditStatus: TxAuditStatus | null;
 | 
				
			||||||
  isAcceleration: boolean = false;
 | 
					  isAcceleration: boolean = false;
 | 
				
			||||||
  filters: Filter[] = [];
 | 
					  filters: Filter[] = [];
 | 
				
			||||||
  showCpfpDetails = false;
 | 
					  showCpfpDetails = false;
 | 
				
			||||||
@ -374,33 +374,41 @@ 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;
 | 
				
			||||||
        return fetchAudit ? this.apiService.getBlockAudit$(hash).pipe(
 | 
					        if (fetchAudit) {
 | 
				
			||||||
          map(audit => {
 | 
					        // If block audit is already cached, use it to get transaction audit
 | 
				
			||||||
            const isAdded = audit.addedTxs.includes(txid);
 | 
					          const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash);
 | 
				
			||||||
            const isPrioritized = audit.prioritizedTxs.includes(txid);
 | 
					          if (blockAuditLoaded) {
 | 
				
			||||||
            const isAccelerated = audit.acceleratedTxs.includes(txid);
 | 
					            return this.apiService.getBlockAudit$(hash).pipe(
 | 
				
			||||||
            const isConflict = audit.fullrbfTxs.includes(txid);
 | 
					              map(audit => {
 | 
				
			||||||
            const isExpected = audit.template.some(tx => tx.txid === txid);
 | 
					                const isAdded = audit.addedTxs.includes(txid);
 | 
				
			||||||
            const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
 | 
					                const isPrioritized = audit.prioritizedTxs.includes(txid);
 | 
				
			||||||
            return {
 | 
					                const isAccelerated = audit.acceleratedTxs.includes(txid);
 | 
				
			||||||
              seen: isExpected || isPrioritized || isAccelerated,
 | 
					                const isConflict = audit.fullrbfTxs.includes(txid);
 | 
				
			||||||
              expected: isExpected,
 | 
					                const isExpected = audit.template.some(tx => tx.txid === txid);
 | 
				
			||||||
              added: isAdded,
 | 
					                const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
 | 
				
			||||||
              prioritized: isPrioritized,
 | 
					                return {
 | 
				
			||||||
              conflict: isConflict,
 | 
					                  seen: isExpected || isPrioritized || isAccelerated,
 | 
				
			||||||
              accelerated: isAccelerated,
 | 
					                  expected: isExpected,
 | 
				
			||||||
              firstSeen,
 | 
					                  added: isAdded,
 | 
				
			||||||
            };
 | 
					                  prioritized: isPrioritized,
 | 
				
			||||||
          }),
 | 
					                  conflict: isConflict,
 | 
				
			||||||
          retry({ count: 3, delay: 2000 }),
 | 
					                  accelerated: isAccelerated,
 | 
				
			||||||
          catchError(() => {
 | 
					                  firstSeen,
 | 
				
			||||||
            return of(null);
 | 
					                };
 | 
				
			||||||
          })
 | 
					              })
 | 
				
			||||||
        ) : of(isCoinbase ? { coinbase: true } : null);
 | 
					            )
 | 
				
			||||||
 | 
					          } 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 => {
 | 
					    ).subscribe(auditStatus => {
 | 
				
			||||||
      this.auditStatus = auditStatus;
 | 
					      this.auditStatus = auditStatus;
 | 
				
			||||||
      if (this.auditStatus?.firstSeen) {
 | 
					      if (this.auditStatus?.firstSeen) {
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ import { Transaction } from '../interfaces/electrs.interface';
 | 
				
			|||||||
import { Conversion } from './price.service';
 | 
					import { Conversion } from './price.service';
 | 
				
			||||||
import { StorageService } from './storage.service';
 | 
					import { StorageService } from './storage.service';
 | 
				
			||||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
 | 
					import { WebsocketResponse } from '../interfaces/websocket.interface';
 | 
				
			||||||
 | 
					import { TxAuditStatus } from '../components/transaction/transaction.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable({
 | 
					@Injectable({
 | 
				
			||||||
  providedIn: 'root'
 | 
					  providedIn: 'root'
 | 
				
			||||||
@ -17,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 blockAuditLoaded: { [hash: string]: boolean } = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    private httpClient: HttpClient,
 | 
					    private httpClient: HttpClient,
 | 
				
			||||||
@ -369,11 +371,18 @@ export class ApiService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getBlockAudit$(hash: string) : Observable<BlockAudit> {
 | 
					  getBlockAudit$(hash: string) : Observable<BlockAudit> {
 | 
				
			||||||
 | 
					    this.setBlockAuditLoaded(hash);
 | 
				
			||||||
    return this.httpClient.get<BlockAudit>(
 | 
					    return this.httpClient.get<BlockAudit>(
 | 
				
			||||||
      this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`
 | 
					      this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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[]> {
 | 
					  getBlockAuditScores$(from: number): Observable<AuditScore[]> {
 | 
				
			||||||
    return this.httpClient.get<AuditScore[]>(
 | 
					    return this.httpClient.get<AuditScore[]>(
 | 
				
			||||||
      this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/scores` +
 | 
					      this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/scores` +
 | 
				
			||||||
@ -526,4 +535,13 @@ export class ApiService {
 | 
				
			|||||||
      this.apiBaseUrl + this.apiBasePath + '/api/v1/accelerations/total' + (queryString?.length ? '?' + queryString : '')
 | 
					      this.apiBaseUrl + this.apiBasePath + '/api/v1/accelerations/total' + (queryString?.length ? '?' + queryString : '')
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Cache methods
 | 
				
			||||||
 | 
					  async setBlockAuditLoaded(hash: string) {
 | 
				
			||||||
 | 
					    this.blockAuditLoaded[hash] = true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBlockAuditLoaded(hash) {
 | 
				
			||||||
 | 
					    return this.blockAuditLoaded[hash];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -124,6 +124,7 @@ export class CacheService {
 | 
				
			|||||||
  resetBlockCache() {
 | 
					  resetBlockCache() {
 | 
				
			||||||
    this.blockHashCache = {};
 | 
					    this.blockHashCache = {};
 | 
				
			||||||
    this.blockCache = {};
 | 
					    this.blockCache = {};
 | 
				
			||||||
 | 
					    this.apiService.blockAuditLoaded = {};
 | 
				
			||||||
    this.blockLoading = {};
 | 
					    this.blockLoading = {};
 | 
				
			||||||
    this.copiesInBlockQueue = {};
 | 
					    this.copiesInBlockQueue = {};
 | 
				
			||||||
    this.blockPriorities = [];
 | 
					    this.blockPriorities = [];
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user