mexc-rs 0.3.0

Rust client for the mexc.com exchange
Documentation
use hmac::digest::{InvalidLength};
use hmac::{Hmac, Mac};
use sha2::Sha256;

pub mod v3;
#[cfg(feature = "ws")]
pub mod ws;

pub enum MexcSpotApiEndpoint {
    Base,
    Custom(String),
}

impl AsRef<str> for MexcSpotApiEndpoint {
    fn as_ref(&self) -> &str {
        match self {
            MexcSpotApiEndpoint::Base => "https://api.mexc.com",
            MexcSpotApiEndpoint::Custom(endpoint) => endpoint,
        }
    }
}

pub struct MexcSpotApiClient {
    endpoint: MexcSpotApiEndpoint,
    reqwest_client: reqwest::Client,
}

impl MexcSpotApiClient {
    pub fn new(endpoint: MexcSpotApiEndpoint) -> Self {
        let reqwest_client = reqwest::Client::builder()
            .build()
            .expect("Failed to build reqwest client");
        Self {
            endpoint,
            reqwest_client,
        }
    }

    pub fn into_with_authentication(
        self,
        api_key: String,
        secret_key: String,
    ) -> MexcSpotApiClientWithAuthentication {
        MexcSpotApiClientWithAuthentication::new(self.endpoint, api_key, secret_key)
    }
}

impl Default for MexcSpotApiClient {
    fn default() -> Self {
        Self::new(MexcSpotApiEndpoint::Base)
    }
}

pub struct MexcSpotApiClientWithAuthentication {
    endpoint: MexcSpotApiEndpoint,
    reqwest_client: reqwest::Client,
    _api_key: String,
    secret_key: String,
}

impl MexcSpotApiClientWithAuthentication {
    pub fn new(endpoint: MexcSpotApiEndpoint, api_key: String, secret_key: String) -> Self {
        let mut headers = reqwest::header::HeaderMap::new();
        headers.insert(
            "X-MEXC-APIKEY",
            api_key.parse().expect("Failed to parse api key"),
        );
        let reqwest_client = reqwest::Client::builder()
            .default_headers(headers)
            .build()
            .expect("Failed to build reqwest client");
        Self {
            endpoint,
            reqwest_client,
            _api_key: api_key,
            secret_key,
        }
    }

    fn sign_query<T>(&self, query: T) -> Result<QueryWithSignature<T>, SignQueryError> where T: serde::Serialize {
        let query_string = serde_urlencoded::to_string(&query)?;
        let mut mac = Hmac::<Sha256>::new_from_slice(self.secret_key.as_bytes())?;
        mac.update(query_string.as_bytes());
        let mac_result = mac.finalize();
        let mac_bytes = mac_result.into_bytes();
        let signature = hex::encode(mac_bytes);

        Ok(QueryWithSignature::new(query, signature))
    }

    #[cfg(test)]
    fn new_for_test() -> Self {
        dotenv::dotenv().ok();
        let api_key = std::env::var("MEXC_API_KEY").expect("MEXC_API_KEY not set");
        let secret_key = std::env::var("MEXC_SECRET_KEY").expect("MEXC_SECRET_KEY not set");
        Self::new(MexcSpotApiEndpoint::Base, api_key, secret_key)
    }
}

#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryWithSignature<T> {
    #[serde(flatten)]
    pub query: T,
    pub signature: String,
}

impl<T> QueryWithSignature<T> {
    pub fn new(query: T, signature: String) -> Self {
        Self { query, signature }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum SignQueryError {
    #[error("Serde url encoded error: {0}")]
    SerdeUrlencodedError(#[from] serde_urlencoded::ser::Error),

    #[error("Secret key invalid length")]
    SecretKeyInvalidLength(#[from] InvalidLength),
}