use crate::auth::AuthManager;
use crate::config::HttpConfig;
use crate::error::HttpError;
use crate::model::response::api_response::ApiResponse;
use crate::model::types::AuthToken;
use crate::rate_limit::{RateLimiter, categorize_endpoint};
use crate::sync_compat::Mutex;
use reqwest::Client;
use serde::de::DeserializeOwned;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct DeribitHttpClient {
client: Client,
config: Arc<HttpConfig>,
rate_limiter: RateLimiter,
auth_manager: Arc<Mutex<AuthManager>>,
}
impl DeribitHttpClient {
pub fn new() -> Self {
let config = HttpConfig::default();
Self::with_config(config)
}
pub fn with_config(config: HttpConfig) -> Self {
let builder = Client::builder();
#[cfg(not(target_arch = "wasm32"))]
let builder = builder
.timeout(config.timeout)
.user_agent(&config.user_agent);
let client = builder.build().expect("Failed to create HTTP client");
let auth_manager = AuthManager::new(client.clone(), config.clone());
Self {
client,
config: Arc::new(config),
rate_limiter: RateLimiter::new(),
auth_manager: Arc::new(Mutex::new(auth_manager)),
}
}
pub fn config(&self) -> &HttpConfig {
&self.config
}
pub fn base_url(&self) -> &str {
self.config.base_url.as_str()
}
pub fn http_client(&self) -> &Client {
&self.client
}
pub async fn make_request(&self, url: &str) -> Result<reqwest::Response, HttpError> {
let category = categorize_endpoint(url);
self.rate_limiter.wait_for_permission(category).await;
self.client
.get(url)
.send()
.await
.map_err(|e| HttpError::NetworkError(e.to_string()))
}
pub async fn make_authenticated_request(
&self,
url: &str,
) -> Result<reqwest::Response, HttpError> {
let category = categorize_endpoint(url);
self.rate_limiter.wait_for_permission(category).await;
let auth_header = {
let mut auth_manager = self.auth_manager.lock().await;
auth_manager
.get_authorization_header()
.await
.ok_or_else(|| {
HttpError::AuthenticationFailed(
"No valid authentication token available.".to_string(),
)
})?
};
tracing::debug!("Using authorization header: {}", auth_header);
self.client
.get(url)
.header("Authorization", auth_header)
.send()
.await
.map_err(|e| HttpError::NetworkError(e.to_string()))
}
pub async fn make_authenticated_post_request<T: serde::Serialize>(
&self,
url: &str,
body: &T,
) -> Result<reqwest::Response, HttpError> {
let category = categorize_endpoint(url);
self.rate_limiter.wait_for_permission(category).await;
let auth_header = {
let mut auth_manager = self.auth_manager.lock().await;
auth_manager
.get_authorization_header()
.await
.ok_or_else(|| {
HttpError::AuthenticationFailed(
"No valid authentication token available.".to_string(),
)
})?
};
tracing::debug!("Using authorization header: {}", auth_header);
self.client
.post(url)
.header("Authorization", auth_header)
.json(body)
.send()
.await
.map_err(|e| HttpError::NetworkError(e.to_string()))
}
pub fn rate_limiter(&self) -> &RateLimiter {
&self.rate_limiter
}
pub async fn public_get<T>(&self, endpoint: &str, query: &str) -> Result<T, HttpError>
where
T: DeserializeOwned,
{
let url = format!("{}{}{}", self.base_url(), endpoint, query);
let response = self.make_request(&url).await?;
if !response.status().is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(HttpError::RequestFailed(error_text));
}
let api_response: ApiResponse<T> = response
.json()
.await
.map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
if let Some(error) = api_response.error {
return Err(HttpError::RequestFailed(format!(
"API error: {} - {}",
error.code, error.message
)));
}
api_response
.result
.ok_or_else(|| HttpError::InvalidResponse("No result in response".to_string()))
}
pub async fn private_get<T>(&self, endpoint: &str, query: &str) -> Result<T, HttpError>
where
T: DeserializeOwned,
{
let url = format!("{}{}{}", self.base_url(), endpoint, query);
let response = self.make_authenticated_request(&url).await?;
if !response.status().is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(HttpError::RequestFailed(error_text));
}
let body = response.text().await.map_err(|e| {
HttpError::InvalidResponse(format!("Failed to read response body: {}", e))
})?;
let api_response: ApiResponse<T> = serde_json::from_str(&body).map_err(|e| {
tracing::error!(
error = %e,
endpoint = %endpoint,
body_preview = %&body[..body.len().min(1000)],
"Failed to deserialize private API response"
);
HttpError::InvalidResponse(format!(
"error decoding response body: {} - Raw (first 500 chars): {}",
e,
&body[..body.len().min(500)]
))
})?;
if let Some(error) = api_response.error {
return Err(HttpError::RequestFailed(format!(
"API error: {} - {}",
error.code, error.message
)));
}
api_response
.result
.ok_or_else(|| HttpError::InvalidResponse("No result in response".to_string()))
}
pub async fn exchange_token(
&self,
refresh_token: &str,
subject_id: u64,
scope: Option<&str>,
) -> Result<AuthToken, HttpError> {
let mut url = format!(
"{}/public/exchange_token?refresh_token={}&subject_id={}",
self.config.base_url,
urlencoding::encode(refresh_token),
subject_id
);
if let Some(scope) = scope {
url.push_str(&format!("&scope={}", urlencoding::encode(scope)));
}
let response = self
.client
.get(&url)
.header("Content-Type", "application/json")
.send()
.await
.map_err(|e| HttpError::NetworkError(e.to_string()))?;
if !response.status().is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(HttpError::AuthenticationFailed(format!(
"Token exchange failed: {}",
error_text
)));
}
let json_response: serde_json::Value = response
.json()
.await
.map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
if let Some(_error) = json_response.get("error") {
return Err(HttpError::AuthenticationFailed(format!(
"Token exchange failed: {}",
json_response
)));
}
let result = json_response
.get("result")
.ok_or_else(|| HttpError::InvalidResponse("No result in response".to_string()))?;
let token: AuthToken = serde_json::from_value(result.clone())
.map_err(|e| HttpError::InvalidResponse(format!("Failed to parse token: {}", e)))?;
self.auth_manager.lock().await.update_token(token.clone());
Ok(token)
}
pub async fn fork_token(
&self,
refresh_token: &str,
session_name: &str,
scope: Option<&str>,
) -> Result<AuthToken, HttpError> {
let mut url = format!(
"{}/public/fork_token?refresh_token={}&session_name={}",
self.config.base_url,
urlencoding::encode(refresh_token),
urlencoding::encode(session_name)
);
if let Some(scope) = scope {
url.push_str(&format!("&scope={}", urlencoding::encode(scope)));
}
let response = self
.client
.get(&url)
.header("Content-Type", "application/json")
.send()
.await
.map_err(|e| HttpError::NetworkError(e.to_string()))?;
if !response.status().is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(HttpError::AuthenticationFailed(format!(
"Token fork failed: {}",
error_text
)));
}
let json_response: serde_json::Value = response
.json()
.await
.map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
if let Some(_error) = json_response.get("error") {
return Err(HttpError::AuthenticationFailed(format!(
"Token fork failed: {}",
json_response
)));
}
let result = json_response
.get("result")
.ok_or_else(|| HttpError::InvalidResponse("No result in response".to_string()))?;
let token: AuthToken = serde_json::from_value(result.clone())
.map_err(|e| HttpError::InvalidResponse(format!("Failed to parse token: {}", e)))?;
self.auth_manager.lock().await.update_token(token.clone());
Ok(token)
}
}
impl Default for DeribitHttpClient {
fn default() -> Self {
Self::new()
}
}