exc_binance/http/request/
mod.rs

1use std::fmt;
2
3use http::{HeaderValue, Method, Request};
4
5use crate::types::key::BinanceKey;
6
7use super::error::RestError;
8
9/// Utils.
10pub mod utils;
11
12/// Instrument.
13pub mod instrument;
14
15/// Candle.
16pub mod candle;
17
18/// Listen key.
19pub mod listen_key;
20
21/// Trading.
22pub mod trading;
23
24/// Account.
25pub 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
37/// Rest payload.
38pub trait Rest: Send + Sync + 'static {
39    /// Get request method.
40    fn method(&self, endpoint: &RestEndpoint) -> Result<Method, RestError>;
41
42    /// Get request path.
43    fn to_path(&self, endpoint: &RestEndpoint) -> Result<String, RestError>;
44
45    /// add request header.
46    fn add_headers(
47        &self,
48        _endpoint: &RestEndpoint,
49        _headers: &mut hyper::HeaderMap,
50    ) -> Result<(), RestError> {
51        Ok(())
52    }
53
54    /// Whether need apikey.
55    fn need_apikey(&self) -> bool {
56        false
57    }
58
59    /// Whether need sign.
60    fn need_sign(&self) -> bool {
61        false
62    }
63
64    /// Serialize.
65    fn serialize(&self, _endpoint: &RestEndpoint) -> Result<serde_json::Value, RestError> {
66        Ok(serde_json::json!({}))
67    }
68
69    /// Clone.
70    fn to_payload(&self) -> Payload;
71}
72
73/// Payload.
74pub 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    /// Create a payload from a [`Rest`].
86    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/// Margin mode.
131#[derive(Debug, Clone, Copy)]
132pub enum MarginOp {
133    /// Loan.
134    Loan,
135    /// Repay.
136    Repay,
137}
138
139/// Margin options.
140#[derive(Debug, Clone, Copy)]
141pub struct MarginOptions {
142    /// Buy.
143    pub buy: Option<MarginOp>,
144    /// Sell.
145    pub sell: Option<MarginOp>,
146}
147
148/// Spot options.
149#[derive(Debug, Clone, Copy, Default)]
150pub struct SpotOptions {
151    /// Enable margin.
152    pub margin: Option<MarginOptions>,
153}
154
155impl SpotOptions {
156    /// With margin.
157    pub fn with_margin(buy: Option<MarginOp>, sell: Option<MarginOp>) -> Self {
158        Self {
159            margin: Some(MarginOptions { buy, sell }),
160        }
161    }
162}
163
164/// Binance rest api endpoints.
165#[derive(Debug, Clone, Copy)]
166pub enum RestEndpoint {
167    /// USD-M Futures.
168    UsdMarginFutures,
169    /// Spot.
170    /// Set it to `true` to enable margin trading.
171    Spot(SpotOptions),
172    /// European options.
173    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    /// Get host.
188    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/// Binance rest requests.
198#[derive(Debug, Clone)]
199pub struct RestRequest<T> {
200    payload: T,
201}
202
203impl RestRequest<Payload> {
204    /// Create a rest request with given payload.
205    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                        // FIXME: this is too dirty.
230                        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        // FIXME: This is required by binance european options for now.
247        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}