exc_binance/http/request/
mod.rs1use std::fmt;
2
3use http::{HeaderValue, Method, Request};
4
5use crate::types::key::BinanceKey;
6
7use super::error::RestError;
8
9pub mod utils;
11
12pub mod instrument;
14
15pub mod candle;
17
18pub mod listen_key;
20
21pub mod trading;
23
24pub mod account;
26
27pub use self::{
28 account::{
29 GetSubAccountAssets, GetSubAccountFutures, GetSubAccountFuturesPositions,
30 GetSubAccountMargin, ListSubAccounts,
31 },
32 candle::{Interval, QueryCandles},
33 instrument::ExchangeInfo,
34 listen_key::{CurrentListenKey, DeleteListenKey},
35};
36
37pub trait Rest: Send + Sync + 'static {
39 fn method(&self, endpoint: &RestEndpoint) -> Result<Method, RestError>;
41
42 fn to_path(&self, endpoint: &RestEndpoint) -> Result<String, RestError>;
44
45 fn add_headers(
47 &self,
48 _endpoint: &RestEndpoint,
49 _headers: &mut hyper::HeaderMap,
50 ) -> Result<(), RestError> {
51 Ok(())
52 }
53
54 fn need_apikey(&self) -> bool {
56 false
57 }
58
59 fn need_sign(&self) -> bool {
61 false
62 }
63
64 fn serialize(&self, _endpoint: &RestEndpoint) -> Result<serde_json::Value, RestError> {
66 Ok(serde_json::json!({}))
67 }
68
69 fn to_payload(&self) -> Payload;
71}
72
73pub struct Payload {
75 inner: Box<dyn Rest>,
76}
77
78impl Clone for Payload {
79 fn clone(&self) -> Self {
80 self.inner.to_payload()
81 }
82}
83
84impl Payload {
85 pub fn new<T>(inner: T) -> Self
87 where
88 T: Rest,
89 {
90 Self {
91 inner: Box::new(inner),
92 }
93 }
94}
95
96impl Rest for Payload {
97 fn method(&self, endpoint: &RestEndpoint) -> Result<Method, RestError> {
98 self.inner.method(endpoint)
99 }
100
101 fn add_headers(
102 &self,
103 endpoint: &RestEndpoint,
104 headers: &mut hyper::HeaderMap,
105 ) -> Result<(), RestError> {
106 self.inner.add_headers(endpoint, headers)
107 }
108
109 fn to_path(&self, endpoint: &RestEndpoint) -> Result<String, RestError> {
110 self.inner.to_path(endpoint)
111 }
112
113 fn need_apikey(&self) -> bool {
114 self.inner.need_apikey()
115 }
116
117 fn need_sign(&self) -> bool {
118 self.inner.need_sign()
119 }
120
121 fn serialize(&self, endpoint: &RestEndpoint) -> Result<serde_json::Value, RestError> {
122 self.inner.serialize(endpoint)
123 }
124
125 fn to_payload(&self) -> Payload {
126 self.clone()
127 }
128}
129
130#[derive(Debug, Clone, Copy)]
132pub enum MarginOp {
133 Loan,
135 Repay,
137}
138
139#[derive(Debug, Clone, Copy)]
141pub struct MarginOptions {
142 pub buy: Option<MarginOp>,
144 pub sell: Option<MarginOp>,
146}
147
148#[derive(Debug, Clone, Copy, Default)]
150pub struct SpotOptions {
151 pub margin: Option<MarginOptions>,
153}
154
155impl SpotOptions {
156 pub fn with_margin(buy: Option<MarginOp>, sell: Option<MarginOp>) -> Self {
158 Self {
159 margin: Some(MarginOptions { buy, sell }),
160 }
161 }
162}
163
164#[derive(Debug, Clone, Copy)]
166pub enum RestEndpoint {
167 UsdMarginFutures,
169 Spot(SpotOptions),
172 EuropeanOptions,
174}
175
176impl fmt::Display for RestEndpoint {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 match self {
179 Self::UsdMarginFutures => write!(f, "binance-u"),
180 Self::Spot(_) => write!(f, "binance-s"),
181 Self::EuropeanOptions => write!(f, "binance-e"),
182 }
183 }
184}
185
186impl RestEndpoint {
187 pub fn host(&self) -> &'static str {
189 match self {
190 Self::UsdMarginFutures => "https://fapi.binance.com",
191 Self::Spot(_) => "https://api.binance.com",
192 Self::EuropeanOptions => "https://eapi.binance.com",
193 }
194 }
195}
196
197#[derive(Debug, Clone)]
199pub struct RestRequest<T> {
200 payload: T,
201}
202
203impl RestRequest<Payload> {
204 pub fn with_payload<T>(payload: T) -> Self
206 where
207 T: Rest,
208 {
209 Self::from(Payload::new(payload))
210 }
211}
212
213impl<T: Rest> RestRequest<T> {
214 pub(crate) fn to_http(
215 &self,
216 endpoint: &RestEndpoint,
217 key: Option<&BinanceKey>,
218 ) -> Result<Request<hyper::Body>, RestError> {
219 let mut uri = format!("{}{}", endpoint.host(), self.payload.to_path(endpoint)?);
220 tracing::trace!("building http request: uri={uri}");
221 let value = self.payload.serialize(endpoint)?;
222 let body = if self.payload.need_sign() {
223 if let Some(key) = key.as_ref() {
224 let value = key.sign(value)?;
225 let s = serde_urlencoded::to_string(value)?;
226 tracing::trace!("params: {s}");
227 match self.payload.method(endpoint)? {
228 http::Method::GET => {
229 uri.push('?');
231 uri.push_str(&s);
232 hyper::Body::empty()
233 }
234 _ => hyper::Body::from(s),
235 }
236 } else {
237 return Err(RestError::NeedApikey);
238 }
239 } else {
240 hyper::Body::from(serde_urlencoded::to_string(&value)?)
241 };
242 let method = self.payload.method(endpoint)?;
243
244 let mut builder = Request::builder().method(method.clone()).uri(uri);
245
246 if !matches!(
248 (endpoint, method),
249 (RestEndpoint::EuropeanOptions, Method::GET)
250 ) {
251 builder = builder.header("content-type", "application/x-www-form-urlencoded");
252 }
253
254 let mut request = builder.body(body)?;
255 let headers = request.headers_mut();
256 if let Some(key) = key {
257 if self.payload.need_apikey() {
258 headers.insert("X-MBX-APIKEY", HeaderValue::from_str(&key.apikey)?);
259 }
260 }
261 self.payload.add_headers(endpoint, headers)?;
262 Ok(request)
263 }
264}
265
266impl<T: Rest> From<T> for RestRequest<T> {
267 fn from(payload: T) -> Self {
268 Self { payload }
269 }
270}
271
272#[cfg(all(test, not(feature = "ci")))]
273mod test {
274 use std::env::var;
275 use tower::ServiceExt;
276
277 use crate::{
278 http::{request, response},
279 types::key::BinanceKey,
280 Binance, Request,
281 };
282
283 async fn do_test_exchange_info(api: Binance) -> anyhow::Result<()> {
284 let resp = api
285 .oneshot(Request::with_rest_payload(request::ExchangeInfo))
286 .await?
287 .into_response::<response::ExchangeInfo>()?;
288 println!("{:?}", resp);
289 Ok(())
290 }
291
292 async fn do_test_candle(api: Binance, inst: &str) -> anyhow::Result<()> {
293 let resp = api
294 .oneshot(Request::with_rest_payload(request::QueryCandles {
295 symbol: inst.to_uppercase(),
296 interval: request::Interval::M1,
297 start_time: None,
298 end_time: None,
299 limit: None,
300 }))
301 .await?
302 .into_response::<response::Candles>()?;
303 for c in resp {
304 println!("{c:?}");
305 }
306 Ok(())
307 }
308
309 async fn do_test_listen_key(api: Binance) -> anyhow::Result<()> {
310 let listen_key = api
311 .oneshot(Request::with_rest_payload(request::CurrentListenKey))
312 .await?
313 .into_response::<response::ListenKey>()?;
314 println!("{listen_key}");
315 Ok(())
316 }
317
318 async fn do_test_delete_listen_key(
319 api: Binance,
320 listen_key: Option<String>,
321 ) -> anyhow::Result<()> {
322 let listen_key = api
323 .oneshot(Request::with_rest_payload(request::DeleteListenKey {
324 listen_key,
325 }))
326 .await?
327 .into_response::<response::Unknown>()?;
328 println!("{listen_key:?}");
329 Ok(())
330 }
331
332 async fn do_test_list_sub_accounts(api: Binance) -> anyhow::Result<response::SubAccounts> {
333 let sub_accounts = api
334 .oneshot(Request::with_rest_payload(
335 request::ListSubAccounts::default(),
336 ))
337 .await?
338 .into_response::<response::SubAccounts>()?;
339 println!("{sub_accounts:?}");
340 Ok(sub_accounts)
341 }
342
343 async fn do_test_get_sub_account_assets(api: Binance, email: &str) -> anyhow::Result<()> {
344 let assets = api
345 .oneshot(Request::with_rest_payload(request::GetSubAccountAssets {
346 email: email.to_string(),
347 }))
348 .await?
349 .into_response::<response::SubAccountBalances>()?;
350 println!("{assets:?}");
351 Ok(())
352 }
353
354 async fn do_test_get_sub_account_margin(api: Binance, email: &str) -> anyhow::Result<()> {
355 let assets = api
356 .oneshot(Request::with_rest_payload(request::GetSubAccountMargin {
357 email: email.to_string(),
358 }))
359 .await?
360 .into_response::<response::SubAccountMargin>()?;
361 println!("{assets:?}");
362 Ok(())
363 }
364
365 async fn do_test_get_sub_account_futures(
366 api: Binance,
367 email: &str,
368 usd: bool,
369 ) -> anyhow::Result<()> {
370 let assets = api
371 .oneshot(Request::with_rest_payload(if usd {
372 request::GetSubAccountFutures::usd(email)
373 } else {
374 request::GetSubAccountFutures::coin(email)
375 }))
376 .await?
377 .into_response::<response::SubAccountFutures>()?;
378 println!("{assets:?}");
379 Ok(())
380 }
381
382 async fn do_test_get_sub_account_futures_positions(
383 api: Binance,
384 email: &str,
385 usd: bool,
386 ) -> anyhow::Result<()> {
387 let positions = api
388 .oneshot(Request::with_rest_payload(if usd {
389 request::GetSubAccountFuturesPositions::usd(email)
390 } else {
391 request::GetSubAccountFuturesPositions::coin(email)
392 }))
393 .await?
394 .into_response::<response::SubAccountFuturesPositions>()?;
395 println!("{positions:?}");
396 Ok(())
397 }
398
399 #[tokio::test]
400 async fn test_exchange_info() -> anyhow::Result<()> {
401 let apis = [
402 Binance::usd_margin_futures().connect(),
403 Binance::spot().connect(),
404 ];
405 for api in apis {
406 do_test_exchange_info(api).await?;
407 }
408 Ok(())
409 }
410
411 #[tokio::test]
412 async fn test_candle() -> anyhow::Result<()> {
413 let apis = [
414 (Binance::usd_margin_futures().connect(), "btcusdt"),
415 (Binance::spot().connect(), "btcusdt"),
416 ];
417 for (api, inst) in apis {
418 do_test_candle(api, inst).await?;
419 }
420 Ok(())
421 }
422
423 #[tokio::test]
424 async fn test_listen_key() -> anyhow::Result<()> {
425 if let Ok(key) = var("BINANCE_KEY") {
426 let key = serde_json::from_str::<BinanceKey>(&key)?;
427 let apis = [
428 Binance::usd_margin_futures().private(key.clone()).connect(),
429 Binance::spot().private(key).connect(),
430 ];
431 for api in apis {
432 do_test_listen_key(api).await?;
433 }
434 }
435 Ok(())
436 }
437
438 #[tokio::test]
439 async fn test_delete_listen_key() -> anyhow::Result<()> {
440 if let Ok(key) = var("BINANCE_KEY") {
441 let key = serde_json::from_str::<BinanceKey>(&key)?;
442 let apis = [
443 (
444 Binance::usd_margin_futures().private(key.clone()).connect(),
445 false,
446 ),
447 (Binance::spot().private(key).connect(), true),
448 ];
449 for (mut api, listen_key) in apis {
450 let listen_key = if listen_key {
451 let listen_key = (&mut api)
452 .oneshot(Request::with_rest_payload(request::CurrentListenKey))
453 .await?
454 .into_response::<response::ListenKey>()?;
455 Some(listen_key.to_string())
456 } else {
457 None
458 };
459 do_test_delete_listen_key(api, listen_key).await?;
460 }
461 }
462 Ok(())
463 }
464
465 #[tokio::test]
466 async fn test_list_sub_accounts() -> anyhow::Result<()> {
467 if let Ok(key) = var("BINANCE_MAIN") {
468 let key = serde_json::from_str::<BinanceKey>(&key)?;
469 let api = Binance::spot().private(key).connect();
470 do_test_list_sub_accounts(api).await?;
471 }
472 Ok(())
473 }
474
475 #[tokio::test]
476 async fn test_get_sub_account_assets() -> anyhow::Result<()> {
477 if let Ok(key) = var("BINANCE_MAIN") {
478 let key = serde_json::from_str::<BinanceKey>(&key)?;
479 let api = Binance::spot().private(key).connect();
480 let sub_accounts = do_test_list_sub_accounts(api.clone()).await?;
481 for account in sub_accounts.sub_accounts {
482 do_test_get_sub_account_assets(api.clone(), &account.email).await?;
483 }
484 }
485 Ok(())
486 }
487
488 #[tokio::test]
489 async fn test_get_sub_account_margin() -> anyhow::Result<()> {
490 if let Ok(key) = var("BINANCE_MAIN") {
491 let key = serde_json::from_str::<BinanceKey>(&key)?;
492 let api = Binance::spot().private(key).connect();
493 let sub_accounts = do_test_list_sub_accounts(api.clone()).await?;
494 for account in sub_accounts.sub_accounts {
495 do_test_get_sub_account_margin(api.clone(), &account.email).await?;
496 }
497 }
498 Ok(())
499 }
500
501 #[tokio::test]
502 async fn test_get_sub_account_futures() -> anyhow::Result<()> {
503 if let Ok(key) = var("BINANCE_MAIN") {
504 let key = serde_json::from_str::<BinanceKey>(&key)?;
505 let api = Binance::spot().private(key).connect();
506 let sub_accounts = do_test_list_sub_accounts(api.clone()).await?;
507 for account in sub_accounts.sub_accounts {
508 do_test_get_sub_account_futures(api.clone(), &account.email, true).await?;
509 do_test_get_sub_account_futures(api.clone(), &account.email, false).await?;
510 }
511 }
512 Ok(())
513 }
514
515 #[tokio::test]
516 async fn test_get_sub_account_futures_positions() -> anyhow::Result<()> {
517 if let Ok(key) = var("BINANCE_MAIN") {
518 let key = serde_json::from_str::<BinanceKey>(&key)?;
519 let api = Binance::spot().private(key).connect();
520 let sub_accounts = do_test_list_sub_accounts(api.clone()).await?;
521 for account in sub_accounts.sub_accounts {
522 do_test_get_sub_account_futures_positions(api.clone(), &account.email, true)
523 .await?;
524 do_test_get_sub_account_futures_positions(api.clone(), &account.email, false)
525 .await?;
526 }
527 }
528 Ok(())
529 }
530}