asterdex-sdk 0.1.5

AsterDex Futures SDK v3 — Rust async client for REST and WebSocket APIs
Documentation
// US-008: Account endpoints — get_balance, get_account_with_join_margin,
//          get_position_side_dual, set_position_side_dual, get_leverage_bracket

use crate::futures::models::account::{
    AccountInfoResponse, Balance, LeverageBracket, PositionSideDualResponse, StatusMsgResponse,
};
use crate::rest::client::RestClient;
use crate::rest::error::AsterDexError;
use crate::rest::response::ApiResponse;

impl RestClient {
    /// Get account balance across all assets.
    ///
    /// Requires authentication. Returns wallet, cross-wallet, and available balances
    /// for each asset held in the futures account.
    ///
    /// # Errors
    /// - `ConfigError` — credentials not set
    /// - `ApiError` — exchange error (e.g. invalid timestamp)
    /// - `RateLimited` — HTTP 429
    pub async fn get_balance(&self) -> Result<ApiResponse<Vec<Balance>>, AsterDexError> {
        self.signed_get("/fapi/v3/balance", &[]).await
    }

    /// Get full account info with join margin details.
    ///
    /// Requires authentication.
    ///
    /// # Errors
    /// - `ConfigError` — credentials not set
    pub async fn get_account_with_join_margin(
        &self,
    ) -> Result<ApiResponse<AccountInfoResponse>, AsterDexError> {
        self.signed_get("/fapi/v3/accountWithJoinMargin", &[]).await
    }

    /// Get the current position side dual mode setting.
    ///
    /// Returns `dual_side_position = true` when hedge mode is active, `false` for one-way mode.
    ///
    /// # Errors
    /// - `ConfigError` — credentials not set
    pub async fn get_position_side_dual(
        &self,
    ) -> Result<ApiResponse<PositionSideDualResponse>, AsterDexError> {
        self.signed_get("/fapi/v3/positionSide/dual", &[]).await
    }

    /// Enable or disable position side dual (hedge) mode.
    ///
    /// Pass `true` to enable hedge mode, `false` for one-way mode.
    ///
    /// # Errors
    /// - `ConfigError` — credentials not set
    /// - `ApiError { code: -4059 }` — no need to change position side
    pub async fn set_position_side_dual(
        &self,
        dual_side_position: bool,
    ) -> Result<ApiResponse<StatusMsgResponse>, AsterDexError> {
        let val = if dual_side_position { "true" } else { "false" };
        self.signed_post("/fapi/v3/positionSide/dual", &[("dualSidePosition", val)])
            .await
    }

    /// Get leverage bracket information.
    ///
    /// Pass `Some("BTCUSDT")` for a specific symbol, or `None` for all symbols.
    ///
    /// # Errors
    /// - `ConfigError` — credentials not set
    /// - `ApiError { code: -1121 }` — invalid symbol
    pub async fn get_leverage_bracket(
        &self,
        symbol: Option<&str>,
    ) -> Result<ApiResponse<Vec<LeverageBracket>>, AsterDexError> {
        let mut params: Vec<(&str, &str)> = vec![];
        if let Some(s) = symbol {
            params.push(("symbol", s));
        }
        self.signed_get("/fapi/v3/leverageBracket", &params).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::auth::Credentials;

    // US-008: get_balance without credentials returns ConfigError
    #[tokio::test]
    async fn get_balance_no_creds_config_error() {
        let server = mockito::Server::new_async().await;
        // No mock needed — error is returned before any HTTP call
        let client = RestClient::new_public(&server.url()).unwrap();
        let result = client.get_balance().await;
        assert!(
            matches!(result, Err(AsterDexError::ConfigError { .. })),
            "expected ConfigError, got: {:?}",
            result
        );
        drop(server);
    }

    // US-008: get_position_side_dual without credentials returns ConfigError
    #[tokio::test]
    async fn get_position_side_dual_no_creds_config_error() {
        let server = mockito::Server::new_async().await;
        let client = RestClient::new_public(&server.url()).unwrap();
        let result = client.get_position_side_dual().await;
        assert!(
            matches!(result, Err(AsterDexError::ConfigError { .. })),
            "expected ConfigError, got: {:?}",
            result
        );
        drop(server);
    }

    // US-008: get_leverage_bracket with mock credentials and mockito server returns correct data
    #[tokio::test]
    async fn get_leverage_bracket_with_creds() {
        let mut server = mockito::Server::new_async().await;
        let _mock = server
            .mock("GET", "/fapi/v3/leverageBracket")
            .match_query(mockito::Matcher::Any)
            .with_status(200)
            .with_header("content-type", "application/json")
            .with_body(
                r#"[{"symbol":"BTCUSDT","brackets":[{"bracket":1,"initialLeverage":125,"notionalCap":"50000","notionalFloor":"0","maintMarginRatio":"0.004","cum":"0"}]}]"#,
            )
            .create_async()
            .await;

        // Use dummy credentials (private key = scalar 1)
        let creds = Credentials::new(
            "0x0000000000000000000000000000000000000001",
            "0x0000000000000000000000000000000000000002",
            "0000000000000000000000000000000000000000000000000000000000000001",
            714,
        )
        .unwrap();

        let client = RestClient::new(&server.url(), creds).unwrap();
        let resp = client.get_leverage_bracket(Some("BTCUSDT")).await.unwrap();

        assert_eq!(resp.data[0].symbol, "BTCUSDT");
        assert_eq!(resp.data[0].brackets[0].initial_leverage, 125);
    }
}