Skip to main content

binance/spot/http/
client.rs

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