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#[derive(Clone)]
14pub struct Client {
15 http: ClientWithMiddleware,
16 config: Config,
17 credentials: Option<Credentials>,
18}
19
20impl Client {
21 pub fn new(config: Config, credentials: Credentials) -> Result<Self> {
23 Self::build(config, Some(credentials))
24 }
25
26 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 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 pub fn config(&self) -> &Config {
57 &self.config
58 }
59
60 pub fn has_credentials(&self) -> bool {
62 self.credentials.is_some()
63 }
64
65 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 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 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 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 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 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 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 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 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 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 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}