asterdex-sdk 0.1.5

AsterDex Futures SDK v3 — Rust async client for REST and WebSocket APIs
Documentation
// US-014: Remaining trading and account endpoints

use crate::futures::models::account::{
    AdlQuantileResponse, CommissionRateResponse, MultiAssetsModeResponse,
    PositionMarginHistoryRecord, StatusMsgResponse,
};
use crate::futures::models::trading::{
    AllOrdersParams, CountdownParams, CountdownResponse, EmptyResponse, GetOrderParams,
    IncomeParams, IncomeRecord, OrderResponse, PositionMarginParams, PositionMarginResponse,
    UserTrade, UserTradesParams,
};
use crate::rest::client::RestClient;
use crate::rest::error::AsterDexError;
use crate::rest::response::ApiResponse;

impl RestClient {
    /// Query a specific order by ID or client order ID.
    pub async fn get_order(
        &self,
        params: GetOrderParams,
    ) -> Result<ApiResponse<OrderResponse>, AsterDexError> {
        let order_id_str = params.order_id.map(|id| id.to_string());
        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(3);
        pairs.push(("symbol", &params.symbol));
        if let Some(ref s) = order_id_str {
            pairs.push(("orderId", s));
        }
        if let Some(ref cid) = params.orig_client_order_id {
            pairs.push(("origClientOrderId", cid));
        }
        self.signed_get("/fapi/v3/order", &pairs).await
    }

    /// Query a specific open order by ID or client order ID.
    pub async fn get_open_order(
        &self,
        params: GetOrderParams,
    ) -> Result<ApiResponse<OrderResponse>, AsterDexError> {
        let order_id_str = params.order_id.map(|id| id.to_string());
        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(3);
        pairs.push(("symbol", &params.symbol));
        if let Some(ref s) = order_id_str {
            pairs.push(("orderId", s));
        }
        if let Some(ref cid) = params.orig_client_order_id {
            pairs.push(("origClientOrderId", cid));
        }
        self.signed_get("/fapi/v3/openOrder", &pairs).await
    }

    /// Get all orders (open + historical) for a symbol.
    pub async fn get_all_orders(
        &self,
        params: AllOrdersParams,
    ) -> Result<ApiResponse<Vec<OrderResponse>>, AsterDexError> {
        let order_id_str = params.order_id.map(|id| id.to_string());
        let start_time_str = params.start_time.map(|t| t.to_string());
        let end_time_str = params.end_time.map(|t| t.to_string());
        let limit_str = params.limit.map(|l| l.to_string());

        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(5);
        pairs.push(("symbol", &params.symbol));
        if let Some(ref s) = order_id_str {
            pairs.push(("orderId", s));
        }
        if let Some(ref s) = start_time_str {
            pairs.push(("startTime", s));
        }
        if let Some(ref s) = end_time_str {
            pairs.push(("endTime", s));
        }
        if let Some(ref s) = limit_str {
            pairs.push(("limit", s));
        }
        self.signed_get("/fapi/v3/allOrders", &pairs).await
    }

    /// Get user trade history for a symbol.
    pub async fn get_user_trades(
        &self,
        params: UserTradesParams,
    ) -> Result<ApiResponse<Vec<UserTrade>>, AsterDexError> {
        let order_id_str = params.order_id.map(|id| id.to_string());
        let start_time_str = params.start_time.map(|t| t.to_string());
        let end_time_str = params.end_time.map(|t| t.to_string());
        let from_id_str = params.from_id.map(|id| id.to_string());
        let limit_str = params.limit.map(|l| l.to_string());

        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(6);
        pairs.push(("symbol", &params.symbol));
        if let Some(ref s) = order_id_str {
            pairs.push(("orderId", s));
        }
        if let Some(ref s) = start_time_str {
            pairs.push(("startTime", s));
        }
        if let Some(ref s) = end_time_str {
            pairs.push(("endTime", s));
        }
        if let Some(ref s) = from_id_str {
            pairs.push(("fromId", s));
        }
        if let Some(ref s) = limit_str {
            pairs.push(("limit", s));
        }
        self.signed_get("/fapi/v3/trades", &pairs).await
    }

