Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14864772
D15210.id44489.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Subscribers
None
D15210.id44489.diff
View Options
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
Details
Attached
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)
Attached To
D15210: [chronik-client] Add block and blocks methods to in-node chronik-client
Event Timeline
Log In to Comment