Skip to main content

deribit_http/
client.rs

1//! HTTP client implementation for Deribit REST API
2
3use crate::auth::AuthManager;
4use crate::config::HttpConfig;
5use crate::error::HttpError;
6use crate::model::response::api_response::ApiResponse;
7use crate::model::types::AuthToken;
8use crate::rate_limit::{RateLimiter, categorize_endpoint};
9use crate::sync_compat::Mutex;
10use reqwest::Client;
11use serde::de::DeserializeOwned;
12use std::sync::Arc;
13
14/// HTTP client for Deribit REST API
15#[derive(Debug, Clone)]
16pub struct DeribitHttpClient {
17    /// HTTP client instance
18    client: Client,
19    /// Configuration
20    config: Arc<HttpConfig>,
21    /// Rate limiter
22    rate_limiter: RateLimiter,
23    /// Authentication manager
24    auth_manager: Arc<Mutex<AuthManager>>,
25}
26
27impl DeribitHttpClient {
28    /// Create a new HTTP client
29    pub fn new() -> Self {
30        let config = HttpConfig::default();
31        Self::with_config(config)
32    }
33
34    /// Create a new HTTP client with custom configuration
35    pub fn with_config(config: HttpConfig) -> Self {
36        let builder = Client::builder();
37
38        #[cfg(not(target_arch = "wasm32"))]
39        let builder = builder
40            .timeout(config.timeout)
41            .user_agent(&config.user_agent);
42
43        let client = builder.build().expect("Failed to create HTTP client");
44
45        let auth_manager = AuthManager::new(client.clone(), config.clone());
46
47        Self {
48            client,
49            config: Arc::new(config),
50            rate_limiter: RateLimiter::new(),
51            auth_manager: Arc::new(Mutex::new(auth_manager)),
52        }
53    }
54
55    /// Get the configuration
56    pub fn config(&self) -> &HttpConfig {
57        &self.config
58    }
59
60    /// Get the base URL
61    pub fn base_url(&self) -> &str {
62        self.config.base_url.as_str()
63    }
64
65    /// Get the HTTP client
66    pub fn http_client(&self) -> &Client {
67        &self.client
68    }
69
70    /// Make a rate-limited HTTP request
71    pub async fn make_request(&self, url: &str) -> Result<reqwest::Response, HttpError> {
72        // Determine rate limit category from URL
73        let category = categorize_endpoint(url);
74
75        // Wait for rate limit permission
76        self.rate_limiter.wait_for_permission(category).await;
77
78        // Make the request
79        self.client
80            .get(url)
81            .send()
82            .await
83            .map_err(|e| HttpError::NetworkError(e.to_string()))
84    }
85
86    /// Make an authenticated HTTP GET request for private endpoints
87    pub async fn make_authenticated_request(
88        &self,
89        url: &str,
90    ) -> Result<reqwest::Response, HttpError> {
91        // Determine rate limit category from URL
92        let category = categorize_endpoint(url);
93
94        // Wait for rate limit permission
95        self.rate_limiter.wait_for_permission(category).await;
96
97        // Get authorization header
98        let auth_header = {
99            let mut auth_manager = self.auth_manager.lock().await;
100            auth_manager
101                .get_authorization_header()
102                .await
103                .ok_or_else(|| {
104                    HttpError::AuthenticationFailed(
105                        "No valid authentication token available.".to_string(),
106                    )
107                })?
108        };
109
110        // Debug: log the authorization header being used
111        tracing::debug!("Using authorization header: {}", auth_header);
112
113        // Make the authenticated request
114        self.client
115            .get(url)
116            .header("Authorization", auth_header)
117            .send()
118            .await
119            .map_err(|e| HttpError::NetworkError(e.to_string()))
120    }
121
122    /// Make an authenticated HTTP POST request for private endpoints
123    pub async fn make_authenticated_post_request<T: serde::Serialize>(
124        &self,
125        url: &str,
126        body: &T,
127    ) -> Result<reqwest::Response, HttpError> {
128        // Determine rate limit category from URL
129        let category = categorize_endpoint(url);
130
131        // Wait for rate limit permission
132        self.rate_limiter.wait_for_permission(category).await;
133
134        // Get authorization header
135        let auth_header = {
136            let mut auth_manager = self.auth_manager.lock().await;
137            auth_manager
138                .get_authorization_header()
139                .await
140                .ok_or_else(|| {
141                    HttpError::AuthenticationFailed(
142                        "No valid authentication token available.".to_string(),
143                    )
144                })?
145        };
146
147        // Debug: log the authorization header being used
148        tracing::debug!("Using authorization header: {}", auth_header);
149
150        // Make the authenticated POST request
151        self.client
152            .post(url)
153            .header("Authorization", auth_header)
154            .json(body)
155            .send()
156            .await
157            .map_err(|e| HttpError::NetworkError(e.to_string()))
158    }
159
160    /// Get rate limiter for advanced usage
161    pub fn rate_limiter(&self) -> &RateLimiter {
162        &self.rate_limiter
163    }
164
165    /// Generic helper for public GET endpoints.
166    ///
167    /// Performs a rate-limited GET request to a public endpoint, parses the
168    /// API response, and extracts the result. Handles all standard error cases:
169    /// network errors, HTTP errors, API errors, and missing results.
170    ///
171    /// # Arguments
172    ///
173    /// * `endpoint` - The API endpoint path (e.g., "/public/get_currencies")
174    /// * `query` - Query string including leading "?" if non-empty, or empty string
175    ///
176    /// # Type Parameters
177    ///
178    /// * `T` - The expected result type, must implement `DeserializeOwned`
179    ///
180    /// # Errors
181    ///
182    /// Returns `HttpError` if the request fails at any stage.
183    pub async fn public_get<T>(&self, endpoint: &str, query: &str) -> Result<T, HttpError>
184    where
185        T: DeserializeOwned,
186    {
187        let url = format!("{}{}{}", self.base_url(), endpoint, query);
188
189        let response = self.make_request(&url).await?;
190
191        if !response.status().is_success() {
192            let error_text = response
193                .text()
194                .await
195                .unwrap_or_else(|_| "Unknown error".to_string());
196            return Err(HttpError::RequestFailed(error_text));
197        }
198
199        let api_response: ApiResponse<T> = response
200            .json()
201            .await
202            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
203
204        if let Some(error) = api_response.error {
205            return Err(HttpError::RequestFailed(format!(
206                "API error: {} - {}",
207                error.code, error.message
208            )));
209        }
210
211        api_response
212            .result
213            .ok_or_else(|| HttpError::InvalidResponse("No result in response".to_string()))
214    }
215
216    /// Generic helper for private GET endpoints.
217    ///
218    /// Performs a rate-limited, authenticated GET request to a private endpoint,
219    /// parses the API response, and extracts the result. Handles all standard
220    /// error cases: authentication errors, network errors, HTTP errors, API errors,
221    /// and missing results.
222    ///
223    /// # Arguments
224    ///
225    /// * `endpoint` - The API endpoint path (e.g., "/private/get_account_summary")
226    /// * `query` - Query string including leading "?" if non-empty, or empty string
227    ///
228    /// # Type Parameters
229    ///
230    /// * `T` - The expected result type, must implement `DeserializeOwned`
231    ///
232    /// # Errors
233    ///
234    /// Returns `HttpError` if the request fails at any stage.
235    pub async fn private_get<T>(&self, endpoint: &str, query: &str) -> Result<T, HttpError>
236    where
237        T: DeserializeOwned,
238    {
239        let url = format!("{}{}{}", self.base_url(), endpoint, query);
240
241        let response = self.make_authenticated_request(&url).await?;
242
243        if !response.status().is_success() {
244            let error_text = response
245                .text()
246                .await
247                .unwrap_or_else(|_| "Unknown error".to_string());
248            return Err(HttpError::RequestFailed(error_text));
249        }
250
251        let body = response.text().await.map_err(|e| {
252            HttpError::InvalidResponse(format!("Failed to read response body: {}", e))
253        })?;
254
255        let api_response: ApiResponse<T> = serde_json::from_str(&body).map_err(|e| {
256            tracing::error!(
257                error = %e,
258                endpoint = %endpoint,
259                body_preview = %&body[..body.len().min(1000)],
260                "Failed to deserialize private API response"
261            );
262            HttpError::InvalidResponse(format!(
263                "error decoding response body: {} - Raw (first 500 chars): {}",
264                e,
265                &body[..body.len().min(500)]
266            ))
267        })?;
268
269        if let Some(error) = api_response.error {
270            return Err(HttpError::RequestFailed(format!(
271                "API error: {} - {}",
272                error.code, error.message
273            )));
274        }
275
276        api_response
277            .result
278            .ok_or_else(|| HttpError::InvalidResponse("No result in response".to_string()))
279    }
280
281    /// Exchange refresh token for a new access token with different subject_id
282    pub async fn exchange_token(
283        &self,
284        refresh_token: &str,
285        subject_id: u64,
286        scope: Option<&str>,
287    ) -> Result<AuthToken, HttpError> {
288        let mut url = format!(
289            "{}/public/exchange_token?refresh_token={}&subject_id={}",
290            self.config.base_url,
291            urlencoding::encode(refresh_token),
292            subject_id
293        );
294
295        if let Some(scope) = scope {
296            url.push_str(&format!("&scope={}", urlencoding::encode(scope)));
297        }
298
299        let response = self
300            .client
301            .get(&url)
302            .header("Content-Type", "application/json")
303            .send()
304            .await
305            .map_err(|e| HttpError::NetworkError(e.to_string()))?;
306
307        if !response.status().is_success() {
308            let error_text = response
309                .text()
310                .await
311                .unwrap_or_else(|_| "Unknown error".to_string());
312            return Err(HttpError::AuthenticationFailed(format!(
313                "Token exchange failed: {}",
314                error_text
315            )));
316        }
317
318        // Parse the JSON-RPC response directly
319        let json_response: serde_json::Value = response
320            .json()
321            .await
322            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
323
324        // Check for JSON-RPC error
325        if let Some(_error) = json_response.get("error") {
326            return Err(HttpError::AuthenticationFailed(format!(
327                "Token exchange failed: {}",
328                json_response
329            )));
330        }
331
332        // Extract the result and parse as AuthToken
333        let result = json_response
334            .get("result")
335            .ok_or_else(|| HttpError::InvalidResponse("No result in response".to_string()))?;
336
337        let token: AuthToken = serde_json::from_value(result.clone())
338            .map_err(|e| HttpError::InvalidResponse(format!("Failed to parse token: {}", e)))?;
339
340        // Update the stored token
341        self.auth_manager.lock().await.update_token(token.clone());
342
343        Ok(token)
344    }
345
346    /// Fork a token to create a new session with the same permissions
347    pub async fn fork_token(
348        &self,
349        refresh_token: &str,
350        session_name: &str,
351        scope: Option<&str>,
352    ) -> Result<AuthToken, HttpError> {
353        let mut url = format!(
354            "{}/public/fork_token?refresh_token={}&session_name={}",
355            self.config.base_url,
356            urlencoding::encode(refresh_token),
357            urlencoding::encode(session_name)
358        );
359
360        if let Some(scope) = scope {
361            url.push_str(&format!("&scope={}", urlencoding::encode(scope)));
362        }
363
364        let response = self
365            .client
366            .get(&url)
367            .header("Content-Type", "application/json")
368            .send()
369            .await
370            .map_err(|e| HttpError::NetworkError(e.to_string()))?;
371
372        if !response.status().is_success() {
373            let error_text = response
374                .text()
375                .await
376                .unwrap_or_else(|_| "Unknown error".to_string());
377            return Err(HttpError::AuthenticationFailed(format!(
378                "Token fork failed: {}",
379                error_text
380            )));
381        }
382
383        // Parse the JSON-RPC response directly
384        let json_response: serde_json::Value = response
385            .json()
386            .await
387            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
388
389        // Check for JSON-RPC error
390        if let Some(_error) = json_response.get("error") {
391            return Err(HttpError::AuthenticationFailed(format!(
392                "Token fork failed: {}",
393                json_response
394            )));
395        }
396
397        // Extract the result and parse as AuthToken
398        let result = json_response
399            .get("result")
400            .ok_or_else(|| HttpError::InvalidResponse("No result in response".to_string()))?;
401
402        let token: AuthToken = serde_json::from_value(result.clone())
403            .map_err(|e| HttpError::InvalidResponse(format!("Failed to parse token: {}", e)))?;
404
405        self.auth_manager.lock().await.update_token(token.clone());
406
407        Ok(token)
408    }
409}
410
411impl Default for DeribitHttpClient {
412    fn default() -> Self {
413        Self::new()
414    }
415}