1use std::{fmt::Debug, path::PathBuf, sync::OnceLock, time::Duration};
2
3pub use bytes::Bytes;
4use eyre::{Report, eyre};
5use jiff::Timestamp;
6use reqwest::Url;
7pub use reqwest::{
8 Method, Request, RequestBuilder, StatusCode,
9 header::{self, HeaderMap},
10};
11use serde::Serialize;
12use tracing::{Span, debug, error, field::Empty, info, instrument, warn};
13
14use crate::{AuthError, UrlError};
15
16pub static USER_AGENT: &str = concat!("v_exchanges_api_generics/", env!("CARGO_PKG_VERSION"));
18
19#[derive(Clone, Debug, Default)]
24pub struct Client {
25 client: reqwest::Client,
26 pub config: RequestConfig,
27}
28
29impl Client {
30 #[instrument(skip_all, fields(?url, ?query, request_builder = Empty))] pub async fn request<Q, B, H>(&self, method: Method, url: &str, query: Option<&Q>, body: Option<B>, handler: &H) -> Result<H::Successful, RequestError>
38 where
39 Q: Serialize + ?Sized + std::fmt::Debug,
40 H: RequestHandler<B>, {
41 let config = &self.config;
42 config.verify();
43 let base_url = handler.base_url(config.use_testnet)?;
44 let url = base_url.join(url).map_err(|_| RequestError::Other(eyre!("Failed to parse provided URL")))?;
45 debug!(?config);
46
47 for i in 1..=config.max_tries {
48 let mut request_builder = self.client.request(method.clone(), url.clone()).timeout(config.timeout);
50 if let Some(query) = query {
51 request_builder = request_builder.query(query);
52 }
53 Span::current().record("request_builder", format!("{request_builder:?}"));
54
55 if config.use_testnet
56 && let Some(cache_duration) = config.cache_testnet_calls
57 {
58 let path = test_calls_path(&url, &query);
59 if let Ok(file) = std::fs::read_to_string(&path)
60 && path
61 .metadata()
62 .expect("already read the file, guaranteed to exist")
63 .modified()
64 .expect("switch OSes, you're on something stupid")
65 .elapsed()
66 .unwrap() < cache_duration
67 {
68 let body = Bytes::from(file);
69 let (status, headers) = (StatusCode::OK, header::HeaderMap::new()); return handler.handle_response(status, headers, body).map_err(RequestError::HandleResponse);
71 }
72 }
73
74 let request = handler.build_request(request_builder, &body, i).map_err(RequestError::BuildRequest)?;
76 match self.client.execute(request).await {
77 Ok(mut response) => {
78 let status = response.status();
79 let headers = std::mem::take(response.headers_mut());
80 debug!(?status, ?headers, "Received response headers");
81 let body: Bytes = match response.bytes().await {
82 Ok(b) => b,
83 Err(e) => {
84 error!(?status, ?headers, ?e, "Failed to read response body");
85 return Err(RequestError::ReceiveResponse(e));
86 }
87 };
88 {
89 let truncated_body = v_utils::utils::truncate_msg(std::str::from_utf8(&body)?.trim());
90 debug!(truncated_body);
91 }
92
93 match config.use_testnet {
94 true => {
95 let handled = handler.handle_response(status, headers.clone(), body.clone())?;
97 std::fs::write(test_calls_path(&url, &query), &body).ok();
98 return Ok(handled);
99 }
100 false => {
101 return handler.handle_response(status, headers.clone(), body.clone()).map_err(|e| {
102 error!(?status, ?headers, body = ?v_utils::utils::truncate_msg(std::str::from_utf8(&body).unwrap_or("<invalid utf8>")), "Failed to handle response");
103 RequestError::HandleResponse(e)
104 });
105 }
106 }
107 }
108 Err(e) =>
109 if i < config.max_tries && e.is_timeout() {
111 info!("Retrying sending request; made so far: {i}");
112 tokio::time::sleep(config.retry_cooldown).await;
113 } else {
114 warn!(?e);
115 debug!("{:?}\nAnd then trying the .is_timeout(): {}", e.status(), e.is_timeout());
116 return Err(RequestError::SendRequest(e));
117 },
118 }
119 }
120
121 unreachable!()
122 }
123
124 pub async fn get<Q, H>(&self, url: &str, query: &Q, handler: &H) -> Result<H::Successful, RequestError>
131 where
132 Q: Serialize + ?Sized + Debug,
133 H: RequestHandler<()>, {
134 self.request::<Q, (), H>(Method::GET, url, Some(query), None, handler).await
135 }
136
137 pub async fn get_no_query<H>(&self, url: &str, handler: &H) -> Result<H::Successful, RequestError>
139 where
140 H: RequestHandler<()>, {
141 self.request::<&[(&str, &str)], (), H>(Method::GET, url, None, None, handler).await
142 }
143
144 pub async fn post<B, H>(&self, url: &str, body: B, handler: &H) -> Result<H::Successful, RequestError>
150 where
151 H: RequestHandler<B>, {
152 self.request::<(), B, H>(Method::POST, url, None, Some(body), handler).await
153 }
154
155 pub async fn post_no_body<H>(&self, url: &str, handler: &H) -> Result<H::Successful, RequestError>
157 where
158 H: RequestHandler<()>, {
159 self.request::<(), (), H>(Method::POST, url, None, None, handler).await
160 }
161
162 pub async fn put<B, H>(&self, url: &str, body: B, handler: &H) -> Result<H::Successful, RequestError>
168 where
169 H: RequestHandler<B>, {
170 self.request::<(), B, H>(Method::PUT, url, None, Some(body), handler).await
171 }
172
173 pub async fn put_no_body<H>(&self, url: &str, handler: &H) -> Result<H::Successful, RequestError>
175 where
176 H: RequestHandler<()>, {
177 self.request::<(), (), H>(Method::PUT, url, None, None, handler).await
178 }
179
180 pub async fn delete<Q, H>(&self, url: &str, query: &Q, handler: &H) -> Result<H::Successful, RequestError>
187 where
188 Q: Serialize + ?Sized + Debug,
189 H: RequestHandler<()>, {
190 self.request::<Q, (), H>(Method::DELETE, url, Some(query), None, handler).await
191 }
192
193 pub async fn delete_no_query<H>(&self, url: &str, handler: &H) -> Result<H::Successful, RequestError>
195 where
196 H: RequestHandler<()>, {
197 self.request::<&[(&str, &str)], (), H>(Method::DELETE, url, None, None, handler).await
198 }
199}
200
201pub trait RequestHandler<B> {
203 type Successful;
205
206 #[allow(unused_variables)]
208 fn base_url(&self, is_test: bool) -> Result<url::Url, UrlError> {
209 Url::parse("").map_err(UrlError::Parse)
210 }
211
212 fn build_request(&self, builder: RequestBuilder, request_body: &Option<B>, attempt_count: u8) -> Result<Request, BuildError>;
217
218 fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes) -> Result<Self::Successful, HandleError>;
238}
239
240#[derive(Clone, Debug, Default)]
244pub struct RequestConfig {
245 pub max_tries: u8 = 1,
250 pub retry_cooldown: Duration = Duration::from_millis(500),
252 pub timeout: Duration = Duration::from_secs(3),
257
258 pub use_testnet: bool,
260 pub cache_testnet_calls: Option<Duration> = Some(Duration::from_days(30)),
262}
263
264impl RequestConfig {
265 fn verify(&self) {
266 assert_ne!(self.max_tries, 0, "RequestConfig.max_tries must not be equal to 0");
267 }
268}
269
270#[derive(Debug, derive_more::Display, thiserror::Error, derive_more::From)]
272pub enum HandleError {
273 Api(ApiError),
275 Parse(Report),
277}
278#[derive(Debug, thiserror::Error, derive_more::From)]
280pub enum ApiError {
281 #[error("IP has been timed out or banned until {until:?}")]
283 IpTimeout {
284 until: Option<Timestamp>,
286 },
287 #[error("{0}")]
289 Other(Report),
290}
291
292#[derive(Debug, thiserror::Error)]
294pub enum RequestError {
295 #[error("failed to send HTTP request: {0}")]
296 SendRequest(#[source] reqwest::Error),
297 #[error("failed to parse response body as UTF-8: {0}")]
298 Utf8Error(#[from] std::str::Utf8Error),
299 #[error("failed to receive HTTP response: {0}")]
300 ReceiveResponse(#[source] reqwest::Error),
301 #[error("handler failed to build a request: {0}")]
302 BuildRequest(#[from] BuildError),
303 #[error("handler failed to process the response: {0}")]
304 HandleResponse(#[from] HandleError),
305 #[error("{0}")]
306 Url(#[from] UrlError),
307 #[allow(missing_docs)]
309 #[error("{0}")]
310 Other(#[from] Report),
311}
312
313#[derive(Debug, derive_more::Display, thiserror::Error, derive_more::From)]
315pub enum BuildError {
316 Auth(AuthError),
318 UrlSerialization(serde_urlencoded::ser::Error),
320 JsonSerialization(serde_json::Error),
322 #[allow(missing_docs)]
326 Other(Report),
327}
328
329static TEST_CALLS_PATH: OnceLock<PathBuf> = OnceLock::new();
330fn test_calls_path<Q: Serialize>(url: &Url, query: &Option<Q>) -> PathBuf {
331 let base = TEST_CALLS_PATH.get_or_init(|| v_utils::xdg_cache_dir!("test_calls"));
332
333 let mut filename = url.to_string();
334 if query.is_some() {
335 filename.push('?');
336 filename.push_str(&serde_urlencoded::to_string(query).unwrap_or_default());
337 }
338 base.join(filename)
339}