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)]
16pub 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 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 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 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 _ => {
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]
195macro_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 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(¶ms, key).unwrap();
250
251 assert_eq!(
252 signature,
253 "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71"
254 );
255 }
256}