Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ default-features = false
features = ["hex", "macros"]

[dev-dependencies]
futures = "0.3.31"
tokio = { version = "1.35.1", default-features = false, features = [
"rt-multi-thread",
] }
serde_path_to_error = "0.1.16"

[features]
Expand Down
205 changes: 205 additions & 0 deletions examples/batch_requests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//! # Concurrent Requests Example
//!
//! This example demonstrates how to make multiple batched JSON-RPC requests
//! to efficiently fetch data from Bitcoin Core.

use std::{env, time::Duration};

use futures::TryFutureExt;
use jsonrpsee::core::{
client::{BatchResponse, ClientT as _},
params::BatchRequestBuilder,
};

use bitcoin_jsonrpsee::{client::MainClient, jsonrpsee::http_client::HttpClient};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🚀 Bitcoin JSON-RPC Batched Requests Example");
println!("===============================================\n");

// Get configuration from environment
let rpc_host = env::var("BITCOIN_RPC_HOST").expect("BITCOIN_RPC_HOST must be set");
let rpc_port = env::var("BITCOIN_RPC_PORT").expect("BITCOIN_RPC_PORT must be set");

let user = env::var("BITCOIN_RPC_USER").expect("BITCOIN_RPC_USER must be set");
let password = env::var("BITCOIN_RPC_PASSWORD").expect("BITCOIN_RPC_PASSWORD must be set");

let target = format!("http://{}:{}", rpc_host, rpc_port);
println!("📡 Connecting to Bitcoin RPC at {}", target);

// Create client
let client = bitcoin_jsonrpsee::client(target, None, &password, &user)?;

// Example 1: Concurrent basic blockchain queries
println!("\n📊 Example 1: Basic blockchain queries");
basic_info(&client).await?;

// Example 2: Concurrent block header requests for multiple heights
println!("\n🏗️ Example 2: Batched block header requests");
let start = std::time::Instant::now();
let batched_block_hashes = batched_block_headers(&client).await?;
let batched_time = start.elapsed();

// Example 3: Non-batched block header requests
let start = std::time::Instant::now();
println!("\n⚡ Example 3: Non-batched block header requests");
let non_batched_block_hashes = non_batched_block_headers(&client).await?;
let non_batched_time = start.elapsed();

if batched_block_hashes.len() != non_batched_block_hashes.len() {
return Err("Batched and non-batched block hashes have different lengths".into());
}

for (i, (batched, non_batched)) in batched_block_hashes
.iter()
.zip(non_batched_block_hashes.iter())
.enumerate()
{
if batched != non_batched {
println!(
"⚠️ Header at index {} mismatch: batched: {}, non-batched: {}",
i, batched, non_batched
);
}
}

performance_comparison(batched_time, non_batched_time).await?;

println!("\n🎉 Batched requests example completed!");

Ok(())
}

async fn basic_info(client: &HttpClient) -> Result<(), Box<dyn std::error::Error>> {
println!("Fetching blockchain info, best block hash, and block count...");

// Make three concurrent requests using tokio::join!
let (blockchain_info, best_hash, block_count) = tokio::join!(
client.get_blockchain_info(),
client.getbestblockhash(),
client.getblockcount()
);

// Handle results
match blockchain_info {
Ok(info) => {
println!("✅ Chain: {:?}", info.chain);
println!("✅ Blocks: {}", info.blocks);
println!("✅ Difficulty: {:.2}", info.difficulty);
}
Err(e) => println!("⚠️ Failed to get blockchain info: {}", e),
}

match best_hash {
Ok(hash) => println!("✅ Best hash: {}", hash),
Err(e) => println!("⚠️ Failed to get best hash: {}", e),
}

match block_count {
Ok(count) => println!("✅ Block count: {}", count),
Err(e) => println!("⚠️ Failed to get block count: {}", e),
}

Ok(())
}

const BATCHED_HEADER_COUNT: usize = 30;

