Skip to main content

nym_validator_client/rpc/
reqwest.rs

1// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::rpc::TendermintRpcClient;
5use async_trait::async_trait;
6use base64::Engine;
7use cosmrs::tendermint;
8use cosmrs::tendermint::{block::Height, evidence::Evidence, Hash};
9use reqwest::header::HeaderMap;
10use reqwest::{header, RequestBuilder};
11use tendermint_rpc::dialect::{v0_34, v0_37, v0_38, LatestDialect};
12use tendermint_rpc::{
13    client::CompatMode,
14    dialect::{self, Dialect},
15    endpoint::{self, *},
16    query::Query,
17    Error, Order, Response, SimpleRequest,
18};
19use url::Url;
20
21// copied macro from tendermint-rpc crate because that's exactly what we have to do here too
22macro_rules! perform_with_compat {
23    ($self:expr, $request:expr) => {{
24        let request = $request;
25        match $self.compat {
26            CompatMode::V0_38 => {
27                $self
28                    .perform_request_with_dialect(request, dialect::v0_38::Dialect)
29                    .await
30            }
31            CompatMode::V0_37 => {
32                $self
33                    .perform_request_with_dialect(request, dialect::v0_37::Dialect)
34                    .await
35            }
36            CompatMode::V0_34 => {
37                $self
38                    .perform_request_with_dialect(request, dialect::v0_34::Dialect)
39                    .await
40            }
41        }
42    }};
43}
44
45// the separate implementation is now completely redundant
46#[deprecated(note = "use HttpClient directly instead")]
47pub struct ReqwestRpcClient {
48    compat: CompatMode,
49    inner: reqwest::Client,
50    url: Url,
51}
52
53#[allow(deprecated)]
54impl ReqwestRpcClient {
55    pub fn new(url: Url) -> Self {
56        ReqwestRpcClient {
57            // after updating to nyxd 0.42 and thus updating to cometbft, the compat mode changed
58            compat: CompatMode::V0_37,
59            inner: reqwest::Client::new(),
60            url,
61        }
62    }
63
64    pub fn set_compat_mode(&mut self, compat: CompatMode) {
65        self.compat = compat;
66    }
67
68    fn build_request<R, S>(&self, request: R) -> RequestBuilder
69    where
70        R: SimpleRequest<S>,
71        S: Dialect,
72    {
73        let mut headers = HeaderMap::new();
74        headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
75        headers.insert(
76            header::USER_AGENT,
77            format!("nym-reqwest-rpc-client/{}", env!("CARGO_PKG_VERSION"))
78                .parse()
79                .unwrap(),
80        );
81        if let Some(auth) = extract_authorization(&self.url) {
82            headers.insert(header::AUTHORIZATION, auth.parse().unwrap());
83        }
84
85        self.inner
86            .post(self.url.clone())
87            .body(request.into_json().into_bytes())
88            .headers(headers)
89    }
90
91    async fn perform_request_with_dialect<R, S>(
92        &self,
93        request: R,
94        _dialect: S,
95    ) -> Result<R::Output, Error>
96    where
97        R: SimpleRequest<S>,
98        S: Dialect,
99    {
100        let request = self.build_request(request);
101        // that's extremely unfortunate. the trait requires returning tendermint rpc error so we have to make best effort error mapping
102        let response = request
103            .send()
104            .await
105            .map_err(TendermintRpcErrorMap::into_rpc_err)?;
106        let response_status = response.status();
107        let bytes = response
108            .bytes()
109            .await
110            .map_err(TendermintRpcErrorMap::into_rpc_err)?;
111
112        // Successful JSON-RPC requests are expected to return a 200 OK HTTP status.
113        // Otherwise, this means that the HTTP request failed as a whole,
114        // as opposed to the JSON-RPC request returning an error,
115        // and we cannot expect the response body to be a valid JSON-RPC response.
116        if response_status != reqwest::StatusCode::OK {
117            // hehe, that's so nasty but we have to somehow convert between different versions of the same lib
118            return Err(Error::http_request_failed(
119                response_status.as_u16().try_into().unwrap(),
120            ));
121        }
122
123        R::Response::from_string(bytes).map(Into::into)
124    }
125}
126
127trait TendermintRpcErrorMap {
128    fn into_rpc_err(self) -> Error;
129}
130
131impl TendermintRpcErrorMap for reqwest::Error {
132    fn into_rpc_err(self) -> Error {
133        todo!()
134    }
135}
136
137#[allow(deprecated)]
138#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
139#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
140impl TendermintRpcClient for ReqwestRpcClient {
141    async fn perform<R>(&self, request: R) -> Result<R::Output, Error>
142    where
143        R: SimpleRequest,
144    {
145        self.perform_request_with_dialect(request, LatestDialect)
146            .await
147    }
148
149    async fn block<H>(&self, height: H) -> Result<endpoint::block::Response, Error>
150    where
151        H: Into<Height> + Send,
152    {
153        perform_with_compat!(self, endpoint::block::Request::new(height.into()))
154    }
155
156    async fn block_by_hash(
157        &self,
158        hash: tendermint::Hash,
159    ) -> Result<endpoint::block_by_hash::Response, Error> {
160        perform_with_compat!(self, endpoint::block_by_hash::Request::new(hash))
161    }
162
163    async fn latest_block(&self) -> Result<endpoint::block::Response, Error> {
164        perform_with_compat!(self, endpoint::block::Request::default())
165    }
166
167    async fn block_results<H>(&self, height: H) -> Result<endpoint::block_results::Response, Error>
168    where
169        H: Into<Height> + Send,
170    {
171        perform_with_compat!(self, endpoint::block_results::Request::new(height.into()))
172    }
173
174    async fn latest_block_results(&self) -> Result<endpoint::block_results::Response, Error> {
175        perform_with_compat!(self, endpoint::block_results::Request::default())
176    }
177
178    async fn block_search(
179        &self,
180        query: Query,
181        page: u32,
182        per_page: u8,
183        order: Order,
184    ) -> Result<endpoint::block_search::Response, Error> {
185        perform_with_compat!(
186            self,
187            endpoint::block_search::Request::new(query, page, per_page, order)
188        )
189    }
190
191    async fn header<H>(&self, height: H) -> Result<endpoint::header::Response, Error>
192    where
193        H: Into<Height> + Send,
194    {
195        let height = height.into();
196        match self.compat {
197            CompatMode::V0_38 => {
198                self.perform_request_with_dialect(
199                    endpoint::header::Request::new(height),
200                    v0_38::Dialect,
201                )
202                .await
203            }
204            CompatMode::V0_37 => {
205                self.perform_request_with_dialect(
206                    endpoint::header::Request::new(height),
207                    v0_37::Dialect,
208                )
209                .await
210            }
211            CompatMode::V0_34 => {
212                // Back-fill with a request to /block endpoint and
213                // taking just the header from the response.
214                let resp = self
215                    .perform_request_with_dialect(block::Request::new(height), v0_34::Dialect)
216                    .await?;
217                Ok(resp.into())
218            }
219        }
220    }
221
222    async fn header_by_hash(&self, hash: Hash) -> Result<header_by_hash::Response, Error> {
223        match self.compat {
224            CompatMode::V0_38 => {
225                self.perform_request_with_dialect(
226                    header_by_hash::Request::new(hash),
227                    v0_38::Dialect,
228                )
229                .await
230            }
231            CompatMode::V0_37 => {
232                self.perform_request_with_dialect(
233                    header_by_hash::Request::new(hash),
234                    v0_37::Dialect,
235                )
236                .await
237            }
238            CompatMode::V0_34 => {
239                // Back-fill with a request to /block_by_hash endpoint and
240                // taking just the header from the response.
241                let resp = self
242                    .perform_request_with_dialect(block_by_hash::Request::new(hash), v0_34::Dialect)
243                    .await?;
244                Ok(resp.into())
245            }
246        }
247    }
248
249    /// `/broadcast_evidence`: broadcast an evidence.
250    async fn broadcast_evidence(&self, e: Evidence) -> Result<evidence::Response, Error> {
251        match self.compat {
252            CompatMode::V0_38 => {
253                self.perform_request_with_dialect(evidence::Request::new(e), v0_38::Dialect)
254                    .await
255            }
256            CompatMode::V0_37 => {
257                self.perform_request_with_dialect(evidence::Request::new(e), v0_37::Dialect)
258                    .await
259            }
260            CompatMode::V0_34 => {
261                self.perform_request_with_dialect(evidence::Request::new(e), v0_34::Dialect)
262                    .await
263            }
264        }
265    }
266
267    async fn tx(&self, hash: Hash, prove: bool) -> Result<tx::Response, Error> {
268        perform_with_compat!(self, tx::Request::new(hash, prove))
269    }
270
271    async fn tx_search(
272        &self,
273        query: Query,
274        prove: bool,
275        page: u32,
276        per_page: u8,
277        order: Order,
278    ) -> Result<tx_search::Response, Error> {
279        perform_with_compat!(
280            self,
281            tx_search::Request::new(query, prove, page, per_page, order)
282        )
283    }
284
285    async fn broadcast_tx_commit<T>(&self, tx: T) -> Result<broadcast::tx_commit::Response, Error>
286    where
287        T: Into<Vec<u8>> + Send,
288    {
289        perform_with_compat!(self, broadcast::tx_commit::Request::new(tx))
290    }
291}
292
293// essentially https://github.com/informalsystems/tendermint-rs/blob/v0.32.0/rpc/src/client/transport/auth.rs#L31
294pub fn extract_authorization(url: &Url) -> Option<String> {
295    if !url.has_authority() {
296        return None;
297    }
298
299    let authority = url.authority();
300    if let Some((userpass, _)) = authority.split_once('@') {
301        Some(base64::prelude::BASE64_STANDARD.encode(userpass))
302    } else {
303        None
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[cfg(test)]
312    mod extracting_url_authorization {
313        use super::*;
314        use std::str::FromStr;
315
316        #[test]
317        fn extract_auth_absent() {
318            let uri = Url::from_str("http://example.com").unwrap();
319            assert_eq!(extract_authorization(&uri), None);
320        }
321
322        #[test]
323        fn extract_auth_username_only() {
324            let uri = Url::from_str("http://toto@example.com").unwrap();
325            let base64 = "dG90bw==".to_string();
326            assert_eq!(extract_authorization(&uri), Some(base64));
327        }
328
329        #[test]
330        fn extract_auth_username_password() {
331            let uri = Url::from_str("http://toto:tata@example.com").unwrap();
332            let base64 = "dG90bzp0YXRh".to_string();
333            assert_eq!(extract_authorization(&uri), Some(base64));
334        }
335    }
336}