polymarket 0.1.0

Rust SDK for Polymarket prediction market - CLOB trading, on-chain operations, and WebSocket streaming
Documentation
use alloy::primitives::hex::encode_prefixed;
use alloy::signers::local::PrivateKeySigner;
use alloy_sol_types::{eip712_domain, Eip712Domain};
use alloy::signers::Signer as AlloySigner;

use crate::clob::{ClobError, Result};

use super::model::{CLOB_DOMAIN_NAME, CLOB_VERSION, MSG_TO_SIGN};

// Define the EIP-712 struct using alloy's sol! macro
// IMPORTANT: Struct name must match Python implementation exactly: "ClobAuth"
// The EIP-712 type hash includes the struct name, so "ClobAuth" != "ClobAuthMessage"
alloy::sol! {
    #[derive(Debug)]
    struct ClobAuth {
        address address;
        string timestamp;
        uint256 nonce;
        string message;
    }
}

/// Get the CLOB auth EIP-712 domain for a given chain ID
pub fn get_clob_auth_domain(chain_id: u64) -> Eip712Domain {
    eip712_domain! {
        name: CLOB_DOMAIN_NAME,
        version: CLOB_VERSION,
        chain_id: chain_id,
    }
}

/// Sign a CLOB authentication message using EIP-712
///
/// # Arguments
/// * `signer` - The private key signer
/// * `chain_id` - The chain ID
/// * `timestamp` - Unix timestamp
/// * `nonce` - Nonce value
///
/// # Returns
/// Hex-encoded signature with "0x" prefix
pub async fn sign_clob_auth_message(
    signer: &PrivateKeySigner,
    chain_id: u64,
    timestamp: u64,
    nonce: u64,
) -> Result<String> {
    // Create the message
    let message = ClobAuth {
        address: signer.address(),
        timestamp: timestamp.to_string(),
        nonce: alloy::primitives::U256::from(nonce),
        message: MSG_TO_SIGN.to_string(),
    };

    // Get the domain
    let domain = get_clob_auth_domain(chain_id);

    // Use sign_typed_data like polymarket-rs-client (async version)
    let signature = signer
        .sign_typed_data(&message, &domain)
        .await
        .map_err(|e| ClobError::SigningError(e.to_string()))?;

    // Return hex-encoded signature with 0x prefix (using encode_prefixed)
    Ok(encode_prefixed(signature.as_bytes()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::str::FromStr;

    #[tokio::test]
    async fn test_clob_auth_signing() {
        // Test private key (DO NOT use in production!)
        let private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
        let signer = PrivateKeySigner::from_str(private_key).unwrap();

        let timestamp = 1234567890u64;
        let nonce = 1u64;
        let chain_id = 137u64;

        let signature = sign_clob_auth_message(&signer, chain_id, timestamp, nonce).await;

        assert!(signature.is_ok());
        let sig = signature.unwrap();
        assert!(sig.starts_with("0x"));
        assert_eq!(sig.len(), 132); // 0x + 130 hex chars (65 bytes * 2)
    }

    #[tokio::test]
    async fn test_clob_auth_signature_matches_python() {
        // Publicly known private key - same as Python test
        let private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
        let signer = PrivateKeySigner::from_str(private_key).unwrap();

        // Amoy testnet chain ID (80002)
        let chain_id = 80002u64;
        let timestamp = 10000000u64;
        let nonce = 23u64;

        // Sign the message
        let signature = sign_clob_auth_message(&signer, chain_id, timestamp, nonce)
            .await
            .unwrap();

        // Expected signature from Python test
        let expected_sig = "0xf62319a987514da40e57e2f4d7529f7bac38f0355bd88bb5adbb3768d80de6c1682518e0af677d5260366425f4361e7b70c25ae232aff0ab2331e2b164a1aedc1b";

        println!("Rust signature:   {}", signature);
        println!("Python signature: {}", expected_sig);

        // Signatures should match exactly
        assert_eq!(
            signature.to_lowercase(),
            expected_sig.to_lowercase(),
            "Rust signature should match Python signature exactly"
        );
    }
}