Skip to main content

asterdex_sdk/futures/endpoints/
account.rs

1// US-008: Account endpoints — get_balance, get_account_with_join_margin,
2//          get_position_side_dual, set_position_side_dual, get_leverage_bracket
3
4use crate::futures::models::account::{
5    AccountInfoResponse, Balance, LeverageBracket, PositionSideDualResponse, StatusMsgResponse,
6};
7use crate::rest::client::RestClient;
8use crate::rest::error::AsterDexError;
9use crate::rest::response::ApiResponse;
10
11impl RestClient {
12    /// Get account balance across all assets.
13    ///
14    /// Requires authentication. Returns wallet, cross-wallet, and available balances
15    /// for each asset held in the futures account.
16    ///
17    /// # Errors
18    /// - `ConfigError` — credentials not set
19    /// - `ApiError` — exchange error (e.g. invalid timestamp)
20    /// - `RateLimited` — HTTP 429
21    pub async fn get_balance(&self) -> Result<ApiResponse<Vec<Balance>>, AsterDexError> {
22        self.signed_get("/fapi/v3/balance", &[]).await
23    }
24
25    /// Get full account info with join margin details.
26    ///
27    /// Requires authentication.
28    ///
29    /// # Errors
30    /// - `ConfigError` — credentials not set
31    pub async fn get_account_with_join_margin(
32        &self,
33    ) -> Result<ApiResponse<AccountInfoResponse>, AsterDexError> {
34        self.signed_get("/fapi/v3/accountWithJoinMargin", &[]).await
35    }
36
37    /// Get the current position side dual mode setting.
38    ///
39    /// Returns `dual_side_position = true` when hedge mode is active, `false` for one-way mode.
40    ///
41    /// # Errors
42    /// - `ConfigError` — credentials not set
43    pub async fn get_position_side_dual(
44        &self,
45    ) -> Result<ApiResponse<PositionSideDualResponse>, AsterDexError> {
46        self.signed_get("/fapi/v3/positionSide/dual", &[]).await
47    }
48
49    /// Enable or disable position side dual (hedge) mode.
50    ///
51    /// Pass `true` to enable hedge mode, `false` for one-way mode.
52    ///
53    /// # Errors
54    /// - `ConfigError` — credentials not set
55    /// - `ApiError { code: -4059 }` — no need to change position side
56    pub async fn set_position_side_dual(
57        &self,
58        dual_side_position: bool,
59    ) -> Result<ApiResponse<StatusMsgResponse>, AsterDexError> {
60        let val = if dual_side_position { "true" } else { "false" };
61        self.signed_post("/fapi/v3/positionSide/dual", &[("dualSidePosition", val)])
62            .await
63    }
64
65    /// Get leverage bracket information.
66    ///
67    /// Pass `Some("BTCUSDT")` for a specific symbol, or `None` for all symbols.
68    ///
69    /// # Errors
70    /// - `ConfigError` — credentials not set
71    /// - `ApiError { code: -1121 }` — invalid symbol
72    pub async fn get_leverage_bracket(
73        &self,
74        symbol: Option<&str>,
75    ) -> Result<ApiResponse<Vec<LeverageBracket>>, AsterDexError> {
76        let mut params: Vec<(&str, &str)> = vec![];
77        if let Some(s) = symbol {
78            params.push(("symbol", s));
79        }
80        self.signed_get("/fapi/v3/leverageBracket", &params).await
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::auth::Credentials;
88
89    // US-008: get_balance without credentials returns ConfigError
90    #[tokio::test]
91    async fn get_balance_no_creds_config_error() {
92        let server = mockito::Server::new_async().await;
93        // No mock needed — error is returned before any HTTP call
94        let client = RestClient::new_public(&server.url()).unwrap();
95        let result = client.get_balance().await;
96        assert!(
97            matches!(result, Err(AsterDexError::ConfigError { .. })),
98            "expected ConfigError, got: {:?}",
99            result
100        );
101        drop(server);
102    }
103
104    // US-008: get_position_side_dual without credentials returns ConfigError
105    #[tokio::test]
106    async fn get_position_side_dual_no_creds_config_error() {
107        let server = mockito::Server::new_async().await;
108        let client = RestClient::new_public(&server.url()).unwrap();
109        let result = client.get_position_side_dual().await;
110        assert!(
111            matches!(result, Err(AsterDexError::ConfigError { .. })),
112            "expected ConfigError, got: {:?}",
113            result
114        );
115        drop(server);
116    }
117
118    // US-008: get_leverage_bracket with mock credentials and mockito server returns correct data
119    #[tokio::test]
120    async fn get_leverage_bracket_with_creds() {
121        let mut server = mockito::Server::new_async().await;
122        let _mock = server
123            .mock("GET", "/fapi/v3/leverageBracket")
124            .match_query(mockito::Matcher::Any)
125            .with_status(200)
126            .with_header("content-type", "application/json")
127            .with_body(
128                r#"[{"symbol":"BTCUSDT","brackets":[{"bracket":1,"initialLeverage":125,"notionalCap":"50000","notionalFloor":"0","maintMarginRatio":"0.004","cum":"0"}]}]"#,
129            )
130            .create_async()
131            .await;
132
133        // Use dummy credentials (private key = scalar 1)
134        let creds = Credentials::new(
135            "0x0000000000000000000000000000000000000001",
136            "0x0000000000000000000000000000000000000002",
137            "0000000000000000000000000000000000000000000000000000000000000001",
138            714,
139        )
140        .unwrap();
141
142        let client = RestClient::new(&server.url(), creds).unwrap();
143        let resp = client.get_leverage_bracket(Some("BTCUSDT")).await.unwrap();
144
145        assert_eq!(resp.data[0].symbol, "BTCUSDT");
146        assert_eq!(resp.data[0].brackets[0].initial_leverage, 125);
147    }
148}