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