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,175 changes: 1,126 additions & 49 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ path = "src/main.rs"

[dependencies]
polymarket-client-sdk = { version = "0.4", features = ["gamma", "data", "bridge", "clob", "ctf"] }
alloy = { version = "1.6.3", default-features = false, features = ["providers", "sol-types", "contract", "reqwest", "reqwest-rustls-tls", "signer-local", "signers"] }
alloy = { version = "1.6.3", default-features = false, features = ["eip712", "providers", "sol-types", "contract", "reqwest", "reqwest-rustls-tls", "signer-local", "signers"] }
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
serde_json = "1"
serde = { version = "1", features = ["derive"] }
tabled = "0.17"
rust_decimal = "1"
async-trait = "0.1"
anyhow = "1"
chrono = "0.4"
dirs = "6"
Expand Down
83 changes: 71 additions & 12 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use std::str::FromStr;

use alloy::providers::ProviderBuilder;
use anyhow::{Context, Result};
use alloy::primitives::B256;
use alloy::providers::{Provider, ProviderBuilder};
use anyhow::{Context, Result, bail};
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::types::Address;
use polymarket_client_sdk::{POLYGON, clob};

use crate::config;
use crate::config::{self, SignerType};
use crate::cwp::{CwpSigner, PolySigner};

const DEFAULT_RPC_URL: &str = "https://polygon.drpc.org";

Expand All @@ -23,14 +26,33 @@ fn parse_signature_type(s: &str) -> SignatureType {
}
}

