ccxt_exchanges/bitget/rest/
mod.rs1mod 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 #[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 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 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 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 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 #[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(×tamp, method, &sign_path, &body_string);
158
159 let mut headers = HeaderMap::new();
160 auth.add_auth_headers(&mut headers, ×tamp, &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}