Expose RpcClient and RestClient interfaces as pub
[rust-lightning] / lightning-block-sync / src / rpc.rs
1 use crate::{BlockHeaderData, BlockSource, AsyncBlockSourceResult};
2 use crate::http::{HttpClient, HttpEndpoint, JsonResponse};
3
4 use bitcoin::blockdata::block::Block;
5 use bitcoin::hash_types::BlockHash;
6 use bitcoin::hashes::hex::ToHex;
7
8 use serde_json;
9
10 use std::convert::TryFrom;
11 use std::convert::TryInto;
12 use std::sync::atomic::{AtomicUsize, Ordering};
13
14 /// A simple RPC client for calling methods using HTTP `POST`.
15 pub struct RpcClient {
16         basic_auth: String,
17         endpoint: HttpEndpoint,
18         client: HttpClient,
19         id: AtomicUsize,
20 }
21
22 impl RpcClient {
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)?;
28                 Ok(Self {
29                         basic_auth: "Basic ".to_string() + credentials,
30                         endpoint,
31                         client,
32                         id: AtomicUsize::new(0),
33                 })
34         }
35
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!({
42                         "method": method,
43                         "params": params,
44                         "id": &self.id.fetch_add(1, Ordering::AcqRel).to_string()
45                 });
46
47                 let mut response = self.client.post::<JsonResponse>(&uri, &host, &self.basic_auth, content)
48                         .await?.0;
49                 if !response.is_object() {
50                         return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON object"));
51                 }
52
53                 let error = &response["error"];
54                 if !error.is_null() {
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));
58                 }
59
60                 let result = &mut response["result"];
61                 if result.is_null() {
62                         return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON result"));
63                 }
64
65                 JsonResponse(result.take()).try_into()
66         }
67 }
68
69 impl BlockSource for RpcClient {
70         fn get_header<'a>(&'a mut self, header_hash: &'a BlockHash, _height: Option<u32>) -> AsyncBlockSourceResult<'a, BlockHeaderData> {
71                 Box::pin(async move {
72                         let header_hash = serde_json::json!(header_hash.to_hex());
73                         Ok(self.call_method("getblockheader", &[header_hash]).await?)
74                 })
75         }
76
77         fn get_block<'a>(&'a mut self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, Block> {
78                 Box::pin(async move {
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?)
82                 })
83         }
84
85         fn get_best_block<'a>(&'a mut self) -> AsyncBlockSourceResult<'a, (BlockHash, Option<u32>)> {
86                 Box::pin(async move {
87                         Ok(self.call_method("getblockchaininfo", &[]).await?)
88                 })
89         }
90 }
91
92 #[cfg(test)]
93 mod tests {
94         use super::*;
95         use crate::http::client_tests::{HttpServer, MessageBody};
96
97         /// Credentials encoded in base64.
98         const CREDENTIALS: &'static str = "dXNlcjpwYXNzd29yZA==";
99
100         /// Converts a JSON value into `u64`.
101         impl TryInto<u64> for JsonResponse {
102                 type Error = std::io::Error;
103
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")),
107                                 Some(n) => Ok(n),
108                         }
109                 }
110         }
111
112         #[tokio::test]
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();
116
117                 match client.call_method::<u64>("getblockcount", &[]).await {
118                         Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::NotFound),
119                         Ok(_) => panic!("Expected error"),
120                 }
121         }
122
123         #[tokio::test]
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();
128
129                 match client.call_method::<u64>("getblockcount", &[]).await {
130                         Err(e) => {
131                                 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
132                                 assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON object");
133                         },
134                         Ok(_) => panic!("Expected error"),
135                 }
136         }
137
138         #[tokio::test]
139         async fn call_method_returning_error() {
140                 let response = serde_json::json!({
141                         "error": { "code": -8, "message": "invalid parameter" },
142                 });
143                 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
144                 let mut client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
145
146                 let invalid_block_hash = serde_json::json!("foo");
147                 match client.call_method::<u64>("getblock", &[invalid_block_hash]).await {
148                         Err(e) => {
149                                 assert_eq!(e.kind(), std::io::ErrorKind::Other);
150                                 assert_eq!(e.get_ref().unwrap().to_string(), "invalid parameter");
151                         },
152                         Ok(_) => panic!("Expected error"),
153                 }
154         }
155
156         #[tokio::test]
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();
161
162                 match client.call_method::<u64>("getblockcount", &[]).await {
163                         Err(e) => {
164                                 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
165                                 assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON result");
166                         },
167                         Ok(_) => panic!("Expected error"),
168                 }
169         }
170
171         #[tokio::test]
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();
176
177                 match client.call_method::<u64>("getblockcount", &[]).await {
178                         Err(e) => {
179                                 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
180                                 assert_eq!(e.get_ref().unwrap().to_string(), "not a number");
181                         },
182                         Ok(_) => panic!("Expected error"),
183                 }
184         }
185
186         #[tokio::test]
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();
191
192                 match client.call_method::<u64>("getblockcount", &[]).await {
193                         Err(e) => panic!("Unexpected error: {:?}", e),
194                         Ok(count) => assert_eq!(count, 654470),
195                 }
196         }
197 }