async fn batched_block_headers(
client: &HttpClient,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
// First get the current height
let current_height = client.getblockcount().await?;

if current_height < BATCHED_HEADER_COUNT {
println!(
"⚠️ Not enough blocks for this example (need at least {})",
BATCHED_HEADER_COUNT
);
return Ok(Vec::new());
}

println!(
"Fetching headers for the last {} blocks batched...",
BATCHED_HEADER_COUNT
);

let mut req = BatchRequestBuilder::new();

for i in 0..BATCHED_HEADER_COUNT {
req.insert("getblockhash", vec![current_height - i])?;
}
let res: BatchResponse<String> = client.batch_request(req).await?;

// Collect successful hashes
let mut block_hashes = Vec::new();
let mut heights = Vec::new();

for (i, hash_result) in res.iter().enumerate() {
let height = current_height - i;
match hash_result {
Ok(hash) => {
block_hashes.push(hash.clone());
heights.push(height);
}
Err(e) => println!("⚠️ Failed to get hash for block {}: {}", height, e),
}
}

if block_hashes.is_empty() {
return Err("❌ No block hashes could be retrieved".into());
}

for (i, hash) in block_hashes.iter().enumerate() {
let height = current_height - i;
println!("✅ Block #{}: {}", height, hash);
}

Ok(block_hashes)
}

async fn non_batched_block_headers(
client: &HttpClient,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let current_height = client.getblockcount().await?;
let mut futures = Vec::new();
for i in 0..BATCHED_HEADER_COUNT {
futures.push(
client
.getblockhash(current_height - i)
.map_ok(|hash| hash.to_string()),
);
}

let results = futures::future::join_all(futures).await;

let mut block_hashes: Vec<String> = Vec::new();

for res in results.into_iter() {
match res {
Ok(hash) => block_hashes.push(hash),
Err(e) => return Err(e.into()),
}
}

Ok(block_hashes)
}

async fn performance_comparison(
batched_time: Duration,
non_batched_time: Duration,
) -> Result<(), Box<dyn std::error::Error>> {
println!("⏱️ Batched time: {:?}", batched_time);
println!("⏱️ Non-batched time: {:?}", non_batched_time);

if batched_time < non_batched_time {
let speedup = non_batched_time.as_micros() as f64 / batched_time.as_micros() as f64;
println!("🚀 Batched requests were {:.1}x faster!", speedup);
} else {
println!("📊 Results may vary based on network latency and server load");
println!("💡 Concurrent benefits are more apparent with higher latency connections");
}

Ok(())
}
27 changes: 20 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
use std::net::SocketAddr;
//! # Bitcoin JSON-RPC Client
//!
//! A Rust library for interacting with Bitcoin Core via JSON-RPC.
//!
//! ## Usage Example
//!
//! Example that illustrates basic usage of the library as well as how
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be removed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in the entire doc comment, or something more specific? Doing it this way causes the example to appear in Cargo docs, which (IMO) is pretty neat

//! to do batched requests (multiple requests + responses over a single
//! network roundtrip).
//!
//! ```rust,no_run
#![doc = include_str!("../examples/batch_requests.rs")]
//! ```

use base64::Engine as _;
use http::HeaderValue;
Expand All @@ -14,11 +26,11 @@ pub use client::Header;

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("jsonrpsee error ({})", .main_addr)]
#[error("jsonrpsee error ({target})")]
Jsonrpsee {
#[source]
source: jsonrpsee::core::ClientError,
main_addr: SocketAddr,
target: String,
},
#[error("header error")]
InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
Expand All @@ -33,12 +45,13 @@ pub enum Error {
}

/// Use the `builder` argument to manually set client options
pub fn client(
main_addr: SocketAddr,
pub fn client<T: Into<String>>(
target: T,
builder: Option<HttpClientBuilder>,
password: &str,
user: &str,
) -> Result<HttpClient, Error> {
let target = target.into();
let mut headers = HeaderMap::new();
let auth = format!("{user}:{password}");
let header_value = format!(
Expand All @@ -50,8 +63,8 @@ pub fn client(
builder
.unwrap_or_default()
.set_headers(headers)
.build(format!("http://{main_addr}"))
.map_err(|source| Error::Jsonrpsee { source, main_addr })
.build(target.clone())
.map_err(|source| Error::Jsonrpsee { source, target })
}

#[cfg(test)]
Expand Down