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
26 changes: 25 additions & 1 deletion src/auth.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<Address> {
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::*;
Expand Down
213 changes: 155 additions & 58 deletions src/commands/approve.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -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)]
Expand Down Expand Up @@ -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<Address>,
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?;
Expand Down Expand Up @@ -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<serde_json::Value> = 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}"),
})),
}
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/commands/ctf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,12 @@ fn binary_u256_vec() -> Vec<U256> {
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,
Expand Down
16 changes: 14 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down