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,