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