use crate::clob::{
error::{ClobError, Result},
order_builder::salt::generate_salt,
to_raw_amount_decimal,
types::{OrderSide, PartialOrderArgs, SignatureType, SignedOrderArgs},
Signer,
};
use rust_decimal::Decimal;
pub struct LimitOrderBuilder {
token_id: String,
amount: Decimal,
price: Decimal,
side: OrderSide,
fee_rate_bps: Option<u16>,
nonce: Option<u64>,
expiration: Option<u64>,
signature_type: SignatureType,
funder: Option<String>,
neg_risk: Option<bool>,
}
impl LimitOrderBuilder {
pub fn new(token_id: impl Into<String>, amount: Decimal, price: Decimal, side: OrderSide) -> Self {
Self {
token_id: token_id.into(),
amount,
price,
side,
fee_rate_bps: None,
nonce: None,
expiration: None,
signature_type: SignatureType::Eoa,
funder: None,
neg_risk: None,
}
}
pub fn fee_rate_bps(mut self, fee_rate_bps: u16) -> Self {
self.fee_rate_bps = Some(fee_rate_bps);
self
}
pub fn nonce(mut self, nonce: u64) -> Self {
self.nonce = Some(nonce);
self
}
pub fn expiration(mut self, expiration: u64) -> Self {
self.expiration = Some(expiration);
self
}
pub fn signature_type(mut self, signature_type: SignatureType) -> Self {
self.signature_type = signature_type;
self
}
pub fn funder(mut self, funder: impl Into<String>) -> Self {
self.funder = Some(funder.into());
self
}
pub fn neg_risk(mut self, neg_risk: bool) -> Self {
self.neg_risk = Some(neg_risk);
self
}
pub fn build_partial(self) -> Result<PartialOrderArgs> {
if self.amount <= Decimal::ZERO {
return Err(ClobError::InvalidOrder(
"Amount must be positive".to_string(),
));
}
if self.price <= Decimal::ZERO || self.price > Decimal::ONE {
return Err(ClobError::InvalidOrder(
"Price must be between 0 and 1".to_string(),
));
}
let (maker_amount, taker_amount) = match self.side {
OrderSide::SELL => {
let tokens = self.amount.round_dp(5);
let usdc = (self.amount * self.price).round_dp(2);
(to_raw_amount_decimal(tokens), to_raw_amount_decimal(usdc))
}
OrderSide::BUY => {
let usdc = (self.amount * self.price).round_dp(2);
let tokens = self.amount.round_dp(5);
(to_raw_amount_decimal(usdc), to_raw_amount_decimal(tokens))
}
};
Ok(PartialOrderArgs {
salt: self.nonce.unwrap_or_else(generate_salt),
maker: String::new(), signer: String::new(), taker: crate::clob::constants::ZERO_ADDRESS.to_string(),
token_id: self.token_id,
maker_amount,
taker_amount,
side: self.side,
fee_rate_bps: self.fee_rate_bps.unwrap_or(0),
nonce: 0, signature_type: self.signature_type,
expiration: self.expiration.unwrap_or(0),
})
}
pub async fn build_and_sign(self, signer: &Signer) -> Result<SignedOrderArgs> {
let funder = self.funder.clone();
let neg_risk = self.neg_risk.unwrap_or(false);
let mut partial = self.build_partial()?;
partial.maker = funder.unwrap_or_else(|| signer.address());
partial.signer = signer.address();
super::sign_order(partial, signer, neg_risk).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clob::OrderSide;
use std::str::FromStr;
#[test]
fn test_limit_order_builder() {
let builder = LimitOrderBuilder::new("456", Decimal::from(50), Decimal::from_str("0.75").unwrap(), OrderSide::SELL)
.fee_rate_bps(20)
.nonce(789012);
let partial = builder.build_partial().unwrap();
assert_eq!(partial.token_id, "456");
assert_eq!(partial.maker_amount, "50000000"); assert_eq!(partial.side, OrderSide::SELL);
assert_eq!(partial.fee_rate_bps, 20);
}
#[test]
fn test_limit_order_taker_amount() {
let builder = LimitOrderBuilder::new("456", Decimal::from(100), Decimal::from_str("0.5").unwrap(), OrderSide::BUY);
let partial = builder.build_partial().unwrap();
assert_eq!(partial.maker_amount, "50000000"); assert_eq!(partial.taker_amount, "100000000"); }
#[test]
fn test_invalid_amount() {
let builder = LimitOrderBuilder::new("456", Decimal::from(-50), Decimal::from_str("0.75").unwrap(), OrderSide::SELL);
assert!(builder.build_partial().is_err());
}
#[test]
fn test_invalid_price_too_high() {
let builder = LimitOrderBuilder::new("456", Decimal::from(50), Decimal::from_str("1.5").unwrap(), OrderSide::SELL);
assert!(builder.build_partial().is_err());
}
#[test]
fn test_invalid_price_zero() {
let builder = LimitOrderBuilder::new("456", Decimal::from(50), Decimal::ZERO, OrderSide::SELL);
assert!(builder.build_partial().is_err());
}
}