From d3d1f23a6f5d28c8f492b56209684c9d72041880 Mon Sep 17 00:00:00 2001 From: croll83 Date: Wed, 25 Feb 2026 17:36:31 +0000 Subject: [PATCH 1/5] feat: add SOCKS5/HTTP proxy support via --proxy flag and POLYMARKET_PROXY env var --- Cargo.lock | 1 + Cargo.toml | 1 + src/main.rs | 15 +++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 26a3032..0b45d9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2976,6 +2976,7 @@ dependencies = [ "dirs", "polymarket-client-sdk", "predicates", + "reqwest 0.13.2", "rust_decimal", "rust_decimal_macros", "rustyline", diff --git a/Cargo.toml b/Cargo.toml index a01bf05..3299fb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ serde = { version = "1", features = ["derive"] } tabled = "0.17" rust_decimal = "1" anyhow = "1" +reqwest = { version = "0.13", features = ["socks"] } chrono = "0.4" dirs = "6" rustyline = "15" diff --git a/src/main.rs b/src/main.rs index 61af087..c2c7d60 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,10 @@ pub(crate) struct Cli { /// Signature type: eoa, proxy, or gnosis-safe #[arg(long, global = true)] signature_type: Option, + + /// SOCKS5 or HTTP proxy URL (e.g., socks5://127.0.0.1:1080) + #[arg(long, global = true)] + proxy: Option, } #[derive(Subcommand)] @@ -71,6 +75,17 @@ async fn main() -> ExitCode { let cli = Cli::parse(); let output = cli.output; + // Apply proxy: --proxy flag > POLYMARKET_PROXY > ALL_PROXY (already set by user) + let proxy_url = cli.proxy + .as_deref() + .map(String::from) + .or_else(|| std::env::var("POLYMARKET_PROXY").ok()); + if let Some(ref url) = proxy_url { + // SAFETY: called before any threads are spawned by the async runtime + unsafe { std::env::set_var("HTTPS_PROXY", url); } + unsafe { std::env::set_var("HTTP_PROXY", url); } + } + if let Err(e) = run(cli).await { match output { OutputFormat::Json => { From 5acf9f772c077a11af188136b4d2eba72d97f1dc Mon Sep 17 00:00:00 2001 From: croll83 Date: Wed, 25 Feb 2026 18:06:23 +0000 Subject: [PATCH 2/5] feat: read proxy URL from config file (priority: --proxy flag > POLYMARKET_PROXY env > config file) --- src/config.rs | 21 +++++++++++++++++++++ src/main.rs | 8 ++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/config.rs b/src/config.rs index d2f5395..fadd3d1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; const ENV_VAR: &str = "POLYMARKET_PRIVATE_KEY"; const SIG_TYPE_ENV_VAR: &str = "POLYMARKET_SIGNATURE_TYPE"; +const PROXY_ENV_VAR: &str = "POLYMARKET_PROXY"; pub const DEFAULT_SIGNATURE_TYPE: &str = "proxy"; pub const NO_WALLET_MSG: &str = @@ -13,10 +14,13 @@ pub const NO_WALLET_MSG: &str = #[derive(Serialize, Deserialize)] pub struct Config { + #[serde(default)] pub private_key: String, pub chain_id: u64, #[serde(default = "default_signature_type")] pub signature_type: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub proxy: Option, } fn default_signature_type() -> String { @@ -98,6 +102,7 @@ pub fn save_wallet(key: &str, chain_id: u64, signature_type: &str) -> Result<()> private_key: key.to_string(), chain_id, signature_type: signature_type.to_string(), + proxy: None, }; let json = serde_json::to_string_pretty(&config)?; let path = config_path()?; @@ -125,6 +130,22 @@ pub fn save_wallet(key: &str, chain_id: u64, signature_type: &str) -> Result<()> Ok(()) } + +/// Priority: CLI flag > env var > config file. +pub fn resolve_proxy(cli_flag: Option<&str>) -> Option { + if let Some(url) = cli_flag { + return Some(url.to_string()); + } + if let Ok(url) = std::env::var(PROXY_ENV_VAR) + && !url.is_empty() + { + return Some(url); + } + if let Some(config) = load_config() { + return config.proxy; + } + None +} /// Priority: CLI flag > env var > config file. pub fn resolve_key(cli_flag: Option<&str>) -> (Option, KeySource) { if let Some(key) = cli_flag { diff --git a/src/main.rs b/src/main.rs index c2c7d60..ff5abbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,12 +75,8 @@ async fn main() -> ExitCode { let cli = Cli::parse(); let output = cli.output; - // Apply proxy: --proxy flag > POLYMARKET_PROXY > ALL_PROXY (already set by user) - let proxy_url = cli.proxy - .as_deref() - .map(String::from) - .or_else(|| std::env::var("POLYMARKET_PROXY").ok()); - if let Some(ref url) = proxy_url { + // Apply proxy: --proxy flag > POLYMARKET_PROXY env > config file proxy field + if let Some(ref url) = config::resolve_proxy(cli.proxy.as_deref()) { // SAFETY: called before any threads are spawned by the async runtime unsafe { std::env::set_var("HTTPS_PROXY", url); } unsafe { std::env::set_var("HTTP_PROXY", url); } From e690e5f06ae3ae4f4aa41e9345fcb9d3828fafc0 Mon Sep 17 00:00:00 2001 From: croll83 Date: Wed, 25 Feb 2026 18:13:36 +0000 Subject: [PATCH 3/5] fix: set env vars before tokio runtime, exclude alloy RPC from proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback: - Move from #[tokio::main] to sync fn main() so set_var runs before any worker threads are spawned (eliminates UB on Rust 2024 edition) - Add NO_PROXY=polygon.drpc.org,drpc.org to exclude Polygon RPC from SOCKS5 proxying — alloy depends on reqwest 0.12 which lacks socks support and would fail if HTTP(S)_PROXY points to a socks5:// URL - Build tokio runtime manually with Builder::new_multi_thread() Tested: CLOB calls route through SOCKS5, approve check (alloy RPC) bypasses proxy and queries Polygon directly. --- src/main.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index ff5abbd..1b74553 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,19 +70,32 @@ enum Commands { Upgrade, } -#[tokio::main] -async fn main() -> ExitCode { +fn main() -> ExitCode { + // Resolve proxy BEFORE tokio spawns worker threads. + // Parse CLI args early (sync) to get --proxy flag. let cli = Cli::parse(); - let output = cli.output; - // Apply proxy: --proxy flag > POLYMARKET_PROXY env > config file proxy field + // Apply proxy: --proxy flag > POLYMARKET_PROXY env > config file proxy field. + // Only set HTTP(S)_PROXY for CLOB/Gamma API calls. + // Exclude the Polygon RPC so alloy (which uses reqwest 0.12 without socks + // support) can still reach the RPC directly. if let Some(ref url) = config::resolve_proxy(cli.proxy.as_deref()) { - // SAFETY: called before any threads are spawned by the async runtime - unsafe { std::env::set_var("HTTPS_PROXY", url); } - unsafe { std::env::set_var("HTTP_PROXY", url); } + // SAFETY: no threads exist yet — called before tokio runtime is built. + unsafe { + std::env::set_var("HTTPS_PROXY", url); + std::env::set_var("HTTP_PROXY", url); + std::env::set_var("NO_PROXY", "polygon.drpc.org,drpc.org"); + } } - if let Err(e) = run(cli).await { + let output = cli.output; + + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("Failed to build tokio runtime"); + + if let Err(e) = runtime.block_on(run(cli)) { match output { OutputFormat::Json => { println!("{}", serde_json::json!({"error": e.to_string()})); From 27cb3e4b5a058125073ffbddd6ad68d4b27f5939 Mon Sep 17 00:00:00 2001 From: Marco Monaco Date: Wed, 25 Feb 2026 20:26:11 +0000 Subject: [PATCH 4/5] fix: preserve proxy in save_wallet, handle empty private_key Address cursor bot review: - save_wallet now reads existing proxy from config before overwriting, preventing silent loss of user-configured proxy URL - resolve_key treats empty string from config as None, so callers get the helpful "No wallet configured" message instead of a confusing "Invalid private key" error when private_key is absent from config --- src/config.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index fadd3d1..df299e5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -98,11 +98,14 @@ pub fn save_wallet(key: &str, chain_id: u64, signature_type: &str) -> Result<()> fs::set_permissions(&dir, fs::Permissions::from_mode(0o700))?; } + // Preserve existing proxy setting from config file + let existing_proxy = load_config().and_then(|c| c.proxy); + let config = Config { private_key: key.to_string(), chain_id, signature_type: signature_type.to_string(), - proxy: None, + proxy: existing_proxy, }; let json = serde_json::to_string_pretty(&config)?; let path = config_path()?; @@ -157,7 +160,9 @@ pub fn resolve_key(cli_flag: Option<&str>) -> (Option, KeySource) { return (Some(key), KeySource::EnvVar); } if let Some(config) = load_config() { - return (Some(config.private_key), KeySource::ConfigFile); + if !config.private_key.is_empty() { + return (Some(config.private_key), KeySource::ConfigFile); + } } (None, KeySource::None) } From 489d83ed9cb1b1de375db6c8d8607f1989f1eed2 Mon Sep 17 00:00:00 2001 From: Marco Monaco Date: Thu, 26 Feb 2026 00:38:22 +0100 Subject: [PATCH 5/5] fix: derive trading wallet from signature_type config (gnosis-safe support) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wallet show/create/import now use the configured signature_type to derive the correct trading wallet address: - "proxy" → EIP-1167 minimal proxy (Magic/email wallets) - "gnosis-safe" → Gnosis Safe 1-of-1 (browser/MetaMask wallets) - "eoa" → no derived wallet, use EOA directly Previously all wallet commands hardcoded derive_proxy_wallet(), ignoring the signature_type setting entirely. This caused the CLI to show the wrong deposit address when using gnosis-safe mode (which is what polymarket.com creates for browser wallet users). Co-Authored-By: Claude Opus 4.6 --- src/commands/setup.rs | 22 ++++++++++---------- src/commands/wallet.rs | 47 ++++++++++++++++++++++++++++-------------- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/commands/setup.rs b/src/commands/setup.rs index dd04671..7d5082f 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -5,9 +5,9 @@ use std::str::FromStr; use anyhow::{Context, Result}; use polymarket_client_sdk::auth::{LocalSigner, Signer as _}; use polymarket_client_sdk::types::Address; -use polymarket_client_sdk::{POLYGON, derive_proxy_wallet}; +use polymarket_client_sdk::POLYGON; -use super::wallet::normalize_key; +use super::wallet::{derive_trading_wallet, normalize_key}; use crate::config; fn print_banner() { @@ -154,18 +154,18 @@ fn setup_wallet() -> Result
{ fn finish_setup(address: Address) -> Result<()> { let total = 4; - step_header(2, total, "Proxy Wallet"); + step_header(2, total, "Trading Wallet"); - let proxy = derive_proxy_wallet(address, POLYGON); - match proxy { - Some(proxy) => { - println!(" ✓ Proxy wallet derived"); - println!(" Proxy: {proxy}"); + let sig_type = config::resolve_signature_type(None); + let trading = derive_trading_wallet(address, POLYGON, &sig_type); + match trading { + Some(tw) => { + println!(" ✓ Trading wallet derived ({sig_type})"); + println!(" Trading wallet: {tw}"); println!(" Deposit USDC to this address to start trading."); } None => { - println!(" ✗ Could not derive proxy wallet"); - println!(" You may need to use --signature-type eoa"); + println!(" ℹ Using EOA directly (signature type: {sig_type})"); } } @@ -173,7 +173,7 @@ fn finish_setup(address: Address) -> Result<()> { step_header(3, total, "Fund Wallet"); - let deposit_addr = proxy.unwrap_or(address); + let deposit_addr = trading.unwrap_or(address); println!(" ○ Deposit USDC to your wallet to start trading"); println!(" Run: polymarket bridge deposit {deposit_addr}"); println!(" Or transfer USDC directly on Polygon"); diff --git a/src/commands/wallet.rs b/src/commands/wallet.rs index ce7597f..8b46109 100644 --- a/src/commands/wallet.rs +++ b/src/commands/wallet.rs @@ -5,11 +5,27 @@ use anyhow::{Context, Result, bail}; use clap::{Args, Subcommand}; use polymarket_client_sdk::auth::LocalSigner; use polymarket_client_sdk::auth::Signer as _; -use polymarket_client_sdk::{POLYGON, derive_proxy_wallet}; +use polymarket_client_sdk::{POLYGON, derive_proxy_wallet, derive_safe_wallet}; use crate::config; use crate::output::OutputFormat; +/// Derive the trading wallet address based on the configured signature type. +/// - "proxy" → Polymarket Proxy wallet (Magic/email) +/// - "gnosis-safe" → Gnosis Safe wallet (browser/MetaMask) +/// - anything else (e.g. "eoa") → None (the EOA itself is used) +pub(crate) fn derive_trading_wallet( + address: polymarket_client_sdk::types::Address, + chain_id: u64, + signature_type: &str, +) -> Option { + match signature_type { + "proxy" => derive_proxy_wallet(address, chain_id), + "gnosis-safe" => derive_safe_wallet(address, chain_id), + _ => None, + } +} + #[derive(Args)] pub struct WalletArgs { #[command(subcommand)] @@ -103,7 +119,7 @@ fn cmd_create(output: &OutputFormat, force: bool, signature_type: &str) -> Resul config::save_wallet(&key_hex, POLYGON, signature_type)?; let config_path = config::config_path()?; - let proxy_addr = derive_proxy_wallet(address, POLYGON); + let trading_addr = derive_trading_wallet(address, POLYGON, signature_type); match output { OutputFormat::Json => { @@ -111,7 +127,7 @@ fn cmd_create(output: &OutputFormat, force: bool, signature_type: &str) -> Resul "{}", serde_json::json!({ "address": address.to_string(), - "proxy_address": proxy_addr.map(|a| a.to_string()), + "trading_wallet": trading_addr.map(|a| a.to_string()), "signature_type": signature_type, "config_path": config_path.display().to_string(), }) @@ -120,8 +136,8 @@ fn cmd_create(output: &OutputFormat, force: bool, signature_type: &str) -> Resul OutputFormat::Table => { println!("Wallet created successfully!"); println!("Address: {address}"); - if let Some(proxy) = proxy_addr { - println!("Proxy wallet: {proxy}"); + if let Some(tw) = trading_addr { + println!("Trading wallet: {tw}"); } println!("Signature type: {signature_type}"); println!("Config: {}", config_path.display()); @@ -144,7 +160,7 @@ fn cmd_import(key: &str, output: &OutputFormat, force: bool, signature_type: &st config::save_wallet(&normalized, POLYGON, signature_type)?; let config_path = config::config_path()?; - let proxy_addr = derive_proxy_wallet(address, POLYGON); + let trading_addr = derive_trading_wallet(address, POLYGON, signature_type); match output { OutputFormat::Json => { @@ -152,7 +168,7 @@ fn cmd_import(key: &str, output: &OutputFormat, force: bool, signature_type: &st "{}", serde_json::json!({ "address": address.to_string(), - "proxy_address": proxy_addr.map(|a| a.to_string()), + "trading_wallet": trading_addr.map(|a| a.to_string()), "signature_type": signature_type, "config_path": config_path.display().to_string(), }) @@ -161,8 +177,8 @@ fn cmd_import(key: &str, output: &OutputFormat, force: bool, signature_type: &st OutputFormat::Table => { println!("Wallet imported successfully!"); println!("Address: {address}"); - if let Some(proxy) = proxy_addr { - println!("Proxy wallet: {proxy}"); + if let Some(tw) = trading_addr { + println!("Trading wallet: {tw}"); } println!("Signature type: {signature_type}"); println!("Config: {}", config_path.display()); @@ -193,12 +209,13 @@ fn cmd_show(output: &OutputFormat, private_key_flag: Option<&str>) -> Result<()> let (key, source) = config::resolve_key(private_key_flag); let signer = key.as_deref().and_then(|k| LocalSigner::from_str(k).ok()); let address = signer.as_ref().map(|s| s.address().to_string()); - let proxy_addr = signer + + let sig_type = config::resolve_signature_type(None); + let trading_addr = signer .as_ref() - .and_then(|s| derive_proxy_wallet(s.address(), POLYGON)) + .and_then(|s| derive_trading_wallet(s.address(), POLYGON, &sig_type)) .map(|a| a.to_string()); - let sig_type = config::resolve_signature_type(None); let config_path = config::config_path()?; match output { @@ -207,7 +224,7 @@ fn cmd_show(output: &OutputFormat, private_key_flag: Option<&str>) -> Result<()> "{}", serde_json::json!({ "address": address, - "proxy_address": proxy_addr, + "trading_wallet": trading_addr, "signature_type": sig_type, "config_path": config_path.display().to_string(), "source": source.label(), @@ -220,8 +237,8 @@ fn cmd_show(output: &OutputFormat, private_key_flag: Option<&str>) -> Result<()> Some(addr) => println!("Address: {addr}"), None => println!("Address: (not configured)"), } - if let Some(proxy) = &proxy_addr { - println!("Proxy wallet: {proxy}"); + if let Some(tw) = &trading_addr { + println!("Trading wallet: {tw}"); } println!("Signature type: {sig_type}"); println!("Config path: {}", config_path.display());