Surface bitcoind rpc error code
[rust-lightning] / lightning-block-sync / src / rpc.rs
1 //! Simple RPC client implementation which implements [`BlockSource`] against a Bitcoin Core RPC
2 //! endpoint.
3
4 use crate::{BlockData, BlockHeaderData, BlockSource, AsyncBlockSourceResult};
5 use crate::http::{HttpClient, HttpEndpoint, HttpError, JsonResponse};
6
7 use bitcoin::hash_types::BlockHash;
8 use bitcoin::hashes::hex::ToHex;
9
10 use futures_util::lock::Mutex;
11
12 use serde_json;
13
14 use std::convert::TryFrom;
15 use std::convert::TryInto;
16 use std::error::Error;
17 use std::fmt;
18 use std::sync::atomic::{AtomicUsize, Ordering};
19
20 /// An error returned by the RPC server.
21 #[derive(Debug)]
22 pub struct RpcError {
23         /// The error code.
24         pub code: i64,
25         /// The error message.
26         pub message: String,
27 }
28
29 impl fmt::Display for RpcError {
30     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31         write!(f, "RPC error {}: {}", self.code, self.message)
32     }
33 }
34
35 impl Error for RpcError {}
36
37 /// A simple RPC client for calling methods using HTTP `POST`.
38 pub struct RpcClient {
39         basic_auth: String,
40         endpoint: HttpEndpoint,
41         client: Mutex<HttpClient>,
42         id: AtomicUsize,
43 }
44
45 impl RpcClient {
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> {
50                 let client = Mutex::new(HttpClient::connect(&endpoint)?);
51                 Ok(Self {
52                         basic_auth: "Basic ".to_string() + credentials,
53                         endpoint,
54                         client,
55                         id: AtomicUsize::new(0),
56                 })
57         }
58
59         /// Calls a method with the response encoded in JSON format and interpreted as type `T`.
60         pub async fn call_method<T>(&self, method: &str, params: &[serde_json::Value]) -> std::io::Result<T>
61         where JsonResponse: TryFrom<Vec<u8>, Error = std::io::Error> + TryInto<T, Error = std::io::Error> {
62                 let host = format!("{}:{}", self.endpoint.host(), self.endpoint.port());
63                 let uri = self.endpoint.path();
64                 let content = serde_json::json!({
65                         "method": method,
66                         "params": params,
67                         "id": &self.id.fetch_add(1, Ordering::AcqRel).to_string()
68                 });
69
70                 let mut response = match self.client.lock().await.post::<JsonResponse>(&uri, &host, &self.basic_auth, content).await {
71                         Ok(JsonResponse(response)) => response,
72                         Err(e) if e.kind() == std::io::ErrorKind::Other => {
73                                 match e.get_ref().unwrap().downcast_ref::<HttpError>() {
74                                         Some(http_error) => match JsonResponse::try_from(http_error.contents.clone()) {
75                                                 Ok(JsonResponse(response)) => response,
76                                                 Err(_) => Err(e)?,
77                                         },
78                                         None => Err(e)?,
79                                 }
80                         },
81                         Err(e) => Err(e)?,
82                 };
83
84                 if !response.is_object() {
85                         return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON object"));
86                 }
87
88                 let error = &response["error"];
89                 if !error.is_null() {
90                         // TODO: Examine error code for a more precise std::io::ErrorKind.
91                         let rpc_error = RpcError { 
92                                 code: error["code"].as_i64().unwrap_or(-1), 
93                                 message: error["message"].as_str().unwrap_or("unknown error").to_string() 
94                         };
95                         return Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error));
96                 }
97
98                 let result = &mut response["result"];
99                 if result.is_null() {
100                         return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON result"));
101                 }
102
103                 JsonResponse(result.take()).try_into()
104         }
105 }
106
107 impl BlockSource for RpcClient {
108         fn get_header<'a>(&'a self, header_hash: &'a BlockHash, _height: Option<u32>) -> AsyncBlockSourceResult<'a, BlockHeaderData> {
109                 Box::pin(async move {
110                         let header_hash = serde_json::json!(header_hash.to_hex());
111                         Ok(self.call_method("getblockheader", &[header_hash]).await?)
112                 })
113         }
114
115         fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, BlockData> {
116                 Box::pin(async move {
117                         let header_hash = serde_json::json!(header_hash.to_hex());
118                         let verbosity = serde_json::json!(0);
119                         Ok(BlockData::FullBlock(self.call_method("getblock", &[header_hash, verbosity]).await?))
120                 })
121         }
122
123         fn get_best_block<'a>(&'a self) -> AsyncBlockSourceResult<'a, (BlockHash, Option<u32>)> {
124                 Box::pin(async move {
125                         Ok(self.call_method("getblockchaininfo", &[]).await?)
126                 })
127         }
128 }
129
130 #[cfg(test)]
131 mod tests {
132         use super::*;
133         use crate::http::client_tests::{HttpServer, MessageBody};
134
135         /// Credentials encoded in base64.
136         const CREDENTIALS: &'static str = "dXNlcjpwYXNzd29yZA==";
137
138         /// Converts a JSON value into `u64`.
139         impl TryInto<u64> for JsonResponse {
140                 type Error = std::io::Error;
141
142                 fn try_into(self) -> std::io::Result<u64> {
143                         match self.0.as_u64() {
144                                 None => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "not a number")),
145                                 Some(n) => Ok(n),
146                         }
147                 }
148         }
149
150         #[tokio::test]
151         async fn call_method_returning_unknown_response() {
152                 let server = HttpServer::responding_with_not_found();
153                 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
154
155                 match client.call_method::<u64>("getblockcount", &[]).await {
156                         Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::Other),
157                         Ok(_) => panic!("Expected error"),
158                 }
159         }
160
161         #[tokio::test]
162         async fn call_method_returning_malfomred_response() {
163                 let response = serde_json::json!("foo");
164                 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
165                 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
166
167                 match client.call_method::<u64>("getblockcount", &[]).await {
168                         Err(e) => {
169                                 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
170                                 assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON object");
171                         },
172                         Ok(_) => panic!("Expected error"),
173                 }
174         }
175
176         #[tokio::test]
177         async fn call_method_returning_error() {
178                 let response = serde_json::json!({
179                         "error": { "code": -8, "message": "invalid parameter" },
180                 });
181                 let server = HttpServer::responding_with_server_error(response);
182                 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
183
184                 let invalid_block_hash = serde_json::json!("foo");
185                 match client.call_method::<u64>("getblock", &[invalid_block_hash]).await {
186                         Err(e) => {
187                                 assert_eq!(e.kind(), std::io::ErrorKind::Other);
188                                 let rpc_error: Box<RpcError> = e.into_inner().unwrap().downcast().unwrap();
189                                 assert_eq!(rpc_error.code, -8);
190                                 assert_eq!(rpc_error.message, "invalid parameter");
191                         },
192                         Ok(_) => panic!("Expected error"),
193                 }
194         }
195
196         #[tokio::test]
197         async fn call_method_returning_missing_result() {
198                 let response = serde_json::json!({ "result": null });
199                 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
200                 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
201
202                 match client.call_method::<u64>("getblockcount", &[]).await {
203                         Err(e) => {
204                                 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
205                                 assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON result");
206                         },
207                         Ok(_) => panic!("Expected error"),
208                 }
209         }
210
211         #[tokio::test]
212         async fn call_method_returning_malformed_result() {
213                 let response = serde_json::json!({ "result": "foo" });
214                 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
215                 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
216
217                 match client.call_method::<u64>("getblockcount", &[]).await {
218                         Err(e) => {
219                                 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
220                                 assert_eq!(e.get_ref().unwrap().to_string(), "not a number");
221                         },
222                         Ok(_) => panic!("Expected error"),
223                 }
224         }
225
226         #[tokio::test]
227         async fn call_method_returning_valid_result() {
228                 let response = serde_json::json!({ "result": 654470 });
229                 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
230                 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
231
232                 match client.call_method::<u64>("getblockcount", &[]).await {
233                         Err(e) => panic!("Unexpected error: {:?}", e),
234                         Ok(count) => assert_eq!(count, 654470),
235                 }
236         }
237 }