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