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 ×tamp.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}