binance_api/
client.rs

1use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};
2
3use hmac::Mac as _;
4use reqwest::{
5    header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE, USER_AGENT},
6    Response, StatusCode,
7};
8use serde::de::DeserializeOwned;
9
10use crate::{
11    endpoints::Endpoint,
12    error::{Error, ResponseError, Result},
13};
14
15#[derive(Debug, Clone)]
16/// The HTTP client able to call the [endpoints][Endpoint].
17///
18/// The client will automatically call the required method
19/// for an endpoint with the optional data to ensure security needs.
20pub struct Client {
21    host: String,
22    api_key: Option<String>,
23    secret_key: Option<String>,
24    recv_window: Option<u16>,
25    inner_client: reqwest::Client,
26}
27
28impl Client {
29    /// Create a new client with an optional API key.
30    pub fn new(host: String, api_key: impl Into<Option<String>>) -> Self {
31        Self {
32            host,
33            api_key: api_key.into(),
34            secret_key: None,
35            recv_window: None,
36            inner_client: reqwest::Client::new(),
37        }
38    }
39
40    /// Add a secret key to sign the queries.
41    ///
42    /// An additional parameter, `recv_window`, may be sent to specify the number of milliseconds
43    /// after timestamp the request is valid for.
44    /// If recv_window will not be sent, it defaults to 5_000.
45    pub fn with_signed(self, key: String, recv_window: impl Into<Option<u16>>) -> Self {
46        Self {
47            secret_key: Some(key),
48            recv_window: recv_window.into(),
49            ..self
50        }
51    }
52
53    /// Request data from [endpoint][Endpoint].
54    ///
55    /// For better experience use the [url_query!][crate::url_query] macro
56    /// to construct key-value pairs of the URL query.
57    pub async fn request<T: DeserializeOwned, E: Endpoint>(
58        &self,
59        endpoint: &E,
60        query: &[(&dyn ToString, &dyn ToString)],
61    ) -> Result<T> {
62        let query_pairs = self.construct_query(endpoint, query)?;
63        let url = format!("{}{}", self.host, endpoint.as_endpoint());
64        let method = endpoint.http_verb();
65
66        let security = endpoint.security_type();
67        let headers = self.build_headers(security.is_key_required())?;
68
69        let response = self
70            .inner_client
71            .request(method, url)
72            .query(&query_pairs)
73            .headers(headers)
74            .send()
75            .await?;
76
77        self.response_handler(response).await
78    }
79
80    const TIMESTAMP_KEY: &'static str = "timestamp";
81    const RECEIVE_WINDOW_KEY: &'static str = "recvWindow";
82    const SIGNATURE_KEY: &'static str = "signature";
83
84    fn construct_query<E: Endpoint>(
85        &self,
86        endpoint: &E,
87        query: &[(&dyn ToString, &dyn ToString)],
88    ) -> Result<Vec<(String, String)>> {
89        let mut query_pairs: Vec<_> = query
90            .iter()
91            .map(|(key, value)| (key.to_string(), value.to_string()))
92            .collect();
93
94        let security = endpoint.security_type();
95        if security.is_signature_required() {
96            let timestamp = get_timestamp_millis().map_err(|_| Error::CannotGetTimestamp)?;
97            query_pairs.push((Self::TIMESTAMP_KEY.into(), timestamp.to_string()));
98
99            if let Some(recv_window) = self.recv_window {
100                query_pairs.push((Self::RECEIVE_WINDOW_KEY.into(), recv_window.to_string()));
101            }
102
103            let query = serde_urlencoded::to_string(&query_pairs)?;
104            let signature = self.get_signature(&query)?;
105            query_pairs.push((Self::SIGNATURE_KEY.into(), signature));
106        }
107
108        Ok(query_pairs)
109    }
110
111    fn get_signature(&self, query: &str) -> Result<String> {
112        if let Some(secret_key) = &self.secret_key {
113            get_hmac_signature(query, secret_key)
114        } else {
115            Err(Error::MissingSecretKey)
116        }
117    }
118
119    const API_KEY_HEADER: &'static str = "X-MBX-APIKEY";
120    const USER_AGENT: &'static str = "binance-api rust client";
121
122    fn build_headers(&self, with_api_key: bool) -> Result<HeaderMap> {
123        let mut headers = HeaderMap::from_iter([
124            (USER_AGENT, HeaderValue::from_static(Self::USER_AGENT)),
125            (
126                CONTENT_TYPE,
127                HeaderValue::from_static("application/x-www-form-urlencoded"),
128            ),
129        ]);
130        if with_api_key {
131            if let Some(api_key) = &self.api_key {
132                let api_key = HeaderValue::from_str(api_key)
133                    .map_err(|_| Error::InvalidApiKey(api_key.clone()))?;
134                let _ = headers.insert(HeaderName::from_static(Self::API_KEY_HEADER), api_key);
135            } else {
136                return Err(Error::MissingApiKey);
137            }
138        }
139
140        Ok(headers)
141    }
142
143    async fn response_handler<T: DeserializeOwned>(&self, response: Response) -> Result<T> {
144        let status = response.status();
145        match status {
146            StatusCode::OK => Ok(response.json().await?),
147            StatusCode::BAD_REQUEST => {
148                let error: ResponseError = response.json().await?;
149                Err(Error::Server {
150                    inner: error,
151                    http_code: status,
152                })
153            }
154            // all other codes are not guaranteed to return valid JSON
155            _ => {
156                let text_err = response.text().await;
157                if let Ok(err) = text_err {
158                    if let Ok(json_err) = serde_json::from_str(&err) {
159                        Err(Error::Server {
160                            inner: json_err,
161                            http_code: status,
162                        })
163                    } else {
164                        Err(Error::ServerPlain {
165                            inner: err,
166                            http_code: status,
167                        })
168                    }
169                } else {
170                    Err(Error::ServerUnknown(status))
171                }
172            }
173        }
174    }
175}
176
177type Hmac256 = hmac::Hmac<sha2::Sha256>;
178
179fn get_hmac_signature(data: &str, key: &str) -> Result<String> {
180    let mut signed_key = Hmac256::new_from_slice(key.as_bytes())
181        .map_err(|_| Error::InvalidSecretKey { size: key.len() })?;
182
183    signed_key.update(data.as_bytes());
184
185    Ok(hex::encode(signed_key.finalize().into_bytes()))
186}
187
188fn get_timestamp_millis() -> std::result::Result<u64, SystemTimeError> {
189    let now = SystemTime::now();
190    let since_epoch = now.duration_since(UNIX_EPOCH)?;
191    Ok(since_epoch.as_secs() * 1000 + u64::from(since_epoch.subsec_millis()))
192}
193
194#[macro_export]
195/// Allow to construct a [Vec][std::vec::Vec] of pairs
196/// to later feed them to the request:
197///
198/// ```
199/// # use binance_api::url_query;
200/// let query = url_query!(foo="VALUE", bar=1, baz=0.05);
201///
202/// let encoded: String = query.iter().map(|(k, v)| {
203///     format!("{}={}&", k.to_string(), v.to_string())
204/// }).collect();
205/// assert_eq!(encoded, "foo=VALUE&bar=1&baz=0.05&");
206/// ```
207macro_rules! url_query {
208    ( $( $key:tt = $value:expr ),+ $(,)? ) => {{
209        let query: Vec<(&dyn ToString, &dyn ToString)> = vec![
210            $(
211
212                (&stringify!($key), &$value),
213            )+
214        ];
215        query
216    }}
217}
218
219#[cfg(test)]
220fn sign_query(query: &[(&dyn ToString, &dyn ToString)], key: &str) -> Result<String> {
221    let query_pairs: Vec<_> = query
222        .iter()
223        .map(|(key, value)| (key.to_string(), value.to_string()))
224        .collect();
225
226    let query = serde_urlencoded::to_string(&query_pairs)?;
227    get_hmac_signature(&query, key)
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    /// https://binance-docs.github.io/apidocs/spot/en/#signed-endpoint-examples-for-post-api-v3-order
236    fn reproduce_example_signature() {
237        let params = url_query!(
238            symbol = "LTCBTC",
239            side = "BUY",
240            type = "LIMIT",
241            timeInForce = "GTC",
242            quantity = 1,
243            price = 0.1,
244            recvWindow = 5000,
245            timestamp = 1499827319559_u64,
246        );
247
248        let key = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
249        let signature = sign_query(&params, key).unwrap();
250
251        assert_eq!(
252            signature,
253            "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71"
254        );
255    }
256}