Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ serde = { version = "1", features = ["derive"] }
tabled = "0.17"
rust_decimal = "1"
anyhow = "1"
reqwest = { version = "0.13", features = ["socks"] }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cargo.lock missing tokio-socks breaks SOCKS5 proxy support

High Severity

The Cargo.toml adds reqwest with features = ["socks"], but the committed Cargo.lock does not contain tokio-socks anywhere — the required transitive dependency for SOCKS5 support. The reqwest 0.13.2 entry in the lockfile lists no socks-related dependencies. This means builds using cargo install --locked or CI with --locked will compile reqwest without actual SOCKS5 support, silently making the PR's core feature non-functional at runtime.

Additional Locations (1)

Fix in Cursor Fix in Web

chrono = "0.4"
dirs = "6"
rustyline = "15"
Expand Down
22 changes: 11 additions & 11 deletions src/commands/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -154,26 +154,26 @@ fn setup_wallet() -> Result<Address> {
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})");
}
}

println!();

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");
Expand Down
47 changes: 32 additions & 15 deletions src/commands/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<polymarket_client_sdk::types::Address> {
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)]
Expand Down Expand Up @@ -103,15 +119,15 @@ 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 => {
println!(
"{}",
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(),
})
Expand All @@ -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());
Expand All @@ -144,15 +160,15 @@ 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 => {
println!(
"{}",
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(),
})
Expand All @@ -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());
Expand Down Expand Up @@ -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 {
Expand All @@ -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(),
Expand All @@ -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());
Expand Down
28 changes: 27 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@ 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 =
"No wallet configured. Run `polymarket wallet create` or `polymarket wallet import <key>`";

#[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<String>,
}

fn default_signature_type() -> String {
Expand Down Expand Up @@ -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()?;
Expand Down Expand Up @@ -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<String> {
if let Some(url) = cli_flag {
return Some(url.to_string());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty proxy string not filtered from CLI flag

Low Severity

resolve_proxy filters empty strings for the POLYMARKET_PROXY env var path (via !url.is_empty()) but does not apply the same check to the CLI flag path or the config file path. An empty --proxy "" value or an empty proxy string in the config file would result in setting HTTPS_PROXY and HTTP_PROXY to empty strings, likely causing confusing request failures.

Fix in Cursor Fix in Web

}
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<String>, KeySource) {
if let Some(key) = cli_flag {
Expand All @@ -136,7 +160,9 @@ pub fn resolve_key(cli_flag: Option<&str>) -> (Option<String>, 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)
}
Expand Down
30 changes: 27 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub(crate) struct Cli {
/// Signature type: eoa, proxy, or gnosis-safe
#[arg(long, global = true)]
signature_type: Option<String>,

/// SOCKS5 or HTTP proxy URL (e.g., socks5://127.0.0.1:1080)
#[arg(long, global = true)]
proxy: Option<String>,
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -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");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NO_PROXY overwrites existing user environment variable values

Medium Severity

Setting NO_PROXY to a hardcoded value unconditionally replaces any existing NO_PROXY entries the user already has in their environment. Since this feature specifically targets corporate/VPN users — who commonly have NO_PROXY configured for internal services — this overwrites those entries within the CLI process. The existing entries need to be preserved by reading the current NO_PROXY value and appending polygon.drpc.org,drpc.org to it rather than replacing it.

Fix in Cursor Fix in Web

}
}

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()}));
Expand Down