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};
7 use bitcoin::hash_types::BlockHash;
8 use bitcoin::hashes::hex::ToHex;
12 use std::convert::TryFrom;
13 use std::convert::TryInto;
14 use std::error::Error;
16 use std::sync::atomic::{AtomicUsize, Ordering};
18 /// An error returned by the RPC server.
23 /// The error message.
27 impl fmt::Display for RpcError {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 write!(f, "RPC error {}: {}", self.code, self.message)
33 impl Error for RpcError {}
35 /// A simple RPC client for calling methods using HTTP `POST`.
37 /// Implements [`BlockSource`] and may return an `Err` containing [`RpcError`]. See
38 /// [`RpcClient::call_method`] for details.
39 pub struct RpcClient {
41 endpoint: HttpEndpoint,
46 /// Creates a new RPC client connected to the given endpoint with the provided credentials. The
47 /// credentials should be a base64 encoding of a user name and password joined by a colon, as is
48 /// required for HTTP basic access authentication.
49 pub fn new(credentials: &str, endpoint: HttpEndpoint) -> std::io::Result<Self> {
51 basic_auth: "Basic ".to_string() + credentials,
53 id: AtomicUsize::new(0),
57 /// Calls a method with the response encoded in JSON format and interpreted as type `T`.
59 /// When an `Err` is returned, [`std::io::Error::into_inner`] may contain an [`RpcError`] if
60 /// [`std::io::Error::kind`] is [`std::io::ErrorKind::Other`].
61 pub async fn call_method<T>(&self, method: &str, params: &[serde_json::Value]) -> std::io::Result<T>
62 where JsonResponse: TryFrom<Vec<u8>, Error = std::io::Error> + TryInto<T, Error = std::io::Error> {
63 let host = format!("{}:{}", self.endpoint.host(), self.endpoint.port());
64 let uri = self.endpoint.path();
65 let content = serde_json::json!({
68 "id": &self.id.fetch_add(1, Ordering::AcqRel).to_string()
71 let mut client = HttpClient::connect(&self.endpoint)?;
72 let mut response = match client.post::<JsonResponse>(&uri, &host, &self.basic_auth, content).await {
73 Ok(JsonResponse(response)) => response,
74 Err(e) if e.kind() == std::io::ErrorKind::Other => {
75 match e.get_ref().unwrap().downcast_ref::<HttpError>() {
76 Some(http_error) => match JsonResponse::try_from(http_error.contents.clone()) {
77 Ok(JsonResponse(response)) => response,
86 if !response.is_object() {
87 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON object"));
90 let error = &response["error"];
92 // TODO: Examine error code for a more precise std::io::ErrorKind.
93 let rpc_error = RpcError {
94 code: error["code"].as_i64().unwrap_or(-1),
95 message: error["message"].as_str().unwrap_or("unknown error").to_string()
97 return Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error));
100 let result = &mut response["result"];
101 if result.is_null() {
102 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON result"));
105 JsonResponse(result.take()).try_into()
109 impl BlockSource for RpcClient {
110 fn get_header<'a>(&'a self, header_hash: &'a BlockHash, _height: Option<u32>) -> AsyncBlockSourceResult<'a, BlockHeaderData> {
111 Box::pin(async move {
112 let header_hash = serde_json::json!(header_hash.to_hex());
113 Ok(self.call_method("getblockheader", &[header_hash]).await?)
117 fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, BlockData> {
118 Box::pin(async move {
119 let header_hash = serde_json::json!(header_hash.to_hex());
120 let verbosity = serde_json::json!(0);
121 Ok(BlockData::FullBlock(self.call_method("getblock", &[header_hash, verbosity]).await?))
125 fn get_best_block<'a>(&'a self) -> AsyncBlockSourceResult<'a, (BlockHash, Option<u32>)> {
126 Box::pin(async move {
127 Ok(self.call_method("getblockchaininfo", &[]).await?)
135 use crate::http::client_tests::{HttpServer, MessageBody};
137 /// Credentials encoded in base64.
138 const CREDENTIALS: &'static str = "dXNlcjpwYXNzd29yZA==";
140 /// Converts a JSON value into `u64`.
141 impl TryInto<u64> for JsonResponse {
142 type Error = std::io::Error;
144 fn try_into(self) -> std::io::Result<u64> {
145 match self.0.as_u64() {
146 None => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "not a number")),
153 async fn call_method_returning_unknown_response() {
154 let server = HttpServer::responding_with_not_found();
155 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
157 match client.call_method::<u64>("getblockcount", &[]).await {
158 Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::Other),
159 Ok(_) => panic!("Expected error"),
164 async fn call_method_returning_malfomred_response() {
165 let response = serde_json::json!("foo");
166 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
167 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
169 match client.call_method::<u64>("getblockcount", &[]).await {
171 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
172 assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON object");
174 Ok(_) => panic!("Expected error"),
179 async fn call_method_returning_error() {
180 let response = serde_json::json!({
181 "error": { "code": -8, "message": "invalid parameter" },
183 let server = HttpServer::responding_with_server_error(response);
184 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
186 let invalid_block_hash = serde_json::json!("foo");
187 match client.call_method::<u64>("getblock", &[invalid_block_hash]).await {
189 assert_eq!(e.kind(), std::io::ErrorKind::Other);
190 let rpc_error: Box<RpcError> = e.into_inner().unwrap().downcast().unwrap();
191 assert_eq!(rpc_error.code, -8);
192 assert_eq!(rpc_error.message, "invalid parameter");
194 Ok(_) => panic!("Expected error"),
199 async fn call_method_returning_missing_result() {
200 let response = serde_json::json!({ "result": null });
201 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
202 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
204 match client.call_method::<u64>("getblockcount", &[]).await {
206 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
207 assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON result");
209 Ok(_) => panic!("Expected error"),
214 async fn call_method_returning_malformed_result() {
215 let response = serde_json::json!({ "result": "foo" });
216 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
217 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
219 match client.call_method::<u64>("getblockcount", &[]).await {
221 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
222 assert_eq!(e.get_ref().unwrap().to_string(), "not a number");
224 Ok(_) => panic!("Expected error"),
229 async fn call_method_returning_valid_result() {
230 let response = serde_json::json!({ "result": 654470 });
231 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
232 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
234 match client.call_method::<u64>("getblockcount", &[]).await {
235 Err(e) => panic!("Unexpected error: {:?}", e),
236 Ok(count) => assert_eq!(count, 654470),