hopr_chain_rpc/
transport.rs

1use alloy::transports::{http::Http, utils::guess_local_url};
2use async_trait::async_trait;
3#[cfg(feature = "runtime-tokio")]
4pub use reqwest::Client as ReqwestClient;
5use url::Url;
6
7use crate::errors::HttpRequestError;
8
9/// Abstraction for an HTTP client that performs HTTP GET with serializable request data.
10#[async_trait]
11pub trait HttpRequestor: std::fmt::Debug + Send + Sync {
12    /// Performs HTTP GET query to the given URL and gets the JSON response.
13    async fn http_get(&self, url: &str) -> std::result::Result<Box<[u8]>, HttpRequestError>;
14}
15
16/// Local wrapper for `Http`
17#[derive(Clone, Debug)]
18pub struct HttpWrapper<T> {
19    client: T,
20    url: Url,
21}
22
23impl<T> HttpWrapper<T> {
24    /// Create a new [`Http`] transport with a custom client.
25    pub const fn with_client(client: T, url: Url) -> Self {
26        Self { client, url }
27    }
28
29    /// Set the URL.
30    pub fn set_url(&mut self, url: Url) {
31        self.url = url;
32    }
33
34    /// Set the client.
35    pub fn set_client(&mut self, client: T) {
36        self.client = client;
37    }
38
39    /// Guess whether the URL is local, based on the hostname.
40    ///
41    /// The output of this function is best-efforts, and should be checked if
42    /// possible. It simply returns `true` if the connection has no hostname,
43    /// or the hostname is `localhost` or `127.0.0.1`.
44    pub fn guess_local(&self) -> bool {
45        guess_local_url(&self.url)
46    }
47
48    /// Get a reference to the client.
49    pub const fn client(&self) -> &T {
50        &self.client
51    }
52
53    /// Get a reference to the URL.
54    pub fn url(&self) -> &str {
55        self.url.as_ref()
56    }
57}
58
59impl<T: Clone> From<Http<T>> for HttpWrapper<T> {
60    fn from(value: Http<T>) -> Self {
61        Self {
62            client: value.client().clone(),
63            url: Url::parse(value.url()).unwrap(),
64        }
65    }
66}
67
68impl<T: Clone> From<HttpWrapper<T>> for Http<T> {
69    fn from(value: HttpWrapper<T>) -> Self {
70        Self::with_client(value.client().clone(), Url::parse(value.url()).unwrap())
71    }
72}
73
74#[cfg(feature = "runtime-tokio")]
75#[async_trait]
76impl HttpRequestor for ReqwestClient {
77    #[inline]
78    async fn http_get(&self, url: &str) -> std::result::Result<Box<[u8]>, HttpRequestError> {
79        let res = self
80            .get(url)
81            .send()
82            .await
83            .map_err(|e| HttpRequestError::TransportError(e.to_string()))?;
84
85        let status = res.status();
86
87        tracing::debug!(%status, "received response from server");
88
89        let body = res
90            .bytes()
91            .await
92            .map_err(|e| HttpRequestError::UnknownError(e.to_string()))?;
93
94        tracing::debug!(bytes = body.len(), "retrieved response body. Use `trace` for full body");
95        tracing::trace!(body = %String::from_utf8_lossy(&body), "response body");
96
97        if !status.is_success() {
98            return Err(HttpRequestError::HttpError(
99                http::StatusCode::try_from(status.as_u16()).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR),
100            ));
101        }
102
103        Ok(body.to_vec().into_boxed_slice())
104    }
105}