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 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}