predict-sdk 0.1.0

Rust SDK for Predict.fun prediction market - order building, EIP-712 signing, and real-time WebSocket data
Documentation
use crate::{Error, Result};
use crate::types::{LimitOrderAmounts, LimitOrderData, Side};
use rust_decimal::Decimal;

/// Minimum order size: 1e16 (0.01 tokens)
const MIN_ORDER_SIZE: i64 = 10_000_000_000_000_000; // 1e16

/// Standard precision for orders (18 decimals for wei)
const PRECISION: i64 = 1_000_000_000_000_000_000; // 1e18

/// Retain the specified number of significant digits for a Decimal
///
/// This matches the TypeScript SDK's `retainSignificantDigits` function
pub fn retain_significant_digits(value: Decimal, sig_figs: u32) -> Decimal {
    if value.is_zero() {
        return Decimal::ZERO;
    }

    let is_negative = value.is_sign_negative();
    let abs_value = value.abs().normalize();

    // Convert to string to find magnitude
    let str_value = abs_value.to_string();

    // Remove decimal point for counting digits
    let digits_only: String = str_value.chars().filter(|c| c.is_ascii_digit()).collect();
    let magnitude = digits_only.len();

    // Calculate excess digits
    let excess = magnitude as i32 - sig_figs as i32;

    if excess <= 0 {
        return value; // No truncation needed
    }

    // Calculate divisor to remove excess digits
    // Use iterative multiplication to avoid i64::pow overflow for large excess values
    let mut divisor = Decimal::ONE;
    for _ in 0..excess {
        divisor *= Decimal::TEN;
    }

    // Divide then multiply to truncate
    let truncated = (abs_value / divisor).floor() * divisor;

    if is_negative {
        -truncated
    } else {
        truncated
    }
}

/// Calculate order amounts for a limit order
///
/// This function matches the TypeScript SDK's `getLimitOrderAmounts` method
///
/// # Arguments
///
/// * `data` - The limit order data containing side, price, and quantity
///
/// # Returns
///
/// The calculated order amounts including maker/taker amounts and price per share
///
/// # Errors
///
/// Returns an error if the quantity is less than the minimum order size (1e16)
pub fn get_limit_order_amounts(data: LimitOrderData) -> Result<LimitOrderAmounts> {
    let min_size = Decimal::from(MIN_ORDER_SIZE);
    let precision = Decimal::from(PRECISION);

    // Validate quantity
    if data.quantity_wei < min_size {
        return Err(Error::InvalidQuantity(format!(
            "Quantity {} is less than minimum {}",
            data.quantity_wei, min_size
        )));
    }

    // Truncate to 3 significant digits for price, and 5 for quantity
    // This helps avoid precision loss when calculating the amounts
    let price = retain_significant_digits(data.price_per_share_wei, 3);
    let qty = retain_significant_digits(data.quantity_wei, 5);

    // Calculate amounts based on side
    let (maker_amount, taker_amount) = match data.side {
        Side::Buy => {
            // BUY: maker gives USDT (collateral), taker gives outcome tokens
            // Use checked_mul to avoid overflow, or divide first
            let maker_amount = price.checked_mul(qty)
                .map(|result| result / precision)
                .unwrap_or_else(|| {
                    // If overflow, divide first then multiply
                    (price / precision) * qty
                });
            let taker_amount = qty;
            (maker_amount, taker_amount)
        }
        Side::Sell => {
            // SELL: maker gives outcome tokens, taker gives USDT (collateral)
            let maker_amount = qty;
            let taker_amount = price.checked_mul(qty)
                .map(|result| result / precision)
                .unwrap_or_else(|| {
                    // If overflow, divide first then multiply
                    (price / precision) * qty
                });
            (maker_amount, taker_amount)
        }
    };

    Ok(LimitOrderAmounts {
        last_price: price,
        price_per_share: price,
        maker_amount,
        taker_amount,
    })
}

// Tests are in integration tests