1use 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}