ic_web3_rs/transports/
ic_http.rs

1//! IC HTTP Transport
2
3use crate::transports::ICHttpClient;
4use crate::{
5    error::{Error, Result, TransportError},
6    helpers, BatchTransport, RequestId, Transport,
7};
8#[cfg(not(feature = "wasm"))]
9use futures::future::BoxFuture;
10use ic_cdk::api::management_canister::http_request::TransformContext;
11use jsonrpc_core::types::{Call, Output, Request, Value};
12use serde::de::DeserializeOwned;
13use std::{
14    collections::HashMap,
15    sync::{
16        atomic::{AtomicUsize, Ordering},
17        Arc,
18    },
19};
20
21pub use super::ic_http_client::{CallOptions, CallOptionsBuilder};
22
23/// HTTP Transport
24#[derive(Clone, Debug)]
25pub struct ICHttp {
26    client: ICHttpClient,
27    inner: Arc<Inner>,
28}
29
30#[derive(Debug)]
31struct Inner {
32    url: String,
33    id: AtomicUsize,
34}
35
36impl ICHttp {
37    /// Create new HTTP transport connecting to given URL, cycles: cycles amount to perform http call
38    ///
39    /// Note that the http [Client] automatically enables some features like setting the basic auth
40    /// header or enabling a proxy from the environment. You can customize it with
41    /// [Http::with_client].
42    pub fn new(url: &str, max_resp: Option<u64>) -> Result<Self> {
43        Ok(Self {
44            client: ICHttpClient::new(max_resp),
45            inner: Arc::new(Inner {
46                url: url.to_string(),
47                id: AtomicUsize::new(0),
48            }),
49        })
50    }
51
52    fn next_id(&self) -> RequestId {
53        self.inner.id.fetch_add(1, Ordering::AcqRel)
54    }
55
56    fn new_request(&self) -> (ICHttpClient, String) {
57        (self.client.clone(), self.inner.url.clone())
58    }
59}
60
61// Id is only used for logging.
62async fn execute_rpc<T: DeserializeOwned>(
63    client: &ICHttpClient,
64    url: String,
65    request: &Request,
66    id: RequestId,
67    options: CallOptions,
68) -> Result<T> {
69    let response = client
70        .post(url, request, options)
71        .await
72        .map_err(|err| Error::Transport(TransportError::Message(err)))?;
73    helpers::arbitrary_precision_deserialize_workaround(&response).map_err(|err| {
74        Error::Transport(TransportError::Message(format!(
75            "failed to deserialize response: {}: {}",
76            err,
77            String::from_utf8_lossy(&response)
78        )))
79    })
80}
81
82type RpcResult = Result<Value>;
83
84impl Transport for ICHttp {
85    type Out = BoxFuture<'static, Result<Value>>;
86
87    fn prepare(&self, method: &str, params: Vec<Value>) -> (RequestId, Call) {
88        let id = self.next_id();
89        let request = helpers::build_request(id, method, params);
90        (id, request)
91    }
92
93    fn send(&self, id: RequestId, call: Call, options: CallOptions) -> Self::Out {
94        let (client, url) = self.new_request();
95        Box::pin(async move {
96            let output: Output = execute_rpc(&client, url, &Request::Single(call), id, options).await?;
97            helpers::to_result_from_output(output)
98        })
99    }
100
101    fn set_max_response_bytes(&mut self, v: u64) {
102        self.client.set_max_response_bytes(v);
103    }
104}
105
106impl BatchTransport for ICHttp {
107    type Batch = BoxFuture<'static, Result<Vec<RpcResult>>>;
108
109    fn send_batch<T>(&self, requests: T) -> Self::Batch
110    where
111        T: IntoIterator<Item = (RequestId, Call)>,
112    {
113        // Batch calls don't need an id but it helps associate the response log with the request log.
114        let id = self.next_id();
115        let (client, url) = self.new_request();
116        let (ids, calls): (Vec<_>, Vec<_>) = requests.into_iter().unzip();
117        Box::pin(async move {
118            let outputs: Vec<Output> =
119                execute_rpc(&client, url, &Request::Batch(calls), id, CallOptions::default()).await?;
120            handle_batch_response(&ids, outputs)
121        })
122    }
123}
124
125// According to the jsonrpc specification batch responses can be returned in any order so we need to
126// restore the intended order.
127fn handle_batch_response(ids: &[RequestId], outputs: Vec<Output>) -> Result<Vec<RpcResult>> {
128    if ids.len() != outputs.len() {
129        return Err(Error::InvalidResponse("unexpected number of responses".to_string()));
130    }
131    let mut outputs = outputs
132        .into_iter()
133        .map(|output| Ok((id_of_output(&output)?, helpers::to_result_from_output(output))))
134        .collect::<Result<HashMap<_, _>>>()?;
135    ids.iter()
136        .map(|id| {
137            outputs
138                .remove(id)
139                .ok_or_else(|| Error::InvalidResponse(format!("batch response is missing id {}", id)))
140        })
141        .collect()
142}
143
144fn id_of_output(output: &Output) -> Result<RequestId> {
145    let id = match output {
146        Output::Success(success) => &success.id,
147        Output::Failure(failure) => &failure.id,
148    };
149    match id {
150        jsonrpc_core::Id::Num(num) => Ok(*num as RequestId),
151        _ => Err(Error::InvalidResponse("response id is not u64".to_string())),
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    async fn server(req: hyper::Request<hyper::Body>) -> hyper::Result<hyper::Response<hyper::Body>> {
160        use hyper::body::HttpBody;
161
162        let expected = r#"{"jsonrpc":"2.0","method":"eth_getAccounts","params":[],"id":0}"#;
163        let response = r#"{"jsonrpc":"2.0","id":0,"result":"x"}"#;
164
165        assert_eq!(req.method(), &hyper::Method::POST);
166        assert_eq!(req.uri().path(), "/");
167        let mut content: Vec<u8> = vec![];
168        let mut body = req.into_body();
169        while let Some(Ok(chunk)) = body.data().await {
170            content.extend(&*chunk);
171        }
172        assert_eq!(std::str::from_utf8(&*content), Ok(expected));
173
174        Ok(hyper::Response::new(response.into()))
175    }
176
177    #[tokio::test]
178    async fn should_make_a_request() {
179        //use hyper::service::{make_service_fn, service_fn};
180        //
181        //// given
182        //let addr = "127.0.0.1:3001";
183        //// start server
184        //let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(server)) });
185        //let server = hyper::Server::server(&addr.parse().unwrap()).serve(service);
186        //tokio::spawn(async move {
187        //    println!("Listening on http://{}", addr);
188        //    server.await.unwrap();
189        //});
190        //
191        //// when
192        //let client = Http::new(&format!("http://{}", addr)).unwrap();
193        //println!("Sending request");
194        //let response = client.execute("eth_getAccounts", vec![]).await;
195        //println!("Got response");
196        //
197        //// then
198        //assert_eq!(response, Ok(Value::String("x".into())));
199    }
200
201    #[test]
202    fn handles_batch_response_being_in_different_order_than_input() {
203        let ids = vec![0, 1, 2];
204        // This order is different from the ids.
205        let outputs = [1u64, 0, 2]
206            .iter()
207            .map(|&id| {
208                Output::Success(jsonrpc_core::Success {
209                    jsonrpc: None,
210                    result: id.into(),
211                    id: jsonrpc_core::Id::Num(id),
212                })
213            })
214            .collect();
215        let results = handle_batch_response(&ids, outputs)
216            .unwrap()
217            .into_iter()
218            .map(|result| result.unwrap().as_u64().unwrap() as usize)
219            .collect::<Vec<_>>();
220        // The order of the ids should have been restored.
221        assert_eq!(ids, results);
222    }
223}