polymarket 0.1.0

Rust SDK for Polymarket prediction market - CLOB trading, on-chain operations, and WebSocket streaming
Documentation
use crate::clob::{
    error::{ClobError, Result},
    order_builder::salt::generate_salt,
    to_raw_amount_decimal,
    types::{OrderSide, PartialOrderArgs, SignatureType, SignedOrderArgs},
    Signer, ZERO_ADDRESS,
};
use rust_decimal::Decimal;

/// Builder for creating market orders (BUY or SELL)
pub struct MarketOrderBuilder {
    token_id: String,
    amount: Decimal,
    side: OrderSide,
    price: Option<Decimal>,
    fee_rate_bps: Option<u16>,
    nonce: Option<u64>,
    expiration: Option<u64>,
    signature_type: SignatureType,
    funder: Option<String>,
    neg_risk: Option<bool>,
}

impl MarketOrderBuilder {
    /// Create a new market order builder
    ///
    /// # Arguments
    /// * `token_id` - The token ID to trade
    /// * `amount` - For BUY orders: dollar amount to spend. For SELL orders: number of tokens to sell
    /// * `side` - BUY or SELL
    pub fn new(token_id: impl Into<String>, amount: Decimal, side: OrderSide) -> Self {
        Self {
            token_id: token_id.into(),
            amount,
            side,
            price: None,
            fee_rate_bps: None,
            nonce: None,
            expiration: None,
            signature_type: SignatureType::Eoa,
            funder: None,
            neg_risk: None,
        }
    }

    /// Set the price for the order
    pub fn price(mut self, price: Decimal) -> Self {
        self.price = Some(price);
        self
    }

    /// Set the fee rate in basis points (e.g., 100 = 1%)
    pub fn fee_rate_bps(mut self, fee_rate_bps: u16) -> Self {
        self.fee_rate_bps = Some(fee_rate_bps);
        self
    }

    /// Set the nonce for the order
    pub fn nonce(mut self, nonce: u64) -> Self {
        self.nonce = Some(nonce);
        self
    }

    /// Set the expiration timestamp (seconds since epoch)
    pub fn expiration(mut self, expiration: u64) -> Self {
        self.expiration = Some(expiration);
        self
    }

    /// Set the signature type (EOA, POLY_PROXY, or POLY_GNOSIS_SAFE)
    pub fn signature_type(mut self, signature_type: SignatureType) -> Self {
        self.signature_type = signature_type;
        self
    }

    /// Set the funder address for proxy wallets
    pub fn funder(mut self, funder: impl Into<String>) -> Self {
        self.funder = Some(funder.into());
        self
    }

    /// Set whether this is a neg risk order
    pub fn neg_risk(mut self, neg_risk: bool) -> Self {
        self.neg_risk = Some(neg_risk);
        self
    }

    /// Build the order into PartialOrderArgs (without signature)
    ///
    /// This creates an unsigned order that still needs to be signed
    pub fn build_partial(self) -> Result<PartialOrderArgs> {
        let price = self.price.ok_or_else(|| {
            ClobError::InvalidOrder("Price is required for market orders".to_string())
        })?;

        if self.amount <= Decimal::ZERO {
            return Err(ClobError::InvalidOrder(
                "Amount must be positive".to_string(),
            ));
        }

        if price <= Decimal::ZERO || price > Decimal::ONE {
            return Err(ClobError::InvalidOrder(
                "Price must be between 0 and 1".to_string(),
            ));
        }

        // Market order semantics differ from limit orders:
        // BUY: amount is dollars to spend -> maker_amount=dollars, taker_amount=dollars/price (tokens)
        // SELL: amount is tokens to sell -> maker_amount=tokens, taker_amount=tokens*price (dollars)
        //
        // Calculate amounts following Python client's approach (same as limit orders):
        // 1. Calculate in high precision (Decimal)
        // 2. Convert to raw (multiply by 10^6) FIRST
        // 3. Round to integer (done inside to_raw_amount_decimal)
        // Polymarket API constraints:
        //   BUY:  maker_amount (USDC) max 2 decimals, taker_amount (tokens) max 5 decimals
        //   SELL: maker_amount (tokens) max 5 decimals, taker_amount (USDC) max 2 decimals
        let (maker_amount, taker_amount) = match self.side {
            OrderSide::SELL => {
                // amount is number of tokens to sell
                let tokens = self.amount.round_dp(5);
                let usdc = (self.amount * price).round_dp(2);
                (to_raw_amount_decimal(tokens), to_raw_amount_decimal(usdc))
            }
            OrderSide::BUY => {
                // amount is dollars to spend
                let usdc = self.amount.round_dp(2);
                let tokens = (self.amount / price).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(),  // Will be filled by signer
            signer: String::new(), // Will be filled by signer
            taker: 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, // Nonce is separate from salt
            signature_type: self.signature_type,
            expiration: self.expiration.unwrap_or(0),
        })
    }