pub fn resolve_signer(
private_key: Option<&str>,
) -> Result<impl polymarket_client_sdk::auth::Signer> {
let (key, _) = config::resolve_key(private_key)?;
let key = key.ok_or_else(|| anyhow::anyhow!("{}", config::NO_WALLET_MSG))?;
LocalSigner::from_str(&key)
.context("Invalid private key")
.map(|s| s.with_chain_id(Some(POLYGON)))
pub fn resolve_signer(private_key: Option<&str>) -> Result<PolySigner> {
// CLI flag or env var always uses local signer
let (key, _source) = config::resolve_key(private_key)?;
if let Some(key) = key {
let signer = LocalSigner::from_str(&key)
.context("Invalid private key")?
.with_chain_id(Some(POLYGON));
return Ok(PolySigner::Local(signer));
}

// Check config for CWP
if let Some(cfg) = config::load_config()? {
if cfg.signer_type == SignerType::Cwp {
let provider = cfg
.cwp_provider
.context("CWP provider not configured")?;
let address: alloy::primitives::Address = cfg
.cwp_address
.context("CWP address not configured")?
.parse()
.context("Invalid CWP address in config")?;
let signer = CwpSigner::new(&provider, address, Some(POLYGON));
return Ok(PolySigner::Cwp(signer));
}
}

bail!("{}", config::NO_WALLET_MSG)
}

pub async fn authenticated_clob_client(
Expand Down Expand Up @@ -66,7 +88,17 @@ pub async fn create_provider(
private_key: Option<&str>,
) -> Result<impl alloy::providers::Provider + Clone> {
let (key, _) = config::resolve_key(private_key)?;
let key = key.ok_or_else(|| anyhow::anyhow!("{}", config::NO_WALLET_MSG))?;
let key = key.ok_or_else(|| {
// Check if CWP wallet is configured — give a better error message
if let Some(cfg) = config::load_config().ok().flatten() {
if cfg.signer_type == SignerType::Cwp {
return anyhow::anyhow!(
"CTF operations require a local wallet. Use `polymarket wallet` to configure one."
);
}
}
anyhow::anyhow!("{}", config::NO_WALLET_MSG)
})?;
let signer = LocalSigner::from_str(&key)
.context("Invalid private key")?
.with_chain_id(Some(POLYGON));
Expand All @@ -77,6 +109,33 @@ pub async fn create_provider(
.context("Failed to connect to Polygon RPC with wallet")
}

pub async fn send_and_confirm_cwp_tx(
cwp_signer: &CwpSigner,
provider: &(impl Provider + Sync),
to: Address,
calldata: Vec<u8>,
) -> Result<B256> {
// Send via CWP wallet (wallet handles gas estimation)
let tx_hash = cwp_signer
.send_transaction(to, calldata, alloy::primitives::U256::ZERO, None)
.await
.context("CWP send-transaction failed")?;

// Wait for on-chain confirmation (poll every 2s, timeout at 2 min)
for i in 0..60 {
if i > 0 {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
if let Some(receipt) = provider.get_transaction_receipt(tx_hash).await? {
if !receipt.status() {
bail!("Transaction {tx_hash} reverted on-chain");
}
return Ok(tx_hash);
}
}
bail!("Transaction {tx_hash} not confirmed after 2 minutes")
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
144 changes: 93 additions & 51 deletions src/commands/approve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

use alloy::primitives::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 crate::auth;
use crate::cwp::PolySigner;
use crate::output::OutputFormat;
use crate::output::approve::{ApprovalStatus, print_approval_status, print_tx_result};

Expand Down Expand Up @@ -135,13 +137,30 @@ 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")?;
struct TxStep<'a> {
step: usize,
total: usize,
label: &'a str,
token_type: &'static str,
contract_name: &'a str,
tx_hash: alloy::primitives::B256,
}

let usdc = IERC20::new(USDC_ADDRESS, provider.clone());
let ctf = IERC1155::new(config.conditional_tokens, provider.clone());
fn emit_result(output: &OutputFormat, results: &mut Vec<serde_json::Value>, tx: &TxStep<'_>) {
match output {
OutputFormat::Table => print_tx_result(tx.step, tx.total, tx.label, tx.tx_hash),
OutputFormat::Json => results.push(serde_json::json!({
"step": tx.step,
"type": tx.token_type,
"contract": tx.contract_name,
"tx_hash": format!("{}", tx.tx_hash),
})),
}
}

async fn set(private_key: Option<&str>, output: OutputFormat) -> Result<()> {
let signer = auth::resolve_signer(private_key)?;
let config = contract_config(POLYGON, false).context("No contract config for Polygon")?;
let targets = approval_targets()?;
let total = targets.len() * 2;

Expand All @@ -152,53 +171,76 @@ async fn set(private_key: Option<&str>, output: OutputFormat) -> Result<()> {
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}"),
})),
match &signer {
PolySigner::Local(_) => {
let provider = auth::create_provider(private_key).await?;
let usdc = IERC20::new(USDC_ADDRESS, provider.clone());
let ctf = IERC1155::new(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
))?;
emit_result(&output, &mut results, &TxStep { step, total, label: &label, token_type: "erc20", contract_name: target.name, 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
))?;
emit_result(&output, &mut results, &TxStep { step, total, label: &label, token_type: "erc1155", contract_name: target.name, 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}"),
})),
PolySigner::Cwp(cwp_signer) => {
let provider = auth::create_readonly_provider().await?;
for target in &targets {
step += 1;
let label = format!("USDC \u{2192} {}", target.name);
let calldata = IERC20::approveCall {
spender: target.address,
value: U256::MAX,
}
.abi_encode();
let tx_hash = auth::send_and_confirm_cwp_tx(cwp_signer, &provider, USDC_ADDRESS, calldata)
.await
.context(format!("Failed USDC approval for {}", target.name))?;
emit_result(&output, &mut results, &TxStep { step, total, label: &label, token_type: "erc20", contract_name: target.name, tx_hash });

step += 1;
let label = format!("CTF \u{2192} {}", target.name);
let calldata = IERC1155::setApprovalForAllCall {
operator: target.address,
approved: true,
}
.abi_encode();
let tx_hash = auth::send_and_confirm_cwp_tx(
cwp_signer,
&provider,
config.conditional_tokens,
calldata,
)
.await
.context(format!("Failed CTF approval for {}", target.name))?;
emit_result(&output, &mut results, &TxStep { step, total, label: &label, token_type: "erc1155", contract_name: target.name, tx_hash });
}
}
}

Expand Down
Loading