use std::{collections::HashSet, marker::PhantomData, str::FromStr, time::SystemTime};
use ahash::AHashSet;
use eyre::eyre;
use generics::{ConstructAuthError, UrlError};
use hmac::{Hmac, KeyInit as _, Mac};
use jiff::{SignedDuration, Timestamp};
use secrecy::{ExposeSecret as _, SecretString};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use sha2::Sha256;
use url::Url;
use v_exchanges_api_generics::{http::*, ws::*};
use crate::traits::*;
#[derive(Debug, Default)]
pub enum MexcOption {
#[default]
Default,
Pubkey(String),
Secret(SecretString),
Testnet(bool),
HttpUrl(MexcHttpUrl),
HttpAuth(MexcAuth),
RecvWindow(std::time::Duration),
WsUrl(MexcWsUrl),
WsConfig(WsConfig),
WsTopics(Vec<String>),
}
#[derive(Clone, derive_more::Debug, Default)]
pub struct MexcOptions {
pub pubkey: Option<String>,
#[debug("[REDACTED]")]
pub secret: Option<SecretString>,
pub testnet: bool,
pub http_url: MexcHttpUrl,
pub http_auth: MexcAuth,
pub recv_window: Option<std::time::Duration>,
pub ws_url: MexcWsUrl,
pub ws_config: WsConfig,
pub ws_topics: AHashSet<String>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum MexcHttpUrl {
Spot,
Futures,
#[default]
None,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum MexcAuth {
Sign,
Key,
#[default]
None,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct MexcError {
pub code: MexcErrorCode,
#[serde(alias = "message")]
pub msg: String,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
#[serde(from = "i32", into = "i32")]
pub enum MexcErrorCode {
Unauthorized(i32),
InvalidApiKey(i32),
InvalidSignature(i32),
ApiKeyExpired(i32),
SignatureNotValid(i32),
TooManyRequests(i32),
BadSymbol(i32),
PermissionDenied(i32),
Other(i32),
}
impl MexcErrorCode {
fn as_i32(self) -> i32 {
match self {
Self::Unauthorized(c)
| Self::InvalidApiKey(c)
| Self::InvalidSignature(c)
| Self::ApiKeyExpired(c)
| Self::SignatureNotValid(c)
| Self::TooManyRequests(c)
| Self::BadSymbol(c)
| Self::PermissionDenied(c)
| Self::Other(c) => c,
}
}
}
pub struct MexcRequestHandler<'a, R: DeserializeOwned> {
options: MexcOptions,
_phantom: PhantomData<&'a R>,
}
#[derive(Debug, derive_new::new)]
pub struct MexcWsHandler {
options: MexcOptions,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum MexcWsUrl {
Spot,
Futures,
#[default]
None,
}
static MAX_RECV_WINDOW: std::time::Duration = std::time::Duration::from_millis(60000);
impl EndpointUrl for MexcHttpUrl {
fn url_mainnet(&self) -> Url {
match self {
Self::Spot => Url::parse("https://api.mexc.com").unwrap(),
Self::Futures => Url::parse("https://contract.mexc.com").unwrap(),
Self::None => Url::parse("").unwrap(),
}
}
fn url_testnet(&self) -> Option<Url> {
match self {
Self::Spot => Some(Url::parse("https://api-testnet.mexc.com").unwrap()),
Self::Futures => Some(Url::parse("https://contract-testnet.mexc.com").unwrap()),
Self::None => Some(Url::parse("").unwrap()),
}
}
}
#[derive(Deserialize)]
struct MexcEnvelope {
success: bool,
code: i32,
#[serde(default)]
message: String,
}
impl From<MexcError> for ApiError {
fn from(e: MexcError) -> Self {
use v_exchanges_api_generics::http::AuthError;
match e.code {
MexcErrorCode::ApiKeyExpired(_) => AuthError::KeyExpired { msg: e.msg }.into(),
MexcErrorCode::Unauthorized(_) | MexcErrorCode::InvalidApiKey(_) | MexcErrorCode::InvalidSignature(_) | MexcErrorCode::SignatureNotValid(_) =>
AuthError::Unauthorized { msg: e.msg }.into(),
_ => ApiError::Other(eyre!("MEXC API error {}: {}", e.code.as_i32(), e.msg)),
}
}
}
impl From<i32> for MexcErrorCode {
fn from(code: i32) -> Self {
match code {
602 => Self::Unauthorized(code),
10001 => Self::InvalidApiKey(code),
140002 => Self::InvalidSignature(code),
402 | 700001 => Self::ApiKeyExpired(code),
700002 => Self::SignatureNotValid(code),
700007 | 700013 => Self::InvalidSignature(code),
70011 => Self::PermissionDenied(code),
10007 => Self::BadSymbol(code),
code => Self::Other(code),
}
}
}
impl From<MexcErrorCode> for i32 {
fn from(code: MexcErrorCode) -> Self {
code.as_i32()
}
}
impl<B, R> RequestHandler<B> for MexcRequestHandler<'_, R>
where
B: Serialize,
R: DeserializeOwned,
{
type Successful = R;
fn base_url(&self, is_test: bool) -> Result<Url, UrlError> {
match is_test {
true => self.options.http_url.url_testnet().ok_or_else(|| UrlError::MissingTestnet(self.options.http_url.url_mainnet())),
false => Ok(self.options.http_url.url_mainnet()),
}
}
#[tracing::instrument(skip_all, fields(?builder))]
fn build_request(&self, mut builder: RequestBuilder, request_body: &Option<B>, _: u8) -> Result<Request, BuildError> {
if let Some(body) = request_body {
let encoded = serde_urlencoded::to_string(body)?;
builder = builder.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded").body(encoded);
}
if self.options.http_auth != MexcAuth::None {
let pubkey = self.options.pubkey.as_deref().ok_or(ConstructAuthError::new_missing_pubkey())?;
builder = builder.header("ApiKey", pubkey);
let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
let timestamp = time.as_millis();
builder = builder.header("Request-Time", timestamp.to_string());
if let Some(recv_window) = self.options.recv_window {
builder = builder.header("Recv-Window", (recv_window.as_millis() as u64).to_string());
}
if self.options.http_auth == MexcAuth::Sign {
let secret = self.options.secret.as_ref().map(|s| s.expose_secret()).ok_or(ConstructAuthError::new_missing_secret())?;
let mut hmac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
let mut request = builder.build().expect("My understanding is that this doesn't fail on client, so fail fast for dev");
let param_string = if request.method() == Method::GET || request.method() == Method::DELETE {
if let Some(body) = request_body { serde_urlencoded::to_string(body)? } else { String::new() }
} else {
String::from_utf8(request.body().and_then(|body| body.as_bytes()).unwrap_or_default().to_vec()).unwrap_or_default()
};
let signature_base = format!("{pubkey}{timestamp}{param_string}");
hmac.update(signature_base.as_bytes());
let signature = hex::encode(hmac.finalize().into_bytes());
request.headers_mut().insert("Signature", signature.parse().unwrap());
return Ok(request);
}
}
Ok(builder.build().expect("Don't expect this to be reached by client. Same reasoning - fail fast for dev"))
}
fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes) -> Result<Self::Successful, HandleError> {
if status.is_success() {
if let Ok(envelope) = serde_json::from_slice::<MexcEnvelope>(&response_body)
&& !envelope.success
{
let api_error = MexcError {
code: envelope.code.into(),
msg: envelope.message,
};
return Err(ApiError::from(api_error).into());
}
serde_json::from_slice(&response_body).map_err(|error| {
let response_str = v_utils::utils::truncate_msg(String::from_utf8_lossy(&response_body));
HandleError::Parse(eyre!("Failed to parse response: {error}\nResponse body: {response_str}"))
})
} else {
if status == 401 {
use v_exchanges_api_generics::http::AuthError;
let msg = match std::str::from_utf8(&response_body) {
Ok(s) if !s.is_empty() => s.to_string(),
_ => "HTTP 401 Unauthorized".to_string(),
};
return Err(ApiError::Auth(AuthError::Unauthorized { msg }).into());
}
if status == 429 {
let retry_after_sec = if let Some(value) = headers.get("Retry-After") {
if let Ok(string) = value.to_str() {
if let Ok(retry_after) = u32::from_str(string) {
Some(retry_after)
} else {
tracing::debug!("Invalid number in Retry-After header");
None
}
} else {
tracing::debug!("Non-ASCII character in Retry-After header");
None
}
} else {
None
};
let e = match retry_after_sec {
Some(s) => {
let until = Some(Timestamp::now() + SignedDuration::from_secs(s as i64));
ApiError::from(IpError::Timeout { until }).into()
}
None => eyre!("Could't interpret Retry-After header").into(),
};
return Err(e);
}
let api_error: MexcError = match serde_json::from_slice(&response_body) {
Ok(parsed) => parsed,
Err(error) => {
let response_str = v_utils::utils::truncate_msg(String::from_utf8_lossy(&response_body));
return Err(HandleError::Parse(eyre!("Failed to parse error response: {error}\nResponse body: {response_str}")));
}
};
Err(ApiError::from(api_error).into())
}
}
}
impl WsHandler for MexcWsHandler {
fn config(&self) -> Result<WsConfig, UrlError> {
let mut config = self.options.ws_config.clone();
if self.options.ws_url != MexcWsUrl::None {
config.base_url = match self.options.testnet {
true => Some(self.options.ws_url.url_testnet().ok_or_else(|| UrlError::MissingTestnet(self.options.ws_url.url_mainnet()))?),
false => Some(self.options.ws_url.url_mainnet()),
}
}
config.topics = config.topics.union(&self.options.ws_topics).cloned().collect();
Ok(config)
}
fn handle_jrpc(&mut self, _jrpc: serde_json::Value) -> Result<ResponseOrContent, WsError> {
todo!();
}
fn handle_subscribe(&mut self, _topics: AHashSet<Topic>) -> Result<Vec<generics::tokio_tungstenite::tungstenite::Message>, WsError> {
todo!()
}
}
impl EndpointUrl for MexcWsUrl {
fn url_mainnet(&self) -> Url {
match self {
Self::Spot => Url::parse("wss://stream.mexc.com/ws").unwrap(),
Self::Futures => Url::parse("wss://contract.mexc.com/ws").unwrap(),
Self::None => Url::parse("").unwrap(),
}
}
fn url_testnet(&self) -> Option<Url> {
match self {
Self::Spot => Some(Url::parse("wss://stream-testnet.mexc.com/ws").unwrap()),
Self::Futures => Some(Url::parse("wss://contract-testnet.mexc.com/ws").unwrap()),
Self::None => None,
}
}
}
impl WsOption for MexcOption {
type WsHandler = MexcWsHandler;
fn ws_handler(options: Self::Options) -> Self::WsHandler {
MexcWsHandler::new(options)
}
}
impl HandlerOptions for MexcOptions {
type OptionItem = MexcOption;
fn update(&mut self, option: Self::OptionItem) {
match option {
MexcOption::Default => (),
MexcOption::Pubkey(v) => self.pubkey = Some(v),
MexcOption::Secret(v) => self.secret = Some(v),
MexcOption::Testnet(v) => self.testnet = v,
MexcOption::HttpUrl(v) => self.http_url = v,
MexcOption::HttpAuth(v) => self.http_auth = v,
MexcOption::RecvWindow(v) =>
if v > MAX_RECV_WINDOW {
tracing::warn!("recvWindow is too large, overwriting with maximum value of {MAX_RECV_WINDOW:?}");
self.recv_window = Some(MAX_RECV_WINDOW);
} else {
self.recv_window = Some(v);
},
MexcOption::WsUrl(v) => self.ws_url = v,
MexcOption::WsConfig(v) => self.ws_config = v,
MexcOption::WsTopics(v) => self.ws_topics = v.into_iter().collect(),
}
}
fn is_authenticated(&self) -> bool {
self.pubkey.is_some() }
}
impl<'a, R, B> HttpOption<'a, R, B> for MexcOption
where
R: DeserializeOwned + 'a,
B: Serialize,
{
type RequestHandler = MexcRequestHandler<'a, R>;
fn request_handler(options: Self::Options) -> Self::RequestHandler {
MexcRequestHandler::<'a, R> { options, _phantom: PhantomData }
}
}
impl HandlerOption for MexcOption {
type Options = MexcOptions;
}