asterdex-sdk 0.1.2

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::futures::models::trading::EmptyResponse;
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
    /// (if supported by the API).
    ///
    /// # Errors
    /// - `ConfigError` — credentials not set
    /// - `ApiError { code: -1121 }` — invalid or unknown symbol
    /// - `RateLimited` — HTTP 429
    /// - `IpBanned` — HTTP 418
    pub async fn get_mmp(
        &self,
        symbol: Option<&str>,
    ) -> Result<ApiResponse<MmpResponse>, AsterDexError> {
        let mut params = vec![];
        if let Some(s) = symbol {
            params.push(("symbol", s));
        }
        self.signed_get("/fapi/v3/mmp", &params).await
    }

    /// Update MMP configuration for a symbol.
    ///
    /// # Errors
    /// - `ConfigError` — credentials not set
    /// - `ApiError` — invalid parameters or symbol
    /// - `RateLimited` — HTTP 429
    /// - `IpBanned` — HTTP 418
    pub async fn update_mmp(
        &self,
        params: MmpUpdateParams,
    ) -> Result<ApiResponse<MmpResponse>, AsterDexError> {
        let time_window_str = params.time_window_ms.to_string();
        let frozen_time_str = params.frozen_time_ms.to_string();
        self.signed_post(
            "/fapi/v3/mmp/update",
            &[
                ("symbol", params.symbol.as_str()),
                ("quantityLimit", params.quantity_limit.as_str()),
                ("deltaLimit", params.delta_limit.as_str()),
                ("timeWindowMs", &time_window_str),
                ("frozenTimeMs", &frozen_time_str),
            ],
        )
        .await
    }

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

    /// Reset (unfreeze) a frozen MMP for a symbol.
    ///
    /// # Errors
    /// - `ConfigError` — credentials not set
    /// - `ApiError` — no frozen MMP for the symbol
    /// - `RateLimited` — HTTP 429
    /// - `IpBanned` — HTTP 418
    pub async fn reset_mmp(
        &self,
        symbol: &str,
    ) -> Result<ApiResponse<EmptyResponse>, AsterDexError> {
        self.signed_post("/fapi/v3/mmp/reset", &[("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;
        // No mock needed — error is returned before any HTTP call
        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 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","quantityLimit":"100","deltaLimit":"50","timeWindowMs":5000,"frozenTimeMs":3000}"#)
            .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.symbol, "BTCUSDT");
        assert_eq!(resp.data.quantity_limit, "100");
    }

    // 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(),
            quantity_limit: "100".to_string(),
            delta_limit: "50".to_string(),
            time_window_ms: 5000,
            frozen_time_ms: 3000,
        };
        let result = client.update_mmp(params).await;
        assert!(
            matches!(result, Err(AsterDexError::ConfigError { .. })),
            "expected ConfigError, got: {:?}",
            result
        );
        drop(server);
    }
}