1use crate::prelude::*;
2use log::trace;
3
4use base64::Engine;
5use chrono::Utc;
6use std::time::Duration;
7
8#[derive(Clone)]
18pub struct Client {
19 pub api_key: Option<String>,
21
22 pub secret_key: Option<String>,
25
26 pub host: String,
28
29 pub inner_client: ReqwestClient,
31}
32
33impl Client {
34 pub fn new(api_key: Option<String>, secret_key: Option<String>, host: String) -> Self {
36 let inner_client = ReqwestClient::builder()
37 .timeout(Duration::from_secs(30))
38 .build()
39 .expect("Failed to build reqwest client");
40
41 Client {
42 api_key,
43 secret_key,
44 host,
45 inner_client,
46 }
47 }
48
49 pub async fn get<T: DeserializeOwned + Send + 'static>(
52 &self,
53 url_path: &str,
54 request: Option<String>,
55 ) -> Result<T, LimitlessError> {
56 let mut url = format!("{}/{}", self.host, url_path);
57 if let Some(ref req) = request {
58 if !req.is_empty() {
59 url.push('?');
60 url.push_str(req);
61 }
62 }
63 trace!("GET {}", url);
64 let response = self.inner_client.get(&url).send().await?;
65 self.handler(response).await
66 }
67
68 pub async fn get_signed<T: DeserializeOwned + Send + 'static>(
71 &self,
72 url_path: &str,
73 request: Option<String>,
74 ) -> Result<T, LimitlessError> {
75 let query_string = request.unwrap_or_default();
76 let full_path = if query_string.is_empty() {
77 url_path.to_string()
78 } else {
79 format!("{}?{}", url_path, query_string)
80 };
81
82 let headers = self.build_signed_headers("GET", &full_path, "")?;
83 let url = format!("{}/{}", self.host, full_path);
84
85 trace!("GET (signed) {}", url);
86 let response = self.inner_client.get(&url).headers(headers).send().await?;
87 self.handler(response).await
88 }
89
90 pub async fn post_signed<T: DeserializeOwned + Send + 'static>(
93 &self,
94 url_path: &str,
95 raw_request_body: Option<String>,
96 ) -> Result<T, LimitlessError> {
97 let body = raw_request_body.unwrap_or_default();
98 let headers = self.build_signed_headers("POST", url_path, &body)?;
99 let url = format!("{}/{}", self.host, url_path);
100
101 trace!("POST (signed) {}", url);
102 let response = self
103 .inner_client
104 .post(&url)
105 .headers(headers)
106 .body(body)
107 .send()
108 .await?;
109 self.handler(response).await
110 }
111
112 pub async fn delete_signed<T: DeserializeOwned + Send + 'static>(
115 &self,
116 url_path: &str,
117 ) -> Result<T, LimitlessError> {
118 let headers = self.build_signed_headers("DELETE", url_path, "")?;
119 let url = format!("{}/{}", self.host, url_path);
120
121 trace!("DELETE (signed) {}", url);
122 let response = self
123 .inner_client
124 .delete(&url)
125 .headers(headers)
126 .send()
127 .await?;
128 self.handler(response).await
129 }
130
131 fn build_signed_headers(
138 &self,
139 method: &str,
140 path_and_query: &str,
141 body: &str,
142 ) -> Result<HeaderMap, LimitlessError> {
143 let mut headers = HeaderMap::new();
144 headers.insert(USER_AGENT, HeaderValue::from_static("rs_limitless"));
145
146 let secret = match &self.secret_key {
148 Some(s) if !s.is_empty() => s.clone(),
149 _ => {
150 if let Some(ref key) = self.api_key {
152 headers.insert(
153 HeaderName::from_static("x-api-key"),
154 HeaderValue::from_str(key)?,
155 );
156 }
157 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
158 return Ok(headers);
159 }
160 };
161
162 let timestamp = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
164 let message = format!("{}\n{}\n{}\n{}", timestamp, method, path_and_query, body);
165
166 let secret_bytes = base64::engine::general_purpose::STANDARD
168 .decode(&secret)
169 .map_err(|e| LimitlessError::Base(format!("Invalid base64 secret: {}", e)))?;
170
171 let mut mac = Hmac::<Sha256>::new_from_slice(&secret_bytes)
173 .map_err(|e| LimitlessError::Base(format!("HMAC init error: {}", e)))?;
174 mac.update(message.as_bytes());
175 let result = mac.finalize();
176 let code_bytes = result.into_bytes();
177
178 let signature = base64::engine::general_purpose::STANDARD.encode(&code_bytes);
180
181 if let Some(ref key) = self.api_key {
182 headers.insert(
183 HeaderName::from_static("lmts-api-key"),
184 HeaderValue::from_str(key)?,
185 );
186 }
187
188 headers.insert(
189 HeaderName::from_static("lmts-timestamp"),
190 HeaderValue::from_str(×tamp)?,
191 );
192 headers.insert(
193 HeaderName::from_static("lmts-signature"),
194 HeaderValue::from_str(&signature)?,
195 );
196 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
197
198 Ok(headers)
199 }
200
201 async fn handler<T: DeserializeOwned>(
204 &self,
205 response: ReqwestResponse,
206 ) -> Result<T, LimitlessError> {
207 let status = response.status();
208
209 if status.is_success() {
210 let body = response.text().await?;
211 if body.trim().is_empty() {
212 return Err(LimitlessError::Base("Empty response body".into()));
213 }
214 serde_json::from_str(&body).map_err(LimitlessError::from)
215 } else {
216 let status_code = status.as_u16();
217 match status_code {
218 401 => {
219 let body = response.text().await.unwrap_or_default();
220 if let Ok(content_error) = serde_json::from_str::<LimitlessContentError>(&body)
221 {
222 Err(LimitlessError::ApiError(content_error))
223 } else {
224 Err(LimitlessError::Unauthorized)
225 }
226 }
227 429 => Err(LimitlessError::RateLimited),
228 500 => Err(LimitlessError::InternalServerError),
229 503 => Err(LimitlessError::ServiceUnavailable),
230 _ => {
231 let body = response.text().await.unwrap_or_default();
232 if let Ok(content_error) = serde_json::from_str::<LimitlessContentError>(&body)
233 {
234 Err(LimitlessError::ApiError(content_error))
235 } else {
236 Err(LimitlessError::StatusCode(status_code))
237 }
238 }
239 }
240 }
241 }
242
243 pub async fn wss_connect(
258 &self,
259 _request: Option<String>,
260 _authenticated: bool,
261 _timeout_secs: Option<u64>,
262 ) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>, LimitlessError> {
263 let ws_url =
268 WsUrl::parse("wss://ws.limitless.exchange/socket.io/?EIO=4&transport=websocket")
269 .map_err(|e| LimitlessError::Base(format!("Invalid WS URL: {}", e)))?;
270
271 let (stream, _response) = connect_async(ws_url.to_string()).await?;
272 Ok(stream)
273 }
274}