ccxt_exchanges/okx/rest/
mod.rs

1//! OKX REST API implementation.
2//!
3//! Implements all REST API endpoint operations for the OKX exchange.
4
5mod 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    /// Get the current timestamp in ISO 8601 format for OKX API.
18    ///
19    /// # Deprecated
20    ///
21    /// This method is deprecated. Use [`signed_request()`](Self::signed_request) instead.
22    /// The `signed_request()` builder handles timestamp generation internally.
23    #[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    /// Get the authentication instance if credentials are configured.
35    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    /// Check that required credentials are configured.
59    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    /// Build the API path for OKX V5 API.
68    pub(crate) fn build_api_path(endpoint: &str) -> String {
69        format!("/api/v5{}", endpoint)
70    }
71
72    /// Get the instrument type for API requests.
73    ///
74    /// Maps the configured `default_type` to OKX's instrument type (instType) parameter.
75    /// OKX uses a unified V5 API, so this primarily affects market filtering.
76    ///
77    /// # Returns
78    ///
79    /// Returns the OKX instrument type string:
80    /// - "SPOT" for spot trading
81    /// - "MARGIN" for margin trading
82    /// - "SWAP" for perpetual contracts
83    /// - "FUTURES" for delivery contracts
84    /// - "OPTION" for options trading
85    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    /// Make a public API request (no authentication required).
98    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    /// Make a private API request (authentication required).
158    ///
159    /// # Deprecated
160    ///
161    /// This method is deprecated. Use [`signed_request()`](Self::signed_request) instead.
162    /// The `signed_request()` builder provides a cleaner, more maintainable API for
163    /// constructing authenticated requests.
164    #[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(&timestamp, method, &sign_path, &body_string);
203
204        let mut headers = HeaderMap::new();
205        auth.add_auth_headers(&mut headers, &timestamp, &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}