From 083a87bec115803301d2199a6f90359f157c1ac8 Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Mon, 6 Apr 2026 01:58:33 +0530 Subject: [PATCH 1/6] fix: route CTF operations through proxy wallet factory --- Cargo.lock | 13 +++ Cargo.toml | 2 +- src/commands/ctf.rs | 240 +++++++++++++++++++++++++++++++------------- src/main.rs | 8 +- 4 files changed, 190 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26a3032..1bdc832 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,7 @@ dependencies = [ "alloy-network", "alloy-provider", "alloy-rpc-client", + "alloy-rpc-types", "alloy-serde", "alloy-signer", "alloy-signer-local", @@ -403,6 +404,18 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "alloy-rpc-types" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bdcbf9dfd5eea8bfeb078b1d906da8cd3a39c4d4dbe7a628025648e323611f6" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + [[package]] name = "alloy-rpc-types-any" version = "1.7.3" diff --git a/Cargo.toml b/Cargo.toml index a01bf05..2707e4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ path = "src/main.rs" [dependencies] polymarket-client-sdk = { version = "0.4", features = ["gamma", "data", "bridge", "clob", "ctf"] } -alloy = { version = "1.6.3", default-features = false, features = ["providers", "sol-types", "contract", "reqwest", "reqwest-rustls-tls", "signer-local", "signers"] } +alloy = { version = "1.6.3", default-features = false, features = ["providers", "rpc-types", "sol-types", "contract", "reqwest", "reqwest-rustls-tls", "signer-local", "signers"] } clap = { version = "4", features = ["derive"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } serde_json = "1" diff --git a/src/commands/ctf.rs b/src/commands/ctf.rs index 5d06ea5..9426c7e 100644 --- a/src/commands/ctf.rs +++ b/src/commands/ctf.rs @@ -1,10 +1,10 @@ use alloy::primitives::U256; +use alloy::providers::Provider as _; +use alloy::sol; +use alloy::sol_types::SolCall; use anyhow::{Context, Result}; use clap::{Args, Subcommand}; -use polymarket_client_sdk::ctf::types::{ - CollectionIdRequest, ConditionIdRequest, MergePositionsRequest, PositionIdRequest, - RedeemNegRiskRequest, RedeemPositionsRequest, SplitPositionRequest, -}; +use polymarket_client_sdk::ctf::types::{CollectionIdRequest, ConditionIdRequest, PositionIdRequest}; use polymarket_client_sdk::types::{Address, B256}; use polymarket_client_sdk::{POLYGON, ctf}; use rust_decimal::Decimal; @@ -15,6 +15,56 @@ use crate::output::ctf as ctf_output; use super::{USDC_ADDRESS_STR, USDC_DECIMALS}; +// Polymarket Proxy Wallet Factory interface (CallType: INVALID=0, CALL=1, DELEGATECALL=2) +sol! { + #[sol(rpc)] + interface IProxyWallet { + struct ProxyCall { + uint8 typeCode; + address to; + uint256 value; + bytes data; + } + + function proxy(ProxyCall[] memory calls) + external payable returns (bytes[] memory returnValues); + } +} + +sol! { + interface IConditionalTokens { + function splitPosition( + address collateralToken, + bytes32 parentCollectionId, + bytes32 conditionId, + uint256[] calldata partition, + uint256 amount + ) external; + + function mergePositions( + address collateralToken, + bytes32 parentCollectionId, + bytes32 conditionId, + uint256[] calldata partition, + uint256 amount + ) external; + + function redeemPositions( + address collateralToken, + bytes32 parentCollectionId, + bytes32 conditionId, + uint256[] calldata indexSets + ) external; + } + + interface INegRiskAdapter { + function redeemPositions( + bytes32 conditionId, + uint256[] calldata amounts + ) external; + } +} + #[derive(Args)] pub struct CtfArgs { #[command(subcommand)] @@ -171,7 +221,54 @@ fn binary_u256_vec() -> Vec { DEFAULT_BINARY_SETS.iter().map(|&n| U256::from(n)).collect() } -pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&str>) -> Result<()> { +fn is_proxy_mode(signature_type: Option<&str>) -> Result { + Ok(crate::config::resolve_signature_type(signature_type)? == "proxy") +} + +const PROXY_FACTORY: Address = polymarket_client_sdk::types::address!( + "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052" +); + +async fn send_ctf_call( + private_key: Option<&str>, + use_proxy: bool, + target: Address, + calldata: Vec, +) -> Result<(B256, u64)> { + let provider = auth::create_provider(private_key).await?; + + let (tx_hash, block_number) = if use_proxy { + let factory = IProxyWallet::new(PROXY_FACTORY, &provider); + let call = IProxyWallet::ProxyCall { + typeCode: 1, + to: target, + value: U256::ZERO, + data: calldata.into(), + }; + let pending = factory.proxy(vec![call]).send().await?; + let hash = *pending.tx_hash(); + let receipt = pending.get_receipt().await?; + (hash, receipt.block_number) + } else { + let tx = alloy::rpc::types::TransactionRequest::default() + .to(target) + .input(alloy::primitives::Bytes::from(calldata).into()); + let pending = provider.send_transaction(tx).await?; + let hash = *pending.tx_hash(); + let receipt = pending.get_receipt().await?; + (hash, receipt.block_number) + }; + + let block_number = block_number.context("Block number not available in receipt")?; + Ok((tx_hash, block_number)) +} + +pub async fn execute( + args: CtfArgs, + output: OutputFormat, + private_key: Option<&str>, + signature_type: Option<&str>, +) -> Result<()> { match args.command { CtfCommand::Split { condition, @@ -187,23 +284,24 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s None => binary_u256_vec(), }; - let provider = auth::create_provider(private_key).await?; - let client = ctf::Client::new(provider, POLYGON)?; - - let req = SplitPositionRequest::builder() - .collateral_token(collateral) - .parent_collection_id(parent) - .condition_id(condition) - .partition(partition) - .amount(usdc_amount) - .build(); - - let resp = client - .split_position(&req) - .await - .context("Split position failed")?; - - ctf_output::print_tx_result("split", resp.transaction_hash, resp.block_number, &output) + let proxy = is_proxy_mode(signature_type)?; + let config = polymarket_client_sdk::contract_config(POLYGON, false) + .context("CTF contract config not found")?; + let calldata = IConditionalTokens::splitPositionCall { + collateralToken: collateral, + parentCollectionId: parent, + conditionId: condition, + partition, + amount: usdc_amount, + } + .abi_encode(); + + let (tx_hash, block_number) = + send_ctf_call(private_key, proxy, config.conditional_tokens, calldata) + .await + .context("Split position failed")?; + + ctf_output::print_tx_result("split", tx_hash, block_number, &output) } CtfCommand::Merge { condition, @@ -219,23 +317,24 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s None => binary_u256_vec(), }; - let provider = auth::create_provider(private_key).await?; - let client = ctf::Client::new(provider, POLYGON)?; - - let req = MergePositionsRequest::builder() - .collateral_token(collateral) - .parent_collection_id(parent) - .condition_id(condition) - .partition(partition) - .amount(usdc_amount) - .build(); - - let resp = client - .merge_positions(&req) - .await - .context("Merge positions failed")?; - - ctf_output::print_tx_result("merge", resp.transaction_hash, resp.block_number, &output) + let proxy = is_proxy_mode(signature_type)?; + let config = polymarket_client_sdk::contract_config(POLYGON, false) + .context("CTF contract config not found")?; + let calldata = IConditionalTokens::mergePositionsCall { + collateralToken: collateral, + parentCollectionId: parent, + conditionId: condition, + partition, + amount: usdc_amount, + } + .abi_encode(); + + let (tx_hash, block_number) = + send_ctf_call(private_key, proxy, config.conditional_tokens, calldata) + .await + .context("Merge positions failed")?; + + ctf_output::print_tx_result("merge", tx_hash, block_number, &output) } CtfCommand::Redeem { condition, @@ -249,45 +348,44 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s None => binary_u256_vec(), }; - let provider = auth::create_provider(private_key).await?; - let client = ctf::Client::new(provider, POLYGON)?; - - let req = RedeemPositionsRequest::builder() - .collateral_token(collateral) - .parent_collection_id(parent) - .condition_id(condition) - .index_sets(index_sets) - .build(); - - let resp = client - .redeem_positions(&req) - .await - .context("Redeem positions failed")?; - - ctf_output::print_tx_result("redeem", resp.transaction_hash, resp.block_number, &output) + let proxy = is_proxy_mode(signature_type)?; + let config = polymarket_client_sdk::contract_config(POLYGON, false) + .context("CTF contract config not found")?; + let calldata = IConditionalTokens::redeemPositionsCall { + collateralToken: collateral, + parentCollectionId: parent, + conditionId: condition, + indexSets: index_sets, + } + .abi_encode(); + + let (tx_hash, block_number) = + send_ctf_call(private_key, proxy, config.conditional_tokens, calldata) + .await + .context("Redeem positions failed")?; + + ctf_output::print_tx_result("redeem", tx_hash, block_number, &output) } CtfCommand::RedeemNegRisk { condition, amounts } => { let amounts = parse_usdc_amounts(&amounts)?; - let provider = auth::create_provider(private_key).await?; - let client = ctf::Client::with_neg_risk(provider, POLYGON)?; - - let req = RedeemNegRiskRequest::builder() - .condition_id(condition) - .amounts(amounts) - .build(); - - let resp = client - .redeem_neg_risk(&req) + let proxy = is_proxy_mode(signature_type)?; + let config = polymarket_client_sdk::contract_config(POLYGON, true) + .context("NegRisk contract config not found")?; + let target = config + .neg_risk_adapter + .context("NegRisk adapter not configured")?; + let calldata = INegRiskAdapter::redeemPositionsCall { + conditionId: condition, + amounts, + } + .abi_encode(); + + let (tx_hash, block_number) = send_ctf_call(private_key, proxy, target, calldata) .await .context("Redeem neg-risk positions failed")?; - ctf_output::print_tx_result( - "redeem-neg-risk", - resp.transaction_hash, - resp.block_number, - &output, - ) + ctf_output::print_tx_result("redeem-neg-risk", tx_hash, block_number, &output) } CtfCommand::ConditionId { oracle, diff --git a/src/main.rs b/src/main.rs index 2abb55f..a01d411 100644 --- a/src/main.rs +++ b/src/main.rs @@ -109,7 +109,13 @@ pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> { .await } Commands::Ctf(args) => { - commands::ctf::execute(args, cli.output, cli.private_key.as_deref()).await + commands::ctf::execute( + args, + cli.output, + cli.private_key.as_deref(), + cli.signature_type.as_deref(), + ) + .await } Commands::Data(args) => commands::data::execute(&data, args, cli.output).await, Commands::Bridge(args) => commands::bridge::execute(&bridge, args, cli.output).await, From df81741f96755633b590a14ffe7f42b1284b6d0e Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Mon, 6 Apr 2026 02:04:01 +0530 Subject: [PATCH 2/6] run formatter --- src/commands/ctf.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/commands/ctf.rs b/src/commands/ctf.rs index 9426c7e..1ddf439 100644 --- a/src/commands/ctf.rs +++ b/src/commands/ctf.rs @@ -4,7 +4,9 @@ use alloy::sol; use alloy::sol_types::SolCall; use anyhow::{Context, Result}; use clap::{Args, Subcommand}; -use polymarket_client_sdk::ctf::types::{CollectionIdRequest, ConditionIdRequest, PositionIdRequest}; +use polymarket_client_sdk::ctf::types::{ + CollectionIdRequest, ConditionIdRequest, PositionIdRequest, +}; use polymarket_client_sdk::types::{Address, B256}; use polymarket_client_sdk::{POLYGON, ctf}; use rust_decimal::Decimal; @@ -225,9 +227,8 @@ fn is_proxy_mode(signature_type: Option<&str>) -> Result { Ok(crate::config::resolve_signature_type(signature_type)? == "proxy") } -const PROXY_FACTORY: Address = polymarket_client_sdk::types::address!( - "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052" -); +const PROXY_FACTORY: Address = + polymarket_client_sdk::types::address!("0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"); async fn send_ctf_call( private_key: Option<&str>, From fb1488a9aa4d2f35d4c84ebf0571626c94216097 Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Mon, 6 Apr 2026 02:15:50 +0530 Subject: [PATCH 3/6] fix: route approve through proxy wallet factory --- src/commands/approve.rs | 168 +++++++++++++++++++++++++++------------- src/commands/ctf.rs | 75 +++--------------- src/commands/mod.rs | 100 ++++++++++++++++++++++++ src/main.rs | 8 +- 4 files changed, 230 insertions(+), 121 deletions(-) diff --git a/src/commands/approve.rs b/src/commands/approve.rs index b52dc0c..1f525ab 100644 --- a/src/commands/approve.rs +++ b/src/commands/approve.rs @@ -3,6 +3,7 @@ use alloy::primitives::U256; use alloy::sol; +use alloy::sol_types::SolCall; use anyhow::{Context, Result}; use clap::{Args, Subcommand}; use polymarket_client_sdk::types::{Address, address}; @@ -12,6 +13,8 @@ use crate::auth; use crate::output::OutputFormat; use crate::output::approve::{ApprovalStatus, print_approval_status, print_tx_result}; +use super::proxy; + /// Polygon USDC (same address as `USDC_ADDRESS_STR`; `address!` requires a literal). const USDC_ADDRESS: Address = address!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"); @@ -81,20 +84,26 @@ pub async fn execute( args: ApproveArgs, output: OutputFormat, private_key: Option<&str>, + signature_type: Option<&str>, ) -> Result<()> { match args.command { - ApproveCommand::Check { address } => check(address, private_key, output).await, - ApproveCommand::Set => set(private_key, output).await, + ApproveCommand::Check { address } => { + check(address, private_key, signature_type, output).await + } + ApproveCommand::Set => set(private_key, signature_type, output).await, } } async fn check( address_arg: Option
, private_key: Option<&str>, + signature_type: Option<&str>, output: OutputFormat, ) -> Result<()> { let owner: Address = if let Some(addr) = address_arg { addr + } else if proxy::is_proxy_mode(signature_type)? { + proxy::derive_proxy_address(private_key)? } else { let signer = auth::resolve_signer(private_key)?; polymarket_client_sdk::auth::Signer::address(&signer) @@ -135,13 +144,13 @@ async fn check( print_approval_status(&statuses, &output) } -async fn set(private_key: Option<&str>, output: OutputFormat) -> Result<()> { - let provider = auth::create_provider(private_key).await?; +async fn set( + private_key: Option<&str>, + signature_type: Option<&str>, + output: OutputFormat, +) -> Result<()> { + let use_proxy = proxy::is_proxy_mode(signature_type)?; let config = contract_config(POLYGON, false).context("No contract config for Polygon")?; - - let usdc = IERC20::new(USDC_ADDRESS, provider.clone()); - let ctf = IERC1155::new(config.conditional_tokens, provider.clone()); - let targets = approval_targets()?; let total = targets.len() * 2; @@ -152,53 +161,104 @@ async fn set(private_key: Option<&str>, output: OutputFormat) -> Result<()> { let mut results: Vec = Vec::new(); let mut step = 0; - for target in &targets { - step += 1; - let label = format!("USDC \u{2192} {}", target.name); - let tx_hash = usdc - .approve(target.address, U256::MAX) - .send() - .await - .context(format!("Failed to send USDC approval for {}", target.name))? - .watch() - .await - .context(format!( - "Failed to confirm USDC approval for {}", - target.name - ))?; - - match output { - OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), - OutputFormat::Json => results.push(serde_json::json!({ - "step": step, - "type": "erc20", - "contract": target.name, - "tx_hash": format!("{tx_hash}"), - })), + if use_proxy { + for target in &targets { + step += 1; + let label = format!("USDC \u{2192} {}", target.name); + let calldata = IERC20::approveCall { + spender: target.address, + value: U256::MAX, + } + .abi_encode(); + let tx_hash = proxy::send_via_factory(private_key, USDC_ADDRESS, calldata) + .await + .context(format!("Failed USDC approval for {}", target.name))?; + + match output { + OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), + OutputFormat::Json => results.push(serde_json::json!({ + "step": step, + "type": "erc20", + "contract": target.name, + "tx_hash": format!("{tx_hash}"), + })), + } + + step += 1; + let label = format!("CTF \u{2192} {}", target.name); + let calldata = IERC1155::setApprovalForAllCall { + operator: target.address, + approved: true, + } + .abi_encode(); + let tx_hash = + proxy::send_via_factory(private_key, config.conditional_tokens, calldata) + .await + .context(format!("Failed CTF approval for {}", target.name))?; + + match output { + OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), + OutputFormat::Json => results.push(serde_json::json!({ + "step": step, + "type": "erc1155", + "contract": target.name, + "tx_hash": format!("{tx_hash}"), + })), + } } - - step += 1; - let label = format!("CTF \u{2192} {}", target.name); - let tx_hash = ctf - .setApprovalForAll(target.address, true) - .send() - .await - .context(format!("Failed to send CTF approval for {}", target.name))? - .watch() - .await - .context(format!( - "Failed to confirm CTF approval for {}", - target.name - ))?; - - match output { - OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), - OutputFormat::Json => results.push(serde_json::json!({ - "step": step, - "type": "erc1155", - "contract": target.name, - "tx_hash": format!("{tx_hash}"), - })), + } else { + let provider = auth::create_provider(private_key).await?; + let usdc = IERC20::new(USDC_ADDRESS, provider.clone()); + let ctf = IERC1155::new(config.conditional_tokens, provider.clone()); + + for target in &targets { + step += 1; + let label = format!("USDC \u{2192} {}", target.name); + let tx_hash = usdc + .approve(target.address, U256::MAX) + .send() + .await + .context(format!("Failed to send USDC approval for {}", target.name))? + .watch() + .await + .context(format!( + "Failed to confirm USDC approval for {}", + target.name + ))?; + + match output { + OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), + OutputFormat::Json => results.push(serde_json::json!({ + "step": step, + "type": "erc20", + "contract": target.name, + "tx_hash": format!("{tx_hash}"), + })), + } + + step += 1; + let label = format!("CTF \u{2192} {}", target.name); + let tx_hash = ctf + .setApprovalForAll(target.address, true) + .send() + .await + .context(format!("Failed to send CTF approval for {}", target.name))? + .watch() + .await + .context(format!( + "Failed to confirm CTF approval for {}", + target.name + ))?; + + match output { + OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), + OutputFormat::Json => results.push(serde_json::json!({ + "step": step, + "type": "erc1155", + "contract": target.name, + "tx_hash": format!("{tx_hash}"), + })), + } } } diff --git a/src/commands/ctf.rs b/src/commands/ctf.rs index 1ddf439..f87aa0e 100644 --- a/src/commands/ctf.rs +++ b/src/commands/ctf.rs @@ -1,5 +1,4 @@ use alloy::primitives::U256; -use alloy::providers::Provider as _; use alloy::sol; use alloy::sol_types::SolCall; use anyhow::{Context, Result}; @@ -15,24 +14,9 @@ use crate::auth; use crate::output::OutputFormat; use crate::output::ctf as ctf_output; +use super::proxy; use super::{USDC_ADDRESS_STR, USDC_DECIMALS}; -// Polymarket Proxy Wallet Factory interface (CallType: INVALID=0, CALL=1, DELEGATECALL=2) -sol! { - #[sol(rpc)] - interface IProxyWallet { - struct ProxyCall { - uint8 typeCode; - address to; - uint256 value; - bytes data; - } - - function proxy(ProxyCall[] memory calls) - external payable returns (bytes[] memory returnValues); - } -} - sol! { interface IConditionalTokens { function splitPosition( @@ -223,47 +207,6 @@ fn binary_u256_vec() -> Vec { DEFAULT_BINARY_SETS.iter().map(|&n| U256::from(n)).collect() } -fn is_proxy_mode(signature_type: Option<&str>) -> Result { - Ok(crate::config::resolve_signature_type(signature_type)? == "proxy") -} - -const PROXY_FACTORY: Address = - polymarket_client_sdk::types::address!("0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"); - -async fn send_ctf_call( - private_key: Option<&str>, - use_proxy: bool, - target: Address, - calldata: Vec, -) -> Result<(B256, u64)> { - let provider = auth::create_provider(private_key).await?; - - let (tx_hash, block_number) = if use_proxy { - let factory = IProxyWallet::new(PROXY_FACTORY, &provider); - let call = IProxyWallet::ProxyCall { - typeCode: 1, - to: target, - value: U256::ZERO, - data: calldata.into(), - }; - let pending = factory.proxy(vec![call]).send().await?; - let hash = *pending.tx_hash(); - let receipt = pending.get_receipt().await?; - (hash, receipt.block_number) - } else { - let tx = alloy::rpc::types::TransactionRequest::default() - .to(target) - .input(alloy::primitives::Bytes::from(calldata).into()); - let pending = provider.send_transaction(tx).await?; - let hash = *pending.tx_hash(); - let receipt = pending.get_receipt().await?; - (hash, receipt.block_number) - }; - - let block_number = block_number.context("Block number not available in receipt")?; - Ok((tx_hash, block_number)) -} - pub async fn execute( args: CtfArgs, output: OutputFormat, @@ -285,7 +228,7 @@ pub async fn execute( None => binary_u256_vec(), }; - let proxy = is_proxy_mode(signature_type)?; + let proxy = proxy::is_proxy_mode(signature_type)?; let config = polymarket_client_sdk::contract_config(POLYGON, false) .context("CTF contract config not found")?; let calldata = IConditionalTokens::splitPositionCall { @@ -298,7 +241,7 @@ pub async fn execute( .abi_encode(); let (tx_hash, block_number) = - send_ctf_call(private_key, proxy, config.conditional_tokens, calldata) + proxy::send_call(private_key, proxy, config.conditional_tokens, calldata) .await .context("Split position failed")?; @@ -318,7 +261,7 @@ pub async fn execute( None => binary_u256_vec(), }; - let proxy = is_proxy_mode(signature_type)?; + let proxy = proxy::is_proxy_mode(signature_type)?; let config = polymarket_client_sdk::contract_config(POLYGON, false) .context("CTF contract config not found")?; let calldata = IConditionalTokens::mergePositionsCall { @@ -331,7 +274,7 @@ pub async fn execute( .abi_encode(); let (tx_hash, block_number) = - send_ctf_call(private_key, proxy, config.conditional_tokens, calldata) + proxy::send_call(private_key, proxy, config.conditional_tokens, calldata) .await .context("Merge positions failed")?; @@ -349,7 +292,7 @@ pub async fn execute( None => binary_u256_vec(), }; - let proxy = is_proxy_mode(signature_type)?; + let proxy = proxy::is_proxy_mode(signature_type)?; let config = polymarket_client_sdk::contract_config(POLYGON, false) .context("CTF contract config not found")?; let calldata = IConditionalTokens::redeemPositionsCall { @@ -361,7 +304,7 @@ pub async fn execute( .abi_encode(); let (tx_hash, block_number) = - send_ctf_call(private_key, proxy, config.conditional_tokens, calldata) + proxy::send_call(private_key, proxy, config.conditional_tokens, calldata) .await .context("Redeem positions failed")?; @@ -370,7 +313,7 @@ pub async fn execute( CtfCommand::RedeemNegRisk { condition, amounts } => { let amounts = parse_usdc_amounts(&amounts)?; - let proxy = is_proxy_mode(signature_type)?; + let proxy = proxy::is_proxy_mode(signature_type)?; let config = polymarket_client_sdk::contract_config(POLYGON, true) .context("NegRisk contract config not found")?; let target = config @@ -382,7 +325,7 @@ pub async fn execute( } .abi_encode(); - let (tx_hash, block_number) = send_ctf_call(private_key, proxy, target, calldata) + let (tx_hash, block_number) = proxy::send_call(private_key, proxy, target, calldata) .await .context("Redeem neg-risk positions failed")?; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index d4c985b..9e72fa9 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,6 +1,106 @@ pub(crate) const USDC_ADDRESS_STR: &str = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; pub(crate) const USDC_DECIMALS: u32 = 6; +pub(crate) mod proxy { + use alloy::primitives::U256; + use alloy::sol; + use anyhow::{Context, Result}; + use polymarket_client_sdk::types::{Address, B256}; + use polymarket_client_sdk::POLYGON; + + use crate::auth; + + // Polymarket Proxy Wallet Factory interface (CallType: INVALID=0, CALL=1, DELEGATECALL=2) + sol! { + #[sol(rpc)] + interface IProxyWallet { + struct ProxyCall { + uint8 typeCode; + address to; + uint256 value; + bytes data; + } + + function proxy(ProxyCall[] memory calls) + external payable returns (bytes[] memory returnValues); + } + } + + const PROXY_FACTORY: Address = + polymarket_client_sdk::types::address!("0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"); + + pub fn is_proxy_mode(signature_type: Option<&str>) -> Result { + Ok(crate::config::resolve_signature_type(signature_type)? == "proxy") + } + + pub fn derive_proxy_address(private_key: Option<&str>) -> Result
{ + let signer = auth::resolve_signer(private_key)?; + let eoa = polymarket_client_sdk::auth::Signer::address(&signer); + polymarket_client_sdk::derive_proxy_wallet(eoa, POLYGON) + .ok_or_else(|| anyhow::anyhow!("Proxy wallet derivation not supported on this chain")) + } + + pub async fn send_via_factory( + private_key: Option<&str>, + target: Address, + calldata: Vec, + ) -> Result { + let provider = auth::create_provider(private_key).await?; + let factory = IProxyWallet::new(PROXY_FACTORY, &provider); + let call = IProxyWallet::ProxyCall { + typeCode: 1, + to: target, + value: U256::ZERO, + data: calldata.into(), + }; + let tx_hash = factory + .proxy(vec![call]) + .send() + .await + .context("Failed to send proxy transaction")? + .watch() + .await + .context("Failed to confirm proxy transaction")?; + Ok(tx_hash) + } + + pub async fn send_call( + private_key: Option<&str>, + use_proxy: bool, + target: Address, + calldata: Vec, + ) -> Result<(B256, u64)> { + use alloy::providers::Provider as _; + + let provider = auth::create_provider(private_key).await?; + + let (tx_hash, block_number) = if use_proxy { + let factory = IProxyWallet::new(PROXY_FACTORY, &provider); + let call = IProxyWallet::ProxyCall { + typeCode: 1, + to: target, + value: U256::ZERO, + data: calldata.into(), + }; + let pending = factory.proxy(vec![call]).send().await?; + let hash = *pending.tx_hash(); + let receipt = pending.get_receipt().await?; + (hash, receipt.block_number) + } else { + let tx = alloy::rpc::types::TransactionRequest::default() + .to(target) + .input(alloy::primitives::Bytes::from(calldata).into()); + let pending = provider.send_transaction(tx).await?; + let hash = *pending.tx_hash(); + let receipt = pending.get_receipt().await?; + (hash, receipt.block_number) + }; + + let block_number = block_number.context("Block number not available in receipt")?; + Ok((tx_hash, block_number)) + } +} + pub(crate) mod approve; pub(crate) mod bridge; pub(crate) mod clob; diff --git a/src/main.rs b/src/main.rs index a01d411..da01221 100644 --- a/src/main.rs +++ b/src/main.rs @@ -97,7 +97,13 @@ pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> { Commands::Profiles(args) => commands::profiles::execute(&gamma, args, cli.output).await, Commands::Sports(args) => commands::sports::execute(&gamma, args, cli.output).await, Commands::Approve(args) => { - commands::approve::execute(args, cli.output, cli.private_key.as_deref()).await + commands::approve::execute( + args, + cli.output, + cli.private_key.as_deref(), + cli.signature_type.as_deref(), + ) + .await } Commands::Clob(args) => { commands::clob::execute( From 544775c978a8995aff1d01e4e10e5239abb8c7e9 Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Mon, 6 Apr 2026 02:16:28 +0530 Subject: [PATCH 4/6] format --- src/commands/approve.rs | 7 +++---- src/commands/mod.rs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/commands/approve.rs b/src/commands/approve.rs index 1f525ab..49ada63 100644 --- a/src/commands/approve.rs +++ b/src/commands/approve.rs @@ -191,10 +191,9 @@ async fn set( approved: true, } .abi_encode(); - let tx_hash = - proxy::send_via_factory(private_key, config.conditional_tokens, calldata) - .await - .context(format!("Failed CTF approval for {}", target.name))?; + let tx_hash = proxy::send_via_factory(private_key, config.conditional_tokens, calldata) + .await + .context(format!("Failed CTF approval for {}", target.name))?; match output { OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 9e72fa9..ed5d562 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -5,8 +5,8 @@ pub(crate) mod proxy { use alloy::primitives::U256; use alloy::sol; use anyhow::{Context, Result}; - use polymarket_client_sdk::types::{Address, B256}; use polymarket_client_sdk::POLYGON; + use polymarket_client_sdk::types::{Address, B256}; use crate::auth; From 0c63aca64118e4aa36b706cba404ecde21431400 Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Mon, 6 Apr 2026 02:38:46 +0530 Subject: [PATCH 5/6] remove redundant send_via_factory --- src/commands/approve.rs | 9 +++++---- src/commands/mod.rs | 24 ------------------------ 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/src/commands/approve.rs b/src/commands/approve.rs index 49ada63..d189c6e 100644 --- a/src/commands/approve.rs +++ b/src/commands/approve.rs @@ -170,7 +170,7 @@ async fn set( value: U256::MAX, } .abi_encode(); - let tx_hash = proxy::send_via_factory(private_key, USDC_ADDRESS, calldata) + let (tx_hash, _) = proxy::send_call(private_key, true, USDC_ADDRESS, calldata) .await .context(format!("Failed USDC approval for {}", target.name))?; @@ -191,9 +191,10 @@ async fn set( approved: true, } .abi_encode(); - let tx_hash = proxy::send_via_factory(private_key, config.conditional_tokens, calldata) - .await - .context(format!("Failed CTF approval for {}", target.name))?; + let (tx_hash, _) = + proxy::send_call(private_key, true, config.conditional_tokens, calldata) + .await + .context(format!("Failed CTF approval for {}", target.name))?; match output { OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ed5d562..1788b44 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -40,30 +40,6 @@ pub(crate) mod proxy { .ok_or_else(|| anyhow::anyhow!("Proxy wallet derivation not supported on this chain")) } - pub async fn send_via_factory( - private_key: Option<&str>, - target: Address, - calldata: Vec, - ) -> Result { - let provider = auth::create_provider(private_key).await?; - let factory = IProxyWallet::new(PROXY_FACTORY, &provider); - let call = IProxyWallet::ProxyCall { - typeCode: 1, - to: target, - value: U256::ZERO, - data: calldata.into(), - }; - let tx_hash = factory - .proxy(vec![call]) - .send() - .await - .context("Failed to send proxy transaction")? - .watch() - .await - .context("Failed to confirm proxy transaction")?; - Ok(tx_hash) - } - pub async fn send_call( private_key: Option<&str>, use_proxy: bool, From 41f965b005b623abfdb92543d3a399eb6f6aff46 Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Mon, 6 Apr 2026 03:13:52 +0530 Subject: [PATCH 6/6] conditional statement cleanup --- src/commands/approve.rs | 134 ++++++++++++---------------------------- 1 file changed, 39 insertions(+), 95 deletions(-) diff --git a/src/commands/approve.rs b/src/commands/approve.rs index d189c6e..86aba39 100644 --- a/src/commands/approve.rs +++ b/src/commands/approve.rs @@ -161,104 +161,48 @@ async fn set( let mut results: Vec = Vec::new(); let mut step = 0; - if use_proxy { - for target in &targets { - step += 1; - let label = format!("USDC \u{2192} {}", target.name); - let calldata = IERC20::approveCall { - spender: target.address, - value: U256::MAX, - } - .abi_encode(); - let (tx_hash, _) = proxy::send_call(private_key, true, USDC_ADDRESS, calldata) - .await - .context(format!("Failed USDC approval for {}", target.name))?; - - match output { - OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), - OutputFormat::Json => results.push(serde_json::json!({ - "step": step, - "type": "erc20", - "contract": target.name, - "tx_hash": format!("{tx_hash}"), - })), - } - - step += 1; - let label = format!("CTF \u{2192} {}", target.name); - let calldata = IERC1155::setApprovalForAllCall { - operator: target.address, - approved: true, - } - .abi_encode(); - let (tx_hash, _) = - proxy::send_call(private_key, true, config.conditional_tokens, calldata) - .await - .context(format!("Failed CTF approval for {}", target.name))?; - - match output { - OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), - OutputFormat::Json => results.push(serde_json::json!({ - "step": step, - "type": "erc1155", - "contract": target.name, - "tx_hash": format!("{tx_hash}"), - })), - } + for target in &targets { + step += 1; + let label = format!("USDC \u{2192} {}", target.name); + let calldata = IERC20::approveCall { + spender: target.address, + value: U256::MAX, + } + .abi_encode(); + let (tx_hash, _) = proxy::send_call(private_key, use_proxy, USDC_ADDRESS, calldata) + .await + .context(format!("Failed USDC approval for {}", target.name))?; + + match output { + OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), + OutputFormat::Json => results.push(serde_json::json!({ + "step": step, + "type": "erc20", + "contract": target.name, + "tx_hash": format!("{tx_hash}"), + })), } - } else { - let provider = auth::create_provider(private_key).await?; - let usdc = IERC20::new(USDC_ADDRESS, provider.clone()); - let ctf = IERC1155::new(config.conditional_tokens, provider.clone()); - - for target in &targets { - step += 1; - let label = format!("USDC \u{2192} {}", target.name); - let tx_hash = usdc - .approve(target.address, U256::MAX) - .send() - .await - .context(format!("Failed to send USDC approval for {}", target.name))? - .watch() - .await - .context(format!( - "Failed to confirm USDC approval for {}", - target.name - ))?; - - match output { - OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), - OutputFormat::Json => results.push(serde_json::json!({ - "step": step, - "type": "erc20", - "contract": target.name, - "tx_hash": format!("{tx_hash}"), - })), - } - step += 1; - let label = format!("CTF \u{2192} {}", target.name); - let tx_hash = ctf - .setApprovalForAll(target.address, true) - .send() - .await - .context(format!("Failed to send CTF approval for {}", target.name))? - .watch() + step += 1; + let label = format!("CTF \u{2192} {}", target.name); + let calldata = IERC1155::setApprovalForAllCall { + operator: target.address, + approved: true, + } + .abi_encode(); + let (tx_hash, _) = + proxy::send_call(private_key, use_proxy, config.conditional_tokens, calldata) .await - .context(format!( - "Failed to confirm CTF approval for {}", - target.name - ))?; - - match output { - OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), - OutputFormat::Json => results.push(serde_json::json!({ - "step": step, - "type": "erc1155", - "contract": target.name, - "tx_hash": format!("{tx_hash}"), - })), - } + .context(format!("Failed CTF approval for {}", target.name))?; + + match output { + OutputFormat::Table => print_tx_result(step, total, &label, tx_hash), + OutputFormat::Json => results.push(serde_json::json!({ + "step": step, + "type": "erc1155", + "contract": target.name, + "tx_hash": format!("{tx_hash}"), + })), } }