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 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 { export interface Vin {
txid?: string; // (string) The transaction id txid?: string; // (string) The transaction id
vout?: number; // (string) vout?: number; // (string)

View File

@ -2,11 +2,12 @@ import config from '../config';
import bitcoinApi from './bitcoin/bitcoin-api-factory'; import bitcoinApi from './bitcoin/bitcoin-api-factory';
import logger from '../logger'; import logger from '../logger';
import memPool from './mempool'; 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 { Common } from './common';
import diskCache from './disk-cache'; import diskCache from './disk-cache';
import transactionUtils from './transaction-utils'; import transactionUtils from './transaction-utils';
import bitcoinClient from './bitcoin/bitcoin-client'; import bitcoinClient from './bitcoin/bitcoin-client';
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
import { IEsploraApi } from './bitcoin/esplora-api.interface'; import { IEsploraApi } from './bitcoin/esplora-api.interface';
import poolsRepository from '../repositories/PoolsRepository'; import poolsRepository from '../repositories/PoolsRepository';
import blocksRepository from '../repositories/BlocksRepository'; import blocksRepository from '../repositories/BlocksRepository';
@ -22,6 +23,7 @@ import poolsParser from './pools-parser';
class Blocks { class Blocks {
private blocks: BlockExtended[] = []; private blocks: BlockExtended[] = [];
private blockSummaries: BlockSummary[] = [];
private currentBlockHeight = 0; private currentBlockHeight = 0;
private currentDifficulty = 0; private currentDifficulty = 0;
private lastDifficultyAdjustmentTime = 0; private lastDifficultyAdjustmentTime = 0;
@ -38,6 +40,14 @@ class Blocks {
this.blocks = 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) { public setNewBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void) {
this.newBlockCallbacks.push(fn); this.newBlockCallbacks.push(fn);
} }
@ -106,6 +116,27 @@ class Blocks {
return transactions; 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...) * Return a block with additional data (reward, coinbase, fees...)
* @param block * @param block
@ -341,10 +372,12 @@ class Blocks {
} }
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight); 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 txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false); const transactions = await this.$getTransactionsExtended(blockHash, block.height, false);
const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions); const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions);
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
if (!fastForwarded) { if (!fastForwarded) {
@ -375,6 +408,10 @@ class Blocks {
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) { if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
this.blocks = this.blocks.slice(-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) { if (this.newBlockCallbacks.length) {
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions)); this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
@ -440,6 +477,17 @@ class Blocks {
return blockExtended; 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[]> { public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
try { try {
let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight(); let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight();

View File

@ -43,6 +43,7 @@ class DiskCache {
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({ await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
cacheSchemaVersion: this.cacheSchemaVersion, cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(), blocks: blocks.getBlocks(),
blockSummaries: blocks.getBlockSummaries(),
mempool: {}, mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize), mempoolArray: mempoolArray.splice(0, chunkSize),
}), {flag: 'w'}); }), {flag: 'w'});
@ -109,6 +110,7 @@ class DiskCache {
memPool.setMempool(data.mempool); memPool.setMempool(data.mempool);
blocks.setBlocks(data.blocks); blocks.setBlocks(data.blocks);
blocks.setBlockSummaries(data.blockSummaries || []);
} catch (e) { } catch (e) {
logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : 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 this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks.bind(routes)) .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 + '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') { if (config.MEMPOOL.BACKEND !== 'esplora') {
this.app this.app

View File

@ -106,6 +106,11 @@ export interface BlockExtended extends IEsploraApi.Block {
extras: BlockExtension; extras: BlockExtension;
} }
export interface BlockSummary {
id: string;
transactions: TransactionStripped[];
}
export interface TransactionMinerInfo { export interface TransactionMinerInfo {
vin: VinStrippedToScriptsig[]; vin: VinStrippedToScriptsig[];
vout: VoutStrippedToScriptPubkey[]; 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) { public async getBlocks(req: Request, res: Response) {
try { try {
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin