polymarket-client-sdk 0.4.4

Polymarket CLOB (Central Limit Order Book) API client SDK
Documentation
#![allow(clippy::exhaustive_enums, reason = "Generated by sol! macro")]
#![allow(clippy::exhaustive_structs, reason = "Generated by sol! macro")]
#![allow(clippy::unwrap_used, reason = "Examples use unwrap for brevity")]

//! Token approval example for Polymarket CLOB trading.
//!
//! This example demonstrates how to set the required token allowances for trading on Polymarket.
//! You must approve three contracts:
//!
//! 1. **CTF Exchange** (`config.exchange`) - Standard market trading
//! 2. **Neg Risk CTF Exchange** (`neg_risk_config.exchange`) - Neg-risk market trading
//! 3. **Neg Risk Adapter** (`neg_risk_config.neg_risk_adapter`) - Token minting/splitting for neg-risk
//!
//! Each contract needs two approvals:
//! - ERC-20 approval for USDC (collateral token)
//! - ERC-1155 approval for Conditional Tokens (outcome tokens)
//!
//! You only need to run these approvals once per wallet.
//!
//! Run with tracing enabled:
//! ```sh
//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example approvals --all-features
//! ```
//!
//! Dry run (no transactions executed):
//! ```sh
//! RUST_LOG=info cargo run --example approvals --all-features -- --dry-run
//! ```
//!
//! Optionally log to a file:
//! ```sh
//! LOG_FILE=approvals.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example approvals --all-features
//! ```

use std::env;
use std::fs::File;
use std::str::FromStr as _;

use alloy::primitives::U256;
use alloy::providers::ProviderBuilder;
use alloy::signers::Signer as _;
use alloy::signers::local::LocalSigner;
use alloy::sol;
use polymarket_client_sdk::types::{Address, address};
use polymarket_client_sdk::{POLYGON, PRIVATE_KEY_VAR, contract_config};
use tracing::{error, info};
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;

const RPC_URL: &str = "https://polygon-rpc.com";

const USDC_ADDRESS: Address = address!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174");
const TOKEN_TO_APPROVE: Address = USDC_ADDRESS;

sol! {
    #[sol(rpc)]
    interface IERC20 {
        function approve(address spender, uint256 value) external returns (bool);
        function allowance(address owner, address spender) external view returns (uint256);
    }

    #[sol(rpc)]
    interface IERC1155 {
        function setApprovalForAll(address operator, bool approved) external;
        function isApprovedForAll(address account, address operator) external view returns (bool);
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    if let Ok(path) = env::var("LOG_FILE") {
        let file = File::create(path)?;
        tracing_subscriber::registry()
            .with(EnvFilter::from_default_env())
            .with(
                tracing_subscriber::fmt::layer()
                    .with_writer(file)
                    .with_ansi(false),
            )
            .init();
    } else {
        tracing_subscriber::fmt::init();
    }

    let args: Vec<String> = env::args().collect();
    let dry_run = args.iter().any(|arg| arg == "--dry-run");

    let chain = POLYGON;
    let config = contract_config(chain, false).unwrap();
    let neg_risk_config = contract_config(chain, true).unwrap();

    // Collect all contracts that need approval
    let mut targets: Vec<(&str, Address)> = vec![
        ("CTF Exchange", config.exchange),
        ("Neg Risk CTF Exchange", neg_risk_config.exchange),
    ];

    // Add the Neg Risk Adapter if available
    if let Some(adapter) = neg_risk_config.neg_risk_adapter {
        targets.push(("Neg Risk Adapter", adapter));
    }

    if dry_run {
        info!(mode = "dry_run", "showing approvals without executing");
        for (name, target) in &targets {
            info!(contract = name, address = %target, "would receive approval");
        }
        info!(total = targets.len(), "contracts would be approved");
        return Ok(());
    }

    let private_key = env::var(PRIVATE_KEY_VAR).expect("Need a private key");
    let signer = LocalSigner::from_str(&private_key)?.with_chain_id(Some(chain));

    let provider = ProviderBuilder::new()
        .wallet(signer.clone())
        .connect(RPC_URL)
        .await?;

    let owner = signer.address();
    info!(address = %owner, "wallet loaded");

    let token = IERC20::new(TOKEN_TO_APPROVE, provider.clone());
    let ctf = IERC1155::new(config.conditional_tokens, provider.clone());

    info!(phase = "checking", "querying current allowances");

    for (name, target) in &targets {
        match check_allowance(&token, owner, *target).await {
            Ok(allowance) => info!(contract = name, usdc_allowance = %allowance),
            Err(e) => error!(contract = name, error = ?e, "failed to check USDC allowance"),
        }

        match check_approval_for_all(&ctf, owner, *target).await {
            Ok(approved) => info!(contract = name, ctf_approved = approved),
            Err(e) => error!(contract = name, error = ?e, "failed to check CTF approval"),
        }
    }

    info!(phase = "approving", "setting approvals");

    for (name, target) in &targets {
        info!(contract = name, address = %target, "approving");

        match approve(&token, *target, U256::MAX).await {
            Ok(tx_hash) => info!(contract = name, tx = %tx_hash, "USDC approved"),
            Err(e) => error!(contract = name, error = ?e, "USDC approve failed"),
        }

        match set_approval_for_all(&ctf, *target, true).await {
            Ok(tx_hash) => info!(contract = name, tx = %tx_hash, "CTF approved"),
            Err(e) => error!(contract = name, error = ?e, "CTF setApprovalForAll failed"),
        }
    }

    info!(phase = "verifying", "confirming approvals");

    for (name, target) in &targets {
        match check_allowance(&token, owner, *target).await {
            Ok(allowance) => info!(contract = name, usdc_allowance = %allowance, "verified"),
            Err(e) => error!(contract = name, error = ?e, "verification failed"),
        }

        match check_approval_for_all(&ctf, owner, *target).await {
            Ok(approved) => info!(contract = name, ctf_approved = approved, "verified"),
            Err(e) => error!(contract = name, error = ?e, "verification failed"),
        }
    }

    info!("all approvals complete");

    Ok(())
}

async fn check_allowance<P: alloy::providers::Provider>(
    token: &IERC20::IERC20Instance<P>,
    owner: Address,
    spender: Address,
) -> anyhow::Result<U256> {
    let allowance = token.allowance(owner, spender).call().await?;
    Ok(allowance)
}

async fn check_approval_for_all<P: alloy::providers::Provider>(
    ctf: &IERC1155::IERC1155Instance<P>,
    account: Address,
    operator: Address,
) -> anyhow::Result<bool> {
    let approved = ctf.isApprovedForAll(account, operator).call().await?;
    Ok(approved)
}

async fn approve<P: alloy::providers::Provider>(
    usdc: &IERC20::IERC20Instance<P>,
    spender: Address,
    amount: U256,
) -> anyhow::Result<alloy::primitives::FixedBytes<32>> {
    let tx_hash = usdc.approve(spender, amount).send().await?.watch().await?;
    Ok(tx_hash)
}

async fn set_approval_for_all<P: alloy::providers::Provider>(
    ctf: &IERC1155::IERC1155Instance<P>,
    operator: Address,
    approved: bool,
) -> anyhow::Result<alloy::primitives::FixedBytes<32>> {
    let tx_hash = ctf
        .setApprovalForAll(operator, approved)
        .send()
        .await?
        .watch()
        .await?;
    Ok(tx_hash)
}