1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// US-008: Account endpoints — get_balance, get_account_with_join_margin,
// get_position_side_dual, set_position_side_dual, get_leverage_bracket
use crate::futures::models::account::{
AccountInfoResponse, Balance, LeverageBracket, PositionSideDualResponse, StatusMsgResponse,
};
use crate::rest::client::RestClient;
use crate::rest::error::AsterDexError;
use crate::rest::response::ApiResponse;
impl RestClient {
/// Get account balance across all assets.
///
/// Requires authentication. Returns wallet, cross-wallet, and available balances
/// for each asset held in the futures account.
///
/// # Errors
/// - `ConfigError` — credentials not set
/// - `ApiError` — exchange error (e.g. invalid timestamp)
/// - `RateLimited` — HTTP 429
pub async fn get_balance(&self) -> Result<ApiResponse<Vec<Balance>>, AsterDexError> {
self.signed_get("/fapi/v3/balance", &[]).await
}
/// Get full account info with join margin details.
///
/// Requires authentication.
///
/// # Errors
/// - `ConfigError` — credentials not set
pub async fn get_account_with_join_margin(
&self,
) -> Result<ApiResponse<AccountInfoResponse>, AsterDexError> {
self.signed_get("/fapi/v3/accountWithJoinMargin", &[]).await
}
/// Get the current position side dual mode setting.
///
/// Returns `dual_side_position = true` when hedge mode is active, `false` for one-way mode.
///
/// # Errors
/// - `ConfigError` — credentials not set
pub async fn get_position_side_dual(
&self,
) -> Result<ApiResponse<PositionSideDualResponse>, AsterDexError> {
self.signed_get("/fapi/v3/positionSide/dual", &[]).await
}
/// Enable or disable position side dual (hedge) mode.
///
/// Pass `true` to enable hedge mode, `false` for one-way mode.
///
/// # Errors
/// - `ConfigError` — credentials not set
/// - `ApiError { code: -4059 }` — no need to change position side
pub async fn set_position_side_dual(
&self,
dual_side_position: bool,
) -> Result<ApiResponse<StatusMsgResponse>, AsterDexError> {
let val = if dual_side_position { "true" } else { "false" };
self.signed_post("/fapi/v3/positionSide/dual", &[("dualSidePosition", val)])
.await
}
/// Get leverage bracket information.
///
/// Pass `Some("BTCUSDT")` for a specific symbol, or `None` for all symbols.
///
/// # Errors
/// - `ConfigError` — credentials not set
/// - `ApiError { code: -1121 }` — invalid symbol
pub async fn get_leverage_bracket(
&self,
symbol: Option<&str>,
) -> Result<ApiResponse<Vec<LeverageBracket>>, AsterDexError> {
let mut params: Vec<(&str, &str)> = vec![];
if let Some(s) = symbol {
params.push(("symbol", s));
}
self.signed_get("/fapi/v3/leverageBracket", ¶ms).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::Credentials;
// US-008: get_balance without credentials returns ConfigError
#[tokio::test]
async fn get_balance_no_creds_config_error() {
let server = mockito::Server::new_async().await;
// No mock needed — error is returned before any HTTP call
let client = RestClient::new_public(&server.url()).unwrap();
let result = client.get_balance().await;
assert!(
matches!(result, Err(AsterDexError::ConfigError { .. })),
"expected ConfigError, got: {:?}",
result
);
drop(server);
}
// US-008: get_position_side_dual without credentials returns ConfigError
#[tokio::test]
async fn get_position_side_dual_no_creds_config_error() {
let server = mockito::Server::new_async().await;
let client = RestClient::new_public(&server.url()).unwrap();
let result = client.get_position_side_dual().await;
assert!(
matches!(result, Err(AsterDexError::ConfigError { .. })),
"expected ConfigError, got: {:?}",
result
);
drop(server);
}
// US-008: get_leverage_bracket with mock credentials and mockito server returns correct data
#[tokio::test]
async fn get_leverage_bracket_with_creds() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/fapi/v3/leverageBracket")
.match_query(mockito::Matcher::Any)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"[{"symbol":"BTCUSDT","brackets":[{"bracket":1,"initialLeverage":125,"notionalCap":"50000","notionalFloor":"0","maintMarginRatio":"0.004","cum":"0"}]}]"#,
)
.create_async()
.await;
// Use dummy credentials (private key = scalar 1)
let creds = Credentials::new(
"0x0000000000000000000000000000000000000001",
"0x0000000000000000000000000000000000000002",
"0000000000000000000000000000000000000000000000000000000000000001",
714,
)
.unwrap();
let client = RestClient::new(&server.url(), creds).unwrap();
let resp = client.get_leverage_bracket(Some("BTCUSDT")).await.unwrap();
assert_eq!(resp.data[0].symbol, "BTCUSDT");
assert_eq!(resp.data[0].brackets[0].initial_leverage, 125);
}
}