crypto_botters_binance/
lib.rs

1//! A crate for communicating with the [Binance API](https://binance-docs.github.io/apidocs/spot/en/).
2//! For example usages, see files in the examples/ directory.
3
4use std::{
5    str::FromStr,
6    marker::PhantomData,
7    time::{SystemTime, Duration},
8};
9use hmac::{Hmac, Mac};
10use sha2::Sha256;
11use serde::{de::DeserializeOwned, Deserialize, Serialize};
12use crypto_botters_api::*;
13use generic_api_client::{http::*, websocket::*};
14
15/// The type returned by [Client::request()].
16pub type BinanceRequestResult<T> = Result<T, BinanceRequestError>;
17pub type BinanceRequestError = RequestError<&'static str, BinanceHandlerError>;
18
19/// Options that can be set when creating handlers
20pub enum BinanceOption {
21    /// [Default] variant, does nothing
22    Default,
23    /// API key
24    Key(String),
25    /// Api secret
26    Secret(String),
27    /// Base url for HTTP requests
28    HttpUrl(BinanceHttpUrl),
29    /// Authentication type for HTTP requests
30    HttpAuth(BinanceAuth),
31    /// [RequestConfig] used when sending requests.
32    /// `url_prefix` will be overridden by [HttpUrl](Self::HttpUrl) unless `HttpUrl` is [BinanceHttpUrl::None].
33    RequestConfig(RequestConfig),
34    /// Base url for WebSocket connections
35    WebSocketUrl(BinanceWebSocketUrl),
36    /// [WebSocketConfig] used for creating [WebSocketConnection]s
37    /// `url_prefix` will be overridden by [WebSocketUrl](Self::WebSocketUrl) unless `WebSocketUrl` is [BinanceWebSocketUrl::None].
38    /// By default, `refresh_after` is set to 12 hours and `ignore_duplicate_during_reconnection` is set to `true`.
39    WebSocketConfig(WebSocketConfig),
40}
41
42/// A `struct` that represents a set of [BinanceOption] s.
43#[derive(Clone, Debug)]
44pub struct BinanceOptions {
45    /// see [BinanceOption::Key]
46    pub key: Option<String>,
47    /// see [BinanceOption::Secret]
48    pub secret: Option<String>,
49    /// see [BinanceOption::HttpUrl]
50    pub http_url: BinanceHttpUrl,
51    /// see [BinanceOption::HttpAuth]
52    pub http_auth: BinanceAuth,
53    /// see [BinanceOption::RequestConfig]
54    pub request_config: RequestConfig,
55    /// see [BinanceOption::WebSocketUrl]
56    pub websocket_url: BinanceWebSocketUrl,
57    /// see [BinanceOption::WebSocketConfig]
58    pub websocket_config: WebSocketConfig,
59}
60
61/// A `enum` that represents the base url of the Binance REST API.
62#[derive(Debug, Eq, PartialEq, Copy, Clone)]
63#[non_exhaustive]
64pub enum BinanceHttpUrl {
65    /// `https://api.binance.com`
66    Spot,
67    /// `https://api1.binance.com`
68    Spot1,
69    /// `https://api2.binance.com`
70    Spot2,
71    /// `https://api3.binance.com`
72    Spot3,
73    /// `https://api4.binance.com`
74    Spot4,
75    /// `https://testnet.binance.vision`
76    SpotTest,
77    /// `https://data.binance.com`
78    SpotData,
79    /// `https://fapi.binance.com`
80    FuturesUsdM,
81    /// `https://dapi.binance.com`
82    FuturesCoinM,
83    /// `https://testnet.binancefuture.com`
84    FuturesTest,
85    /// `https://eapi.binance.com`
86    EuropeanOptions,
87    /// The url will not be modified by [BinanceRequestHandler]
88    None,
89}
90
91/// A `enum` that represents the base url of the Binance WebSocket API
92#[derive(Debug, Eq, PartialEq, Copy, Clone)]
93#[non_exhaustive]
94pub enum BinanceWebSocketUrl {
95    /// `wss://stream.binance.com:9443`
96    Spot9443,
97    /// `wss://stream.binance.com:443`
98    Spot443,
99    /// `wss://testnet.binance.vision`
100    SpotTest,
101    /// `wss://data-stream.binance.com`
102    SpotData,
103    /// `wss://ws-api.binance.com:443`
104    WebSocket443,
105    /// `wss://ws-api.binance.com:9443`
106    WebSocket9443,
107    /// `wss://fstream.binance.com`
108    FuturesUsdM,
109    /// `wss://fstream-auth.binance.com`
110    FuturesUsdMAuth,
111    /// `wss://dstream.binance.com`
112    FuturesCoinM,
113    /// `wss://stream.binancefuture.com`
114    FuturesUsdMTest,
115    /// `wss://dstream.binancefuture.com`
116    FuturesCoinMTest,
117    /// `wss://nbstream.binance.com`
118    EuropeanOptions,
119    /// The url will not be modified by [BinanceRequestHandler]
120    None,
121}
122
123#[derive(Debug, Eq, PartialEq, Copy, Clone)]
124pub enum BinanceAuth {
125    Sign,
126    Key,
127    None,
128}
129
130#[derive(Debug)]
131pub enum BinanceHandlerError {
132    ApiError(BinanceError),
133    RateLimitError { retry_after: Option<u32> },
134    ParseError,
135}
136
137#[derive(Deserialize, Debug)]
138pub struct BinanceError {
139    pub code: i32,
140    pub msg: String,
141}
142
143/// A `struct` that implements [RequestHandler]
144pub struct BinanceRequestHandler<'a, R: DeserializeOwned> {
145    options: BinanceOptions,
146    _phantom: PhantomData<&'a R>,
147}
148
149/// A `struct` that implements [WebSocketHandler]
150pub struct BinanceWebSocketHandler<H: FnMut(serde_json::Value) + Send + 'static> {
151    message_handler: H,
152    options: BinanceOptions,
153}
154
155// https://binance-docs.github.io/apidocs/spot/en/#general-api-information
156impl<'a, B, R> RequestHandler<B> for BinanceRequestHandler<'a, R>
157where
158    B: Serialize,
159    R: DeserializeOwned,
160{
161    type Successful = R;
162    type Unsuccessful = BinanceHandlerError;
163    type BuildError = &'static str;
164
165    fn request_config(&self) -> RequestConfig {
166        let mut config = self.options.request_config.clone();
167        if self.options.http_url != BinanceHttpUrl::None {
168            config.url_prefix = self.options.http_url.as_str().to_owned();
169        }
170        config
171    }
172
173    fn build_request(&self, mut builder: RequestBuilder, request_body: &Option<B>, _: u8) -> Result<Request, Self::BuildError> {
174        if let Some(body) = request_body {
175            let encoded = serde_urlencoded::to_string(body).or(
176                Err("could not serialize body as application/x-www-form-urlencoded"),
177            )?;
178            builder = builder
179                .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
180                .body(encoded);
181        }
182
183        if self.options.http_auth != BinanceAuth::None {
184            // https://binance-docs.github.io/apidocs/spot/en/#signed-trade-user_data-and-margin-endpoint-security
185            let key = self.options.key.as_deref().ok_or("API key not set")?;
186            builder = builder.header("X-MBX-APIKEY", key);
187
188            if self.options.http_auth == BinanceAuth::Sign {
189                let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); // always after the epoch
190                let timestamp = time.as_millis();
191
192                builder = builder.query(&[("timestamp", timestamp)]);
193
194                let secret = self.options.secret.as_deref().ok_or("API secret not set")?;
195                let mut hmac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap(); // hmac accepts key of any length
196
197                let mut request = builder.build().or(Err("Failed to build request"))?;
198                let query = request.url().query().unwrap(); // we added the timestamp query
199                let body = request.body().and_then(|body| body.as_bytes()).unwrap_or_default();
200
201                hmac.update(&[query.as_bytes(), body].concat());
202                let signature = hex::encode(hmac.finalize().into_bytes());
203
204                request.url_mut().query_pairs_mut().append_pair("signature", &signature);
205
206                return Ok(request);
207            }
208        }
209        builder.build().or(Err("failed to build request"))
210    }
211
212    fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes) -> Result<Self::Successful, Self::Unsuccessful> {
213        if status.is_success() {
214            serde_json::from_slice(&response_body).map_err(|error| {
215                log::error!("Failed to parse response due to an error: {}", error);
216                BinanceHandlerError::ParseError
217            })
218        } else {
219            // https://binance-docs.github.io/apidocs/spot/en/#limits
220            if status == 429 || status == 418 {
221                let retry_after = if let Some(value) = headers.get("Retry-After") {
222                    if let Ok(string) = value.to_str() {
223                        if let Ok(retry_after) = u32::from_str(string) {
224                            Some(retry_after)
225                        } else {
226                            log::warn!("Invalid number in Retry-After header");
227                            None
228                        }
229                    } else {
230                        log::warn!("Non-ASCII character in Retry-After header");
231                        None
232                    }
233                } else {
234                    None
235                };
236                return Err(BinanceHandlerError::RateLimitError { retry_after });
237            }
238
239            let error = match serde_json::from_slice(&response_body) {
240                Ok(parsed_error) => BinanceHandlerError::ApiError(parsed_error),
241                Err(error) => {
242                    log::error!("Failed to parse error response due to an error: {}", error);
243                    BinanceHandlerError::ParseError
244                }
245            };
246            Err(error)
247        }
248    }
249}
250
251impl<H> WebSocketHandler for BinanceWebSocketHandler<H> where H: FnMut(serde_json::Value) + Send + 'static, {
252    fn websocket_config(&self) -> WebSocketConfig {
253        let mut config = self.options.websocket_config.clone();
254        if self.options.websocket_url != BinanceWebSocketUrl::None {
255            config.url_prefix = self.options.websocket_url.as_str().to_owned();
256        }
257        config
258    }
259
260    fn handle_message(&mut self, message: WebSocketMessage) -> Vec<WebSocketMessage> {
261        match message {
262            WebSocketMessage::Text(message) => {
263                if let Ok(message) = serde_json::from_str(&message) {
264                    (self.message_handler)(message);
265                } else {
266                    log::error!("Invalid JSON message received");
267                }
268            },
269            WebSocketMessage::Binary(_) => log::warn!("Unexpected binary message received"),
270            WebSocketMessage::Ping(_) | WebSocketMessage::Pong(_) => (),
271        }
272        vec![]
273    }
274}
275
276impl BinanceHttpUrl {
277    /// The URL that this variant represents.
278    #[inline(always)]
279    fn as_str(&self) -> &'static str {
280        match self {
281            Self::Spot => "https://api.binance.com",
282            Self::Spot1 => "https://api1.binance.com",
283            Self::Spot2 => "https://api2.binance.com",
284            Self::Spot3 => "https://api3.binance.com",
285            Self::Spot4 => "https://api4.binance.com",
286            Self::SpotTest => "https://testnet.binance.vision",
287            Self::SpotData => "https://data.binance.com",
288            Self::FuturesUsdM => "https://fapi.binance.com",
289            Self::FuturesCoinM => "https://dapi.binance.com",
290            Self::FuturesTest => "https://testnet.binancefuture.com",
291            Self::EuropeanOptions => "https://eapi.binance.com",
292            Self::None => "",
293        }
294    }
295}
296
297impl BinanceWebSocketUrl {
298    /// The URL that this variant represents.
299    #[inline(always)]
300    pub fn as_str(&self) -> &'static str {
301        match self {
302            Self::Spot9443 => "wss://stream.binance.com:9443",
303            Self::Spot443 => "wss://stream.binance.com:443",
304            Self::SpotTest => "wss://testnet.binance.vision",
305            Self::SpotData => "wss://data-stream.binance.com",
306            Self::WebSocket443 => "wss://ws-api.binance.com:443",
307            Self::WebSocket9443 => "wss://ws-api.binance.com:9443",
308            Self::FuturesUsdM => "wss://fstream.binance.com",
309            Self::FuturesUsdMAuth => "wss://fstream-auth.binance.com",
310            Self::FuturesCoinM => "wss://dstream.binance.com",
311            Self::FuturesUsdMTest => "wss://stream.binancefuture.com",
312            Self::FuturesCoinMTest => "wss://dstream.binancefuture.com",
313            Self::EuropeanOptions => "wss://nbstream.binance.com",
314            Self::None => "",
315        }
316    }
317}
318
319impl HandlerOptions for BinanceOptions {
320    type OptionItem = BinanceOption;
321
322    fn update(&mut self, option: Self::OptionItem) {
323        match option {
324            BinanceOption::Default => (),
325            BinanceOption::Key(v) => self.key = Some(v),
326            BinanceOption::Secret(v) => self.secret = Some(v),
327            BinanceOption::HttpUrl(v) => self.http_url = v,
328            BinanceOption::HttpAuth(v) => self.http_auth = v,
329            BinanceOption::RequestConfig(v) => self.request_config = v,
330            BinanceOption::WebSocketUrl(v) => self.websocket_url = v,
331            BinanceOption::WebSocketConfig(v) => self.websocket_config = v,
332        }
333    }
334}
335
336impl Default for BinanceOptions {
337    fn default() -> Self {
338        let mut websocket_config = WebSocketConfig::new();
339        websocket_config.refresh_after = Duration::from_secs(60 * 60 * 12);
340        websocket_config.ignore_duplicate_during_reconnection = true;
341        Self {
342            key: None,
343            secret: None,
344            http_url: BinanceHttpUrl::None,
345            http_auth: BinanceAuth::None,
346            request_config: RequestConfig::default(),
347            websocket_url: BinanceWebSocketUrl::None,
348            websocket_config,
349        }
350    }
351}
352
353impl<'a, R: DeserializeOwned + 'a> HttpOption<'a, R> for BinanceOption {
354    type RequestHandler = BinanceRequestHandler<'a, R>;
355
356    #[inline(always)]
357    fn request_handler(options: Self::Options) -> Self::RequestHandler {
358        BinanceRequestHandler::<'a, R> {
359            options,
360            _phantom: PhantomData,
361        }
362    }
363}
364
365impl<H: FnMut(serde_json::Value) + Send + 'static> WebSocketOption<H> for BinanceOption {
366    type WebSocketHandler = BinanceWebSocketHandler<H>;
367
368    #[inline(always)]
369    fn websocket_handler(handler: H, options: Self::Options) -> Self::WebSocketHandler {
370        BinanceWebSocketHandler {
371            message_handler: handler,
372            options,
373        }
374    }
375}
376
377impl HandlerOption for BinanceOption {
378    type Options = BinanceOptions;
379}
380
381impl Default for BinanceOption {
382    fn default() -> Self {
383        Self::Default
384    }
385}