v_exchanges_adapters/exchanges/
mexc.rs1use std::{marker::PhantomData, str::FromStr, time::SystemTime};
4
5use generics::{AuthError, UrlError};
6use hmac::{Hmac, Mac};
7use jiff::{SignedDuration, Timestamp};
8use secrecy::{ExposeSecret as _, SecretString};
9use serde::{Deserialize, Serialize, de::DeserializeOwned};
10use sha2::Sha256;
11use url::Url;
12use v_exchanges_api_generics::{http::*, ws::*};
13use v_utils::prelude::*;
14
15use crate::traits::*;
16
17static MAX_RECV_WINDOW: std::time::Duration = std::time::Duration::from_millis(60000); #[derive(Debug, Default)]
21pub enum MexcOption {
22 #[default]
24 Default,
25 Pubkey(String),
27 Secret(SecretString),
29 Testnet(bool),
31 HttpUrl(MexcHttpUrl),
33 HttpAuth(MexcAuth),
35 RecvWindow(std::time::Duration),
37 WsUrl(MexcWsUrl),
39 WsConfig(WsConfig),
41 WsTopics(Vec<String>),
43}
44
45#[derive(Clone, derive_more::Debug, Default)]
47pub struct MexcOptions {
48 pub pubkey: Option<String>,
50 #[debug("[REDACTED]")]
52 pub secret: Option<SecretString>,
53 pub testnet: bool,
55 pub http_url: MexcHttpUrl,
57 pub http_auth: MexcAuth,
59 pub recv_window: Option<std::time::Duration>,
61 pub ws_url: MexcWsUrl,
63 pub ws_config: WsConfig,
65 pub ws_topics: HashSet<String>,
67}
68
69#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
71#[non_exhaustive]
72pub enum MexcHttpUrl {
73 Spot,
74 Futures,
75 #[default]
76 None,
77}
78impl EndpointUrl for MexcHttpUrl {
79 fn url_mainnet(&self) -> Url {
80 match self {
81 Self::Spot => Url::parse("https://api.mexc.com").unwrap(),
82 Self::Futures => Url::parse("https://contract.mexc.com").unwrap(),
83 Self::None => Url::parse("").unwrap(),
84 }
85 }
86
87 fn url_testnet(&self) -> Option<Url> {
88 match self {
89 Self::Spot => Some(Url::parse("https://api-testnet.mexc.com").unwrap()),
90 Self::Futures => Some(Url::parse("https://contract-testnet.mexc.com").unwrap()),
91 Self::None => Some(Url::parse("").unwrap()),
92 }
93 }
94}
95
96#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
97pub enum MexcAuth {
98 Sign,
99 Key,
100 #[default]
101 None,
102}
103
104#[derive(Debug, Deserialize, Serialize)]
105pub struct MexcError {
106 pub code: i32,
107 pub msg: String,
108}
109impl From<MexcError> for ApiError {
110 fn from(e: MexcError) -> Self {
111 ApiError::Other(eyre!("MEXC API error: {}: {}", e.code, e.msg))
112 }
113}
114
115pub struct MexcRequestHandler<'a, R: DeserializeOwned> {
117 options: MexcOptions,
118 _phantom: PhantomData<&'a R>,
119}
120
121impl<B, R> RequestHandler<B> for MexcRequestHandler<'_, R>
122where
123 B: Serialize,
124 R: DeserializeOwned,
125{
126 type Successful = R;
127
128 fn base_url(&self, is_test: bool) -> Result<Url, UrlError> {
129 match is_test {
130 true => self.options.http_url.url_testnet().ok_or_else(|| UrlError::MissingTestnet(self.options.http_url.url_mainnet())),
131 false => Ok(self.options.http_url.url_mainnet()),
132 }
133 }
134
135 #[tracing::instrument(skip_all, fields(?builder))]
136 fn build_request(&self, mut builder: RequestBuilder, request_body: &Option<B>, _: u8) -> Result<Request, BuildError> {
137 if let Some(body) = request_body {
138 let encoded = serde_urlencoded::to_string(body)?;
139 builder = builder.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded").body(encoded);
140 }
142
143 if self.options.http_auth != MexcAuth::None {
144 let pubkey = self.options.pubkey.as_deref().ok_or(AuthError::MissingPubkey)?;
145 builder = builder.header("ApiKey", pubkey);
146
147 let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
148 let timestamp = time.as_millis();
149 builder = builder.header("Request-Time", timestamp.to_string());
150
151 if let Some(recv_window) = self.options.recv_window {
152 builder = builder.header("Recv-Window", (recv_window.as_millis() as u64).to_string());
153 }
154
155 if self.options.http_auth == MexcAuth::Sign {
156 let secret = self.options.secret.as_ref().map(|s| s.expose_secret()).ok_or(AuthError::MissingSecret)?;
157 let mut hmac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
158
159 let mut request = builder.build().expect("My understanding is that this doesn't fail on client, so fail fast for dev");
160 let param_string = if request.method() == Method::GET || request.method() == Method::DELETE {
161 if let Some(body) = request_body { serde_urlencoded::to_string(body)? } else { String::new() }
162 } else {
163 String::from_utf8(request.body().and_then(|body| body.as_bytes()).unwrap_or_default().to_vec()).unwrap_or_default()
165 };
166
167 let signature_base = format!("{pubkey}{timestamp}{param_string}");
168 hmac.update(signature_base.as_bytes());
169 let signature = hex::encode(hmac.finalize().into_bytes());
170 request.headers_mut().insert("Signature", signature.parse().unwrap());
171
172 return Ok(request);
173 }
174 }
175 Ok(builder.build().expect("Don't expect this to be reached by client. Same reasoning - fail fast for dev"))
176 }
177
178 fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes) -> Result<Self::Successful, HandleError> {
179 if status.is_success() {
180 serde_json::from_slice(&response_body).map_err(|error| {
181 let response_str = v_utils::utils::truncate_msg(String::from_utf8_lossy(&response_body));
182 HandleError::Parse(eyre!("Failed to parse response: {error}\nResponse body: {response_str}"))
183 })
184 } else {
185 if status == 429 {
187 let retry_after_sec = if let Some(value) = headers.get("Retry-After") {
188 if let Ok(string) = value.to_str() {
189 if let Ok(retry_after) = u32::from_str(string) {
190 Some(retry_after)
191 } else {
192 tracing::debug!("Invalid number in Retry-After header");
193 None
194 }
195 } else {
196 tracing::debug!("Non-ASCII character in Retry-After header");
197 None
198 }
199 } else {
200 None
201 };
202 let e = match retry_after_sec {
203 Some(s) => {
204 let until = Some(Timestamp::now() + SignedDuration::from_secs(s as i64));
205 ApiError::IpTimeout { until }.into()
206 }
207 None => eyre!("Could't interpret Retry-After header").into(),
208 };
209 return Err(e);
210 }
211
212 let api_error: MexcError = match serde_json::from_slice(&response_body) {
213 Ok(parsed) => parsed,
214 Err(error) => {
215 let response_str = v_utils::utils::truncate_msg(String::from_utf8_lossy(&response_body));
216 return Err(HandleError::Parse(eyre!("Failed to parse error response: {error}\nResponse body: {response_str}")));
217 }
218 };
219 Err(ApiError::from(api_error).into())
220 }
221 }
222}
223
224#[derive(Debug, derive_new::new)]
227pub struct MexcWsHandler {
228 options: MexcOptions,
229}
230impl WsHandler for MexcWsHandler {
231 fn config(&self) -> Result<WsConfig, UrlError> {
232 let mut config = self.options.ws_config.clone();
233 if self.options.ws_url != MexcWsUrl::None {
234 config.base_url = match self.options.testnet {
235 true => Some(self.options.ws_url.url_testnet().ok_or_else(|| UrlError::MissingTestnet(self.options.ws_url.url_mainnet()))?),
236 false => Some(self.options.ws_url.url_mainnet()),
237 }
238 }
239 config.topics = config.topics.union(&self.options.ws_topics).cloned().collect();
240 Ok(config)
241 }
242
243 fn handle_jrpc(&mut self, _jrpc: serde_json::Value) -> Result<ResponseOrContent, WsError> {
244 todo!();
245 }
246
247 fn handle_subscribe(&mut self, _topics: HashSet<Topic>) -> Result<Vec<generics::tokio_tungstenite::tungstenite::Message>, WsError> {
248 todo!()
249 }
250}
251#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
253#[non_exhaustive]
254pub enum MexcWsUrl {
255 Spot,
256 Futures,
257 #[default]
258 None,
259}
260impl EndpointUrl for MexcWsUrl {
261 fn url_mainnet(&self) -> Url {
262 match self {
263 Self::Spot => Url::parse("wss://stream.mexc.com/ws").unwrap(),
264 Self::Futures => Url::parse("wss://contract.mexc.com/ws").unwrap(),
265 Self::None => Url::parse("").unwrap(),
266 }
267 }
268
269 fn url_testnet(&self) -> Option<Url> {
270 match self {
271 Self::Spot => Some(Url::parse("wss://stream-testnet.mexc.com/ws").unwrap()),
272 Self::Futures => Some(Url::parse("wss://contract-testnet.mexc.com/ws").unwrap()),
273 Self::None => None,
274 }
275 }
276}
277impl WsOption for MexcOption {
278 type WsHandler = MexcWsHandler;
279
280 fn ws_handler(options: Self::Options) -> Self::WsHandler {
281 MexcWsHandler::new(options)
282 }
283}
284impl HandlerOptions for MexcOptions {
287 type OptionItem = MexcOption;
288
289 fn update(&mut self, option: Self::OptionItem) {
290 match option {
291 MexcOption::Default => (),
292 MexcOption::Pubkey(v) => self.pubkey = Some(v),
293 MexcOption::Secret(v) => self.secret = Some(v),
294 MexcOption::Testnet(v) => self.testnet = v,
295 MexcOption::HttpUrl(v) => self.http_url = v,
296 MexcOption::HttpAuth(v) => self.http_auth = v,
297 MexcOption::RecvWindow(v) =>
298 if v > MAX_RECV_WINDOW {
299 tracing::warn!("recvWindow is too large, overwriting with maximum value of {MAX_RECV_WINDOW:?}");
300 self.recv_window = Some(MAX_RECV_WINDOW);
301 } else {
302 self.recv_window = Some(v);
303 },
304 MexcOption::WsUrl(v) => self.ws_url = v,
305 MexcOption::WsConfig(v) => self.ws_config = v,
306 MexcOption::WsTopics(v) => self.ws_topics = v.into_iter().collect(),
307 }
308 }
309
310 fn is_authenticated(&self) -> bool {
311 self.pubkey.is_some() }
313}
314
315impl<'a, R, B> HttpOption<'a, R, B> for MexcOption
316where
317 R: DeserializeOwned + 'a,
318 B: Serialize,
319{
320 type RequestHandler = MexcRequestHandler<'a, R>;
321
322 fn request_handler(options: Self::Options) -> Self::RequestHandler {
323 MexcRequestHandler::<'a, R> { options, _phantom: PhantomData }
324 }
325}
326
327impl HandlerOption for MexcOption {
328 type Options = MexcOptions;
329}