Skip to main content

binance_sdk/common/
config.rs

1use derive_builder::Builder;
2use reqwest::{Client, ClientBuilder};
3use std::collections::HashMap;
4use std::fmt;
5use std::sync::Arc;
6use tokio_tungstenite::Connector;
7
8use super::models::{ConfigBuildError, TimeUnit, WebsocketMode};
9use super::utils::{SignatureGenerator, build_client};
10
11#[derive(Clone)]
12pub struct AgentConnector(pub Connector);
13
14impl fmt::Debug for AgentConnector {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        write!(f, "Connector(…)")
17    }
18}
19
20/// The result produced by a custom [`WebsocketHandshakeFn`]: either the established
21/// WebSocket stream together with the server's HTTP handshake response, or a
22/// `tungstenite` error describing why the handshake could not be completed.
23///
24/// This mirrors the return type of
25/// [`tokio_tungstenite::connect_async_tls_with_config`], so an implementation can
26/// simply delegate to it after doing any transport-level setup it needs (see
27/// [`WebsocketHandshakeFn`] for details).
28pub type WebsocketHandshakeResult = Result<
29    (
30        tokio_tungstenite::WebSocketStream<
31            tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
32        >,
33        tokio_tungstenite::tungstenite::handshake::client::Response,
34    ),
35    tokio_tungstenite::tungstenite::Error,
36>;
37
38/// A caller-supplied async function that performs the TLS/WebSocket handshake in
39/// place of the SDK's default connection path.
40///
41/// This is useful when the underlying transport needs to be established before the
42/// handshake runs — for example, tunneling the connection through a SOCKS5 proxy or
43/// another custom stream — something the SDK does not implement itself. The
44/// callback is used for the initial connection, reconnects, and connection renewal,
45/// and is subject to the same 10-second timeout and error mapping as the SDK's
46/// built-in handshake.
47///
48/// # Arguments passed to the function
49///
50/// The function receives the same inputs that
51/// [`tokio_tungstenite::connect_async_tls_with_config`] takes, so an implementation
52/// can open its own transport and then delegate to that function (or an equivalent)
53/// to finish the handshake:
54///
55/// 1. `Request` — the outgoing HTTP upgrade request (already carries the target URL
56///    and any headers the SDK has set, such as `User-Agent`).
57/// 2. `Option<WebSocketConfig>` — optional WebSocket protocol configuration.
58/// 3. `bool` — whether to disable Nagle's algorithm on the underlying TCP stream.
59/// 4. `Option<Connector>` — the TLS connector derived from the configuration's
60///    `agent` field, if one was set. A custom handshake function that ignores this
61///    value effectively overrides `agent`; forward it to the delegate call if the
62///    two options are meant to compose.
63///
64/// # Relationship with `agent`
65///
66/// `agent` and `handshake` are independent, composable settings: `agent` only
67/// customizes the TLS connector, while `handshake` replaces how the transport is
68/// opened and the handshake is performed. Setting both is valid — the resolved
69/// `agent` connector is simply passed through as the fourth argument above for the
70/// custom handshake function to use as it sees fit.
71pub type WebsocketHandshakeFn = Arc<
72    dyn Fn(
73            tokio_tungstenite::tungstenite::handshake::client::Request,
74            Option<tokio_tungstenite::tungstenite::protocol::WebSocketConfig>,
75            bool,
76            Option<Connector>,
77        ) -> std::pin::Pin<Box<dyn Future<Output = WebsocketHandshakeResult> + Send>>
78        + Send
79        + Sync,
80>;
81
82/// A caller-supplied source for the `timestamp` (Unix time in milliseconds) attached
83/// to signed requests, used in place of the host clock (`SystemTime::now()`).
84///
85/// It is called once per signed REST request, signed WebSocket API request and signed
86/// WebSocket streams connection URL (including reconnects and renewals), so it should
87/// be cheap and non-blocking.
88///
89/// Typical use is compensating a host clock that is skewed against Binance's beyond
90/// `recvWindow` (error `-1021`): measure the offset against the server time endpoint
91/// and keep it in a shared atomic that the provider reads, so it can be updated at
92/// runtime without rebuilding the configuration:
93///
94/// ```
95/// use std::sync::{Arc, atomic::{AtomicI64, Ordering}};
96/// use std::time::{SystemTime, UNIX_EPOCH};
97/// use binance_sdk::config::{ConfigurationRestApi, TimestampProvider};
98///
99/// let offset_ms = Arc::new(AtomicI64::new(0));
100/// let offset = Arc::clone(&offset_ms);
101/// let provider: TimestampProvider = Arc::new(move || {
102///     let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as i64;
103///     (now + offset.load(Ordering::Relaxed)) as u64
104/// });
105///
106/// let config = ConfigurationRestApi::builder()
107///     .api_key("key")
108///     .api_secret("secret")
109///     .timestamp_provider(provider)
110///     .build()
111///     .unwrap();
112///
113/// // Later, after measuring `serverTime - local time`:
114/// offset_ms.store(-1_500, Ordering::Relaxed);
115/// # let _ = config;
116/// ```
117pub type TimestampProvider = Arc<dyn Fn() -> u64 + Send + Sync>;
118
119#[derive(Clone)]
120pub struct HttpAgent(pub Arc<dyn Fn(ClientBuilder) -> ClientBuilder + Send + Sync>);
121
122impl fmt::Debug for HttpAgent {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(f, "HttpAgent(<custom agent fn>)")
125    }
126}
127
128#[derive(Clone)]
129pub struct ProxyAuth {
130    pub username: String,
131    pub password: String,
132}
133
134#[derive(Debug, Clone)]
135pub struct ProxyConfig {
136    pub host: String,
137    pub port: u16,
138    pub protocol: Option<String>,
139    pub auth: Option<ProxyAuth>,
140}
141
142impl fmt::Debug for ProxyAuth {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        f.debug_struct("ProxyAuth")
145            .field("username", &self.username)
146            .field("password", &"[REDACTED]")
147            .finish()
148    }
149}
150
151#[derive(Clone)]
152pub enum PrivateKey {
153    File(String),
154    Raw(Vec<u8>),
155}
156
157impl fmt::Debug for PrivateKey {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        match self {
160            PrivateKey::File(_) => write!(f, "PrivateKey::File([REDACTED])"),
161            PrivateKey::Raw(_) => write!(f, "PrivateKey::Raw([REDACTED])"),
162        }
163    }
164}
165
166#[derive(Clone, Builder)]
167#[builder(
168    pattern = "owned",
169    build_fn(name = "try_build", error = "ConfigBuildError")
170)]
171pub struct ConfigurationRestApi {
172    #[builder(setter(into, strip_option), default)]
173    pub api_key: Option<String>,
174
175    #[builder(setter(into, strip_option), default)]
176    pub api_secret: Option<String>,
177
178    #[builder(setter(into, strip_option), default)]
179    pub base_path: Option<String>,
180
181    #[builder(default = "1000")]
182    pub timeout: u64,
183
184    #[builder(default = "true")]
185    pub keep_alive: bool,
186
187    #[builder(default = "true")]
188    pub compression: bool,
189
190    #[builder(default = "3")]
191    pub retries: u32,
192
193    #[builder(default = "1000")]
194    pub backoff: u64,
195
196    #[builder(setter(strip_option), default)]
197    pub proxy: Option<ProxyConfig>,
198
199    #[builder(setter(strip_option, into), default)]
200    pub custom_headers: Option<HashMap<String, String>>,
201
202    #[builder(setter(strip_option), default)]
203    pub agent: Option<HttpAgent>,
204
205    #[builder(setter(strip_option), default)]
206    pub private_key: Option<PrivateKey>,
207
208    #[builder(setter(strip_option), default)]
209    pub private_key_passphrase: Option<String>,
210
211    #[builder(setter(strip_option), default)]
212    pub time_unit: Option<TimeUnit>,
213
214    /// Source of the `timestamp` of signed requests; defaults to the host clock.
215    /// See [`TimestampProvider`].
216    #[builder(setter(strip_option), default)]
217    pub timestamp_provider: Option<TimestampProvider>,
218
219    #[builder(setter(skip))]
220    pub(crate) client: Client,
221
222    #[builder(setter(skip))]
223    pub(crate) user_agent: String,
224
225    #[builder(setter(skip))]
226    pub(crate) signature_gen: SignatureGenerator,
227}
228
229impl fmt::Debug for ConfigurationRestApi {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        f.debug_struct("ConfigurationRestApi")
232            .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
233            .field(
234                "api_secret",
235                &self.api_secret.as_ref().map(|_| "[REDACTED]"),
236            )
237            .field("base_path", &self.base_path)
238            .field("timeout", &self.timeout)
239            .field("keep_alive", &self.keep_alive)
240            .field("compression", &self.compression)
241            .field("retries", &self.retries)
242            .field("backoff", &self.backoff)
243            .field("proxy", &self.proxy)
244            .field("custom_headers", &self.custom_headers)
245            .field("agent", &self.agent)
246            .field(
247                "private_key",
248                &self.private_key.as_ref().map(|_| "[REDACTED]"),
249            )
250            .field(
251                "private_key_passphrase",
252                &self.private_key_passphrase.as_ref().map(|_| "[REDACTED]"),
253            )
254            .field("time_unit", &self.time_unit)
255            .field(
256                "timestamp_provider",
257                &self
258                    .timestamp_provider
259                    .as_ref()
260                    .map(|_| "<custom timestamp fn>"),
261            )
262            .field("client", &"<reqwest::Client>")
263            .field("user_agent", &self.user_agent)
264            .field("signature_gen", &self.signature_gen)
265            .finish()
266    }
267}
268
269impl ConfigurationRestApi {
270    #[must_use]
271    pub fn builder() -> ConfigurationRestApiBuilder {
272        ConfigurationRestApiBuilder::default()
273    }
274}
275
276impl ConfigurationRestApiBuilder {
277    /// Builds a `ConfigurationRestApi` instance with configured HTTP client and signature generator.
278    ///
279    /// # Returns
280    ///
281    /// A `Result` containing the fully configured `ConfigurationRestApi` or a `ConfigBuildError` if configuration fails.
282    ///
283    /// # Errors
284    ///
285    /// Returns a `ConfigBuildError` if the initial configuration build fails or if client setup encounters issues.
286    pub fn build(self) -> Result<ConfigurationRestApi, ConfigBuildError> {
287        let mut cfg = self.try_build()?;
288        cfg.client = build_client(
289            cfg.timeout,
290            cfg.keep_alive,
291            cfg.proxy.as_ref(),
292            cfg.agent.clone(),
293        );
294        cfg.signature_gen = SignatureGenerator::new(
295            cfg.api_secret.clone(),
296            cfg.private_key.clone(),
297            cfg.private_key_passphrase.clone(),
298        );
299
300        Ok(cfg)
301    }
302}
303
304#[derive(Clone, Builder)]
305#[builder(
306    pattern = "owned",
307    build_fn(name = "try_build", error = "ConfigBuildError")
308)]
309pub struct ConfigurationWebsocketApi {
310    #[builder(setter(into, strip_option), default)]
311    pub api_key: Option<String>,
312
313    #[builder(setter(into, strip_option), default)]
314    pub api_secret: Option<String>,
315
316    #[builder(setter(into, strip_option), default)]
317    pub ws_url: Option<String>,
318
319    #[builder(default = "5000")]
320    pub timeout: u64,
321
322    #[builder(default = "5000")]
323    pub reconnect_delay: u64,
324
325    #[builder(default = "WebsocketMode::Single")]
326    pub mode: WebsocketMode,
327
328    #[builder(setter(strip_option), default)]
329    pub agent: Option<AgentConnector>,
330
331    /// Overrides how the SDK performs the TLS/WebSocket handshake. See
332    /// [`WebsocketHandshakeFn`] for details and its interaction with `agent`.
333    #[builder(setter(strip_option), default)]
334    pub handshake: Option<WebsocketHandshakeFn>,
335
336    #[builder(setter(strip_option), default)]
337    pub private_key: Option<PrivateKey>,
338
339    #[builder(setter(strip_option), default)]
340    pub private_key_passphrase: Option<String>,
341
342    #[builder(setter(strip_option), default)]
343    pub time_unit: Option<TimeUnit>,
344
345    #[builder(default = "true")]
346    pub auto_session_relogon: bool,
347
348    /// Source of the `timestamp` of signed requests; defaults to the host clock.
349    /// See [`TimestampProvider`].
350    #[builder(setter(strip_option), default)]
351    pub timestamp_provider: Option<TimestampProvider>,
352
353    #[builder(setter(skip))]
354    pub(crate) user_agent: String,
355
356    #[builder(setter(skip))]
357    pub(crate) signature_gen: SignatureGenerator,
358}
359
360impl fmt::Debug for ConfigurationWebsocketApi {
361    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362        f.debug_struct("ConfigurationWebsocketApi")
363            .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
364            .field(
365                "api_secret",
366                &self.api_secret.as_ref().map(|_| "[REDACTED]"),
367            )
368            .field("ws_url", &self.ws_url)
369            .field("timeout", &self.timeout)
370            .field("reconnect_delay", &self.reconnect_delay)
371            .field("mode", &self.mode)
372            .field("agent", &self.agent)
373            .field(
374                "handshake",
375                &self.handshake.as_ref().map(|_| "<custom handshake fn>"),
376            )
377            .field(
378                "private_key",
379                &self.private_key.as_ref().map(|_| "[REDACTED]"),
380            )
381            .field(
382                "private_key_passphrase",
383                &self.private_key_passphrase.as_ref().map(|_| "[REDACTED]"),
384            )
385            .field("time_unit", &self.time_unit)
386            .field("auto_session_relogon", &self.auto_session_relogon)
387            .field(
388                "timestamp_provider",
389                &self
390                    .timestamp_provider
391                    .as_ref()
392                    .map(|_| "<custom timestamp fn>"),
393            )
394            .field("user_agent", &self.user_agent)
395            .field("signature_gen", &self.signature_gen)
396            .finish()
397    }
398}
399
400impl ConfigurationWebsocketApi {
401    /// Creates a builder for `ConfigurationWebsocketApi` with the specified API key.
402    ///
403    /// # Arguments
404    ///
405    /// * `api_key` - The API key to be used for the WebSocket API configuration
406    ///
407    /// # Returns
408    ///
409    /// A `ConfigurationWebsocketApiBuilder` initialized with the provided API key
410    #[must_use]
411    pub fn builder() -> ConfigurationWebsocketApiBuilder {
412        ConfigurationWebsocketApiBuilder::default()
413    }
414}
415
416impl ConfigurationWebsocketApiBuilder {
417    /// Builds the `ConfigurationWebsocketApi` with a generated signature generator.
418    ///
419    /// This method attempts to build the configuration using the builder's settings
420    /// and then initializes the signature generator with the API secret, private key,
421    /// and private key passphrase.
422    ///
423    /// # Returns
424    ///
425    /// A `Result` containing the fully configured `ConfigurationWebsocketApi` or a
426    /// `ConfigBuildError` if the build process fails.
427    ///
428    /// # Errors
429    ///
430    /// Returns a `ConfigBuildError` if the initial configuration build fails or if signature generation fails.
431    ///
432    pub fn build(self) -> Result<ConfigurationWebsocketApi, ConfigBuildError> {
433        let mut cfg = self.try_build()?;
434        cfg.signature_gen = SignatureGenerator::new(
435            cfg.api_secret.clone(),
436            cfg.private_key.clone(),
437            cfg.private_key_passphrase.clone(),
438        );
439
440        Ok(cfg)
441    }
442}
443
444#[derive(Clone, Builder)]
445#[builder(pattern = "owned", build_fn(error = "ConfigBuildError"))]
446pub struct ConfigurationWebsocketStreams {
447    #[builder(setter(into, strip_option), default)]
448    pub ws_url: Option<String>,
449
450    #[builder(setter(into, strip_option), default)]
451    pub api_key: Option<String>,
452
453    #[builder(setter(into, strip_option), default)]
454    pub api_secret: Option<String>,
455
456    #[builder(setter(strip_option), default)]
457    pub private_key: Option<PrivateKey>,
458
459    #[builder(setter(strip_option), default)]
460    pub private_key_passphrase: Option<String>,
461
462    #[builder(default = "5000")]
463    pub reconnect_delay: u64,
464
465    #[builder(default = "WebsocketMode::Single")]
466    pub mode: WebsocketMode,
467
468    #[builder(setter(strip_option), default)]
469    pub agent: Option<AgentConnector>,
470
471    /// Overrides how the SDK performs the TLS/WebSocket handshake. See
472    /// [`WebsocketHandshakeFn`] for details and its interaction with `agent`.
473    #[builder(setter(strip_option), default)]
474    pub handshake: Option<WebsocketHandshakeFn>,
475
476    #[builder(setter(strip_option), default)]
477    pub time_unit: Option<TimeUnit>,
478
479    /// Source of the `timestamp` of signed requests; defaults to the host clock.
480    /// See [`TimestampProvider`].
481    #[builder(setter(strip_option), default)]
482    pub timestamp_provider: Option<TimestampProvider>,
483
484    #[builder(setter(skip))]
485    pub(crate) user_agent: String,
486}
487
488impl fmt::Debug for ConfigurationWebsocketStreams {
489    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490        f.debug_struct("ConfigurationWebsocketStreams")
491            .field("ws_url", &self.ws_url)
492            .field("reconnect_delay", &self.reconnect_delay)
493            .field("mode", &self.mode)
494            .field("agent", &self.agent)
495            .field(
496                "handshake",
497                &self.handshake.as_ref().map(|_| "<custom handshake fn>"),
498            )
499            .field("time_unit", &self.time_unit)
500            .field(
501                "timestamp_provider",
502                &self
503                    .timestamp_provider
504                    .as_ref()
505                    .map(|_| "<custom timestamp fn>"),
506            )
507            .field("user_agent", &self.user_agent)
508            .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
509            .field(
510                "api_secret",
511                &self.api_secret.as_ref().map(|_| "[REDACTED]"),
512            )
513            .field(
514                "private_key",
515                &self.private_key.as_ref().map(|_| "[REDACTED]"),
516            )
517            .field(
518                "private_key_passphrase",
519                &self.private_key_passphrase.as_ref().map(|_| "[REDACTED]"),
520            )
521            .finish()
522    }
523}
524
525impl ConfigurationWebsocketStreams {
526    #[must_use]
527    /// Creates a builder for `ConfigurationWebsocketStreams` with default settings.
528    ///
529    /// # Returns
530    ///
531    /// A `ConfigurationWebsocketStreamsBuilder` initialized with default values
532    pub fn builder() -> ConfigurationWebsocketStreamsBuilder {
533        ConfigurationWebsocketStreamsBuilder::default()
534    }
535}