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