diff --git a/Cargo.lock b/Cargo.lock
index 26a3032..1bdc832 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -41,6 +41,7 @@ dependencies = [
"alloy-network",
"alloy-provider",
"alloy-rpc-client",
+ "alloy-rpc-types",
"alloy-serde",
"alloy-signer",
"alloy-signer-local",
@@ -403,6 +404,18 @@ dependencies = [
"wasmtimer",
]
+[[package]]
+name = "alloy-rpc-types"
+version = "1.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bdcbf9dfd5eea8bfeb078b1d906da8cd3a39c4d4dbe7a628025648e323611f6"
+dependencies = [
+ "alloy-primitives",
+ "alloy-rpc-types-eth",
+ "alloy-serde",
+ "serde",
+]
+
[[package]]
name = "alloy-rpc-types-any"
version = "1.7.3"
diff --git a/Cargo.toml b/Cargo.toml
index a01bf05..2707e4c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,7 +15,7 @@ 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 = ["providers", "rpc-types", "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"
diff --git a/src/commands/approve.rs b/src/commands/approve.rs
index b52dc0c..86aba39 100644
--- a/src/commands/approve.rs
+++ b/src/commands/approve.rs
@@ -3,6 +3,7 @@
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};
@@ -12,6 +13,8 @@ use crate::auth;
use crate::output::OutputFormat;
use crate::output::approve::{ApprovalStatus, print_approval_status, print_tx_result};
+use super::proxy;
+
/// Polygon USDC (same address as `USDC_ADDRESS_STR`; `address!` requires a literal).
const USDC_ADDRESS: Address = address!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174");
@@ -81,20 +84,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
,
private_key: Option<&str>,
+ signature_type: Option<&str>,
output: OutputFormat,
) -> Result<()> {
let owner: Address = if let Some(addr) = address_arg {
addr
+ } else if proxy::is_proxy_mode(signature_type)? {
+ proxy::derive_proxy_address(private_key)?
} else {
let signer = auth::resolve_signer(private_key)?;
polymarket_client_sdk::auth::Signer::address(&signer)
@@ -135,13 +144,13 @@ 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?;
+async fn set(
+ private_key: Option<&str>,
+ signature_type: Option<&str>,
+ output: OutputFormat,
+) -> Result<()> {
+ let use_proxy = proxy::is_proxy_mode(signature_type)?;
let config = contract_config(POLYGON, false).context("No contract config for Polygon")?;
-
- let usdc = IERC20::new(USDC_ADDRESS, provider.clone());
- let ctf = IERC1155::new(config.conditional_tokens, provider.clone());
-
let targets = approval_targets()?;
let total = targets.len() * 2;
@@ -155,17 +164,14 @@ async fn set(private_key: Option<&str>, output: OutputFormat) -> Result<()> {
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()
+ let calldata = IERC20::approveCall {
+ spender: target.address,
+ value: U256::MAX,
+ }
+ .abi_encode();
+ let (tx_hash, _) = proxy::send_call(private_key, use_proxy, USDC_ADDRESS, calldata)
.await
- .context(format!(
- "Failed to confirm USDC approval for {}",
- target.name
- ))?;
+ .context(format!("Failed USDC approval for {}", target.name))?;
match output {
OutputFormat::Table => print_tx_result(step, total, &label, tx_hash),
@@ -179,17 +185,15 @@ async fn set(private_key: Option<&str>, output: OutputFormat) -> Result<()> {
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
- ))?;
+ let calldata = IERC1155::setApprovalForAllCall {
+ operator: target.address,
+ approved: true,
+ }
+ .abi_encode();
+ let (tx_hash, _) =
+ proxy::send_call(private_key, use_proxy, config.conditional_tokens, calldata)
+ .await
+ .context(format!("Failed CTF approval for {}", target.name))?;
match output {
OutputFormat::Table => print_tx_result(step, total, &label, tx_hash),
diff --git a/src/commands/ctf.rs b/src/commands/ctf.rs
index 5d06ea5..f87aa0e 100644
--- a/src/commands/ctf.rs
+++ b/src/commands/ctf.rs
@@ -1,9 +1,10 @@
use alloy::primitives::U256;
+use alloy::sol;
+use alloy::sol_types::SolCall;
use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use polymarket_client_sdk::ctf::types::{
- CollectionIdRequest, ConditionIdRequest, MergePositionsRequest, PositionIdRequest,
- RedeemNegRiskRequest, RedeemPositionsRequest, SplitPositionRequest,
+ CollectionIdRequest, ConditionIdRequest, PositionIdRequest,
};
use polymarket_client_sdk::types::{Address, B256};
use polymarket_client_sdk::{POLYGON, ctf};
@@ -13,8 +14,43 @@ use crate::auth;
use crate::output::OutputFormat;
use crate::output::ctf as ctf_output;
+use super::proxy;
use super::{USDC_ADDRESS_STR, USDC_DECIMALS};
+sol! {
+ interface IConditionalTokens {
+ function splitPosition(
+ address collateralToken,
+ bytes32 parentCollectionId,
+ bytes32 conditionId,
+ uint256[] calldata partition,
+ uint256 amount
+ ) external;
+
+ function mergePositions(
+ address collateralToken,
+ bytes32 parentCollectionId,
+ bytes32 conditionId,
+ uint256[] calldata partition,
+ uint256 amount
+ ) external;
+
+ function redeemPositions(
+ address collateralToken,
+ bytes32 parentCollectionId,
+ bytes32 conditionId,
+ uint256[] calldata indexSets
+ ) external;
+ }
+
+ interface INegRiskAdapter {
+ function redeemPositions(
+ bytes32 conditionId,
+ uint256[] calldata amounts
+ ) external;
+ }
+}
+
#[derive(Args)]
pub struct CtfArgs {
#[command(subcommand)]
@@ -171,7 +207,12 @@ fn binary_u256_vec() -> Vec {
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,
@@ -187,23 +228,24 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s
None => binary_u256_vec(),
};
- let provider = auth::create_provider(private_key).await?;
- let client = ctf::Client::new(provider, POLYGON)?;
-
- let req = SplitPositionRequest::builder()
- .collateral_token(collateral)
- .parent_collection_id(parent)
- .condition_id(condition)
- .partition(partition)
- .amount(usdc_amount)
- .build();
-
- let resp = client
- .split_position(&req)
- .await
- .context("Split position failed")?;
-
- ctf_output::print_tx_result("split", resp.transaction_hash, resp.block_number, &output)
+ let proxy = proxy::is_proxy_mode(signature_type)?;
+ let config = polymarket_client_sdk::contract_config(POLYGON, false)
+ .context("CTF contract config not found")?;
+ let calldata = IConditionalTokens::splitPositionCall {
+ collateralToken: collateral,
+ parentCollectionId: parent,
+ conditionId: condition,
+ partition,
+ amount: usdc_amount,
+ }
+ .abi_encode();
+
+ let (tx_hash, block_number) =
+ proxy::send_call(private_key, proxy, config.conditional_tokens, calldata)
+ .await
+ .context("Split position failed")?;
+
+ ctf_output::print_tx_result("split", tx_hash, block_number, &output)
}
CtfCommand::Merge {
condition,
@@ -219,23 +261,24 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s
None => binary_u256_vec(),
};
- let provider = auth::create_provider(private_key).await?;
- let client = ctf::Client::new(provider, POLYGON)?;
-
- let req = MergePositionsRequest::builder()
- .collateral_token(collateral)
- .parent_collection_id(parent)
- .condition_id(condition)
- .partition(partition)
- .amount(usdc_amount)
- .build();
-
- let resp = client
- .merge_positions(&req)
- .await
- .context("Merge positions failed")?;
-
- ctf_output::print_tx_result("merge", resp.transaction_hash, resp.block_number, &output)
+ let proxy = proxy::is_proxy_mode(signature_type)?;
+ let config = polymarket_client_sdk::contract_config(POLYGON, false)
+ .context("CTF contract config not found")?;
+ let calldata = IConditionalTokens::mergePositionsCall {
+ collateralToken: collateral,
+ parentCollectionId: parent,
+ conditionId: condition,
+ partition,
+ amount: usdc_amount,
+ }
+ .abi_encode();
+
+ let (tx_hash, block_number) =
+ proxy::send_call(private_key, proxy, config.conditional_tokens, calldata)
+ .await
+ .context("Merge positions failed")?;
+
+ ctf_output::print_tx_result("merge", tx_hash, block_number, &output)
}
CtfCommand::Redeem {
condition,
@@ -249,45 +292,44 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s
None => binary_u256_vec(),
};
- let provider = auth::create_provider(private_key).await?;
- let client = ctf::Client::new(provider, POLYGON)?;
-
- let req = RedeemPositionsRequest::builder()
- .collateral_token(collateral)
- .parent_collection_id(parent)
- .condition_id(condition)
- .index_sets(index_sets)
- .build();
-
- let resp = client
- .redeem_positions(&req)
- .await
- .context("Redeem positions failed")?;
-
- ctf_output::print_tx_result("redeem", resp.transaction_hash, resp.block_number, &output)
+ let proxy = proxy::is_proxy_mode(signature_type)?;
+ let config = polymarket_client_sdk::contract_config(POLYGON, false)
+ .context("CTF contract config not found")?;
+ let calldata = IConditionalTokens::redeemPositionsCall {
+ collateralToken: collateral,
+ parentCollectionId: parent,
+ conditionId: condition,
+ indexSets: index_sets,
+ }
+ .abi_encode();
+
+ let (tx_hash, block_number) =
+ proxy::send_call(private_key, proxy, config.conditional_tokens, calldata)
+ .await
+ .context("Redeem positions failed")?;
+
+ ctf_output::print_tx_result("redeem", tx_hash, block_number, &output)
}
CtfCommand::RedeemNegRisk { condition, amounts } => {
let amounts = parse_usdc_amounts(&amounts)?;
- let provider = auth::create_provider(private_key).await?;
- let client = ctf::Client::with_neg_risk(provider, POLYGON)?;
-
- let req = RedeemNegRiskRequest::builder()
- .condition_id(condition)
- .amounts(amounts)
- .build();
-
- let resp = client
- .redeem_neg_risk(&req)
+ let proxy = proxy::is_proxy_mode(signature_type)?;
+ let config = polymarket_client_sdk::contract_config(POLYGON, true)
+ .context("NegRisk contract config not found")?;
+ let target = config
+ .neg_risk_adapter
+ .context("NegRisk adapter not configured")?;
+ let calldata = INegRiskAdapter::redeemPositionsCall {
+ conditionId: condition,
+ amounts,
+ }
+ .abi_encode();
+
+ let (tx_hash, block_number) = proxy::send_call(private_key, proxy, target, calldata)
.await
.context("Redeem neg-risk positions failed")?;
- ctf_output::print_tx_result(
- "redeem-neg-risk",
- resp.transaction_hash,
- resp.block_number,
- &output,
- )
+ ctf_output::print_tx_result("redeem-neg-risk", tx_hash, block_number, &output)
}
CtfCommand::ConditionId {
oracle,
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index d4c985b..1788b44 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -1,6 +1,82 @@
pub(crate) const USDC_ADDRESS_STR: &str = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
pub(crate) const USDC_DECIMALS: u32 = 6;
+pub(crate) mod proxy {
+ use alloy::primitives::U256;
+ use alloy::sol;
+ use anyhow::{Context, Result};
+ use polymarket_client_sdk::POLYGON;
+ use polymarket_client_sdk::types::{Address, B256};
+
+ use crate::auth;
+
+ // Polymarket Proxy Wallet Factory interface (CallType: INVALID=0, CALL=1, DELEGATECALL=2)
+ sol! {
+ #[sol(rpc)]
+ interface IProxyWallet {
+ struct ProxyCall {
+ uint8 typeCode;
+ address to;
+ uint256 value;
+ bytes data;
+ }
+
+ function proxy(ProxyCall[] memory calls)
+ external payable returns (bytes[] memory returnValues);
+ }
+ }
+
+ const PROXY_FACTORY: Address =
+ polymarket_client_sdk::types::address!("0xaB45c5A4B0c941a2F231C04C3f49182e1A254052");
+
+ pub fn is_proxy_mode(signature_type: Option<&str>) -> Result {
+ Ok(crate::config::resolve_signature_type(signature_type)? == "proxy")
+ }
+
+ pub fn derive_proxy_address(private_key: Option<&str>) -> Result {
+ let signer = auth::resolve_signer(private_key)?;
+ let eoa = polymarket_client_sdk::auth::Signer::address(&signer);
+ polymarket_client_sdk::derive_proxy_wallet(eoa, POLYGON)
+ .ok_or_else(|| anyhow::anyhow!("Proxy wallet derivation not supported on this chain"))
+ }
+
+ pub async fn send_call(
+ private_key: Option<&str>,
+ use_proxy: bool,
+ target: Address,
+ calldata: Vec,
+ ) -> Result<(B256, u64)> {
+ use alloy::providers::Provider as _;
+
+ let provider = auth::create_provider(private_key).await?;
+
+ let (tx_hash, block_number) = if use_proxy {
+ let factory = IProxyWallet::new(PROXY_FACTORY, &provider);
+ let call = IProxyWallet::ProxyCall {
+ typeCode: 1,
+ to: target,
+ value: U256::ZERO,
+ data: calldata.into(),
+ };
+ let pending = factory.proxy(vec![call]).send().await?;
+ let hash = *pending.tx_hash();
+ let receipt = pending.get_receipt().await?;
+ (hash, receipt.block_number)
+ } else {
+ let tx = alloy::rpc::types::TransactionRequest::default()
+ .to(target)
+ .input(alloy::primitives::Bytes::from(calldata).into());
+ let pending = provider.send_transaction(tx).await?;
+ let hash = *pending.tx_hash();
+ let receipt = pending.get_receipt().await?;
+ (hash, receipt.block_number)
+ };
+
+ let block_number = block_number.context("Block number not available in receipt")?;
+ Ok((tx_hash, block_number))
+ }
+}
+
pub(crate) mod approve;
pub(crate) mod bridge;
pub(crate) mod clob;
diff --git a/src/main.rs b/src/main.rs
index 2abb55f..da01221 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -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(
@@ -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,