Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions ccxt-exchanges/src/binance/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,32 @@ impl Binance {
}
Ok(response)
}

/// Executes a public GET request with unified error checking and rate limiter updates.
///
/// This method wraps `http_client.get()` to provide:
/// - Rate limiter updates from response headers
/// - API error checking (Binance may return errors in the body with HTTP 200)
///
/// All public REST API methods should use this instead of calling `http_client.get()` directly.
pub(crate) async fn public_get(
&self,
url: &str,
headers: Option<reqwest::header::HeaderMap>,
) -> Result<serde_json::Value> {
let data = self.base().http_client.get(url, headers).await?;

// Update rate limiter from response headers
if let Some(resp_headers) = data.get("responseHeaders") {
let rate_info = rate_limiter::RateLimitInfo::from_headers(resp_headers);
if rate_info.has_data() {
self.rate_limiter().update(rate_info);
}
}

// Check for API errors in response body
self.check_response(data)
}
}

#[cfg(test)]
Expand Down
7 changes: 6 additions & 1 deletion ccxt-exchanges/src/binance/rate_limiter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,15 @@ impl RateLimitInfo {
let mut info = Self::default();

if let Some(obj) = headers.as_object() {
// Extract X-MBX-USED-WEIGHT-1M
// Extract X-MBX-USED-WEIGHT-1M (also fallback to x-mbx-used-weight without -1m suffix,
// and x-sapi-used-uid-weight-1m for SAPI endpoints)
if let Some(weight) = obj
.get("x-mbx-used-weight-1m")
.or_else(|| obj.get("X-MBX-USED-WEIGHT-1M"))
.or_else(|| obj.get("x-mbx-used-weight"))
.or_else(|| obj.get("X-MBX-USED-WEIGHT"))
.or_else(|| obj.get("x-sapi-used-uid-weight-1m"))
.or_else(|| obj.get("X-SAPI-USED-UID-WEIGHT-1M"))
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<u64>().ok())
{
Expand Down
6 changes: 3 additions & 3 deletions ccxt-exchanges/src/binance/rest/futures/funding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ impl Binance {
let _ = write!(request_url, "{}={}&", key, value);
}

let response = self.base().http_client.get(&request_url, None).await?;
let response = self.public_get(&request_url, None).await?;

let data = if market.linear.unwrap_or(true) {
&response
Expand Down Expand Up @@ -91,7 +91,7 @@ impl Binance {
}
}

let response = self.base().http_client.get(&request_url, None).await?;
let response = self.public_get(&request_url, None).await?;

let mut rates = BTreeMap::new();

Expand Down Expand Up @@ -173,7 +173,7 @@ impl Binance {
let _ = write!(request_url, "{}={}&", key, value);
}

let response = self.base().http_client.get(&request_url, None).await?;
let response = self.public_get(&request_url, None).await?;

let history_array = response.as_array().ok_or_else(|| {
Error::from(ParseError::invalid_format(
Expand Down
145 changes: 19 additions & 126 deletions ccxt-exchanges/src/binance/rest/futures/margin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use super::super::super::{Binance, signed_request::HttpMethod};
use ccxt_core::{Error, ParseError, Result, types::MarketType};
use reqwest::header::HeaderMap;
use rust_decimal::Decimal;
use serde_json::Value;
use std::collections::{BTreeMap, HashMap};
Expand Down Expand Up @@ -70,118 +69,45 @@ impl Binance {

/// Set position mode (hedge mode or one-way mode).
pub async fn set_position_mode(&self, dual_side: bool, params: Option<Value>) -> Result<Value> {
self.check_required_credentials()?;

let use_dapi = params
.as_ref()
.and_then(|p| p.get("type"))
.and_then(serde_json::Value::as_str)
.is_some_and(|t| t == "delivery" || t == "coin_m");

let mut request_params = BTreeMap::new();
request_params.insert("dualSidePosition".to_string(), dual_side.to_string());

if let Some(params) = params {
if let Some(obj) = params.as_object() {
for (key, value) in obj {
if key == "type" {
continue;
}
if let Some(v) = value.as_str() {
request_params.insert(key.clone(), v.to_string());
} else if let Some(v) = value.as_bool() {
request_params.insert(key.clone(), v.to_string());
} else if let Some(v) = value.as_i64() {
request_params.insert(key.clone(), v.to_string());
}
}
}
}

let timestamp = self.get_signing_timestamp().await?;
let auth = self.get_auth()?;
let signed_params =
auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;

let url = if use_dapi {
format!("{}/positionSide/dual", self.urls().dapi_private)
} else {
format!("{}/positionSide/dual", self.urls().fapi_private)
};

let mut headers = HeaderMap::new();
auth.add_auth_headers_reqwest(&mut headers);

let body = serde_json::to_value(&signed_params).map_err(|e| {
Error::from(ParseError::invalid_format(
"data",
format!("Failed to serialize params: {}", e),
))
})?;

let data = self
.base()
.http_client
.post(&url, Some(headers), Some(body))
.await?;
let builder = self
.signed_request(url)
.method(HttpMethod::Post)
.param("dualSidePosition", dual_side)
.merge_json_params(params);

let data = builder.execute().await?;
Ok(data)
}

/// Fetch current position mode.
pub async fn fetch_position_mode(&self, params: Option<Value>) -> Result<bool> {
self.check_required_credentials()?;

let use_dapi = params
.as_ref()
.and_then(|p| p.get("type"))
.and_then(serde_json::Value::as_str)
.is_some_and(|t| t == "delivery" || t == "coin_m");

let mut request_params = BTreeMap::new();

if let Some(params) = params {
if let Some(obj) = params.as_object() {
for (key, value) in obj {
if key == "type" {
continue;
}
if let Some(v) = value.as_str() {
request_params.insert(key.clone(), v.to_string());
}
}
}
}

let timestamp = self.get_signing_timestamp().await?;
let auth = self.get_auth()?;
let signed_params =
auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;

let query_string = signed_params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");

let url = if use_dapi {
format!(
"{}/positionSide/dual?{}",
self.urls().dapi_private,
query_string
)
format!("{}/positionSide/dual", self.urls().dapi_private)
} else {
format!(
"{}/positionSide/dual?{}",
self.urls().fapi_private,
query_string
)
format!("{}/positionSide/dual", self.urls().fapi_private)
};

let mut headers = HeaderMap::new();
auth.add_auth_headers_reqwest(&mut headers);
let builder = self.signed_request(url).merge_json_params(params);

let data = self.base().http_client.get(&url, Some(headers)).await?;
let data = builder.execute().await?;

if let Some(dual_side) = data.get("dualSidePosition") {
if let Some(value) = dual_side.as_bool() {
Expand All @@ -205,58 +131,25 @@ impl Binance {
amount: Decimal,
params: Option<Value>,
) -> Result<Value> {
self.check_required_credentials()?;

let market = self.base().market(symbol).await?;
let mut request_params = BTreeMap::new();
request_params.insert("symbol".to_string(), market.id.clone());
request_params.insert("amount".to_string(), amount.abs().to_string());
request_params.insert(
"type".to_string(),
if amount > Decimal::ZERO {
"1".to_string()
} else {
"2".to_string()
},
);

if let Some(params) = params {
if let Some(obj) = params.as_object() {
for (key, value) in obj {
if let Some(v) = value.as_str() {
request_params.insert(key.clone(), v.to_string());
}
}
}
}

let timestamp = self.get_signing_timestamp().await?;
let auth = self.get_auth()?;
let signed_params =
auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;

let url = if market.linear.unwrap_or(true) {
format!("{}/positionMargin", self.urls().fapi_private)
} else {
format!("{}/positionMargin", self.urls().dapi_private)
};

let mut headers = HeaderMap::new();
auth.add_auth_headers_reqwest(&mut headers);
let margin_type = if amount > Decimal::ZERO { "1" } else { "2" };

let body = serde_json::to_value(&signed_params).map_err(|e| {
Error::from(ParseError::invalid_format(
"data",
format!("Failed to serialize params: {}", e),
))
})?;

let data = self
.base()
.http_client
.post(&url, Some(headers), Some(body))
.await?;
let builder = self
.signed_request(url)
.method(HttpMethod::Post)
.param("symbol", &market.id)
.param("amount", amount.abs())
.param("type", margin_type)
.merge_json_params(params);

let data = builder.execute().await?;
Ok(data)
}
}
Loading