1pub mod spot;
2pub mod usdm;
3
4use crate::{
5 config::Config,
6 error::{
7 BinanceError::{self, *},
8 BinanceResponse,
9 },
10 models::Product,
11};
12use chrono::Utc;
13use fehler::{throw, throws};
14use hex::encode as hexify;
15use hmac::{Hmac, Mac};
16use log::{debug, trace};
17use reqwest::{
18 header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE, USER_AGENT},
19 Client, Method, Response,
20};
21use serde::{de::DeserializeOwned, Serialize};
22use sha2::Sha256;
23
24pub trait Request: Serialize {
25 const PRODUCT: Product;
26 const ENDPOINT: &'static str;
27 const METHOD: Method;
28 const KEYED: bool = false; const SIGNED: bool = false;
30 type Response: DeserializeOwned;
31}
32
33#[derive(Clone, Default)]
34pub struct Binance {
35 key: Option<String>,
36 secret: Option<String>,
37 client: Client,
38 config: Config,
39}
40
41impl Binance {
42 pub fn new() -> Self {
43 Default::default()
44 }
45
46 pub fn with_key(api_key: &str) -> Self {
47 Binance {
48 client: Client::new(),
49 key: Some(api_key.into()),
50 secret: None,
51 config: Config::default(),
52 }
53 }
54
55 pub fn with_key_and_secret(api_key: &str, api_secret: &str) -> Self {
56 Binance {
57 client: Client::new(),
58 key: Some(api_key.into()),
59 secret: Some(api_secret.into()),
60 config: Config::default(),
61 }
62 }
63
64 pub fn config(&mut self, config: Config) {
65 self.config = config;
66 }
67
68 #[throws(BinanceError)]
69 pub async fn request<R>(&self, req: R) -> R::Response
70 where
71 R: Request,
72 {
73 let mut params = if matches!(R::METHOD, Method::GET) {
74 serde_qs::to_string(&req)?
75 } else {
76 String::new()
77 };
78
79 let body = if !matches!(R::METHOD, Method::GET) {
80 serde_qs::to_string(&req)?
81 } else {
82 String::new()
83 };
84
85 if R::SIGNED {
86 if !params.is_empty() {
87 params.push('&');
88 }
89 params.push_str(&format!("timestamp={}", Utc::now().timestamp_millis()));
90 params.push_str(&format!("&recvWindow={}", self.config.recv_window));
91
92 let signature = self.signature(¶ms, &body)?;
93 params.push_str(&format!("&signature={}", signature));
94 }
95
96 let path = R::ENDPOINT.to_string();
97
98 let base = match R::PRODUCT {
99 Product::Spot => &self.config.rest_api_endpoint,
100 Product::UsdMFutures => &self.config.usdm_futures_rest_api_endpoint,
101 Product::CoinMFutures => &self.config.coinm_futures_rest_api_endpoint,
102 Product::EuropeanOptions => &self.config.european_options_rest_api_endpoint,
103 };
104 let url = format!("{base}{path}?{params}");
105
106 let mut custom_headers = HeaderMap::new();
107 custom_headers.insert(USER_AGENT, HeaderValue::from_static("binance-async-rs"));
108 if !body.is_empty() {
109 custom_headers.insert(
110 CONTENT_TYPE,
111 HeaderValue::from_static("application/x-www-form-urlencoded"),
112 );
113 }
114 if R::SIGNED || R::KEYED {
115 let key = match &self.key {
116 Some(key) => key,
117 None => throw!(MissingApiKey),
118 };
119 custom_headers.insert(
120 HeaderName::from_static("x-mbx-apikey"),
121 HeaderValue::from_str(key)?,
122 );
123 }
124
125 debug!("[REST] url: {url}, body: {body}");
126
127 let resp = self
128 .client
129 .request(R::METHOD, url.as_str())
130 .headers(custom_headers)
131 .body(body)
132 .send()
133 .await?;
134
135 self.handle_response(resp).await?
136 }
137
138 #[throws(BinanceError)]
139 fn signature(&self, params: &str, body: &str) -> String {
140 let secret = match &self.secret {
141 Some(s) => s,
142 None => throw!(MissingApiSecret),
143 };
144 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
146 let sign_message = format!("{}{}", params, body);
147 trace!("Sign message: {}", sign_message);
148 mac.update(sign_message.as_bytes());
149 let signature = hexify(mac.finalize().into_bytes());
150 signature
151 }
152
153 #[throws(BinanceError)]
154 async fn handle_response<O: DeserializeOwned>(&self, resp: Response) -> O {
155 let resp: BinanceResponse<O> = if cfg!(feature = "print-response") {
156 use serde_json::from_str;
157 let body = resp.text().await?;
158 debug!("Response is {body}");
159 from_str(&body)?
160 } else {
161 resp.json().await?
162 };
163 resp.to_result()?
164 }
165}
166
167#[cfg(test)]
168mod test {
169 use super::Binance;
170 use anyhow::Error;
171 use fehler::throws;
172 use url::{form_urlencoded::Serializer, Url};
173
174 #[throws(Error)]
175 #[test]
176 fn signature_query() {
177 let tr = Binance::with_key_and_secret(
178 "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A",
179 "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j",
180 );
181 let sig = tr.signature(
182 &Url::parse_with_params(
183 "http://a.com/api/v1/test",
184 &[
185 ("symbol", "LTCBTC"),
186 ("side", "BUY"),
187 ("type", "LIMIT"),
188 ("timeInForce", "GTC"),
189 ("quantity", "1"),
190 ("price", "0.1"),
191 ("recvWindow", "5000"),
192 ("timestamp", "1499827319559"),
193 ],
194 )?
195 .query()
196 .unwrap_or_default(),
197 "",
198 )?;
199 assert_eq!(
200 sig,
201 "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71"
202 );
203 }
204
205 #[throws(Error)]
206 #[test]
207 fn signature_body() {
208 let tr = Binance::with_key_and_secret(
209 "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A",
210 "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j",
211 );
212 let mut s = Serializer::new(String::new());
213 s.extend_pairs(&[
214 ("symbol", "LTCBTC"),
215 ("side", "BUY"),
216 ("type", "LIMIT"),
217 ("timeInForce", "GTC"),
218 ("quantity", "1"),
219 ("price", "0.1"),
220 ("recvWindow", "5000"),
221 ("timestamp", "1499827319559"),
222 ]);
223
224 let sig = tr.signature(
225 &Url::parse("http://a.com/api/v1/test")?
226 .query()
227 .unwrap_or_default(),
228 &s.finish(),
229 )?;
230 assert_eq!(
231 sig,
232 "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71"
233 );
234 }
235
236 #[throws(Error)]
237 #[test]
238 fn signature_query_body() {
239 let tr = Binance::with_key_and_secret(
240 "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A",
241 "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j",
242 );
243
244 let mut s = Serializer::new(String::new());
245 s.extend_pairs(&[
246 ("quantity", "1"),
247 ("price", "0.1"),
248 ("recvWindow", "5000"),
249 ("timestamp", "1499827319559"),
250 ]);
251
252 let sig = tr.signature(
253 &Url::parse_with_params(
254 "http://a.com/api/v1/order",
255 &[
256 ("symbol", "LTCBTC"),
257 ("side", "BUY"),
258 ("type", "LIMIT"),
259 ("timeInForce", "GTC"),
260 ],
261 )?
262 .query()
263 .unwrap_or_default(),
264 &s.finish(),
265 )?;
266 assert_eq!(
267 sig,
268 "0fd168b8ddb4876a0358a8d14d0c9f3da0e9b20c5d52b2a00fcf7d1c602f9a77"
269 );
270 }
271
272 #[throws(Error)]
273 #[test]
274 fn signature_body2() {
275 let tr = Binance::with_key_and_secret(
276 "vj1e6h50pFN9CsXT5nsL25JkTuBHkKw3zJhsA6OPtruIRalm20vTuXqF3htCZeWW",
277 "5Cjj09rLKWNVe7fSalqgpilh5I3y6pPplhOukZChkusLqqi9mQyFk34kJJBTdlEJ",
278 );
279
280 let q = &mut [
281 ("symbol", "ETHBTC"),
282 ("side", "BUY"),
283 ("type", "LIMIT"),
284 ("timeInForce", "GTC"),
285 ("quantity", "1"),
286 ("price", "0.1"),
287 ("recvWindow", "5000"),
288 ("timestamp", "1540687064555"),
289 ];
290 q.sort();
291 let q: Vec<_> = q.into_iter().map(|(k, v)| format!("{}={}", k, v)).collect();
292 let q = q.join("&");
293 let sig = tr.signature(
294 &Url::parse("http://a.com/api/v1/test")?
295 .query()
296 .unwrap_or_default(),
297 &q,
298 )?;
299 assert_eq!(
300 sig,
301 "1ee5a75760b9496a2144a22116e02bc0b7fdcf828781fa87ca273540dfcf2cb0"
302 );
303 }
304}