From 4d906e97505900929a79471f76eeb3a2e3f06ecb Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Mon, 2 Sep 2024 15:57:49 +1000 Subject: [PATCH] v17: Add support for blockchain methods Add support to the `v17` client and integration tests for the following methods (all from the `blockchain` section): - `getblockcount` - `getblockhash` - `getblockheader` --- client/src/client_sync/v17/blockchain.rs | 111 ++++- client/src/client_sync/v17/mod.rs | 8 + integration_test/src/v17/blockchain.rs | 125 +++++- integration_test/tests/v17_api.rs | 7 + json/src/model/blockchain.rs | 175 +++++++- json/src/model/mod.rs | 8 +- json/src/v17/blockchain.rs | 530 ++++++++++++++++++++++- json/src/v17/mod.rs | 21 +- 8 files changed, 966 insertions(+), 19 deletions(-) diff --git a/client/src/client_sync/v17/blockchain.rs b/client/src/client_sync/v17/blockchain.rs index f45e341..a7ec514 100644 --- a/client/src/client_sync/v17/blockchain.rs +++ b/client/src/client_sync/v17/blockchain.rs @@ -45,10 +45,7 @@ macro_rules! impl_client_v17__getblock { self.call("getblock", &[into_json(hash)?, 0.into()]) } - pub fn get_block_verbosity_one( - &self, - hash: BlockHash, - ) -> Result { + pub fn get_block_verbosity_one(&self, hash: BlockHash) -> Result { self.call("getblock", &[into_json(hash)?, 1.into()]) } } @@ -67,6 +64,112 @@ macro_rules! impl_client_v17__getblockchaininfo { }; } +/// Implements bitcoind JSON-RPC API method `getblockcount` +#[macro_export] +macro_rules! impl_client_v17__getblockcount { + () => { + impl Client { + pub fn get_block_count(&self) -> Result { + self.call("getblockcount", &[]) + } + } + }; +} + +/// Implements bitcoind JSON-RPC API method `getblockhash` +#[macro_export] +macro_rules! impl_client_v17__getblockhash { + () => { + impl Client { + pub fn get_block_hash(&self, height: u64) -> Result { + self.call("getblockhash", &[into_json(height)?]) + } + } + }; +} + +/// Implements bitcoind JSON-RPC API method `getblockheader` +#[macro_export] +macro_rules! impl_client_v17__getblockheader { + () => { + impl Client { + pub fn get_block_header(&self, hash: &BlockHash) -> Result { + self.call("getblockheader", &[into_json(hash)?, into_json(false)?]) + } + + // This is the same as calling getblockheader with verbose==true. + pub fn get_block_header_verbose( + &self, + hash: &BlockHash, + ) -> Result { + self.call("getblockheader", &[into_json(hash)?]) + } + } + }; +} + +/// Implements bitcoind JSON-RPC API method `getblockstats` +#[macro_export] +macro_rules! impl_client_v17__getblockstats { + () => { + impl Client { + pub fn get_block_stats_by_height(&self, height: u32) -> Result { + self.call("getblockstats", &[into_json(height)?]) + } + + pub fn get_block_stats_by_block_hash(&self, hash: &BlockHash) -> Result { + self.call("getblockstats", &[into_json(hash)?]) + } + } + }; +} + +/// Implements bitcoind JSON-RPC API method `getchaintips` +#[macro_export] +macro_rules! impl_client_v17__getchaintips { + () => { + impl Client { + pub fn get_chain_tips(&self) -> Result { self.call("getchaintips", &[]) } + } + }; +} + +/// Implements bitcoind JSON-RPC API method `getchaintxstats` +#[macro_export] +macro_rules! impl_client_v17__getchaintxstats { + () => { + impl Client { + pub fn get_chain_tx_stats(&self) -> Result { + self.call("getchaintxstats", &[]) + } + } + }; +} + +/// Implements bitcoind JSON-RPC API method `getdifficulty` +#[macro_export] +macro_rules! impl_client_v17__getdifficulty { + () => { + impl Client { + pub fn get_difficulty(&self) -> Result { + self.call("getdifficulty", &[]) + } + } + }; +} + +/// Implements bitcoind JSON-RPC API method `getmempoolancestors` +#[macro_export] +macro_rules! impl_client_v17__getmempoolancestors { + () => { + impl Client { + pub fn get_mempool_ancestors(&self, txid: Txid) -> Result { + self.call("getmempoolancestors", &[into_json(txid)?]) + } + } + }; +} + /// Implements bitcoind JSON-RPC API method `gettxout` #[macro_export] macro_rules! impl_client_v17__gettxout { diff --git a/client/src/client_sync/v17/mod.rs b/client/src/client_sync/v17/mod.rs index 8d8438c..f600735 100644 --- a/client/src/client_sync/v17/mod.rs +++ b/client/src/client_sync/v17/mod.rs @@ -25,6 +25,14 @@ crate::impl_client_v17__getblockchaininfo!(); crate::impl_client_v17__getbestblockhash!(); crate::impl_client_v17__getblock!(); crate::impl_client_v17__gettxout!(); +crate::impl_client_v17__getblockcount!(); +crate::impl_client_v17__getblockhash!(); +crate::impl_client_v17__getblockheader!(); +crate::impl_client_v17__getblockstats!(); +crate::impl_client_v17__getchaintips!(); +crate::impl_client_v17__getchaintxstats!(); +crate::impl_client_v17__getdifficulty!(); +crate::impl_client_v17__getmempoolancestors!(); // == Control == crate::impl_client_v17__stop!(); diff --git a/integration_test/src/v17/blockchain.rs b/integration_test/src/v17/blockchain.rs index 1c0080e..c64c811 100644 --- a/integration_test/src/v17/blockchain.rs +++ b/integration_test/src/v17/blockchain.rs @@ -81,11 +81,134 @@ macro_rules! impl_test_v17__getblockchaininfo { }; } +/// Requires `Client` to be in scope and to implement `getblockcount`. +#[macro_export] +macro_rules! impl_test_v17__getblockcount { + () => { + #[test] + fn get_block_count() { + let bitcoind = $crate::bitcoind_no_wallet(); + let json = bitcoind.client.get_block_count().expect("getblockcount"); + let _ = json.into_model(); + } + }; +} + +/// Requires `Client` to be in scope and to implement `getblockhash`. +#[macro_export] +macro_rules! impl_test_v17__getblockhash { + () => { + #[test] + fn get_block_hash() { + let bitcoind = $crate::bitcoind_no_wallet(); + let json = bitcoind.client.get_block_hash(0).expect("getblockhash"); + assert!(json.into_model().is_ok()); + } + }; +} + +/// Requires `Client` to be in scope and to implement `getblockheader`. +#[macro_export] +macro_rules! impl_test_v17__getblockheader { + () => { + #[test] + fn get_block_header() { // verbose = false + let bitcoind = $crate::bitcoind_no_wallet(); + let block_hash = best_block_hash(); + let json = bitcoind.client.get_block_header(&block_hash).expect("getblockheader"); + assert!(json.into_model().is_ok()); + } + + #[test] + fn get_block_header_verbose() { // verbose = true + let bitcoind = $crate::bitcoind_no_wallet(); + let block_hash = best_block_hash(); + let json = bitcoind.client.get_block_header_verbose(&block_hash).expect("getblockheader"); + assert!(json.into_model().is_ok()); + } + }; +} + +/// Requires `Client` to be in scope and to implement `getblockstats`. +#[macro_export] +macro_rules! impl_test_v17__getblockstats { + () => { + #[test] + fn get_block_stats_by_height() { + let bitcoind = $crate::bitcoind_no_wallet(); + let json = bitcoind.client.get_block_stats_by_height(0).expect("getblockstats"); + assert!(json.into_model().is_ok()); + } + + #[test] + fn get_block_stats_by_hash() { // verbose = true + let bitcoind = $crate::bitcoind_no_wallet(); + let block_hash = best_block_hash(); + let json = bitcoind.client.get_block_stats_by_block_hash(&block_hash).expect("getblockstats"); + assert!(json.into_model().is_ok()); + } + }; +} + +/// Requires `Client` to be in scope and to implement `getchaintips`. +#[macro_export] +macro_rules! impl_test_v17__getchaintips { + () => { + #[test] + fn get_chain_tips() { + let bitcoind = $crate::bitcoind_no_wallet(); + let json = bitcoind.client.get_chain_tips().expect("getchaintips"); + assert!(json.into_model().is_ok()); + } + } +} + +/// Requires `Client` to be in scope and to implement `getchaintxstats`. +#[macro_export] +macro_rules! impl_test_v17__getchaintxstats { + () => { + #[test] + fn get_chain_tx_stats() { + let bitcoind = $crate::bitcoind_no_wallet(); + let json = bitcoind.client.get_chain_tx_stats().expect("getchaintxstats"); + assert!(json.into_model().is_ok()); + } + } +} + +/// Requires `Client` to be in scope and to implement `getdifficulty`. +#[macro_export] +macro_rules! impl_test_v17__getdifficulty { + () => { + #[test] + fn get_difficulty() { + let bitcoind = $crate::bitcoind_no_wallet(); + let json = bitcoind.client.get_difficulty().expect("getdifficulty"); + let _ = json.into_model(); + } + } +} + +/// Requires `Client` to be in scope and to implement `getmempoolancestors`. +#[macro_export] +macro_rules! impl_test_v17__getmempoolancestors { + () => { + #[test] + fn get_mempool_ancestors() { + // FIXME: We need a valid txid to test this. + todo!() + } + } +} + /// Requires `Client` to be in scope and to implement `get_tx_out`. #[macro_export] macro_rules! impl_test_v17__gettxout { () => { #[test] - fn get_tx_out() { todo!() } + fn get_tx_out() { + // FIXME: We need a valid txid to test this. + todo!() + } }; } diff --git a/integration_test/tests/v17_api.rs b/integration_test/tests/v17_api.rs index 2ee2106..c65c9b1 100644 --- a/integration_test/tests/v17_api.rs +++ b/integration_test/tests/v17_api.rs @@ -12,6 +12,13 @@ mod blockchain { impl_test_v17__getblock_verbosity_0!(); impl_test_v17__getblock_verbosity_1!(); impl_test_v17__getblockchaininfo!(); + impl_test_v17__getblockcount!(); + impl_test_v17__getblockhash!(); + impl_test_v17__getblockheader!(); + impl_test_v17__getblockstats!(); + impl_test_v17__getchaintips!(); + impl_test_v17__getchaintxstats!(); + impl_test_v17__getdifficulty!(); } // == Control == diff --git a/json/src/model/blockchain.rs b/json/src/model/blockchain.rs index d841cfb..135b684 100644 --- a/json/src/model/blockchain.rs +++ b/json/src/model/blockchain.rs @@ -9,7 +9,8 @@ use std::collections::BTreeMap; use bitcoin::address::NetworkUnchecked; use bitcoin::{ - block, Address, Block, BlockHash, CompactTarget, Network, TxOut, Txid, Weight, Work, + block, Address, Amount, Block, BlockHash, CompactTarget, FeeRate, Network, TxMerkleNode, TxOut, + Txid, Weight, Work, }; use serde::{Deserialize, Serialize}; @@ -176,6 +177,178 @@ pub struct Bip9SoftforkStatistics { pub possible: Option, } +/// Models the result of JSON-RPC method `getblockcount`. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct GetBlockCount(pub u64); + +/// Models the result of JSON-RPC method `getblockhash`. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct GetBlockHash(pub BlockHash); + +/// Models the result of JSON-RPC method `getblockheader`. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct GetBlockHeader(pub block::Header); + +/// Models the result of JSON-RPC method `getblockheader`. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetBlockHeaderVerbose { + /// the block hash (same as provided). + pub hash: BlockHash, + /// The number of confirmations, or -1 if the block is not on the main chain. + pub confirmations: i64, + /// The block height or index. + pub height: u64, + /// Block version, now repurposed for soft fork signalling. + pub version: block::Version, + /// The root hash of the Merkle tree of transactions in the block. + pub merkle_root: TxMerkleNode, + /// The timestamp of the block, as claimed by the miner (seconds since epoch (Jan 1 1970 GMT). + pub time: u64, + /// The median block time in seconds since epoch (Jan 1 1970 GMT). + pub median_time: u64, + /// The nonce. + pub nonce: u64, + /// The target value below which the blockhash must lie. + pub bits: CompactTarget, + /// The difficulty. + pub difficulty: f64, + /// Expected number of hashes required to produce the current chain. + pub chain_work: Work, + /// The number of transactions in the block. + pub n_tx: u32, + /// The hash of the previous block (if available). + pub previous_block_hash: Option, + /// The hash of the next block (if available). + pub next_block_hash: Option, +} + +/// Models the result of JSON-RPC method `getblockstats`. +// FIXME: Should all the sizes be u32, u64, or usize? +pub struct GetBlockStats { + /// Average fee in the block. + pub average_fee: Amount, + /// Average feerate. + pub average_fee_rate: Option, + /// Average transaction size. + pub average_tx_size: u64, + /// The block hash (to check for potential reorgs). + pub block_hash: BlockHash, + /// Feerates at the 10th, 25th, 50th, 75th, and 90th percentile weight unit (in satoshis per virtual byte). + pub fee_rate_percentiles: Vec>, + /// The height of the block. + pub height: u64, + /// The number of inputs (excluding coinbase). + pub inputs: u64, + /// Maximum fee in the block. + pub max_fee: Amount, + /// Maximum feerate (in satoshis per virtual byte). + pub max_fee_rate: Option, + /// Maximum transaction size. + pub max_tx_size: u64, + /// Truncated median fee in the block. + pub median_fee: Amount, + /// The block median time past. + pub median_time: u32, + /// Truncated median transaction size + pub median_tx_size: u64, + /// Minimum fee in the block. + pub minimum_fee: Amount, + /// Minimum feerate (in satoshis per virtual byte). + pub minimum_fee_rate: Option, + /// Minimum transaction size. + pub minimum_tx_size: u64, + /// The number of outputs. + pub outputs: u64, + /// The block subsidy. + pub subsidy: Amount, + /// Total size of all segwit transactions. + pub segwit_total_size: u64, + /// Total weight of all segwit transactions divided by segwit scale factor (4). + pub segwit_total_weight: Option, + /// The number of segwit transactions. + pub segwit_txs: u64, + /// The block time. + pub time: u32, + /// Total amount in all outputs (excluding coinbase and thus reward [ie subsidy + totalfee]). + pub total_out: Amount, + /// Total size of all non-coinbase transactions. + pub total_size: u64, + /// Total weight of all non-coinbase transactions divided by segwit scale factor (4). + pub total_weight: Option, + /// The fee total. + pub total_fee: Amount, + /// The number of transactions (excluding coinbase). + pub txs: u64, + /// The increase/decrease in the number of unspent outputs. + pub utxo_increase: u64, + /// The increase/decrease in size for the utxo index (not discounting op_return and similar). + pub utxo_size_increase: u64, +} + +/// Result of JSON-RPC method `getchaintips`. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetChainTips(pub Vec); + +/// An individual list item from the result of JSON-RPC method `getchaintips`. +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct ChainTips { + /// Height of the chain tip. + pub height: u64, + /// Block hash of the tip. + pub hash: BlockHash, + /// Zero for main chain. + pub branch_length: u64, + /// "active" for the main chain. + pub status: ChainTipsStatus, +} + +/// The `status` field from an individual list item from the result of JSON-RPC method `getchaintips`. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum ChainTipsStatus { + /// This branch contains at least one invalid block. + Invalid, + /// Not all blocks for this branch are available, but the headers are valid. + HeadersOnly, + /// All blocks are available for this branch, but they were never fully validated. + ValidHeaders, + /// This branch is not part of the active chain, but is fully validated. + ValidFork, + /// This is the tip of the active main chain, which is certainly valid. + Active, +} + +/// Result of JSON-RPC method `getchaintxstats`. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetChainTxStats { + /// The timestamp for the final block in the window in UNIX format. + pub time: u32, + /// The total number of transactions in the chain up to that point. + pub tx_count: u64, + /// The hash of the final block in the window. + pub window_final_block_hash: BlockHash, + /// Size of the window in number of blocks. + pub window_block_count: u64, + /// The number of transactions in the window. Only returned if "window_block_count" is > 0. + pub window_tx_count: Option, + /// The elapsed time in the window in seconds. Only returned if "window_block_count" is > 0. + pub window_interval: Option, + /// The average rate of transactions per second in the window. Only returned if "window_interval" is > 0. + pub tx_rate: Option, +} + +/// Result of JSON-RPC method `getdifficulty`. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetDifficulty(pub f64); + +/// Result of JSON-RPC method `getmempoolancestors` with verbose set to false. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetMempoolAncestors(pub Vec); + +/// Result of JSON-RPC method `getmempoolancestors` with verbose set to true. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetMempoolAncestorsVerbose {} + /// Models the result of JSON-RPC method `gettxout`. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct GetTxOut { diff --git a/json/src/model/mod.rs b/json/src/model/mod.rs index f724434..98d2136 100644 --- a/json/src/model/mod.rs +++ b/json/src/model/mod.rs @@ -26,9 +26,11 @@ mod zmq; #[doc(inline)] pub use self::{ blockchain::{ - Bip9SoftforkInfo, Bip9SoftforkStatistics, Bip9SoftforkStatus, GetBestBlockHash, - GetBlockVerbosityOne, GetBlockVerbosityZero, GetBlockchainInfo, GetTxOut, Softfork, - SoftforkType, + Bip9SoftforkInfo, Bip9SoftforkStatistics, Bip9SoftforkStatus, ChainTips, ChainTipsStatus, + GetBestBlockHash, GetBlockCount, GetBlockHash, GetBlockHeader, GetBlockHeaderVerbose, + GetBlockStats, GetBlockVerbosityOne, GetBlockVerbosityZero, GetBlockchainInfo, + GetChainTips, GetChainTxStats, GetDifficulty, GetMempoolAncestors, + GetMempoolAncestorsVerbose, GetTxOut, Softfork, SoftforkType, }, generating::GenerateToAddress, network::{GetNetworkInfo, GetNetworkInfoAddress, GetNetworkInfoNetwork}, diff --git a/json/src/v17/blockchain.rs b/json/src/v17/blockchain.rs index 7bf648c..fa0a657 100644 --- a/json/src/v17/blockchain.rs +++ b/json/src/v17/blockchain.rs @@ -10,9 +10,10 @@ use std::str::FromStr; use bitcoin::consensus::encode; use bitcoin::error::UnprefixedHexError; +use bitcoin::hex::FromHex; use bitcoin::{ address, amount, block, hex, network, Address, Amount, Block, BlockHash, CompactTarget, - Network, ScriptBuf, TxOut, Txid, Weight, Work, + FeeRate, Network, ScriptBuf, TxMerkleNode, TxOut, Txid, Weight, Work, }; use internals::write_err; use serde::{Deserialize, Serialize}; @@ -388,6 +389,533 @@ impl std::error::Error for GetBlockchainInfoError { } } +/// Result of JSON-RPC method `getblockcount`. +/// +/// > getblockcount +/// +/// > Returns the number of blocks in the longest blockchain. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetBlockCount(pub u64); + +impl GetBlockCount { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> model::GetBlockCount { model::GetBlockCount(self.0) } +} + +/// Result of JSON-RPC method `getblockhash`. +/// +/// > Returns hash of block in best-block-chain at height provided. +/// +/// > Arguments: +/// > 1. height (numeric, required) The height index +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetBlockHash(pub String); + +impl GetBlockHash { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> Result { + let hash = self.0.parse::()?; + Ok(model::GetBlockHash(hash)) + } + + /// Converts json straight to a `bitcoin::BlockHash`. + pub fn block_hash(self) -> Result { Ok(self.into_model()?.0) } +} + +/// Result of JSON-RPC method `getblockheader` with verbosity set to `false`. +/// +/// > If verbose is false, returns a string that is serialized, hex-encoded data for blockheader 'hash'. +/// > If verbose is true, returns an Object with information about blockheader 'hash'. +/// > +/// > Arguments: +/// > 1. "hash" (string, required) The block hash +/// > 2. verbose (boolean, optional, default=true) true for a json object, false for the hex encoded data +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetBlockHeader(pub String); + +impl GetBlockHeader { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> Result { + use GetBlockHeaderError as E; + + let v = Vec::from_hex(&self.0).map_err(E::Hex)?; + let header = encode::deserialize::(&v).map_err(E::Consensus)?; + + Ok(model::GetBlockHeader(header)) + } + + /// Converts json straight to a `bitcoin::BlockHeader`. + pub fn block_header(self) -> Result { + Ok(self.into_model()?.0) + } +} + +/// Error when converting a `GetBlockHeader` type into the model type. +#[derive(Debug)] +pub enum GetBlockHeaderError { + /// Conversion of hex data to bytes failed. + Hex(hex::HexToBytesError), + /// Consensus decoding of bytes to header failed. + Consensus(encode::Error), +} + +impl fmt::Display for GetBlockHeaderError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use GetBlockHeaderError::*; + + match *self { + Hex(ref e) => write_err!(f, "conversion of hex data to bytes failed"; e), + Consensus(ref e) => write_err!(f, "consensus decoding of bytes to header failed"; e), + } + } +} + +impl std::error::Error for GetBlockHeaderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use GetBlockHeaderError::*; + + match *self { + Hex(ref e) => Some(e), + Consensus(ref e) => Some(e), + } + } +} + +/// Result of JSON-RPC method `getblockheader` with verbosity set to `true`. +/// +/// > If verbose is false, returns a string that is serialized, hex-encoded data for blockheader 'hash'. +/// > If verbose is true, returns an Object with information about blockheader ``. +/// > +/// > Arguments: +/// > 1. "hash" (string, required) The block hash +/// > 2. verbose (boolean, optional, default=true) true for a json object, false for the hex encoded data +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetBlockHeaderVerbose { + /// The block hash (same as provided). + pub hash: String, + /// The number of confirmations, or -1 if the block is not on the main chain. + pub confirmations: i64, + /// The block height or index. + pub height: u64, + /// The block version. + pub version: i32, + /// The block version formatted in hexadecimal. + #[serde(rename = "versionHex")] + pub version_hex: String, + /// The merkle root. + #[serde(rename = "merkleroot")] + pub merkle_root: String, + /// The block time in seconds since epoch (Jan 1 1970 GMT). + pub time: u64, + /// The median block time in seconds since epoch (Jan 1 1970 GMT). + #[serde(rename = "mediantime")] + pub median_time: u64, + /// The nonce. + pub nonce: u64, + /// The bits. + pub bits: String, + /// The difficulty. + pub difficulty: f64, + /// Expected number of hashes required to produce the current chain (in hex). + #[serde(rename = "chainwork")] + pub chain_work: String, + /// The number of transactions in the block. + #[serde(rename = "nTx")] + pub n_tx: u32, + /// The hash of the previous block (if available). + #[serde(rename = "previousblockhash")] + pub previous_block_hash: Option, + /// The hash of the next block (if available). + #[serde(rename = "nextblockhash")] + pub next_block_hash: Option, +} + +impl GetBlockHeaderVerbose { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> Result { + use GetBlockHeaderVerboseError as E; + + let hash = self.hash.parse::().map_err(E::Hash)?; + let version = block::Version::from_consensus(self.version); + let merkle_root = self.merkle_root.parse::().map_err(E::MerkleRoot)?; + let bits = CompactTarget::from_unprefixed_hex(&self.bits).map_err(E::Bits)?; + let chain_work = Work::from_unprefixed_hex(&self.bits).map_err(E::ChainWork)?; + let previous_block_hash = self + .previous_block_hash + .map(|s| s.parse::().map_err(E::PreviousBlockHash)) + .transpose()?; + let next_block_hash = self + .next_block_hash + .map(|s| s.parse::().map_err(E::NextBlockHash)) + .transpose()?; + + Ok(model::GetBlockHeaderVerbose { + hash, + confirmations: self.confirmations, + height: self.height, + version, + merkle_root, + time: self.time, + median_time: self.median_time, + nonce: self.nonce, + bits, + difficulty: self.difficulty, + chain_work, + n_tx: self.n_tx, + previous_block_hash, + next_block_hash, + }) + } + + /// Converts json straight to a `bitcoin::BlockHeader`. + pub fn block_header(self) -> Result { todo!() } +} + +/// Error when converting a `GetBlockHeader` type into the model type. +#[derive(Debug)] +pub enum GetBlockHeaderVerboseError { + /// Conversion of `hash` field failed. + Hash(hex::HexToArrayError), + /// Conversion of `merkle_root` field failed. + MerkleRoot(hex::HexToArrayError), + /// Conversion of `bits` field failed. + Bits(UnprefixedHexError), + /// Conversion of `chain_work` field failed. + ChainWork(UnprefixedHexError), + /// Conversion of `previous_block_hash` field failed. + PreviousBlockHash(hex::HexToArrayError), + /// Conversion of `next_block_hash` field failed. + NextBlockHash(hex::HexToArrayError), +} + +/// Result of JSON-RPC method `getblockstats`. +/// +/// > getblockstats hash_or_height ( stats ) +/// +/// > Returns the number of blocks in the longest blockchain. +/// > getblockstats hash_or_height ( stats ) +/// > +/// > Compute per block statistics for a given window. All amounts are in satoshis. +/// > It won't work for some heights with pruning. +/// > It won't work without -txindex for utxo_size_inc, *fee or *feerate stats. +/// > +/// > Arguments: +/// > 1. "hash_or_height" (string or numeric, required) The block hash or height of the target block +/// > 2. "stats" (array, optional) Values to plot, by default all values (see result below) +/// > [ +/// > "height", (string, optional) Selected statistic +/// > "time", (string, optional) Selected statistic +/// > ,... +/// > ] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +// FIXME: Should these fields be u64 or u32? +pub struct GetBlockStats { + /// Average fee in the block. + #[serde(rename = "avgfee")] + pub average_fee: u64, + // TODO: Remember these doces will become silently stale when unit changes in a later version of Core. + /// Average feerate (in satoshis per virtual byte). + #[serde(rename = "avgfeerate")] + pub average_fee_rate: u64, + /// Average transaction size. + #[serde(rename = "avgtxsize")] + pub average_tx_size: u64, + /// The block hash (to check for potential reorgs). + #[serde(rename = "blockhash")] + pub block_hash: String, + /// Feerates at the 10th, 25th, 50th, 75th, and 90th percentile weight unit (in satoshis per + /// virtual byte). + #[serde(rename = "feerate_percentiles")] + pub fee_rate_percentiles: [u64; 5], + /// The height of the block. + pub height: u64, + /// The number of inputs (excluding coinbase). + #[serde(rename = "ins")] + pub inputs: u64, + /// Maximum fee in the block. + #[serde(rename = "maxfee")] + pub max_fee: u64, + /// Maximum feerate (in satoshis per virtual byte). + #[serde(rename = "maxfeerate")] + pub max_fee_rate: u64, + /// Maximum transaction size. + #[serde(rename = "maxtxsize")] + pub max_tx_size: u64, + /// Truncated median fee in the block. + #[serde(rename = "medianfee")] + pub median_fee: u64, + /// The block median time past. + #[serde(rename = "mediantime")] + pub median_time: u32, + /// Truncated median transaction size + #[serde(rename = "mediantxsize")] + pub median_tx_size: u64, + /// Minimum fee in the block. + #[serde(rename = "minfee")] + pub minimum_fee: u64, + /// Minimum feerate (in satoshis per virtual byte). + #[serde(rename = "minfeerate")] + pub minimum_fee_rate: u64, + /// Minimum transaction size. + #[serde(rename = "mintxsize")] + pub minimum_tx_size: u64, + /// The number of outputs. + #[serde(rename = "outs")] + pub outputs: u64, + /// The block subsidy. + pub subsidy: u64, + /// Total size of all segwit transactions. + #[serde(rename = "swtotal_size")] + pub segwit_total_size: u64, + /// Total weight of all segwit transactions divided by segwit scale factor (4). + #[serde(rename = "swtotal_weight")] + pub segwit_total_weight: u64, + /// The number of segwit transactions. + #[serde(rename = "swtxs")] + pub segwit_txs: u64, + /// The block time. + pub time: u32, + /// Total amount in all outputs (excluding coinbase and thus reward [ie subsidy + totalfee]). + pub total_out: u64, + /// Total size of all non-coinbase transactions. + pub total_size: u64, + /// Total weight of all non-coinbase transactions divided by segwit scale factor (4). + pub total_weight: u64, + /// The fee total. + #[serde(rename = "totalfee")] + pub total_fee: u64, + /// The number of transactions (excluding coinbase). + pub txs: u64, + /// The increase/decrease in the number of unspent outputs. + pub utxo_increase: u64, + /// The increase/decrease in size for the utxo index (not discounting op_return and similar). + #[serde(rename = "utxo_size_inc")] + pub utxo_size_increase: u64, +} + +impl GetBlockStats { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> Result { + let block_hash = self.block_hash.parse::()?; + let fee_rate_percentiles = self + .fee_rate_percentiles + .iter() + .map(|vb| FeeRate::from_sat_per_vb(*vb)) + .collect::>>(); + + // `FeeRate::sat_per_vb` returns an option if value overflows. + let average_fee_rate = FeeRate::from_sat_per_vb(self.average_fee_rate); + let max_fee_rate = FeeRate::from_sat_per_vb(self.max_fee_rate); + let minimum_fee_rate = FeeRate::from_sat_per_vb(self.minimum_fee_rate); + + // FIXME: Double check that these values are virtual bytes and not weight units. + let segwit_total_weight = Weight::from_vb(self.segwit_total_weight); + let total_weight = Weight::from_vb(self.total_weight); + + Ok(model::GetBlockStats { + average_fee: Amount::from_sat(self.average_fee), + average_fee_rate, + average_tx_size: self.average_tx_size, + block_hash, + fee_rate_percentiles, + height: self.height, + inputs: self.inputs, + max_fee: Amount::from_sat(self.max_fee), + max_fee_rate, + max_tx_size: self.max_tx_size, + median_fee: Amount::from_sat(self.median_fee), + median_time: self.median_time, + median_tx_size: self.median_tx_size, + minimum_fee: Amount::from_sat(self.minimum_fee), + minimum_fee_rate, + minimum_tx_size: self.minimum_tx_size, + outputs: self.outputs, + subsidy: Amount::from_sat(self.subsidy), + segwit_total_size: self.segwit_total_size, + segwit_total_weight, + segwit_txs: self.segwit_txs, + time: self.time, + total_out: Amount::from_sat(self.total_out), + total_size: self.total_size, + total_weight, + total_fee: Amount::from_sat(self.total_fee), + txs: self.txs, + utxo_increase: self.utxo_increase, + utxo_size_increase: self.utxo_size_increase, + }) + } +} + +/// Result of JSON-RPC method `getchaintips`. +/// +/// > Return information about all known tips in the block tree, including the main chain as well as orphaned branches. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetChainTips(pub Vec); + +/// An individual list item from the result of JSON-RPC method `getchaintips`. +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct ChainTips { + /// Height of the chain tip. + pub height: u64, + /// Block hash of the tip. + pub hash: String, + /// Zero for main chain. + #[serde(rename = "branchlen")] + pub branch_length: u64, + /// "active" for the main chain. + pub status: ChainTipsStatus, +} + +/// The `status` field from an individual list item from the result of JSON-RPC method `getchaintips`. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum ChainTipsStatus { + /// This branch contains at least one invalid block. + Invalid, + /// Not all blocks for this branch are available, but the headers are valid. + HeadersOnly, + /// All blocks are available for this branch, but they were never fully validated. + ValidHeaders, + /// This branch is not part of the active chain, but is fully validated. + ValidFork, + /// This is the tip of the active main chain, which is certainly valid. + Active, +} + +impl GetChainTips { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> Result { + let v = self.0.into_iter().map(|item| item.into_model()).collect::, _>>()?; + Ok(model::GetChainTips(v)) + } +} + +impl ChainTips { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> Result { + let hash = self.hash.parse::()?; + + Ok(model::ChainTips { + height: self.height, + hash, + branch_length: self.branch_length, + status: self.status.into_model(), + }) + } +} + +impl ChainTipsStatus { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> model::ChainTipsStatus { + use model::ChainTipsStatus::*; + + match self { + Self::Invalid => Invalid, + Self::HeadersOnly => HeadersOnly, + Self::ValidHeaders => ValidHeaders, + Self::ValidFork => ValidFork, + Self::Active => Active, + } + } +} + +/// Result of JSON-RPC method `getchaintxstats`. +/// +/// > getchaintxstats ( nblocks blockhash ) +/// > +/// > Compute statistics about the total number and rate of transactions in the chain. +/// > +/// > Arguments: +/// > 1. nblocks (numeric, optional) Size of the window in number of blocks (default: one month). +/// > 2. "blockhash" (string, optional) The hash of the block that ends the window. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetChainTxStats { + /// The timestamp for the final block in the window in UNIX format. + pub time: u32, + /// The total number of transactions in the chain up to that point. + #[serde(rename = "txcount")] + pub tx_count: u64, + /// The hash of the final block in the window. + pub window_final_block_hash: String, + /// Size of the window in number of blocks. + pub window_block_count: u64, + /// The number of transactions in the window. Only returned if "window_block_count" is > 0. + pub window_tx_count: Option, + /// The elapsed time in the window in seconds. Only returned if "window_block_count" is > 0. + pub window_interval: Option, + /// The average rate of transactions per second in the window. Only returned if "window_interval" is > 0. + #[serde(rename = "txrate")] + pub tx_rate: Option, +} + +impl GetChainTxStats { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> Result { + let window_final_block_hash = self.window_final_block_hash.parse::()?; + + Ok(model::GetChainTxStats { + time: self.time, + tx_count: self.tx_count, + window_final_block_hash, + window_block_count: self.window_block_count, + window_tx_count: self.window_tx_count, + window_interval: self.window_interval, + tx_rate: self.tx_rate, + }) + } +} + +/// Result of JSON-RPC method `getdifficulty`. +/// +/// > getdifficulty +/// +/// > Returns the proof-of-work difficulty as a multiple of the minimum difficulty. +/// > +/// > Result: +/// > n.nnn (numeric) the proof-of-work difficulty as a multiple of the minimum difficulty. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetDifficulty(pub f64); + +impl GetDifficulty { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> model::GetDifficulty { model::GetDifficulty(self.0) } +} + +/// Result of JSON-RPC method `getmempoolancestors` with verbose set to false. +/// +/// > getmempoolancestors txid (verbose) +/// > +/// > If txid is in the mempool, returns all in-mempool ancestors. +/// > +/// > Arguments: +/// > 1. "txid" (string, required) The transaction id (must be in mempool) +/// > 2. verbose (boolean, optional, default=false) True for a json object, false for array of transaction ids +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetMempoolAncestors(pub Vec); + +impl GetMempoolAncestors { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> Result { + let v = self + .0 + .iter() + .map(|t| encode::deserialize_hex::(t)) + .collect::, _>>()?; + Ok(model::GetMempoolAncestors(v)) + } +} + +/// Result of JSON-RPC method `getmempoolancestors` with verbose set to true +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetMempoolAncestorsVerbose {} + +impl GetMempoolAncestorsVerbose { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> model::GetMempoolAncestorsVerbose { + model::GetMempoolAncestorsVerbose {} + } +} + /// Result of JSON-RPC method `gettxout`. /// /// > gettxout "txid" n ( include_mempool ) diff --git a/json/src/v17/mod.rs b/json/src/v17/mod.rs index 0f600cf..6da5b66 100644 --- a/json/src/v17/mod.rs +++ b/json/src/v17/mod.rs @@ -11,13 +11,13 @@ //! - [x] `getbestblockhash` //! - [x] `getblock "blockhash" ( verbosity ) ` //! - [x] `getblockchaininfo` -//! - [ ] `getblockcount` -//! - [ ] `getblockhash height` -//! - [ ] `getblockheader "hash" ( verbose )` -//! - [ ] `getblockstats hash_or_height ( stats )` -//! - [ ] `getchaintips` -//! - [ ] `getchaintxstats ( nblocks blockhash )` -//! - [ ] `getdifficulty` +//! - [x] `getblockcount` +//! - [x] `getblockhash height` +//! - [x] `getblockheader "hash" ( verbose )` +//! - [x] `getblockstats hash_or_height ( stats )` +//! - [x] `getchaintips` +//! - [x] `getchaintxstats ( nblocks blockhash )` +//! - [x] `getdifficulty` //! - [ ] `getmempoolancestors txid (verbose)` //! - [ ] `getmempooldescendants txid (verbose)` //! - [ ] `getmempoolentry txid` @@ -166,8 +166,11 @@ mod zmq; #[doc(inline)] pub use self::{ blockchain::{ - Bip9Softfork, Bip9SoftforkStatus, GetBestBlockHash, GetBlockVerbosityOne, - GetBlockVerbosityZero, GetBlockchainInfo, GetTxOut, ScriptPubkey, Softfork, SoftforkReject, + Bip9Softfork, Bip9SoftforkStatus, ChainTips, ChainTipsStatus, GetBestBlockHash, + GetBlockCount, GetBlockHash, GetBlockHeader, GetBlockHeaderVerbose, GetBlockStats, + GetBlockVerbosityOne, GetBlockVerbosityZero, GetBlockchainInfo, GetChainTips, + GetChainTxStats, GetDifficulty, GetMempoolAncestors, GetMempoolAncestorsVerbose, GetTxOut, + ScriptPubkey, Softfork, SoftforkReject, }, generating::GenerateToAddress, network::{GetNetworkInfo, GetNetworkInfoAddress, GetNetworkInfoNetwork},