Skip to content

Commit 9bbca7f

Browse files
committed
Merge #157: Add RPC call 'getblockstats'
60b0dd7 Implement Display and serde_json::Value for BlockStatsFields (Gabriel Comte) 826e01a Fix code formatting (Gabriel Comte) f2d74ca Change data types, fix absent values (Gabriel Comte) c807b14 Add per-field querying for getblockstats (Gabriel Comte) 1740440 getblockstats requires txindex (Gabriel Comte) 64b7a5e Add rpc call getblockstats (Gabriel Comte) Pull request description: `getblockstats` is available in Bitcoin Core since version `0.17.0` . > It won't work for some heights with pruning. > It won't work without -txindex for utxo_size_inc, *fee or *feerate stats. More info (here)[https://bitcoincore.org/en/doc/0.17.0/rpc/blockchain/getblockstats/] I'm completely new to Rust and this is my first Rust-PR ever, so please have mercy. ACKs for top commit: RCasatta: utACK 60b0dd7 Tree-SHA512: c3ca291e8d30b3796a510b6fed6fe8810a00c9e448e24ed11bf992c5ae992e1ba27b5ee99a39f265dae47a95bd9ea59a5105110e25c3c643c5c9f7bafee2c6cd
2 parents d9a1dd0 + 60b0dd7 commit 9bbca7f

File tree

4 files changed

+297
-1
lines changed

4 files changed

+297
-1
lines changed

client/src/client.rs

+12
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,18 @@ pub trait RpcApi: Sized {
462462
self.call("getblockhash", &[height.into()])
463463
}
464464

465+
fn get_block_stats(&self, height: u64) -> Result<json::GetBlockStatsResult> {
466+
self.call("getblockstats", &[height.into()])
467+
}
468+
469+
fn get_block_stats_fields(
470+
&self,
471+
height: u64,
472+
fields: &[json::BlockStatsFields],
473+
) -> Result<json::GetBlockStatsResultPartial> {
474+
self.call("getblockstats", &[height.into(), fields.into()])
475+
}
476+
465477
fn get_raw_transaction(
466478
&self,
467479
txid: &bitcoin::Txid,

integration_test/run.sh

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ bitcoind -regtest $BLOCKFILTERARG $FALLBACKFEEARG \
3333
-connect=127.0.0.1:12348 \
3434
-rpcport=12349 \
3535
-server=1 \
36+
-txindex=1 \
3637
-printtoconsole=0 &
3738
PID2=$!
3839

integration_test/src/main.rs

+25
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use bitcoin::{
3333
use bitcoincore_rpc::bitcoincore_rpc_json::{
3434
GetBlockTemplateModes, GetBlockTemplateRules, ScanTxOutRequest,
3535
};
36+
use json::BlockStatsFields as BsFields;
3637

3738
lazy_static! {
3839
static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
@@ -147,6 +148,8 @@ fn main() {
147148
test_get_block_hash(&cl);
148149
test_get_block(&cl);
149150
test_get_block_header_get_block_header_info(&cl);
151+
test_get_block_stats(&cl);
152+
test_get_block_stats_fields(&cl);
150153
test_get_address_info(&cl);
151154
test_set_label(&cl);
152155
test_send_to_address(&cl);
@@ -316,6 +319,28 @@ fn test_get_block_header_get_block_header_info(cl: &Client) {
316319
assert!(info.previous_block_hash.is_some());
317320
}
318321

322+
fn test_get_block_stats(cl: &Client) {
323+
let tip = cl.get_block_count().unwrap();
324+
let tip_hash = cl.get_best_block_hash().unwrap();
325+
let header = cl.get_block_header(&tip_hash).unwrap();
326+
let stats = cl.get_block_stats(tip).unwrap();
327+
assert_eq!(header.block_hash(), stats.block_hash);
328+
assert_eq!(header.time, stats.time as u32);
329+
assert_eq!(tip, stats.height);
330+
}
331+
332+
fn test_get_block_stats_fields(cl: &Client) {
333+
let tip = cl.get_block_count().unwrap();
334+
let tip_hash = cl.get_best_block_hash().unwrap();
335+
let header = cl.get_block_header(&tip_hash).unwrap();
336+
let fields = [BsFields::BlockHash, BsFields::Height, BsFields::TotalFee];
337+
let stats = cl.get_block_stats_fields(tip, &fields).unwrap();
338+
assert_eq!(header.block_hash(), stats.block_hash.unwrap());
339+
assert_eq!(tip, stats.height.unwrap());
340+
assert!(stats.total_fee.is_some());
341+
assert!(stats.avg_fee.is_none());
342+
}
343+
319344
fn test_get_address_info(cl: &Client) {
320345
let addr = cl.get_new_address(None, Some(json::AddressType::Legacy)).unwrap();
321346
let info = cl.get_address_info(&addr).unwrap();

json/src/lib.rs

+259-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use bitcoin::util::{bip158, bip32};
3131
use bitcoin::{Address, Amount, PrivateKey, PublicKey, Script, SignedAmount, Transaction};
3232
use serde::de::Error as SerdeError;
3333
use serde::{Deserialize, Serialize};
34+
use std::fmt;
3435

3536
//TODO(stevenroose) consider using a Time type
3637

@@ -225,6 +226,264 @@ pub struct GetBlockHeaderResult {
225226
pub next_block_hash: Option<bitcoin::BlockHash>,
226227
}
227228

229+
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
230+
pub struct GetBlockStatsResult {
231+
#[serde(rename = "avgfee", with = "bitcoin::util::amount::serde::as_sat")]
232+
pub avg_fee: Amount,
233+
#[serde(rename = "avgfeerate", with = "bitcoin::util::amount::serde::as_sat")]
234+
pub avg_fee_rate: Amount,
235+
#[serde(rename = "avgtxsize")]
236+
pub avg_tx_size: u32,
237+
#[serde(rename = "blockhash")]
238+
pub block_hash: bitcoin::BlockHash,
239+
#[serde(rename = "feerate_percentiles")]
240+
pub fee_rate_percentiles: FeeRatePercentiles,
241+
pub height: u64,
242+
pub ins: usize,
243+
#[serde(rename = "maxfee", with = "bitcoin::util::amount::serde::as_sat")]
244+
pub max_fee: Amount,
245+
#[serde(rename = "maxfeerate", with = "bitcoin::util::amount::serde::as_sat")]
246+
pub max_fee_rate: Amount,
247+
#[serde(rename = "maxtxsize")]
248+
pub max_tx_size: u32,
249+
#[serde(rename = "medianfee", with = "bitcoin::util::amount::serde::as_sat")]
250+
pub median_fee: Amount,
251+
#[serde(rename = "mediantime")]
252+
pub median_time: u64,
253+
#[serde(rename = "mediantxsize")]
254+
pub median_tx_size: u32,
255+
#[serde(rename = "minfee", with = "bitcoin::util::amount::serde::as_sat")]
256+
pub min_fee: Amount,
257+
#[serde(rename = "minfeerate", with = "bitcoin::util::amount::serde::as_sat")]
258+
pub min_fee_rate: Amount,
259+
#[serde(rename = "mintxsize")]
260+
pub min_tx_size: u32,
261+
pub outs: usize,
262+
#[serde(with = "bitcoin::util::amount::serde::as_sat")]
263+
pub subsidy: Amount,
264+
#[serde(rename = "swtotal_size")]
265+
pub sw_total_size: usize,
266+
#[serde(rename = "swtotal_weight")]
267+
pub sw_total_weight: usize,
268+
#[serde(rename = "swtxs")]
269+
pub sw_txs: usize,
270+
pub time: u64,
271+
#[serde(with = "bitcoin::util::amount::serde::as_sat")]
272+
pub total_out: Amount,
273+
pub total_size: usize,
274+
pub total_weight: usize,
275+
#[serde(rename = "totalfee", with = "bitcoin::util::amount::serde::as_sat")]
276+
pub total_fee: Amount,
277+
pub txs: usize,
278+
pub utxo_increase: i32,
279+
pub utxo_size_inc: i32,
280+
}
281+
282+
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
283+
pub struct GetBlockStatsResultPartial {
284+
#[serde(
285+
default,
286+
rename = "avgfee",
287+
with = "bitcoin::util::amount::serde::as_sat::opt",
288+
skip_serializing_if = "Option::is_none"
289+
)]
290+
pub avg_fee: Option<Amount>,
291+
#[serde(
292+
default,
293+
rename = "avgfeerate",
294+
with = "bitcoin::util::amount::serde::as_sat::opt",
295+
skip_serializing_if = "Option::is_none"
296+
)]
297+
pub avg_fee_rate: Option<Amount>,
298+
#[serde(default, rename = "avgtxsize", skip_serializing_if = "Option::is_none")]
299+
pub avg_tx_size: Option<u32>,
300+
#[serde(default, rename = "blockhash", skip_serializing_if = "Option::is_none")]
301+
pub block_hash: Option<bitcoin::BlockHash>,
302+
#[serde(default, rename = "feerate_percentiles", skip_serializing_if = "Option::is_none")]
303+
pub fee_rate_percentiles: Option<FeeRatePercentiles>,
304+
#[serde(default, skip_serializing_if = "Option::is_none")]
305+
pub height: Option<u64>,
306+
#[serde(default, skip_serializing_if = "Option::is_none")]
307+
pub ins: Option<usize>,
308+
#[serde(
309+
default,
310+
rename = "maxfee",
311+
with = "bitcoin::util::amount::serde::as_sat::opt",
312+
skip_serializing_if = "Option::is_none"
313+
)]
314+
pub max_fee: Option<Amount>,
315+
#[serde(
316+
default,
317+
rename = "maxfeerate",
318+
with = "bitcoin::util::amount::serde::as_sat::opt",
319+
skip_serializing_if = "Option::is_none"
320+
)]
321+
pub max_fee_rate: Option<Amount>,
322+
#[serde(default, rename = "maxtxsize", skip_serializing_if = "Option::is_none")]
323+
pub max_tx_size: Option<u32>,
324+
#[serde(
325+
default,
326+
rename = "medianfee",
327+
with = "bitcoin::util::amount::serde::as_sat::opt",
328+
skip_serializing_if = "Option::is_none"
329+
)]
330+
pub median_fee: Option<Amount>,
331+
#[serde(default, rename = "mediantime", skip_serializing_if = "Option::is_none")]
332+
pub median_time: Option<u64>,
333+
#[serde(default, rename = "mediantxsize", skip_serializing_if = "Option::is_none")]
334+
pub median_tx_size: Option<u32>,
335+
#[serde(
336+
default,
337+
rename = "minfee",
338+
with = "bitcoin::util::amount::serde::as_sat::opt",
339+
skip_serializing_if = "Option::is_none"
340+
)]
341+
pub min_fee: Option<Amount>,
342+
#[serde(
343+
default,
344+
rename = "minfeerate",
345+
with = "bitcoin::util::amount::serde::as_sat::opt",
346+
skip_serializing_if = "Option::is_none"
347+
)]
348+
pub min_fee_rate: Option<Amount>,
349+
#[serde(default, rename = "mintxsize", skip_serializing_if = "Option::is_none")]
350+
pub min_tx_size: Option<u32>,
351+
#[serde(default, skip_serializing_if = "Option::is_none")]
352+
pub outs: Option<usize>,
353+
#[serde(
354+
default,
355+
with = "bitcoin::util::amount::serde::as_sat::opt",
356+
skip_serializing_if = "Option::is_none"
357+
)]
358+
pub subsidy: Option<Amount>,
359+
#[serde(default, rename = "swtotal_size", skip_serializing_if = "Option::is_none")]
360+
pub sw_total_size: Option<usize>,
361+
#[serde(default, rename = "swtotal_weight", skip_serializing_if = "Option::is_none")]
362+
pub sw_total_weight: Option<usize>,
363+
#[serde(default, rename = "swtxs", skip_serializing_if = "Option::is_none")]
364+
pub sw_txs: Option<usize>,
365+
#[serde(default, skip_serializing_if = "Option::is_none")]
366+
pub time: Option<u64>,
367+
#[serde(
368+
default,
369+
with = "bitcoin::util::amount::serde::as_sat::opt",
370+
skip_serializing_if = "Option::is_none"
371+
)]
372+
pub total_out: Option<Amount>,
373+
#[serde(default, skip_serializing_if = "Option::is_none")]
374+
pub total_size: Option<usize>,
375+
#[serde(default, skip_serializing_if = "Option::is_none")]
376+
pub total_weight: Option<usize>,
377+
#[serde(
378+
default,
379+
rename = "totalfee",
380+
with = "bitcoin::util::amount::serde::as_sat::opt",
381+
skip_serializing_if = "Option::is_none"
382+
)]
383+
pub total_fee: Option<Amount>,
384+
#[serde(default, skip_serializing_if = "Option::is_none")]
385+
pub txs: Option<usize>,
386+
#[serde(default, skip_serializing_if = "Option::is_none")]
387+
pub utxo_increase: Option<i32>,
388+
#[serde(default, skip_serializing_if = "Option::is_none")]
389+
pub utxo_size_inc: Option<i32>,
390+
}
391+
392+
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
393+
pub struct FeeRatePercentiles {
394+
#[serde(with = "bitcoin::util::amount::serde::as_sat")]
395+
pub fr_10th: Amount,
396+
#[serde(with = "bitcoin::util::amount::serde::as_sat")]
397+
pub fr_25th: Amount,
398+
#[serde(with = "bitcoin::util::amount::serde::as_sat")]
399+
pub fr_50th: Amount,
400+
#[serde(with = "bitcoin::util::amount::serde::as_sat")]
401+
pub fr_75th: Amount,
402+
#[serde(with = "bitcoin::util::amount::serde::as_sat")]
403+
pub fr_90th: Amount,
404+
}
405+
406+
#[derive(Clone)]
407+
pub enum BlockStatsFields {
408+
AverageFee,
409+
AverageFeeRate,
410+
AverageTxSize,
411+
BlockHash,
412+
FeeRatePercentiles,
413+
Height,
414+
Ins,
415+
MaxFee,
416+
MaxFeeRate,
417+
MaxTxSize,
418+
MedianFee,
419+
MedianTime,
420+
MedianTxSize,
421+
MinFee,
422+
MinFeeRate,
423+
MinTxSize,
424+
Outs,
425+
Subsidy,
426+
SegWitTotalSize,
427+
SegWitTotalWeight,
428+
SegWitTxs,
429+
Time,
430+
TotalOut,
431+
TotalSize,
432+
TotalWeight,
433+
TotalFee,
434+
Txs,
435+
UtxoIncrease,
436+
UtxoSizeIncrease,
437+
}
438+
439+
impl BlockStatsFields {
440+
fn get_rpc_keyword(&self) -> &str {
441+
match *self {
442+
BlockStatsFields::AverageFee => "avgfee",
443+
BlockStatsFields::AverageFeeRate => "avgfeerate",
444+
BlockStatsFields::AverageTxSize => "avgtxsize",
445+
BlockStatsFields::BlockHash => "blockhash",
446+
BlockStatsFields::FeeRatePercentiles => "feerate_percentiles",
447+
BlockStatsFields::Height => "height",
448+
BlockStatsFields::Ins => "ins",
449+
BlockStatsFields::MaxFee => "maxfee",
450+
BlockStatsFields::MaxFeeRate => "maxfeerate",
451+
BlockStatsFields::MaxTxSize => "maxtxsize",
452+
BlockStatsFields::MedianFee => "medianfee",
453+
BlockStatsFields::MedianTime => "mediantime",
454+
BlockStatsFields::MedianTxSize => "mediantxsize",
455+
BlockStatsFields::MinFee => "minfee",
456+
BlockStatsFields::MinFeeRate => "minfeerate",
457+
BlockStatsFields::MinTxSize => "minfeerate",
458+
BlockStatsFields::Outs => "outs",
459+
BlockStatsFields::Subsidy => "subsidy",
460+
BlockStatsFields::SegWitTotalSize => "swtotal_size",
461+
BlockStatsFields::SegWitTotalWeight => "swtotal_weight",
462+
BlockStatsFields::SegWitTxs => "swtxs",
463+
BlockStatsFields::Time => "time",
464+
BlockStatsFields::TotalOut => "total_out",
465+
BlockStatsFields::TotalSize => "total_size",
466+
BlockStatsFields::TotalWeight => "total_weight",
467+
BlockStatsFields::TotalFee => "totalfee",
468+
BlockStatsFields::Txs => "txs",
469+
BlockStatsFields::UtxoIncrease => "utxo_increase",
470+
BlockStatsFields::UtxoSizeIncrease => "utxo_size_inc",
471+
}
472+
}
473+
}
474+
475+
impl fmt::Display for BlockStatsFields {
476+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
477+
write!(f, "{}", self.get_rpc_keyword())
478+
}
479+
}
480+
481+
impl From<BlockStatsFields> for serde_json::Value {
482+
fn from(bsf: BlockStatsFields) -> Self {
483+
Self::from(bsf.to_string())
484+
}
485+
}
486+
228487
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
229488
#[serde(rename_all = "camelCase")]
230489
pub struct GetMiningInfoResult {
@@ -912,7 +1171,6 @@ impl<'de> serde::Deserialize<'de> for ImportMultiRescanSince {
9121171
D: serde::Deserializer<'de>,
9131172
{
9141173
use serde::de;
915-
use std::fmt;
9161174
struct Visitor;
9171175
impl<'de> de::Visitor<'de> for Visitor {
9181176
type Value = ImportMultiRescanSince;

0 commit comments

Comments
 (0)