binance/spot/
client.rs

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