ccxt_exchanges/okx/rest/
mod.rs1mod account;
6mod market_data;
7mod trading;
8
9use super::{Okx, OkxAuth};
10use ccxt_core::{Error, Result};
11use reqwest::header::{HeaderMap, HeaderValue};
12use serde_json::Value;
13use std::collections::HashMap;
14use tracing::debug;
15
16impl Okx {
17 #[deprecated(
24 since = "0.1.0",
25 note = "Use `signed_request()` builder instead which handles timestamps internally"
26 )]
27 #[allow(dead_code)]
28 fn get_timestamp() -> String {
29 chrono::Utc::now()
30 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
31 .to_string()
32 }
33
34 pub fn get_auth(&self) -> Result<OkxAuth> {
36 let config = &self.base().config;
37
38 let api_key = config
39 .api_key
40 .as_ref()
41 .ok_or_else(|| Error::authentication("API key is required"))?;
42 let secret = config
43 .secret
44 .as_ref()
45 .ok_or_else(|| Error::authentication("API secret is required"))?;
46 let passphrase = config
47 .password
48 .as_ref()
49 .ok_or_else(|| Error::authentication("Passphrase is required"))?;
50
51 Ok(OkxAuth::new(
52 api_key.expose_secret().to_string(),
53 secret.expose_secret().to_string(),
54 passphrase.expose_secret().to_string(),
55 ))
56 }
57
58 pub fn check_required_credentials(&self) -> Result<()> {
60 self.base().check_required_credentials()?;
61 if self.base().config.password.is_none() {
62 return Err(Error::authentication("Passphrase is required for OKX"));
63 }
64 Ok(())
65 }
66
67 pub(crate) fn build_api_path(endpoint: &str) -> String {
69 format!("/api/v5{}", endpoint)
70 }
71
72 pub fn get_inst_type(&self) -> &str {
86 use ccxt_core::types::default_type::DefaultType;
87
88 match self.options().default_type {
89 DefaultType::Spot => "SPOT",
90 DefaultType::Margin => "MARGIN",
91 DefaultType::Swap => "SWAP",
92 DefaultType::Futures => "FUTURES",
93 DefaultType::Option => "OPTION",
94 }
95 }
96
97 pub(crate) async fn public_request(
99 &self,
100 method: &str,
101 path: &str,
102 params: Option<&HashMap<String, String>>,
103 ) -> Result<Value> {
104 let urls = self.urls();
105 let mut url = format!("{}{}", urls.rest, path);
106
107 if let Some(p) = params {
108 if !p.is_empty() {
109 let query: Vec<String> = p
110 .iter()
111 .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
112 .collect();
113 url = format!("{}?{}", url, query.join("&"));
114 }
115 }
116
117 debug!("OKX public request: {} {}", method, url);
118
119 let mut headers = HeaderMap::new();
120 if self.is_testnet_trading() {
121 headers.insert("x-simulated-trading", HeaderValue::from_static("1"));
122 }
123
124 let response = match method.to_uppercase().as_str() {
125 "GET" => {
126 if headers.is_empty() {
127 self.base().http_client.get(&url, None).await?
128 } else {
129 self.base().http_client.get(&url, Some(headers)).await?
130 }
131 }
132 "POST" => {
133 if headers.is_empty() {
134 self.base().http_client.post(&url, None, None).await?
135 } else {
136 self.base()
137 .http_client
138 .post(&url, Some(headers), None)
139 .await?
140 }
141 }
142 _ => {
143 return Err(Error::invalid_request(format!(
144 "Unsupported HTTP method: {}",
145 method
146 )));
147 }
148 };
149
150 if super::error::is_error_response(&response) {
151 return Err(super::error::parse_error(&response));
152 }
153
154 Ok(response)
155 }
156
157 #[deprecated(
165 since = "0.1.0",
166 note = "Use `signed_request()` builder instead for cleaner, more maintainable code"
167 )]
168 #[allow(dead_code)]
169 #[allow(deprecated)]
170 async fn private_request(
171 &self,
172 method: &str,
173 path: &str,
174 params: Option<&HashMap<String, String>>,
175 body: Option<&Value>,
176 ) -> Result<Value> {
177 self.check_required_credentials()?;
178
179 let auth = self.get_auth()?;
180 let urls = self.urls();
181 let timestamp = Self::get_timestamp();
182
183 let query_string = if let Some(p) = params {
184 if p.is_empty() {
185 String::new()
186 } else {
187 let query: Vec<String> = p
188 .iter()
189 .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
190 .collect();
191 format!("?{}", query.join("&"))
192 }
193 } else {
194 String::new()
195 };
196
197 let body_string = body
198 .map(|b| serde_json::to_string(b).unwrap_or_default())
199 .unwrap_or_default();
200
201 let sign_path = format!("{}{}", path, query_string);
202 let signature = auth.sign(×tamp, method, &sign_path, &body_string);
203
204 let mut headers = HeaderMap::new();
205 auth.add_auth_headers(&mut headers, ×tamp, &signature);
206 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
207
208 if self.is_testnet_trading() {
209 headers.insert("x-simulated-trading", HeaderValue::from_static("1"));
210 }
211
212 let url = format!("{}{}{}", urls.rest, path, query_string);
213 debug!("OKX private request: {} {}", method, url);
214
215 let response = match method.to_uppercase().as_str() {
216 "GET" => self.base().http_client.get(&url, Some(headers)).await?,
217 "POST" => {
218 let body_value = body.cloned();
219 self.base()
220 .http_client
221 .post(&url, Some(headers), body_value)
222 .await?
223 }
224 "DELETE" => {
225 self.base()
226 .http_client
227 .delete(&url, Some(headers), None)
228 .await?
229 }
230 _ => {
231 return Err(Error::invalid_request(format!(
232 "Unsupported HTTP method: {}",
233 method
234 )));
235 }
236 };
237
238 if super::error::is_error_response(&response) {
239 return Err(super::error::parse_error(&response));
240 }
241
242 Ok(response)
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn test_build_api_path() {
252 let _okx = Okx::builder().build().unwrap();
253 let path = Okx::build_api_path("/public/instruments");
254 assert_eq!(path, "/api/v5/public/instruments");
255 }
256
257 #[test]
258 fn test_get_inst_type_spot() {
259 let okx = Okx::builder().build().unwrap();
260 let inst_type = okx.get_inst_type();
261 assert_eq!(inst_type, "SPOT");
262 }
263
264 #[test]
265 fn test_get_inst_type_margin() {
266 use ccxt_core::types::default_type::DefaultType;
267 let okx = Okx::builder()
268 .default_type(DefaultType::Margin)
269 .build()
270 .unwrap();
271 let inst_type = okx.get_inst_type();
272 assert_eq!(inst_type, "MARGIN");
273 }
274
275 #[test]
276 fn test_get_inst_type_swap() {
277 use ccxt_core::types::default_type::DefaultType;
278 let okx = Okx::builder()
279 .default_type(DefaultType::Swap)
280 .build()
281 .unwrap();
282 let inst_type = okx.get_inst_type();
283 assert_eq!(inst_type, "SWAP");
284 }
285
286 #[test]
287 fn test_get_inst_type_futures() {
288 use ccxt_core::types::default_type::DefaultType;
289 let okx = Okx::builder()
290 .default_type(DefaultType::Futures)
291 .build()
292 .unwrap();
293 let inst_type = okx.get_inst_type();
294 assert_eq!(inst_type, "FUTURES");
295 }
296
297 #[test]
298 fn test_get_inst_type_option() {
299 use ccxt_core::types::default_type::DefaultType;
300 let okx = Okx::builder()
301 .default_type(DefaultType::Option)
302 .build()
303 .unwrap();
304 let inst_type = okx.get_inst_type();
305 assert_eq!(inst_type, "OPTION");
306 }
307
308 #[test]
309 fn test_get_timestamp() {
310 let _okx = Okx::builder().build().unwrap();
311 let ts = Okx::get_timestamp();
312
313 assert!(ts.contains("T"));
314 assert!(ts.contains("Z"));
315 assert!(ts.len() > 20);
316 }
317}