use bitcoin::hash_types::BlockHash;
use bitcoin::hashes::hex::ToHex;
-use futures_util::lock::Mutex;
+use std::sync::Mutex;
use serde_json;
use std::convert::TryFrom;
use std::convert::TryInto;
+use std::error::Error;
+use std::fmt;
use std::sync::atomic::{AtomicUsize, Ordering};
+/// An error returned by the RPC server.
+#[derive(Debug)]
+pub struct RpcError {
+ /// The error code.
+ pub code: i64,
+ /// The error message.
+ pub message: String,
+}
+
+impl fmt::Display for RpcError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "RPC error {}: {}", self.code, self.message)
+ }
+}
+
+impl Error for RpcError {}
+
/// A simple RPC client for calling methods using HTTP `POST`.
+///
+/// Implements [`BlockSource`] and may return an `Err` containing [`RpcError`]. See
+/// [`RpcClient::call_method`] for details.
pub struct RpcClient {
basic_auth: String,
endpoint: HttpEndpoint,
- client: Mutex<HttpClient>,
+ client: Mutex<Option<HttpClient>>,
id: AtomicUsize,
}
/// credentials should be a base64 encoding of a user name and password joined by a colon, as is
/// required for HTTP basic access authentication.
pub fn new(credentials: &str, endpoint: HttpEndpoint) -> std::io::Result<Self> {
- let client = Mutex::new(HttpClient::connect(&endpoint)?);
Ok(Self {
basic_auth: "Basic ".to_string() + credentials,
endpoint,
- client,
+ client: Mutex::new(None),
id: AtomicUsize::new(0),
})
}
/// Calls a method with the response encoded in JSON format and interpreted as type `T`.
+ ///
+ /// When an `Err` is returned, [`std::io::Error::into_inner`] may contain an [`RpcError`] if
+ /// [`std::io::Error::kind`] is [`std::io::ErrorKind::Other`].
pub async fn call_method<T>(&self, method: &str, params: &[serde_json::Value]) -> std::io::Result<T>
where JsonResponse: TryFrom<Vec<u8>, Error = std::io::Error> + TryInto<T, Error = std::io::Error> {
let host = format!("{}:{}", self.endpoint.host(), self.endpoint.port());
"id": &self.id.fetch_add(1, Ordering::AcqRel).to_string()
});
- let mut response = match self.client.lock().await.post::<JsonResponse>(&uri, &host, &self.basic_auth, content).await {
+ let mut client = if let Some(client) = self.client.lock().unwrap().take() { client }
+ else { HttpClient::connect(&self.endpoint)? };
+ let http_response = client.post::<JsonResponse>(&uri, &host, &self.basic_auth, content).await;
+ *self.client.lock().unwrap() = Some(client);
+
+ let mut response = match http_response {
Ok(JsonResponse(response)) => response,
Err(e) if e.kind() == std::io::ErrorKind::Other => {
match e.get_ref().unwrap().downcast_ref::<HttpError>() {
let error = &response["error"];
if !error.is_null() {
// TODO: Examine error code for a more precise std::io::ErrorKind.
- let message = error["message"].as_str().unwrap_or("unknown error");
- return Err(std::io::Error::new(std::io::ErrorKind::Other, message));
+ let rpc_error = RpcError {
+ code: error["code"].as_i64().unwrap_or(-1),
+ message: error["message"].as_str().unwrap_or("unknown error").to_string()
+ };
+ return Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error));
}
let result = &mut response["result"];
match client.call_method::<u64>("getblock", &[invalid_block_hash]).await {
Err(e) => {
assert_eq!(e.kind(), std::io::ErrorKind::Other);
- assert_eq!(e.get_ref().unwrap().to_string(), "invalid parameter");
+ let rpc_error: Box<RpcError> = e.into_inner().unwrap().downcast().unwrap();
+ assert_eq!(rpc_error.code, -8);
+ assert_eq!(rpc_error.message, "invalid parameter");
},
Ok(_) => panic!("Expected error"),
}