Skip to main content

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