1use 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
13const SENSITIVE_JSON_KEYS: &[&str] = &[
18 "secret",
19 "password",
20 "apiSecret",
21 "api_secret",
22 "apiKey",
23 "api_key",
24 "privateKey",
25 "private_key",
26 "token",
27 "sign",
28];
29
30fn mask_sensitive(json_body: &str) -> String {
37 let mut out = json_body.to_owned();
38 for key in SENSITIVE_JSON_KEYS {
39 let needle = format!("\"{}\":\"", key);
42 let needle_lower = needle.to_lowercase();
43 let mut cursor = 0usize;
44 loop {
45 let lower_remaining = out[cursor..].to_lowercase();
47 let Some(rel) = lower_remaining.find(&needle_lower) else {
48 break;
49 };
50 let start = cursor + rel + needle.len();
51 let Some(end_rel) = out[start..].find('"') else {
53 break;
54 };
55 let end = start + end_rel;
56 out.replace_range(start..end, "***REDACTED***");
57 cursor = start + "***REDACTED***".len();
58 }
59 }
60 out
61}
62
63fn encode_query(params: &[(&str, &str)]) -> String {
67 url::form_urlencoded::Serializer::new(String::new())
68 .extend_pairs(params.iter().copied())
69 .finish()
70}
71
72#[cfg(test)]
73mod mask_tests {
74 use super::mask_sensitive;
75
76 #[test]
77 fn masks_secret_field() {
78 let input = r#"{"apiKey":"k1","secret":"superSecretValue","other":"plain"}"#;
79 let out = mask_sensitive(input);
80 assert!(out.contains("***REDACTED***"));
81 assert!(!out.contains("superSecretValue"));
82 assert!(!out.contains("k1"));
83 assert!(out.contains("plain"));
84 }
85
86 #[test]
87 fn masks_password_case_insensitively() {
88 let input = r#"{"Password":"p@ss","apisecret":"x"}"#;
89 let out = mask_sensitive(input);
90 assert!(!out.contains("p@ss"));
91 assert!(!out.contains("\"x\""));
92 }
93
94 #[test]
95 fn leaves_unrelated_keys_intact() {
96 let input = r#"{"category":"linear","symbol":"BTCUSDT"}"#;
97 assert_eq!(mask_sensitive(input), input);
98 }
99}
100
101#[cfg(test)]
102mod query_tests {
103 use super::encode_query;
104
105 #[test]
106 fn empty_params_produce_empty_string() {
107 assert_eq!(encode_query(&[]), "");
108 }
109
110 #[test]
111 fn percent_encodes_base64url_cursor() {
112 let qs = encode_query(&[("cursor", "abc+/=def"), ("limit", "50")]);
116 assert_eq!(qs, "cursor=abc%2B%2F%3Ddef&limit=50");
117 }
118
119 #[test]
120 fn preserves_param_order() {
121 let qs = encode_query(&[("z", "1"), ("a", "2"), ("m", "3")]);
122 assert_eq!(qs, "z=1&a=2&m=3");
123 }
124
125 #[test]
126 fn encodes_ampersand_in_value() {
127 let qs = encode_query(&[("symbol", "BTCÐ")]);
128 assert_eq!(qs, "symbol=BTC%26ETH");
129 }
130}
131
132#[derive(Debug, serde::Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct ApiResponse<T> {
136 pub ret_code: i32,
138 pub ret_msg: String,
140 pub result: T,
142 #[serde(default)]
144 #[allow(dead_code)]
145 pub ret_ext_info: serde_json::Value,
146 #[allow(dead_code)]
148 pub time: u64,
149}
150
151#[derive(Debug, Clone)]
153pub struct BybitClient {
154 config: ClientConfig,
155 http: reqwest::Client,
156}
157
158impl BybitClient {
159 pub fn new(config: ClientConfig) -> Result<Self> {
161 let http = reqwest::Client::builder()
162 .timeout(config.timeout)
163 .build()
164 .map_err(BybitError::Http)?;
165
166 Ok(Self { config, http })
167 }
168
169 pub fn with_credentials(
171 api_key: impl Into<String>,
172 api_secret: impl Into<String>,
173 ) -> Result<Self> {
174 let config = ClientConfig::builder(api_key, api_secret).build();
175 Self::new(config)
176 }
177
178 pub fn testnet(api_key: impl Into<String>, api_secret: impl Into<String>) -> Result<Self> {
180 let config = ClientConfig::builder(api_key, api_secret)
181 .base_url(TESTNET)
182 .build();
183 Self::new(config)
184 }
185
186 pub fn demo(api_key: impl Into<String>, api_secret: impl Into<String>) -> Result<Self> {
188 let config = ClientConfig::builder(api_key, api_secret)
189 .base_url(DEMO)
190 .build();
191 Self::new(config)
192 }
193
194 pub fn config(&self) -> &ClientConfig {
196 &self.config
197 }
198
199 pub async fn get_public<T: DeserializeOwned>(
201 &self,
202 endpoint: &str,
203 params: &[(&str, &str)],
204 ) -> Result<T> {
205 let url = format!("{}{}", self.config.base_url, endpoint);
206
207 let response = tokio::time::timeout(
208 self.config.timeout,
209 self.http.get(&url).query(params).send(),
210 )
211 .await
212 .map_err(|_| BybitError::Timeout)?
213 .map_err(BybitError::Http)?;
214
215 self.parse_response(response).await
216 }
217
218 pub async fn get<T: DeserializeOwned>(
220 &self,
221 endpoint: &str,
222 params: &[(&str, &str)],
223 ) -> Result<T> {
224 let timestamp = get_timestamp();
225
226 let query_string = encode_query(params);
233
234 let signature = generate_signature(
235 &self.config.api_secret,
236 timestamp,
237 &self.config.api_key,
238 self.config.recv_window,
239 &query_string,
240 );
241
242 let url = if query_string.is_empty() {
243 format!("{}{}", self.config.base_url, endpoint)
244 } else {
245 format!("{}{}?{}", self.config.base_url, endpoint, query_string)
246 };
247
248 let headers = self.build_auth_headers(timestamp, &signature);
249
250 let response = tokio::time::timeout(
251 self.config.timeout,
252 self.http.get(&url).headers(headers).send(),
253 )
254 .await
255 .map_err(|_| BybitError::Timeout)?
256 .map_err(BybitError::Http)?;
257
258 self.parse_response(response).await
259 }
260
261 pub async fn post<T: DeserializeOwned, B: Serialize>(
263 &self,
264 endpoint: &str,
265 body: &B,
266 ) -> Result<T> {
267 let url = format!("{}{}", self.config.base_url, endpoint);
268 let timestamp = get_timestamp();
269
270 let body_str = serde_json::to_string(body).map_err(|e| BybitError::Parse(e.to_string()))?;
271
272 let signature = generate_signature(
273 &self.config.api_secret,
274 timestamp,
275 &self.config.api_key,
276 self.config.recv_window,
277 &body_str,
278 );
279
280 let headers = self.build_auth_headers(timestamp, &signature);
281
282 if self.config.debug {
283 debug!("POST {} body: {}", url, mask_sensitive(&body_str));
284 }
285
286 let response = tokio::time::timeout(
287 self.config.timeout,
288 self.http
289 .post(&url)
290 .headers(headers)
291 .header(CONTENT_TYPE, "application/json")
292 .body(body_str)
293 .send(),
294 )
295 .await
296 .map_err(|_| BybitError::Timeout)?
297 .map_err(BybitError::Http)?;
298
299 self.parse_response(response).await
300 }
301
302 fn build_auth_headers(&self, timestamp: u64, signature: &str) -> HeaderMap {
304 let mut headers = HeaderMap::new();
305
306 headers.insert(
307 HEADER_API_KEY,
308 HeaderValue::from_str(&self.config.api_key)
309 .unwrap_or_else(|_| HeaderValue::from_static("")),
310 );
311 headers.insert(
312 HEADER_TIMESTAMP,
313 HeaderValue::from_str(×tamp.to_string())
314 .unwrap_or_else(|_| HeaderValue::from_static("0")),
315 );
316 headers.insert(
317 HEADER_SIGN,
318 HeaderValue::from_str(signature).unwrap_or_else(|_| HeaderValue::from_static("")),
319 );
320 headers.insert(HEADER_SIGN_TYPE, HeaderValue::from_static("2"));
321 headers.insert(
322 HEADER_RECV_WINDOW,
323 HeaderValue::from_str(&self.config.recv_window.to_string())
324 .unwrap_or_else(|_| HeaderValue::from_static("5000")),
325 );
326
327 headers
328 }
329
330 async fn parse_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
332 let status = response.status();
333 let text = response.text().await.map_err(BybitError::Http)?;
334
335 if self.config.debug {
336 debug!(
337 "Response status: {}, body: {}",
338 status,
339 mask_sensitive(&text)
340 );
341 }
342
343 if !status.is_success() {
344 if let Ok(api_resp) = serde_json::from_str::<ApiResponse<serde_json::Value>>(&text) {
346 return Err(BybitError::Api {
347 code: api_resp.ret_code,
348 msg: api_resp.ret_msg,
349 });
350 }
351 return Err(BybitError::Parse(format!(
352 "HTTP {} - {}",
353 status.as_u16(),
354 text
355 )));
356 }
357
358 let api_resp: ApiResponse<T> = serde_json::from_str(&text).map_err(|e| {
360 warn!("Failed to parse response: {}, body: {}", e, text);
361 BybitError::Parse(format!(
362 "JSON parse error: {} - body: {}",
363 e,
364 &text[..text.len().min(200)]
365 ))
366 })?;
367
368 if api_resp.ret_code != 0 {
370 return Err(BybitError::Api {
371 code: api_resp.ret_code,
372 msg: api_resp.ret_msg,
373 });
374 }
375
376 Ok(api_resp.result)
377 }
378}