ccxt_exchanges/binance/rest/
account.rs

1//! Binance account operations.
2//!
3//! This module contains all account-related methods including balance,
4//! trade history, account configuration, and user data stream management.
5
6use super::super::{Binance, parser};
7use ccxt_core::types::AccountType;
8use ccxt_core::{
9    Error, ParseError, Result,
10    types::{Balance, Currency, FeeTradingFee, MarketType, Trade},
11};
12use reqwest::header::HeaderMap;
13use std::collections::HashMap;
14use tracing::warn;
15
16/// Balance fetch parameters for Binance.
17#[derive(Debug, Clone, Default)]
18pub struct BalanceFetchParams {
19    /// Account type to query (spot, margin, futures, etc.).
20    pub account_type: Option<AccountType>,
21    /// Margin mode: "cross" or "isolated".
22    pub margin_mode: Option<String>,
23    /// Symbols for isolated margin (e.g., `["BTC/USDT", "ETH/USDT"]`).
24    pub symbols: Option<Vec<String>>,
25    /// Whether to use Portfolio Margin API.
26    pub portfolio_margin: bool,
27    /// Sub-type for futures: "linear" or "inverse".
28    pub sub_type: Option<String>,
29}
30
31impl BalanceFetchParams {
32    /// Create params for spot balance.
33    pub fn spot() -> Self {
34        Self {
35            account_type: Some(AccountType::Spot),
36            ..Default::default()
37        }
38    }
39
40    /// Create params for cross margin balance.
41    pub fn cross_margin() -> Self {
42        Self {
43            account_type: Some(AccountType::Margin),
44            margin_mode: Some("cross".to_string()),
45            ..Default::default()
46        }
47    }
48
49    /// Create params for isolated margin balance.
50    pub fn isolated_margin(symbols: Option<Vec<String>>) -> Self {
51        Self {
52            account_type: Some(AccountType::IsolatedMargin),
53            margin_mode: Some("isolated".to_string()),
54            symbols,
55            ..Default::default()
56        }
57    }
58
59    /// Create params for USDT-margined futures (linear).
60    pub fn linear_futures() -> Self {
61        Self {
62            account_type: Some(AccountType::Futures),
63            sub_type: Some("linear".to_string()),
64            ..Default::default()
65        }
66    }
67
68    /// Create params for coin-margined futures (inverse/delivery).
69    pub fn inverse_futures() -> Self {
70        Self {
71            account_type: Some(AccountType::Delivery),
72            sub_type: Some("inverse".to_string()),
73            ..Default::default()
74        }
75    }
76
77    /// Create params for funding wallet.
78    pub fn funding() -> Self {
79        Self {
80            account_type: Some(AccountType::Funding),
81            ..Default::default()
82        }
83    }
84
85    /// Create params for options account.
86    pub fn option() -> Self {
87        Self {
88            account_type: Some(AccountType::Option),
89            ..Default::default()
90        }
91    }
92
93    /// Create params for portfolio margin.
94    pub fn portfolio_margin() -> Self {
95        Self {
96            portfolio_margin: true,
97            ..Default::default()
98        }
99    }
100}
101
102impl Binance {
103    /// Fetch account balance.
104    ///
105    /// # Returns
106    ///
107    /// Returns the account [`Balance`] information.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if authentication fails or the API request fails.
112    pub async fn fetch_balance_simple(&self) -> Result<Balance> {
113        let url = format!("{}/account", self.urls().private);
114        let data = self.signed_request(url).execute().await?;
115        parser::parse_balance(&data)
116    }
117
118    /// Fetch account balance with optional account type parameter.
119    ///
120    /// Query for balance and get the amount of funds available for trading or funds locked in orders.
121    ///
122    /// # Arguments
123    ///
124    /// * `account_type` - Optional account type. Supported values:
125    ///   - `Spot` - Spot trading account (default)
126    ///   - `Margin` - Cross margin account
127    ///   - `IsolatedMargin` - Isolated margin account
128    ///   - `Futures` - USDT-margined futures (linear)
129    ///   - `Delivery` - Coin-margined futures (inverse)
130    ///   - `Funding` - Funding wallet
131    ///   - `Option` - Options account
132    ///
133    /// # Returns
134    ///
135    /// Returns the account [`Balance`] information.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if authentication fails or the API request fails.
140    ///
141    /// # Example
142    ///
143    /// ```no_run
144    /// # use ccxt_exchanges::binance::Binance;
145    /// # use ccxt_core::ExchangeConfig;
146    /// # use ccxt_core::types::AccountType;
147    /// # async fn example() -> ccxt_core::Result<()> {
148    /// let mut config = ExchangeConfig::default();
149    /// config.api_key = Some("your_api_key".to_string());
150    /// config.secret = Some("your_secret".to_string());
151    /// let binance = Binance::new(config)?;
152    ///
153    /// // Fetch spot balance (default)
154    /// let balance = binance.fetch_balance(None).await?;
155    ///
156    /// // Fetch futures balance
157    /// let futures_balance = binance.fetch_balance(Some(AccountType::Futures)).await?;
158    /// # Ok(())
159    /// # }
160    /// ```
161    pub async fn fetch_balance(&self, account_type: Option<AccountType>) -> Result<Balance> {
162        let params = BalanceFetchParams {
163            account_type,
164            ..Default::default()
165        };
166        self.fetch_balance_with_params(params).await
167    }
168
169    /// Fetch account balance with detailed parameters.
170    ///
171    /// This method provides full control over balance fetching, supporting all Binance account types
172    /// and margin modes.
173    ///
174    /// # Arguments
175    ///
176    /// * `params` - Balance fetch parameters including:
177    ///   - `account_type` - Account type (spot, margin, futures, etc.)
178    ///   - `margin_mode` - "cross" or "isolated" for margin trading
179    ///   - `symbols` - Symbols for isolated margin queries
180    ///   - `portfolio_margin` - Use Portfolio Margin API
181    ///   - `sub_type` - "linear" or "inverse" for futures
182    ///
183    /// # Returns
184    ///
185    /// Returns the account [`Balance`] information.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if authentication fails or the API request fails.
190    ///
191    /// # API Endpoints
192    ///
193    /// - Spot: `GET /api/v3/account`
194    /// - Cross Margin: `GET /sapi/v1/margin/account`
195    /// - Isolated Margin: `GET /sapi/v1/margin/isolated/account`
196    /// - Funding Wallet: `POST /sapi/v1/asset/get-funding-asset`
197    /// - USDT-M Futures: `GET /fapi/v2/balance`
198    /// - COIN-M Futures: `GET /dapi/v1/balance`
199    /// - Options: `GET /eapi/v1/account`
200    /// - Portfolio Margin: `GET /papi/v1/balance`
201    ///
202    /// # Example
203    ///
204    /// ```no_run
205    /// # use ccxt_exchanges::binance::Binance;
206    /// # use ccxt_exchanges::binance::rest::account::BalanceFetchParams;
207    /// # use ccxt_core::ExchangeConfig;
208    /// # async fn example() -> ccxt_core::Result<()> {
209    /// let mut config = ExchangeConfig::default();
210    /// config.api_key = Some("your_api_key".to_string());
211    /// config.secret = Some("your_secret".to_string());
212    /// let binance = Binance::new(config)?;
213    ///
214    /// // Fetch cross margin balance
215    /// let margin_balance = binance.fetch_balance_with_params(
216    ///     BalanceFetchParams::cross_margin()
217    /// ).await?;
218    ///
219    /// // Fetch isolated margin balance for specific symbols
220    /// let isolated_balance = binance.fetch_balance_with_params(
221    ///     BalanceFetchParams::isolated_margin(Some(vec!["BTC/USDT".to_string()]))
222    /// ).await?;
223    ///
224    /// // Fetch USDT-margined futures balance
225    /// let futures_balance = binance.fetch_balance_with_params(
226    ///     BalanceFetchParams::linear_futures()
227    /// ).await?;
228    ///
229    /// // Fetch portfolio margin balance
230    /// let pm_balance = binance.fetch_balance_with_params(
231    ///     BalanceFetchParams::portfolio_margin()
232    /// ).await?;
233    /// # Ok(())
234    /// # }
235    /// ```
236    pub async fn fetch_balance_with_params(&self, params: BalanceFetchParams) -> Result<Balance> {
237        // Handle portfolio margin first
238        if params.portfolio_margin {
239            return self.fetch_portfolio_margin_balance().await;
240        }
241
242        let account_type = params.account_type.unwrap_or(AccountType::Spot);
243
244        match account_type {
245            AccountType::Spot => self.fetch_spot_balance().await,
246            AccountType::Margin => {
247                let margin_mode = params.margin_mode.as_deref().unwrap_or("cross");
248                if margin_mode == "isolated" {
249                    self.fetch_isolated_margin_balance(params.symbols).await
250                } else {
251                    self.fetch_cross_margin_balance().await
252                }
253            }
254            AccountType::IsolatedMargin => self.fetch_isolated_margin_balance(params.symbols).await,
255            AccountType::Futures => {
256                let sub_type = params.sub_type.as_deref().unwrap_or("linear");
257                if sub_type == "inverse" {
258                    self.fetch_delivery_balance().await
259                } else {
260                    self.fetch_futures_balance().await
261                }
262            }
263            AccountType::Delivery => self.fetch_delivery_balance().await,
264            AccountType::Funding => self.fetch_funding_balance().await,
265            AccountType::Option => self.fetch_option_balance().await,
266        }
267    }
268
269    /// Fetch spot account balance.
270    ///
271    /// Uses the `/api/v3/account` endpoint.
272    async fn fetch_spot_balance(&self) -> Result<Balance> {
273        let url = format!("{}/account", self.urls().private);
274        let data = self.signed_request(url).execute().await?;
275        parser::parse_balance_with_type(&data, "spot")
276    }
277
278    /// Fetch cross margin account balance.
279    ///
280    /// Uses the `/sapi/v1/margin/account` endpoint.
281    async fn fetch_cross_margin_balance(&self) -> Result<Balance> {
282        let url = format!("{}/margin/account", self.urls().sapi);
283        let data = self.signed_request(url).execute().await?;
284        parser::parse_balance_with_type(&data, "margin")
285    }
286
287    /// Fetch isolated margin account balance.
288    ///
289    /// Uses the `/sapi/v1/margin/isolated/account` endpoint.
290    ///
291    /// # Arguments
292    ///
293    /// * `symbols` - Optional list of symbols to query. If None, fetches all isolated margin pairs.
294    async fn fetch_isolated_margin_balance(&self, symbols: Option<Vec<String>>) -> Result<Balance> {
295        let url = format!("{}/margin/isolated/account", self.urls().sapi);
296
297        let mut request = self.signed_request(url);
298
299        // Add symbols parameter if provided
300        if let Some(syms) = symbols {
301            // Convert unified symbols to market IDs
302            let mut market_ids = Vec::new();
303            for sym in &syms {
304                if let Ok(market) = self.base().market(sym).await {
305                    market_ids.push(market.id.clone());
306                } else {
307                    // If market not found, use the symbol as-is (might be already in exchange format)
308                    market_ids.push(sym.replace('/', ""));
309                }
310            }
311            if !market_ids.is_empty() {
312                request = request.param("symbols", market_ids.join(","));
313            }
314        }
315
316        let data = request.execute().await?;
317        parser::parse_balance_with_type(&data, "isolated")
318    }
319
320    /// Fetch funding wallet balance.
321    ///
322    /// Uses the `/sapi/v1/asset/get-funding-asset` endpoint.
323    async fn fetch_funding_balance(&self) -> Result<Balance> {
324        use super::super::signed_request::HttpMethod;
325
326        let url = format!("{}/asset/get-funding-asset", self.urls().sapi);
327        let data = self
328            .signed_request(url)
329            .method(HttpMethod::Post)
330            .execute()
331            .await?;
332        parser::parse_balance_with_type(&data, "funding")
333    }
334
335    /// Fetch USDT-margined futures balance.
336    ///
337    /// Uses the `/fapi/v2/balance` endpoint.
338    async fn fetch_futures_balance(&self) -> Result<Balance> {
339        // Use v2 endpoint for better data
340        let url = format!("{}/balance", self.urls().fapi_private.replace("/v1", "/v2"));
341        let data = self.signed_request(url).execute().await?;
342        parser::parse_balance_with_type(&data, "linear")
343    }
344
345    /// Fetch coin-margined futures (delivery) balance.
346    ///
347    /// Uses the `/dapi/v1/balance` endpoint.
348    async fn fetch_delivery_balance(&self) -> Result<Balance> {
349        let url = format!("{}/balance", self.urls().dapi_private);
350        let data = self.signed_request(url).execute().await?;
351        parser::parse_balance_with_type(&data, "inverse")
352    }
353
354    /// Fetch options account balance.
355    ///
356    /// Uses the `/eapi/v1/account` endpoint.
357    async fn fetch_option_balance(&self) -> Result<Balance> {
358        let url = format!("{}/account", self.urls().eapi_private);
359        let data = self.signed_request(url).execute().await?;
360        parser::parse_balance_with_type(&data, "option")
361    }
362
363    /// Fetch portfolio margin account balance.
364    ///
365    /// Uses the `/papi/v1/balance` endpoint.
366    async fn fetch_portfolio_margin_balance(&self) -> Result<Balance> {
367        let url = format!("{}/balance", self.urls().papi);
368        let data = self.signed_request(url).execute().await?;
369        parser::parse_balance_with_type(&data, "portfolio")
370    }
371
372    /// Fetch user's trade history.
373    ///
374    /// # Arguments
375    ///
376    /// * `symbol` - Trading pair symbol.
377    /// * `since` - Optional start timestamp.
378    /// * `limit` - Optional limit on number of trades.
379    ///
380    /// # Returns
381    ///
382    /// Returns a vector of [`Trade`] structures for the user's trades.
383    ///
384    /// # Errors
385    ///
386    /// Returns an error if authentication fails, market is not found, or the API request fails.
387    pub async fn fetch_my_trades(
388        &self,
389        symbol: &str,
390        since: Option<i64>,
391        limit: Option<u32>,
392    ) -> Result<Vec<Trade>> {
393        let market = self.base().market(symbol).await?;
394        let url = format!("{}/myTrades", self.urls().private);
395
396        let data = self
397            .signed_request(url)
398            .param("symbol", &market.id)
399            .optional_param("startTime", since)
400            .optional_param("limit", limit)
401            .execute()
402            .await?;
403
404        let trades_array = data.as_array().ok_or_else(|| {
405            Error::from(ParseError::invalid_format(
406                "data",
407                "Expected array of trades",
408            ))
409        })?;
410
411        let mut trades = Vec::new();
412
413        for trade_data in trades_array {
414            match parser::parse_trade(trade_data, Some(&market)) {
415                Ok(trade) => trades.push(trade),
416                Err(e) => {
417                    warn!(error = %e, "Failed to parse trade");
418                }
419            }
420        }
421
422        Ok(trades)
423    }
424
425    /// Fetch user's recent trade history with additional parameters.
426    ///
427    /// This method is similar to `fetch_my_trades` but accepts additional parameters
428    /// for more flexible querying.
429    ///
430    /// # Arguments
431    ///
432    /// * `symbol` - Trading pair symbol.
433    /// * `since` - Optional start timestamp in milliseconds.
434    /// * `limit` - Optional limit on number of trades (default: 500, max: 1000).
435    /// * `params` - Optional additional parameters that may include:
436    ///   - `orderId`: Filter by order ID.
437    ///   - `fromId`: Start from specific trade ID.
438    ///   - `endTime`: End timestamp in milliseconds.
439    ///
440    /// # Returns
441    ///
442    /// Returns a vector of [`Trade`] structures for the user's trades.
443    ///
444    /// # Errors
445    ///
446    /// Returns an error if authentication fails, market is not found, or the API request fails.
447    ///
448    /// # Example
449    ///
450    /// ```no_run
451    /// # use ccxt_exchanges::binance::Binance;
452    /// # use ccxt_core::ExchangeConfig;
453    /// # async fn example() -> ccxt_core::Result<()> {
454    /// let mut config = ExchangeConfig::default();
455    /// config.api_key = Some("your_api_key".to_string());
456    /// config.secret = Some("your_secret".to_string());
457    /// let binance = Binance::new(config)?;
458    /// let my_trades = binance.fetch_my_recent_trades("BTC/USDT", None, Some(50), None).await?;
459    /// for trade in &my_trades {
460    ///     println!("Trade: {} {} @ {}", trade.side, trade.amount, trade.price);
461    /// }
462    /// # Ok(())
463    /// # }
464    /// ```
465    pub async fn fetch_my_recent_trades(
466        &self,
467        symbol: &str,
468        since: Option<i64>,
469        limit: Option<u32>,
470        params: Option<HashMap<String, String>>,
471    ) -> Result<Vec<Trade>> {
472        let market = self.base().market(symbol).await?;
473        let url = format!("{}/myTrades", self.urls().private);
474
475        // Convert HashMap to serde_json::Value for merge_json_params
476        let json_params = params.map(|p| {
477            serde_json::Value::Object(
478                p.into_iter()
479                    .map(|(k, v)| (k, serde_json::Value::String(v)))
480                    .collect(),
481            )
482        });
483
484        let data = self
485            .signed_request(url)
486            .param("symbol", &market.id)
487            .optional_param("startTime", since)
488            .optional_param("limit", limit)
489            .merge_json_params(json_params)
490            .execute()
491            .await?;
492
493        let trades_array = data.as_array().ok_or_else(|| {
494            Error::from(ParseError::invalid_format(
495                "data",
496                "Expected array of trades",
497            ))
498        })?;
499
500        let mut trades = Vec::new();
501        for trade_data in trades_array {
502            match parser::parse_trade(trade_data, Some(&market)) {
503                Ok(trade) => trades.push(trade),
504                Err(e) => {
505                    warn!(error = %e, "Failed to parse my trade");
506                }
507            }
508        }
509
510        Ok(trades)
511    }
512
513    /// Fetch all currency information.
514    ///
515    /// # Returns
516    ///
517    /// Returns a vector of [`Currency`] structures.
518    ///
519    /// # Errors
520    ///
521    /// Returns an error if authentication fails or the API request fails.
522    pub async fn fetch_currencies(&self) -> Result<Vec<Currency>> {
523        let url = format!("{}/capital/config/getall", self.urls().sapi);
524        let data = self.signed_request(url).execute().await?;
525        parser::parse_currencies(&data)
526    }
527
528    /// Fetch trading fees for a symbol.
529    ///
530    /// # Arguments
531    ///
532    /// * `symbol` - Trading pair symbol.
533    /// * `params` - Optional parameters. Supports `portfolioMargin` key for Portfolio Margin mode.
534    ///
535    /// # Returns
536    ///
537    /// Returns trading fee information for the symbol as a [`FeeTradingFee`] structure.
538    ///
539    /// # Errors
540    ///
541    /// Returns an error if authentication fails, market is not found, or the API request fails.
542    ///
543    /// # Example
544    ///
545    /// ```no_run
546    /// # use ccxt_exchanges::binance::Binance;
547    /// # use ccxt_core::ExchangeConfig;
548    /// # async fn example() -> ccxt_core::Result<()> {
549    /// let mut config = ExchangeConfig::default();
550    /// config.api_key = Some("your_api_key".to_string());
551    /// config.secret = Some("your_secret".to_string());
552    /// let binance = Binance::new(config)?;
553    /// let fee = binance.fetch_trading_fee("BTC/USDT", None).await?;
554    /// println!("Maker: {}, Taker: {}", fee.maker, fee.taker);
555    /// # Ok(())
556    /// # }
557    /// ```
558    pub async fn fetch_trading_fee(
559        &self,
560        symbol: &str,
561        params: Option<HashMap<String, String>>,
562    ) -> Result<FeeTradingFee> {
563        self.load_markets(false).await?;
564        let market = self.base().market(symbol).await?;
565
566        let is_portfolio_margin = params
567            .as_ref()
568            .and_then(|p| p.get("portfolioMargin"))
569            .is_some_and(|v| v == "true");
570
571        // Select API endpoint based on market type and Portfolio Margin mode
572        let url = match market.market_type {
573            MarketType::Spot => format!("{}/asset/tradeFee", self.urls().sapi),
574            MarketType::Futures | MarketType::Swap => {
575                if is_portfolio_margin {
576                    // Portfolio Margin mode uses papi endpoints
577                    if market.is_linear() {
578                        format!("{}/um/commissionRate", self.urls().papi)
579                    } else {
580                        format!("{}/cm/commissionRate", self.urls().papi)
581                    }
582                } else {
583                    // Standard mode
584                    if market.is_linear() {
585                        format!("{}/commissionRate", self.urls().fapi_private)
586                    } else {
587                        format!("{}/commissionRate", self.urls().dapi_private)
588                    }
589                }
590            }
591            MarketType::Option => {
592                return Err(Error::invalid_request(format!(
593                    "fetch_trading_fee not supported for market type: {:?}",
594                    market.market_type
595                )));
596            }
597        };
598
599        // Convert HashMap to serde_json::Value, filtering out portfolioMargin
600        let json_params = params.map(|p| {
601            serde_json::Value::Object(
602                p.into_iter()
603                    .filter(|(k, _)| k != "portfolioMargin")
604                    .map(|(k, v)| (k, serde_json::Value::String(v)))
605                    .collect(),
606            )
607        });
608
609        let response = self
610            .signed_request(url)
611            .param("symbol", &market.id)
612            .merge_json_params(json_params)
613            .execute()
614            .await?;
615
616        parser::parse_trading_fee(&response)
617    }
618
619    /// Fetch trading fees for multiple symbols.
620    ///
621    /// # Arguments
622    ///
623    /// * `symbols` - Optional list of trading pair symbols. `None` fetches all pairs.
624    /// * `params` - Optional parameters.
625    ///
626    /// # Returns
627    ///
628    /// Returns a `HashMap` of trading fees keyed by symbol.
629    ///
630    /// # Errors
631    ///
632    /// Returns an error if authentication fails or the API request fails.
633    ///
634    /// # Example
635    ///
636    /// ```no_run
637    /// # use ccxt_exchanges::binance::Binance;
638    /// # use ccxt_core::ExchangeConfig;
639    /// # async fn example() -> ccxt_core::Result<()> {
640    /// let mut config = ExchangeConfig::default();
641    /// config.api_key = Some("your_api_key".to_string());
642    /// config.secret = Some("your_secret".to_string());
643    /// let binance = Binance::new(config)?;
644    /// let fees = binance.fetch_trading_fees(None, None).await?;
645    /// for (symbol, fee) in &fees {
646    ///     println!("{}: maker={}, taker={}", symbol, fee.maker, fee.taker);
647    /// }
648    /// # Ok(())
649    /// # }
650    /// ```
651    pub async fn fetch_trading_fees(
652        &self,
653        symbols: Option<Vec<String>>,
654        params: Option<HashMap<String, String>>,
655    ) -> Result<HashMap<String, FeeTradingFee>> {
656        self.load_markets(false).await?;
657
658        let url = format!("{}/asset/tradeFee", self.urls().sapi);
659
660        // Build symbols parameter if provided
661        let symbols_param = if let Some(syms) = &symbols {
662            let mut market_ids: Vec<String> = Vec::new();
663            for s in syms {
664                if let Ok(market) = self.base().market(s).await {
665                    market_ids.push(market.id.clone());
666                }
667            }
668            if market_ids.is_empty() {
669                None
670            } else {
671                Some(market_ids.join(","))
672            }
673        } else {
674            None
675        };
676
677        // Convert HashMap to serde_json::Value for merge_json_params
678        let json_params = params.map(|p| {
679            serde_json::Value::Object(
680                p.into_iter()
681                    .map(|(k, v)| (k, serde_json::Value::String(v)))
682                    .collect(),
683            )
684        });
685
686        let response = self
687            .signed_request(url)
688            .optional_param("symbols", symbols_param)
689            .merge_json_params(json_params)
690            .execute()
691            .await?;
692
693        let fees_array = response.as_array().ok_or_else(|| {
694            Error::from(ParseError::invalid_format(
695                "data",
696                "Expected array of trading fees",
697            ))
698        })?;
699
700        let mut fees = HashMap::new();
701        for fee_data in fees_array {
702            if let Ok(symbol_id) = fee_data["symbol"]
703                .as_str()
704                .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))
705            {
706                if let Ok(market) = self.base().market_by_id(symbol_id).await {
707                    if let Ok(fee) = parser::parse_trading_fee(fee_data) {
708                        fees.insert(market.symbol.clone(), fee);
709                    }
710                }
711            }
712        }
713
714        Ok(fees)
715    }
716
717    /// Create a listen key for user data stream.
718    ///
719    /// Creates a new listen key that can be used to subscribe to user data streams
720    /// via WebSocket. The listen key is valid for 60 minutes.
721    ///
722    /// # Returns
723    ///
724    /// Returns the listen key string.
725    ///
726    /// # Errors
727    ///
728    /// Returns an error if:
729    /// - API credentials are not configured
730    /// - API request fails
731    ///
732    /// # Example
733    ///
734    /// ```no_run
735    /// # use ccxt_exchanges::binance::Binance;
736    /// # use ccxt_core::ExchangeConfig;
737    /// # async fn example() -> ccxt_core::Result<()> {
738    /// let mut config = ExchangeConfig::default();
739    /// config.api_key = Some("your_api_key".to_string());
740    /// config.secret = Some("your_secret".to_string());
741    /// let binance = Binance::new(config)?;
742    ///
743    /// let listen_key = binance.create_listen_key().await?;
744    /// println!("Listen Key: {}", listen_key);
745    /// # Ok(())
746    /// # }
747    /// ```
748    pub async fn create_listen_key(&self) -> Result<String> {
749        self.check_required_credentials()?;
750
751        let url = format!("{}/userDataStream", self.urls().public);
752        let mut headers = HeaderMap::new();
753
754        let auth = self.get_auth()?;
755        auth.add_auth_headers_reqwest(&mut headers);
756
757        let response = self
758            .base()
759            .http_client
760            .post(&url, Some(headers), None)
761            .await?;
762
763        response["listenKey"]
764            .as_str()
765            .map(ToString::to_string)
766            .ok_or_else(|| Error::from(ParseError::missing_field("listenKey")))
767    }
768
769    /// Refresh listen key to extend validity.
770    ///
771    /// Extends the listen key validity by 60 minutes. Recommended to call
772    /// every 30 minutes to maintain the connection.
773    ///
774    /// # Arguments
775    ///
776    /// * `listen_key` - The listen key to refresh.
777    ///
778    /// # Returns
779    ///
780    /// Returns `Ok(())` on success.
781    ///
782    /// # Errors
783    ///
784    /// Returns an error if:
785    /// - API credentials are not configured
786    /// - Listen key is invalid or expired
787    /// - API request fails
788    pub async fn refresh_listen_key(&self, listen_key: &str) -> Result<()> {
789        self.check_required_credentials()?;
790
791        let url = format!(
792            "{}/userDataStream?listenKey={}",
793            self.urls().public,
794            listen_key
795        );
796        let mut headers = HeaderMap::new();
797
798        let auth = self.get_auth()?;
799        auth.add_auth_headers_reqwest(&mut headers);
800
801        let _response = self
802            .base()
803            .http_client
804            .put(&url, Some(headers), None)
805            .await?;
806
807        Ok(())
808    }
809
810    /// Delete listen key to close user data stream.
811    ///
812    /// Closes the user data stream connection and invalidates the listen key.
813    ///
814    /// # Arguments
815    ///
816    /// * `listen_key` - The listen key to delete.
817    ///
818    /// # Returns
819    ///
820    /// Returns `Ok(())` on success.
821    ///
822    /// # Errors
823    ///
824    /// Returns an error if:
825    /// - API credentials are not configured
826    /// - API request fails
827    pub async fn delete_listen_key(&self, listen_key: &str) -> Result<()> {
828        self.check_required_credentials()?;
829
830        let url = format!(
831            "{}/userDataStream?listenKey={}",
832            self.urls().public,
833            listen_key
834        );
835        let mut headers = HeaderMap::new();
836
837        let auth = self.get_auth()?;
838        auth.add_auth_headers_reqwest(&mut headers);
839
840        let _response = self
841            .base()
842            .http_client
843            .delete(&url, Some(headers), None)
844            .await?;
845
846        Ok(())
847    }
848}