From 288bddcaf2385d3eac98dc1c5ec708b16fcd9ff0 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 15 Jun 2022 19:53:37 +0000 Subject: [PATCH] Add API endpoint for block summary data --- .../src/api/bitcoin/bitcoin-api.interface.ts | 8 +++ backend/src/api/blocks.ts | 52 ++++++++++++++++++- backend/src/api/disk-cache.ts | 2 + backend/src/index.ts | 3 +- backend/src/mempool.interfaces.ts | 5 ++ backend/src/routes.ts | 10 ++++ 6 files changed, 77 insertions(+), 3 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api.interface.ts b/backend/src/api/bitcoin/bitcoin-api.interface.ts index 6a22af9a0..54d666794 100644 --- a/backend/src/api/bitcoin/bitcoin-api.interface.ts +++ b/backend/src/api/bitcoin/bitcoin-api.interface.ts @@ -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) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 92062f2b4..c013bbdd3 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -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 { + // 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 { try { let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight(); diff --git a/backend/src/api/disk-cache.ts b/backend/src/api/disk-cache.ts index 78c6b6c09..fc185a31a 100644 --- a/backend/src/api/disk-cache.ts +++ b/backend/src/api/disk-cache.ts @@ -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)); } diff --git a/backend/src/index.ts b/backend/src/index.ts index d421a6fba..6bd8de841 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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 diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 8d0fa6972..a35dc6d76 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -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[]; diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 12368b1ee..99f54a9f8 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -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