Skip to main content

asterdex_sdk/futures/endpoints/
mmp.rs

1// US-015: MMP management endpoints
2
3use crate::futures::models::account::{MmpResponse, MmpUpdateParams};
4use crate::futures::models::trading::EmptyResponse;
5use crate::rest::client::RestClient;
6use crate::rest::error::AsterDexError;
7use crate::rest::response::ApiResponse;
8
9impl RestClient {
10    /// Get MMP (Market Maker Protection) configuration.
11    ///
12    /// Pass `Some("BTCUSDT")` to query a specific symbol, or `None` to query all symbols
13    /// (if supported by the API).
14    ///
15    /// # Errors
16    /// - `ConfigError` — credentials not set
17    /// - `ApiError { code: -1121 }` — invalid or unknown symbol
18    /// - `RateLimited` — HTTP 429
19    /// - `IpBanned` — HTTP 418
20    pub async fn get_mmp(
21        &self,
22        symbol: Option<&str>,
23    ) -> Result<ApiResponse<MmpResponse>, AsterDexError> {
24        let mut params = vec![];
25        if let Some(s) = symbol {
26            params.push(("symbol", s));
27        }
28        self.signed_get("/fapi/v3/mmp", &params).await
29    }
30
31    /// Update MMP configuration for a symbol.
32    ///
33    /// # Errors
34    /// - `ConfigError` — credentials not set
35    /// - `ApiError` — invalid parameters or symbol
36    /// - `RateLimited` — HTTP 429
37    /// - `IpBanned` — HTTP 418
38    pub async fn update_mmp(
39        &self,
40        params: MmpUpdateParams,
41    ) -> Result<ApiResponse<MmpResponse>, AsterDexError> {
42        let time_window_str = params.time_window_ms.to_string();
43        let frozen_time_str = params.frozen_time_ms.to_string();
44        self.signed_post(
45            "/fapi/v3/mmp/update",
46            &[
47                ("symbol", params.symbol.as_str()),
48                ("quantityLimit", params.quantity_limit.as_str()),
49                ("deltaLimit", params.delta_limit.as_str()),
50                ("timeWindowMs", &time_window_str),
51                ("frozenTimeMs", &frozen_time_str),
52            ],
53        )
54        .await
55    }
56
57    /// Delete MMP configuration for a symbol.
58    ///
59    /// # Errors
60    /// - `ConfigError` — credentials not set
61    /// - `ApiError` — no MMP configured for the symbol
62    /// - `RateLimited` — HTTP 429
63    /// - `IpBanned` — HTTP 418
64    pub async fn delete_mmp(
65        &self,
66        symbol: &str,
67    ) -> Result<ApiResponse<EmptyResponse>, AsterDexError> {
68        self.signed_delete("/fapi/v3/mmp", &[("symbol", symbol)])
69            .await
70    }
71
72    /// Reset (unfreeze) a frozen MMP for a symbol.
73    ///
74    /// # Errors
75    /// - `ConfigError` — credentials not set
76    /// - `ApiError` — no frozen MMP for the symbol
77    /// - `RateLimited` — HTTP 429
78    /// - `IpBanned` — HTTP 418
79    pub async fn reset_mmp(
80        &self,
81        symbol: &str,
82    ) -> Result<ApiResponse<EmptyResponse>, AsterDexError> {
83        self.signed_post("/fapi/v3/mmp/reset", &[("symbol", symbol)])
84            .await
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    // US-015: get_mmp without credentials returns ConfigError
93    #[tokio::test]
94    async fn get_mmp_without_credentials_returns_config_error() {
95        let server = mockito::Server::new_async().await;
96        // No mock needed — error is returned before any HTTP call
97        let client = RestClient::new_public(&server.url()).unwrap();
98        let result = client.get_mmp(Some("BTCUSDT")).await;
99        assert!(
100            matches!(result, Err(AsterDexError::ConfigError { .. })),
101            "expected ConfigError, got: {:?}",
102            result
103        );
104        drop(server);
105    }
106
107    // US-015: get_mmp with mock credentials returns MmpResponse
108    #[tokio::test]
109    async fn get_mmp_mock() {
110        let mut server = mockito::Server::new_async().await;
111        let _mock = server
112            .mock("GET", "/fapi/v3/mmp")
113            .match_query(mockito::Matcher::Any)
114            .with_status(200)
115            .with_header("content-type", "application/json")
116            .with_body(r#"{"symbol":"BTCUSDT","quantityLimit":"100","deltaLimit":"50","timeWindowMs":5000,"frozenTimeMs":3000}"#)
117            .create_async()
118            .await;
119        let creds = crate::auth::Credentials::new(
120            "0x0000000000000000000000000000000000000001",
121            "0x0000000000000000000000000000000000000002",
122            "0000000000000000000000000000000000000000000000000000000000000001",
123            714,
124        ).unwrap();
125        let client = RestClient::new(&server.url(), creds).unwrap();
126        let resp = client.get_mmp(Some("BTCUSDT")).await.unwrap();
127        assert_eq!(resp.data.symbol, "BTCUSDT");
128        assert_eq!(resp.data.quantity_limit, "100");
129    }
130
131    // US-015: update_mmp without credentials returns ConfigError
132    #[tokio::test]
133    async fn update_mmp_without_credentials_returns_config_error() {
134        let server = mockito::Server::new_async().await;
135        let client = RestClient::new_public(&server.url()).unwrap();
136        let params = MmpUpdateParams {
137            symbol: "BTCUSDT".to_string(),
138            quantity_limit: "100".to_string(),
139            delta_limit: "50".to_string(),
140            time_window_ms: 5000,
141            frozen_time_ms: 3000,
142        };
143        let result = client.update_mmp(params).await;
144        assert!(
145            matches!(result, Err(AsterDexError::ConfigError { .. })),
146            "expected ConfigError, got: {:?}",
147            result
148        );
149        drop(server);
150    }
151}