polymarket 0.1.0

Rust SDK for Polymarket prediction market - CLOB trading, on-chain operations, and WebSocket streaming
Documentation
//! Relayer client for submitting gasless transactions

use reqwest::Client;

use super::types::*;

/// Client for interacting with the Polymarket relayer service
#[derive(Clone, Debug)]
pub struct RelayerClient {
    base_url: String,
    client: Client,
}

impl RelayerClient {
    /// Create a new relayer client
    ///
    /// # Arguments
    /// * `base_url` - Base URL of the relayer service (e.g., "https://relayer-v2.polymarket.com")
    pub fn new(base_url: impl Into<String>) -> Self {
        Self {
            base_url: base_url.into(),
            client: Client::new(),
        }
    }

    /// Create a relayer client for mainnet
    pub fn mainnet() -> Self {
        Self::new("https://relayer-v2.polymarket.com")
    }

    /// Get relay payload (relayer address and nonce)
    ///
    /// # Arguments
    /// * `proxy_wallet` - The proxy wallet address
    /// * `tx_type` - Transaction type (usually "PROXY")
    pub async fn get_relay_payload(
        &self,
        proxy_wallet: &str,
        tx_type: &str,
    ) -> Result<RelayPayload, Box<dyn std::error::Error>> {
        let url = format!("{}/relay-payload", self.base_url);

        let response = self
            .client
            .get(&url)
            .query(&[("address", proxy_wallet), ("type", tx_type)])
            .send()
            .await?;

        // Log response status and body for debugging
        let status = response.status();
        let body_text = response.text().await?;

        if !status.is_success() {
            eprintln!("Relayer error (get_relay_payload): Status {}, Body: {}", status, body_text);
            return Err(format!("Relayer returned status {}: {}", status, body_text).into());
        }

        serde_json::from_str(&body_text).map_err(|e| {
            eprintln!("Failed to parse relay payload. Body: {}", body_text);
            eprintln!("Parse error: {}", e);
            e.into()
        })
    }

    /// Submit a transaction to the relayer
    ///
    /// # Arguments
    /// * `request` - The relay request containing transaction data and signature
    pub async fn submit(
        &self,
        request: RelayerSubmitRequest,
    ) -> Result<RelayerSubmitResponse, Box<dyn std::error::Error>> {
        let url = format!("{}/submit", self.base_url);

        let response = self
            .client
            .post(&url)
            .json(&request)
            .send()
            .await?;

        // Log response status and body for debugging
        let status = response.status();
        let body_text = response.text().await?;

        if !status.is_success() {
            eprintln!("Relayer error (submit): Status {}, Body: {}", status, body_text);
            return Err(format!("Relayer returned status {}: {}", status, body_text).into());
        }

        serde_json::from_str(&body_text).map_err(|e| {
            eprintln!("Failed to parse submit response. Body: {}", body_text);
            eprintln!("Parse error: {}", e);
            e.into()
        })
    }

    /// Get transaction status
    ///
    /// # Arguments
    /// * `transaction_id` - The transaction ID returned from submit()
    pub async fn get_transaction(
        &self,
        transaction_id: &str,
    ) -> Result<Vec<RelayerTransaction>, reqwest::Error> {
        let url = format!("{}/transaction", self.base_url);

        let response = self
            .client
            .get(&url)
            .query(&[("id", transaction_id)])
            .send()
            .await?;

        response.json().await
    }

    /// Wait for transaction to be executed
    ///
    /// Polls the relayer until the transaction is executed or fails.
    ///
    /// # Arguments
    /// * `transaction_id` - The transaction ID
    /// * `max_attempts` - Maximum number of polling attempts (default: 60)
    /// * `poll_interval_ms` - Milliseconds between polls (default: 1000)
    pub async fn wait_for_execution(
        &self,
        transaction_id: &str,
        max_attempts: Option<u32>,
        poll_interval_ms: Option<u64>,
    ) -> Result<RelayerTransaction, Box<dyn std::error::Error>> {
        let max_attempts = max_attempts.unwrap_or(60);
        let poll_interval = std::time::Duration::from_millis(poll_interval_ms.unwrap_or(1000));

        for attempt in 0..max_attempts {
            let transactions = self.get_transaction(transaction_id).await?;

            if let Some(tx) = transactions.first() {
                match tx.state.as_str() {
                    "STATE_EXECUTED" => return Ok(tx.clone()),
                    "STATE_FAILED" => {
                        return Err(format!("Transaction failed: {}", tx.state).into());
                    }
                    _ => {
                        // STATE_NEW, STATE_PENDING, etc. - keep polling
                        if attempt < max_attempts - 1 {
                            tokio::time::sleep(poll_interval).await;
                        }
                    }
                }
            } else {
                return Err("Transaction not found".into());
            }
        }

        Err("Transaction did not complete within timeout".into())
    }
}

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

    #[test]
    fn test_relayer_client_creation() {
        let client = RelayerClient::mainnet();
        assert_eq!(client.base_url, "https://relayer-v2.polymarket.com");
    }

    #[test]
    fn test_custom_relayer_url() {
        let client = RelayerClient::new("https://custom-relayer.example.com");
        assert_eq!(client.base_url, "https://custom-relayer.example.com");
    }
}