    /// Get income history (funding fees, realized PnL, etc.).
    pub async fn get_income(
        &self,
        params: IncomeParams,
    ) -> Result<ApiResponse<Vec<IncomeRecord>>, AsterDexError> {
        let start_time_str = params.start_time.map(|t| t.to_string());
        let end_time_str = params.end_time.map(|t| t.to_string());
        let limit_str = params.limit.map(|l| l.to_string());

        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(5);
        if let Some(ref s) = params.symbol {
            pairs.push(("symbol", s));
        }
        if let Some(ref it) = params.income_type {
            pairs.push(("incomeType", it));
        }
        if let Some(ref s) = start_time_str {
            pairs.push(("startTime", s));
        }
        if let Some(ref s) = end_time_str {
            pairs.push(("endTime", s));
        }
        if let Some(ref s) = limit_str {
            pairs.push(("limit", s));
        }
        self.signed_get("/fapi/v3/income", &pairs).await
    }

    /// Add or reduce isolated position margin (type 1 = add, type 2 = reduce).
    pub async fn set_position_margin(
        &self,
        params: PositionMarginParams,
    ) -> Result<ApiResponse<PositionMarginResponse>, AsterDexError> {
        let margin_type_str = params.margin_type.to_string();
        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(4);
        pairs.push(("symbol", &params.symbol));
        pairs.push(("amount", &params.amount));
        pairs.push(("type", &margin_type_str));
        if let Some(ref ps) = params.position_side {
            pairs.push(("positionSide", ps));
        }
        self.signed_post("/fapi/v3/positionMargin", &pairs).await
    }

    /// Get position margin change history.
    pub async fn get_position_margin_history(
        &self,
        symbol: &str,
        margin_type: Option<u8>,
        start_time: Option<u64>,
        end_time: Option<u64>,
        limit: Option<u32>,
    ) -> Result<ApiResponse<Vec<PositionMarginHistoryRecord>>, AsterDexError> {
        let margin_type_str = margin_type.map(|t| t.to_string());
        let start_time_str = start_time.map(|t| t.to_string());
        let end_time_str = end_time.map(|t| t.to_string());
        let limit_str = limit.map(|l| l.to_string());

        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(5);
        pairs.push(("symbol", symbol));
        if let Some(ref s) = margin_type_str {
            pairs.push(("type", s));
        }
        if let Some(ref s) = start_time_str {
            pairs.push(("startTime", s));
        }
        if let Some(ref s) = end_time_str {
            pairs.push(("endTime", s));
        }
        if let Some(ref s) = limit_str {
            pairs.push(("limit", s));
        }
        self.signed_get("/fapi/v3/positionMargin/history", &pairs).await
    }

    /// Get auto-deleverage quantile for positions.
    pub async fn get_adl_quantile(
        &self,
        symbol: Option<&str>,
    ) -> Result<ApiResponse<Vec<AdlQuantileResponse>>, AsterDexError> {
        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(1);
        if let Some(s) = symbol {
            pairs.push(("symbol", s));
        }
        self.signed_get("/fapi/v3/adlQuantile", &pairs).await
    }

    /// Get forced liquidation order history.
    pub async fn get_force_orders(
        &self,
        symbol: Option<&str>,
        auto_close_type: Option<&str>,
        start_time: Option<u64>,
        end_time: Option<u64>,
        limit: Option<u32>,
    ) -> Result<ApiResponse<Vec<OrderResponse>>, AsterDexError> {
        let start_time_str = start_time.map(|t| t.to_string());
        let end_time_str = end_time.map(|t| t.to_string());
        let limit_str = limit.map(|l| l.to_string());

        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(5);
        if let Some(s) = symbol {
            pairs.push(("symbol", s));
        }
        if let Some(t) = auto_close_type {
            pairs.push(("autoCloseType", t));
        }
        if let Some(ref s) = start_time_str {
            pairs.push(("startTime", s));
        }
        if let Some(ref s) = end_time_str {
            pairs.push(("endTime", s));
        }
        if let Some(ref s) = limit_str {
            pairs.push(("limit", s));
        }
        self.signed_get("/fapi/v3/forceOrders", &pairs).await
    }

    /// Get user commission rate for a symbol.
    pub async fn get_commission_rate(
        &self,
        symbol: &str,
    ) -> Result<ApiResponse<CommissionRateResponse>, AsterDexError> {
        self.signed_get("/fapi/v3/commissionRate", &[("symbol", symbol)]).await
    }

