diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index ea8154206..0ff30376c 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -95,6 +95,8 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary) .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/:to', this.getBlocksByBulk.bind(this)) ; if (config.MEMPOOL.BACKEND !== 'esplora') { @@ -402,6 +404,32 @@ class BitcoinRoutes { } } + private async getBlocksByBulk(req: Request, res: Response) { + try { + if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid, Bisq - Not implemented + return res.status(404).send(`Not implemented`); + } + + const from = parseInt(req.params.from, 10); + if (!from) { + return res.status(400).send(`Parameter 'from' must be a block height (integer)`); + } + const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10); + if (!to) { + return res.status(400).send(`Parameter 'to' must be a block height (integer)`); + } + if (from > to) { + return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`); + } + + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(await blocks.$getBlocksByBulk(from, to)); + + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async getLegacyBlocks(req: Request, res: Response) { try { const returnBlocks: IEsploraApi.Block[] = []; diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index d110186f5..5c8884c71 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -688,7 +688,6 @@ class Blocks { } public async $getBlocks(fromHeight?: number, limit: number = 15): Promise { - let currentHeight = fromHeight !== undefined ? fromHeight : this.currentBlockHeight; if (currentHeight > this.currentBlockHeight) { limit -= currentHeight - this.currentBlockHeight; @@ -728,6 +727,63 @@ class Blocks { return returnBlocks; } + public async $getBlocksByBulk(start: number, end: number) { + start = Math.max(1, start); + + const blocks: any[] = []; + for (let i = end; i >= start; --i) { + const blockHash = await bitcoinApi.$getBlockHash(i); + const coreBlock = await bitcoinClient.getBlock(blockHash); + const electrsBlock = await bitcoinApi.$getBlock(blockHash); + const txs = await this.$getTransactionsExtended(blockHash, i, true); + const stats = await bitcoinClient.getBlockStats(blockHash); + const header = await bitcoinClient.getBlockHeader(blockHash, false); + const txoutset = await bitcoinClient.getTxoutSetinfo('none', i); + + const formatted = { + blockhash: coreBlock.id, + blockheight: coreBlock.height, + prev_blockhash: coreBlock.previousblockhash, + timestamp: coreBlock.timestamp, + median_timestamp: coreBlock.mediantime, + // @ts-ignore + blocktime: coreBlock.time, + orphaned: null, + header: header, + version: coreBlock.version, + difficulty: coreBlock.difficulty, + merkle_root: coreBlock.merkle_root, + bits: coreBlock.bits, + nonce: coreBlock.nonce, + coinbase_scriptsig: txs[0].vin[0].scriptsig, + coinbase_address: txs[0].vout[0].scriptpubkey_address, + coinbase_signature: txs[0].vout[0].scriptpubkey_asm, + size: coreBlock.size, + virtual_size: coreBlock.weight / 4.0, + weight: coreBlock.weight, + utxoset_size: txoutset.txouts, + utxoset_change: stats.utxo_increase, + total_txs: coreBlock.tx_count, + avg_tx_size: Math.round(stats.total_size / stats.txs * 100) * 0.01, + total_inputs: stats.ins, + total_outputs: stats.outs, + total_input_amt: Math.round(txoutset.block_info.prevout_spent * 100000000), + total_output_amt: stats.total_out, + block_subsidy: txs[0].vout.reduce((acc, curr) => acc + curr.value, 0), + total_fee: stats.totalfee, + avg_feerate: stats.avgfeerate, + feerate_percentiles: [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(), + avg_fee: stats.avgfee, + fee_percentiles: null, + segwit_total_txs: stats.swtxs, + segwit_total_size: stats.swtotal_size, + segwit_total_weight: stats.swtotal_weight, + }; + blocks.push(formatted); + } + return blocks; + } + public async $getBlockAuditSummary(hash: string): Promise { let summary; if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { diff --git a/backend/src/rpc-api/commands.ts b/backend/src/rpc-api/commands.ts index ea9bd7bf0..5905a2bb6 100644 --- a/backend/src/rpc-api/commands.ts +++ b/backend/src/rpc-api/commands.ts @@ -88,5 +88,6 @@ module.exports = { verifyTxOutProof: 'verifytxoutproof', // bitcoind v0.11.0+ walletLock: 'walletlock', walletPassphrase: 'walletpassphrase', - walletPassphraseChange: 'walletpassphrasechange' + walletPassphraseChange: 'walletpassphrasechange', + getTxoutSetinfo: 'gettxoutsetinfo' }