bybit_async/rest/
mod.rs

1mod account;
2mod market;
3pub mod trade;
4
5use crate::{
6    config::Config,
7    error::BybitError::{self, *},
8    models::Product,
9    BybitResponseError,
10};
11use chrono::Utc;
12use fehler::{throw, throws};
13use hex::encode as hexify;
14use hmac::{Hmac, Mac};
15use log::{debug, trace};
16#[cfg(feature = "zero-copy")]
17use owning_ref::OwningHandle;
18use reqwest::{
19    header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE, USER_AGENT},
20    Client, Method, Response,
21};
22use serde::{de::DeserializeOwned, Serialize};
23use serde_json::from_str;
24use sha2::Sha256;
25#[cfg(feature = "zero-copy")]
26use std::ops::Deref;
27
28pub trait Request: Serialize {
29    const PRODUCT: Product;
30    const ENDPOINT: &'static str;
31    const METHOD: Method;
32    const KEYED: bool = false; // SIGNED imples KEYED no matter KEYED is true or false
33    const SIGNED: bool = false;
34    type Response: DeserializeOwned;
35}
36
37#[derive(Clone, Default)]
38pub struct Bybit {
39    key: Option<String>,
40    secret: Option<String>,
41    client: Client,
42    config: Config,
43}
44
45impl Bybit {
46    pub fn new() -> Self {
47        Default::default()
48    }
49
50    pub fn with_key(api_key: &str) -> Self {
51        Bybit {
52            client: Client::new(),
53            key: Some(api_key.into()),
54            secret: None,
55            config: Config::default(),
56        }
57    }
58
59    pub fn with_key_and_secret(api_key: &str, api_secret: &str) -> Self {
60        Bybit {
61            client: Client::new(),
62            key: Some(api_key.into()),
63            secret: Some(api_secret.into()),
64            config: Config::default(),
65        }
66    }
67
68    pub fn config(&mut self, config: Config) {
69        self.config = config;
70    }
71
72    #[throws(BybitError)]
73    pub async fn request<R>(&self, req: R) -> RestResponse<R::Response>
74    where
75        R: Request,
76    {
77        let mut params = if matches!(R::METHOD, Method::GET) {
78            serde_qs::to_string(&req)?
79        } else {
80            String::new()
81        };
82
83        let body = if !matches!(R::METHOD, Method::GET) {
84            serde_qs::to_string(&req)?
85        } else {
86            String::new()
87        };
88
89        if R::SIGNED {
90            if !params.is_empty() {
91                params.push('&');
92            }
93            params.push_str(&format!("timestamp={}", Utc::now().timestamp_millis()));
94            params.push_str(&format!("&recvWindow={}", self.config.recv_window));
95
96            let signature = self.signature(&params, &body)?;
97            params.push_str(&format!("&signature={}", signature));
98        }
99
100        let path = R::ENDPOINT.to_string();
101
102        let base = &self.config.rest_api_endpoint;
103        let url = format!("{base}{path}?{params}");
104
105        let mut custom_headers = HeaderMap::new();
106        custom_headers.insert(USER_AGENT, HeaderValue::from_static("bybit-async-rs"));
107        if !body.is_empty() {
108            custom_headers.insert(
109                CONTENT_TYPE,
110                HeaderValue::from_static("application/x-www-form-urlencoded"),
111            );
112        }
113        if R::SIGNED || R::KEYED {
114            let key = match &self.key {
115                Some(key) => key,
116                None => throw!(MissingApiKey),
117            };
118            custom_headers.insert(
119                HeaderName::from_static("x-mbx-apikey"),
120                HeaderValue::from_str(key)?,
121            );
122        }
123
124        debug!("[REST] url: {url}, body: {body}");
125
126        let resp = self
127            .client
128            .request(R::METHOD, url.as_str())
129            .headers(custom_headers)
130            .body(body)
131            .send()
132            .await?;
133
134        self.handle_response(resp).await?
135    }
136
137    #[throws(BybitError)]
138    fn signature(&self, params: &str, body: &str) -> String {
139        let secret = match &self.secret {
140            Some(s) => s,
141            None => throw!(MissingApiSecret),
142        };
143        // Signature: hex(HMAC_SHA256(queries + data))
144        let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
145        let sign_message = format!("{}{}", params, body);
146        trace!("Sign message: {}", sign_message);
147        mac.update(sign_message.as_bytes());
148        let signature = hexify(mac.finalize().into_bytes());
149        signature
150    }
151
152    #[cfg(not(feature = "zero-copy"))]
153    #[throws(BybitError)]
154    async fn handle_response<O: DeserializeOwned>(&self, resp: Response) -> RestResponse<O> {
155        let status = resp.status();
156        let body = resp.text().await?;
157
158        if cfg!(feature = "print-response") {
159            debug!("Response is {status} {body}");
160        };
161
162        match from_str(&body) {
163            Ok(v) => v,
164            Err(e) => match from_str::<BybitResponseError>(&body) {
165                Ok(e) => throw!(e),
166                Err(_) => throw!(e),
167            },
168        }
169    }
170
171    #[cfg(feature = "zero-copy")]
172    #[throws(BybitError)]
173    async fn handle_response<O: DeserializeOwned>(&self, resp: Response) -> RestResponse<O> {
174        let status = resp.status();
175        let body = resp.text().await?;
176
177        if cfg!(feature = "print-response") {
178            debug!("Response is {status} {body}");
179        };
180
181        OwningHandle::try_new(body, |body| -> Result<_, BybitError> {
182            let body = unsafe { &*body };
183            match from_str(body) {
184                Ok(v) => Ok(C(v)),
185                Err(e) => match from_str::<BybitResponseError>(&body) {
186                    Ok(e) => throw!(e),
187                    Err(_) => throw!(e),
188                },
189            }
190        })?
191    }
192}
193
194#[cfg(feature = "zero-copy")]
195#[derive(Clone, Copy, Debug)]
196#[repr(transparent)]
197pub struct C<T>(pub T);
198#[cfg(feature = "zero-copy")]
199impl<T> Deref for C<T> {
200    type Target = T;
201    fn deref(&self) -> &Self::Target {
202        &self.0
203    }
204}
205#[cfg(feature = "zero-copy")]
206pub type RestResponse<O> = OwningHandle<String, C<O>>;
207
208#[cfg(not(feature = "zero-copy"))]
209pub type RestResponse<O> = O;
210
211#[cfg(test)]
212mod test {
213    use super::Bybit;
214    use anyhow::Error;
215    use fehler::throws;
216    use url::{form_urlencoded::Serializer, Url};
217
218    #[throws(Error)]
219    #[test]
220    fn signature_query() {
221        let tr = Bybit::with_key_and_secret(
222            "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A",
223            "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j",
224        );
225        let sig = tr.signature(
226            &Url::parse_with_params(
227                "http://a.com/api/v1/test",
228                &[
229                    ("symbol", "LTCBTC"),
230                    ("side", "BUY"),
231                    ("type", "LIMIT"),
232                    ("timeInForce", "GTC"),
233                    ("quantity", "1"),
234                    ("price", "0.1"),
235                    ("recvWindow", "5000"),
236                    ("timestamp", "1499827319559"),
237                ],
238            )?
239            .query()
240            .unwrap_or_default(),
241            "",
242        )?;
243        assert_eq!(
244            sig,
245            "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71"
246        );
247    }
248
249    #[throws(Error)]
250    #[test]
251    fn signature_body() {
252        let tr = Bybit::with_key_and_secret(
253            "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A",
254            "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j",
255        );
256        let mut s = Serializer::new(String::new());
257        s.extend_pairs(&[
258            ("symbol", "LTCBTC"),
259            ("side", "BUY"),
260            ("type", "LIMIT"),
261            ("timeInForce", "GTC"),
262            ("quantity", "1"),
263            ("price", "0.1"),
264            ("recvWindow", "5000"),
265            ("timestamp", "1499827319559"),
266        ]);
267
268        let sig = tr.signature(
269            &Url::parse("http://a.com/api/v1/test")?
270                .query()
271                .unwrap_or_default(),
272            &s.finish(),
273        )?;
274        assert_eq!(
275            sig,
276            "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71"
277        );
278    }
279
280    #[throws(Error)]
281    #[test]
282    fn signature_query_body() {
283        let tr = Bybit::with_key_and_secret(
284            "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A",
285            "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j",
286        );
287
288        let mut s = Serializer::new(String::new());
289        s.extend_pairs(&[
290            ("quantity", "1"),
291            ("price", "0.1"),
292            ("recvWindow", "5000"),
293            ("timestamp", "1499827319559"),
294        ]);
295
296        let sig = tr.signature(
297            &Url::parse_with_params(
298                "http://a.com/api/v1/order",
299                &[
300                    ("symbol", "LTCBTC"),
301                    ("side", "BUY"),
302                    ("type", "LIMIT"),
303                    ("timeInForce", "GTC"),
304                ],
305            )?
306            .query()
307            .unwrap_or_default(),
308            &s.finish(),
309        )?;
310        assert_eq!(
311            sig,
312            "0fd168b8ddb4876a0358a8d14d0c9f3da0e9b20c5d52b2a00fcf7d1c602f9a77"
313        );
314    }
315
316    #[throws(Error)]
317    #[test]
318    fn signature_body2() {
319        let tr = Bybit::with_key_and_secret(
320            "vj1e6h50pFN9CsXT5nsL25JkTuBHkKw3zJhsA6OPtruIRalm20vTuXqF3htCZeWW",
321            "5Cjj09rLKWNVe7fSalqgpilh5I3y6pPplhOukZChkusLqqi9mQyFk34kJJBTdlEJ",
322        );
323
324        let q = &mut [
325            ("symbol", "ETHBTC"),
326            ("side", "BUY"),
327            ("type", "LIMIT"),
328            ("timeInForce", "GTC"),
329            ("quantity", "1"),
330            ("price", "0.1"),
331            ("recvWindow", "5000"),
332            ("timestamp", "1540687064555"),
333        ];
334        q.sort();
335        let q: Vec<_> = q.into_iter().map(|(k, v)| format!("{}={}", k, v)).collect();
336        let q = q.join("&");
337        let sig = tr.signature(
338            &Url::parse("http://a.com/api/v1/test")?
339                .query()
340                .unwrap_or_default(),
341            &q,
342        )?;
343        assert_eq!(
344            sig,
345            "1ee5a75760b9496a2144a22116e02bc0b7fdcf828781fa87ca273540dfcf2cb0"
346        );
347    }
348}