Skip to main content

cashweb_bitcoin_client/
lib.rs

1#![warn(
2    missing_debug_implementations,
3    missing_docs,
4    rust_2018_idioms,
5    unreachable_pub
6)]
7
8//! `cashweb-bitcoin-client` is a library providing a [`BitcoinClient`] with
9//! basic asynchronous methods for interacting with bitcoind.
10
11use hex::FromHexError;
12use hyper::{
13    client::HttpConnector, Body, Client as HyperClient, Error as HyperError,
14    Request as HttpRequest, Response as HttpResponse,
15};
16use hyper_tls::HttpsConnector;
17use json_rpc::{
18    clients::{
19        http::{Client as JsonClient, ConnectionError},
20        Error as RpcCallError,
21    },
22    prelude::{JsonError, RequestFactory, RpcError},
23};
24use serde_json::Value;
25use thiserror::Error;
26use tower_service::Service;
27
28/// Standard HTTP client.
29pub type HttpClient = HyperClient<HttpConnector>;
30
31/// Standard HTTPs client.
32pub type HttpsClient = HyperClient<HttpsConnector<HttpConnector>>;
33
34/// Standard HTTP error.
35pub type HttpError = NodeError<HyperError>;
36
37/// Basic Bitcoin JSON-RPC client.
38#[derive(Clone, Debug)]
39pub struct BitcoinClient<S>(JsonClient<S>);
40
41impl<S> BitcoinClient<S> {
42    /// Create a new [`BitcoinClient`] using a user-defined client service.
43    pub fn from_service(service: S, endpoint: String, username: String, password: String) -> Self {
44        BitcoinClient(JsonClient::from_service(
45            service,
46            endpoint,
47            Some(username),
48            Some(password),
49        ))
50    }
51}
52
53impl BitcoinClient<HyperClient<HttpConnector>> {
54    /// Create a new HTTP [`BitcoinClient`].
55    pub fn new(endpoint: String, username: String, password: String) -> Self {
56        BitcoinClient(JsonClient::new(endpoint, Some(username), Some(password)))
57    }
58}
59
60impl BitcoinClient<HyperClient<HttpsConnector<HttpConnector>>> {
61    /// Create a new HTTPS [`BitcoinClient`].
62    pub fn new_tls(endpoint: String, username: String, password: String) -> Self {
63        BitcoinClient(JsonClient::new_tls(
64            endpoint,
65            Some(username),
66            Some(password),
67        ))
68    }
69}
70
71impl<C> std::ops::Deref for BitcoinClient<C> {
72    type Target = JsonClient<C>;
73
74    fn deref(&self) -> &Self::Target {
75        &self.0
76    }
77}
78
79/// Error associated with the Bitcoin RPC.
80#[derive(Debug, Error)]
81pub enum NodeError<E: std::fmt::Debug + std::fmt::Display + 'static> {
82    /// Error connecting to bitcoind.
83    #[error(transparent)]
84    Http(RpcCallError<ConnectionError<E>>),
85    /// bitcoind responded with an JSON-RPC error.
86    #[error("{0:?}")]
87    Rpc(RpcError),
88    /// Failed to deserialize response JSON.
89    #[error(transparent)]
90    Json(JsonError),
91    /// The response JSON was empty.
92    #[error("empty response")]
93    EmptyResponse,
94    /// Failed to decode hexidecimal response.
95    #[error(transparent)]
96    HexDecode(#[from] FromHexError),
97}
98
99impl<S> BitcoinClient<S>
100where
101    S: Service<HttpRequest<Body>, Response = HttpResponse<Body>> + Clone,
102    S::Error: std::fmt::Debug + std::fmt::Display + 'static,
103    S::Future: Send + 'static,
104{
105    /// Calls the `getnewaddress` method.
106    pub async fn get_new_addr(&self) -> Result<String, NodeError<S::Error>> {
107        let request = self
108            .build_request()
109            .method("getnewaddress")
110            .finish()
111            .unwrap();
112        let response = self.send(request).await.map_err(NodeError::Http)?;
113        if response.is_error() {
114            return Err(NodeError::Rpc(response.error().unwrap()));
115        }
116        response
117            .into_result()
118            .ok_or(NodeError::EmptyResponse)?
119            .map_err(NodeError::Json)
120    }
121
122    /// Calls the `sendrawtransaction` method.
123    pub async fn send_tx(&self, raw_tx: &[u8]) -> Result<String, NodeError<S::Error>> {
124        let request = self
125            .build_request()
126            .method("sendrawtransaction")
127            .params(vec![Value::String(hex::encode(raw_tx))])
128            .finish()
129            .unwrap();
130        let response = self.send(request).await.map_err(NodeError::Http)?;
131        if response.is_error() {
132            let err = response.error().unwrap();
133            return Err(NodeError::Rpc(err));
134        }
135        response
136            .into_result()
137            .ok_or(NodeError::EmptyResponse)?
138            .map_err(NodeError::Json)
139    }
140
141    /// Calls the `getrawtransaction` method.
142    pub async fn get_raw_transaction(&self, tx_id: &[u8]) -> Result<Vec<u8>, NodeError<S::Error>> {
143        let request = self
144            .build_request()
145            .method("getrawtransaction")
146            .params(vec![Value::String(hex::encode(tx_id))])
147            .finish()
148            .unwrap();
149        let response = self.send(request).await.map_err(NodeError::Http)?;
150        if response.is_error() {
151            return Err(NodeError::Rpc(response.error().unwrap()));
152        }
153        let tx_hex: String = response
154            .into_result()
155            .ok_or(NodeError::EmptyResponse)?
156            .map_err(NodeError::Json)?;
157        hex::decode(tx_hex).map_err(Into::into)
158    }
159}