1use async_trait::async_trait;
2use hmac::digest::InvalidLength;
3use hmac::{Hmac, Mac};
4use sha2::Sha256;
5
6pub mod v3;
7#[cfg(feature = "ws")]
8pub mod ws;
9
10#[derive(Debug, Clone)]
11pub enum MexcSpotApiEndpoint {
12 Base,
13 Custom(String),
14}
15
16impl AsRef<str> for MexcSpotApiEndpoint {
17 fn as_ref(&self) -> &str {
18 match self {
19 MexcSpotApiEndpoint::Base => "https://api.mexc.com",
20 MexcSpotApiEndpoint::Custom(endpoint) => endpoint,
21 }
22 }
23}
24
25#[derive(Clone)]
26pub struct MexcSpotApiClient {
27 endpoint: MexcSpotApiEndpoint,
28 reqwest_client: reqwest::Client,
29}
30
31#[async_trait]
32trait MexcSpotApiTrait {
33 fn endpoint(&self) -> &MexcSpotApiEndpoint;
34 fn reqwest_client(&self) -> &reqwest::Client;
35}
36
37impl MexcSpotApiClient {
38 pub fn new(endpoint: MexcSpotApiEndpoint) -> Self {
39 let reqwest_client = reqwest::Client::builder()
40 .build()
41 .expect("Failed to build reqwest client");
42 Self {
43 endpoint,
44 reqwest_client,
45 }
46 }
47
48 pub fn into_with_authentication(
49 self,
50 api_key: String,
51 secret_key: String,
52 ) -> MexcSpotApiClientWithAuthentication {
53 MexcSpotApiClientWithAuthentication::new(self.endpoint, api_key, secret_key)
54 }
55}
56
57impl Default for MexcSpotApiClient {
58 fn default() -> Self {
59 Self::new(MexcSpotApiEndpoint::Base)
60 }
61}
62
63impl MexcSpotApiTrait for MexcSpotApiClient {
64 fn endpoint(&self) -> &MexcSpotApiEndpoint {
65 &self.endpoint
66 }
67
68 fn reqwest_client(&self) -> &reqwest::Client {
69 &self.reqwest_client
70 }
71}
72
73#[derive(Clone)]
74pub struct MexcSpotApiClientWithAuthentication {
75 endpoint: MexcSpotApiEndpoint,
76 reqwest_client: reqwest::Client,
77 _api_key: String,
78 secret_key: String,
79}
80
81impl MexcSpotApiClientWithAuthentication {
82 pub fn new(endpoint: MexcSpotApiEndpoint, api_key: String, secret_key: String) -> Self {
83 let mut headers = reqwest::header::HeaderMap::new();
84 headers.insert(
85 "X-MEXC-APIKEY",
86 api_key.parse().expect("Failed to parse api key"),
87 );
88 let reqwest_client = reqwest::Client::builder()
89 .default_headers(headers)
90 .build()
91 .expect("Failed to build reqwest client");
92 Self {
93 endpoint,
94 reqwest_client,
95 _api_key: api_key,
96 secret_key,
97 }
98 }
99
100 fn sign_query<T>(&self, query: T) -> Result<QueryWithSignature<T>, SignQueryError>
101 where
102 T: serde::Serialize,
103 {
104 let query_string = serde_urlencoded::to_string(&query)?;
105 let mut mac = Hmac::<Sha256>::new_from_slice(self.secret_key.as_bytes())?;
106 mac.update(query_string.as_bytes());
107 let mac_result = mac.finalize();
108 let mac_bytes = mac_result.into_bytes();
109 let signature = hex::encode(mac_bytes);
110
111 Ok(QueryWithSignature::new(query, signature))
112 }
113
114 #[cfg(test)]
115 fn new_for_test() -> Self {
116 dotenv::dotenv().ok();
117 let api_key = std::env::var("MEXC_API_KEY").expect("MEXC_API_KEY not set");
118 let secret_key = std::env::var("MEXC_SECRET_KEY").expect("MEXC_SECRET_KEY not set");
119 Self::new(MexcSpotApiEndpoint::Base, api_key, secret_key)
120 }
121}
122
123impl MexcSpotApiTrait for MexcSpotApiClientWithAuthentication {
124 fn endpoint(&self) -> &MexcSpotApiEndpoint {
125 &self.endpoint
126 }
127
128 fn reqwest_client(&self) -> &reqwest::Client {
129 &self.reqwest_client
130 }
131}
132
133#[derive(Debug, serde::Serialize)]
134#[serde(rename_all = "camelCase")]
135pub struct QueryWithSignature<T> {
136 #[serde(flatten)]
137 pub query: T,
138 pub signature: String,
139}
140
141impl<T> QueryWithSignature<T> {
142 pub fn new(query: T, signature: String) -> Self {
143 Self { query, signature }
144 }
145}
146
147#[derive(Debug, thiserror::Error)]
148pub enum SignQueryError {
149 #[error("Serde url encoded error: {0}")]
150 SerdeUrlencodedError(#[from] serde_urlencoded::ser::Error),
151
152 #[error("Secret key invalid length")]
153 SecretKeyInvalidLength(#[from] InvalidLength),
154}