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,