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 .map(|v| v == "true")
570 .unwrap_or(false);
571
572 // Select API endpoint based on market type and Portfolio Margin mode
573 let url = match market.market_type {
574 MarketType::Spot => format!("{}/asset/tradeFee", self.urls().sapi),
575 MarketType::Futures | MarketType::Swap => {
576 if is_portfolio_margin {
577 // Portfolio Margin mode uses papi endpoints
578 if market.is_linear() {
579 format!("{}/um/commissionRate", self.urls().papi)
580 } else {
581 format!("{}/cm/commissionRate", self.urls().papi)
582 }
583 } else {
584 // Standard mode
585 if market.is_linear() {
586 format!("{}/commissionRate", self.urls().fapi_private)
587 } else {
588 format!("{}/commissionRate", self.urls().dapi_private)
589 }
590 }
591 }
592 _ => {
593 return Err(Error::invalid_request(format!(
594 "fetch_trading_fee not supported for market type: {:?}",
595 market.market_type
596 )));
597 }
598 };
599
600 // Convert HashMap to serde_json::Value, filtering out portfolioMargin
601 let json_params = params.map(|p| {
602 serde_json::Value::Object(
603 p.into_iter()
604 .filter(|(k, _)| k != "portfolioMargin")
605 .map(|(k, v)| (k, serde_json::Value::String(v)))
606 .collect(),
607 )
608 });
609
610 let response = self
611 .signed_request(url)
612 .param("symbol", &market.id)
613 .merge_json_params(json_params)
614 .execute()
615 .await?;
616
617 parser::parse_trading_fee(&response)
618 }
619
620 /// Fetch trading fees for multiple symbols.
621 ///
622 /// # Arguments
623 ///
624 /// * `symbols` - Optional list of trading pair symbols. `None` fetches all pairs.
625 /// * `params` - Optional parameters.
626 ///
627 /// # Returns
628 ///
629 /// Returns a `HashMap` of trading fees keyed by symbol.
630 ///
631 /// # Errors
632 ///
633 /// Returns an error if authentication fails or the API request fails.
634 ///
635 /// # Example
636 ///
637 /// ```no_run
638 /// # use ccxt_exchanges::binance::Binance;
639 /// # use ccxt_core::ExchangeConfig;
640 /// # async fn example() -> ccxt_core::Result<()> {
641 /// let mut config = ExchangeConfig::default();
642 /// config.api_key = Some("your_api_key".to_string());
643 /// config.secret = Some("your_secret".to_string());
644 /// let binance = Binance::new(config)?;
645 /// let fees = binance.fetch_trading_fees(None, None).await?;
646 /// for (symbol, fee) in &fees {
647 /// println!("{}: maker={}, taker={}", symbol, fee.maker, fee.taker);
648 /// }
649 /// # Ok(())
650 /// # }
651 /// ```
652 pub async fn fetch_trading_fees(
653 &self,
654 symbols: Option<Vec<String>>,
655 params: Option<HashMap<String, String>>,
656 ) -> Result<HashMap<String, FeeTradingFee>> {
657 self.load_markets(false).await?;
658
659 let url = format!("{}/asset/tradeFee", self.urls().sapi);
660
661 // Build symbols parameter if provided
662 let symbols_param = if let Some(syms) = &symbols {
663 let mut market_ids: Vec<String> = Vec::new();
664 for s in syms {
665 if let Ok(market) = self.base().market(s).await {
666 market_ids.push(market.id.clone());
667 }
668 }
669 if !market_ids.is_empty() {
670 Some(market_ids.join(","))
671 } else {
672 None
673 }
674 } else {
675 None
676 };
677
678 // Convert HashMap to serde_json::Value for merge_json_params
679 let json_params = params.map(|p| {
680 serde_json::Value::Object(
681 p.into_iter()
682 .map(|(k, v)| (k, serde_json::Value::String(v)))
683 .collect(),
684 )
685 });
686
687 let response = self
688 .signed_request(url)
689 .optional_param("symbols", symbols_param)
690 .merge_json_params(json_params)
691 .execute()
692 .await?;
693
694 let fees_array = response.as_array().ok_or_else(|| {
695 Error::from(ParseError::invalid_format(
696 "data",
697 "Expected array of trading fees",
698 ))
699 })?;
700
701 let mut fees = HashMap::new();
702 for fee_data in fees_array {
703 if let Ok(symbol_id) = fee_data["symbol"]
704 .as_str()
705 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))
706 {
707 if let Ok(market) = self.base().market_by_id(symbol_id).await {
708 if let Ok(fee) = parser::parse_trading_fee(fee_data) {
709 fees.insert(market.symbol.clone(), fee);
710 }
711 }
712 }
713 }
714
715 Ok(fees)
716 }
717
718 /// Create a listen key for user data stream.
719 ///
720 /// Creates a new listen key that can be used to subscribe to user data streams
721 /// via WebSocket. The listen key is valid for 60 minutes.
722 ///
723 /// # Returns
724 ///
725 /// Returns the listen key string.
726 ///
727 /// # Errors
728 ///
729 /// Returns an error if:
730 /// - API credentials are not configured
731 /// - API request fails
732 ///
733 /// # Example
734 ///
735 /// ```no_run
736 /// # use ccxt_exchanges::binance::Binance;
737 /// # use ccxt_core::ExchangeConfig;
738 /// # async fn example() -> ccxt_core::Result<()> {
739 /// let mut config = ExchangeConfig::default();
740 /// config.api_key = Some("your_api_key".to_string());
741 /// config.secret = Some("your_secret".to_string());
742 /// let binance = Binance::new(config)?;
743 ///
744 /// let listen_key = binance.create_listen_key().await?;
745 /// println!("Listen Key: {}", listen_key);
746 /// # Ok(())
747 /// # }
748 /// ```
749 pub async fn create_listen_key(&self) -> Result<String> {
750 self.check_required_credentials()?;
751
752 let url = format!("{}/userDataStream", self.urls().public);
753 let mut headers = HeaderMap::new();
754
755 let auth = self.get_auth()?;
756 auth.add_auth_headers_reqwest(&mut headers);
757
758 let response = self
759 .base()
760 .http_client
761 .post(&url, Some(headers), None)
762 .await?;
763
764 response["listenKey"]
765 .as_str()
766 .map(|s| s.to_string())
767 .ok_or_else(|| Error::from(ParseError::missing_field("listenKey")))
768 }
769
770 /// Refresh listen key to extend validity.
771 ///
772 /// Extends the listen key validity by 60 minutes. Recommended to call
773 /// every 30 minutes to maintain the connection.
774 ///
775 /// # Arguments
776 ///
777 /// * `listen_key` - The listen key to refresh.
778 ///
779 /// # Returns
780 ///
781 /// Returns `Ok(())` on success.
782 ///
783 /// # Errors
784 ///
785 /// Returns an error if:
786 /// - API credentials are not configured
787 /// - Listen key is invalid or expired
788 /// - API request fails
789 pub async fn refresh_listen_key(&self, listen_key: &str) -> Result<()> {
790 self.check_required_credentials()?;
791
792 let url = format!(
793 "{}/userDataStream?listenKey={}",
794 self.urls().public,
795 listen_key
796 );
797 let mut headers = HeaderMap::new();
798
799 let auth = self.get_auth()?;
800 auth.add_auth_headers_reqwest(&mut headers);
801
802 let _response = self
803 .base()
804 .http_client
805 .put(&url, Some(headers), None)
806 .await?;
807
808 Ok(())
809 }
810
811 /// Delete listen key to close user data stream.
812 ///
813 /// Closes the user data stream connection and invalidates the listen key.
814 ///
815 /// # Arguments
816 ///
817 /// * `listen_key` - The listen key to delete.
818 ///
819 /// # Returns
820 ///
821 /// Returns `Ok(())` on success.
822 ///
823 /// # Errors
824 ///
825 /// Returns an error if:
826 /// - API credentials are not configured
827 /// - API request fails
828 pub async fn delete_listen_key(&self, listen_key: &str) -> Result<()> {
829 self.check_required_credentials()?;
830
831 let url = format!(
832 "{}/userDataStream?listenKey={}",
833 self.urls().public,
834 listen_key
835 );
836 let mut headers = HeaderMap::new();
837
838 let auth = self.get_auth()?;
839 auth.add_auth_headers_reqwest(&mut headers);
840
841 let _response = self
842 .base()
843 .http_client
844 .delete(&url, Some(headers), None)
845 .await?;
846
847 Ok(())
848 }
849}