Skip to main content

ccxt_exchanges/bitget/rest/
mod.rs

1//! Bitget REST API implementation.
2//!
3//! Implements all REST API endpoint operations for the Bitget exchange.
4
5mod account;
6pub mod futures;
7mod market_data;
8mod trading;
9
10use super::{Bitget, BitgetAuth, error};
11use ccxt_core::{Error, Result};
12use reqwest::header::{HeaderMap, HeaderValue};
13use serde_json::Value;
14use std::collections::HashMap;
15use tracing::debug;
16
17impl Bitget {
18    /// Get the current timestamp in milliseconds.
19    #[deprecated(
20        since = "0.1.0",
21        note = "Use `signed_request()` builder instead which handles timestamps internally"
22    )]
23    #[allow(dead_code)]
24    fn get_timestamp() -> String {
25        chrono::Utc::now().timestamp_millis().to_string()
26    }
27
28    /// Get the authentication instance if credentials are configured.
29    pub fn get_auth(&self) -> Result<BitgetAuth> {
30        let config = &self.base().config;
31
32        let api_key = config
33            .api_key
34            .as_ref()
35            .ok_or_else(|| Error::authentication("API key is required"))?;
36        let secret = config
37            .secret
38            .as_ref()
39            .ok_or_else(|| Error::authentication("API secret is required"))?;
40        let passphrase = config
41            .password
42            .as_ref()
43            .ok_or_else(|| Error::authentication("Passphrase is required"))?;
44
45        Ok(BitgetAuth::new(
46            api_key.expose_secret().to_string(),
47            secret.expose_secret().to_string(),
48            passphrase.expose_secret().to_string(),
49        ))
50    }
51
52    /// Check that required credentials are configured.
53    pub fn check_required_credentials(&self) -> Result<()> {
54        self.base().check_required_credentials()?;
55        if self.base().config.password.is_none() {
56            return Err(Error::authentication("Passphrase is required for Bitget"));
57        }
58        Ok(())
59    }
60
61    /// Build the API path with product type prefix.
62    fn build_api_path(&self, endpoint: &str) -> String {
63        let product_type = self.options().effective_product_type();
64        match product_type {
65            "umcbl" | "usdt-futures" | "dmcbl" | "coin-futures" => {
66                format!("/api/v2/mix{}", endpoint)
67            }
68            _ => format!("/api/v2/spot{}", endpoint),
69        }
70    }
71
72    /// Make a public API request (no authentication required).
73    async fn public_request(
74        &self,
75        method: &str,
76        path: &str,
77        params: Option<&HashMap<String, String>>,
78    ) -> Result<Value> {
79        let urls = self.urls();
80        let mut url = format!("{}{}", urls.rest, path);
81
82        if let Some(p) = params {
83            if !p.is_empty() {
84                let query: Vec<String> = p
85                    .iter()
86                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
87                    .collect();
88                url = format!("{}?{}", url, query.join("&"));
89            }
90        }
91
92        debug!("Bitget public request: {} {}", method, url);
93
94        let response = match method.to_uppercase().as_str() {
95            "GET" => self.base().http_client.get(&url, None).await?,
96            "POST" => self.base().http_client.post(&url, None, None).await?,
97            _ => {
98                return Err(Error::invalid_request(format!(
99                    "Unsupported HTTP method: {}",
100                    method
101                )));
102            }
103        };
104
105        if error::is_error_response(&response) {
106            return Err(error::parse_error(&response));
107        }
108
109        Ok(response)
110    }
111
112    /// Make a private API request (authentication required).
113    #[deprecated(
114        since = "0.1.0",
115        note = "Use `signed_request()` builder instead for cleaner, more maintainable code"
116    )]
117    #[allow(dead_code)]
118    #[allow(deprecated)]
119    async fn private_request(
120        &self,
121        method: &str,
122        path: &str,
123        params: Option<&HashMap<String, String>>,
124        body: Option<&Value>,
125    ) -> Result<Value> {
126        self.check_required_credentials()?;
127
128        let auth = self.get_auth()?;
129        let urls = self.urls();
130        let timestamp = Self::get_timestamp();
131
132        let query_string = if let Some(p) = params {
133            if p.is_empty() {
134                String::new()
135            } else {
136                let query: Vec<String> = p
137                    .iter()
138                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
139                    .collect();
140                format!("?{}", query.join("&"))
141            }
142        } else {
143            String::new()
144        };
145
146        let body_string = match body {
147            Some(b) => serde_json::to_string(b).map_err(|e| {
148                ccxt_core::Error::from(ccxt_core::ParseError::invalid_format(
149                    "request body",
150                    format!("JSON serialization failed: {}", e),
151                ))
152            })?,
153            None => String::new(),
154        };
155
156        let sign_path = format!("{}{}", path, query_string);
157        let signature = auth.sign(&timestamp, method, &sign_path, &body_string);
158
159        let mut headers = HeaderMap::new();
160        auth.add_auth_headers(&mut headers, &timestamp, &signature);
161        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
162
163        let url = format!("{}{}{}", urls.rest, path, query_string);
164        debug!("Bitget private request: {} {}", method, url);
165
166        let response = match method.to_uppercase().as_str() {
167            "GET" => self.base().http_client.get(&url, Some(headers)).await?,
168            "POST" => {
169                let body_value = body.cloned();
170                self.base()
171                    .http_client
172                    .post(&url, Some(headers), body_value)
173                    .await?
174            }
175            "DELETE" => {
176                self.base()
177                    .http_client
178                    .delete(&url, Some(headers), None)
179                    .await?
180            }
181            _ => {
182                return Err(Error::invalid_request(format!(
183                    "Unsupported HTTP method: {}",
184                    method
185                )));
186            }
187        };
188
189        if error::is_error_response(&response) {
190            return Err(error::parse_error(&response));
191        }
192
193        Ok(response)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    #![allow(clippy::disallowed_methods)]
200    use super::*;
201    use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
202
203    #[test]
204    fn test_build_api_path_spot() {
205        let bitget = Bitget::builder().build().unwrap();
206        let path = bitget.build_api_path("/public/symbols");
207        assert_eq!(path, "/api/v2/spot/public/symbols");
208    }
209
210    #[test]
211    fn test_build_api_path_futures_legacy() {
212        let bitget = Bitget::builder()
213            .default_type(DefaultType::Swap)
214            .default_sub_type(DefaultSubType::Linear)
215            .build()
216            .unwrap();
217        let path = bitget.build_api_path("/public/symbols");
218        assert_eq!(path, "/api/v2/mix/public/symbols");
219    }
220
221    #[test]
222    fn test_build_api_path_with_default_type_spot() {
223        let bitget = Bitget::builder()
224            .default_type(DefaultType::Spot)
225            .build()
226            .unwrap();
227        let path = bitget.build_api_path("/public/symbols");
228        assert_eq!(path, "/api/v2/spot/public/symbols");
229    }
230
231    #[test]
232    fn test_build_api_path_with_default_type_swap_linear() {
233        let bitget = Bitget::builder()
234            .default_type(DefaultType::Swap)
235            .default_sub_type(DefaultSubType::Linear)
236            .build()
237            .unwrap();
238        let path = bitget.build_api_path("/public/symbols");
239        assert_eq!(path, "/api/v2/mix/public/symbols");
240    }
241
242    #[test]
243    fn test_build_api_path_with_default_type_swap_inverse() {
244        let bitget = Bitget::builder()
245            .default_type(DefaultType::Swap)
246            .default_sub_type(DefaultSubType::Inverse)
247            .build()
248            .unwrap();
249        let path = bitget.build_api_path("/public/symbols");
250        assert_eq!(path, "/api/v2/mix/public/symbols");
251    }
252
253    #[test]
254    fn test_build_api_path_with_default_type_futures() {
255        let bitget = Bitget::builder()
256            .default_type(DefaultType::Futures)
257            .build()
258            .unwrap();
259        let path = bitget.build_api_path("/public/symbols");
260        assert_eq!(path, "/api/v2/mix/public/symbols");
261    }
262
263    #[test]
264    fn test_build_api_path_with_default_type_margin() {
265        let bitget = Bitget::builder()
266            .default_type(DefaultType::Margin)
267            .build()
268            .unwrap();
269        let path = bitget.build_api_path("/public/symbols");
270        assert_eq!(path, "/api/v2/spot/public/symbols");
271    }
272
273    #[test]
274    #[allow(deprecated)]
275    fn test_get_timestamp() {
276        let _bitget = Bitget::builder().build().unwrap();
277        let ts = Bitget::get_timestamp();
278
279        let parsed: i64 = ts.parse().unwrap();
280        assert!(parsed > 0);
281
282        let now = chrono::Utc::now().timestamp_millis();
283        assert!((now - parsed).abs() < 1000);
284    }
285}