use serde::Serialize;
use crate::model::{
RequestValidationError, ValidateRequest, collection_length, length_range, non_empty, one_of,
optional_non_empty, optional_one_of, optional_unsigned_integer_string, positive_decimal_string,
range_u64,
};
const PROTOCOL_TYPES: &[&str] = &["staking", "defi"];
#[derive(Debug, Clone, Default, Serialize)]
pub struct StakingDefiOffersRequest {
#[serde(rename = "productId", skip_serializing_if = "Option::is_none")]
product_id: Option<String>,
#[serde(rename = "protocolType", skip_serializing_if = "Option::is_none")]
protocol_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
}
impl StakingDefiOffersRequest {
pub fn new() -> Self {
Self::default()
}
pub fn product_id(mut self, value: impl Into<String>) -> Self {
self.product_id = Some(value.into());
self
}
pub fn protocol_type(mut self, value: impl Into<String>) -> Self {
self.protocol_type = Some(value.into());
self
}
pub fn currency(mut self, value: impl Into<String>) -> Self {
self.ccy = Some(value.into());
self
}
}
impl ValidateRequest for StakingDefiOffersRequest {
fn validate(&self) -> Result<(), RequestValidationError> {
optional_non_empty("productId", self.product_id.as_deref())?;
optional_one_of(
"protocolType",
self.protocol_type.as_deref(),
PROTOCOL_TYPES,
"staking or defi",
)?;
optional_non_empty("ccy", self.ccy.as_deref())
}
}
#[derive(Debug, Clone, Serialize)]
pub struct StakingDefiInvestment {
ccy: String,
amt: String,
}
impl StakingDefiInvestment {
pub fn new(ccy: impl Into<String>, amt: impl Into<String>) -> Self {
Self {
ccy: ccy.into(),
amt: amt.into(),
}
}
}
impl ValidateRequest for StakingDefiInvestment {
fn validate(&self) -> Result<(), RequestValidationError> {
non_empty("investData.ccy", &self.ccy)?;
positive_decimal_string("investData.amt", &self.amt)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct StakingDefiPurchaseRequest {
#[serde(rename = "productId")]
product_id: String,
#[serde(rename = "investData")]
invest_data: Vec<StakingDefiInvestment>,
#[serde(skip_serializing_if = "Option::is_none")]
term: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tag: Option<String>,
}
impl StakingDefiPurchaseRequest {
pub fn new(product_id: impl Into<String>, invest_data: Vec<StakingDefiInvestment>) -> Self {
Self {
product_id: product_id.into(),
invest_data,
term: None,
tag: None,
}
}
pub fn term(mut self, value: impl Into<String>) -> Self {
self.term = Some(value.into());
self
}
pub fn tag(mut self, value: impl Into<String>) -> Self {
self.tag = Some(value.into());
self
}
}
impl ValidateRequest for StakingDefiPurchaseRequest {
fn validate(&self) -> Result<(), RequestValidationError> {
non_empty("productId", &self.product_id)?;
collection_length("investData", self.invest_data.len(), 1, usize::MAX)?;
for investment in &self.invest_data {
investment.validate()?;
}
optional_non_empty("term", self.term.as_deref())?;
if let Some(tag) = self.tag.as_deref() {
length_range("tag", tag, 1, 16)?;
if !tag.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
return Err(RequestValidationError::InvalidFormat {
field: "tag",
expected: "1-16 ASCII alphanumeric characters",
});
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize)]
pub struct StakingDefiRedeemRequest {
#[serde(rename = "ordId")]
ord_id: String,
#[serde(rename = "protocolType")]
protocol_type: String,
#[serde(rename = "allowEarlyRedeem", skip_serializing_if = "Option::is_none")]
allow_early_redeem: Option<bool>,
}
impl StakingDefiRedeemRequest {
pub fn new(ord_id: impl Into<String>, protocol_type: impl Into<String>) -> Self {
Self {
ord_id: ord_id.into(),
protocol_type: protocol_type.into(),
allow_early_redeem: None,
}
}
pub fn allow_early_redeem(mut self, value: bool) -> Self {
self.allow_early_redeem = Some(value);
self
}
}
impl ValidateRequest for StakingDefiRedeemRequest {
fn validate(&self) -> Result<(), RequestValidationError> {
non_empty("ordId", &self.ord_id)?;
one_of(
"protocolType",
&self.protocol_type,
PROTOCOL_TYPES,
"staking or defi",
)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct StakingDefiCancelRequest {
#[serde(rename = "ordId")]
ord_id: String,
#[serde(rename = "protocolType")]
protocol_type: String,
}
impl StakingDefiCancelRequest {
pub fn new(ord_id: impl Into<String>, protocol_type: impl Into<String>) -> Self {
Self {
ord_id: ord_id.into(),
protocol_type: protocol_type.into(),
}
}
}
impl ValidateRequest for StakingDefiCancelRequest {
fn validate(&self) -> Result<(), RequestValidationError> {
non_empty("ordId", &self.ord_id)?;
one_of(
"protocolType",
&self.protocol_type,
PROTOCOL_TYPES,
"staking or defi",
)
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct StakingDefiActiveOrdersRequest {
#[serde(rename = "productId", skip_serializing_if = "Option::is_none")]
product_id: Option<String>,
#[serde(rename = "protocolType", skip_serializing_if = "Option::is_none")]
protocol_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
state: Option<String>,
}
impl StakingDefiActiveOrdersRequest {
pub fn new() -> Self {
Self::default()
}
pub fn product_id(mut self, value: impl Into<String>) -> Self {
self.product_id = Some(value.into());
self
}
pub fn protocol_type(mut self, value: impl Into<String>) -> Self {
self.protocol_type = Some(value.into());
self
}
pub fn currency(mut self, value: impl Into<String>) -> Self {
self.ccy = Some(value.into());
self
}
pub fn state(mut self, value: impl Into<String>) -> Self {
self.state = Some(value.into());
self
}
}
impl ValidateRequest for StakingDefiActiveOrdersRequest {
fn validate(&self) -> Result<(), RequestValidationError> {
optional_non_empty("productId", self.product_id.as_deref())?;
optional_one_of(
"protocolType",
self.protocol_type.as_deref(),
PROTOCOL_TYPES,
"staking or defi",
)?;
optional_non_empty("ccy", self.ccy.as_deref())?;
optional_one_of(
"state",
self.state.as_deref(),
&["1", "2", "8", "9", "13"],
"1, 2, 8, 9, or 13",
)
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct StakingDefiOrderHistoryRequest {
#[serde(rename = "productId", skip_serializing_if = "Option::is_none")]
product_id: Option<String>,
#[serde(rename = "protocolType", skip_serializing_if = "Option::is_none")]
protocol_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u32>,
}
impl StakingDefiOrderHistoryRequest {
pub fn new() -> Self {
Self::default()
}
pub fn product_id(mut self, value: impl Into<String>) -> Self {
self.product_id = Some(value.into());
self
}
pub fn protocol_type(mut self, value: impl Into<String>) -> Self {
self.protocol_type = Some(value.into());
self
}
pub fn currency(mut self, value: impl Into<String>) -> Self {
self.ccy = Some(value.into());
self
}
pub fn after(mut self, value: impl Into<String>) -> Self {
self.after = Some(value.into());
self
}
pub fn before(mut self, value: impl Into<String>) -> Self {
self.before = Some(value.into());
self
}
pub fn limit(mut self, value: u32) -> Self {
self.limit = Some(value);
self
}
}
impl ValidateRequest for StakingDefiOrderHistoryRequest {
fn validate(&self) -> Result<(), RequestValidationError> {
optional_non_empty("productId", self.product_id.as_deref())?;
optional_one_of(
"protocolType",
self.protocol_type.as_deref(),
PROTOCOL_TYPES,
"staking or defi",
)?;
optional_non_empty("ccy", self.ccy.as_deref())?;
optional_unsigned_integer_string("after", self.after.as_deref())?;
optional_unsigned_integer_string("before", self.before.as_deref())?;
if let Some(limit) = self.limit {
range_u64("limit", u64::from(limit), 1, 100)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn purchase_serializes_nested_investment_data() {
let request = StakingDefiPurchaseRequest::new(
"product-1",
vec![StakingDefiInvestment::new("ETH", "0.5")],
);
request.validate().unwrap();
let value = serde_json::to_value(request).unwrap();
assert_eq!(value["productId"], "product-1");
assert_eq!(value["investData"][0]["ccy"], "ETH");
}
#[test]
fn active_orders_reject_unknown_state() {
assert!(
StakingDefiActiveOrdersRequest::new()
.state("3")
.validate()
.is_err()
);
}
}