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