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/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()); diff --git a/src/config.rs b/src/config.rs index d2f5395..df299e5 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 { @@ -94,10 +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: existing_proxy, }; let json = serde_json::to_string_pretty(&config)?; let path = config_path()?; @@ -125,6 +133,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 { @@ -136,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) } diff --git a/src/main.rs b/src/main.rs index 61af087..1b74553 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)] @@ -66,12 +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(); + + // 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: 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"); + } + } + let output = cli.output; - if let Err(e) = run(cli).await { + 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()}));