polymarket 0.1.0

Rust SDK for Polymarket prediction market - CLOB trading, on-chain operations, and WebSocket streaming
Documentation
//! ERC20 token operations (USDC and other tokens)

use alloy::network::TransactionBuilder;
use alloy::primitives::{Address, U256};
use alloy::providers::{Provider, ProviderBuilder};

use crate::onchain::{
    contracts::IERC20,
    wallet::OnchainClient,
    OnchainError, Result, TransactionOptions,
};

/// ERC20 token operations
impl OnchainClient {
    /// Get ERC20 token balance for the wallet
    ///
    /// # Example
    /// ```no_run
    /// # use polymarket_sdk::onchain::OnchainClientBuilder;
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = OnchainClientBuilder::new()
    ///     .mainnet()
    ///     .private_key("0x1234...")?
    ///     .build()?;
    ///
    /// let balance = client.get_token_balance(client.usdc_address()).await?;
    /// println!("Balance: {}", balance);
    /// # Ok(())
    /// # }
    /// ```
    pub async fn get_token_balance(&self, token: Address) -> Result<U256> {
        self.get_token_balance_of(token, self.address()).await
    }

    /// Get ERC20 token balance for a specific address
    ///
    /// # Arguments
    /// * `token` - The ERC20 token contract address
    /// * `owner` - The address to check balance for
    pub async fn get_token_balance_of(&self, token: Address, owner: Address) -> Result<U256> {
        let contract = IERC20::new(token, self.provider().provider());

        let balance = contract
            .balanceOf(owner)
            .call()
            .await
            .map_err(|e| OnchainError::ContractError(format!("Failed to get balance: {}", e)))?;

        // In Alloy 1.0, balanceOf returns U256 directly
        Ok(balance)
    }

    /// Get ERC20 token allowance
    ///
    /// Returns how much `spender` is allowed to spend on behalf of `owner`
    ///
    /// # Arguments
    /// * `token` - The ERC20 token contract address
    /// * `owner` - The token owner address
    /// * `spender` - The spender address
    pub async fn get_token_allowance(
        &self,
        token: Address,
        owner: Address,
        spender: Address,
    ) -> Result<U256> {
        let contract = IERC20::new(token, self.provider().provider());

        let allowance = contract
            .allowance(owner, spender)
            .call()
            .await
            .map_err(|e| OnchainError::ContractError(format!("Failed to get allowance: {}", e)))?;

        // In Alloy 1.0, allowance returns U256 directly
        Ok(allowance)
    }

    /// Check if a token is approved for a spender
    ///
    /// Returns true if `spender` has sufficient allowance (>= amount)
    ///
    /// # Arguments
    /// * `token` - The ERC20 token contract address
    /// * `spender` - The spender address
    /// * `amount` - The required amount
    pub async fn is_token_approved(
        &self,
        token: Address,
        spender: Address,
        amount: U256,
    ) -> Result<bool> {
        let allowance = self.get_token_allowance(token, self.address(), spender).await?;
        Ok(allowance >= amount)
    }

