Skip to content

Commit e1232c8

Browse files
committed
Use trait methods for RPCs
Use trait methods instead of inherent methods to define the RPCs to better allow downstream clients that define their own RPCs. This helps when different implementations of the ones defined in the client are required. Move the error module to the crate root. Create a new `IntoModelError` error type that carries the RPC method name as context and the original conversion error as a boxed `source`. Use this error type in both sync and async clients. Assisted-by: GPT-5.4
1 parent ccc3f0c commit e1232c8

4 files changed

Lines changed: 109 additions & 77 deletions

File tree

client/src/client_async/error.rs

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,6 @@
33
use std::{error, fmt, io};
44

55
use bitcoin::hex;
6-
use types::v17::{
7-
GetBlockHeaderError, GetBlockHeaderVerboseError, GetBlockVerboseOneError,
8-
GetRawTransactionVerboseError,
9-
};
10-
use types::v19::GetBlockFilterError;
11-
use types::v29::{
12-
GetBlockHeaderVerboseError as GetBlockHeaderVerboseErrorV29,
13-
GetBlockVerboseOneError as GetBlockVerboseOneErrorV29,
14-
};
156

167
/// The error type for errors produced in this library.
178
#[derive(Debug)]
@@ -27,6 +18,8 @@ pub enum Error {
2718
UnexpectedStructure,
2819
/// The daemon returned an error string.
2920
Returned(String),
21+
/// A model conversion error.
22+
Model(IntoModelError),
3023
/// The server version did not match what was expected.
3124
ServerVersion(UnexpectedServerVersionError),
3225
/// Missing user/password.
@@ -57,34 +50,6 @@ impl From<io::Error> for Error {
5750
fn from(e: io::Error) -> Error { Error::Io(e) }
5851
}
5952

60-
impl From<GetBlockHeaderError> for Error {
61-
fn from(e: GetBlockHeaderError) -> Self { Self::Returned(e.to_string()) }
62-
}
63-
64-
impl From<GetBlockHeaderVerboseError> for Error {
65-
fn from(e: GetBlockHeaderVerboseError) -> Self { Self::Returned(e.to_string()) }
66-
}
67-
68-
impl From<GetBlockVerboseOneError> for Error {
69-
fn from(e: GetBlockVerboseOneError) -> Self { Self::Returned(e.to_string()) }
70-
}
71-
72-
impl From<GetRawTransactionVerboseError> for Error {
73-
fn from(e: GetRawTransactionVerboseError) -> Self { Self::Returned(e.to_string()) }
74-
}
75-
76-
impl From<GetBlockHeaderVerboseErrorV29> for Error {
77-
fn from(e: GetBlockHeaderVerboseErrorV29) -> Self { Self::Returned(e.to_string()) }
78-
}
79-
80-
impl From<GetBlockVerboseOneErrorV29> for Error {
81-
fn from(e: GetBlockVerboseOneErrorV29) -> Self { Self::Returned(e.to_string()) }
82-
}
83-
84-
impl From<GetBlockFilterError> for Error {
85-
fn from(e: GetBlockFilterError) -> Self { Self::Returned(e.to_string()) }
86-
}
87-
8853
impl fmt::Display for Error {
8954
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
9055
use Error::*;
@@ -99,6 +64,7 @@ impl fmt::Display for Error {
9964
InvalidCookieFile => write!(f, "invalid cookie file"),
10065
UnexpectedStructure => write!(f, "the JSON result had an unexpected structure"),
10166
Returned(ref s) => write!(f, "the daemon returned an error string: {}", s),
67+
Model(ref e) => write!(f, "model conversion error: {e}"),
10268
ServerVersion(ref e) => write!(f, "server version: {}", e),
10369
MissingUserPassword => write!(f, "missing user and/or password"),
10470
}
@@ -117,6 +83,7 @@ impl error::Error for Error {
11783
BitcoinSerialization(ref e) => Some(e),
11884
Io(ref e) => Some(e),
11985
ServerVersion(ref e) => Some(e),
86+
Model(ref e) => Some(e),
12087
InvalidCookieFile | UnexpectedStructure | Returned(_) | MissingUserPassword => None,
12188
}
12289
}
@@ -147,3 +114,37 @@ impl error::Error for UnexpectedServerVersionError {}
147114
impl From<UnexpectedServerVersionError> for Error {
148115
fn from(e: UnexpectedServerVersionError) -> Self { Self::ServerVersion(e) }
149116
}
117+
118+
/// Error returned when converting an RPC response into a model type fails.
119+
#[derive(Debug)]
120+
pub struct IntoModelError {
121+
context: &'static str,
122+
source: Box<dyn error::Error + Send + Sync + 'static>,
123+
}
124+
125+
impl IntoModelError {
126+
/// Creates a new model conversion error with caller-provided context.
127+
pub fn new<E>(context: &'static str, source: E) -> Self
128+
where
129+
E: error::Error + Send + Sync + 'static,
130+
{
131+
Self { context, source: Box::new(source) }
132+
}
133+
134+
/// Returns the context for the failed conversion.
135+
pub fn context(&self) -> &'static str { self.context }
136+
}
137+
138+
impl fmt::Display for IntoModelError {
139+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
140+
write!(f, "conversion of {} into a model type failed", self.context)
141+
}
142+
}
143+
144+
impl error::Error for IntoModelError {
145+
fn source(&self) -> Option<&(dyn error::Error + 'static)> { Some(&*self.source) }
146+
}
147+
148+
impl From<IntoModelError> for Error {
149+
fn from(e: IntoModelError) -> Self { Self::Model(e) }
150+
}

client/src/client_async/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ use std::fs::File;
1010
use std::io::{BufRead, BufReader};
1111
use std::path::PathBuf;
1212

13-
pub use crate::client_async::error::Error;
13+
pub use error::{Error, IntoModelError, UnexpectedServerVersionError};
14+
pub use rpcs::BitcoinRpcs;
15+
1416
pub(crate) use crate::{into_json, log_response};
1517

1618
/// Crate-specific Result type.

client/src/client_async/rpcs.rs

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,129 @@
11
// SPDX-License-Identifier: CC0-1.0
22

3-
//! RPC set used by BDK.
3+
//! RPC methods for the async Bitcoin Core client.
44
//! All functions return the version nonspecific, strongly typed types.
55
66
use bitcoin::{block, Block, BlockHash, Transaction, Txid};
77
use serde_json::value::RawValue;
88

9-
use crate::client_async::{into_json, Client, Result};
9+
use super::{into_json, Client, IntoModelError, Result};
1010
use crate::types::model::{GetBlockFilter, GetBlockHeaderVerbose, GetBlockVerboseOne};
1111

12-
impl Client {
12+
/// Bitcoin Core RPC methods (v25 to v30).
13+
///
14+
/// This trait exposes the Bitcoin Core RPC methods available on [`Client`]. Downstream users
15+
/// can define their own extension traits with additional methods without risk of
16+
/// name collision with the methods defined here.
17+
// `async fn` in traits produces non-`Send` futures by default; we suppress this lint because
18+
// `BitcoinRpcs` is intended only for use with concrete types (never `dyn BitcoinRpcs`).
19+
#[allow(async_fn_in_trait)]
20+
pub trait BitcoinRpcs {
1321
/// Gets a block by blockhash.
14-
pub async fn get_block(&self, hash: &BlockHash) -> Result<Block> {
22+
async fn get_block(&self, hash: &BlockHash) -> Result<Block>;
23+
24+
/// Gets the block count.
25+
async fn get_block_count(&self) -> Result<u64>;
26+
27+
/// Gets the block hash for a height.
28+
async fn get_block_hash(&self, height: u32) -> Result<BlockHash>;
29+
30+
/// Gets the hash of the chain tip.
31+
async fn get_best_block_hash(&self) -> Result<BlockHash>;
32+
33+
/// Gets the block header by blockhash.
34+
async fn get_block_header(&self, hash: &BlockHash) -> Result<block::Header>;
35+
36+
/// Gets the block header with verbose output.
37+
async fn get_block_header_verbose(&self, hash: &BlockHash) -> Result<GetBlockHeaderVerbose>;
38+
39+
/// Gets a block by blockhash with verbose set to 1.
40+
async fn get_block_verbose(&self, hash: &BlockHash) -> Result<GetBlockVerboseOne>;
41+
42+
/// Gets the block filter for a blockhash.
43+
async fn get_block_filter(&self, hash: &BlockHash) -> Result<GetBlockFilter>;
44+
45+
/// Gets the transaction IDs currently in the mempool.
46+
async fn get_raw_mempool(&self) -> Result<Vec<Txid>>;
47+
48+
/// Gets the raw transaction by txid.
49+
async fn get_raw_transaction(&self, txid: &Txid) -> Result<Transaction>;
50+
51+
/// Returns the version integer reported by the server (e.g. `250200` for v25.2.0).
52+
async fn server_version(&self) -> Result<usize>;
53+
}
54+
55+
impl BitcoinRpcs for Client {
56+
async fn get_block(&self, hash: &BlockHash) -> Result<Block> {
1557
let json: crate::types::v25::GetBlockVerboseZero =
1658
self.call("getblock", &[into_json(hash)?, into_json(0)?]).await?;
17-
Ok(json.into_model()?.0)
59+
Ok(json.into_model().map_err(|e| IntoModelError::new("`getblock`", e))?.0)
1860
}
1961

20-
/// Gets the block count.
21-
pub async fn get_block_count(&self) -> Result<u64> {
62+
async fn get_block_count(&self) -> Result<u64> {
2263
let json: crate::types::v25::GetBlockCount = self.call("getblockcount", &[]).await?;
2364
Ok(json.into_model().0)
2465
}
2566

26-
/// Gets the block hash for a height.
27-
pub async fn get_block_hash(&self, height: u32) -> Result<BlockHash> {
67+
async fn get_block_hash(&self, height: u32) -> Result<BlockHash> {
2868
let json: crate::types::v25::GetBlockHash =
2969
self.call("getblockhash", &[into_json(height)?]).await?;
30-
Ok(json.into_model()?.0)
70+
Ok(json.into_model().map_err(|e| IntoModelError::new("`getblockhash`", e))?.0)
3171
}
3272

33-
/// Gets the hash of the chain tip.
34-
pub async fn get_best_block_hash(&self) -> Result<BlockHash> {
73+
async fn get_best_block_hash(&self) -> Result<BlockHash> {
3574
let json: crate::types::v25::GetBestBlockHash = self.call("getbestblockhash", &[]).await?;
36-
Ok(json.into_model()?.0)
75+
Ok(json.into_model().map_err(|e| IntoModelError::new("`getbestblockhash`", e))?.0)
3776
}
3877

39-
/// Gets the block header by blockhash.
40-
pub async fn get_block_header(&self, hash: &BlockHash) -> Result<block::Header> {
78+
async fn get_block_header(&self, hash: &BlockHash) -> Result<block::Header> {
4179
let json: crate::types::v25::GetBlockHeader =
4280
self.call("getblockheader", &[into_json(hash)?, into_json(false)?]).await?;
43-
Ok(json.into_model()?.0)
81+
Ok(json.into_model().map_err(|e| IntoModelError::new("`getblockheader`", e))?.0)
4482
}
4583

46-
/// Gets the block header with verbose output.
47-
pub async fn get_block_header_verbose(
48-
&self,
49-
hash: &BlockHash,
50-
) -> Result<GetBlockHeaderVerbose> {
84+
async fn get_block_header_verbose(&self, hash: &BlockHash) -> Result<GetBlockHeaderVerbose> {
5185
let raw: Box<RawValue> =
5286
self.call("getblockheader", &[into_json(hash)?, into_json(true)?]).await?;
5387

5488
if let Ok(json) =
5589
serde_json::from_str::<crate::types::v29::GetBlockHeaderVerbose>(raw.get())
5690
{
57-
Ok(json.into_model()?)
91+
Ok(json.into_model().map_err(|e| IntoModelError::new("`getblockheader` verbose", e))?)
5892
} else {
5993
let json: crate::types::v25::GetBlockHeaderVerbose = serde_json::from_str(raw.get())?;
60-
Ok(json.into_model()?)
94+
Ok(json.into_model().map_err(|e| IntoModelError::new("`getblockheader` verbose", e))?)
6195
}
6296
}
6397

64-
/// Gets a block by blockhash with verbose set to 1.
65-
pub async fn get_block_verbose(&self, hash: &BlockHash) -> Result<GetBlockVerboseOne> {
98+
async fn get_block_verbose(&self, hash: &BlockHash) -> Result<GetBlockVerboseOne> {
6699
let raw: Box<RawValue> = self.call("getblock", &[into_json(hash)?, into_json(1)?]).await?;
67100

68101
if let Ok(json) = serde_json::from_str::<crate::types::v29::GetBlockVerboseOne>(raw.get()) {
69-
Ok(json.into_model()?)
102+
Ok(json.into_model().map_err(|e| IntoModelError::new("`getblock` verbose=1", e))?)
70103
} else {
71104
let json: crate::types::v25::GetBlockVerboseOne = serde_json::from_str(raw.get())?;
72-
Ok(json.into_model()?)
105+
Ok(json.into_model().map_err(|e| IntoModelError::new("`getblock` verbose=1", e))?)
73106
}
74107
}
75108

76-
/// Gets the block filter for a blockhash.
77-
pub async fn get_block_filter(&self, hash: &BlockHash) -> Result<GetBlockFilter> {
109+
async fn get_block_filter(&self, hash: &BlockHash) -> Result<GetBlockFilter> {
78110
let json: crate::types::v25::GetBlockFilter =
79111
self.call("getblockfilter", &[into_json(hash)?]).await?;
80-
Ok(json.into_model()?)
112+
Ok(json.into_model().map_err(|e| IntoModelError::new("`getblockfilter`", e))?)
81113
}
82114

83-
/// Gets the transaction IDs currently in the mempool.
84-
pub async fn get_raw_mempool(&self) -> Result<Vec<Txid>> {
115+
async fn get_raw_mempool(&self) -> Result<Vec<Txid>> {
85116
let json: crate::types::v25::GetRawMempool = self.call("getrawmempool", &[]).await?;
86-
Ok(json.into_model()?.0)
117+
Ok(json.into_model().map_err(|e| IntoModelError::new("`getrawmempool`", e))?.0)
87118
}
88119

89-
/// Gets the raw transaction by txid.
90-
pub async fn get_raw_transaction(&self, txid: &Txid) -> Result<Transaction> {
120+
async fn get_raw_transaction(&self, txid: &Txid) -> Result<Transaction> {
91121
let json: crate::types::v25::GetRawTransaction =
92122
self.call("getrawtransaction", &[into_json(txid)?]).await?;
93-
Ok(json.into_model()?.0)
123+
Ok(json.into_model().map_err(|e| IntoModelError::new("`getrawtransaction`", e))?.0)
94124
}
95125

96-
/// Returns the version integer reported by the server (e.g. `250200` for v25.2.0).
97-
pub async fn server_version(&self) -> Result<usize> {
126+
async fn server_version(&self) -> Result<usize> {
98127
// Use a minimal type to read only the `version` field; the shape of other fields
99128
// (e.g. `warnings` changed from String to Vec<String> at v28) differs across the
100129
// supported version range.

integration_test/tests/bdk_client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use bitcoin::address::KnownHrp;
1010
use bitcoin::{Address, CompressedPublicKey, PrivateKey};
1111
use bitcoind::mtype;
12-
use corepc_client::client_async::{Auth, Client, Error as AsyncClientError};
12+
use corepc_client::client_async::{Auth, BitcoinRpcs as _, Client, Error as AsyncClientError};
1313
use integration_test::{BitcoinD, BitcoinDExt as _, Wallet};
1414

1515
fn async_client_for(node: &BitcoinD) -> Client {

0 commit comments

Comments
 (0)