neo3/neo_clients/rpc/transports/
http_provider.rs

1// Code adapted from: https://github.com/althea-net/guac_rs/tree/master/web3/src/jsonrpc
2
3use std::{
4	str::FromStr,
5	sync::atomic::{AtomicU64, Ordering},
6};
7
8use async_trait::async_trait;
9use http::HeaderValue;
10use log::debug;
11use reqwest::{header, Client, Error as ReqwestError};
12use serde::{de::DeserializeOwned, Serialize};
13use thiserror::Error;
14use url::Url;
15
16use super::common::{JsonRpcError, Request, Response};
17use crate::neo_clients::{Authorization, JsonRpcProvider, ProviderError};
18use neo3::config::NeoConstants;
19
20/// A low-level JSON-RPC Client over HTTP.
21///
22/// # Example
23///
24/// ```no_run
25/// use neo3::neo_clients::{HttpProvider, RpcClient, APITrait};
26/// use neo3::neo_config::NeoConstants;
27/// use primitive_types::H256;
28///
29/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
30/// let provider = HttpProvider::new(NeoConstants::SEED_1)?;
31/// let client = RpcClient::new(provider);
32/// let block = client.get_block(H256::zero(), false).await?;
33/// # Ok(())
34/// # }
35/// ```
36#[derive(Debug)]
37pub struct HttpProvider {
38	id: AtomicU64,
39	client: Client,
40	url: Url,
41}
42
43#[derive(Error, Debug)]
44/// Error thrown when sending an HTTP request
45pub enum ClientError {
46	/// Thrown if the request failed
47	#[error(transparent)]
48	ReqwestError(#[from] ReqwestError),
49	#[error(transparent)]
50	/// Thrown if the response could not be parsed
51	JsonRpcError(#[from] JsonRpcError),
52
53	#[error("Deserialization Error: {err}. Response: {text}")]
54	/// Serde JSON Error
55	SerdeJson {
56		/// Underlying error
57		err: serde_json::Error,
58		/// The contents of the HTTP response that could not be deserialized
59		text: String,
60	},
61}
62
63impl From<ClientError> for ProviderError {
64	fn from(src: ClientError) -> Self {
65		match src {
66			ClientError::ReqwestError(err) => ProviderError::HTTPError(err.into()),
67			ClientError::JsonRpcError(err) => ProviderError::JsonRpcError(err),
68			ClientError::SerdeJson { err, text } => {
69				debug!("SerdeJson Error: {:#?}, Response: {:#?}", err, text);
70				ProviderError::SerdeJson(err)
71			},
72			_ => ProviderError::IllegalState("unexpected error".to_string()),
73		}
74	}
75}
76
77#[cfg_attr(target_arch = "wasm32", async_trait(? Send))]
78#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
79impl JsonRpcProvider for HttpProvider {
80	type Error = ClientError;
81
82	async fn fetch<T: Serialize + Send + Sync, R: DeserializeOwned>(
83		&self,
84		method: &str,
85		params: T,
86	) -> Result<R, ClientError> {
87		let next_id = self.id.fetch_add(1, Ordering::SeqCst);
88		let payload = Request::new(next_id, method, params);
89
90		let res = self.client.post(self.url.as_ref()).json(&payload).send().await?;
91		let body = res.bytes().await?;
92
93		let raw = match serde_json::from_slice(&body) {
94			Ok(Response::Success { result, .. }) => result.to_owned(),
95			Ok(Response::Error { error, .. }) => return Err(error.into()),
96			Ok(_) => {
97				let err = ClientError::SerdeJson {
98					err: serde::de::Error::custom("unexpected notification over HTTP transport"),
99					text: String::from_utf8_lossy(&body).to_string(),
100				};
101				return Err(err);
102			},
103			Err(err) =>
104				return Err(ClientError::SerdeJson {
105					err,
106					text: String::from_utf8_lossy(&body).to_string(),
107				}),
108		};
109
110		let res = serde_json::from_str(raw.get())
111			.map_err(|err| ClientError::SerdeJson { err, text: raw.to_string() })?;
112
113		Ok(res)
114	}
115}
116
117impl Default for HttpProvider {
118	/// Default HTTP Provider from SEED_1
119	fn default() -> Self {
120		Self::new(Url::parse(NeoConstants::SEED_1).unwrap()).unwrap()
121	}
122}
123
124impl HttpProvider {
125	/// Initializes a new HTTP Client
126	///
127	/// # Example
128	///
129	/// ```
130	/// use neo3::neo_clients::HttpProvider;
131	/// use url::Url;
132	///
133	/// // Using a string
134	/// let provider = HttpProvider::new("http://localhost:8545")?;
135	///
136	/// // Using a &str
137	/// let provider = HttpProvider::new("http://localhost:8545")?;
138	///
139	/// // Using a Url
140	/// let url = Url::parse("http://localhost:8545").unwrap();
141	/// let provider = HttpProvider::new(url)?;
142	/// # Ok::<(), Box<dyn std::error::Error>>(())
143	/// ```
144	pub fn new<T: TryInto<Url>>(url: T) -> Result<Self, T::Error> {
145		let url = url.try_into()?;
146		Ok(Self::new_with_client(url, Client::new()))
147	}
148
149	/// The Url to which requests are made
150	pub fn url(&self) -> &Url {
151		&self.url
152	}
153
154	/// Mutable access to the Url to which requests are made
155	pub fn url_mut(&mut self) -> &mut Url {
156		&mut self.url
157	}
158
159	/// Initializes a new HTTP Client with authentication
160	///
161	/// # Example
162	///
163	/// ```
164	/// use neo3::neo_clients::{HttpProvider, Authorization};
165	/// use url::Url;
166	///
167	/// let url = Url::parse("http://localhost:8545").unwrap();
168	/// let provider = HttpProvider::new_with_auth(url, Authorization::basic("admin", "good_password"))?;
169	/// # Ok::<(), Box<dyn std::error::Error>>(())
170	/// ```
171	pub fn new_with_auth(
172		url: impl Into<Url>,
173		auth: Authorization,
174	) -> Result<Self, HttpClientError> {
175		let mut auth_value = HeaderValue::from_str(&auth.to_string())?;
176		auth_value.set_sensitive(true);
177
178		let mut headers = reqwest::header::HeaderMap::new();
179		headers.insert(reqwest::header::AUTHORIZATION, auth_value);
180
181		let client = Client::builder().default_headers(headers).build()?;
182
183		Ok(Self::new_with_client(url, client))
184	}
185
186	/// Allows to customize the provider by providing your own http client
187	///
188	/// # Example
189	///
190	/// ```
191	/// use neo3::neo_clients::HttpProvider;
192	/// use url::Url;
193	///
194	/// let url = Url::parse("http://localhost:8545").unwrap();
195	/// let client = reqwest::Client::builder().build().unwrap();
196	/// let provider = HttpProvider::new_with_client(url, client);
197	/// ```
198	pub fn new_with_client(url: impl Into<Url>, client: reqwest::Client) -> Self {
199		Self { id: AtomicU64::new(1), client, url: url.into() }
200	}
201}
202
203impl Clone for HttpProvider {
204	fn clone(&self) -> Self {
205		Self { id: AtomicU64::new(1), client: self.client.clone(), url: self.url.clone() }
206	}
207}
208
209#[derive(Error, Debug)]
210/// Error thrown when dealing with Http clients
211pub enum HttpClientError {
212	/// Thrown if unable to build headers for client
213	#[error(transparent)]
214	InvalidHeader(#[from] header::InvalidHeaderValue),
215
216	/// Thrown if unable to build client
217	#[error(transparent)]
218	ClientBuild(#[from] reqwest::Error),
219}