1use 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#[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 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
61async 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 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
125fn 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 }
200
201 #[test]
202 fn handles_batch_response_being_in_different_order_than_input() {
203 let ids = vec![0, 1, 2];
204 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 assert_eq!(ids, results);
222 }
223}