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;
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    #![allow(clippy::disallowed_methods)]
199    use super::*;
200    use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
201
202    #[test]
203    fn test_build_api_path_spot() {
204        let bitget = Bitget::builder().build().unwrap();
205        let path = bitget.build_api_path("/public/symbols");
206        assert_eq!(path, "/api/v2/spot/public/symbols");
207    }
208
209    #[test]
210    fn test_build_api_path_futures_legacy() {
211        let bitget = Bitget::builder()
212            .default_type(DefaultType::Swap)
213            .default_sub_type(DefaultSubType::Linear)
214            .build()
215            .unwrap();
216        let path = bitget.build_api_path("/public/symbols");
217        assert_eq!(path, "/api/v2/mix/public/symbols");
218    }
219
220    #[test]
221    fn test_build_api_path_with_default_type_spot() {
222        let bitget = Bitget::builder()
223            .default_type(DefaultType::Spot)
224            .build()
225            .unwrap();
226        let path = bitget.build_api_path("/public/symbols");
227        assert_eq!(path, "/api/v2/spot/public/symbols");
228    }
229
230    #[test]
231    fn test_build_api_path_with_default_type_swap_linear() {
232        let bitget = Bitget::builder()
233            .default_type(DefaultType::Swap)
234            .default_sub_type(DefaultSubType::Linear)
235            .build()
236            .unwrap();
237        let path = bitget.build_api_path("/public/symbols");
238        assert_eq!(path, "/api/v2/mix/public/symbols");
239    }
240
241    #[test]
242    fn test_build_api_path_with_default_type_swap_inverse() {
243        let bitget = Bitget::builder()
244            .default_type(DefaultType::Swap)
245            .default_sub_type(DefaultSubType::Inverse)
246            .build()
247            .unwrap();
248        let path = bitget.build_api_path("/public/symbols");
249        assert_eq!(path, "/api/v2/mix/public/symbols");
250    }
251
252    #[test]
253    fn test_build_api_path_with_default_type_futures() {
254        let bitget = Bitget::builder()
255            .default_type(DefaultType::Futures)
256            .build()
257            .unwrap();
258        let path = bitget.build_api_path("/public/symbols");
259        assert_eq!(path, "/api/v2/mix/public/symbols");
260    }
261
262    #[test]
263    fn test_build_api_path_with_default_type_margin() {
264        let bitget = Bitget::builder()
265            .default_type(DefaultType::Margin)
266            .build()
267            .unwrap();
268        let path = bitget.build_api_path("/public/symbols");
269        assert_eq!(path, "/api/v2/spot/public/symbols");
270    }
271
272    #[test]
273    #[allow(deprecated)]
274    fn test_get_timestamp() {
275        let _bitget = Bitget::builder().build().unwrap();
276        let ts = Bitget::get_timestamp();
277
278        let parsed: i64 = ts.parse().unwrap();
279        assert!(parsed > 0);
280
281        let now = chrono::Utc::now().timestamp_millis();
282        assert!((now - parsed).abs() < 1000);
283    }
284}