rusty_bybit/
client.rs

1//! HTTP client for Bybit v5 API
2//!
3//! Handles all HTTP requests, authentication, and response parsing.
4//! Provides methods for public and authenticated API endpoints.
5//!
6//! # Authentication
7//!
8//! For authenticated endpoints, provide credentials via [`BybitClient::with_credentials`].
9//! Authentication uses HMAC-SHA256 signature generation.
10//!
11//! # Example
12//!
13//! ````rust,no_run
14//! use rusty_bybit::BybitClient;
15//!
16//! #[tokio::main]
17//! async fn main() {
18//!     let client = BybitClient::testnet();
19//!     let time = client.get_server_time().await.unwrap();
20//!     println!("Server time: {}", time.time_second);
21//! }
22//! ```
23
24use crate::auth::{Credentials, generate_signature, get_current_timestamp_ms};
25use crate::error::{BybitError, Result};
26use crate::types::ApiResponse;
27use reqwest::header::{HeaderMap, HeaderValue};
28
29const RECV_WINDOW: u64 = 5000;
30
31#[derive(Debug, Clone)]
32pub struct BybitClient {
33    pub base_url: String,
34    http_client: reqwest::Client,
35    credentials: Option<Credentials>,
36}
37
38impl BybitClient {
39    pub fn new(base_url: String) -> Self {
40        let http_client = reqwest::Client::builder()
41            .build()
42            .expect("Failed to create HTTP client");
43
44        Self {
45            base_url,
46            http_client,
47            credentials: None,
48        }
49    }
50
51    pub fn with_credentials(mut self, api_key: String, api_secret: String) -> Self {
52        self.credentials = Some(Credentials::new(api_key, api_secret));
53        self
54    }
55
56    pub fn testnet() -> Self {
57        Self::new("https://api-testnet.bybit.com".to_string())
58    }
59
60    pub fn mainnet() -> Self {
61        Self::new("https://api.bybit.com".to_string())
62    }
63
64    async fn request<T: serde::de::DeserializeOwned>(
65        &self,
66        method: &reqwest::Method,
67        path: &str,
68        query: Option<&[(&str, &str)]>,
69        body: Option<&serde_json::Value>,
70    ) -> Result<T> {
71        let url = format!("{}{}", self.base_url, path);
72
73        let mut builder = self.http_client.request(method.clone(), &url);
74
75        if let Some(q) = query {
76            builder = builder.query(q);
77        }
78
79        if let Some(creds) = &self.credentials {
80            let headers = self.build_auth_headers(method, path, query, body, creds)?;
81            builder = builder.headers(headers);
82        }
83
84        if let Some(b) = body {
85            builder = builder.json(b);
86        }
87
88        let response = builder.send().await?;
89        let response_text = response.text().await?;
90
91        let api_response: ApiResponse<T> = serde_json::from_str(&response_text)?;
92
93        if api_response.ret_code != 0 {
94            return Err(BybitError::ApiError {
95                ret_code: api_response.ret_code,
96                ret_msg: api_response.ret_msg,
97            });
98        }
99
100        Ok(api_response.result)
101    }
102
103    pub(crate) async fn get<T: serde::de::DeserializeOwned>(
104        &self,
105        path: &str,
106        query: Option<Vec<(&str, &str)>>,
107    ) -> Result<T> {
108        self.request(&reqwest::Method::GET, path, query.as_deref(), None)
109            .await
110    }
111
112    pub(crate) async fn post<T: serde::de::DeserializeOwned>(
113        &self,
114        path: &str,
115        body: Option<serde_json::Value>,
116    ) -> Result<T> {
117        self.request(&reqwest::Method::POST, path, None, body.as_ref())
118            .await
119    }
120
121    fn build_auth_headers(
122        &self,
123        method: &reqwest::Method,
124        _path: &str,
125        query: Option<&[(&str, &str)]>,
126        body: Option<&serde_json::Value>,
127        credentials: &Credentials,
128    ) -> Result<HeaderMap> {
129        let timestamp = get_current_timestamp_ms();
130
131        let payload = match *method {
132            reqwest::Method::GET => {
133                if let Some(q) = query {
134                    serde_urlencoded::to_string(q).unwrap_or_default()
135                } else {
136                    String::new()
137                }
138            }
139            reqwest::Method::POST => {
140                if let Some(b) = body {
141                    serde_json::to_string(b).unwrap_or_default()
142                } else {
143                    String::new()
144                }
145            }
146            _ => String::new(),
147        };
148
149        let signature = generate_signature(
150            timestamp,
151            &credentials.api_key,
152            RECV_WINDOW,
153            &payload,
154            &credentials.api_secret,
155        );
156
157        let mut headers = HeaderMap::new();
158        headers.insert(
159            "X-BAPI-API-KEY",
160            HeaderValue::try_from(credentials.api_key.as_str())
161                .map_err(|e| BybitError::InvalidParameter(e.to_string()))?,
162        );
163        headers.insert(
164            "X-BAPI-TIMESTAMP",
165            HeaderValue::try_from(timestamp.to_string().as_str())
166                .map_err(|e| BybitError::InvalidParameter(e.to_string()))?,
167        );
168        headers.insert(
169            "X-BAPI-SIGN",
170            HeaderValue::try_from(signature.as_str())
171                .map_err(|e| BybitError::InvalidParameter(e.to_string()))?,
172        );
173        headers.insert(
174            "X-BAPI-RECV-WINDOW",
175            HeaderValue::try_from(RECV_WINDOW.to_string().as_str())
176                .map_err(|e| BybitError::InvalidParameter(e.to_string()))?,
177        );
178        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
179
180        Ok(headers)
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_client_creation() {
190        let client = BybitClient::testnet();
191        assert_eq!(client.base_url, "https://api-testnet.bybit.com");
192
193        let client = BybitClient::mainnet();
194        assert_eq!(client.base_url, "https://api.bybit.com");
195    }
196
197    #[test]
198    fn test_client_with_credentials() {
199        let client = BybitClient::testnet()
200            .with_credentials("test_key".to_string(), "test_secret".to_string());
201        assert!(client.credentials.is_some());
202    }
203}