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(×tamp, &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(×tamp.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}