polymarket 0.1.0

Rust SDK for Polymarket prediction market - CLOB trading, on-chain operations, and WebSocket streaming
Documentation
//! Builder pattern for constructing OnchainClient instances

use alloy::primitives::Address;

use crate::onchain::{NetworkConfig, OnchainError, Result};

use super::client::OnchainClient;
use super::provider::OnchainProvider;
use super::signer::OnchainSigner;

/// Builder for creating OnchainClient instances with fluent API
///
/// # Examples
///
/// ## EOA (Externally Owned Account)
/// ```no_run
/// # use polymarket_sdk::onchain::OnchainClientBuilder;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = OnchainClientBuilder::new()
///     .private_key("0x1234...")?
///     .build()?;
/// # Ok(())
/// # }
/// ```
///
/// ## Proxy Wallet
/// ```no_run
/// # use polymarket_sdk::onchain::OnchainClientBuilder;
/// # use alloy::primitives::Address;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = OnchainClientBuilder::new()
///     .private_key("0x1234...")?
///     .proxy_wallet(Address::repeat_byte(0x42))?
///     .build()?;
/// # Ok(())
/// # }
/// ```
///
/// ## Safe Wallet
/// ```no_run
/// # use polymarket_sdk::onchain::OnchainClientBuilder;
/// # use alloy::primitives::Address;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = OnchainClientBuilder::new()
///     .private_key("0x1234...")?
///     .safe_wallet(Address::repeat_byte(0x99), Some(2))?
///     .build()?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug)]
pub struct OnchainClientBuilder {
    network: NetworkConfig,
    signer: Option<OnchainSigner>,
}

impl OnchainClientBuilder {
    /// Create a new builder with Polygon mainnet as default
    pub fn new() -> Self {
        Self {
            network: NetworkConfig::polygon_mainnet(),
            signer: None,
        }
    }

    /// Set the network configuration (mainnet or testnet)
    pub fn network(mut self, network: NetworkConfig) -> Self {
        self.network = network;
        self
    }

    /// Use Polygon mainnet
    pub fn mainnet(mut self) -> Self {
        self.network = NetworkConfig::polygon_mainnet();
        self
    }

    /// Use Polygon Amoy testnet
    pub fn testnet(mut self) -> Self {
        self.network = NetworkConfig::polygon_amoy();
        self
    }

    /// Initialize with a private key (EOA - Externally Owned Account)
    ///
    /// This creates a basic EOA signer. Can be chained with `.proxy_wallet()` or
    /// `.safe_wallet()` to upgrade to a smart wallet.
    ///
    /// # Example
    /// ```no_run
    /// # use polymarket_sdk::onchain::OnchainClientBuilder;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// // EOA only
    /// let client = OnchainClientBuilder::new()
    ///     .private_key("0x1234...")?.build()?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn private_key(mut self, private_key: impl AsRef<str>) -> Result<Self> {
        self.signer = Some(OnchainSigner::from_private_key(private_key)?);
        Ok(self)
    }

    /// Upgrade to a proxy wallet (chainable after `private_key()`)
    ///
    /// Converts the EOA signer into a proxy wallet signer. Must be called after
    /// `private_key()` to set the controlling EOA.
    ///
    /// # Arguments
    /// * `proxy_address` - The proxy wallet address (derived from salt)
    ///
    /// # Example
    /// ```no_run
    /// # use polymarket_sdk::onchain::OnchainClientBuilder;
    /// # use alloy::primitives::Address;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = OnchainClientBuilder::new()
    ///     .private_key("0x1234...")?
    ///     .proxy_wallet(Address::repeat_byte(0x42))?
    ///     .build()?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn proxy_wallet(mut self, proxy_address: Address) -> Result<Self> {
        // Get the private key from the existing EOA signer
        let private_key_signer = self
            .signer
            .as_ref()
            .ok_or_else(|| {
                OnchainError::ConfigurationError(
                    "Must call .private_key() before .proxy_wallet()".to_string(),
                )
            })?
            .private_key_signer()
            .clone();

        // Create a new proxy signer using the same private key
        self.signer = Some(OnchainSigner::Proxy {
            signer: private_key_signer,
            proxy_address,
        });

        Ok(self)
    }

    /// Upgrade to a Safe wallet (chainable after `private_key()`)
    ///
    /// Converts the EOA signer into a Safe wallet signer. Must be called after
    /// `private_key()` to set the controlling EOA.
    ///
    /// # Arguments
    /// * `safe_address` - The Safe wallet address
    /// * `threshold` - Number of required signatures (default: 1)
    ///
    /// # Example
    /// ```no_run
    /// # use polymarket_sdk::onchain::OnchainClientBuilder;
    /// # use alloy::primitives::Address;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = OnchainClientBuilder::new()
    ///     .private_key("0x1234...")?
    ///     .safe_wallet(Address::repeat_byte(0x99), Some(2))?
    ///     .build()?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn safe_wallet(
        mut self,
        safe_address: Address,
        threshold: Option<u64>,
    ) -> Result<Self> {
        // Get the private key from the existing EOA signer
        let private_key_signer = self
            .signer
            .as_ref()
            .ok_or_else(|| {
                OnchainError::ConfigurationError(
                    "Must call .private_key() before .safe_wallet()".to_string(),
                )
            })?
            .private_key_signer()
            .clone();

        // Create a new Safe signer using the same private key
        self.signer = Some(OnchainSigner::Safe {
            signer: private_key_signer,
            safe_address,
            threshold: threshold.unwrap_or(1),
        });

        Ok(self)
    }

    /// Set a pre-configured signer
    pub fn signer(mut self, signer: OnchainSigner) -> Self {
        self.signer = Some(signer);
        self
    }

    /// Build the OnchainClient
    ///
    /// This will:
    /// 1. Create the provider from the network configuration
    /// 2. Verify the signer is set
    /// 3. Construct the OnchainClient
    pub fn build(self) -> Result<OnchainClient> {
        // Create provider from network
        let provider = OnchainProvider::new(&self.network)?;

        // Verify signer
        let signer = self.signer.ok_or_else(|| {
            OnchainError::ConfigurationError("Must specify a signer".to_string())
        })?;

        Ok(OnchainClient::new(provider, signer))
    }
}