    /// Approve ERC20 token spending
    ///
    /// Grants `spender` permission to spend `amount` tokens on behalf of the wallet
    ///
    /// # Arguments
    /// * `token` - The ERC20 token contract address
    /// * `spender` - The spender address (e.g., CTF contract)
    /// * `amount` - The amount to approve
    /// * `options` - Transaction options (gas, confirmations, etc.)
    ///
    /// # Example
    /// ```no_run
    /// # use polymarket_sdk::onchain::OnchainClientBuilder;
    /// # use alloy::primitives::U256;
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = OnchainClientBuilder::new()
    ///     .mainnet()
    ///     .private_key("0x1234...")?
    ///     .build()?;
    ///
    /// // Approve CTF contract to spend USDC
    /// let amount = U256::from(1_000_000); // 1 USDC (6 decimals)
    /// client.approve_token(
    ///     client.usdc_address(),
    ///     client.ctf_address(),
    ///     amount,
    ///     None,
    /// ).await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn approve_token(
        &self,
        token: Address,
        spender: Address,
        amount: U256,
        options: Option<TransactionOptions>,
    ) -> Result<alloy::primitives::TxHash> {
        let wallet = self.ethereum_wallet();

        // Build the provider with wallet
        let url = self.network().rpc_url.parse().map_err(|e| {
            OnchainError::NetworkError(format!("Invalid RPC URL: {}", e))
        })?;

        let provider = ProviderBuilder::new()
            .wallet(wallet)
            .connect_http(url);

        let contract = IERC20::new(token, &provider);

        let call = contract.approve(spender, amount);

        // Apply transaction options if provided
        let tx = if let Some(opts) = options {
            let mut builder = call.into_transaction_request();

            if let Some(gas_price) = opts.gas_price {
                builder = builder.with_gas_price(gas_price.to::<u128>());
            }

            if let Some(gas_limit) = opts.gas_limit {
                builder = builder.with_gas_limit(gas_limit.to::<u64>());
            }

            // Send the transaction
            let pending = provider
                .send_transaction(builder)
                .await
                .map_err(|e| OnchainError::TransactionFailed(format!("Failed to send: {}", e)))?;

            if opts.wait_for_confirmation {
                let receipt = pending
                    .with_required_confirmations(opts.confirmations.unwrap_or(1))
                    .get_receipt()
                    .await
                    .map_err(|e| {
                        OnchainError::TransactionFailed(format!("Failed to confirm: {}", e))
                    })?;

                receipt.transaction_hash
            } else {
                *pending.tx_hash()
            }
        } else {
            // Send without custom options
            let pending = call
                .send()
                .await
                .map_err(|e| OnchainError::TransactionFailed(format!("Failed to send: {}", e)))?;

            *pending.tx_hash()
        };

        Ok(tx)
    }

    /// Approve maximum amount for a spender (2^256 - 1)
    ///
    /// This is a convenience method that approves the maximum possible amount,
    /// avoiding the need for multiple approval transactions.
    ///
    /// # Arguments
    /// * `token` - The ERC20 token contract address
    /// * `spender` - The spender address
    /// * `options` - Transaction options (gas, confirmations, etc.)
    pub async fn approve_token_max(
        &self,
        token: Address,
        spender: Address,
        options: Option<TransactionOptions>,
    ) -> Result<alloy::primitives::TxHash> {
        self.approve_token(token, spender, U256::MAX, options).await
    }

    /// Transfer ERC20 tokens to another address
    ///
    /// # Arguments
    /// * `token` - The ERC20 token contract address
    /// * `to` - The recipient address
    /// * `amount` - The amount to transfer
    /// * `options` - Transaction options (gas, confirmations, etc.)
    pub async fn transfer_token(
        &self,
        token: Address,
        to: Address,
        amount: U256,
        options: Option<TransactionOptions>,
    ) -> Result<alloy::primitives::TxHash> {
        let wallet = self.ethereum_wallet();

        // Build the provider with wallet
        let url = self.network().rpc_url.parse().map_err(|e| {
            OnchainError::NetworkError(format!("Invalid RPC URL: {}", e))
        })?;

        let provider = ProviderBuilder::new()
            .wallet(wallet)
            .connect_http(url);

        let contract = IERC20::new(token, &provider);

        let call = contract.transfer(to, amount);

        // Apply transaction options if provided
        let tx = if let Some(opts) = options {
            let mut builder = call.into_transaction_request();

            if let Some(gas_price) = opts.gas_price {
                builder = builder.with_gas_price(gas_price.to::<u128>());
            }

            if let Some(gas_limit) = opts.gas_limit {
                builder = builder.with_gas_limit(gas_limit.to::<u64>());
            }

            // Send the transaction
            let pending = provider
                .send_transaction(builder)
                .await
                .map_err(|e| OnchainError::TransactionFailed(format!("Failed to send: {}", e)))?;

            if opts.wait_for_confirmation {
                let receipt = pending
                    .with_required_confirmations(opts.confirmations.unwrap_or(1))
                    .get_receipt()
                    .await
                    .map_err(|e| {
                        OnchainError::TransactionFailed(format!("Failed to confirm: {}", e))
                    })?;

                receipt.transaction_hash
            } else {
                *pending.tx_hash()
            }
        } else {
            // Send without custom options
            let pending = call
                .send()
                .await
                .map_err(|e| OnchainError::TransactionFailed(format!("Failed to send: {}", e)))?;

            *pending.tx_hash()
        };

        Ok(tx)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::onchain::{NetworkConfig, OnchainProvider, OnchainSigner};
    use alloy::network::AnyNetwork;
    use alloy::providers::ProviderBuilder;

    const TEST_PRIVATE_KEY: &str =
        "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";

    fn create_test_client() -> OnchainClient {
        let network = NetworkConfig::polygon_mainnet();
        let url = network.rpc_url.parse().unwrap();
        let provider = OnchainProvider {
            provider: ProviderBuilder::new()
                .network::<AnyNetwork>()
                .connect_http(url)
                .erased(),
            network: network.clone(),
        };
        let signer = OnchainSigner::from_private_key(TEST_PRIVATE_KEY).unwrap();

        OnchainClient::new(provider, signer)
    }

    #[test]
    fn test_token_methods_exist() {
        let client = create_test_client();

        // Just verify the methods are available
        assert_eq!(client.usdc_address(), client.addresses().usdc);
        assert_eq!(client.ctf_address(), client.addresses().ctf);
    }
}