predict-sdk 0.1.0

Rust SDK for Predict.fun prediction market - order building, EIP-712 signing, and real-time WebSocket data
Documentation
use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// Chain ID for BNB Chain networks
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u64)]
pub enum ChainId {
    BnbMainnet = 56,
    BnbTestnet = 97,
}

impl ChainId {
    pub fn as_u64(&self) -> u64 {
        *self as u64
    }
}

impl TryFrom<u64> for ChainId {
    type Error = crate::Error;

    fn try_from(value: u64) -> Result<Self, Self::Error> {
        match value {
            56 => Ok(ChainId::BnbMainnet),
            97 => Ok(ChainId::BnbTestnet),
            _ => Err(crate::Error::InvalidChainId(value)),
        }
    }
}

/// Order side: BUY or SELL
///
/// Serializes as numeric value (0 = Buy, 1 = Sell) to match the Predict API format.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Side {
    Buy = 0,
    Sell = 1,
}

impl Serialize for Side {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_u8(*self as u8)
    }
}

impl<'de> Deserialize<'de> for Side {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        struct SideVisitor;

        impl<'de> serde::de::Visitor<'de> for SideVisitor {
            type Value = Side;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("0, 1, \"BUY\", or \"SELL\"")
            }

            fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Side, E> {
                match value {
                    0 => Ok(Side::Buy),
                    1 => Ok(Side::Sell),
                    _ => Err(E::invalid_value(serde::de::Unexpected::Unsigned(value), &self)),
                }
            }

            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Side, E> {
                match value {
                    "BUY" | "Buy" | "buy" => Ok(Side::Buy),
                    "SELL" | "Sell" | "sell" => Ok(Side::Sell),
                    _ => Err(E::invalid_value(serde::de::Unexpected::Str(value), &self)),
                }
            }
        }

        deserializer.deserialize_any(SideVisitor)
    }
}

/// Signature type for orders
/// EOA also supports EIP-1271
///
/// Serializes as numeric value (0 = EOA, 1 = PolyProxy, 2 = PolyGnosisSafe) to match the Predict API format.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SignatureType {
    Eoa = 0,
    PolyProxy = 1,
    PolyGnosisSafe = 2,
}

impl Serialize for SignatureType {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_u8(*self as u8)
    }
}

impl<'de> Deserialize<'de> for SignatureType {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        struct SignatureTypeVisitor;

        impl<'de> serde::de::Visitor<'de> for SignatureTypeVisitor {
            type Value = SignatureType;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("0, 1, 2, or a string like \"EOA\"")
            }

            fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<SignatureType, E> {
                match value {
                    0 => Ok(SignatureType::Eoa),
                    1 => Ok(SignatureType::PolyProxy),
                    2 => Ok(SignatureType::PolyGnosisSafe),
                    _ => Err(E::invalid_value(serde::de::Unexpected::Unsigned(value), &self)),
                }
            }

            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<SignatureType, E> {
                match value {
                    "EOA" | "eoa" => Ok(SignatureType::Eoa),
                    "POLY_PROXY" => Ok(SignatureType::PolyProxy),
                    "POLY_GNOSIS_SAFE" => Ok(SignatureType::PolyGnosisSafe),
                    _ => Err(E::invalid_value(serde::de::Unexpected::Str(value), &self)),
                }
            }
        }

        deserializer.deserialize_any(SignatureTypeVisitor)
    }
}

/// Market type indicators
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MarketType {
    pub is_neg_risk: bool,
    pub is_yield_bearing: bool,
}