impl Default for OnchainClientBuilder {
    fn default() -> Self {
        Self::new()
    }
}

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

    const TEST_PRIVATE_KEY: &str =
        "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";

    #[test]
    fn test_builder_new() {
        let builder = OnchainClientBuilder::new();
        assert_eq!(builder.network.chain_id, 137); // Defaults to mainnet
        assert!(builder.signer.is_none());
    }

    #[test]
    fn test_builder_mainnet() {
        let builder = OnchainClientBuilder::new().mainnet();
        assert_eq!(builder.network.chain_id, 137);
    }

    #[test]
    fn test_builder_testnet() {
        let builder = OnchainClientBuilder::new().testnet();
        assert_eq!(builder.network.chain_id, 80002);
    }

    #[test]
    fn test_builder_private_key() {
        let builder = OnchainClientBuilder::new()
            .private_key(TEST_PRIVATE_KEY)
            .unwrap();
        assert!(builder.signer.is_some());
    }

    #[test]
    fn test_builder_proxy_wallet() {
        let proxy_address = Address::repeat_byte(0x42);
        let builder = OnchainClientBuilder::new()
            .private_key(TEST_PRIVATE_KEY)
            .unwrap()
            .proxy_wallet(proxy_address)
            .unwrap();
        assert!(builder.signer.is_some());

        // Verify it's a proxy wallet
        if let Some(signer) = &builder.signer {
            assert!(signer.is_smart_wallet());
            assert_eq!(signer.proxy_address(), Some(proxy_address));
        }
    }

    #[test]
    fn test_builder_safe_wallet() {
        let safe_address = Address::repeat_byte(0x99);
        let builder = OnchainClientBuilder::new()
            .private_key(TEST_PRIVATE_KEY)
            .unwrap()
            .safe_wallet(safe_address, Some(2))
            .unwrap();
        assert!(builder.signer.is_some());

        // Verify it's a Safe wallet
        if let Some(signer) = &builder.signer {
            assert!(signer.is_smart_wallet());
            assert_eq!(signer.safe_address(), Some(safe_address));
            assert_eq!(signer.safe_threshold(), Some(2));
        }
    }

    #[test]
    fn test_builder_proxy_without_private_key() {
        let proxy_address = Address::repeat_byte(0x42);
        let result = OnchainClientBuilder::new().proxy_wallet(proxy_address);

        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .to_string()
            .contains("Must call .private_key() before .proxy_wallet()"));
    }

    #[test]
    fn test_builder_safe_without_private_key() {
        let safe_address = Address::repeat_byte(0x99);
        let result = OnchainClientBuilder::new().safe_wallet(safe_address, Some(2));

        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .to_string()
            .contains("Must call .private_key() before .safe_wallet()"));
    }

    #[test]
    fn test_builder_missing_signer() {
        let result = OnchainClientBuilder::new().mainnet().build();

        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .to_string()
            .contains("Must specify a signer"));
    }
}