Skip to main content

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