Skip to main content

gateio_rs/ureq/
client.rs

1use crate::http::{Credentials, request::Request};
2use crate::ureq::{Error, Response};
3use crate::version::VERSION;
4use http::Uri;
5use std::time::{SystemTime, UNIX_EPOCH};
6use ureq::{Agent, Error as UreqError};
7
8/// Synchronous HTTP client for Gate.io API using ureq.
9///
10/// This client provides blocking I/O operations and is the default client
11/// for the Gate.io Rust SDK. It automatically handles request signing,
12/// authentication, and provides a simple interface for all API endpoints.
13///
14/// # Features
15///
16/// - **Request Signing**: Automatic HMAC SHA-512 signing for authenticated endpoints
17/// - **Error Handling**: Comprehensive error handling with detailed error types
18/// - **Flexible Configuration**: Configurable base URL, timeouts, and credentials
19/// - **Thread Safe**: Can be safely shared across threads using `Arc`
20///
21/// # Examples
22///
23/// ## Basic Usage (Public API)
24///
25/// ```rust,no_run
26/// use gateio_rs::{api::spot::get_ticker, ureq::GateHttpClient};
27///
28/// let client = GateHttpClient::default();
29/// let request = get_ticker().currency_pair("BTC_USDT");
30/// let response = client.send(request)?;
31/// # Ok::<(), Box<dyn std::error::Error>>(()).expect("");
32/// ```
33///
34/// ## Authenticated Usage
35///
36/// ```rust,no_run
37/// use gateio_rs::{
38///     api::spot::get_account,
39///     http::Credentials,
40///     ureq::GateHttpClient,
41/// };
42///
43/// let credentials = Credentials::new("api_key", "api_secret");
44/// let client = GateHttpClient::default().credentials(credentials);
45/// let request = get_account();
46/// let response = client.send(request)?;
47/// # Ok::<(), Box<dyn std::error::Error>>(()).expect("");
48/// ```
49///
50/// ## Custom Configuration
51///
52/// ```rust
53/// use gateio_rs::{http::Credentials, ureq::GateHttpClient};
54///
55/// let client = GateHttpClient::with_url("https://api.gateio.ws")
56///     .credentials(Credentials::new("api_key", "api_secret"))
57///     .timestamp_delta(1000); // Adjust for server time differences
58/// ```
59#[derive(Clone)]
60pub struct GateHttpClient {
61    client: Agent,
62    base_url: String,
63    timestamp_delta: u64,
64    credentials: Option<Credentials>,
65}
66
67impl GateHttpClient {
68    /// Creates a new client with default settings and Gate.io production URL
69    pub fn default() -> Self {
70        Self::with_url("https://api.gateio.ws")
71    }
72
73    /// Creates a new client with a custom base URL
74    pub fn with_url(url: &str) -> Self {
75        Self {
76            client: Agent::config_builder().build().into(),
77            base_url: url.to_owned(),
78            timestamp_delta: 0,
79            credentials: None,
80        }
81    }
82
83    /// Creates a new client with a custom ureq Agent and base URL
84    pub fn with_custom_agent(agent: Agent, url: &str) -> Self {
85        Self {
86            client: agent,
87            base_url: url.to_owned(),
88            timestamp_delta: 0,
89            credentials: None,
90        }
91    }
92
93    /// Sets the default API credentials for all requests
94    pub fn credentials(mut self, credentials: Credentials) -> Self {
95        self.credentials = Some(credentials);
96        self
97    }
98
99    /// Sets the timestamp delta to adjust for server time differences
100    pub fn timestamp_delta(mut self, timestamp_delta: u64) -> Self {
101        self.timestamp_delta = timestamp_delta;
102        self
103    }
104
105    /// Sends an HTTP request to the Gate.io API
106    pub fn send<R: Into<Request>>(&self, request: R) -> Result<Response, Box<Error>> {
107        let Request {
108            method,
109            path,
110            params,
111            payload,
112            x_gate_exp_time,
113            credentials,
114            sign,
115        } = request.into();
116
117        // Map query parameters (no-ureq)
118        let query_string = params
119            .iter()
120            .map(|(k, v)| format!("{}={}", k, v))
121            .collect::<Vec<String>>()
122            .join("&");
123
124        // Build URL
125        let full_url: Uri = format!("{}{}?{}", self.base_url, path, query_string).parse()?;
126
127        // Handle different HTTP methods and their respective RequestBuilder types
128        let url_string = full_url.to_string();
129        let user_agent = &format!("gateio-rs/{}", VERSION);
130
131        // Create common headers
132        let headers = vec![
133            ("User-Agent", user_agent.as_str()),
134            ("Accept", "application/json"),
135            ("Content-Type", "application/json"),
136        ];
137
138        // Handle credentials and signing
139        let client_credentials = self.credentials.as_ref();
140        let request_credentials = credentials.as_ref();
141        let mut auth_headers: Vec<(&str, String)> = Vec::new();
142
143        if let Some(Credentials {
144            api_key,
145            api_secret,
146        }) = request_credentials.or(client_credentials)
147        {
148            if sign {
149                // Use system clock, panic if system clock is behind `std::time::UNIX_EPOCH`
150                let mut timestamp = SystemTime::now()
151                    .duration_since(UNIX_EPOCH)
152                    .expect("Clock may have gone backwards")
153                    .as_secs();
154
155                // Append timestamp delta to sync up with server time.
156                timestamp -= self.timestamp_delta;
157
158                // Set API-Key and Timestamp in header
159                auth_headers.push(("KEY", api_key.clone()));
160                auth_headers.push(("Timestamp", timestamp.to_string()));
161
162                // Set x-gate-exptime header
163                if let Some(exp_time_ms) = x_gate_exp_time {
164                    auth_headers.push(("x-gate-exptime", exp_time_ms.to_string()));
165                }
166
167                // Stringify available query parameters and append back to query parameters
168                let signature = crate::utils::sign_hmac(
169                    method.as_ref(),
170                    &path.to_string(),
171                    &query_string,
172                    &payload,
173                    &timestamp.to_string(),
174                    api_secret,
175                )
176                .map_err(|_| Error::InvalidApiSecret)?;
177
178                auth_headers.push(("SIGN", signature));
179            }
180        }
181
182        // Make the request based on method type
183        let raw_response = match method {
184            crate::http::Method::Get => {
185                let mut req = self.client.get(&url_string);
186                for (key, value) in &headers {
187                    req = req.header(*key, *value);
188                }
189                for (key, value) in &auth_headers {
190                    req = req.header(*key, value.as_str());
191                }
192                req.call()
193            }
194            crate::http::Method::Post => {
195                let mut req = self.client.post(&url_string);
196                for (key, value) in &headers {
197                    req = req.header(*key, *value);
198                }
199                for (key, value) in &auth_headers {
200                    req = req.header(*key, value.as_str());
201                }
202                if payload.is_empty() {
203                    req.send_empty()
204                } else {
205                    req.send(payload.as_bytes())
206                }
207            }
208            crate::http::Method::Put => {
209                let mut req = self.client.put(&url_string);
210                for (key, value) in &headers {
211                    req = req.header(*key, *value);
212                }
213                for (key, value) in &auth_headers {
214                    req = req.header(*key, value.as_str());
215                }
216                if payload.is_empty() {
217                    req.send_empty()
218                } else {
219                    req.send(payload.as_bytes())
220                }
221            }
222            crate::http::Method::Delete => {
223                let mut req = self.client.delete(&url_string);
224                for (key, value) in &headers {
225                    req = req.header(*key, *value);
226                }
227                for (key, value) in &auth_headers {
228                    req = req.header(*key, value.as_str());
229                }
230                req.call()
231            }
232            crate::http::Method::Patch => {
233                let mut req = self.client.patch(&url_string);
234                for (key, value) in &headers {
235                    req = req.header(*key, *value);
236                }
237                for (key, value) in &auth_headers {
238                    req = req.header(*key, value.as_str());
239                }
240                if payload.is_empty() {
241                    req.send_empty()
242                } else {
243                    req.send(payload.as_bytes())
244                }
245            }
246        };
247
248        let response = match raw_response {
249            Ok(response) => Ok(response),
250            Err(UreqError::StatusCode(status)) => {
251                // In ureq 3.x, StatusCode errors need to be handled differently
252                // We need to get the response from the error
253                return Err(Box::new(Error::Send(UreqError::StatusCode(status))));
254            }
255            Err(err) => Err(Error::Send(err)),
256        }?;
257
258        Ok(Response::from(response))
259    }
260}