Skip to main content

bybit_api/
client.rs

1//! HTTP client for Bybit REST API.
2
3use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
4use serde::de::DeserializeOwned;
5use serde::Serialize;
6use tracing::{debug, warn};
7
8use crate::auth::{generate_signature, get_timestamp};
9use crate::config::ClientConfig;
10use crate::constants::*;
11use crate::error::{BybitError, Result};
12
13/// API response wrapper from Bybit.
14#[derive(Debug, serde::Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct ApiResponse<T> {
17    /// Return code (0 = success)
18    pub ret_code: i32,
19    /// Return message
20    pub ret_msg: String,
21    /// Response data
22    pub result: T,
23    /// Extended info
24    #[serde(default)]
25    #[allow(dead_code)]
26    pub ret_ext_info: serde_json::Value,
27    /// Server time
28    #[allow(dead_code)]
29    pub time: u64,
30}
31
32/// Bybit HTTP API client.
33#[derive(Debug, Clone)]
34pub struct BybitClient {
35    config: ClientConfig,
36    http: reqwest::Client,
37}
38
39impl BybitClient {
40    /// Create a new client with the given configuration.
41    pub fn new(config: ClientConfig) -> Result<Self> {
42        let http = reqwest::Client::builder()
43            .timeout(config.timeout)
44            .build()
45            .map_err(BybitError::Http)?;
46
47        Ok(Self { config, http })
48    }
49
50    /// Create a new client with API credentials using default settings.
51    pub fn with_credentials(
52        api_key: impl Into<String>,
53        api_secret: impl Into<String>,
54    ) -> Result<Self> {
55        let config = ClientConfig::builder(api_key, api_secret).build();
56        Self::new(config)
57    }
58
59    /// Create a new client for testnet.
60    pub fn testnet(api_key: impl Into<String>, api_secret: impl Into<String>) -> Result<Self> {
61        let config = ClientConfig::builder(api_key, api_secret)
62            .base_url(TESTNET)
63            .build();
64        Self::new(config)
65    }
66
67    /// Create a new client for demo environment.
68    pub fn demo(api_key: impl Into<String>, api_secret: impl Into<String>) -> Result<Self> {
69        let config = ClientConfig::builder(api_key, api_secret)
70            .base_url(DEMO)
71            .build();
72        Self::new(config)
73    }
74
75    /// Get the client configuration.
76    pub fn config(&self) -> &ClientConfig {
77        &self.config
78    }
79
80    /// Send a public GET request (no authentication).
81    pub async fn get_public<T: DeserializeOwned>(
82        &self,
83        endpoint: &str,
84        params: &[(&str, &str)],
85    ) -> Result<T> {
86        let url = format!("{}{}", self.config.base_url, endpoint);
87
88        let response = tokio::time::timeout(
89            self.config.timeout,
90            self.http.get(&url).query(params).send(),
91        )
92        .await
93        .map_err(|_| BybitError::Timeout)?
94        .map_err(BybitError::Http)?;
95
96        self.parse_response(response).await
97    }
98
99    /// Send an authenticated GET request.
100    pub async fn get<T: DeserializeOwned>(
101        &self,
102        endpoint: &str,
103        params: &[(&str, &str)],
104    ) -> Result<T> {
105        let url = format!("{}{}", self.config.base_url, endpoint);
106        let timestamp = get_timestamp();
107
108        // Build query string for signature
109        let query_string = params
110            .iter()
111            .map(|(k, v)| format!("{}={}", k, v))
112            .collect::<Vec<_>>()
113            .join("&");
114
115        let signature = generate_signature(
116            &self.config.api_secret,
117            timestamp,
118            &self.config.api_key,
119            self.config.recv_window,
120            &query_string,
121        );
122
123        let headers = self.build_auth_headers(timestamp, &signature);
124
125        let response = tokio::time::timeout(
126            self.config.timeout,
127            self.http.get(&url).query(params).headers(headers).send(),
128        )
129        .await
130        .map_err(|_| BybitError::Timeout)?
131        .map_err(BybitError::Http)?;
132
133        self.parse_response(response).await
134    }
135
136    /// Send an authenticated POST request.
137    pub async fn post<T: DeserializeOwned, B: Serialize>(
138        &self,
139        endpoint: &str,
140        body: &B,
141    ) -> Result<T> {
142        let url = format!("{}{}", self.config.base_url, endpoint);
143        let timestamp = get_timestamp();
144
145        let body_str = serde_json::to_string(body).map_err(|e| BybitError::Parse(e.to_string()))?;
146
147        let signature = generate_signature(
148            &self.config.api_secret,
149            timestamp,
150            &self.config.api_key,
151            self.config.recv_window,
152            &body_str,
153        );
154
155        let headers = self.build_auth_headers(timestamp, &signature);
156
157        if self.config.debug {
158            debug!("POST {} body: {}", url, body_str);
159        }
160
161        let response = tokio::time::timeout(
162            self.config.timeout,
163            self.http
164                .post(&url)
165                .headers(headers)
166                .header(CONTENT_TYPE, "application/json")
167                .body(body_str)
168                .send(),
169        )
170        .await
171        .map_err(|_| BybitError::Timeout)?
172        .map_err(BybitError::Http)?;
173
174        self.parse_response(response).await
175    }
176
177    /// Build authentication headers.
178    fn build_auth_headers(&self, timestamp: u64, signature: &str) -> HeaderMap {
179        let mut headers = HeaderMap::new();
180
181        headers.insert(
182            HEADER_API_KEY,
183            HeaderValue::from_str(&self.config.api_key)
184                .unwrap_or_else(|_| HeaderValue::from_static("")),
185        );
186        headers.insert(
187            HEADER_TIMESTAMP,
188            HeaderValue::from_str(&timestamp.to_string())
189                .unwrap_or_else(|_| HeaderValue::from_static("0")),
190        );
191        headers.insert(
192            HEADER_SIGN,
193            HeaderValue::from_str(signature).unwrap_or_else(|_| HeaderValue::from_static("")),
194        );
195        headers.insert(HEADER_SIGN_TYPE, HeaderValue::from_static("2"));
196        headers.insert(
197            HEADER_RECV_WINDOW,
198            HeaderValue::from_str(&self.config.recv_window.to_string())
199                .unwrap_or_else(|_| HeaderValue::from_static("5000")),
200        );
201
202        headers
203    }
204
205    /// Parse API response and handle errors.
206    async fn parse_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
207        let status = response.status();
208        let text = response.text().await.map_err(BybitError::Http)?;
209
210        if self.config.debug {
211            debug!("Response status: {}, body: {}", status, text);
212        }
213
214        if !status.is_success() {
215            // Try to parse as API error
216            if let Ok(api_resp) = serde_json::from_str::<ApiResponse<serde_json::Value>>(&text) {
217                return Err(BybitError::Api {
218                    code: api_resp.ret_code,
219                    msg: api_resp.ret_msg,
220                });
221            }
222            return Err(BybitError::Parse(format!(
223                "HTTP {} - {}",
224                status.as_u16(),
225                text
226            )));
227        }
228
229        // Parse successful response
230        let api_resp: ApiResponse<T> = serde_json::from_str(&text).map_err(|e| {
231            warn!("Failed to parse response: {}, body: {}", e, text);
232            BybitError::Parse(format!(
233                "JSON parse error: {} - body: {}",
234                e,
235                &text[..text.len().min(200)]
236            ))
237        })?;
238
239        // Check for API-level errors
240        if api_resp.ret_code != 0 {
241            return Err(BybitError::Api {
242                code: api_resp.ret_code,
243                msg: api_resp.ret_msg,
244            });
245        }
246
247        Ok(api_resp.result)
248    }
249}