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
111		let res = serde_json::from_str(raw.get())
112			.map_err(|err| ClientError::SerdeJson { err, text: raw.to_string() })?;
113
114		Ok(res)
115	}
116}
117
118impl Default for HttpProvider {
119	/// Default HTTP Provider from SEED_1
120	fn default() -> Self {
121		Self::new(Url::parse(NeoConstants::SEED_1).unwrap()).unwrap()
122	}
123}
124
125impl HttpProvider {
126	/// Initializes a new HTTP Client
127	///
128	/// # Example
129	///
130	/// ```
131	/// use neo3::neo_clients::HttpProvider;
132	/// use url::Url;
133	///
134	/// // Using a string
135	/// let provider = HttpProvider::new("http://localhost:8545")?;
136	///
137	/// // Using a &str
138	/// let provider = HttpProvider::new("http://localhost:8545")?;
139	///
140	/// // Using a Url
141	/// let url = Url::parse("http://localhost:8545").unwrap();
142	/// let provider = HttpProvider::new(url)?;
143	/// # Ok::<(), Box<dyn std::error::Error>>(())
144	/// ```
145	pub fn new<T: TryInto<Url>>(url: T) -> Result<Self, T::Error> {
146		let url = url.try_into()?;
147		Ok(Self::new_with_client(url, Client::new()))
148	}
149
150	/// The Url to which requests are made
151	pub fn url(&self) -> &Url {
152		&self.url
153	}
154
155	/// Mutable access to the Url to which requests are made
156	pub fn url_mut(&mut self) -> &mut Url {
157		&mut self.url
158	}
159
160	/// Initializes a new HTTP Client with authentication
161	///
162	/// # Example
163	///
164	/// ```
165	/// use neo3::neo_clients::{HttpProvider, Authorization};
166	/// use url::Url;
167	///
168	/// let url = Url::parse("http://localhost:8545").unwrap();
169	/// let provider = HttpProvider::new_with_auth(url, Authorization::basic("admin", "good_password"))?;
170	/// # Ok::<(), Box<dyn std::error::Error>>(())
171	/// ```
172	pub fn new_with_auth(
173		url: impl Into<Url>,
174		auth: Authorization,
175	) -> Result<Self, HttpClientError> {
176		let mut auth_value = HeaderValue::from_str(&auth.to_string())?;
177		auth_value.set_sensitive(true);
178
179		let mut headers = reqwest::header::HeaderMap::new();
180		headers.insert(reqwest::header::AUTHORIZATION, auth_value);
181
182		let client = Client::builder().default_headers(headers).build()?;
183
184		Ok(Self::new_with_client(url, client))
185	}
186
187	/// Allows to customize the provider by providing your own http client
188	///
189	/// # Example
190	///
191	/// ```
192	/// use neo3::neo_clients::HttpProvider;
193	/// use url::Url;
194	///
195	/// let url = Url::parse("http://localhost:8545").unwrap();
196	/// let client = reqwest::Client::builder().build().unwrap();
197	/// let provider = HttpProvider::new_with_client(url, client);
198	/// ```
199	pub fn new_with_client(url: impl Into<Url>, client: reqwest::Client) -> Self {
200		Self { id: AtomicU64::new(1), client, url: url.into() }
201	}
202}
203
204impl Clone for HttpProvider {
205	fn clone(&self) -> Self {
206		Self { id: AtomicU64::new(1), client: self.client.clone(), url: self.url.clone() }
207	}
208}
209
210#[derive(Error, Debug)]
211/// Error thrown when dealing with Http clients
212pub enum HttpClientError {
213	/// Thrown if unable to build headers for client
214	#[error(transparent)]
215	InvalidHeader(#[from] header::InvalidHeaderValue),
216
217	/// Thrown if unable to build client
218	#[error(transparent)]
219	ClientBuild(#[from] reqwest::Error),
220}