1 //! Simple RPC client implementation which implements [`BlockSource`] against a Bitcoin Core RPC
4 use crate::{BlockData, BlockHeaderData, BlockSource, AsyncBlockSourceResult};
5 use crate::http::{HttpClient, HttpEndpoint, HttpError, JsonResponse};
6 use crate::gossip::UtxoSource;
8 use bitcoin::hash_types::BlockHash;
15 use std::convert::TryFrom;
16 use std::convert::TryInto;
17 use std::error::Error;
19 use std::sync::atomic::{AtomicUsize, Ordering};
21 /// An error returned by the RPC server.
26 /// The error message.
30 impl fmt::Display for RpcError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 write!(f, "RPC error {}: {}", self.code, self.message)
36 impl Error for RpcError {}
38 /// A simple RPC client for calling methods using HTTP `POST`.
40 /// Implements [`BlockSource`] and may return an `Err` containing [`RpcError`]. See
41 /// [`RpcClient::call_method`] for details.
42 pub struct RpcClient {
44 endpoint: HttpEndpoint,
45 client: Mutex<Option<HttpClient>>,
50 /// Creates a new RPC client connected to the given endpoint with the provided credentials. The
51 /// credentials should be a base64 encoding of a user name and password joined by a colon, as is
52 /// required for HTTP basic access authentication.
53 pub fn new(credentials: &str, endpoint: HttpEndpoint) -> std::io::Result<Self> {
55 basic_auth: "Basic ".to_string() + credentials,
57 client: Mutex::new(None),
58 id: AtomicUsize::new(0),
62 /// Calls a method with the response encoded in JSON format and interpreted as type `T`.
64 /// When an `Err` is returned, [`std::io::Error::into_inner`] may contain an [`RpcError`] if
65 /// [`std::io::Error::kind`] is [`std::io::ErrorKind::Other`].
66 pub async fn call_method<T>(&self, method: &str, params: &[serde_json::Value]) -> std::io::Result<T>
67 where JsonResponse: TryFrom<Vec<u8>, Error = std::io::Error> + TryInto<T, Error = std::io::Error> {
68 let host = format!("{}:{}", self.endpoint.host(), self.endpoint.port());
69 let uri = self.endpoint.path();
70 let content = serde_json::json!({
73 "id": &self.id.fetch_add(1, Ordering::AcqRel).to_string()
76 let mut client = if let Some(client) = self.client.lock().unwrap().take() { client }
77 else { HttpClient::connect(&self.endpoint)? };
78 let http_response = client.post::<JsonResponse>(&uri, &host, &self.basic_auth, content).await;
79 *self.client.lock().unwrap() = Some(client);
81 let mut response = match http_response {
82 Ok(JsonResponse(response)) => response,
83 Err(e) if e.kind() == std::io::ErrorKind::Other => {
84 match e.get_ref().unwrap().downcast_ref::<HttpError>() {
85 Some(http_error) => match JsonResponse::try_from(http_error.contents.clone()) {
86 Ok(JsonResponse(response)) => response,
95 if !response.is_object() {
96 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON object"));
99 let error = &response["error"];
100 if !error.is_null() {
101 // TODO: Examine error code for a more precise std::io::ErrorKind.
102 let rpc_error = RpcError {
103 code: error["code"].as_i64().unwrap_or(-1),
104 message: error["message"].as_str().unwrap_or("unknown error").to_string()
106 return Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error));
109 let result = match response.get_mut("result") {
110 Some(result) => result.take(),
112 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON result")),
115 JsonResponse(result).try_into()
119 impl BlockSource for RpcClient {
120 fn get_header<'a>(&'a self, header_hash: &'a BlockHash, _height: Option<u32>) -> AsyncBlockSourceResult<'a, BlockHeaderData> {
121 Box::pin(async move {
122 let header_hash = serde_json::json!(header_hash.to_string());
123 Ok(self.call_method("getblockheader", &[header_hash]).await?)
127 fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, BlockData> {
128 Box::pin(async move {
129 let header_hash = serde_json::json!(header_hash.to_string());
130 let verbosity = serde_json::json!(0);
131 Ok(BlockData::FullBlock(self.call_method("getblock", &[header_hash, verbosity]).await?))
135 fn get_best_block<'a>(&'a self) -> AsyncBlockSourceResult<'a, (BlockHash, Option<u32>)> {
136 Box::pin(async move {
137 Ok(self.call_method("getblockchaininfo", &[]).await?)
142 impl UtxoSource for RpcClient {
143 fn get_block_hash_by_height<'a>(&'a self, block_height: u32) -> AsyncBlockSourceResult<'a, BlockHash> {
144 Box::pin(async move {
145 let height_param = serde_json::json!(block_height);
146 Ok(self.call_method("getblockhash", &[height_param]).await?)
150 fn is_output_unspent<'a>(&'a self, outpoint: OutPoint) -> AsyncBlockSourceResult<'a, bool> {
151 Box::pin(async move {
152 let txid_param = serde_json::json!(outpoint.txid.to_string());
153 let vout_param = serde_json::json!(outpoint.vout);
154 let include_mempool = serde_json::json!(false);
155 let utxo_opt: serde_json::Value = self.call_method(
156 "gettxout", &[txid_param, vout_param, include_mempool]).await?;
157 Ok(!utxo_opt.is_null())
165 use crate::http::client_tests::{HttpServer, MessageBody};
167 use bitcoin::hashes::Hash;
169 /// Credentials encoded in base64.
170 const CREDENTIALS: &'static str = "dXNlcjpwYXNzd29yZA==";
172 /// Converts a JSON value into `u64`.
173 impl TryInto<u64> for JsonResponse {
174 type Error = std::io::Error;
176 fn try_into(self) -> std::io::Result<u64> {
177 match self.0.as_u64() {
178 None => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "not a number")),
185 async fn call_method_returning_unknown_response() {
186 let server = HttpServer::responding_with_not_found();
187 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
189 match client.call_method::<u64>("getblockcount", &[]).await {
190 Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::Other),
191 Ok(_) => panic!("Expected error"),
196 async fn call_method_returning_malfomred_response() {
197 let response = serde_json::json!("foo");
198 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
199 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
201 match client.call_method::<u64>("getblockcount", &[]).await {
203 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
204 assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON object");
206 Ok(_) => panic!("Expected error"),
211 async fn call_method_returning_error() {
212 let response = serde_json::json!({
213 "error": { "code": -8, "message": "invalid parameter" },
215 let server = HttpServer::responding_with_server_error(response);
216 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
218 let invalid_block_hash = serde_json::json!("foo");
219 match client.call_method::<u64>("getblock", &[invalid_block_hash]).await {
221 assert_eq!(e.kind(), std::io::ErrorKind::Other);
222 let rpc_error: Box<RpcError> = e.into_inner().unwrap().downcast().unwrap();
223 assert_eq!(rpc_error.code, -8);
224 assert_eq!(rpc_error.message, "invalid parameter");
226 Ok(_) => panic!("Expected error"),
231 async fn call_method_returning_missing_result() {
232 let response = serde_json::json!({ });
233 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
234 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
236 match client.call_method::<u64>("getblockcount", &[]).await {
238 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
239 assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON result");
241 Ok(_) => panic!("Expected error"),
246 async fn call_method_returning_malformed_result() {
247 let response = serde_json::json!({ "result": "foo" });
248 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
249 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
251 match client.call_method::<u64>("getblockcount", &[]).await {
253 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
254 assert_eq!(e.get_ref().unwrap().to_string(), "not a number");
256 Ok(_) => panic!("Expected error"),
261 async fn call_method_returning_valid_result() {
262 let response = serde_json::json!({ "result": 654470 });
263 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
264 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
266 match client.call_method::<u64>("getblockcount", &[]).await {
267 Err(e) => panic!("Unexpected error: {:?}", e),
268 Ok(count) => assert_eq!(count, 654470),
273 async fn fails_to_fetch_spent_utxo() {
274 let response = serde_json::json!({ "result": null });
275 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
276 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
277 let outpoint = OutPoint::new(bitcoin::Txid::from_byte_array([0; 32]), 0);
278 let unspent_output = client.is_output_unspent(outpoint).await.unwrap();
279 assert_eq!(unspent_output, false);
283 async fn fetches_utxo() {
284 let response = serde_json::json!({ "result": {"bestblock": 1, "confirmations": 42}});
285 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
286 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
287 let outpoint = OutPoint::new(bitcoin::Txid::from_byte_array([0; 32]), 0);
288 let unspent_output = client.is_output_unspent(outpoint).await.unwrap();
289 assert_eq!(unspent_output, true);