asterdex-sdk 0.1.5

AsterDex Futures SDK v3 — Rust async client for REST and WebSocket APIs
Documentation
// US-015: MMP management endpoints

use crate::futures::models::account::{MmpResponse, MmpUpdateParams};
use crate::rest::client::RestClient;
use crate::rest::error::AsterDexError;
use crate::rest::response::ApiResponse;

impl RestClient {
    /// Get MMP (Market Maker Protection) configuration.
    ///
    /// Pass `Some("BTCUSDT")` to query a specific symbol, or `None` to query all symbols.
    ///
    /// # Errors
    /// - `ConfigError` — credentials not set
    /// - `ApiError { code: -1121 }` — invalid or unknown symbol
    /// - `RateLimited` — HTTP 429
    pub async fn get_mmp(
        &self,
        symbol: Option<&str>,
    ) -> Result<ApiResponse<Vec<MmpResponse>>, AsterDexError> {
        let mut params = vec![];
        if let Some(s) = symbol {
            params.push(("symbol", s));
        }
        self.signed_get("/fapi/v3/mmp", &params).await
    }

    /// Create or update MMP configuration for a symbol (`POST /fapi/v3/mmp`).
    ///
    /// # Errors
    /// - `ConfigError` — credentials not set
    /// - `ApiError` — invalid parameters or symbol
    /// - `RateLimited` — HTTP 429
    pub async fn update_mmp(
        &self,
        params: MmpUpdateParams,
    ) -> Result<ApiResponse<bool>, AsterDexError> {
        let window_str = params.window_time_ms.to_string();
        let frozen_str = params.frozen_time_ms.to_string();
        let mut pairs: Vec<(&str, &str)> = vec![
            ("symbol", params.symbol.as_str()),
            ("windowTimeInMilliseconds", &window_str),
            ("frozenTimeInMilliseconds", &frozen_str),
        ];
        // Temporary storage for optional param strings (must outlive pairs).
        let qty_str;
        let value_str;
        let delta_str;
        if let Some(q) = params.qty_limit {
            qty_str = q.to_string();
            pairs.push(("qtyLimit", &qty_str));
        }
        if let Some(v) = params.value_limit {
            value_str = v.to_string();
            pairs.push(("valueLimit", &value_str));
        }
        if let Some(d) = params.delta_limit {
            delta_str = d.to_string();
            pairs.push(("deltaLimit", &delta_str));
        }
        self.signed_post("/fapi/v3/mmp", &pairs).await
    }

    /// Delete MMP configuration for a symbol (`DELETE /fapi/v3/mmp`).
    ///
    /// # Errors
    /// - `ConfigError` — credentials not set
    /// - `ApiError` — no MMP configured for the symbol
    /// - `RateLimited` — HTTP 429
    pub async fn delete_mmp(
        &self,
        symbol: &str,
    ) -> Result<ApiResponse<bool>, AsterDexError> {
        self.signed_delete("/fapi/v3/mmp", &[("symbol", symbol)])
            .await
    }

    /// Reset (unfreeze) a frozen MMP for a symbol (`POST /fapi/v3/mmpReset`).
    ///
    /// # Errors
    /// - `ConfigError` — credentials not set
    /// - `ApiError` — no frozen MMP for the symbol
    /// - `RateLimited` — HTTP 429
    pub async fn reset_mmp(
        &self,
        symbol: &str,
    ) -> Result<ApiResponse<bool>, AsterDexError> {
        self.signed_post("/fapi/v3/mmpReset", &[("symbol", symbol)])
            .await
    }
}

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

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

    // US-015: get_mmp with mock credentials returns Vec<MmpResponse>
    #[tokio::test]
    async fn get_mmp_mock() {
        let mut server = mockito::Server::new_async().await;
        let _mock = server
            .mock("GET", "/fapi/v3/mmp")
            .match_query(mockito::Matcher::Any)
            .with_status(200)
            .with_header("content-type", "application/json")
            .with_body(r#"[{"symbol":"BTCUSDT","windowTimeInMilliseconds":5000,"frozenTimeInMilliseconds":10000,"qtyLimit":10,"valueLimit":200000000,"deltaLimit":100000000}]"#)
            .create_async()
            .await;
        let creds = crate::auth::Credentials::new(
            "0x0000000000000000000000000000000000000001",
            "0x0000000000000000000000000000000000000002",
            "0000000000000000000000000000000000000000000000000000000000000001",
            714,
        ).unwrap();
        let client = RestClient::new(&server.url(), creds).unwrap();
        let resp = client.get_mmp(Some("BTCUSDT")).await.unwrap();
        assert_eq!(resp.data[0].symbol, "BTCUSDT");
        assert_eq!(resp.data[0].window_time_ms, 5000);
        assert_eq!(resp.data[0].qty_limit, Some(10));
    }

    // US-015: update_mmp without credentials returns ConfigError
    #[tokio::test]
    async fn update_mmp_without_credentials_returns_config_error() {
        let server = mockito::Server::new_async().await;
        let client = RestClient::new_public(&server.url()).unwrap();
        let params = MmpUpdateParams {
            symbol: "BTCUSDT".to_string(),
            window_time_ms: 5000,
            frozen_time_ms: 3000,
            qty_limit: Some(100),
            value_limit: None,
            delta_limit: Some(50),
        };
        let result = client.update_mmp(params).await;
        assert!(
            matches!(result, Err(AsterDexError::ConfigError { .. })),
            "expected ConfigError, got: {:?}",
            result
        );
        drop(server);
    }
}