Add API endpoint for block summary data

This commit is contained in:
Mononaut 2022-06-15 19:53:37 +00:00
parent 2d529bd581
commit 288bddcaf2
6 changed files with 77 additions and 3 deletions

View File

@ -73,6 +73,14 @@ export namespace IBitcoinApi {
time: number; // (numeric) Same as blocktime
}
export interface VerboseBlock extends Block {
tx: VerboseTransaction[]; // The transactions in the format of the getrawtransaction RPC. Different from verbosity = 1 "tx" result
}
export interface VerboseTransaction extends Transaction {
fee?: number; // (numeric) The transaction fee in BTC, omitted if block undo data is not available
}
export interface Vin {
txid?: string; // (string) The transaction id
vout?: number; // (string)

View File

@ -2,11 +2,12 @@ import config from '../config';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import logger from '../logger';
import memPool from './mempool';
import { BlockExtended, PoolTag, TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
import { BlockExtended, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo } from '../mempool.interfaces';
import { Common } from './common';
import diskCache from './disk-cache';
import transactionUtils from './transaction-utils';
import bitcoinClient from './bitcoin/bitcoin-client';
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
import { IEsploraApi } from './bitcoin/esplora-api.interface';
import poolsRepository from '../repositories/PoolsRepository';
import blocksRepository from '../repositories/BlocksRepository';
@ -22,6 +23,7 @@ import poolsParser from './pools-parser';
class Blocks {
private blocks: BlockExtended[] = [];
private blockSummaries: BlockSummary[] = [];
private currentBlockHeight = 0;
private currentDifficulty = 0;
private lastDifficultyAdjustmentTime = 0;
@ -38,6 +40,14 @@ class Blocks {
this.blocks = blocks;
}
public getBlockSummaries(): BlockSummary[] {
return this.blockSummaries;
}
public setBlockSummaries(blockSummaries: BlockSummary[]) {
this.blockSummaries = blockSummaries;
}
public setNewBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void) {
this.newBlockCallbacks.push(fn);
}
@ -106,6 +116,27 @@ class Blocks {
return transactions;
}
/**
* Return a block summary (list of stripped transactions)
* @param block
* @returns BlockSummary
*/
private summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary {
const stripped = block.tx.map((tx) => {
return {
txid: tx.txid,
vsize: tx.vsize,
fee: tx.fee ? Math.round(tx.fee * 100000000) : 0,
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0) * 100000000)
};
});
return {
id: block.hash,
transactions: stripped
};
}
/**
* Return a block with additional data (reward, coinbase, fees...)
* @param block
@ -341,10 +372,12 @@ class Blocks {
}
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
const block = BitcoinApi.convertBlock(await bitcoinClient.getBlock(blockHash));
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
const block = BitcoinApi.convertBlock(verboseBlock);
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false);
const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions);
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
if (Common.indexingEnabled()) {
if (!fastForwarded) {
@ -375,6 +408,10 @@ class Blocks {
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
}
this.blockSummaries.push(blockSummary);
if (this.blockSummaries.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
this.blockSummaries = this.blockSummaries.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
}
if (this.newBlockCallbacks.length) {
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
@ -440,6 +477,17 @@ class Blocks {
return blockExtended;
}
public async $getStrippedBlockTransactions(hash: string): Promise<TransactionStripped[]> {
// Check the memory cache
const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash);
if (cachedSummary) {
return cachedSummary.transactions;
}
const block = await bitcoinClient.getBlock(hash, 2);
const summary = this.summarizeBlock(block);
return summary.transactions;
}
public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
try {
let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight();

View File

@ -43,6 +43,7 @@ class DiskCache {
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(),
blockSummaries: blocks.getBlockSummaries(),
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), {flag: 'w'});
@ -109,6 +110,7 @@ class DiskCache {
memPool.setMempool(data.mempool);
blocks.setBlocks(data.blocks);
blocks.setBlockSummaries(data.blockSummaries || []);
} catch (e) {
logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
}

View File

@ -314,7 +314,8 @@ class Server {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock);
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', routes.getStrippedBlockTransactions);
if (config.MEMPOOL.BACKEND !== 'esplora') {
this.app

View File

@ -106,6 +106,11 @@ export interface BlockExtended extends IEsploraApi.Block {
extras: BlockExtension;
}
export interface BlockSummary {
id: string;
transactions: TransactionStripped[];
}
export interface TransactionMinerInfo {
vin: VinStrippedToScriptsig[];
vout: VoutStrippedToScriptPubkey[];

View File

@ -726,6 +726,16 @@ class Routes {
}
}
public async getStrippedBlockTransactions(req: Request, res: Response) {
try {
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(transactions);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async getBlocks(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin