diff --git a/src/auth.rs b/src/auth.rs index a5cf34d..1e3cf3f 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,11 +1,12 @@ use std::str::FromStr; +use alloy::primitives::Address; use alloy::providers::ProviderBuilder; use anyhow::{Context, Result}; use polymarket_client_sdk::auth::state::Authenticated; use polymarket_client_sdk::auth::{LocalSigner, Normal, Signer as _}; use polymarket_client_sdk::clob::types::SignatureType; -use polymarket_client_sdk::{POLYGON, clob}; +use polymarket_client_sdk::{POLYGON, clob, derive_proxy_wallet}; use crate::config; @@ -77,6 +78,29 @@ pub async fn create_provider( .context("Failed to connect to Polygon RPC with wallet") } +/// Resolve the wallet address that owns positions and allowances. +/// +/// For `eoa` signature type, returns the EOA address directly. +/// For `proxy`, returns the derived proxy wallet address. +/// For `gnosis-safe`, returns the derived Gnosis Safe address. +pub fn resolve_wallet_address( + private_key: Option<&str>, + signature_type_flag: Option<&str>, +) -> Result
{ + let signer = resolve_signer(private_key)?; + let eoa = polymarket_client_sdk::auth::Signer::address(&signer); + let sig_type = parse_signature_type(&config::resolve_signature_type(signature_type_flag)?); + + match sig_type { + SignatureType::Eoa => Ok(eoa), + SignatureType::Proxy => derive_proxy_wallet(eoa, POLYGON) + .context("Failed to derive proxy wallet address for Polygon"), + SignatureType::GnosisSafe => polymarket_client_sdk::derive_safe_wallet(eoa, POLYGON) + .context("Failed to derive Gnosis Safe wallet address for Polygon"), + _ => Ok(eoa), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/commands/approve.rs b/src/commands/approve.rs index b52dc0c..3440226 100644 --- a/src/commands/approve.rs +++ b/src/commands/approve.rs @@ -1,14 +1,16 @@ #![allow(clippy::exhaustive_enums, reason = "Generated by sol! macro")] #![allow(clippy::exhaustive_structs, reason = "Generated by sol! macro")] -use alloy::primitives::U256; +use alloy::primitives::{Bytes, U256}; use alloy::sol; +use alloy::sol_types::SolCall; use anyhow::{Context, Result}; use clap::{Args, Subcommand}; use polymarket_client_sdk::types::{Address, address}; -use polymarket_client_sdk::{POLYGON, contract_config}; +use polymarket_client_sdk::{POLYGON, contract_config, wallet_contract_config}; use crate::auth; +use crate::config; use crate::output::OutputFormat; use crate::output::approve::{ApprovalStatus, print_approval_status, print_tx_result}; @@ -27,6 +29,17 @@ sol! { function setApprovalForAll(address operator, bool approved) external; function isApprovedForAll(address account, address operator) external view returns (bool); } + + struct ProxyCall { + address to; + uint256 value; + bytes data; + } + + #[sol(rpc)] + interface IProxyWalletFactory { + function proxy(ProxyCall[] memory calls) external payable returns (bytes[] memory); + } } #[derive(Args)] @@ -81,23 +94,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 { - let signer = auth::resolve_signer(private_key)?; - polymarket_client_sdk::auth::Signer::address(&signer) + auth::resolve_wallet_address(private_key, signature_type)? }; let provider = auth::create_readonly_provider().await?; @@ -135,70 +151,151 @@ 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?; - let config = contract_config(POLYGON, false).context("No contract config for Polygon")?; +async fn set( + private_key: Option<&str>, + signature_type: Option<&str>, + output: OutputFormat, +) -> Result<()> { + let sig_type = config::resolve_signature_type(signature_type)?; + let is_proxy = sig_type == config::DEFAULT_SIGNATURE_TYPE; + + if sig_type == "gnosis-safe" { + anyhow::bail!( + "Gnosis Safe approvals must be submitted through your Safe wallet interface.\n\ + Use `approve check --signature-type gnosis-safe` to verify allowances on your Safe address." + ); + } - let usdc = IERC20::new(USDC_ADDRESS, provider.clone()); - let ctf = IERC1155::new(config.conditional_tokens, provider.clone()); + let provider = auth::create_provider(private_key).await?; + let ctf_config = contract_config(POLYGON, false).context("No contract config for Polygon")?; let targets = approval_targets()?; let total = targets.len() * 2; if matches!(output, OutputFormat::Table) { - println!("Approving contracts...\n"); + if is_proxy { + println!("Approving contracts via proxy wallet...\n"); + } else { + println!("Approving contracts...\n"); + } } 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 is_proxy { + let wallet_config = + wallet_contract_config(POLYGON).context("No wallet contract config for Polygon")?; + let factory_address = wallet_config + .proxy_factory + .context("No proxy factory address for Polygon")?; + let factory = IProxyWalletFactory::new(factory_address, provider.clone()); + + for target in &targets { + step += 1; + let label = format!("USDC + CTF \u{2192} {} (proxy)", target.name); + + let usdc_calldata = IERC20::approveCall { + spender: target.address, + value: U256::MAX, + } + .abi_encode(); + let ctf_calldata = IERC1155::setApprovalForAllCall { + operator: target.address, + approved: true, + } + .abi_encode(); + + let calls = vec![ + ProxyCall { + to: USDC_ADDRESS, + value: U256::ZERO, + data: Bytes::from(usdc_calldata), + }, + ProxyCall { + to: ctf_config.conditional_tokens, + value: U256::ZERO, + data: Bytes::from(ctf_calldata), + }, + ]; + + let tx_hash = factory + .proxy(calls) + .send() + .await + .context(format!( + "Failed to send proxy approvals for {}", + target.name + ))? + .watch() + .await + .context(format!( + "Failed to confirm proxy approvals for {}", + target.name + ))?; + + match output { + OutputFormat::Table => print_tx_result(step, total / 2, &label, tx_hash), + OutputFormat::Json => results.push(serde_json::json!({ + "step": step, + "type": "proxy_batch", + "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 usdc = IERC20::new(USDC_ADDRESS, provider.clone()); + let ctf = IERC1155::new(ctf_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 5d06ea5..b656bec 100644 --- a/src/commands/ctf.rs +++ b/src/commands/ctf.rs @@ -171,7 +171,12 @@ 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<()> { +pub async fn execute( + args: CtfArgs, + output: OutputFormat, + private_key: Option<&str>, + _signature_type: Option<&str>, +) -> Result<()> { match args.command { CtfCommand::Split { condition, diff --git a/src/main.rs b/src/main.rs index 2abb55f..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( @@ -109,7 +115,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,