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 api_response: ApiResponse<T> = response
252            .json()
253            .await
254            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
255
256        if let Some(error) = api_response.error {
257            return Err(HttpError::RequestFailed(format!(
258                "API error: {} - {}",
259                error.code, error.message
260            )));
261        }
262
263        api_response
264            .result
265            .ok_or_else(|| HttpError::InvalidResponse("No result in response".to_string()))
266    }
267
268    /// Exchange refresh token for a new access token with different subject_id
269    pub async fn exchange_token(
270        &self,
271        refresh_token: &str,
272        subject_id: u64,
273        scope: Option<&str>,
274    ) -> Result<AuthToken, HttpError> {
275        let mut url = format!(
276            "{}/public/exchange_token?refresh_token={}&subject_id={}",
277            self.config.base_url,
278            urlencoding::encode(refresh_token),
279            subject_id
280        );
281
282        if let Some(scope) = scope {
283            url.push_str(&format!("&scope={}", urlencoding::encode(scope)));
284        }
285
286        let response = self
287            .client
288            .get(&url)
289            .header("Content-Type", "application/json")
290            .send()
291            .await
292            .map_err(|e| HttpError::NetworkError(e.to_string()))?;
293
294        if !response.status().is_success() {
295            let error_text = response
296                .text()
297                .await
298                .unwrap_or_else(|_| "Unknown error".to_string());
299            return Err(HttpError::AuthenticationFailed(format!(
300                "Token exchange failed: {}",
301                error_text
302            )));
303        }
304
305        // Parse the JSON-RPC response directly
306        let json_response: serde_json::Value = response
307            .json()
308            .await
309            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
310
311        // Check for JSON-RPC error
312        if let Some(_error) = json_response.get("error") {
313            return Err(HttpError::AuthenticationFailed(format!(
314                "Token exchange failed: {}",
315                json_response
316            )));
317        }
318
319        // Extract the result and parse as AuthToken
320        let result = json_response
321            .get("result")
322            .ok_or_else(|| HttpError::InvalidResponse("No result in response".to_string()))?;
323
324        let token: AuthToken = serde_json::from_value(result.clone())
325            .map_err(|e| HttpError::InvalidResponse(format!("Failed to parse token: {}", e)))?;
326
327        // Update the stored token
328        self.auth_manager.lock().await.update_token(token.clone());
329
330        Ok(token)
331    }
332
333    /// Fork a token to create a new session with the same permissions
334    pub async fn fork_token(
335        &self,
336        refresh_token: &str,
337        session_name: &str,
338        scope: Option<&str>,
339    ) -> Result<AuthToken, HttpError> {
340        let mut url = format!(
341            "{}/public/fork_token?refresh_token={}&session_name={}",
342            self.config.base_url,
343            urlencoding::encode(refresh_token),
344            urlencoding::encode(session_name)
345        );
346
347        if let Some(scope) = scope {
348            url.push_str(&format!("&scope={}", urlencoding::encode(scope)));
349        }
350
351        let response = self
352            .client
353            .get(&url)
354            .header("Content-Type", "application/json")
355            .send()
356            .await
357            .map_err(|e| HttpError::NetworkError(e.to_string()))?;
358
359        if !response.status().is_success() {
360            let error_text = response
361                .text()
362                .await
363                .unwrap_or_else(|_| "Unknown error".to_string());
364            return Err(HttpError::AuthenticationFailed(format!(
365                "Token fork failed: {}",
366                error_text
367            )));
368        }
369
370        // Parse the JSON-RPC response directly
371        let json_response: serde_json::Value = response
372            .json()
373            .await
374            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
375
376        // Check for JSON-RPC error
377        if let Some(_error) = json_response.get("error") {
378            return Err(HttpError::AuthenticationFailed(format!(
379                "Token fork failed: {}",
380                json_response
381            )));
382        }
383
384        // Extract the result and parse as AuthToken
385        let result = json_response
386            .get("result")
387            .ok_or_else(|| HttpError::InvalidResponse("No result in response".to_string()))?;
388
389        let token: AuthToken = serde_json::from_value(result.clone())
390            .map_err(|e| HttpError::InvalidResponse(format!("Failed to parse token: {}", e)))?;
391
392        self.auth_manager.lock().await.update_token(token.clone());
393
394        Ok(token)
395    }
396}
397
398impl Default for DeribitHttpClient {
399    fn default() -> Self {
400        Self::new()
401    }
402}