rust_okx/api/trade/requests/
advanced.rs1use std::collections::HashSet;
2
3use serde::Serialize;
4
5use crate::model::{
6 RequestValidationError, ValidateRequest, collection_length, non_empty, non_empty_items,
7 optional_one_of, optional_unsigned_integer_string, range_u64,
8};
9
10#[derive(Debug, Clone, Serialize)]
12pub struct EasyConvertRequest {
13 #[serde(rename = "fromCcy")]
14 from_ccy: Vec<String>,
15 #[serde(rename = "toCcy")]
16 to_ccy: String,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 source: Option<String>,
19}
20
21impl EasyConvertRequest {
22 pub fn new<I, S>(from_ccy: I, to_ccy: impl Into<String>) -> Self
24 where
25 I: IntoIterator<Item = S>,
26 S: Into<String>,
27 {
28 Self {
29 from_ccy: from_ccy.into_iter().map(Into::into).collect(),
30 to_ccy: to_ccy.into(),
31 source: None,
32 }
33 }
34
35 pub fn source(mut self, value: impl Into<String>) -> Self {
37 self.source = Some(value.into());
38 self
39 }
40}
41
42impl ValidateRequest for EasyConvertRequest {
43 fn validate(&self) -> Result<(), RequestValidationError> {
44 collection_length("fromCcy", self.from_ccy.len(), 1, 5)?;
45 non_empty_items("fromCcy", self.from_ccy.iter().map(String::as_str))?;
46 non_empty("toCcy", &self.to_ccy)?;
47 optional_one_of("source", self.source.as_deref(), &["1", "2"], "1 or 2")?;
48
49 if self.from_ccy.iter().any(|ccy| ccy == &self.to_ccy) {
50 return Err(RequestValidationError::InvalidFormat {
51 field: "toCcy",
52 expected: "a currency different from every fromCcy entry",
53 });
54 }
55 let unique: HashSet<&str> = self.from_ccy.iter().map(String::as_str).collect();
56 if unique.len() != self.from_ccy.len() {
57 return Err(RequestValidationError::InvalidFormat {
58 field: "fromCcy",
59 expected: "one to five distinct currencies",
60 });
61 }
62 Ok(())
63 }
64}
65
66#[derive(Debug, Clone, Default, Serialize)]
68pub struct EasyConvertHistoryRequest {
69 #[serde(skip_serializing_if = "Option::is_none")]
70 after: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 before: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 limit: Option<u32>,
75}
76
77impl EasyConvertHistoryRequest {
78 pub fn new() -> Self {
80 Self::default()
81 }
82
83 pub fn after(mut self, value: impl Into<String>) -> Self {
85 self.after = Some(value.into());
86 self
87 }
88
89 pub fn before(mut self, value: impl Into<String>) -> Self {
91 self.before = Some(value.into());
92 self
93 }
94
95 pub fn limit(mut self, value: u32) -> Self {
97 self.limit = Some(value);
98 self
99 }
100}
101
102impl ValidateRequest for EasyConvertHistoryRequest {
103 fn validate(&self) -> Result<(), RequestValidationError> {
104 optional_unsigned_integer_string("after", self.after.as_deref())?;
105 optional_unsigned_integer_string("before", self.before.as_deref())?;
106 if let Some(limit) = self.limit {
107 range_u64("limit", u64::from(limit), 1, 100)?;
108 }
109 Ok(())
110 }
111}
112
113#[derive(Debug, Clone, Default, Serialize)]
115pub struct OneClickRepayCurrencyListRequest {
116 #[serde(rename = "debtType", skip_serializing_if = "Option::is_none")]
117 debt_type: Option<String>,
118}
119
120impl OneClickRepayCurrencyListRequest {
121 pub fn new() -> Self {
123 Self::default()
124 }
125
126 pub fn debt_type(mut self, value: impl Into<String>) -> Self {
128 self.debt_type = Some(value.into());
129 self
130 }
131}
132
133impl ValidateRequest for OneClickRepayCurrencyListRequest {
134 fn validate(&self) -> Result<(), RequestValidationError> {
135 optional_one_of(
136 "debtType",
137 self.debt_type.as_deref(),
138 &["cross", "isolated"],
139 "cross or isolated",
140 )
141 }
142}
143
144#[derive(Debug, Clone, Serialize)]
146#[serde(untagged)]
147enum DebtCurrencySelection {
148 One(String),
149 Many(Vec<String>),
150}
151
152#[derive(Debug, Clone, Serialize)]
154pub struct OneClickRepayRequest {
155 #[serde(rename = "debtCcy")]
156 debt_ccy: DebtCurrencySelection,
157 #[serde(rename = "repayCcy", skip_serializing_if = "Option::is_none")]
158 repay_ccy: Option<String>,
159 #[serde(rename = "repayCcyList", skip_serializing_if = "Option::is_none")]
160 repay_ccy_list: Option<Vec<String>>,
161}
162
163impl OneClickRepayRequest {
164 pub fn new<I, S>(debt_ccy: I, repay_ccy: impl Into<String>) -> Self
166 where
167 I: IntoIterator<Item = S>,
168 S: Into<String>,
169 {
170 Self {
171 debt_ccy: DebtCurrencySelection::Many(debt_ccy.into_iter().map(Into::into).collect()),
172 repay_ccy: Some(repay_ccy.into()),
173 repay_ccy_list: None,
174 }
175 }
176
177 pub fn v2<I, S>(debt_ccy: impl Into<String>, repay_ccy_list: I) -> Self
179 where
180 I: IntoIterator<Item = S>,
181 S: Into<String>,
182 {
183 Self {
184 debt_ccy: DebtCurrencySelection::One(debt_ccy.into()),
185 repay_ccy: None,
186 repay_ccy_list: Some(repay_ccy_list.into_iter().map(Into::into).collect()),
187 }
188 }
189}
190
191impl ValidateRequest for OneClickRepayRequest {
192 fn validate(&self) -> Result<(), RequestValidationError> {
193 match (&self.debt_ccy, &self.repay_ccy, &self.repay_ccy_list) {
194 (DebtCurrencySelection::Many(debt), Some(repay), None) => {
195 collection_length("debtCcy", debt.len(), 1, 5)?;
196 non_empty_items("debtCcy", debt.iter().map(String::as_str))?;
197 non_empty("repayCcy", repay)?;
198 if debt.iter().any(|ccy| ccy == repay) {
199 return Err(RequestValidationError::InvalidFormat {
200 field: "repayCcy",
201 expected: "a currency different from every debtCcy entry",
202 });
203 }
204 Ok(())
205 }
206 (DebtCurrencySelection::One(debt), None, Some(repay_list)) => {
207 non_empty("debtCcy", debt)?;
208 collection_length("repayCcyList", repay_list.len(), 1, 100)?;
209 non_empty_items("repayCcyList", repay_list.iter().map(String::as_str))
210 }
211 _ => Err(RequestValidationError::InvalidFormat {
212 field: "debtCcy",
213 expected: "the v1 or v2 one-click-repay request shape",
214 }),
215 }
216 }
217}
218
219#[derive(Debug, Clone, Default, Serialize)]
221pub struct OneClickRepayHistoryRequest {
222 #[serde(skip_serializing_if = "Option::is_none")]
223 after: Option<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 before: Option<String>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 limit: Option<u32>,
228}
229
230impl OneClickRepayHistoryRequest {
231 pub fn new() -> Self {
233 Self::default()
234 }
235
236 pub fn after(mut self, value: impl Into<String>) -> Self {
238 self.after = Some(value.into());
239 self
240 }
241
242 pub fn before(mut self, value: impl Into<String>) -> Self {
244 self.before = Some(value.into());
245 self
246 }
247
248 pub fn limit(mut self, value: u32) -> Self {
250 self.limit = Some(value);
251 self
252 }
253}
254
255impl ValidateRequest for OneClickRepayHistoryRequest {
256 fn validate(&self) -> Result<(), RequestValidationError> {
257 optional_unsigned_integer_string("after", self.after.as_deref())?;
258 optional_unsigned_integer_string("before", self.before.as_deref())?;
259 if let Some(limit) = self.limit {
260 range_u64("limit", u64::from(limit), 1, 100)?;
261 }
262 Ok(())
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn easy_convert_rejects_receiving_source_currency() {
272 let request = EasyConvertRequest::new(["BTC"], "BTC");
273 assert!(request.validate().is_err());
274 }
275
276 #[test]
277 fn legacy_repay_is_limited_to_five_debt_currencies() {
278 let request = OneClickRepayRequest::new(["A", "B", "C", "D", "E", "F"], "USDT");
279 assert!(request.validate().is_err());
280 }
281}