/// Order structure matching predict.fun specification
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Order {
    /// A unique salt to ensure entropy
    pub salt: String,

    /// The maker of the order, e.g. the order's signer
    pub maker: String,

    /// The signer of the order
    pub signer: String,

    /// The address of the order taker. The zero address is used to indicate a public order
    pub taker: String,

    /// The token ID of the CTF ERC-1155 asset to be bought or sold
    pub token_id: String,

    /// The maker amount
    ///
    /// For a BUY order, this represents the total `(price per asset * assets quantity)` collateral (e.g. USDT) being offered.
    /// For a SELL order, this represents the total amount of CTF assets being offered.
    pub maker_amount: String,

    /// The taker amount
    ///
    /// For a BUY order, this represents the total amount of CTF assets to be received.
    /// For a SELL order, this represents the total `(price per asset * assets quantity)` amount of collateral (e.g. USDT) to be received.
    pub taker_amount: String,

    /// The timestamp in seconds after which the order is expired
    pub expiration: String,

    /// The nonce used for on-chain cancellations
    pub nonce: String,

    /// The fee rate, in basis points
    pub fee_rate_bps: String,

    /// The side of the order, BUY (Bid) or SELL (Ask)
    pub side: Side,

    /// Signature type used by the Order (EOA also supports EIP-1271)
    pub signature_type: SignatureType,
}

/// Signed order with signature and optional hash
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignedOrder {
    #[serde(flatten)]
    pub order: Order,

    /// The order hash
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hash: Option<String>,

    /// The order signature
    pub signature: String,
}

/// Input for building an order
#[derive(Debug, Clone)]
pub struct BuildOrderInput {
    pub side: Side,
    pub token_id: String,
    pub maker_amount: String,
    pub taker_amount: String,
    pub fee_rate_bps: u64,
    pub signer: Option<String>,
    pub nonce: Option<String>,
    pub salt: Option<String>,
    pub maker: Option<String>,
    pub taker: Option<String>,
    pub signature_type: Option<SignatureType>,
    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
}

/// Order strategy type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderStrategy {
    Market,
    Limit,
}

impl OrderStrategy {
    pub fn as_str(&self) -> &'static str {
        match self {
            OrderStrategy::Market => "MARKET",
            OrderStrategy::Limit => "LIMIT",
        }
    }
}

/// Input data for limit order amount calculations
#[derive(Debug, Clone)]
pub struct LimitOrderData {
    pub side: Side,
    /// Price per share in wei (18 decimals)
    pub price_per_share_wei: rust_decimal::Decimal,
    /// Quantity in wei (18 decimals)
    pub quantity_wei: rust_decimal::Decimal,
}

/// Calculated order amounts for limit orders
#[derive(Debug, Clone)]
pub struct LimitOrderAmounts {
    pub last_price: rust_decimal::Decimal,
    pub price_per_share: rust_decimal::Decimal,
    pub maker_amount: rust_decimal::Decimal,
    pub taker_amount: rust_decimal::Decimal,
}

/// Orderbook depth level [price, size]
pub type DepthLevel = (f64, f64);

/// Orderbook structure
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Book {
    pub market_id: u64,
    pub update_timestamp_ms: u64,
    pub asks: Vec<DepthLevel>,
    pub bids: Vec<DepthLevel>,
}

/// Options for canceling orders
#[derive(Debug, Clone)]
pub struct CancelOrdersOptions {
    pub is_yield_bearing: bool,
    pub is_neg_risk: bool,
    pub with_validation: bool,
}

impl Default for CancelOrdersOptions {
    fn default() -> Self {
        Self {
            is_yield_bearing: false,
            is_neg_risk: false,
            with_validation: true,
        }
    }
}

/// Options for redeeming positions
#[derive(Debug, Clone)]
pub struct RedeemPositionsOptions {
    pub condition_id: String,
    pub index_set: u8, // 1 or 2
    pub is_neg_risk: bool,
    pub is_yield_bearing: bool,
    pub amount: Option<rust_decimal::Decimal>,
}

/// Options for merging positions
#[derive(Debug, Clone)]
pub struct MergePositionsOptions {
    pub condition_id: String,
    pub amount: rust_decimal::Decimal,
    pub is_neg_risk: bool,
    pub is_yield_bearing: bool,
}

/// Options for splitting positions
#[derive(Debug, Clone)]
pub struct SplitPositionsOptions {
    pub condition_id: String,
    pub amount: rust_decimal::Decimal,
    pub is_neg_risk: bool,
    pub is_yield_bearing: bool,
}