Skip to main content

binance/wallet/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    timestamp,
9    wallet::{
10        ApiError, Error, HEADER_RETRY_AFTER, HEADER_X_MBX_APIKEY, Path,
11        http::{
12            AccountStatus, CoinInfo, Deposit, DepositAddress, GetAccountStatusParams,
13            GetAllCoinsParams, GetDepositAddressParams, GetDepositHistoryParams, GetTradeFeeParams,
14            GetWithdrawHistoryParams, Headers, PrivateConfig, Response, TradeFee, Withdraw,
15        },
16    },
17};
18
19/// Client for the authenticated `/sapi/v1/{capital,account,asset}/*` surface.
20///
21/// Wallet has no public endpoints — for unauthenticated market data
22/// (klines, depth, tickers) and connectivity (`/api/v3/ping`, `/api/v3/time`),
23/// use [`crate::spot::http::PublicClient`].
24pub struct PrivateClient {
25    base_url: String,
26    headers: HeaderMap,
27    api_secret: SensitiveString,
28}
29
30impl PrivateClient {
31    pub fn new(cfg: PrivateConfig) -> Self {
32        let headers = {
33            let mut headers = HeaderMap::new();
34            let api_key = cfg.api_key.expose().parse().unwrap();
35            headers.append(HEADER_X_MBX_APIKEY, api_key);
36            if let Some(extra) = cfg.headers {
37                headers.extend(extra);
38            }
39            headers
40        };
41        Self {
42            base_url: cfg.base_url,
43            headers,
44            api_secret: cfg.api_secret,
45        }
46    }
47}
48
49// Capital — coin / network metadata, deposits
50impl PrivateClient {
51    /// List every coin the account can hold, with per-network deposit and
52    /// withdraw configuration. Heavyweight (often >300 coins); cache it.
53    pub async fn get_all_coins(
54        &self,
55        params: GetAllCoinsParams,
56    ) -> Result<Response<Vec<CoinInfo>>, Error> {
57        let query = serialize_query(&params)?;
58        let query = sign_query(&self.api_secret, timestamp(), &query);
59        let url = format!("{}{}?{query}", self.base_url, Path::CapitalConfigGetAll);
60        let client = reqwest::Client::builder().build()?;
61        let request = client
62            .request(Method::GET, url)
63            .headers(self.headers.clone());
64        send(request).await
65    }
66
67    /// Fetch the deposit address for a coin (optionally on a specific network).
68    pub async fn get_deposit_address(
69        &self,
70        params: GetDepositAddressParams,
71    ) -> Result<Response<DepositAddress>, Error> {
72        let query = serialize_query(&params)?;
73        let query = sign_query(&self.api_secret, timestamp(), &query);
74        let url = format!("{}{}?{query}", self.base_url, Path::CapitalDepositAddress);
75        let client = reqwest::Client::builder().build()?;
76        let request = client
77            .request(Method::GET, url)
78            .headers(self.headers.clone());
79        send(request).await
80    }
81
82    /// Recent deposit history. Defaults: last 90 days, up to 1000 records.
83    pub async fn get_deposit_history(
84        &self,
85        params: GetDepositHistoryParams,
86    ) -> Result<Response<Vec<Deposit>>, Error> {
87        let query = serialize_query(&params)?;
88        let query = sign_query(&self.api_secret, timestamp(), &query);
89        let url = format!("{}{}?{query}", self.base_url, Path::CapitalDepositHistory);
90        let client = reqwest::Client::builder().build()?;
91        let request = client
92            .request(Method::GET, url)
93            .headers(self.headers.clone());
94        send(request).await
95    }
96
97    /// Recent withdraw history. Defaults: last 90 days, up to 1000 records.
98    pub async fn get_withdraw_history(
99        &self,
100        params: GetWithdrawHistoryParams,
101    ) -> Result<Response<Vec<Withdraw>>, Error> {
102        let query = serialize_query(&params)?;
103        let query = sign_query(&self.api_secret, timestamp(), &query);
104        let url = format!("{}{}?{query}", self.base_url, Path::CapitalWithdrawHistory);
105        let client = reqwest::Client::builder().build()?;
106        let request = client
107            .request(Method::GET, url)
108            .headers(self.headers.clone());
109        send(request).await
110    }
111}
112
113// Account status
114impl PrivateClient {
115    /// Coarse account-wide status string (`"Normal"`, `"Margin Account dormant"`, …).
116    pub async fn get_account_status(
117        &self,
118        params: GetAccountStatusParams,
119    ) -> Result<Response<AccountStatus>, Error> {
120        let query = serialize_query(&params)?;
121        let query = sign_query(&self.api_secret, timestamp(), &query);
122        let url = format!("{}{}?{query}", self.base_url, Path::AccountStatus);
123        let client = reqwest::Client::builder().build()?;
124        let request = client
125            .request(Method::GET, url)
126            .headers(self.headers.clone());
127        send(request).await
128    }
129}
130
131// Asset
132impl PrivateClient {
133    /// Maker / taker commission rates per symbol. Omit `symbol` to fetch all.
134    pub async fn get_trade_fee(
135        &self,
136        params: GetTradeFeeParams,
137    ) -> Result<Response<Vec<TradeFee>>, Error> {
138        let query = serialize_query(&params)?;
139        let query = sign_query(&self.api_secret, timestamp(), &query);
140        let url = format!("{}{}?{query}", self.base_url, Path::AssetTradeFee);
141        let client = reqwest::Client::builder().build()?;
142        let request = client
143            .request(Method::GET, url)
144            .headers(self.headers.clone());
145        send(request).await
146    }
147}
148
149async fn send<T>(request: RequestBuilder) -> Result<Response<T>, Error>
150where
151    T: serde::de::DeserializeOwned,
152{
153    let response = request.send().await?;
154    let status = response.status();
155    let headers = parse_headers(response.headers());
156    let json = response.text().await?;
157
158    if !status.is_success() {
159        #[cfg(debug_assertions)]
160        debug!(?status, ?json, "request failed");
161
162        // Binance returns `{"code":-XXXX,"msg":"..."}` on error.
163        let api_err = deserialize_json::<ApiError>(&json)?;
164        return Err(Error::Api(api_err));
165    }
166
167    let result = deserialize_json(&json)?;
168    Ok(Response { result, headers })
169}
170
171fn parse_headers(headers: &HeaderMap) -> Headers {
172    let retry_after = headers
173        .get(HEADER_RETRY_AFTER)
174        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
175    Headers { retry_after }
176}