binance_async/rest/
mod.rs

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