rust_okx/api/finance/requests/
staking.rs1use serde::Serialize;
2
3use crate::model::{
4 RequestValidationError, ValidateRequest, collection_length, length_range, non_empty, one_of,
5 optional_non_empty, optional_one_of, optional_unsigned_integer_string, positive_decimal_string,
6 range_u64,
7};
8
9const PROTOCOL_TYPES: &[&str] = &["staking", "defi"];
10
11#[derive(Debug, Clone, Default, Serialize)]
13pub struct StakingDefiOffersRequest {
14 #[serde(rename = "productId", skip_serializing_if = "Option::is_none")]
15 product_id: Option<String>,
16 #[serde(rename = "protocolType", skip_serializing_if = "Option::is_none")]
17 protocol_type: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 ccy: Option<String>,
20}
21
22impl StakingDefiOffersRequest {
23 pub fn new() -> Self {
25 Self::default()
26 }
27
28 pub fn product_id(mut self, value: impl Into<String>) -> Self {
30 self.product_id = Some(value.into());
31 self
32 }
33
34 pub fn protocol_type(mut self, value: impl Into<String>) -> Self {
36 self.protocol_type = Some(value.into());
37 self
38 }
39
40 pub fn currency(mut self, value: impl Into<String>) -> Self {
42 self.ccy = Some(value.into());
43 self
44 }
45}
46
47impl ValidateRequest for StakingDefiOffersRequest {
48 fn validate(&self) -> Result<(), RequestValidationError> {
49 optional_non_empty("productId", self.product_id.as_deref())?;
50 optional_one_of(
51 "protocolType",
52 self.protocol_type.as_deref(),
53 PROTOCOL_TYPES,
54 "staking or defi",
55 )?;
56 optional_non_empty("ccy", self.ccy.as_deref())
57 }
58}
59
60#[derive(Debug, Clone, Serialize)]
62pub struct StakingDefiInvestment {
63 ccy: String,
64 amt: String,
65}
66
67impl StakingDefiInvestment {
68 pub fn new(ccy: impl Into<String>, amt: impl Into<String>) -> Self {
70 Self {
71 ccy: ccy.into(),
72 amt: amt.into(),
73 }
74 }
75}
76
77impl ValidateRequest for StakingDefiInvestment {
78 fn validate(&self) -> Result<(), RequestValidationError> {
79 non_empty("investData.ccy", &self.ccy)?;
80 positive_decimal_string("investData.amt", &self.amt)
81 }
82}
83
84#[derive(Debug, Clone, Serialize)]
86pub struct StakingDefiPurchaseRequest {
87 #[serde(rename = "productId")]
88 product_id: String,
89 #[serde(rename = "investData")]
90 invest_data: Vec<StakingDefiInvestment>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 term: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 tag: Option<String>,
95}
96
97impl StakingDefiPurchaseRequest {
98 pub fn new(product_id: impl Into<String>, invest_data: Vec<StakingDefiInvestment>) -> Self {
100 Self {
101 product_id: product_id.into(),
102 invest_data,
103 term: None,
104 tag: None,
105 }
106 }
107
108 pub fn term(mut self, value: impl Into<String>) -> Self {
110 self.term = Some(value.into());
111 self
112 }
113
114 pub fn tag(mut self, value: impl Into<String>) -> Self {
116 self.tag = Some(value.into());
117 self
118 }
119}
120
121impl ValidateRequest for StakingDefiPurchaseRequest {
122 fn validate(&self) -> Result<(), RequestValidationError> {
123 non_empty("productId", &self.product_id)?;
124 collection_length("investData", self.invest_data.len(), 1, usize::MAX)?;
125 for investment in &self.invest_data {
126 investment.validate()?;
127 }
128 optional_non_empty("term", self.term.as_deref())?;
129 if let Some(tag) = self.tag.as_deref() {
130 length_range("tag", tag, 1, 16)?;
131 if !tag.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
132 return Err(RequestValidationError::InvalidFormat {
133 field: "tag",
134 expected: "1-16 ASCII alphanumeric characters",
135 });
136 }
137 }
138 Ok(())
139 }
140}
141
142#[derive(Debug, Clone, Serialize)]
144pub struct StakingDefiRedeemRequest {
145 #[serde(rename = "ordId")]
146 ord_id: String,
147 #[serde(rename = "protocolType")]
148 protocol_type: String,
149 #[serde(rename = "allowEarlyRedeem", skip_serializing_if = "Option::is_none")]
150 allow_early_redeem: Option<bool>,
151}
152
153impl StakingDefiRedeemRequest {
154 pub fn new(ord_id: impl Into<String>, protocol_type: impl Into<String>) -> Self {
156 Self {
157 ord_id: ord_id.into(),
158 protocol_type: protocol_type.into(),
159 allow_early_redeem: None,
160 }
161 }
162
163 pub fn allow_early_redeem(mut self, value: bool) -> Self {
165 self.allow_early_redeem = Some(value);
166 self
167 }
168}
169
170impl ValidateRequest for StakingDefiRedeemRequest {
171 fn validate(&self) -> Result<(), RequestValidationError> {
172 non_empty("ordId", &self.ord_id)?;
173 one_of(
174 "protocolType",
175 &self.protocol_type,
176 PROTOCOL_TYPES,
177 "staking or defi",
178 )
179 }
180}
181
182#[derive(Debug, Clone, Serialize)]
184pub struct StakingDefiCancelRequest {
185 #[serde(rename = "ordId")]
186 ord_id: String,
187 #[serde(rename = "protocolType")]
188 protocol_type: String,
189}
190
191impl StakingDefiCancelRequest {
192 pub fn new(ord_id: impl Into<String>, protocol_type: impl Into<String>) -> Self {
194 Self {
195 ord_id: ord_id.into(),
196 protocol_type: protocol_type.into(),
197 }
198 }
199}
200
201impl ValidateRequest for StakingDefiCancelRequest {
202 fn validate(&self) -> Result<(), RequestValidationError> {
203 non_empty("ordId", &self.ord_id)?;
204 one_of(
205 "protocolType",
206 &self.protocol_type,
207 PROTOCOL_TYPES,
208 "staking or defi",
209 )
210 }
211}
212
213#[derive(Debug, Clone, Default, Serialize)]
215pub struct StakingDefiActiveOrdersRequest {
216 #[serde(rename = "productId", skip_serializing_if = "Option::is_none")]
217 product_id: Option<String>,
218 #[serde(rename = "protocolType", skip_serializing_if = "Option::is_none")]
219 protocol_type: Option<String>,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 ccy: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 state: Option<String>,
224}
225
226impl StakingDefiActiveOrdersRequest {
227 pub fn new() -> Self {
229 Self::default()
230 }
231
232 pub fn product_id(mut self, value: impl Into<String>) -> Self {
234 self.product_id = Some(value.into());
235 self
236 }
237
238 pub fn protocol_type(mut self, value: impl Into<String>) -> Self {
240 self.protocol_type = Some(value.into());
241 self
242 }
243
244 pub fn currency(mut self, value: impl Into<String>) -> Self {
246 self.ccy = Some(value.into());
247 self
248 }
249
250 pub fn state(mut self, value: impl Into<String>) -> Self {
252 self.state = Some(value.into());
253 self
254 }
255}
256
257impl ValidateRequest for StakingDefiActiveOrdersRequest {
258 fn validate(&self) -> Result<(), RequestValidationError> {
259 optional_non_empty("productId", self.product_id.as_deref())?;
260 optional_one_of(
261 "protocolType",
262 self.protocol_type.as_deref(),
263 PROTOCOL_TYPES,
264 "staking or defi",
265 )?;
266 optional_non_empty("ccy", self.ccy.as_deref())?;
267 optional_one_of(
268 "state",
269 self.state.as_deref(),
270 &["1", "2", "8", "9", "13"],
271 "1, 2, 8, 9, or 13",
272 )
273 }
274}
275
276#[derive(Debug, Clone, Default, Serialize)]
278pub struct StakingDefiOrderHistoryRequest {
279 #[serde(rename = "productId", skip_serializing_if = "Option::is_none")]
280 product_id: Option<String>,
281 #[serde(rename = "protocolType", skip_serializing_if = "Option::is_none")]
282 protocol_type: Option<String>,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 ccy: Option<String>,
285 #[serde(skip_serializing_if = "Option::is_none")]
286 after: Option<String>,
287 #[serde(skip_serializing_if = "Option::is_none")]
288 before: Option<String>,
289 #[serde(skip_serializing_if = "Option::is_none")]
290 limit: Option<u32>,
291}
292
293impl StakingDefiOrderHistoryRequest {
294 pub fn new() -> Self {
296 Self::default()
297 }
298
299 pub fn product_id(mut self, value: impl Into<String>) -> Self {
301 self.product_id = Some(value.into());
302 self
303 }
304
305 pub fn protocol_type(mut self, value: impl Into<String>) -> Self {
307 self.protocol_type = Some(value.into());
308 self
309 }
310
311 pub fn currency(mut self, value: impl Into<String>) -> Self {
313 self.ccy = Some(value.into());
314 self
315 }
316
317 pub fn after(mut self, value: impl Into<String>) -> Self {
319 self.after = Some(value.into());
320 self
321 }
322
323 pub fn before(mut self, value: impl Into<String>) -> Self {
325 self.before = Some(value.into());
326 self
327 }
328
329 pub fn limit(mut self, value: u32) -> Self {
331 self.limit = Some(value);
332 self
333 }
334}
335
336impl ValidateRequest for StakingDefiOrderHistoryRequest {
337 fn validate(&self) -> Result<(), RequestValidationError> {
338 optional_non_empty("productId", self.product_id.as_deref())?;
339 optional_one_of(
340 "protocolType",
341 self.protocol_type.as_deref(),
342 PROTOCOL_TYPES,
343 "staking or defi",
344 )?;
345 optional_non_empty("ccy", self.ccy.as_deref())?;
346 optional_unsigned_integer_string("after", self.after.as_deref())?;
347 optional_unsigned_integer_string("before", self.before.as_deref())?;
348 if let Some(limit) = self.limit {
349 range_u64("limit", u64::from(limit), 1, 100)?;
350 }
351 Ok(())
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn purchase_serializes_nested_investment_data() {
361 let request = StakingDefiPurchaseRequest::new(
362 "product-1",
363 vec![StakingDefiInvestment::new("ETH", "0.5")],
364 );
365 request.validate().unwrap();
366 let value = serde_json::to_value(request).unwrap();
367 assert_eq!(value["productId"], "product-1");
368 assert_eq!(value["investData"][0]["ccy"], "ETH");
369 }
370
371 #[test]
372 fn active_orders_reject_unknown_state() {
373 assert!(
374 StakingDefiActiveOrdersRequest::new()
375 .state("3")
376 .validate()
377 .is_err()
378 );
379 }
380}