Skip to main content

bybit/
client.rs

1use crate::prelude::*;
2use log::trace;
3use std::time::Duration;
4
5/// The main client struct that wraps the reqwest client.
6///
7/// It stores the API key, secret key, and host to make requests to the Bybit API.
8#[derive(Clone)]
9pub struct Client {
10    /// The API key for the Bybit account.
11    pub api_key: String,
12
13    /// The secret key for the Bybit account.
14    pub secret_key: String,
15
16    /// The host to make requests to.
17    pub host: String,
18
19    /// The reqwest client that makes the HTTP requests.
20    pub inner_client: ReqwestClient,
21}
22
23impl Client {
24    /// Create a new instance of `Client`.
25    ///
26    /// # Arguments
27    ///
28    /// * `api_key` - The API key for the Bybit account. It can be `None` if the client is not for authenticated requests.
29    /// * `secret_key` - The secret key for the Bybit account. It can be `None` if the client is not for authenticated requests.
30    /// * `host` - The host to make requests to.
31    ///
32    /// # Returns
33    ///
34    /// A new instance of `Client`.
35    pub fn new(api_key: Option<String>, secret_key: Option<String>, host: String) -> Self {
36        // Create a new instance of the reqwest client.
37        let inner_client = ReqwestClient::builder()
38            .build()
39            .expect("Failed to build reqwest client");
40
41        // Create a new instance of `Client` with the provided arguments.
42        Client {
43            // Set the API key. If `api_key` is `None`, set it to an empty string.
44            api_key: match api_key {
45                Some(api_key) => api_key,
46                None => "".into(),
47            },
48            // Set the secret key. If `secret_key` is `None`, set it to an empty string.
49            secret_key: match secret_key {
50                Some(secret_key) => secret_key,
51                None => "".into(),
52            },
53            // Set the host.
54            host,
55            // Set the reqwest client.
56            inner_client,
57        }
58    }
59
60    /// Makes an unsigned HTTP GET request to the specified endpoint.
61    ///
62    /// # Arguments
63    ///
64    /// * `endpoint` - The endpoint to make the request to.
65    /// * `request` - The query string to append to the URL.
66    ///
67    /// # Returns
68    ///
69    /// A `Result` containing the response deserialized to the specified type `T`.
70    pub async fn get<T: DeserializeOwned + Send + 'static>(
71        &self,
72        endpoint: API,
73        request: Option<String>,
74    ) -> Result<T, BybitError> {
75        // Construct the full URL
76        let mut url = format!("{}/{}", self.host, endpoint.as_ref());
77        // If there is a query string, append it to the URL
78        if let Some(request) = request {
79            if !request.is_empty() {
80                url.push('?');
81                url.push_str(&request);
82            }
83        }
84
85        // Make the request using the reqwest client
86        let response = self.inner_client.get(url).send().await?;
87        // Handle the response using the `handler` method
88        self.handler(response).await
89    }
90
91    /// Makes a signed HTTP GET request to the specified endpoint.
92    ///
93    /// # Arguments
94    ///
95    /// * `endpoint` - The endpoint to make the request to.
96    /// * `recv_window` - The receive window for the request in milliseconds.
97    /// * `request` - The query string to append to the URL.
98    ///
99    /// # Returns
100    ///
101    /// A `Result` containing the response deserialized to the specified type `T`.
102    pub async fn get_signed<T: DeserializeOwned + Send + 'static>(
103        &self,
104        endpoint: API,
105        recv_window: u16,
106        request: Option<String>,
107    ) -> Result<T, BybitError> {
108        // Construct the full URL
109        let mut url: String = format!("{}/{}", self.host, endpoint.as_ref());
110        // If there is a query string, append it to the URL
111        let query_string = request.unwrap_or_default();
112        if !query_string.is_empty() {
113            url.push_str(format!("?{}", query_string).as_str());
114        }
115
116        // Sign the request, passing the query string for signature
117        // The request is signed with the API secret key and requires
118        // the `recv_window` for the request to be within the specified timeframe.
119        let headers = self.build_signed_headers(false, true, recv_window, Some(query_string))?;
120
121        // Make the signed HTTP GET request
122        let client = &self.inner_client;
123        let response = client.get(url.as_str()).headers(headers).send().await?;
124
125        // Handle the response
126        self.handler(response).await
127    }
128
129    /// Makes an unsigned HTTP POST request to the specified endpoint.
130    ///
131    /// # Arguments
132    ///
133    /// * `endpoint` - The endpoint to make the request to.
134    /// * `request` - The query string to append to the URL. Only used if provided.
135    ///
136    /// # Returns
137    ///
138    /// A `Result` containing the response deserialized to the specified type `T`.
139    pub async fn post<T: DeserializeOwned + Send + 'static>(
140        &self,
141        endpoint: API,
142        request: Option<String>,
143    ) -> Result<T, BybitError> {
144        // Construct the URL by appending the base host and endpoint to it
145        let mut url: String = format!("{}/{}", self.host, endpoint.as_ref());
146
147        // If a request is provided, append it to the URL as a query string
148        if let Some(request) = request {
149            if !request.is_empty() {
150                url.push_str(format!("?{}", request).as_str());
151            }
152        }
153
154        // Get a reference to the inner client
155        let client = &self.inner_client;
156
157        // Send the POST request to the constructed URL
158        let response = client.post(url.as_str()).send().await?;
159
160        // Handle the response by passing it to the handler method
161        self.handler(response).await
162    }
163
164    /// Makes a signed HTTP POST request to the specified endpoint.
165    ///
166    /// # Arguments
167    ///
168    /// * `endpoint` - The endpoint to make the request to.
169    /// * `recv_window` - The receive window for the request in milliseconds.
170    /// * `raw_request_body` - The raw request body to sign. Only used if provided.
171    ///
172    /// # Returns
173    ///
174    /// A `Result` containing the response deserialized to the specified type `T`.
175    pub async fn post_signed<T: DeserializeOwned + Send + 'static>(
176        &self,
177        endpoint: API,
178        recv_window: u16,
179        raw_request_body: Option<String>,
180    ) -> Result<T, BybitError> {
181        // Construct the full URL
182        let url = format!("{}{}", self.host, endpoint.as_ref());
183
184        // Sign the request, passing the raw request body for signature
185        // The request is signed with the API secret key and requires
186        // the `recv_window` for the request to be within the specified timeframe.
187        let headers =
188            self.build_signed_headers(true, true, recv_window, raw_request_body.clone())?;
189
190        // Make the signed HTTP POST request
191        let client = &self.inner_client;
192        let response = client
193            .post(url)
194            .headers(headers)
195            .body(raw_request_body.unwrap_or_default())
196            .send()
197            .await?;
198
199        // Handle the response
200        self.handler(response).await
201    }
202
203    /// Builds the signed headers for an HTTP request.
204    ///
205    /// # Arguments
206    ///
207    /// * `content_type` - Whether to include the `Content-Type` header.
208    /// * `signed` - Whether to include the signature in the headers.
209    /// * `recv_window` - The receive window for the request in milliseconds.
210    /// * `request` - The request body to sign.
211    ///
212    /// # Returns
213    ///
214    /// A `Result` containing the signed headers.
215    fn build_signed_headers<'str>(
216        &self,
217        content_type: bool,
218        signed: bool,
219        recv_window: u16,
220        request: Option<String>,
221    ) -> Result<HeaderMap, BybitError> {
222        // Initialize the custom headers map
223        let mut custom_headers = HeaderMap::new();
224        // Set the User-Agent header
225        custom_headers.insert(USER_AGENT, HeaderValue::from_static("bybit-rs"));
226        // Get the current timestamp
227        let timestamp = get_timestamp().to_string();
228        // Get the receive window
229        let window = recv_window.to_string();
230        // Sign the request
231        let signature = self.sign_message(&timestamp, &window, request)?;
232
233        // Set the headers
234        let signature_header = HeaderName::from_static("x-bapi-sign");
235        let api_key_header = HeaderName::from_static("x-bapi-api-key");
236        let timestamp_header = HeaderName::from_static("x-bapi-timestamp");
237        let recv_window_header = HeaderName::from_static("x-bapi-recv-window");
238
239        if signed {
240            // Insert the signature header
241            custom_headers.insert(
242                signature_header,
243                HeaderValue::from_str(&signature.to_owned())?,
244            );
245            // Insert the API key header
246            custom_headers.insert(
247                api_key_header,
248                HeaderValue::from_str(&self.api_key.to_owned())?,
249            );
250        }
251        // Insert the timestamp header
252        custom_headers.insert(
253            timestamp_header,
254            HeaderValue::from_str(&timestamp.to_owned())?,
255        );
256        // Insert the receive window header
257        custom_headers.insert(
258            recv_window_header,
259            HeaderValue::from_str(&window.to_owned())?,
260        );
261        // Insert the Content-Type header if required
262        if content_type {
263            custom_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
264        }
265        // Return the signed headers
266        Ok(custom_headers).map_err(BybitError::ReqError)
267    }
268
269    fn mac_from_secret_key(&self) -> Result<Hmac<Sha256>, BybitError> {
270        Hmac::<Sha256>::new_from_slice(self.secret_key.as_bytes())
271            .map_err(|e| BybitError::Base(format!("Failed to create Hmac, error: {:?}", e)))
272    }
273
274    /// Signs a POST request message.
275    ///
276    /// # Arguments
277    ///
278    /// * `timestamp` - The timestamp of the request.
279    /// * `recv_window` - The receive window of the request.
280    /// * `request` - The request body as an optional string.
281    ///
282    /// # Returns
283    ///
284    /// The signed message as a hex-encoded string.
285    ///
286    /// # Description
287    ///
288    /// This function takes the timestamp, receive window, and an optional request body as input.
289    /// It creates a string by concatenating the timestamp, API key, and receive window.
290    /// If a request body is provided, it appends it to the sign message.
291    /// The function then uses the HMAC-SHA256 algorithm to sign the message.
292    /// The result is hex-encoded and returned as a string.
293    fn sign_message(
294        &self,
295        timestamp: &str,
296        recv_window: &str,
297        request: Option<String>,
298    ) -> Result<String, BybitError> {
299        // Create a new HMAC SHA256 instance with the secret key
300        let mut mac = self.mac_from_secret_key()?;
301
302        // Create the sign message by concatenating the timestamp, API key, and receive window
303        let mut sign_message = format!("{}{}{}", timestamp, self.api_key, recv_window);
304
305        // If a request body is provided, append it to the sign message
306        if let Some(req) = request {
307            sign_message.push_str(&req);
308        }
309
310        // Update the MAC with the sign message
311        mac.update(sign_message.as_bytes());
312
313        // Finalize the MAC and encode the result as a hex string
314        let hex_signature = hex_encode(mac.finalize().into_bytes());
315
316        Ok(hex_signature)
317    }
318
319    /// Internal function to sign a POST request message.
320    ///
321    /// # Arguments
322    ///
323    /// * `timestamp` - The timestamp of the request.
324    /// * `recv_window` - The receive window of the request.
325    /// * `request` - The request body as an optional string.
326    ///
327    /// # Returns
328    ///
329    /// The signed message as a hex-encoded string.
330    fn _sign_post_message(
331        &self,
332        timestamp: &str,
333        recv_window: &str,
334        request: Option<String>,
335    ) -> Result<String, BybitError> {
336        // Create a new HMAC SHA256 instance with the secret key
337        let mut mac = self.mac_from_secret_key()?;
338
339        // Update the MAC with the timestamp
340        mac.update(timestamp.as_bytes());
341        // Update the MAC with the API key
342        mac.update(self.api_key.as_bytes());
343        // Update the MAC with the receive window
344        mac.update(recv_window.as_bytes());
345        // Update the MAC with the request body, if provided
346        if let Some(req) = request {
347            mac.update(req.as_bytes());
348        }
349
350        // Finalize the MAC and encode the result as a hex string
351        let hex_signature = hex_encode(mac.finalize().into_bytes());
352
353        Ok(hex_signature)
354    }
355
356    /// Internal function to handle the response from a HTTP request.
357    ///
358    /// # Arguments
359    ///
360    /// * `response` - The HTTP response from the request.
361    ///
362    /// # Returns
363    ///
364    /// The result of deserializing the response body into a specific type.
365    /// Returns `Ok(T)` if the response status is `StatusCode::OK`,
366
367    /// returns `Err(BybitError::BybitError(BybitContentError))` if the response
368    /// status is `StatusCode::BAD_REQUEST`, returns `Err(BybitError::InternalServerError)`
369    /// if the response status is `StatusCode::INTERNAL_SERVER_ERROR`,
370
371    /// returns `Err(BybitError::ServiceUnavailable)` if the response status is
372    /// `StatusCode::SERVICE_UNAVAILABLE`, returns `Err(BybitError::Unauthorized)`
373    /// if the response status is `StatusCode::UNAUTHORIZED`, and returns
374    /// `Err(BybitError::StatusCode(status))` if the response status is any other
375    /// value.
376
377    async fn handler<T: DeserializeOwned + Send + 'static>(
378        &self,
379        response: ReqwestResponse,
380    ) -> Result<T, BybitError> {
381        // Match the status code of the response
382        let status = response.status();
383        let response_text = &response.text().await.map_err(BybitError::from)?;
384        match status {
385            StatusCode::OK => {
386                // Deserialize the entire response into BybitApiResponseOpaquePayload first.
387                let wrapper: BybitApiResponseOpaquePayload = serde_json::from_str(response_text)
388                    .map_err(|e| {
389                        BybitError::Base(format!(
390                            "Failed to parse response wrapper: {} | Response: {}",
391                            e, response_text
392                        ))
393                    })?;
394
395                match wrapper.ret_code {
396                    0 => {
397                        // If ret_code is 0, the operation was successful at the API level.
398                        // The actual data is in `response_json.result` (a serde_json::Value).
399                        // Deserialize this `Value` into the target type `T`.
400                        serde_json::from_str(response_text).map_err(|e| {
401                            BybitError::Base(format!(
402                                "Failed to parse full response into {}: {} | Response: {}",
403                                type_name::<T>(),
404                                e,
405                                response_text
406                            ))
407                        })
408                    }
409                    _ => {
410                        // If ret_code is non-zero, it's an API error.
411                        Err(BybitError::BybitError(BybitContentError {
412                            code: wrapper.ret_code,
413                            msg: wrapper.ret_msg,
414                        }))
415                    }
416                }
417            }
418            StatusCode::BAD_REQUEST => {
419                let error: BybitContentError =
420                    serde_json::from_str(response_text).map_err(BybitError::from)?;
421                Err(BybitError::BybitError(error))
422            }
423            StatusCode::INTERNAL_SERVER_ERROR => Err(BybitError::InternalServerError),
424            StatusCode::SERVICE_UNAVAILABLE => Err(BybitError::ServiceUnavailable),
425            StatusCode::UNAUTHORIZED => Err(BybitError::Unauthorized),
426            status => Err(BybitError::StatusCode(status.as_u16())),
427        }
428    }
429
430    /// Connects to the Bybit WebSocket endpoint and sends an authentication message.
431    ///
432    /// # Arguments
433    ///
434    /// * `endpoint` - The WebSocket endpoint to connect to.
435    /// * `request_body` - An optional request body to send after authenticating.
436    /// * `private` - A boolean indicating whether to send the authentication message.
437    /// * `alive_dur` - An optional duration in seconds to set the `alive` field of the
438    ///   authentication message to.
439    ///
440    /// # Returns
441    ///
442    /// Returns a `Result` containing a `WebSocketStream` if the connection and authentication
443    /// are successful, or a `BybitError` if an error occurs.
444    pub async fn wss_connect(
445        &self,
446        endpoint: WebsocketAPI,
447        request_body: Option<String>,
448        private: bool,
449        alive_dur: Option<u16>,
450    ) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>, BybitError> {
451        // Construct the WebSocket URL
452        let unparsed_url = format!("{}{}", self.host, endpoint.as_ref()).to_string();
453        let url = WsUrl::parse(unparsed_url.as_str())?;
454
455        // Calculate the expiration time for the authentication message
456        let expiry_time = alive_dur.unwrap_or(9) as u64 * 1000 * 60;
457        let expires = get_timestamp() + expiry_time;
458
459        // Calculate the signature for the authentication message
460        let mut mac = self.mac_from_secret_key()?;
461        mac.update(format!("GET/realtime{expires}").as_bytes());
462        let signature = hex_encode(mac.finalize().into_bytes());
463
464        // Generate a random UUID for the request ID
465        let uuid = generate_random_uid(5);
466
467        // Connect to the WebSocket endpoint
468        match connect_async(url.as_ref()).await {
469            // If the connection is successful, send the authentication message
470            Ok((mut ws_stream, _)) => {
471                let auth_msg = json!({
472                    "req_id": uuid,
473                    "op": "auth",
474                    "args": [self.api_key, expires, signature]
475                });
476
477                if private {
478                    // Send the authentication message if `private` is true
479                    ws_stream
480                        .send(WsMessage::Text(auth_msg.to_string()))
481                        .await?;
482
483                    // Wait for authentication response with timeout
484                    let auth_response = tokio::time::timeout(
485                        Duration::from_secs(5),
486                        wait_for_auth_response(&mut ws_stream, endpoint),
487                    )
488                    .await
489                    .map_err(|_| BybitError::Base("Authentication timeout".to_string()))??;
490
491                    // Check if authentication was successful
492                    if auth_response.is_failure() {
493                        return Err(BybitError::Base(format!(
494                            "Authentication failed: {} (code: {:?})",
495                            auth_response.ret_msg(),
496                            auth_response.error_code()
497                        )));
498                    }
499
500                    trace!(
501                        "WebSocket authentication successful: {}",
502                        auth_response.conn_id()
503                    );
504                }
505
506                // Send the request body if it is not empty
507                let request = request_body.unwrap_or_default();
508                if !request.is_empty() {
509                    ws_stream.send(WsMessage::Text(request)).await?;
510                }
511                Ok(ws_stream)
512            }
513            // If the connection fails, return a BybitError
514            Err(err) => Err(BybitError::Tungstenite(err)),
515        }
516    }
517}
518
519/// Waits for and parses an authentication response from a WebSocket stream.
520///
521/// This function reads messages from the WebSocket stream until it receives
522/// an authentication response, then parses it into an `AuthResponse`.
523async fn wait_for_auth_response(
524    ws_stream: &mut WebSocketStream<MaybeTlsStream<TcpStream>>,
525    endpoint: WebsocketAPI,
526) -> Result<AuthResponse, BybitError> {
527    use futures::StreamExt;
528
529    loop {
530        match ws_stream.next().await {
531            Some(Ok(WsMessage::Text(msg))) => {
532                // Try to parse as AuthResponse
533                if let Ok(auth_response) = serde_json::from_str::<AuthResponse>(&msg) {
534                    return Ok(auth_response);
535                }
536
537                // If it's not an auth response, check if it's a subscription response
538                // (for public streams that don't require auth but send subscription confirmation)
539                let value: Value = serde_json::from_str(&msg)
540                    .map_err(|e| BybitError::Base(format!("Failed to parse message: {}", e)))?;
541
542                // Check if this is a subscription response for public streams
543                if let Some(op) = value.get("op").and_then(|v| v.as_str()) {
544                    if op == "subscribe" {
545                        // This is a subscription confirmation, not an auth response
546                        // For public streams, this is expected
547                        if endpoint == WebsocketAPI::TradeStream {
548                            // Trade stream should send auth response, not subscribe
549                            continue;
550                        }
551                        // For public non-trade streams, create a success response
552                        let conn_id = value
553                            .get("conn_id")
554                            .or_else(|| value.get("connId"))
555                            .and_then(|v| v.as_str())
556                            .unwrap_or("unknown");
557
558                        return Ok(AuthResponse::PrivateAuth(PrivateAuthData::new(
559                            true,
560                            "subscription confirmed",
561                            conn_id,
562                            None,
563                        )));
564                    }
565                }
566
567                // If we get here, it's not an auth or subscribe response
568                // Continue waiting
569            }
570            Some(Ok(_)) => {
571                // Binary or other non-text message, ignore
572                continue;
573            }
574            Some(Err(e)) => {
575                return Err(BybitError::Tungstenite(e));
576            }
577            None => {
578                return Err(BybitError::Base(
579                    "WebSocket stream closed while waiting for authentication".to_string(),
580                ));
581            }
582        }
583    }
584}