Page MenuHomePhabricator

D15210.id44489.diff
No OneTemporary

D15210.id44489.diff

diff --git a/modules/chronik-client/README.md b/modules/chronik-client/README.md
--- a/modules/chronik-client/README.md
+++ b/modules/chronik-client/README.md
@@ -87,3 +87,4 @@
0.10.1 - Deprecate client-side ping keepAlive. Server-side is now available.
0.11.0 - Add support for `chronikInfo()` method to `ChronikClientNode`
0.11.1 - Do not try next server if error is unrelated to server failure
+0.12.0 - Add support for `block(hashOrHeight)` and `blocks(startHeight, endHeight)` methods to `ChronikClientNode`
diff --git a/modules/chronik-client/package.json b/modules/chronik-client/package.json
--- a/modules/chronik-client/package.json
+++ b/modules/chronik-client/package.json
@@ -1,6 +1,6 @@
{
"name": "chronik-client",
- "version": "0.11.1",
+ "version": "0.12.0",
"description": "A client for accessing the Chronik Indexer API",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/modules/chronik-client/src/ChronikClientNode.ts b/modules/chronik-client/src/ChronikClientNode.ts
--- a/modules/chronik-client/src/ChronikClientNode.ts
+++ b/modules/chronik-client/src/ChronikClientNode.ts
@@ -41,6 +41,28 @@
const chronikServerInfo = proto.ChronikInfo.decode(data);
return convertToChronikInfo(chronikServerInfo);
}
+
+ /** Fetch the block given hash or height. */
+ public async block(hashOrHeight: string | number): Promise<Block_InNode> {
+ const data = await this._proxyInterface.get(`/block/${hashOrHeight}`);
+ const block = proto.Block.decode(data);
+ return convertToBlock(block);
+ }
+
+ /**
+ * Fetch block info of a range of blocks.
+ * `startHeight` and `endHeight` are inclusive ranges.
+ */
+ public async blocks(
+ startHeight: number,
+ endHeight: number,
+ ): Promise<BlockInfo_InNode[]> {
+ const data = await this._proxyInterface.get(
+ `/blocks/${startHeight}/${endHeight}`,
+ );
+ const blocks = proto.Blocks.decode(data);
+ return blocks.blocks.map(convertToBlockInfo);
+ }
}
function convertToBlockchainInfo(
@@ -61,7 +83,74 @@
};
}
+function convertToBlock(block: proto.Block): Block_InNode {
+ if (block.blockInfo === undefined) {
+ throw new Error('Block has no blockInfo');
+ }
+ return {
+ blockInfo: convertToBlockInfo(block.blockInfo),
+ };
+}
+
+function convertToBlockInfo(block: proto.BlockInfo): BlockInfo_InNode {
+ return {
+ ...block,
+ hash: toHexRev(block.hash),
+ prevHash: toHexRev(block.prevHash),
+ timestamp: parseInt(block.timestamp),
+ blockSize: parseInt(block.blockSize),
+ numTxs: parseInt(block.numTxs),
+ numInputs: parseInt(block.numInputs),
+ numOutputs: parseInt(block.numOutputs),
+ sumInputSats: parseInt(block.sumInputSats),
+ sumCoinbaseOutputSats: parseInt(block.sumCoinbaseOutputSats),
+ sumNormalOutputSats: parseInt(block.sumNormalOutputSats),
+ sumBurnedSats: parseInt(block.sumBurnedSats),
+ };
+}
+
/** Info about connected chronik server */
export interface ChronikInfo {
version: string;
}
+
+/** BlockInfo interface for in-node chronik */
+export interface BlockInfo_InNode {
+ /** Block hash of the block, in 'human-readable' (big-endian) hex encoding. */
+ hash: string;
+ /** Block hash of the prev block, in 'human-readable' (big-endian) hex encoding. */
+ prevHash: string;
+ /** Height of the block; Genesis block has height 0. */
+ height: number;
+ /** nBits field of the block, encodes the target compactly. */
+ nBits: number;
+ /**
+ * Timestamp of the block. Filled in by the miner,
+ * so might not be 100 % precise.
+ */
+ timestamp: number;
+ /** Is this block avalanche finalized? */
+ isFinal: boolean;
+ /** Block size of this block in bytes (including headers etc.). */
+ blockSize: number;
+ /** Number of txs in this block. */
+ numTxs: number;
+ /** Total number of tx inputs in block (including coinbase). */
+ numInputs: number;
+ /** Total number of tx output in block (including coinbase). */
+ numOutputs: number;
+ /** Total number of satoshis spent by tx inputs. */
+ sumInputSats: number;
+ /** Total block reward for this block. */
+ sumCoinbaseOutputSats: number;
+ /** Total number of satoshis in non-coinbase tx outputs. */
+ sumNormalOutputSats: number;
+ /** Total number of satoshis burned using OP_RETURN. */
+ sumBurnedSats: number;
+}
+
+/** Block interface for in-node chronik */
+export interface Block_InNode {
+ /** Contains the blockInfo object defined above */
+ blockInfo: BlockInfo_InNode;
+}
diff --git a/modules/chronik-client/test/integration/block_and_blocks.ts b/modules/chronik-client/test/integration/block_and_blocks.ts
new file mode 100644
--- /dev/null
+++ b/modules/chronik-client/test/integration/block_and_blocks.ts
@@ -0,0 +1,329 @@
+import * as chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import { ChildProcess } from 'node:child_process';
+import { EventEmitter, once } from 'node:events';
+import { ChronikClientNode } from '../../index';
+import initializeTestRunner from '../setup/testRunner';
+
+const expect = chai.expect;
+chai.use(chaiAsPromised);
+
+describe('/block and /blocks', () => {
+ let testRunner: ChildProcess;
+ let chronik_url: Promise<Array<string>>;
+ const statusEvent = new EventEmitter();
+
+ before(async () => {
+ testRunner = initializeTestRunner('chronik-client_block_and_blocks');
+
+ testRunner.on('message', function (message: any) {
+ if (message && message.chronik) {
+ console.log('Setting chronik url to ', message.chronik);
+ chronik_url = new Promise(resolve => {
+ resolve([message.chronik]);
+ });
+ }
+
+ if (message && message.status) {
+ statusEvent.emit(message.status);
+ }
+ });
+ });
+
+ after(() => {
+ testRunner.send('stop');
+ });
+
+ beforeEach(async () => {
+ await once(statusEvent, 'ready');
+ });
+
+ afterEach(() => {
+ testRunner.send('next');
+ });
+
+ const REGTEST_CHAIN_INIT_HEIGHT = 200;
+
+ it('gives us the block and blocks', async () => {
+ const chronikUrl = await chronik_url;
+ const chronik = new ChronikClientNode(chronikUrl);
+ const blockFromHeight = await chronik.block(REGTEST_CHAIN_INIT_HEIGHT);
+ expect(blockFromHeight.blockInfo.height).to.eql(
+ REGTEST_CHAIN_INIT_HEIGHT,
+ );
+
+ // Get the same block by calling hash instead of height
+ const blockFromHash = await chronik.block(
+ blockFromHeight.blockInfo.hash,
+ );
+
+ // Verify it is the same
+ expect(blockFromHash).to.deep.equal(blockFromHeight);
+
+ // Gives us blocks
+ const lastThreeBlocks = await chronik.blocks(
+ REGTEST_CHAIN_INIT_HEIGHT - 2,
+ REGTEST_CHAIN_INIT_HEIGHT,
+ );
+
+ const lastThreeBlocksLength = lastThreeBlocks.length;
+ expect(lastThreeBlocksLength).to.eql(3);
+ // Expect the last block to equal the most recent one called with block
+ expect(blockFromHash.blockInfo).to.deep.equal(lastThreeBlocks[2]);
+
+ // Expect lastThreeBlocks to be in order
+ for (let i = 0; i < lastThreeBlocksLength - 1; i += 1) {
+ const thisBlock = lastThreeBlocks[i];
+ const { hash, height } = thisBlock;
+ const nextBlock = lastThreeBlocks[i + 1];
+ expect(hash).to.eql(nextBlock.prevHash);
+ expect(height).to.eql(nextBlock.height - 1);
+ }
+
+ // Throws expected error if we call blocks with startHeight > endHeight
+ await expect(
+ chronik.blocks(
+ REGTEST_CHAIN_INIT_HEIGHT,
+ REGTEST_CHAIN_INIT_HEIGHT - 2,
+ ),
+ ).to.be.rejectedWith(
+ Error,
+ 'Failed getting /blocks/200/198 (): 400: Invalid block end height: 198',
+ );
+ });
+ it('gives us the block at 10 higher', async () => {
+ const chronik = new ChronikClientNode(await chronik_url);
+ const blockFromHeight = await chronik.block(
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+ expect(blockFromHeight.blockInfo.height).to.eql(
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+
+ // Get the same block by calling hash instead of height
+ const blockFromHash = await chronik.block(
+ blockFromHeight.blockInfo.hash,
+ );
+
+ // Verify it is the same
+ expect(blockFromHash).to.deep.equal(blockFromHeight);
+
+ // Gives us blocks
+ const lastThreeBlocks = await chronik.blocks(
+ REGTEST_CHAIN_INIT_HEIGHT + 10 - 2,
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+
+ const lastThreeBlocksLength = lastThreeBlocks.length;
+ expect(lastThreeBlocksLength).to.eql(3);
+ // Expect the last block to equal the most recent one called with block
+ expect(blockFromHash.blockInfo).to.deep.equal(lastThreeBlocks[2]);
+
+ // Expect lastThreeBlocks to be in order
+ for (let i = 0; i < lastThreeBlocksLength - 1; i += 1) {
+ const thisBlock = lastThreeBlocks[i];
+ const { hash, height } = thisBlock;
+ const nextBlock = lastThreeBlocks[i + 1];
+ expect(hash).to.eql(nextBlock.prevHash);
+ expect(height).to.eql(nextBlock.height - 1);
+ }
+ });
+ it('gives us the block after parking the last block and throws expected error attempting to get parked block', async () => {
+ const chronik = new ChronikClientNode(await chronik_url);
+ const blockFromHeight = await chronik.block(
+ REGTEST_CHAIN_INIT_HEIGHT + 9,
+ );
+ expect(blockFromHeight.blockInfo.height).to.eql(
+ REGTEST_CHAIN_INIT_HEIGHT + 9,
+ );
+
+ // Get the same block by calling hash instead of height
+ const blockFromHash = await chronik.block(
+ blockFromHeight.blockInfo.hash,
+ );
+
+ // Verify it is the same
+ expect(blockFromHash).to.deep.equal(blockFromHeight);
+
+ // Throws expected error if asked to fetch the parked block
+ await expect(
+ chronik.block(REGTEST_CHAIN_INIT_HEIGHT + 10),
+ ).to.be.rejectedWith(
+ Error,
+ 'Failed getting /block/210 (): 404: Block not found: 210',
+ );
+
+ // blocks does not throw error if asked for parked block, but also does not return it
+ const latestBlocksAvailable = await chronik.blocks(
+ REGTEST_CHAIN_INIT_HEIGHT + 8,
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+
+ // We only get REGTEST_CHAIN_INIT_HEIGHT + 8 and REGTEST_CHAIN_INIT_HEIGHT + 9,
+ // despite asking for REGTEST_CHAIN_INIT_HEIGHT + 10,
+ const latestBlocksAvailableLength = latestBlocksAvailable.length;
+ expect(latestBlocksAvailableLength).to.equal(2);
+
+ // Expect latestBlocksAvailable to be in order
+ for (let i = 0; i < latestBlocksAvailableLength - 1; i += 1) {
+ const thisBlock = latestBlocksAvailable[i];
+ const { hash, height } = thisBlock;
+ const nextBlock = latestBlocksAvailable[i + 1];
+ expect(hash).to.eql(nextBlock.prevHash);
+ expect(height).to.eql(nextBlock.height - 1);
+ }
+
+ // We get an empty array if we ask for an unavailable block
+ const latestBlockAvailableByBlocks = await chronik.blocks(
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+ expect(latestBlockAvailableByBlocks).to.deep.equal([]);
+ });
+ it('gives us the block and blocks after unparking the last block', async () => {
+ const chronik = new ChronikClientNode(await chronik_url);
+ const blockFromHeight = await chronik.block(
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+ expect(blockFromHeight.blockInfo.height).to.eql(
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+
+ // Get the same block by calling hash instead of height
+ const blockFromHash = await chronik.block(
+ blockFromHeight.blockInfo.hash,
+ );
+
+ // Verify it is the same
+ expect(blockFromHash).to.deep.equal(blockFromHeight);
+
+ // Blocks gets the unparked block now
+ const latestBlocksAvailable = await chronik.blocks(
+ REGTEST_CHAIN_INIT_HEIGHT + 8,
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+
+ // Now we get all 3 blocks
+ const latestBlocksAvailableLength = latestBlocksAvailable.length;
+ expect(latestBlocksAvailableLength).to.equal(3);
+
+ // Expect latestBlocksAvailable to be in order
+ for (let i = 0; i < latestBlocksAvailableLength - 1; i += 1) {
+ const thisBlock = latestBlocksAvailable[i];
+ const { hash, height } = thisBlock;
+ const nextBlock = latestBlocksAvailable[i + 1];
+ expect(hash).to.eql(nextBlock.prevHash);
+ expect(height).to.eql(nextBlock.height - 1);
+ }
+
+ // We get the now-unparked block
+ const latestBlockAvailableByBlocks = await chronik.blocks(
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+
+ // We get it in an array of length 1
+ expect([blockFromHash.blockInfo]).to.deep.equal(
+ latestBlockAvailableByBlocks,
+ );
+ });
+ it('gives us the block and blocks after invalidating the last block and throws expected error attempting to get invalidated block', async () => {
+ const chronik = new ChronikClientNode(await chronik_url);
+ const blockFromHeight = await chronik.block(
+ REGTEST_CHAIN_INIT_HEIGHT + 9,
+ );
+ expect(blockFromHeight.blockInfo.height).to.eql(
+ REGTEST_CHAIN_INIT_HEIGHT + 9,
+ );
+
+ // Get the same block by calling hash instead of height
+ const blockFromHash = await chronik.block(
+ blockFromHeight.blockInfo.hash,
+ );
+
+ // Verify it is the same
+ expect(blockFromHash).to.deep.equal(blockFromHeight);
+
+ // Throws expected error if asked to fetch the parked block
+ await expect(
+ chronik.block(REGTEST_CHAIN_INIT_HEIGHT + 10),
+ ).to.be.rejectedWith(
+ Error,
+ 'Failed getting /block/210 (): 404: Block not found: 210',
+ );
+
+ // blocks does not throw error if asked for invalidated block, but also does not return it
+ const latestBlocksAvailable = await chronik.blocks(
+ REGTEST_CHAIN_INIT_HEIGHT + 8,
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+
+ // We only get REGTEST_CHAIN_INIT_HEIGHT + 8 and REGTEST_CHAIN_INIT_HEIGHT + 9,
+ // despite asking for REGTEST_CHAIN_INIT_HEIGHT + 10,
+ const latestBlocksAvailableLength = latestBlocksAvailable.length;
+ expect(latestBlocksAvailableLength).to.equal(2);
+
+ // Expect latestBlocksAvailable to be in order
+ for (let i = 0; i < latestBlocksAvailableLength - 1; i += 1) {
+ const thisBlock = latestBlocksAvailable[i];
+ const { hash, height } = thisBlock;
+ const nextBlock = latestBlocksAvailable[i + 1];
+ expect(hash).to.eql(nextBlock.prevHash);
+ expect(height).to.eql(nextBlock.height - 1);
+ }
+
+ // We get an empty array if we ask for an unavailable (invalidated) block
+ const latestBlockAvailableByBlocks = await chronik.blocks(
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+ expect(latestBlockAvailableByBlocks).to.deep.equal([]);
+ });
+ it('gives us the block and blocks after reconsiderblock called on the last block', async () => {
+ const chronik = new ChronikClientNode(await chronik_url);
+ const blockFromHeight = await chronik.block(
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+ expect(blockFromHeight.blockInfo.height).to.eql(
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+
+ // Get the same block by calling hash instead of height
+ const blockFromHash = await chronik.block(
+ blockFromHeight.blockInfo.hash,
+ );
+
+ // Verify it is the same
+ expect(blockFromHash).to.deep.equal(blockFromHeight);
+
+ // Blocks gets the reconsidered block now
+ const latestBlocksAvailable = await chronik.blocks(
+ REGTEST_CHAIN_INIT_HEIGHT + 8,
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+
+ // Now we get all 3 blocks
+ const latestBlocksAvailableLength = latestBlocksAvailable.length;
+ expect(latestBlocksAvailableLength).to.equal(3);
+
+ // Expect latestBlocksAvailable to be in order
+ for (let i = 0; i < latestBlocksAvailableLength - 1; i += 1) {
+ const thisBlock = latestBlocksAvailable[i];
+ const { hash, height } = thisBlock;
+ const nextBlock = latestBlocksAvailable[i + 1];
+ expect(hash).to.eql(nextBlock.prevHash);
+ expect(height).to.eql(nextBlock.height - 1);
+ }
+
+ // We get the reconsidered block
+ const latestBlockAvailableByBlocks = await chronik.blocks(
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ REGTEST_CHAIN_INIT_HEIGHT + 10,
+ );
+
+ // We get it in an array of length 1
+ expect([blockFromHash.blockInfo]).to.deep.equal(
+ latestBlockAvailableByBlocks,
+ );
+ });
+});
diff --git a/test/functional/setup_scripts/chronik-client_block_and_blocks.py b/test/functional/setup_scripts/chronik-client_block_and_blocks.py
new file mode 100644
--- /dev/null
+++ b/test/functional/setup_scripts/chronik-client_block_and_blocks.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+# Copyright (c) 2024 The Bitcoin developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""
+Setup script to exercise the chronik-client js library block() function
+"""
+
+import pathmagic # noqa
+from ipc import send_ipc_message
+from setup_framework import SetupFramework
+from test_framework.util import assert_equal
+
+
+class ChronikClient_Block_Setup(SetupFramework):
+ def set_test_params(self):
+ self.num_nodes = 1
+ self.extra_args = [["-chronik"]]
+ self.ipc_timeout = 10
+
+ def skip_test_if_missing_module(self):
+ self.skip_if_no_chronik()
+
+ def send_chronik_info(self):
+ send_ipc_message({"chronik": f"http://127.0.0.1:{self.nodes[0].chronik_port}"})
+
+ def run_test(self):
+ # Init
+ node = self.nodes[0]
+
+ self.send_chronik_info()
+
+ yield True
+
+ self.log.info("Step 1: Mine 10 more blocks")
+ tip = self.generate(node, 10)[-1]
+ assert_equal(node.getblockcount(), 210)
+ yield True
+
+ self.log.info("Step 2: Park the last block")
+ node.parkblock(node.getbestblockhash())
+ assert_equal(node.getblockcount(), 209)
+ yield True
+
+ self.log.info("Step 3: Unpark the last block")
+ node.unparkblock(tip)
+ assert_equal(node.getblockcount(), 210)
+ yield True
+
+ self.log.info("Step 4: invalidate the last block")
+ node.invalidateblock(node.getbestblockhash())
+ assert_equal(node.getblockcount(), 209)
+ yield True
+
+ self.log.info("Step 5: Reconsider the last block")
+ node.reconsiderblock(tip)
+ assert_equal(node.getblockcount(), 210)
+ yield True
+
+
+if __name__ == "__main__":
+ ChronikClient_Block_Setup().main()

File Metadata

Mime Type
text/plain
Expires
Tue, May 20, 22:14 (20 h, 57 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5866040
Default Alt Text
D15210.id44489.diff (19 KB)

Event Timeline