diff --git a/client/src/client.rs b/client/src/client.rs index 651acd96..4f20eb0b 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -462,6 +462,18 @@ pub trait RpcApi: Sized { self.call("getblockhash", &[height.into()]) } + fn get_block_stats(&self, height: u64) -> Result { + self.call("getblockstats", &[height.into()]) + } + + fn get_block_stats_fields( + &self, + height: u64, + fields: &[json::BlockStatsFields], + ) -> Result { + self.call("getblockstats", &[height.into(), fields.into()]) + } + fn get_raw_transaction( &self, txid: &bitcoin::Txid, diff --git a/integration_test/run.sh b/integration_test/run.sh index b40aa78a..a442ae47 100755 --- a/integration_test/run.sh +++ b/integration_test/run.sh @@ -33,6 +33,7 @@ bitcoind -regtest $BLOCKFILTERARG $FALLBACKFEEARG \ -connect=127.0.0.1:12348 \ -rpcport=12349 \ -server=1 \ + -txindex=1 \ -printtoconsole=0 & PID2=$! diff --git a/integration_test/src/main.rs b/integration_test/src/main.rs index 0e35ee79..d9229683 100644 --- a/integration_test/src/main.rs +++ b/integration_test/src/main.rs @@ -33,6 +33,7 @@ use bitcoin::{ use bitcoincore_rpc::bitcoincore_rpc_json::{ GetBlockTemplateModes, GetBlockTemplateRules, ScanTxOutRequest, }; +use json::BlockStatsFields as BsFields; lazy_static! { static ref SECP: secp256k1::Secp256k1 = secp256k1::Secp256k1::new(); @@ -147,6 +148,8 @@ fn main() { test_get_block_hash(&cl); test_get_block(&cl); test_get_block_header_get_block_header_info(&cl); + test_get_block_stats(&cl); + test_get_block_stats_fields(&cl); test_get_address_info(&cl); test_set_label(&cl); test_send_to_address(&cl); @@ -310,6 +313,28 @@ fn test_get_block_header_get_block_header_info(cl: &Client) { assert!(info.previous_block_hash.is_some()); } +fn test_get_block_stats(cl: &Client) { + let tip = cl.get_block_count().unwrap(); + let tip_hash = cl.get_best_block_hash().unwrap(); + let header = cl.get_block_header(&tip_hash).unwrap(); + let stats = cl.get_block_stats(tip).unwrap(); + assert_eq!(header.block_hash(), stats.block_hash); + assert_eq!(header.time, stats.time as u32); + assert_eq!(tip, stats.height); +} + +fn test_get_block_stats_fields(cl: &Client) { + let tip = cl.get_block_count().unwrap(); + let tip_hash = cl.get_best_block_hash().unwrap(); + let header = cl.get_block_header(&tip_hash).unwrap(); + let fields = [BsFields::BlockHash, BsFields::Height, BsFields::TotalFee]; + let stats = cl.get_block_stats_fields(tip, &fields).unwrap(); + assert_eq!(header.block_hash(), stats.block_hash.unwrap()); + assert_eq!(tip, stats.height.unwrap()); + assert!(stats.total_fee.is_some()); + assert!(stats.avg_fee.is_none()); +} + fn test_get_address_info(cl: &Client) { let addr = cl.get_new_address(None, Some(json::AddressType::Legacy)).unwrap(); let info = cl.get_address_info(&addr).unwrap(); diff --git a/json/src/lib.rs b/json/src/lib.rs index 9a79d5e8..6495a7f0 100644 --- a/json/src/lib.rs +++ b/json/src/lib.rs @@ -31,6 +31,7 @@ use bitcoin::util::{bip158, bip32}; use bitcoin::{Address, Amount, PrivateKey, PublicKey, Script, SignedAmount, Transaction}; use serde::de::Error as SerdeError; use serde::{Deserialize, Serialize}; +use std::fmt; //TODO(stevenroose) consider using a Time type @@ -225,6 +226,264 @@ pub struct GetBlockHeaderResult { pub next_block_hash: Option, } +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct GetBlockStatsResult { + #[serde(rename = "avgfee", with = "bitcoin::util::amount::serde::as_sat")] + pub avg_fee: Amount, + #[serde(rename = "avgfeerate", with = "bitcoin::util::amount::serde::as_sat")] + pub avg_fee_rate: Amount, + #[serde(rename = "avgtxsize")] + pub avg_tx_size: u32, + #[serde(rename = "blockhash")] + pub block_hash: bitcoin::BlockHash, + #[serde(rename = "feerate_percentiles")] + pub fee_rate_percentiles: FeeRatePercentiles, + pub height: u64, + pub ins: usize, + #[serde(rename = "maxfee", with = "bitcoin::util::amount::serde::as_sat")] + pub max_fee: Amount, + #[serde(rename = "maxfeerate", with = "bitcoin::util::amount::serde::as_sat")] + pub max_fee_rate: Amount, + #[serde(rename = "maxtxsize")] + pub max_tx_size: u32, + #[serde(rename = "medianfee", with = "bitcoin::util::amount::serde::as_sat")] + pub median_fee: Amount, + #[serde(rename = "mediantime")] + pub median_time: u64, + #[serde(rename = "mediantxsize")] + pub median_tx_size: u32, + #[serde(rename = "minfee", with = "bitcoin::util::amount::serde::as_sat")] + pub min_fee: Amount, + #[serde(rename = "minfeerate", with = "bitcoin::util::amount::serde::as_sat")] + pub min_fee_rate: Amount, + #[serde(rename = "mintxsize")] + pub min_tx_size: u32, + pub outs: usize, + #[serde(with = "bitcoin::util::amount::serde::as_sat")] + pub subsidy: Amount, + #[serde(rename = "swtotal_size")] + pub sw_total_size: usize, + #[serde(rename = "swtotal_weight")] + pub sw_total_weight: usize, + #[serde(rename = "swtxs")] + pub sw_txs: usize, + pub time: u64, + #[serde(with = "bitcoin::util::amount::serde::as_sat")] + pub total_out: Amount, + pub total_size: usize, + pub total_weight: usize, + #[serde(rename = "totalfee", with = "bitcoin::util::amount::serde::as_sat")] + pub total_fee: Amount, + pub txs: usize, + pub utxo_increase: i32, + pub utxo_size_inc: i32, +} + +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct GetBlockStatsResultPartial { + #[serde( + default, + rename = "avgfee", + with = "bitcoin::util::amount::serde::as_sat::opt", + skip_serializing_if = "Option::is_none" + )] + pub avg_fee: Option, + #[serde( + default, + rename = "avgfeerate", + with = "bitcoin::util::amount::serde::as_sat::opt", + skip_serializing_if = "Option::is_none" + )] + pub avg_fee_rate: Option, + #[serde(default, rename = "avgtxsize", skip_serializing_if = "Option::is_none")] + pub avg_tx_size: Option, + #[serde(default, rename = "blockhash", skip_serializing_if = "Option::is_none")] + pub block_hash: Option, + #[serde(default, rename = "feerate_percentiles", skip_serializing_if = "Option::is_none")] + pub fee_rate_percentiles: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub height: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ins: Option, + #[serde( + default, + rename = "maxfee", + with = "bitcoin::util::amount::serde::as_sat::opt", + skip_serializing_if = "Option::is_none" + )] + pub max_fee: Option, + #[serde( + default, + rename = "maxfeerate", + with = "bitcoin::util::amount::serde::as_sat::opt", + skip_serializing_if = "Option::is_none" + )] + pub max_fee_rate: Option, + #[serde(default, rename = "maxtxsize", skip_serializing_if = "Option::is_none")] + pub max_tx_size: Option, + #[serde( + default, + rename = "medianfee", + with = "bitcoin::util::amount::serde::as_sat::opt", + skip_serializing_if = "Option::is_none" + )] + pub median_fee: Option, + #[serde(default, rename = "mediantime", skip_serializing_if = "Option::is_none")] + pub median_time: Option, + #[serde(default, rename = "mediantxsize", skip_serializing_if = "Option::is_none")] + pub median_tx_size: Option, + #[serde( + default, + rename = "minfee", + with = "bitcoin::util::amount::serde::as_sat::opt", + skip_serializing_if = "Option::is_none" + )] + pub min_fee: Option, + #[serde( + default, + rename = "minfeerate", + with = "bitcoin::util::amount::serde::as_sat::opt", + skip_serializing_if = "Option::is_none" + )] + pub min_fee_rate: Option, + #[serde(default, rename = "mintxsize", skip_serializing_if = "Option::is_none")] + pub min_tx_size: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub outs: Option, + #[serde( + default, + with = "bitcoin::util::amount::serde::as_sat::opt", + skip_serializing_if = "Option::is_none" + )] + pub subsidy: Option, + #[serde(default, rename = "swtotal_size", skip_serializing_if = "Option::is_none")] + pub sw_total_size: Option, + #[serde(default, rename = "swtotal_weight", skip_serializing_if = "Option::is_none")] + pub sw_total_weight: Option, + #[serde(default, rename = "swtxs", skip_serializing_if = "Option::is_none")] + pub sw_txs: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub time: Option, + #[serde( + default, + with = "bitcoin::util::amount::serde::as_sat::opt", + skip_serializing_if = "Option::is_none" + )] + pub total_out: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub total_size: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub total_weight: Option, + #[serde( + default, + rename = "totalfee", + with = "bitcoin::util::amount::serde::as_sat::opt", + skip_serializing_if = "Option::is_none" + )] + pub total_fee: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub txs: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub utxo_increase: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub utxo_size_inc: Option, +} + +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct FeeRatePercentiles { + #[serde(with = "bitcoin::util::amount::serde::as_sat")] + pub fr_10th: Amount, + #[serde(with = "bitcoin::util::amount::serde::as_sat")] + pub fr_25th: Amount, + #[serde(with = "bitcoin::util::amount::serde::as_sat")] + pub fr_50th: Amount, + #[serde(with = "bitcoin::util::amount::serde::as_sat")] + pub fr_75th: Amount, + #[serde(with = "bitcoin::util::amount::serde::as_sat")] + pub fr_90th: Amount, +} + +#[derive(Clone)] +pub enum BlockStatsFields { + AverageFee, + AverageFeeRate, + AverageTxSize, + BlockHash, + FeeRatePercentiles, + Height, + Ins, + MaxFee, + MaxFeeRate, + MaxTxSize, + MedianFee, + MedianTime, + MedianTxSize, + MinFee, + MinFeeRate, + MinTxSize, + Outs, + Subsidy, + SegWitTotalSize, + SegWitTotalWeight, + SegWitTxs, + Time, + TotalOut, + TotalSize, + TotalWeight, + TotalFee, + Txs, + UtxoIncrease, + UtxoSizeIncrease, +} + +impl BlockStatsFields { + fn get_rpc_keyword(&self) -> &str { + match *self { + BlockStatsFields::AverageFee => "avgfee", + BlockStatsFields::AverageFeeRate => "avgfeerate", + BlockStatsFields::AverageTxSize => "avgtxsize", + BlockStatsFields::BlockHash => "blockhash", + BlockStatsFields::FeeRatePercentiles => "feerate_percentiles", + BlockStatsFields::Height => "height", + BlockStatsFields::Ins => "ins", + BlockStatsFields::MaxFee => "maxfee", + BlockStatsFields::MaxFeeRate => "maxfeerate", + BlockStatsFields::MaxTxSize => "maxtxsize", + BlockStatsFields::MedianFee => "medianfee", + BlockStatsFields::MedianTime => "mediantime", + BlockStatsFields::MedianTxSize => "mediantxsize", + BlockStatsFields::MinFee => "minfee", + BlockStatsFields::MinFeeRate => "minfeerate", + BlockStatsFields::MinTxSize => "minfeerate", + BlockStatsFields::Outs => "outs", + BlockStatsFields::Subsidy => "subsidy", + BlockStatsFields::SegWitTotalSize => "swtotal_size", + BlockStatsFields::SegWitTotalWeight => "swtotal_weight", + BlockStatsFields::SegWitTxs => "swtxs", + BlockStatsFields::Time => "time", + BlockStatsFields::TotalOut => "total_out", + BlockStatsFields::TotalSize => "total_size", + BlockStatsFields::TotalWeight => "total_weight", + BlockStatsFields::TotalFee => "totalfee", + BlockStatsFields::Txs => "txs", + BlockStatsFields::UtxoIncrease => "utxo_increase", + BlockStatsFields::UtxoSizeIncrease => "utxo_size_inc", + } + } +} + +impl fmt::Display for BlockStatsFields { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.get_rpc_keyword()) + } +} + +impl From for serde_json::Value { + fn from(bsf: BlockStatsFields) -> Self { + Self::from(bsf.to_string()) + } +} + #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct GetMiningInfoResult { @@ -912,7 +1171,6 @@ impl<'de> serde::Deserialize<'de> for ImportMultiRescanSince { D: serde::Deserializer<'de>, { use serde::de; - use std::fmt; struct Visitor; impl<'de> de::Visitor<'de> for Visitor { type Value = ImportMultiRescanSince;