Skip to main content

binance_api_client/
client.rs

1use reqwest::StatusCode;
2use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, USER_AGENT};
3use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
4use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
5use reqwest_tracing::TracingMiddleware;
6use serde::de::DeserializeOwned;
7
8use crate::config::Config;
9use crate::credentials::{Credentials, build_signed_query_string};
10use crate::error::{BinanceApiError, Error, Result};
11
12/// HTTP client for Binance REST API.
13#[derive(Clone)]
14pub struct Client {
15    http: ClientWithMiddleware,
16    config: Config,
17    credentials: Option<Credentials>,
18}
19
20impl Client {
21    /// Create a new authenticated client.
22    pub fn new(config: Config, credentials: Credentials) -> Result<Self> {
23        Self::build(config, Some(credentials))
24    }
25
26    /// Create a new unauthenticated client for public endpoints only.
27    pub fn new_unauthenticated(config: Config) -> Result<Self> {
28        Self::build(config, None)
29    }
30
31    fn build(config: Config, credentials: Option<Credentials>) -> Result<Self> {
32        let mut builder = reqwest::Client::builder();
33
34        if let Some(timeout) = config.timeout {
35            builder = builder.timeout(timeout);
36        }
37
38        let reqwest_client = builder.build()?;
39
40        // Set up retry policy for transient errors
41        let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
42
43        let http = ClientBuilder::new(reqwest_client)
44            .with(TracingMiddleware::default())
45            .with(RetryTransientMiddleware::new_with_policy(retry_policy))
46            .build();
47
48        Ok(Self {
49            http,
50            config,
51            credentials,
52        })
53    }
54
55    /// Get the current configuration.
56    pub fn config(&self) -> &Config {
57        &self.config
58    }
59
60    /// Check if this client has credentials.
61    pub fn has_credentials(&self) -> bool {
62        self.credentials.is_some()
63    }
64
65    /// Make an unsigned GET request (for public endpoints).
66    pub async fn get<T: DeserializeOwned>(&self, endpoint: &str, query: Option<&str>) -> Result<T> {
67        let url = match query {
68            Some(q) => format!("{}{}?{}", self.config.rest_api_endpoint, endpoint, q),
69            None => format!("{}{}", self.config.rest_api_endpoint, endpoint),
70        };
71
72        let response = self.http.get(&url).send().await?;
73        self.handle_response(response).await
74    }
75
76    /// Make an unsigned GET request with query parameters as key-value pairs.
77    pub async fn get_with_params<T: DeserializeOwned>(
78        &self,
79        endpoint: &str,
80        params: &[(&str, &str)],
81    ) -> Result<T> {
82        let query = if params.is_empty() {
83            None
84        } else {
85            Some(
86                params
87                    .iter()
88                    .map(|(k, v)| format!("{}={}", k, v))
89                    .collect::<Vec<_>>()
90                    .join("&"),
91            )
92        };
93
94        self.get(endpoint, query.as_deref()).await
95    }
96
97    /// Make a GET request with API key but no signature.
98    ///
99    /// Used for endpoints like historical trades that require authentication
100    /// but not request signing.
101    pub async fn get_with_api_key<T: DeserializeOwned>(
102        &self,
103        endpoint: &str,
104        query: Option<&str>,
105    ) -> Result<T> {
106        let credentials = self
107            .credentials
108            .as_ref()
109            .ok_or(Error::AuthenticationRequired)?;
110
111        let url = match query {
112            Some(q) => format!("{}{}?{}", self.config.rest_api_endpoint, endpoint, q),
113            None => format!("{}{}", self.config.rest_api_endpoint, endpoint),
114        };
115
116        let response = self
117            .http
118            .get(&url)
119            .headers(self.build_auth_headers(credentials)?)
120            .send()
121            .await?;
122
123        self.handle_response(response).await
124    }
125
126    /// Make a signed GET request (requires credentials).
127    pub async fn get_signed<T: DeserializeOwned>(
128        &self,
129        endpoint: &str,
130        params: &[(&str, &str)],
131    ) -> Result<T> {
132        let credentials = self
133            .credentials
134            .as_ref()
135            .ok_or(Error::AuthenticationRequired)?;
136
137        let query = build_signed_query_string(
138            params.iter().copied(),
139            credentials,
140            self.config.recv_window,
141        )?;
142
143        let url = format!("{}{}?{}", self.config.rest_api_endpoint, endpoint, query);
144
145        let response = self
146            .http
147            .get(&url)
148            .headers(self.build_auth_headers(credentials)?)
149            .send()
150            .await?;
151
152        self.handle_response(response).await
153    }
154
155    /// Make a signed POST request (requires credentials).
156    pub async fn post_signed<T: DeserializeOwned>(
157        &self,
158        endpoint: &str,
159        params: &[(&str, &str)],
160    ) -> Result<T> {
161        let credentials = self
162            .credentials
163            .as_ref()
164            .ok_or(Error::AuthenticationRequired)?;
165
166        let query = build_signed_query_string(
167            params.iter().copied(),
168            credentials,
169            self.config.recv_window,
170        )?;
171
172        let url = format!("{}{}?{}", self.config.rest_api_endpoint, endpoint, query);
173
174        let response = self
175            .http
176            .post(&url)
177            .headers(self.build_auth_headers_with_content_type(credentials)?)
178            .send()
179            .await?;
180
181        self.handle_response(response).await
182    }
183
184    /// Make a signed POST request and return the raw response.
185    pub async fn post_signed_raw(
186        &self,
187        endpoint: &str,
188        params: &[(&str, &str)],
189    ) -> Result<reqwest::Response> {
190        let credentials = self
191            .credentials
192            .as_ref()
193            .ok_or(Error::AuthenticationRequired)?;
194
195        let query = build_signed_query_string(
196            params.iter().copied(),
197            credentials,
198            self.config.recv_window,
199        )?;
200
201        let url = format!("{}{}?{}", self.config.rest_api_endpoint, endpoint, query);
202
203        let response = self
204            .http
205            .post(&url)
206            .headers(self.build_auth_headers_with_content_type(credentials)?)
207            .send()
208            .await?;
209
210        Ok(response)
211    }
212
213    /// Make a signed DELETE request (requires credentials).
214    pub async fn delete_signed<T: DeserializeOwned>(
215        &self,
216        endpoint: &str,
217        params: &[(&str, &str)],
218    ) -> Result<T> {
219        let credentials = self
220            .credentials
221            .as_ref()
222            .ok_or(Error::AuthenticationRequired)?;
223
224        let query = build_signed_query_string(
225            params.iter().copied(),
226            credentials,
227            self.config.recv_window,
228        )?;
229
230        let url = format!("{}{}?{}", self.config.rest_api_endpoint, endpoint, query);
231
232        let response = self
233            .http
234            .delete(&url)
235            .headers(self.build_auth_headers_with_content_type(credentials)?)
236            .send()
237            .await?;
238
239        self.handle_response(response).await
240    }
241
242    /// Make a signed PUT request (requires credentials).
243    pub async fn put_signed<T: DeserializeOwned>(
244        &self,
245        endpoint: &str,
246        params: &[(&str, &str)],
247    ) -> Result<T> {
248        let credentials = self
249            .credentials
250            .as_ref()
251            .ok_or(Error::AuthenticationRequired)?;
252
253        let query = build_signed_query_string(
254            params.iter().copied(),
255            credentials,
256            self.config.recv_window,
257        )?;
258
259        let url = format!("{}{}?{}", self.config.rest_api_endpoint, endpoint, query);
260
261        let response = self
262            .http
263            .put(&url)
264            .headers(self.build_auth_headers_with_content_type(credentials)?)
265            .send()
266            .await?;
267
268        self.handle_response(response).await
269    }
270
271    /// Make a POST request with API key but no signature (for user stream endpoints).
272    pub async fn post_with_key<T: DeserializeOwned>(
273        &self,
274        endpoint: &str,
275        params: &[(&str, &str)],
276    ) -> Result<T> {
277        let credentials = self
278            .credentials
279            .as_ref()
280            .ok_or(Error::AuthenticationRequired)?;
281
282        let url = if params.is_empty() {
283            format!("{}{}", self.config.rest_api_endpoint, endpoint)
284        } else {
285            let query = params
286                .iter()
287                .map(|(k, v)| format!("{}={}", k, v))
288                .collect::<Vec<_>>()
289                .join("&");
290            format!("{}{}?{}", self.config.rest_api_endpoint, endpoint, query)
291        };
292
293        let response = self
294            .http
295            .post(&url)
296            .headers(self.build_auth_headers(credentials)?)
297            .send()
298            .await?;
299
300        self.handle_response(response).await
301    }
302
303    /// Make a PUT request with API key but no signature (for user stream keepalive).
304    pub async fn put_with_key<T: DeserializeOwned>(
305        &self,
306        endpoint: &str,
307        params: &[(&str, &str)],
308    ) -> Result<T> {
309        let credentials = self
310            .credentials
311            .as_ref()
312            .ok_or(Error::AuthenticationRequired)?;
313
314        let url = if params.is_empty() {
315            format!("{}{}", self.config.rest_api_endpoint, endpoint)
316        } else {
317            let query = params
318                .iter()
319                .map(|(k, v)| format!("{}={}", k, v))
320                .collect::<Vec<_>>()
321                .join("&");
322            format!("{}{}?{}", self.config.rest_api_endpoint, endpoint, query)
323        };
324
325        let response = self
326            .http
327            .put(&url)
328            .headers(self.build_auth_headers(credentials)?)
329            .send()
330            .await?;
331
332        self.handle_response(response).await
333    }
334
335    /// Make a DELETE request with API key but no signature (for user stream close).
336    pub async fn delete_with_key<T: DeserializeOwned>(
337        &self,
338        endpoint: &str,
339        params: &[(&str, &str)],
340    ) -> Result<T> {
341        let credentials = self
342            .credentials
343            .as_ref()
344            .ok_or(Error::AuthenticationRequired)?;
345
346        let url = if params.is_empty() {
347            format!("{}{}", self.config.rest_api_endpoint, endpoint)
348        } else {
349            let query = params
350                .iter()
351                .map(|(k, v)| format!("{}={}", k, v))
352                .collect::<Vec<_>>()
353                .join("&");
354            format!("{}{}?{}", self.config.rest_api_endpoint, endpoint, query)
355        };
356
357        let response = self
358            .http
359            .delete(&url)
360            .headers(self.build_auth_headers(credentials)?)
361            .send()
362            .await?;
363
364        self.handle_response(response).await
365    }
366
367    fn build_auth_headers(&self, credentials: &Credentials) -> Result<HeaderMap> {
368        let mut headers = HeaderMap::new();
369        headers.insert(USER_AGENT, HeaderValue::from_static("binance-api-client-rs"));
370        headers.insert(
371            HeaderName::from_static("x-mbx-apikey"),
372            HeaderValue::from_str(credentials.api_key())?,
373        );
374        Ok(headers)
375    }
376
377    fn build_auth_headers_with_content_type(&self, credentials: &Credentials) -> Result<HeaderMap> {
378        let mut headers = self.build_auth_headers(credentials)?;
379        headers.insert(
380            CONTENT_TYPE,
381            HeaderValue::from_static("application/x-www-form-urlencoded"),
382        );
383        Ok(headers)
384    }
385
386    async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
387        match response.status() {
388            StatusCode::OK => Ok(response.json().await?),
389            StatusCode::INTERNAL_SERVER_ERROR => Err(Error::Api {
390                code: 500,
391                message: "Internal server error".to_string(),
392            }),
393            StatusCode::SERVICE_UNAVAILABLE => Err(Error::Api {
394                code: 503,
395                message: "Service unavailable".to_string(),
396            }),
397            StatusCode::UNAUTHORIZED => Err(Error::Api {
398                code: 401,
399                message: "Unauthorized".to_string(),
400            }),
401            StatusCode::BAD_REQUEST | StatusCode::FORBIDDEN | StatusCode::TOO_MANY_REQUESTS => {
402                let error: BinanceApiError = response.json().await?;
403                Err(Error::from_binance_error(error))
404            }
405            status => Err(Error::Api {
406                code: status.as_u16() as i32,
407                message: format!("Unexpected status code: {}", status),
408            }),
409        }
410    }
411}
412
413impl std::fmt::Debug for Client {
414    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
415        f.debug_struct("Client")
416            .field("config", &self.config)
417            .field("has_credentials", &self.credentials.is_some())
418            .finish()
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425    use std::time::Duration;
426
427    #[test]
428    fn test_client_new_unauthenticated() {
429        let config = Config::default();
430        let client = Client::new_unauthenticated(config).unwrap();
431        assert!(!client.has_credentials());
432    }
433
434    #[test]
435    fn test_client_new_authenticated() {
436        let config = Config::default();
437        let creds = Credentials::new("api_key", "secret_key");
438        let client = Client::new(config, creds).unwrap();
439        assert!(client.has_credentials());
440    }
441
442    #[test]
443    fn test_client_with_timeout() {
444        let config = Config::builder().timeout(Duration::from_secs(30)).build();
445        let client = Client::new_unauthenticated(config.clone()).unwrap();
446        assert_eq!(client.config().timeout, Some(Duration::from_secs(30)));
447    }
448
449    #[test]
450    fn test_client_debug() {
451        let config = Config::default();
452        let creds = Credentials::new("api_key", "secret_key");
453        let client = Client::new(config, creds).unwrap();
454        let debug_output = format!("{:?}", client);
455        assert!(debug_output.contains("has_credentials: true"));
456        assert!(!debug_output.contains("secret_key"));
457    }
458}