crypto_botters_bybit/
lib.rs

1//! A crate for communicating with the [Bybit API](https://bybit-exchange.github.io/docs/spot/v3/#t-introduction).
2//! For example usages, see files in the examples/ directory.
3
4use std::{time::SystemTime, borrow::Cow, marker::PhantomData, vec};
5use hmac::{Hmac, Mac};
6use sha2::Sha256;
7use serde::{Serialize, de::DeserializeOwned};
8use serde_json::json;
9use crypto_botters_api::{HandlerOption, HandlerOptions, HttpOption, WebSocketOption};
10use generic_api_client::{http::{*, header::HeaderValue}, websocket::*};
11
12/// The type returned by [Client::request()].
13pub type BybitRequestResult<T> = Result<T, BybitRequestError>;
14pub type BybitRequestError = RequestError<&'static str, BybitHandlerError>;
15
16/// Options that can be set when creating handlers
17pub enum BybitOption {
18    /// [Default] variant, does nothing
19    Default,
20    /// API key
21    Key(String),
22    /// Api secret
23    Secret(String),
24    /// Base url for HTTP requests
25    HttpUrl(BybitHttpUrl),
26    /// Type of authentication used for HTTP requests.
27    HttpAuth(BybitHttpAuth),
28    /// receive window parameter used for requests
29    RecvWindow(i32),
30    /// [RequestConfig] used when sending requests.
31    /// `url_prefix` will be overridden by [HttpUrl](Self::HttpUrl) unless `HttpUrl` is [BinanceHttpUrl::None].
32    RequestConfig(RequestConfig),
33    /// Base url for WebSocket connections
34    WebSocketUrl(BybitWebSocketUrl),
35    /// Whether [BybitWebSocketHandler] should perform authentication
36    WebSocketAuth(bool),
37    /// The topics to subscribe to.
38    WebSocketTopics(Vec<String>),
39    /// [WebSocketConfig] used for creating [WebSocketConnection]s
40    /// `url_prefix` will be overridden by [WebSocketUrl](Self::WebSocketUrl) unless `WebSocketUrl` is [BybitWebSocketUrl::None].
41    /// By default, `ignore_duplicate_during_reconnection` is set to `true`.
42    WebSocketConfig(WebSocketConfig),
43}
44
45/// A `struct` that represents a set of [BybitOption] s.
46#[derive(Clone, Debug)]
47pub struct BybitOptions {
48    /// see [BybitOption::Key]
49    pub key: Option<String>,
50    /// see [BybitOption::Secret]
51    pub secret: Option<String>,
52    /// see [BybitOption::HttpUrl]
53    pub http_url: BybitHttpUrl,
54    /// see [BybitOption::HttpAuth]
55    pub http_auth: BybitHttpAuth,
56    /// see [BybitOption::RecvWindow]
57    pub recv_window: Option<i32>,
58    /// see [BybitOption::RequestConfig]
59    pub request_config: RequestConfig,
60    /// see [BybitOption::WebSocketUrl]
61    pub websocket_url: BybitWebSocketUrl,
62    /// see [BybitOption::WebSocketAuth]
63    pub websocket_auth: bool,
64    /// see [BybitOption::WebSocketTopics]
65    pub websocket_topics: Vec<String>,
66    /// see [BybitOption::WebSocketConfig]
67    pub websocket_config: WebSocketConfig,
68}
69
70/// A `enum` that represents the base url of the Bybit REST API.
71#[derive(Debug, Eq, PartialEq, Copy, Clone)]
72pub enum BybitHttpUrl {
73    /// `https://api.bybit.com`
74    Bybit,
75    /// `https://api.bytick.com`
76    Bytick,
77    /// `https://api-testnet.bybit.com`
78    Test,
79    /// The url will not be modified by [BybitRequestHandler]
80    None,
81}
82
83/// A `enum` that represents the base url of the Bybit WebSocket API.
84#[derive(Debug, Eq, PartialEq, Copy, Clone)]
85pub enum BybitWebSocketUrl {
86    /// `wss://stream.bybit.com`
87    Bybit,
88    /// `wss://stream.bytick.com`
89    Bytick,
90    /// `wss://stream-testnet.bybit.com`
91    Test,
92    /// The url will not be modified by [BybitWebSocketHandler]
93    None,
94}
95
96/// Represents the auth type.
97///
98/// |API|type|
99/// |---|----|
100/// |Derivatives v3 Unified Margin|Type2|
101/// |Derivatives v3 Contract|Type2|
102/// |Futures v2 Inverse Perpetual|Type1|
103/// |Futures v2 USDT Perpetual|Type1|
104/// |Futures v2 Inverse Futures|Type1|
105/// |Spot v3|Type2|
106/// |Spot v1|SpotType1|
107/// |Account Asset v3|Type2|
108/// |Account Asset v1|Type1|
109/// |Copy Trading|Type2|
110/// |USDC Contract Option|Type2|
111/// |USDC Contract Perpetual|Type2|
112/// |Tax|Type2|
113#[derive(Debug, Eq, PartialEq, Copy, Clone)]
114pub enum BybitHttpAuth {
115    Type1,
116    SpotType1,
117    Type2,
118    None,
119}
120
121#[derive(Debug)]
122pub enum BybitHandlerError {
123    ApiError(serde_json::Value),
124    IpBan(serde_json::Value),
125    ParseError,
126}
127
128/// A `struct` that implements [RequestHandler]
129pub struct BybitRequestHandler<'a, R: DeserializeOwned> {
130    options: BybitOptions,
131    _phantom: PhantomData<&'a R>,
132}
133
134pub struct BybitWebSocketHandler<H: FnMut(serde_json::Value) + Send + 'static> {
135    message_handler: H,
136    options: BybitOptions,
137}
138
139impl<'a, B, R> RequestHandler<B> for BybitRequestHandler<'a, R>
140where
141    B: Serialize,
142    R: DeserializeOwned,
143{
144    type Successful = R;
145    type Unsuccessful = BybitHandlerError;
146    type BuildError = &'static str;
147
148    fn request_config(&self) -> RequestConfig {
149        let mut config = self.options.request_config.clone();
150        if self.options.http_url != BybitHttpUrl::None {
151            config.url_prefix = self.options.http_url.as_str().to_owned();
152        }
153        config
154    }
155
156    fn build_request(&self, mut builder: RequestBuilder, request_body: &Option<B>, _: u8) -> Result<Request, Self::BuildError> {
157        if self.options.http_auth == BybitHttpAuth::None {
158            if let Some(body) = request_body {
159                let json = serde_json::to_string(body).or(Err("could not serialize body as application/json"))?;
160                builder = builder
161                    .header(header::CONTENT_TYPE, "application/json")
162                    .body(json);
163            }
164            return builder.build().or(Err("failed to build request"));
165        }
166
167        let key = self.options.key.as_deref().ok_or("API key not set")?;
168        let secret = self.options.secret.as_deref().ok_or("API secret not set")?;
169
170        let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); // always after the epoch
171        let timestamp = time.as_millis();
172
173        let hmac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap(); // hmac accepts key of any length
174
175        match self.options.http_auth {
176            BybitHttpAuth::Type1 => Self::type1_auth(builder, request_body, key, timestamp, hmac, false, self.options.recv_window),
177            BybitHttpAuth::SpotType1 => Self::type1_auth(builder, request_body, key, timestamp, hmac, true, self.options.recv_window),
178            BybitHttpAuth::Type2 => Self::type2_auth(builder, request_body, key, timestamp, hmac, self.options.recv_window),
179            BybitHttpAuth::None => unreachable!(), // we've already handled this case
180        }
181    }
182
183    fn handle_response(&self, status: StatusCode, _: HeaderMap, response_body: Bytes) -> Result<Self::Successful, Self::Unsuccessful> {
184        if status.is_success() {
185            serde_json::from_slice(&response_body).map_err(|error| {
186                log::error!("Failed to parse response due to an error: {}", error);
187                BybitHandlerError::ParseError
188            })
189        } else {
190            // https://bybit-exchange.github.io/docs/spot/v3/#t-ratelimits
191            let error = match serde_json::from_slice(&response_body) {
192                Ok(parsed) => {
193                    if status == 403 {
194                        BybitHandlerError::IpBan(parsed)
195                    } else {
196                        BybitHandlerError::ApiError(parsed)
197                    }
198                }
199                Err(error) => {
200                    log::error!("Failed to parse error response due to an error: {}", error);
201                    BybitHandlerError::ParseError
202                },
203            };
204            Err(error)
205        }
206    }
207}
208
209impl<'a, R> BybitRequestHandler<'a, R> where R: DeserializeOwned {
210    fn type1_auth<B>(builder: RequestBuilder, request_body: &Option<B>, key: &str, timestamp: u128, mut hmac: Hmac<Sha256>, spot: bool, window: Option<i32>)
211        -> Result<Request, <BybitRequestHandler<'a, R> as RequestHandler<B>>::BuildError>
212    where
213        B: Serialize,
214    {
215        fn sort_and_add<'a>(mut pairs: Vec<(Cow<str>, Cow<'a, str>)>, key: &'a str, timestamp: u128) -> String {
216            pairs.push((Cow::Borrowed("api_key"), Cow::Borrowed(key)));
217            pairs.push((Cow::Borrowed("timestamp"), Cow::Owned(timestamp.to_string())));
218            pairs.sort_unstable();
219
220            let mut urlencoded = String::new();
221            for (key, value) in pairs {
222                urlencoded.push_str(&key);
223                if !value.is_empty() {
224                    urlencoded.push('=');
225                    urlencoded.push_str(&value);
226                }
227                urlencoded.push('&');
228            }
229            urlencoded.pop(); // the last '&'
230            urlencoded
231        }
232
233        let mut request = builder.build().or(Err("failed to build request"))?;
234        if matches!(*request.method(), Method::GET | Method::DELETE) {
235            let mut queries: Vec<_> = request.url().query_pairs().collect();
236            if let Some(window) = window {
237                if spot {
238                    queries.push((Cow::Borrowed("recvWindow"), Cow::Owned(window.to_string())));
239                } else {
240                    queries.push((Cow::Borrowed("recv_window"), Cow::Owned(window.to_string())));
241                }
242            }
243            let query = sort_and_add(queries, key, timestamp);
244            request.url_mut().set_query(Some(&query));
245
246            hmac.update(query.as_bytes());
247            let signature = hex::encode(hmac.finalize().into_bytes());
248
249            request.url_mut().query_pairs_mut().append_pair("sign", &signature);
250
251            if let Some(body) = request_body {
252                if spot {
253                    let body_string = serde_urlencoded::to_string(body).or(Err("could not serialize body as application/x-www-form-urlencoded"))?;
254                    *request.body_mut() = Some(body_string.into());
255                    request.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"));
256                } else {
257                    let body_string = serde_json::to_string(body).or(Err("could not serialize body as application/json"))?;
258                    *request.body_mut() = Some(body_string.into());
259                    request.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("application/json"));
260                }
261            }
262        } else {
263            let mut body = if let Some(body) = request_body {
264                serde_urlencoded::to_string(body).or(Err("could not serialize body as application/x-www-form-urlencoded"))?
265            } else {
266                String::new()
267            };
268            if let Some(window) = window {
269                if !body.is_empty() {
270                    body.push('&');
271                }
272                if spot {
273                    body.push_str("recvWindow=");
274                } else {
275                    body.push_str("recv_window=");
276                }
277                body.push_str(&window.to_string());
278            }
279
280            let pairs: Vec<_> = body.split('&')
281                .map(|pair| pair.split_once('=').unwrap_or((pair, "")))
282                .map(|(k, v)| (Cow::Borrowed(k), Cow::Borrowed(v)))
283                .collect();
284            let mut body_query_string = sort_and_add(pairs, key, timestamp);
285
286            hmac.update(body_query_string.as_bytes());
287            let signature = hex::encode(hmac.finalize().into_bytes());
288
289            if spot {
290                body_query_string.push_str(&format!("sign={signature}"));
291
292                *request.body_mut() = Some(body_query_string.into());
293                request.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"));
294            } else {
295                let mut json = serde_json::to_value(request_body).or(Err("could not serialize body as application/json"))?;
296                let Some(map) = json.as_object_mut() else {
297                    return Err("body must to be serializable as a JSON object");
298                };
299                map.insert("sign".to_owned(), serde_json::Value::String(signature));
300                if let Some(window) = window {
301                    if spot {
302                        map.insert("recvWindow".to_owned(), serde_json::Value::Number(window.into()));
303                    } else {
304                        map.insert("recv_window".to_owned(), serde_json::Value::Number(window.into()));
305                    }
306                }
307
308                *request.body_mut() = Some(json.to_string().into());
309                request.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("application/json"));
310            }
311        }
312        Ok(request)
313    }
314
315    fn type2_auth<B>(mut builder: RequestBuilder, request_body: &Option<B>, key: &str, timestamp: u128, mut hmac: Hmac<Sha256>, window: Option<i32>)
316        -> Result<Request, <BybitRequestHandler<'a, R> as RequestHandler<B>>::BuildError>
317    where
318        B: Serialize,
319    {
320        let body = if let Some(body) = request_body {
321            let json = serde_json::to_value(body).or(Err("could not serialize body as application/json"))?;
322            builder = builder
323                .header(header::CONTENT_TYPE, "application/json")
324                .body(json.to_string());
325            Some(json)
326        } else {
327            None
328        };
329
330        let mut request = builder.build().or(Err("failed to build request"))?;
331
332        let mut sign_contents = format!("{timestamp}{key}");
333        if let Some(window) = window {
334            sign_contents.push_str(&window.to_string());
335        }
336
337        if matches!(*request.method(), Method::GET | Method::DELETE) {
338            if let Some(query) = request.url().query() {
339                sign_contents.push_str(query);
340            }
341        } else {
342            let body = body.unwrap_or_else(|| {
343                *request.body_mut() = Some("{}".into());
344                request.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("application/json"));
345                json!({})
346            });
347            sign_contents.push_str(&body.to_string());
348        }
349
350        hmac.update(sign_contents.as_bytes());
351        let signature = hex::encode(hmac.finalize().into_bytes());
352
353        let headers = request.headers_mut();
354        headers.insert("X-BAPI-SIGN-TYPE", HeaderValue::from(2));
355        headers.insert("X-BAPI-SIGN", HeaderValue::from_str(&signature).unwrap()); // hex digits are valid
356        headers.insert("X-BAPI-API-KEY", HeaderValue::from_str(key).or(Err("invalid character in API key"))?);
357        headers.insert("X-BAPI-TIMESTAMP", HeaderValue::from(timestamp as u64));
358        if let Some(window) = window {
359            headers.insert("X-BAPI-RECV-WINDOW", HeaderValue::from(window));
360        }
361        Ok(request)
362    }
363}
364
365impl<H> WebSocketHandler for BybitWebSocketHandler<H> where H: FnMut(serde_json::Value) + Send + 'static {
366    fn websocket_config(&self) -> WebSocketConfig {
367        let mut config = self.options.websocket_config.clone();
368        if self.options.websocket_url != BybitWebSocketUrl::None {
369            config.url_prefix = self.options.websocket_url.as_str().to_owned();
370        }
371        config
372    }
373
374    fn handle_start(&mut self) -> Vec<WebSocketMessage> {
375        if self.options.websocket_auth {
376            if let Some(key) = self.options.key.as_deref() {
377                if let Some(secret) = self.options.secret.as_deref() {
378                    let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); // always after the epoch
379                    let expires = time.as_millis() as u64 + 1000;
380
381                    let mut hmac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap(); // hmac accepts key of any length
382
383                    hmac.update(format!("GET/realtime{expires}").as_bytes());
384                    let signature = hex::encode(hmac.finalize().into_bytes());
385
386                    return vec![
387                        WebSocketMessage::Text(json!({
388                            "op": "auth",
389                            "args": [key, expires, signature],
390                        }).to_string()),
391                    ];
392                } else {
393                    log::error!("API secret not set.");
394                };
395            } else {
396                log::error!("API key not set.");
397            };
398        }
399        self.message_subscribe()
400    }
401
402    fn handle_message(&mut self, message: WebSocketMessage) -> Vec<WebSocketMessage> {
403        match message {
404            WebSocketMessage::Text(message) => {
405                let message: serde_json::Value = match serde_json::from_str(&message) {
406                    Ok(message) => message,
407                    Err(_) => {
408                        log::warn!("Invalid JSON received");
409                        return vec![];
410                    },
411                };
412                match message["op"].as_str() {
413                    Some("auth") => {
414                        if message["success"].as_bool() == Some(true) {
415                            log::debug!("WebSocket authentication successful");
416                        } else {
417                            log::error!("WebSocket authentication unsuccessful; message: {}", message["ret_msg"]);
418                        }
419                        return self.message_subscribe();
420                    },
421                    Some("subscribe") => {
422                        if message["success"].as_bool() == Some(true) {
423                            log::debug!("WebSocket topics subscription successful");
424                        } else {
425                            log::error!("WebSocket topics subscription unsuccessful; message: {}", message["ret_msg"]);
426                        }
427                    },
428                    _ => (self.message_handler)(message),
429                }
430            },
431            WebSocketMessage::Binary(_) => log::warn!("Unexpected binary message received"),
432            WebSocketMessage::Ping(_) | WebSocketMessage::Pong(_) => (),
433        }
434        vec![]
435    }
436}
437
438impl<H> BybitWebSocketHandler<H> where H: FnMut(serde_json::Value) + Send + 'static, {
439    #[inline(always)]
440    fn message_subscribe(&self) -> Vec<WebSocketMessage> {
441        vec![WebSocketMessage::Text(
442            json!({ "op": "subscribe", "args": self.options.websocket_topics }).to_string(),
443        )]
444    }
445}
446
447impl BybitHttpUrl {
448    /// The URL that this variant represents.
449    #[inline(always)]
450    pub fn as_str(&self) -> &'static str {
451        match self {
452            Self::Bybit => "https://api.bybit.com",
453            Self::Bytick => "https://api.bytick.com",
454            Self::Test => "https://api-testnet.bybit.com",
455            Self::None => "",
456        }
457    }
458}
459
460impl BybitWebSocketUrl {
461    /// The URL that this variant represents.
462    #[inline(always)]
463    pub fn as_str(&self) -> &'static str {
464        match self {
465            Self::Bybit => "wss://stream.bybit.com",
466            Self::Bytick => "wss://stream.bytick.com",
467            Self::Test => "wss://stream-testnet.bybit.com",
468            Self::None => "",
469        }
470    }
471}
472
473impl HandlerOptions for BybitOptions {
474    type OptionItem = BybitOption;
475
476    fn update(&mut self, option: Self::OptionItem) {
477        match option {
478            BybitOption::Default => (),
479            BybitOption::Key(v) => self.key = Some(v),
480            BybitOption::Secret(v) => self.secret = Some(v),
481            BybitOption::HttpUrl(v) => self.http_url = v,
482            BybitOption::HttpAuth(v) => self.http_auth = v,
483            BybitOption::RecvWindow(v) => self.recv_window = Some(v),
484            BybitOption::RequestConfig(v) => self.request_config = v,
485            BybitOption::WebSocketUrl(v) => self.websocket_url = v,
486            BybitOption::WebSocketAuth(v) => self.websocket_auth = v,
487            BybitOption::WebSocketTopics(v) => self.websocket_topics = v,
488            BybitOption::WebSocketConfig(v) => self.websocket_config = v,
489        }
490    }
491}
492
493impl Default for BybitOptions {
494    fn default() -> Self {
495        let mut websocket_config = WebSocketConfig::new();
496        websocket_config.ignore_duplicate_during_reconnection = true;
497        Self {
498            key: None,
499            secret: None,
500            http_url: BybitHttpUrl::Bybit,
501            http_auth: BybitHttpAuth::None,
502            recv_window: None,
503            request_config: RequestConfig::default(),
504            websocket_url: BybitWebSocketUrl::Bybit,
505            websocket_auth: false,
506            websocket_topics: vec![],
507            websocket_config,
508        }
509    }
510}
511
512impl<'a, R: DeserializeOwned + 'a> HttpOption<'a, R> for BybitOption {
513    type RequestHandler = BybitRequestHandler<'a, R>;
514
515    #[inline(always)]
516    fn request_handler(options: Self::Options) -> Self::RequestHandler {
517        BybitRequestHandler::<'a, R> {
518            options,
519            _phantom: PhantomData,
520        }
521    }
522}
523
524impl <H: FnMut(serde_json::Value) + Send + 'static> WebSocketOption<H> for BybitOption {
525    type WebSocketHandler = BybitWebSocketHandler<H>;
526
527    #[inline(always)]
528    fn websocket_handler(handler: H, options: Self::Options) -> Self::WebSocketHandler {
529        BybitWebSocketHandler {
530            message_handler: handler,
531            options,
532        }
533    }
534}
535
536impl HandlerOption for BybitOption {
537    type Options = BybitOptions;
538}
539
540impl Default for BybitOption {
541    fn default() -> Self {
542        Self::Default
543    }
544}