    /// Build and sign the order
    ///
    /// # Arguments
    /// * `signer` - The signer to use for signing the order
    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()?;

        // Fill in maker and signer addresses
        // For proxy wallets, maker is the funder (proxy wallet address)
        // For EOA wallets, maker is the signer address
        partial.maker = funder.unwrap_or_else(|| signer.address());
        partial.signer = signer.address();

        // Use the shared order signing logic with neg_risk flag
        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_market_order_builder() {
        // BUY market order: amount=100 dollars at price=0.5
        // We GIVE: 100 USDC (maker_amount)
        // We GET: 200 tokens (taker_amount = 100 / 0.5)
        let builder = MarketOrderBuilder::new("123", Decimal::from(100), OrderSide::BUY)
            .price(Decimal::from_str("0.5").unwrap())
            .fee_rate_bps(10)
            .nonce(123456);

        let partial = builder.build_partial().unwrap();
        assert_eq!(partial.token_id, "123");
        assert_eq!(partial.maker_amount, "100000000"); // 100 USDC * 1,000,000
        assert_eq!(partial.taker_amount, "200000000"); // 200 tokens * 1,000,000
        assert_eq!(partial.side, OrderSide::BUY);
        assert_eq!(partial.fee_rate_bps, 10);
    }

    #[test]
    fn test_market_order_sell() {
        // SELL market order: amount=100 tokens at price=0.5
        // We GIVE: 100 tokens (maker_amount)
        // We GET: 50 USDC (taker_amount = 100 * 0.5)
        let builder = MarketOrderBuilder::new("123", Decimal::from(100), OrderSide::SELL)
            .price(Decimal::from_str("0.5").unwrap())
            .fee_rate_bps(10)
            .nonce(123456);

        let partial = builder.build_partial().unwrap();
        assert_eq!(partial.token_id, "123");
        assert_eq!(partial.maker_amount, "100000000"); // 100 tokens * 1,000,000
        assert_eq!(partial.taker_amount, "50000000");  // 50 USDC * 1,000,000
        assert_eq!(partial.side, OrderSide::SELL);
        assert_eq!(partial.fee_rate_bps, 10);
    }

    #[test]
    fn test_invalid_amount() {
        let builder = MarketOrderBuilder::new("123", Decimal::from(-100), OrderSide::BUY).price(Decimal::from_str("0.5").unwrap());
        assert!(builder.build_partial().is_err());
    }

    #[test]
    fn test_invalid_price() {
        let builder = MarketOrderBuilder::new("123", Decimal::from(100), OrderSide::BUY).price(Decimal::from_str("1.5").unwrap());
        assert!(builder.build_partial().is_err());
    }

    #[test]
    fn test_missing_price() {
        let builder = MarketOrderBuilder::new("123", Decimal::from(100), OrderSide::BUY);
        assert!(builder.build_partial().is_err());
    }
}