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