1use std::{fmt::Debug, path::PathBuf, sync::OnceLock, time::Duration};
2
3pub use bytes::Bytes;
4use jiff::Timestamp;
5use reqwest::Url;
6pub use reqwest::{
7 Method, Request, RequestBuilder, StatusCode,
8 header::{self, HeaderMap},
9};
10use v_utils::{prelude::*, xdg_cache_dir};
11
12use crate::{AuthError, UrlError};
13
14pub static USER_AGENT: &str = concat!("v_exchanges_api_generics/", env!("CARGO_PKG_VERSION"));
16
17#[derive(Clone, Debug, Default)]
22pub struct Client {
23 client: reqwest::Client,
24 pub config: RequestConfig,
25}
26
27impl Client {
28 #[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>
36 where
37 Q: Serialize + ?Sized + std::fmt::Debug,
38 H: RequestHandler<B>, {
39 let config = &self.config;
40 config.verify();
41 let base_url = handler.base_url(config.use_testnet)?;
42 let url = base_url.join(url).map_err(|_| RequestError::Other(eyre!("Failed to parse provided URL")))?;
43 debug!(?config);
44
45 for i in 1..=config.max_tries {
46 let mut request_builder = self.client.request(method.clone(), url.clone()).timeout(config.timeout);
48 if let Some(query) = query {
49 request_builder = request_builder.query(query);
50 }
51 Span::current().record("request_builder", format!("{request_builder:?}"));
52
53 if config.use_testnet
54 && let Some(cache_duration) = config.cache_testnet_calls
55 {
56 let path = test_calls_path(&url, &query);
57 if let Ok(file) = std::fs::read_to_string(&path)
58 && path
59 .metadata()
60 .expect("already read the file, guaranteed to exist")
61 .modified()
62 .expect("switch OSes, you're on something stupid")
63 .elapsed()
64 .unwrap() < cache_duration
65 {
66 let body = Bytes::from(file);
67 let (status, headers) = (StatusCode::OK, header::HeaderMap::new()); return handler.handle_response(status, headers, body).map_err(RequestError::HandleResponse);
69 }
70 }
71
72 let request = handler.build_request(request_builder, &body, i).map_err(RequestError::BuildRequest)?;
74 match self.client.execute(request).await {
75 Ok(mut response) => {
76 let status = response.status();
77 let headers = std::mem::take(response.headers_mut());
78 let body: Bytes = response.bytes().await.map_err(RequestError::ReceiveResponse)?;
79 {
80 let truncated_body = v_utils::utils::truncate_msg(std::str::from_utf8(&body)?.trim());
81 debug!(truncated_body);
82 }
83
84 match config.use_testnet {
85 true => {
86 let handled = handler.handle_response(status, headers.clone(), body.clone())?;
88 std::fs::write(test_calls_path(&url, &query), &body).ok();
89 return Ok(handled);
90 }
91 false => {
92 return handler.handle_response(status, headers, body).map_err(RequestError::HandleResponse);
93 }
94 }
95 }
96 Err(e) =>
97 if i < config.max_tries && e.is_timeout() {
99 info!("Retrying sending request; made so far: {i}");
100 tokio::time::sleep(config.retry_cooldown).await;
101 } else {
102 warn!(?e);
103 debug!("{:?}\nAnd then trying the .is_timeout(): {}", e.status(), e.is_timeout());
104 return Err(RequestError::SendRequest(e));
105 },
106 }
107 }
108
109 unreachable!()
110 }
111
112 pub async fn get<Q, H>(&self, url: &str, query: &Q, handler: &H) -> Result<H::Successful, RequestError>
119 where
120 Q: Serialize + ?Sized + Debug,
121 H: RequestHandler<()>, {
122 self.request::<Q, (), H>(Method::GET, url, Some(query), None, handler).await
123 }
124
125 pub async fn get_no_query<H>(&self, url: &str, handler: &H) -> Result<H::Successful, RequestError>
127 where
128 H: RequestHandler<()>, {
129 self.request::<&[(&str, &str)], (), H>(Method::GET, url, None, None, handler).await
130 }
131
132 pub async fn post<B, H>(&self, url: &str, body: B, handler: &H) -> Result<H::Successful, RequestError>
138 where
139 H: RequestHandler<B>, {
140 self.request::<(), B, H>(Method::POST, url, None, Some(body), handler).await
141 }
142
143 pub async fn post_no_body<H>(&self, url: &str, handler: &H) -> Result<H::Successful, RequestError>
145 where
146 H: RequestHandler<()>, {
147 self.request::<(), (), H>(Method::POST, url, None, None, handler).await
148 }
149
150 pub async fn put<B, H>(&self, url: &str, body: B, handler: &H) -> Result<H::Successful, RequestError>
156 where
157 H: RequestHandler<B>, {
158 self.request::<(), B, H>(Method::PUT, url, None, Some(body), handler).await
159 }
160
161 pub async fn put_no_body<H>(&self, url: &str, handler: &H) -> Result<H::Successful, RequestError>
163 where
164 H: RequestHandler<()>, {
165 self.request::<(), (), H>(Method::PUT, url, None, None, handler).await
166 }
167
168 pub async fn delete<Q, H>(&self, url: &str, query: &Q, handler: &H) -> Result<H::Successful, RequestError>
175 where
176 Q: Serialize + ?Sized + Debug,
177 H: RequestHandler<()>, {
178 self.request::<Q, (), H>(Method::DELETE, url, Some(query), None, handler).await
179 }
180
181 pub async fn delete_no_query<H>(&self, url: &str, handler: &H) -> Result<H::Successful, RequestError>
183 where
184 H: RequestHandler<()>, {
185 self.request::<&[(&str, &str)], (), H>(Method::DELETE, url, None, None, handler).await
186 }
187}
188
189pub trait RequestHandler<B> {
191 type Successful;
193
194 #[allow(unused_variables)]
196 fn base_url(&self, is_test: bool) -> Result<url::Url, UrlError> {
197 Url::parse("").map_err(UrlError::Parse)
198 }
199
200 fn build_request(&self, builder: RequestBuilder, request_body: &Option<B>, attempt_count: u8) -> Result<Request, BuildError>;
205
206 fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes) -> Result<Self::Successful, HandleError>;
226}
227
228#[derive(Clone, Debug, Default)]
232pub struct RequestConfig {
233 pub max_tries: u8 = 1,
238 pub retry_cooldown: Duration = Duration::from_millis(500),
240 pub timeout: Duration = Duration::from_secs(3),
245
246 pub use_testnet: bool,
248 pub cache_testnet_calls: Option<Duration> = Some(Duration::from_days(30)),
250}
251
252impl RequestConfig {
253 fn verify(&self) {
254 assert_ne!(self.max_tries, 0, "RequestConfig.max_tries must not be equal to 0");
255 }
256}
257
258#[derive(Debug, derive_more::Display, Error, derive_more::From)]
260pub enum HandleError {
261 Api(ApiError),
263 Parse(serde_json::Error),
265 #[allow(missing_docs)]
266 Other(Report),
267}
268#[derive(Debug, Error, derive_more::From)]
270pub enum ApiError {
271 #[error("IP has been timed out or banned until {until:?}")]
273 IpTimeout {
274 until: Option<Timestamp>,
276 },
277 #[error("{0}")]
279 Other(Report),
280}
281
282#[derive(Debug, thiserror::Error)]
284pub enum RequestError {
285 #[error("failed to send HTTP request: {0}")]
286 SendRequest(#[source] reqwest::Error),
287 #[error("failed to parse response body as UTF-8: {0}")]
288 Utf8Error(#[from] std::str::Utf8Error),
289 #[error("failed to receive HTTP response: {0}")]
290 ReceiveResponse(#[source] reqwest::Error),
291 #[error("handler failed to build a request: {0}")]
292 BuildRequest(#[from] BuildError),
293 #[error("handler failed to process the response: {0}")]
294 HandleResponse(#[from] HandleError),
295 #[error("{0}")]
296 Url(#[from] UrlError),
297 #[allow(missing_docs)]
299 #[error("{0}")]
300 Other(#[from] Report),
301}
302
303#[derive(Debug, derive_more::Display, thiserror::Error, derive_more::From)]
305pub enum BuildError {
306 Auth(AuthError),
308 UrlSerialization(serde_urlencoded::ser::Error),
310 JsonSerialization(serde_json::Error),
312 #[allow(missing_docs)]
316 Other(Report),
317}
318
319static TEST_CALLS_PATH: OnceLock<PathBuf> = OnceLock::new();
320fn test_calls_path<Q: Serialize>(url: &Url, query: &Option<Q>) -> PathBuf {
321 let base = TEST_CALLS_PATH.get_or_init(|| xdg_cache_dir!("test_calls"));
322
323 let mut filename = url.to_string();
324 if query.is_some() {
325 filename.push('?');
326 filename.push_str(&serde_urlencoded::to_string(query).unwrap_or_default());
327 }
328 base.join(filename)
329}