Skip to main content

rust_okx/api/account/requests/
loans.rs

1use serde::Serialize;
2
3use crate::model::{
4    RequestValidationError, ValidateRequest, non_empty, one_of, optional_non_empty,
5    optional_one_of, optional_unsigned_integer_string, positive_decimal_string, range_u64,
6};
7
8fn validate_pagination(
9    after: Option<&str>,
10    before: Option<&str>,
11    limit: Option<u32>,
12) -> Result<(), RequestValidationError> {
13    optional_unsigned_integer_string("after", after)?;
14    optional_unsigned_integer_string("before", before)?;
15    if let Some(limit) = limit {
16        range_u64("limit", u64::from(limit), 1, 100)?;
17    }
18    Ok(())
19}
20
21/// Request body for `POST /api/v5/account/spot-manual-borrow-repay`.
22#[derive(Debug, Clone, Serialize)]
23pub struct SpotManualBorrowRepayRequest {
24    ccy: String,
25    side: String,
26    amt: String,
27}
28
29impl SpotManualBorrowRepayRequest {
30    /// Create a manual spot borrow or repay request.
31    pub fn new(ccy: impl Into<String>, side: impl Into<String>, amt: impl Into<String>) -> Self {
32        Self {
33            ccy: ccy.into(),
34            side: side.into(),
35            amt: amt.into(),
36        }
37    }
38}
39
40impl ValidateRequest for SpotManualBorrowRepayRequest {
41    fn validate(&self) -> Result<(), RequestValidationError> {
42        non_empty("ccy", &self.ccy)?;
43        one_of("side", &self.side, &["borrow", "repay"], "borrow or repay")?;
44        positive_decimal_string("amt", &self.amt)
45    }
46}
47
48/// Request body for `POST /api/v5/account/set-auto-repay`.
49#[derive(Debug, Clone, Serialize)]
50pub struct SetAutoRepayRequest {
51    #[serde(rename = "autoRepay")]
52    auto_repay: bool,
53}
54
55impl SetAutoRepayRequest {
56    /// Enable or disable automatic repayment.
57    pub fn new(auto_repay: bool) -> Self {
58        Self { auto_repay }
59    }
60}
61
62impl ValidateRequest for SetAutoRepayRequest {
63    fn validate(&self) -> Result<(), RequestValidationError> {
64        Ok(())
65    }
66}
67
68/// Query parameters for `GET /api/v5/account/spot-borrow-repay-history`.
69#[derive(Debug, Clone, Default, Serialize)]
70pub struct SpotBorrowRepayHistoryRequest {
71    #[serde(skip_serializing_if = "Option::is_none")]
72    ccy: Option<String>,
73    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
74    event_type: Option<String>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    after: Option<String>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    before: Option<String>,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    limit: Option<u32>,
81}
82
83impl SpotBorrowRepayHistoryRequest {
84    /// Create an unfiltered history query.
85    pub fn new() -> Self {
86        Self::default()
87    }
88
89    /// Filter by currency.
90    pub fn currency(mut self, value: impl Into<String>) -> Self {
91        self.ccy = Some(value.into());
92        self
93    }
94
95    /// Filter by event type.
96    pub fn event_type(mut self, value: impl Into<String>) -> Self {
97        self.event_type = Some(value.into());
98        self
99    }
100
101    /// Return records earlier than this millisecond timestamp.
102    pub fn after(mut self, value: impl Into<String>) -> Self {
103        self.after = Some(value.into());
104        self
105    }
106
107    /// Return records newer than this millisecond timestamp.
108    pub fn before(mut self, value: impl Into<String>) -> Self {
109        self.before = Some(value.into());
110        self
111    }
112
113    /// Set the result count from 1 through 100.
114    pub fn limit(mut self, value: u32) -> Self {
115        self.limit = Some(value);
116        self
117    }
118}
119
120impl ValidateRequest for SpotBorrowRepayHistoryRequest {
121    fn validate(&self) -> Result<(), RequestValidationError> {
122        optional_non_empty("ccy", self.ccy.as_deref())?;
123        optional_one_of(
124            "type",
125            self.event_type.as_deref(),
126            &["auto_borrow", "auto_repay", "manual_borrow", "manual_repay"],
127            "auto_borrow, auto_repay, manual_borrow, or manual_repay",
128        )?;
129        validate_pagination(self.after.as_deref(), self.before.as_deref(), self.limit)
130    }
131}
132
133/// Request body for `POST /api/v5/account/set-auto-earn`.
134#[derive(Debug, Clone, Serialize)]
135pub struct SetAutoEarnRequest {
136    #[serde(rename = "earnType")]
137    earn_type: String,
138    ccy: String,
139    action: String,
140}
141
142impl SetAutoEarnRequest {
143    /// Create an auto-earn update.
144    ///
145    /// `earn_type` is `0` for auto-lend/stake and `1` for USDG-style earn;
146    /// `action` is `turn_on` or `turn_off`.
147    pub fn new(
148        earn_type: impl Into<String>,
149        ccy: impl Into<String>,
150        action: impl Into<String>,
151    ) -> Self {
152        Self {
153            earn_type: earn_type.into(),
154            ccy: ccy.into(),
155            action: action.into(),
156        }
157    }
158}
159
160impl ValidateRequest for SetAutoEarnRequest {
161    fn validate(&self) -> Result<(), RequestValidationError> {
162        one_of("earnType", &self.earn_type, &["0", "1"], "0 or 1")?;
163        non_empty("ccy", &self.ccy)?;
164        one_of(
165            "action",
166            &self.action,
167            &["turn_on", "turn_off"],
168            "turn_on or turn_off",
169        )
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn spot_manual_borrow_repay_rejects_invalid_side() {
179        let request = SpotManualBorrowRepayRequest::new("USDT", "lend", "10");
180        assert!(request.validate().is_err());
181    }
182
183    #[test]
184    fn spot_history_rejects_limit_over_one_hundred() {
185        let request = SpotBorrowRepayHistoryRequest::new().limit(101);
186        assert!(request.validate().is_err());
187    }
188
189    #[test]
190    fn auto_earn_uses_current_wire_fields() {
191        let request = SetAutoEarnRequest::new("0", "BTC", "turn_on");
192        request.validate().unwrap();
193        let value = serde_json::to_value(request).unwrap();
194        assert_eq!(value["earnType"], "0");
195        assert_eq!(value["action"], "turn_on");
196        assert!(value.get("autoEarn").is_none());
197    }
198}