diff --git a/src/auth.rs b/src/auth.rs index 15ad61e..bd65202 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,16 +1,83 @@ use std::str::FromStr; +use alloy::primitives::{Bytes, U256}; use alloy::providers::ProviderBuilder; +use alloy::sol; 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::types::Address; +use polymarket_client_sdk::{POLYGON, clob, derive_proxy_wallet}; use crate::config; pub const RPC_URL: &str = "https://polygon.drpc.org"; +sol! { + #[allow(clippy::exhaustive_structs)] + #[sol(rpc)] + interface IProxyWallet { + struct ProxyCall { + uint8 typeCode; + address to; + uint256 value; + bytes data; + } + + function proxy(ProxyCall[] memory calls) external payable returns (bytes[] memory); + } +} + +/// Returns `true` when the resolved signature type is proxy mode. +pub fn is_proxy_mode(signature_type: Option<&str>) -> bool { + config::resolve_signature_type(signature_type) == config::DEFAULT_SIGNATURE_TYPE +} + +/// Derives the proxy wallet address for the configured private key. +/// Returns `None` when not in proxy mode or when derivation fails. +pub fn resolve_proxy_address( + private_key: Option<&str>, + signature_type: Option<&str>, +) -> Result> { + if !is_proxy_mode(signature_type) { + return Ok(None); + } + let signer = resolve_signer(private_key)?; + let eoa = polymarket_client_sdk::auth::Signer::address(&signer); + let proxy = derive_proxy_wallet(eoa, POLYGON) + .ok_or_else(|| anyhow::anyhow!("Could not derive proxy wallet for {eoa}"))?; + Ok(Some(proxy)) +} + +/// Sends a transaction through the proxy wallet's `proxy` function. +/// +/// Wraps the call in a single-element `ProxyCall` array with `typeCode = 1` +/// (CALL) and `value = 0`, matching the on-chain ProxyWallet contract at +/// . +pub async fn proxy_exec( + provider: &(impl alloy::providers::Provider + Clone), + proxy_address: Address, + target: Address, + calldata: Bytes, +) -> Result { + let proxy = IProxyWallet::new(proxy_address, provider); + let call = IProxyWallet::ProxyCall { + typeCode: 1, // CallType.CALL + to: target, + value: U256::ZERO, + data: calldata, + }; + proxy + .proxy(vec![call]) + .send() + .await + .context("Failed to send proxy transaction")? + .watch() + .await + .context("Failed to confirm proxy transaction") +} + fn parse_signature_type(s: &str) -> SignatureType { match s { config::DEFAULT_SIGNATURE_TYPE => SignatureType::Proxy, diff --git a/src/commands/approve.rs b/src/commands/approve.rs index 329fa83..5ead2aa 100644 --- a/src/commands/approve.rs +++ b/src/commands/approve.rs @@ -1,12 +1,13 @@ #![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 anyhow::{Context, Result}; use clap::{Args, Subcommand}; +use polymarket_client_sdk::auth::Signer as _; use polymarket_client_sdk::types::{Address, address}; -use polymarket_client_sdk::{POLYGON, contract_config}; +use polymarket_client_sdk::{POLYGON, contract_config, derive_proxy_wallet}; use crate::auth; use crate::output::OutputFormat; @@ -80,23 +81,33 @@ 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.as_deref(), private_key, output).await, - ApproveCommand::Set => set(private_key, output).await, + ApproveCommand::Check { address } => { + check(address.as_deref(), private_key, signature_type, output).await + } + ApproveCommand::Set => set(private_key, signature_type, output).await, } } async fn check( address_arg: Option<&str>, private_key: Option<&str>, + signature_type: Option<&str>, output: OutputFormat, ) -> Result<()> { let owner: Address = if let Some(addr) = address_arg { super::parse_address(addr)? } else { let signer = auth::resolve_signer(private_key)?; - polymarket_client_sdk::auth::Signer::address(&signer) + let eoa = signer.address(); + if auth::is_proxy_mode(signature_type) { + derive_proxy_wallet(eoa, POLYGON) + .ok_or_else(|| anyhow::anyhow!("Could not derive proxy wallet for {eoa}"))? + } else { + eoa + } }; let provider = auth::create_readonly_provider().await?; @@ -134,18 +145,24 @@ async fn check( print_approval_status(&statuses, &output) } -async fn set(private_key: Option<&str>, output: OutputFormat) -> Result<()> { +async fn set( + private_key: Option<&str>, + signature_type: Option<&str>, + output: OutputFormat, +) -> Result<()> { + let proxy_addr = auth::resolve_proxy_address(private_key, signature_type)?; let provider = auth::create_provider(private_key).await?; 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; if matches!(output, OutputFormat::Table) { - println!("Approving contracts...\n"); + if let Some(proxy) = proxy_addr { + println!("Approving contracts via proxy {proxy}...\n"); + } else { + println!("Approving contracts...\n"); + } } let mut results: Vec = Vec::new(); @@ -154,17 +171,33 @@ async fn set(private_key: Option<&str>, output: OutputFormat) -> Result<()> { 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() + + let tx_hash = if let Some(proxy) = proxy_addr { + let calldata = IERC20::approveCall { + spender: target.address, + value: U256::MAX, + }; + auth::proxy_exec( + &provider, + proxy, + USDC_ADDRESS, + Bytes::from(alloy::sol_types::SolCall::abi_encode(&calldata)), + ) .await - .context(format!( - "Failed to confirm USDC approval for {}", - target.name - ))?; + .context(format!("Failed to approve USDC for {} via proxy", target.name))? + } else { + let usdc = IERC20::new(USDC_ADDRESS, &provider); + 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), @@ -178,17 +211,36 @@ async fn set(private_key: Option<&str>, output: OutputFormat) -> Result<()> { 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() + + let tx_hash = if let Some(proxy) = proxy_addr { + let calldata = IERC1155::setApprovalForAllCall { + operator: target.address, + approved: true, + }; + auth::proxy_exec( + &provider, + proxy, + config.conditional_tokens, + Bytes::from(alloy::sol_types::SolCall::abi_encode(&calldata)), + ) .await .context(format!( - "Failed to confirm CTF approval for {}", + "Failed to approve CTF for {} via proxy", target.name - ))?; + ))? + } else { + let ctf = IERC1155::new(config.conditional_tokens, &provider); + 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), diff --git a/src/commands/ctf.rs b/src/commands/ctf.rs index eec7170..405bc62 100644 --- a/src/commands/ctf.rs +++ b/src/commands/ctf.rs @@ -1,4 +1,5 @@ -use alloy::primitives::U256; +use alloy::primitives::{Bytes, U256}; +use alloy::sol; use anyhow::{Context, Result}; use clap::{Args, Subcommand}; use polymarket_client_sdk::ctf::types::{ @@ -6,13 +7,51 @@ use polymarket_client_sdk::ctf::types::{ RedeemNegRiskRequest, RedeemPositionsRequest, SplitPositionRequest, }; use polymarket_client_sdk::types::{Address, B256}; -use polymarket_client_sdk::{POLYGON, ctf}; +use polymarket_client_sdk::{POLYGON, contract_config, ctf}; use rust_decimal::Decimal; use crate::auth; use crate::output::OutputFormat; use crate::output::ctf as ctf_output; +sol! { + #[allow(clippy::exhaustive_enums)] + #[allow(clippy::exhaustive_structs)] + 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; + } + + #[allow(clippy::exhaustive_enums)] + #[allow(clippy::exhaustive_structs)] + interface INegRiskAdapter { + function redeemPositions( + bytes32 conditionId, + uint256[] calldata amounts + ) external; + } +} + const USDC_DECIMALS: Decimal = Decimal::from_parts(1_000_000, 0, 0, false, 0); #[derive(Args)] @@ -183,7 +222,12 @@ fn default_index_sets() -> Vec { vec![U256::from(1), U256::from(2)] } -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, @@ -201,23 +245,48 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s None => default_partition(), }; + let proxy_addr = auth::resolve_proxy_address(private_key, signature_type)?; let provider = auth::create_provider(private_key).await?; - let client = ctf::Client::new(provider, POLYGON)?; - let req = SplitPositionRequest::builder() - .collateral_token(collateral_addr) - .parent_collection_id(parent) - .condition_id(condition_id) - .partition(partition) - .amount(usdc_amount) - .build(); - - let resp = client - .split_position(&req) + if let Some(proxy) = proxy_addr { + let config = + contract_config(POLYGON, false).context("No contract config for Polygon")?; + let calldata = IConditionalTokens::splitPositionCall { + collateralToken: collateral_addr, + parentCollectionId: parent, + conditionId: condition_id, + partition, + amount: usdc_amount, + }; + let tx_hash = auth::proxy_exec( + &provider, + proxy, + config.conditional_tokens, + Bytes::from(alloy::sol_types::SolCall::abi_encode(&calldata)), + ) .await - .context("Split position failed")?; - - ctf_output::print_tx_result("split", resp.transaction_hash, resp.block_number, &output) + .context("Split position via proxy failed")?; + ctf_output::print_tx_result("split", tx_hash, None, &output) + } else { + let client = ctf::Client::new(provider, POLYGON)?; + let req = SplitPositionRequest::builder() + .collateral_token(collateral_addr) + .parent_collection_id(parent) + .condition_id(condition_id) + .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, + Some(resp.block_number), + &output, + ) + } } CtfCommand::Merge { condition, @@ -235,23 +304,48 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s None => default_partition(), }; + let proxy_addr = auth::resolve_proxy_address(private_key, signature_type)?; let provider = auth::create_provider(private_key).await?; - let client = ctf::Client::new(provider, POLYGON)?; - let req = MergePositionsRequest::builder() - .collateral_token(collateral_addr) - .parent_collection_id(parent) - .condition_id(condition_id) - .partition(partition) - .amount(usdc_amount) - .build(); - - let resp = client - .merge_positions(&req) + if let Some(proxy) = proxy_addr { + let config = + contract_config(POLYGON, false).context("No contract config for Polygon")?; + let calldata = IConditionalTokens::mergePositionsCall { + collateralToken: collateral_addr, + parentCollectionId: parent, + conditionId: condition_id, + partition, + amount: usdc_amount, + }; + let tx_hash = auth::proxy_exec( + &provider, + proxy, + config.conditional_tokens, + Bytes::from(alloy::sol_types::SolCall::abi_encode(&calldata)), + ) .await - .context("Merge positions failed")?; - - ctf_output::print_tx_result("merge", resp.transaction_hash, resp.block_number, &output) + .context("Merge positions via proxy failed")?; + ctf_output::print_tx_result("merge", tx_hash, None, &output) + } else { + let client = ctf::Client::new(provider, POLYGON)?; + let req = MergePositionsRequest::builder() + .collateral_token(collateral_addr) + .parent_collection_id(parent) + .condition_id(condition_id) + .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, + Some(resp.block_number), + &output, + ) + } } CtfCommand::Redeem { condition, @@ -267,46 +361,90 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s None => default_index_sets(), }; + let proxy_addr = auth::resolve_proxy_address(private_key, signature_type)?; let provider = auth::create_provider(private_key).await?; - let client = ctf::Client::new(provider, POLYGON)?; - - let req = RedeemPositionsRequest::builder() - .collateral_token(collateral_addr) - .parent_collection_id(parent) - .condition_id(condition_id) - .index_sets(index_sets) - .build(); - let resp = client - .redeem_positions(&req) + if let Some(proxy) = proxy_addr { + let config = + contract_config(POLYGON, false).context("No contract config for Polygon")?; + let calldata = IConditionalTokens::redeemPositionsCall { + collateralToken: collateral_addr, + parentCollectionId: parent, + conditionId: condition_id, + indexSets: index_sets, + }; + let tx_hash = auth::proxy_exec( + &provider, + proxy, + config.conditional_tokens, + Bytes::from(alloy::sol_types::SolCall::abi_encode(&calldata)), + ) .await - .context("Redeem positions failed")?; - - ctf_output::print_tx_result("redeem", resp.transaction_hash, resp.block_number, &output) + .context("Redeem positions via proxy failed")?; + ctf_output::print_tx_result("redeem", tx_hash, None, &output) + } else { + let client = ctf::Client::new(provider, POLYGON)?; + let req = RedeemPositionsRequest::builder() + .collateral_token(collateral_addr) + .parent_collection_id(parent) + .condition_id(condition_id) + .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, + Some(resp.block_number), + &output, + ) + } } CtfCommand::RedeemNegRisk { condition, amounts } => { let condition_id = super::parse_condition_id(&condition)?; let amounts = parse_usdc_amounts(&amounts)?; + let proxy_addr = auth::resolve_proxy_address(private_key, signature_type)?; let provider = auth::create_provider(private_key).await?; - let client = ctf::Client::with_neg_risk(provider, POLYGON)?; - - let req = RedeemNegRiskRequest::builder() - .condition_id(condition_id) - .amounts(amounts) - .build(); - let resp = client - .redeem_neg_risk(&req) + if let Some(proxy) = proxy_addr { + let neg_risk_config = contract_config(POLYGON, true) + .context("No neg-risk contract config for Polygon")?; + let adapter = neg_risk_config + .neg_risk_adapter + .context("No neg-risk adapter address configured")?; + let calldata = INegRiskAdapter::redeemPositionsCall { + conditionId: condition_id, + amounts, + }; + let tx_hash = auth::proxy_exec( + &provider, + proxy, + adapter, + Bytes::from(alloy::sol_types::SolCall::abi_encode(&calldata)), + ) .await - .context("Redeem neg-risk positions failed")?; - - ctf_output::print_tx_result( - "redeem-neg-risk", - resp.transaction_hash, - resp.block_number, - &output, - ) + .context("Redeem neg-risk positions via proxy failed")?; + ctf_output::print_tx_result("redeem-neg-risk", tx_hash, None, &output) + } else { + let client = ctf::Client::with_neg_risk(provider, POLYGON)?; + let req = RedeemNegRiskRequest::builder() + .condition_id(condition_id) + .amounts(amounts) + .build(); + let resp = client + .redeem_neg_risk(&req) + .await + .context("Redeem neg-risk positions failed")?; + ctf_output::print_tx_result( + "redeem-neg-risk", + resp.transaction_hash, + Some(resp.block_number), + &output, + ) + } } CtfCommand::ConditionId { oracle, diff --git a/src/main.rs b/src/main.rs index 61af087..dfd7b20 100644 --- a/src/main.rs +++ b/src/main.rs @@ -151,7 +151,13 @@ pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> { .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( @@ -163,7 +169,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( diff --git a/src/output/ctf.rs b/src/output/ctf.rs index b453ceb..ebc795e 100644 --- a/src/output/ctf.rs +++ b/src/output/ctf.rs @@ -6,7 +6,7 @@ use super::{OutputFormat, print_detail_table}; pub fn print_tx_result( operation: &str, tx_hash: B256, - block_number: u64, + block_number: Option, output: &OutputFormat, ) -> Result<()> { match output { @@ -21,15 +21,17 @@ pub fn print_tx_result( Ok(()) } OutputFormat::Table => { - let rows = vec![ + let mut rows = vec![ ["Operation".into(), operation.to_string()], ["Tx Hash".into(), format!("{tx_hash}")], - ["Block".into(), block_number.to_string()], - [ - "Polygonscan".into(), - format!("https://polygonscan.com/tx/{tx_hash}"), - ], ]; + if let Some(block) = block_number { + rows.push(["Block".into(), block.to_string()]); + } + rows.push([ + "Polygonscan".into(), + format!("https://polygonscan.com/tx/{tx_hash}"), + ]); print_detail_table(rows); Ok(()) }