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}