Add API endpoint for block summary data
This commit is contained in:
parent
2d529bd581
commit
288bddcaf2
@ -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)
|
||||||
|
@ -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();
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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[];
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user