Skip to main content

binance/spot/http/
client.rs

1use reqwest::{self, Method, RequestBuilder, header::HeaderMap};
2
3use crate::{
4    SensitiveString,
5    crypto::sign_query,
6    serde::{deserialize_json, serialize_query},
7    spot::{
8        ApiError, Error, HEADER_RETRY_AFTER, HEADER_X_MBX_APIKEY, Path,
9        http::{
10            AccountInformation, AggregateTrade, CurrentAveragePrice, ExchangeInfo,
11            GetAccountInformationParams, GetAggregateTradesParams, GetCurrentAveragePriceParams,
12            GetExchangeInfoParams, GetKlineListParams, GetOlderTradesParams, GetOrderBookParams,
13            GetRecentTradesParams, GetTickerPriceChangeStatisticsParams, Headers, Kline,
14            NewOrderRequest, NewOrderResponse, Order, OrderBook, PrivateConfig, PublicConfig,
15            QueryOrderParams, RecentTrade, Response, ServerTime, TestCommissionRates,
16            TestConnectivity, TickerPriceChangeStatistic,
17        },
18    },
19    timestamp,
20};
21
22pub struct PublicClient {
23    base_url: String,
24    headers: HeaderMap,
25}
26
27impl PublicClient {
28    pub fn new(cfg: PublicConfig) -> Self {
29        Self {
30            base_url: cfg.base_url,
31            headers: cfg.headers.unwrap_or_default(),
32        }
33    }
34}
35
36// General
37impl PublicClient {
38    /// Test connectivity to the Rest API.
39    pub async fn test_connectivity(&self) -> Result<Response<TestConnectivity>, Error> {
40        let url = format!("{}{}", self.base_url, Path::Ping);
41
42        let client = reqwest::Client::builder().build()?;
43        let request = client
44            .request(Method::GET, url)
45            .headers(self.headers.clone());
46
47        send(request).await
48    }
49
50    pub async fn get_server_time(&self) -> Result<Response<ServerTime>, Error> {
51        let url = format!("{}{}", self.base_url, Path::Time);
52
53        let client = reqwest::Client::builder().build()?;
54        let request = client
55            .request(Method::GET, url)
56            .headers(self.headers.clone());
57
58        send(request).await
59    }
60
61    pub async fn get_exchange_info(
62        &self,
63        params: GetExchangeInfoParams,
64    ) -> Result<Response<ExchangeInfo>, Error> {
65        let url = format!("{}{}", self.base_url, Path::ExchangeInfo);
66
67        let client = reqwest::Client::builder().build()?;
68        let request = client
69            .request(Method::GET, url)
70            .headers(self.headers.clone())
71            .query(&params);
72
73        send(request).await
74    }
75}
76
77//  Market
78impl PublicClient {
79    pub async fn get_order_book(
80        &self,
81        params: GetOrderBookParams,
82    ) -> Result<Response<OrderBook>, Error> {
83        let url = format!("{}{}", self.base_url, Path::Depth);
84
85        let client = reqwest::Client::builder().build()?;
86        let request = client
87            .request(Method::GET, url)
88            .headers(self.headers.clone())
89            .query(&params);
90
91        send(request).await
92    }
93
94    /// Get recent trades.
95    pub async fn recent_trades_list(
96        &self,
97        params: GetRecentTradesParams,
98    ) -> Result<Response<Vec<RecentTrade>>, Error> {
99        let url = format!("{}{}", self.base_url, Path::Trades);
100
101        let client = reqwest::Client::builder().build()?;
102        let request = client
103            .request(Method::GET, url)
104            .headers(self.headers.clone())
105            .query(&params);
106
107        send(request).await
108    }
109
110    /// Get older trades.
111    pub async fn old_trade_lookup(
112        &self,
113        params: GetOlderTradesParams,
114    ) -> Result<Response<Vec<RecentTrade>>, Error> {
115        let url = format!("{}{}", self.base_url, Path::HistoricalTrades);
116
117        let client = reqwest::Client::builder().build()?;
118        let request = client
119            .request(Method::GET, url)
120            .headers(self.headers.clone())
121            .query(&params);
122
123        send(request).await
124    }
125
126    /// Compressed/Aggregate trades list.
127    /// Get compressed, aggregate trades. Trades that fill at the time, from the same taker order, with the same price will have the quantity aggregated.
128    ///
129    /// If fromId, startTime, and endTime are not sent, the most recent aggregate trades will be returned.
130    pub async fn aggregate_trades_list(
131        &self,
132        params: GetAggregateTradesParams,
133    ) -> Result<Response<Vec<AggregateTrade>>, Error> {
134        let url = format!("{}{}", self.base_url, Path::AggTrades);
135
136        let client = reqwest::Client::builder().build()?;
137        let request = client
138            .request(Method::GET, url)
139            .headers(self.headers.clone())
140            .query(&params);
141
142        send(request).await
143    }
144
145    /// Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time.
146    ///
147    /// If startTime and endTime are not sent, the most recent klines are returned.
148    /// Supported values for timeZone:
149    /// Hours and minutes (e.g. -1:00, 05:45)
150    /// Only hours (e.g. 0, 8, 4)
151    /// Accepted range is strictly [-12:00 to +14:00] inclusive
152    /// If timeZone provided, kline intervals are interpreted in that timezone instead of UTC.
153    /// Note that startTime and endTime are always interpreted in UTC, regardless of timeZone.
154    pub async fn get_kline_list(
155        &self,
156        params: GetKlineListParams,
157    ) -> Result<Response<Vec<Kline>>, Error> {
158        let url = format!("{}{}", self.base_url, Path::KLines);
159
160        let client = reqwest::Client::builder().build()?;
161        let request = client
162            .request(Method::GET, url)
163            .headers(self.headers.clone())
164            .query(&params);
165
166        send(request).await
167    }
168
169    /// UIKlines
170    ///
171    /// The request is similar to klines having the same parameters and response.
172    /// uiKlines return modified kline data, optimized for presentation of candlestick charts.
173    ///
174    /// If startTime and endTime are not sent, the most recent klines are returned.
175    /// Supported values for timeZone:
176    /// Hours and minutes (e.g. -1:00, 05:45)
177    /// Only hours (e.g. 0, 8, 4)
178    /// Accepted range is strictly [-12:00 to +14:00] inclusive
179    /// If timeZone provided, kline intervals are interpreted in that timezone instead of UTC.
180    /// Note that startTime and endTime are always interpreted in UTC, regardless of timeZone.
181    pub async fn get_ui_kline_list(
182        &self,
183        params: GetKlineListParams,
184    ) -> Result<Response<Vec<Kline>>, Error> {
185        let url = format!("{}{}", self.base_url, Path::UIKLines);
186
187        let client = reqwest::Client::builder().build()?;
188        let request = client
189            .request(Method::GET, url)
190            .headers(self.headers.clone())
191            .query(&params);
192
193        send(request).await
194    }
195
196    /// Current average price for a symbol.
197    pub async fn get_current_average_price(
198        &self,
199        params: GetCurrentAveragePriceParams,
200    ) -> Result<Response<CurrentAveragePrice>, Error> {
201        let url = format!("{}{}", self.base_url, Path::AvgPrice);
202
203        let client = reqwest::Client::builder().build()?;
204        let request = client
205            .request(Method::GET, url)
206            .headers(self.headers.clone())
207            .query(&params);
208
209        send(request).await
210    }
211
212    /// 24 hour rolling window price change statistics. Careful when accessing this with no symbol.
213    pub async fn ticker_price_change_statistics(
214        &self,
215        params: GetTickerPriceChangeStatisticsParams,
216    ) -> Result<Response<TickerPriceChangeStatistic>, Error> {
217        let url = format!("{}{}", self.base_url, Path::Ticker24hr);
218
219        let client = reqwest::Client::builder().build()?;
220        let request = client
221            .request(Method::GET, url)
222            .headers(self.headers.clone())
223            .query(&params);
224
225        send(request).await
226    }
227}
228
229pub struct PrivateClient {
230    base_url: String,
231    headers: HeaderMap,
232    api_secret: SensitiveString,
233}
234
235impl PrivateClient {
236    pub fn new(cfg: PrivateConfig) -> Self {
237        let headers = {
238            let mut headers = HeaderMap::new();
239
240            let api_key = cfg.api_key.expose().parse().unwrap();
241            headers.append(HEADER_X_MBX_APIKEY, api_key);
242
243            if let Some(extra_headers) = cfg.headers {
244                headers.extend(extra_headers);
245            }
246
247            headers
248        };
249
250        Self {
251            base_url: cfg.base_url,
252            headers,
253            api_secret: cfg.api_secret,
254        }
255    }
256}
257
258// Trading
259impl PrivateClient {
260    /// Send in a new order.
261    /// This adds 1 order to the EXCHANGE_MAX_ORDERS filter and the MAX_NUM_ORDERS filter.
262    ///
263    /// Other info:
264    /// Any LIMIT or LIMIT_MAKER type order can be made an iceberg order by sending an icebergQty.
265    /// Any order with an icebergQty MUST have timeInForce set to GTC.
266    /// For STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT_LIMIT and TAKE_PROFIT orders, trailingDelta can be combined with stopPrice.
267    /// MARKET orders using quoteOrderQty will not break LOT_SIZE filter rules; the order will execute a quantity that will have the notional value as close as possible to quoteOrderQty. Trigger order price rules against market price for both MARKET and LIMIT versions:
268    /// Price above market price: STOP_LOSS BUY, TAKE_PROFIT SELL
269    /// Price below market price: STOP_LOSS SELL, TAKE_PROFIT BUY
270    pub async fn new_order(
271        &self,
272        params: NewOrderRequest,
273    ) -> Result<Response<NewOrderResponse>, Error> {
274        let query = serialize_query(&params)?;
275        let query = sign_query(&self.api_secret, timestamp(), &query);
276        let url = format!("{}{}", self.base_url, Path::Order);
277
278        let client = reqwest::Client::builder().build()?;
279        let request = client
280            .request(Method::POST, url)
281            .headers(self.headers.clone())
282            .body(query); // Binance API accepts POST params in both query and body.
283
284        send(request).await
285    }
286
287    /// Test new order creation and signature/recvWindow long. Creates and validates a new order but does not send it into the matching engine.
288    pub async fn test_new_order(
289        &self,
290        params: NewOrderRequest,
291    ) -> Result<Response<TestCommissionRates>, Error> {
292        let query = serialize_query(&params)?;
293        let query = sign_query(&self.api_secret, timestamp(), &query);
294        let url = format!("{}{}", self.base_url, Path::OrderTest);
295
296        let client = reqwest::Client::builder().build()?;
297        let request = client
298            .request(Method::POST, url)
299            .headers(self.headers.clone())
300            .body(query); // Binance API accepts POST params in both query and body.
301
302        send(request).await
303    }
304}
305
306// Account
307impl PrivateClient {
308    /// Get current account information.
309    pub async fn account_information(
310        &self,
311        params: GetAccountInformationParams,
312    ) -> Result<Response<AccountInformation>, Error> {
313        let query = serialize_query(&params)?;
314        let query = sign_query(&self.api_secret, timestamp(), &query);
315        let url = format!("{}{}?{query}", self.base_url, Path::Account);
316
317        let client = reqwest::Client::builder().build()?;
318        let request = client
319            .request(Method::GET, url)
320            .headers(self.headers.clone());
321
322        send(request).await
323    }
324
325    /// Check an order's status.
326    /// Notes:
327    /// Either orderId or origClientOrderId must be sent.
328    /// If both orderId and origClientOrderId are provided, the orderId is searched first, then the origClientOrderId from that result is checked against that order. If both conditions are not met the request will be rejected.
329    /// For some historical orders cummulativeQuoteQty will be < 0, meaning the data is not available at this time.
330    pub async fn query_order(&self, params: QueryOrderParams) -> Result<Response<Order>, Error> {
331        let query = serialize_query(&params)?;
332        let query = sign_query(&self.api_secret, timestamp(), &query);
333        let url = format!("{}{}?{query}", self.base_url, Path::Order);
334
335        let client = reqwest::Client::builder().build()?;
336        let request = client
337            .request(Method::GET, url)
338            .headers(self.headers.clone());
339
340        send(request).await
341    }
342}
343
344async fn send<T>(request: RequestBuilder) -> Result<Response<T>, Error>
345where
346    T: serde::de::DeserializeOwned,
347{
348    let response = request.send().await?;
349    let status = response.status();
350    let headers = parse_headers(response.headers());
351    let json = response.text().await?;
352
353    if !status.is_success() {
354        #[cfg(debug_assertions)]
355        tracing::debug!(?status, ?json, "request failed");
356
357        // Binance returns `{"code":-XXXX,"msg":"..."}` on error.
358        let api_err = deserialize_json::<ApiError>(&json)?;
359        return Err(Error::Api(api_err));
360    }
361
362    let result = deserialize_json(&json)?;
363    Ok(Response { result, headers })
364}
365
366/// Parse response headers: Retry-After
367fn parse_headers(headers: &HeaderMap) -> Headers {
368    let retry_after = headers
369        .get(HEADER_RETRY_AFTER)
370        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
371
372    Headers { retry_after }
373}