1 use crate::{BlockHeaderData, BlockSource, AsyncBlockSourceResult};
2 use crate::http::{HttpClient, HttpEndpoint, JsonResponse};
4 use bitcoin::blockdata::block::Block;
5 use bitcoin::hash_types::BlockHash;
6 use bitcoin::hashes::hex::ToHex;
10 use std::convert::TryFrom;
11 use std::convert::TryInto;
12 use std::sync::atomic::{AtomicUsize, Ordering};
14 /// A simple RPC client for calling methods using HTTP `POST`.
15 pub struct RpcClient {
17 endpoint: HttpEndpoint,
23 /// Creates a new RPC client connected to the given endpoint with the provided credentials. The
24 /// credentials should be a base64 encoding of a user name and password joined by a colon, as is
25 /// required for HTTP basic access authentication.
26 pub fn new(credentials: &str, endpoint: HttpEndpoint) -> std::io::Result<Self> {
27 let client = HttpClient::connect(&endpoint)?;
29 basic_auth: "Basic ".to_string() + credentials,
32 id: AtomicUsize::new(0),
36 /// Calls a method with the response encoded in JSON format and interpreted as type `T`.
37 pub async fn call_method<T>(&mut self, method: &str, params: &[serde_json::Value]) -> std::io::Result<T>
38 where JsonResponse: TryFrom<Vec<u8>, Error = std::io::Error> + TryInto<T, Error = std::io::Error> {
39 let host = format!("{}:{}", self.endpoint.host(), self.endpoint.port());
40 let uri = self.endpoint.path();
41 let content = serde_json::json!({
44 "id": &self.id.fetch_add(1, Ordering::AcqRel).to_string()
47 let mut response = self.client.post::<JsonResponse>(&uri, &host, &self.basic_auth, content)
49 if !response.is_object() {
50 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON object"));
53 let error = &response["error"];
55 // TODO: Examine error code for a more precise std::io::ErrorKind.
56 let message = error["message"].as_str().unwrap_or("unknown error");
57 return Err(std::io::Error::new(std::io::ErrorKind::Other, message));
60 let result = &mut response["result"];
62 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON result"));
65 JsonResponse(result.take()).try_into()
69 impl BlockSource for RpcClient {
70 fn get_header<'a>(&'a mut self, header_hash: &'a BlockHash, _height: Option<u32>) -> AsyncBlockSourceResult<'a, BlockHeaderData> {
72 let header_hash = serde_json::json!(header_hash.to_hex());
73 Ok(self.call_method("getblockheader", &[header_hash]).await?)
77 fn get_block<'a>(&'a mut self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, Block> {
79 let header_hash = serde_json::json!(header_hash.to_hex());
80 let verbosity = serde_json::json!(0);
81 Ok(self.call_method("getblock", &[header_hash, verbosity]).await?)
85 fn get_best_block<'a>(&'a mut self) -> AsyncBlockSourceResult<'a, (BlockHash, Option<u32>)> {
87 Ok(self.call_method("getblockchaininfo", &[]).await?)
95 use crate::http::client_tests::{HttpServer, MessageBody};
97 /// Credentials encoded in base64.
98 const CREDENTIALS: &'static str = "dXNlcjpwYXNzd29yZA==";
100 /// Converts a JSON value into `u64`.
101 impl TryInto<u64> for JsonResponse {
102 type Error = std::io::Error;
104 fn try_into(self) -> std::io::Result<u64> {
105 match self.0.as_u64() {
106 None => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "not a number")),
113 async fn call_method_returning_unknown_response() {
114 let server = HttpServer::responding_with_not_found();
115 let mut client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
117 match client.call_method::<u64>("getblockcount", &[]).await {
118 Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::NotFound),
119 Ok(_) => panic!("Expected error"),
124 async fn call_method_returning_malfomred_response() {
125 let response = serde_json::json!("foo");
126 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
127 let mut client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
129 match client.call_method::<u64>("getblockcount", &[]).await {
131 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
132 assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON object");
134 Ok(_) => panic!("Expected error"),
139 async fn call_method_returning_error() {
140 let response = serde_json::json!({
141 "error": { "code": -8, "message": "invalid parameter" },
143 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
144 let mut client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
146 let invalid_block_hash = serde_json::json!("foo");
147 match client.call_method::<u64>("getblock", &[invalid_block_hash]).await {
149 assert_eq!(e.kind(), std::io::ErrorKind::Other);
150 assert_eq!(e.get_ref().unwrap().to_string(), "invalid parameter");
152 Ok(_) => panic!("Expected error"),
157 async fn call_method_returning_missing_result() {
158 let response = serde_json::json!({ "result": null });
159 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
160 let mut client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
162 match client.call_method::<u64>("getblockcount", &[]).await {
164 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
165 assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON result");
167 Ok(_) => panic!("Expected error"),
172 async fn call_method_returning_malformed_result() {
173 let response = serde_json::json!({ "result": "foo" });
174 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
175 let mut client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
177 match client.call_method::<u64>("getblockcount", &[]).await {
179 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
180 assert_eq!(e.get_ref().unwrap().to_string(), "not a number");
182 Ok(_) => panic!("Expected error"),
187 async fn call_method_returning_valid_result() {
188 let response = serde_json::json!({ "result": 654470 });
189 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
190 let mut client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
192 match client.call_method::<u64>("getblockcount", &[]).await {
193 Err(e) => panic!("Unexpected error: {:?}", e),
194 Ok(count) => assert_eq!(count, 654470),