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; 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(¶ms, &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 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}