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, BinanceEndpointRouter, parser};
7use ccxt_core::types::AccountType;
8use ccxt_core::types::default_type::DefaultSubType;
9use ccxt_core::{
10 Error, ParseError, Result,
11 types::{Balance, Currency, EndpointType, FeeTradingFee, MarketType, Trade},
12};
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 // Use market-type-aware endpoint routing
395 let base_url = self.rest_endpoint(&market, EndpointType::Private);
396 let url = format!("{}/myTrades", base_url);
397
398 let data = self
399 .signed_request(url)
400 .param("symbol", &market.id)
401 .optional_param("startTime", since)
402 .optional_param("limit", limit)
403 .execute()
404 .await?;
405
406 let trades_array = data.as_array().ok_or_else(|| {
407 Error::from(ParseError::invalid_format(
408 "data",
409 "Expected array of trades",
410 ))
411 })?;
412
413 let mut trades = Vec::new();
414
415 for trade_data in trades_array {
416 match parser::parse_trade(trade_data, Some(&market)) {
417 Ok(trade) => trades.push(trade),
418 Err(e) => {
419 warn!(error = %e, "Failed to parse trade");
420 }
421 }
422 }
423
424 Ok(trades)
425 }
426
427 /// Fetch user's recent trade history with additional parameters.
428 ///
429 /// This method is similar to `fetch_my_trades` but accepts additional parameters
430 /// for more flexible querying.
431 ///
432 /// # Arguments
433 ///
434 /// * `symbol` - Trading pair symbol.
435 /// * `since` - Optional start timestamp in milliseconds.
436 /// * `limit` - Optional limit on number of trades (default: 500, max: 1000).
437 /// * `params` - Optional additional parameters that may include:
438 /// - `orderId`: Filter by order ID.
439 /// - `fromId`: Start from specific trade ID.
440 /// - `endTime`: End timestamp in milliseconds.
441 ///
442 /// # Returns
443 ///
444 /// Returns a vector of [`Trade`] structures for the user's trades.
445 ///
446 /// # Errors
447 ///
448 /// Returns an error if authentication fails, market is not found, or the API request fails.
449 ///
450 /// # Example
451 ///
452 /// ```no_run
453 /// # use ccxt_exchanges::binance::Binance;
454 /// # use ccxt_core::ExchangeConfig;
455 /// # async fn example() -> ccxt_core::Result<()> {
456 /// let mut config = ExchangeConfig::default();
457 /// config.api_key = Some("your_api_key".to_string());
458 /// config.secret = Some("your_secret".to_string());
459 /// let binance = Binance::new(config)?;
460 /// let my_trades = binance.fetch_my_recent_trades("BTC/USDT", None, Some(50), None).await?;
461 /// for trade in &my_trades {
462 /// println!("Trade: {} {} @ {}", trade.side, trade.amount, trade.price);
463 /// }
464 /// # Ok(())
465 /// # }
466 /// ```
467 pub async fn fetch_my_recent_trades(
468 &self,
469 symbol: &str,
470 since: Option<i64>,
471 limit: Option<u32>,
472 params: Option<HashMap<String, String>>,
473 ) -> Result<Vec<Trade>> {
474 let market = self.base().market(symbol).await?;
475 // Use market-type-aware endpoint routing
476 let base_url = self.rest_endpoint(&market, EndpointType::Private);
477 let url = format!("{}/myTrades", base_url);
478
479 // Convert HashMap to serde_json::Value for merge_json_params
480 let json_params = params.map(|p| {
481 serde_json::Value::Object(
482 p.into_iter()
483 .map(|(k, v)| (k, serde_json::Value::String(v)))
484 .collect(),
485 )
486 });
487
488 let data = self
489 .signed_request(url)
490 .param("symbol", &market.id)
491 .optional_param("startTime", since)
492 .optional_param("limit", limit)
493 .merge_json_params(json_params)
494 .execute()
495 .await?;
496
497 let trades_array = data.as_array().ok_or_else(|| {
498 Error::from(ParseError::invalid_format(
499 "data",
500 "Expected array of trades",
501 ))
502 })?;
503
504 let mut trades = Vec::new();
505 for trade_data in trades_array {
506 match parser::parse_trade(trade_data, Some(&market)) {
507 Ok(trade) => trades.push(trade),
508 Err(e) => {
509 warn!(error = %e, "Failed to parse my trade");
510 }
511 }
512 }
513
514 Ok(trades)
515 }
516
517 /// Fetch all currency information.
518 ///
519 /// # Returns
520 ///
521 /// Returns a vector of [`Currency`] structures.
522 ///
523 /// # Errors
524 ///
525 /// Returns an error if authentication fails or the API request fails.
526 pub async fn fetch_currencies(&self) -> Result<Vec<Currency>> {
527 let url = format!("{}/capital/config/getall", self.urls().sapi);
528 let data = self.signed_request(url).execute().await?;
529 parser::parse_currencies(&data)
530 }
531
532 /// Fetch trading fees for a symbol.
533 ///
534 /// # Arguments
535 ///
536 /// * `symbol` - Trading pair symbol.
537 /// * `params` - Optional parameters. Supports `portfolioMargin` key for Portfolio Margin mode.
538 ///
539 /// # Returns
540 ///
541 /// Returns trading fee information for the symbol as a [`FeeTradingFee`] structure.
542 ///
543 /// # Errors
544 ///
545 /// Returns an error if authentication fails, market is not found, or the API request fails.
546 ///
547 /// # Example
548 ///
549 /// ```no_run
550 /// # use ccxt_exchanges::binance::Binance;
551 /// # use ccxt_core::ExchangeConfig;
552 /// # async fn example() -> ccxt_core::Result<()> {
553 /// let mut config = ExchangeConfig::default();
554 /// config.api_key = Some("your_api_key".to_string());
555 /// config.secret = Some("your_secret".to_string());
556 /// let binance = Binance::new(config)?;
557 /// let fee = binance.fetch_trading_fee("BTC/USDT", None).await?;
558 /// println!("Maker: {}, Taker: {}", fee.maker, fee.taker);
559 /// # Ok(())
560 /// # }
561 /// ```
562 pub async fn fetch_trading_fee(
563 &self,
564 symbol: &str,
565 params: Option<HashMap<String, String>>,
566 ) -> Result<FeeTradingFee> {
567 self.load_markets(false).await?;
568 let market = self.base().market(symbol).await?;
569
570 let is_portfolio_margin = params
571 .as_ref()
572 .and_then(|p| p.get("portfolioMargin"))
573 .is_some_and(|v| v == "true");
574
575 // Select API endpoint based on market type and Portfolio Margin mode
576 let url = match market.market_type {
577 MarketType::Spot => format!("{}/asset/tradeFee", self.urls().sapi),
578 MarketType::Futures | MarketType::Swap => {
579 if is_portfolio_margin {
580 // Portfolio Margin mode uses papi endpoints
581 if market.is_linear() {
582 format!("{}/um/commissionRate", self.urls().papi)
583 } else {
584 format!("{}/cm/commissionRate", self.urls().papi)
585 }
586 } else {
587 // Standard mode
588 if market.is_linear() {
589 format!("{}/commissionRate", self.urls().fapi_private)
590 } else {
591 format!("{}/commissionRate", self.urls().dapi_private)
592 }
593 }
594 }
595 MarketType::Option => {
596 return Err(Error::invalid_request(format!(
597 "fetch_trading_fee not supported for market type: {:?}",
598 market.market_type
599 )));
600 }
601 };
602
603 // Convert HashMap to serde_json::Value, filtering out portfolioMargin
604 let json_params = params.map(|p| {
605 serde_json::Value::Object(
606 p.into_iter()
607 .filter(|(k, _)| k != "portfolioMargin")
608 .map(|(k, v)| (k, serde_json::Value::String(v)))
609 .collect(),
610 )
611 });
612
613 let response = self
614 .signed_request(url)
615 .param("symbol", &market.id)
616 .merge_json_params(json_params)
617 .execute()
618 .await?;
619
620 parser::parse_trading_fee(&response)
621 }
622
623 /// Fetch trading fees for multiple symbols.
624 ///
625 /// # Arguments
626 ///
627 /// * `symbols` - Optional list of trading pair symbols. `None` fetches all pairs.
628 /// * `params` - Optional parameters.
629 ///
630 /// # Returns
631 ///
632 /// Returns a `HashMap` of trading fees keyed by symbol.
633 ///
634 /// # Errors
635 ///
636 /// Returns an error if authentication fails or the API request fails.
637 ///
638 /// # Example
639 ///
640 /// ```no_run
641 /// # use ccxt_exchanges::binance::Binance;
642 /// # use ccxt_core::ExchangeConfig;
643 /// # async fn example() -> ccxt_core::Result<()> {
644 /// let mut config = ExchangeConfig::default();
645 /// config.api_key = Some("your_api_key".to_string());
646 /// config.secret = Some("your_secret".to_string());
647 /// let binance = Binance::new(config)?;
648 /// let fees = binance.fetch_trading_fees(None, None).await?;
649 /// for (symbol, fee) in &fees {
650 /// println!("{}: maker={}, taker={}", symbol, fee.maker, fee.taker);
651 /// }
652 /// # Ok(())
653 /// # }
654 /// ```
655 pub async fn fetch_trading_fees(
656 &self,
657 symbols: Option<Vec<String>>,
658 params: Option<HashMap<String, String>>,
659 ) -> Result<HashMap<String, FeeTradingFee>> {
660 self.load_markets(false).await?;
661
662 let url = format!("{}/asset/tradeFee", self.urls().sapi);
663
664 // Build symbols parameter if provided
665 let symbols_param = if let Some(syms) = &symbols {
666 let mut market_ids: Vec<String> = Vec::new();
667 for s in syms {
668 if let Ok(market) = self.base().market(s).await {
669 market_ids.push(market.id.clone());
670 }
671 }
672 if market_ids.is_empty() {
673 None
674 } else {
675 Some(market_ids.join(","))
676 }
677 } else {
678 None
679 };
680
681 // Convert HashMap to serde_json::Value for merge_json_params
682 let json_params = params.map(|p| {
683 serde_json::Value::Object(
684 p.into_iter()
685 .map(|(k, v)| (k, serde_json::Value::String(v)))
686 .collect(),
687 )
688 });
689
690 let response = self
691 .signed_request(url)
692 .optional_param("symbols", symbols_param)
693 .merge_json_params(json_params)
694 .execute()
695 .await?;
696
697 let fees_array = response.as_array().ok_or_else(|| {
698 Error::from(ParseError::invalid_format(
699 "data",
700 "Expected array of trading fees",
701 ))
702 })?;
703
704 let mut fees = HashMap::new();
705 for fee_data in fees_array {
706 if let Ok(symbol_id) = fee_data["symbol"]
707 .as_str()
708 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))
709 {
710 if let Ok(market) = self.base().market_by_id(symbol_id).await {
711 if let Ok(fee) = parser::parse_trading_fee(fee_data) {
712 fees.insert(market.symbol.clone(), fee);
713 }
714 }
715 }
716 }
717
718 Ok(fees)
719 }
720
721 /// Create a listen key for user data stream (spot market).
722 ///
723 /// Creates a new listen key that can be used to subscribe to user data streams
724 /// via WebSocket. The listen key is valid for 60 minutes.
725 ///
726 /// # Returns
727 ///
728 /// Returns the listen key string.
729 ///
730 /// # Errors
731 ///
732 /// Returns an error if:
733 /// - API credentials are not configured
734 /// - API request fails
735 ///
736 /// # Example
737 ///
738 /// ```no_run
739 /// # use ccxt_exchanges::binance::Binance;
740 /// # use ccxt_core::ExchangeConfig;
741 /// # async fn example() -> ccxt_core::Result<()> {
742 /// let mut config = ExchangeConfig::default();
743 /// config.api_key = Some("your_api_key".to_string());
744 /// config.secret = Some("your_secret".to_string());
745 /// let binance = Binance::new(config)?;
746 ///
747 /// let listen_key = binance.create_listen_key().await?;
748 /// println!("Listen Key: {}", listen_key);
749 /// # Ok(())
750 /// # }
751 /// ```
752 pub async fn create_listen_key(&self) -> Result<String> {
753 self.create_listen_key_for_market(None).await
754 }
755
756 /// Create a listen key for user data stream with market type routing.
757 ///
758 /// Creates a new listen key for the specified market type. Different market types
759 /// use different API endpoints:
760 /// - Spot: `/api/v3/userDataStream`
761 /// - USDT-M Futures: `/fapi/v1/listenKey`
762 /// - COIN-M Futures: `/dapi/v1/listenKey`
763 /// - Options: `/eapi/v1/listenKey`
764 ///
765 /// # Arguments
766 ///
767 /// * `market_type` - Optional market type. If None, defaults to Spot.
768 ///
769 /// # Returns
770 ///
771 /// Returns the listen key string.
772 ///
773 /// # Errors
774 ///
775 /// Returns an error if:
776 /// - API credentials are not configured
777 /// - API request fails
778 ///
779 /// # Example
780 ///
781 /// ```no_run
782 /// # use ccxt_exchanges::binance::Binance;
783 /// # use ccxt_core::ExchangeConfig;
784 /// # use ccxt_core::types::MarketType;
785 /// # async fn example() -> ccxt_core::Result<()> {
786 /// let mut config = ExchangeConfig::default();
787 /// config.api_key = Some("your_api_key".to_string());
788 /// config.secret = Some("your_secret".to_string());
789 /// let binance = Binance::new(config)?;
790 ///
791 /// // Create listen key for futures
792 /// let listen_key = binance.create_listen_key_for_market(Some(MarketType::Swap)).await?;
793 /// println!("Futures Listen Key: {}", listen_key);
794 /// # Ok(())
795 /// # }
796 /// ```
797 pub async fn create_listen_key_for_market(
798 &self,
799 market_type: Option<MarketType>,
800 ) -> Result<String> {
801 let url = self.get_listen_key_url(market_type, None);
802
803 let response = self
804 .api_key_request(url)
805 .method(crate::binance::signed_request::HttpMethod::Post)
806 .execute()
807 .await?;
808
809 response["listenKey"]
810 .as_str()
811 .map(ToString::to_string)
812 .ok_or_else(|| Error::from(ParseError::missing_field("listenKey")))
813 }
814
815 /// Refresh listen key to extend validity (spot market).
816 ///
817 /// Extends the listen key validity by 60 minutes. Recommended to call
818 /// every 30 minutes to maintain the connection.
819 ///
820 /// # Arguments
821 ///
822 /// * `listen_key` - The listen key to refresh.
823 ///
824 /// # Returns
825 ///
826 /// Returns `Ok(())` on success.
827 ///
828 /// # Errors
829 ///
830 /// Returns an error if:
831 /// - API credentials are not configured
832 /// - Listen key is invalid or expired
833 /// - API request fails
834 pub async fn refresh_listen_key(&self, listen_key: &str) -> Result<()> {
835 self.refresh_listen_key_for_market(listen_key, None).await
836 }
837
838 /// Refresh listen key with market type routing.
839 ///
840 /// Extends the listen key validity by 60 minutes for the specified market type.
841 ///
842 /// # Arguments
843 ///
844 /// * `listen_key` - The listen key to refresh.
845 /// * `market_type` - Optional market type. If None, defaults to Spot.
846 ///
847 /// # Returns
848 ///
849 /// Returns `Ok(())` on success.
850 ///
851 /// # Errors
852 ///
853 /// Returns an error if:
854 /// - API credentials are not configured
855 /// - Listen key is invalid or expired
856 /// - API request fails
857 pub async fn refresh_listen_key_for_market(
858 &self,
859 listen_key: &str,
860 market_type: Option<MarketType>,
861 ) -> Result<()> {
862 let url = self.get_listen_key_url(market_type, Some(listen_key));
863
864 self.api_key_request(url)
865 .method(crate::binance::signed_request::HttpMethod::Put)
866 .execute()
867 .await?;
868
869 Ok(())
870 }
871
872 /// Delete listen key to close user data stream (spot market).
873 ///
874 /// Closes the user data stream connection and invalidates the listen key.
875 ///
876 /// # Arguments
877 ///
878 /// * `listen_key` - The listen key to delete.
879 ///
880 /// # Returns
881 ///
882 /// Returns `Ok(())` on success.
883 ///
884 /// # Errors
885 ///
886 /// Returns an error if:
887 /// - API credentials are not configured
888 /// - API request fails
889 pub async fn delete_listen_key(&self, listen_key: &str) -> Result<()> {
890 self.delete_listen_key_for_market(listen_key, None).await
891 }
892
893 /// Delete listen key with market type routing.
894 ///
895 /// Closes the user data stream connection for the specified market type.
896 ///
897 /// # Arguments
898 ///
899 /// * `listen_key` - The listen key to delete.
900 /// * `market_type` - Optional market type. If None, defaults to Spot.
901 ///
902 /// # Returns
903 ///
904 /// Returns `Ok(())` on success.
905 ///
906 /// # Errors
907 ///
908 /// Returns an error if:
909 /// - API credentials are not configured
910 /// - API request fails
911 pub async fn delete_listen_key_for_market(
912 &self,
913 listen_key: &str,
914 market_type: Option<MarketType>,
915 ) -> Result<()> {
916 let url = self.get_listen_key_url(market_type, Some(listen_key));
917
918 self.api_key_request(url)
919 .method(crate::binance::signed_request::HttpMethod::Delete)
920 .execute()
921 .await?;
922
923 Ok(())
924 }
925
926 /// Get the listen key URL for the specified market type.
927 ///
928 /// # Arguments
929 ///
930 /// * `market_type` - Optional market type. If None, defaults to Spot.
931 /// * `listen_key` - Optional listen key to include in the URL (for refresh/delete).
932 ///
933 /// # Returns
934 ///
935 /// Returns the appropriate URL for the listen key operation.
936 fn get_listen_key_url(
937 &self,
938 market_type: Option<MarketType>,
939 listen_key: Option<&str>,
940 ) -> String {
941 let urls = self.urls();
942 let (base_url, endpoint) = match market_type {
943 Some(MarketType::Swap | MarketType::Futures) => {
944 // Distinguish between linear (USDT-M) and inverse (COIN-M)
945 if self.options().default_sub_type == Some(DefaultSubType::Inverse) {
946 (urls.dapi_public.clone(), "listenKey")
947 } else {
948 (urls.fapi_public.clone(), "listenKey")
949 }
950 }
951 Some(MarketType::Option) => (urls.eapi_public.clone(), "listenKey"),
952 // Spot is the default
953 None | Some(MarketType::Spot) => (urls.public.clone(), "userDataStream"),
954 };
955
956 match listen_key {
957 Some(key) => format!("{}/{}?listenKey={}", base_url, endpoint, key),
958 None => format!("{}/{}", base_url, endpoint),
959 }
960 }
961
962 /// Get the listen key URL for linear (USDT-M) futures.
963 ///
964 /// # Arguments
965 ///
966 /// * `listen_key` - Optional listen key to include in the URL.
967 ///
968 /// # Returns
969 ///
970 /// Returns the URL for linear futures listen key operations.
971 pub fn get_linear_listen_key_url(&self, listen_key: Option<&str>) -> String {
972 let base_url = &self.urls().fapi_public;
973 match listen_key {
974 Some(key) => format!("{}/listenKey?listenKey={}", base_url, key),
975 None => format!("{}/listenKey", base_url),
976 }
977 }
978
979 /// Get the listen key URL for inverse (COIN-M) futures.
980 ///
981 /// # Arguments
982 ///
983 /// * `listen_key` - Optional listen key to include in the URL.
984 ///
985 /// # Returns
986 ///
987 /// Returns the URL for inverse futures listen key operations.
988 pub fn get_inverse_listen_key_url(&self, listen_key: Option<&str>) -> String {
989 let base_url = &self.urls().dapi_public;
990 match listen_key {
991 Some(key) => format!("{}/listenKey?listenKey={}", base_url, key),
992 None => format!("{}/listenKey", base_url),
993 }
994 }
995
996 /// Create a listen key for linear (USDT-M) futures.
997 ///
998 /// # Returns
999 ///
1000 /// Returns the listen key string.
1001 ///
1002 /// # Errors
1003 ///
1004 /// Returns an error if API credentials are not configured or the request fails.
1005 pub async fn create_linear_listen_key(&self) -> Result<String> {
1006 let url = self.get_linear_listen_key_url(None);
1007
1008 let response = self
1009 .api_key_request(url)
1010 .method(crate::binance::signed_request::HttpMethod::Post)
1011 .execute()
1012 .await?;
1013
1014 response["listenKey"]
1015 .as_str()
1016 .map(ToString::to_string)
1017 .ok_or_else(|| Error::from(ParseError::missing_field("listenKey")))
1018 }
1019
1020 /// Create a listen key for inverse (COIN-M) futures.
1021 ///
1022 /// # Returns
1023 ///
1024 /// Returns the listen key string.
1025 ///
1026 /// # Errors
1027 ///
1028 /// Returns an error if API credentials are not configured or the request fails.
1029 pub async fn create_inverse_listen_key(&self) -> Result<String> {
1030 let url = self.get_inverse_listen_key_url(None);
1031
1032 let response = self
1033 .api_key_request(url)
1034 .method(crate::binance::signed_request::HttpMethod::Post)
1035 .execute()
1036 .await?;
1037
1038 response["listenKey"]
1039 .as_str()
1040 .map(ToString::to_string)
1041 .ok_or_else(|| Error::from(ParseError::missing_field("listenKey")))
1042 }
1043
1044 /// Refresh a linear (USDT-M) futures listen key.
1045 ///
1046 /// # Arguments
1047 ///
1048 /// * `listen_key` - The listen key to refresh.
1049 ///
1050 /// # Returns
1051 ///
1052 /// Returns `Ok(())` on success.
1053 pub async fn refresh_linear_listen_key(&self, listen_key: &str) -> Result<()> {
1054 let url = self.get_linear_listen_key_url(Some(listen_key));
1055
1056 self.api_key_request(url)
1057 .method(crate::binance::signed_request::HttpMethod::Put)
1058 .execute()
1059 .await?;
1060
1061 Ok(())
1062 }
1063
1064 /// Refresh an inverse (COIN-M) futures listen key.
1065 ///
1066 /// # Arguments
1067 ///
1068 /// * `listen_key` - The listen key to refresh.
1069 ///
1070 /// # Returns
1071 ///
1072 /// Returns `Ok(())` on success.
1073 pub async fn refresh_inverse_listen_key(&self, listen_key: &str) -> Result<()> {
1074 let url = self.get_inverse_listen_key_url(Some(listen_key));
1075
1076 self.api_key_request(url)
1077 .method(crate::binance::signed_request::HttpMethod::Put)
1078 .execute()
1079 .await?;
1080
1081 Ok(())
1082 }
1083
1084 /// Delete a linear (USDT-M) futures listen key.
1085 ///
1086 /// # Arguments
1087 ///
1088 /// * `listen_key` - The listen key to delete.
1089 ///
1090 /// # Returns
1091 ///
1092 /// Returns `Ok(())` on success.
1093 pub async fn delete_linear_listen_key(&self, listen_key: &str) -> Result<()> {
1094 let url = self.get_linear_listen_key_url(Some(listen_key));
1095
1096 self.api_key_request(url)
1097 .method(crate::binance::signed_request::HttpMethod::Delete)
1098 .execute()
1099 .await?;
1100
1101 Ok(())
1102 }
1103
1104 /// Delete an inverse (COIN-M) futures listen key.
1105 ///
1106 /// # Arguments
1107 ///
1108 /// * `listen_key` - The listen key to delete.
1109 ///
1110 /// # Returns
1111 ///
1112 /// Returns `Ok(())` on success.
1113 pub async fn delete_inverse_listen_key(&self, listen_key: &str) -> Result<()> {
1114 let url = self.get_inverse_listen_key_url(Some(listen_key));
1115
1116 self.api_key_request(url)
1117 .method(crate::binance::signed_request::HttpMethod::Delete)
1118 .execute()
1119 .await?;
1120
1121 Ok(())
1122 }
1123}