    /// Get multi-assets margin mode status.
    pub async fn get_multi_assets_margin(
        &self,
    ) -> Result<ApiResponse<MultiAssetsModeResponse>, AsterDexError> {
        self.signed_get("/fapi/v3/multiAssetsMargin", &[]).await
    }

    /// Change multi-assets margin mode.
    pub async fn set_multi_assets_margin(
        &self,
        enabled: bool,
    ) -> Result<ApiResponse<StatusMsgResponse>, AsterDexError> {
        let val = if enabled { "true" } else { "false" };
        self.signed_post("/fapi/v3/multiAssetsMargin", &[("multiAssetsMargin", val)]).await
    }

    /// Set countdown to auto-cancel all orders. `countdown_time = 0` disables.
    pub async fn countdown_cancel_all(
        &self,
        params: CountdownParams,
    ) -> Result<ApiResponse<CountdownResponse>, AsterDexError> {
        let ct_str = params.countdown_time.to_string();
        self.signed_post(
            "/fapi/v3/countdownCancelAll",
            &[("symbol", params.symbol.as_str()), ("countdownTime", &ct_str)],
        )
        .await
    }

    /// No-operation signed endpoint — useful for testing auth.
    pub async fn noop(&self) -> Result<ApiResponse<EmptyResponse>, AsterDexError> {
        self.signed_post("/fapi/v3/noop", &[]).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::futures::models::trading::{AllOrdersParams, GetOrderParams};
    use mockito::Server;

    // US-014: get_order without credentials returns ConfigError
    #[tokio::test]
    async fn get_order_without_credentials_returns_config_error() {
        let client = RestClient::new_public("https://fapi.asterdex.com").unwrap();
        let params = GetOrderParams {
            symbol: "BTCUSDT".to_string(),
            order_id: Some(123456),
            orig_client_order_id: None,
        };
        let result = client.get_order(params).await;
        assert!(
            matches!(result, Err(AsterDexError::ConfigError { .. })),
            "expected ConfigError, got: {:?}",
            result
        );
    }

    // US-014: get_all_orders without credentials returns ConfigError
    #[tokio::test]
    async fn get_all_orders_without_credentials_returns_config_error() {
        let client = RestClient::new_public("https://fapi.asterdex.com").unwrap();
        let params = AllOrdersParams {
            symbol: "BTCUSDT".to_string(),
            limit: Some(100),
            ..Default::default()
        };
        let result = client.get_all_orders(params).await;
        assert!(
            matches!(result, Err(AsterDexError::ConfigError { .. })),
            "expected ConfigError, got: {:?}",
            result
        );
    }

    // US-014: noop without credentials returns ConfigError
    #[tokio::test]
    async fn noop_without_credentials_returns_config_error() {
        let client = RestClient::new_public("https://fapi.asterdex.com").unwrap();
        let result = client.noop().await;
        assert!(
            matches!(result, Err(AsterDexError::ConfigError { .. })),
            "expected ConfigError, got: {:?}",
            result
        );
    }

    // US-014: get_all_orders mock — returns empty vec
    #[tokio::test]
    async fn get_all_orders_mock() {
        let mut server = Server::new_async().await;
        let mock = server
            .mock("GET", mockito::Matcher::Regex(r"^/fapi/v3/allOrders".to_string()))
            .with_status(200)
            .with_header("content-type", "application/json")
            .with_body("[]")
            .create_async()
            .await;

        // Use a client with credentials to bypass ConfigError — credentials do not
        // need to be real because the mock server returns a canned response.
        use crate::auth::Credentials;
        let creds = Credentials::new(
            "0x0000000000000000000000000000000000000001",
            "0x0000000000000000000000000000000000000001",
            "0x0000000000000000000000000000000000000000000000000000000000000001",
            714,
        )
        .unwrap();
        let client = RestClient::new(&server.url(), creds).unwrap();

        let params = AllOrdersParams {
            symbol: "BTCUSDT".to_string(),
            ..Default::default()
        };
        let resp = client.get_all_orders(params).await.unwrap();
        assert!(resp.data.is_empty(), "expected empty vec, got {:?}", resp.data);

        mock.assert_async().await;
    }
}