mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-07-27 20:47:46 +00:00
fix: support V2 proxy wallet funders
This commit is contained in:
@@ -53,6 +53,13 @@ export POLYMARKET_PASSPHRASE="your_passphrase"
|
||||
export POLYMARKET_HOST="https://clob.polymarket.com"
|
||||
export POLYMARKET_CHAIN_ID="137"
|
||||
|
||||
# Optional, but required when funds live in a Polymarket proxy/Safe wallet.
|
||||
# 0 = EOA, 1 = Proxy, 2 = Gnosis Safe/browser wallet, 3 = Poly1271.
|
||||
export POLYMARKET_SIGNATURE_TYPE="2"
|
||||
|
||||
# Optional: override the derived funder/proxy wallet address.
|
||||
export POLYMARKET_FUNDER_ADDRESS="0x..."
|
||||
|
||||
# Optional: pin clob.polymarket.com to a known A record if local DNS is blocked.
|
||||
# Keep the hostname in POLYMARKET_HOST so HTTPS/SNI still validates.
|
||||
export POLYMARKET_RESOLVE_IP="104.18.34.205"
|
||||
|
||||
@@ -100,6 +100,8 @@ impl PolyfillDemo {
|
||||
private_key: None,
|
||||
api_credentials: None,
|
||||
builder_code: None,
|
||||
signature_type: None,
|
||||
funder: None,
|
||||
timeout: Some(Duration::from_secs(30)),
|
||||
max_connections: Some(100),
|
||||
};
|
||||
|
||||
+29
-5
@@ -11,7 +11,7 @@ use crate::types::{
|
||||
MarketOrderArgs, OrderArgs, OrderType, PostOrder, PostOrderOptions, PostOrderResponse, Side,
|
||||
SignedOrderRequest,
|
||||
};
|
||||
use alloy_primitives::U256;
|
||||
use alloy_primitives::{Address, U256};
|
||||
use alloy_signer_local::PrivateKeySigner;
|
||||
use reqwest::header::HeaderName;
|
||||
use reqwest::Client;
|
||||
@@ -99,6 +99,8 @@ impl ClobClient {
|
||||
signer: Option<PrivateKeySigner>,
|
||||
api_creds: Option<ApiCreds>,
|
||||
builder_code: Option<String>,
|
||||
sig_type: Option<crate::orders::SigType>,
|
||||
funder: Option<Address>,
|
||||
) -> Self {
|
||||
let dns_cache = tokio::runtime::Handle::try_current().ok().and_then(|_| {
|
||||
tokio::task::block_in_place(|| {
|
||||
@@ -132,7 +134,7 @@ impl ClobClient {
|
||||
|
||||
let order_builder = signer
|
||||
.clone()
|
||||
.map(|signer| crate::orders::OrderBuilder::new(signer, None, None));
|
||||
.map(|signer| crate::orders::OrderBuilder::new(signer, sig_type, funder));
|
||||
|
||||
Self {
|
||||
http_client,
|
||||
@@ -152,7 +154,7 @@ impl ClobClient {
|
||||
/// Now includes DNS caching, connection management, and buffer pooling
|
||||
pub fn new(host: &str) -> Self {
|
||||
let http_client = build_http_client(host, None, None);
|
||||
Self::build_client(host, 137, http_client, None, None, None)
|
||||
Self::build_client(host, 137, http_client, None, None, None, None, None)
|
||||
}
|
||||
|
||||
/// Create a V2-native client from config.
|
||||
@@ -166,6 +168,26 @@ impl ClobClient {
|
||||
None => None,
|
||||
};
|
||||
|
||||
let sig_type = config
|
||||
.signature_type
|
||||
.map(crate::orders::sig_type_from_u8)
|
||||
.transpose()?;
|
||||
let explicit_funder = config
|
||||
.funder
|
||||
.as_deref()
|
||||
.map(Address::from_str)
|
||||
.transpose()
|
||||
.map_err(|e| PolyfillError::config(format!("Invalid funder address: {e}")))?;
|
||||
let funder = match (&signer, sig_type) {
|
||||
(Some(signer), Some(sig_type)) => crate::orders::resolve_funder(
|
||||
signer.address(),
|
||||
config.chain,
|
||||
sig_type,
|
||||
explicit_funder,
|
||||
)?,
|
||||
_ => explicit_funder,
|
||||
};
|
||||
|
||||
let http_client =
|
||||
build_http_client(&config.base_url, config.timeout, config.max_connections);
|
||||
|
||||
@@ -176,6 +198,8 @@ impl ClobClient {
|
||||
signer,
|
||||
config.api_credentials,
|
||||
config.builder_code,
|
||||
sig_type,
|
||||
funder,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -187,7 +211,7 @@ impl ClobClient {
|
||||
.build()
|
||||
.expect("Failed to build reqwest client")
|
||||
});
|
||||
Self::build_client(host, 137, http_client, None, None, None)
|
||||
Self::build_client(host, 137, http_client, None, None, None, None, None)
|
||||
}
|
||||
|
||||
/// Create a client optimized for internet connections
|
||||
@@ -198,7 +222,7 @@ impl ClobClient {
|
||||
.build()
|
||||
.expect("Failed to build reqwest client")
|
||||
});
|
||||
Self::build_client(host, 137, http_client, None, None, None)
|
||||
Self::build_client(host, 137, http_client, None, None, None, None, None)
|
||||
}
|
||||
|
||||
/// Create a client with L1 headers (for authentication)
|
||||
|
||||
+97
-2
@@ -8,7 +8,7 @@ use crate::errors::{PolyfillError, Result};
|
||||
use crate::types::{
|
||||
CreateOrderOptions, MarketOrderArgs, OrderArgs, OrderType, Side, SignedOrderRequest,
|
||||
};
|
||||
use alloy_primitives::{Address, B256, U256};
|
||||
use alloy_primitives::{keccak256, Address, B256, U256};
|
||||
use alloy_signer_local::PrivateKeySigner;
|
||||
use rand::Rng;
|
||||
use rust_decimal::Decimal;
|
||||
@@ -21,7 +21,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
pub const BYTES32_ZERO: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
/// Signature types for orders
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SigType {
|
||||
/// ECDSA EIP712 signatures signed by EOAs
|
||||
Eoa = 0,
|
||||
@@ -29,6 +29,8 @@ pub enum SigType {
|
||||
PolyProxy = 1,
|
||||
/// EIP712 signatures signed by EOAs that own Polymarket Gnosis safes
|
||||
PolyGnosisSafe = 2,
|
||||
/// EIP-1271 smart contract wallet signatures (V2 orders only)
|
||||
Poly1271 = 3,
|
||||
}
|
||||
|
||||
/// Rounding configuration for different tick sizes
|
||||
@@ -52,6 +54,13 @@ pub struct OrderBuilder {
|
||||
funder: Address,
|
||||
}
|
||||
|
||||
const POLYGON_PROXY_FACTORY: &str = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052";
|
||||
const POLYGON_SAFE_FACTORY: &str = "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b";
|
||||
const PROXY_INIT_CODE_HASH: &str =
|
||||
"0xd21df8dc65880a8606f09fe0ce3df9b8869287ab0b058be05aa9e8af6330a00b";
|
||||
const SAFE_INIT_CODE_HASH: &str =
|
||||
"0x2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf";
|
||||
|
||||
/// Rounding configurations for different tick sizes
|
||||
static ROUNDING_CONFIG: LazyLock<HashMap<Decimal, RoundConfig>> = LazyLock::new(|| {
|
||||
HashMap::from([
|
||||
@@ -107,6 +116,70 @@ pub fn get_contract_config(chain_id: u64, neg_risk: bool) -> Option<ContractConf
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sig_type_from_u8(signature_type: u8) -> Result<SigType> {
|
||||
match signature_type {
|
||||
0 => Ok(SigType::Eoa),
|
||||
1 => Ok(SigType::PolyProxy),
|
||||
2 => Ok(SigType::PolyGnosisSafe),
|
||||
3 => Ok(SigType::Poly1271),
|
||||
other => Err(PolyfillError::validation(format!(
|
||||
"Unsupported signature_type {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_proxy_wallet(eoa_address: Address, chain_id: u64) -> Result<Address> {
|
||||
if chain_id != 137 {
|
||||
return Err(PolyfillError::config(
|
||||
"Proxy wallet auto-derivation is only configured for Polygon mainnet",
|
||||
));
|
||||
}
|
||||
|
||||
let factory = Address::from_str(POLYGON_PROXY_FACTORY)
|
||||
.map_err(|e| PolyfillError::config(format!("Invalid proxy factory address: {e}")))?;
|
||||
let init_code_hash = B256::from_str(PROXY_INIT_CODE_HASH)
|
||||
.map_err(|e| PolyfillError::config(format!("Invalid proxy init code hash: {e}")))?;
|
||||
let salt = keccak256(eoa_address);
|
||||
Ok(factory.create2(salt, init_code_hash))
|
||||
}
|
||||
|
||||
pub fn derive_safe_wallet(eoa_address: Address, chain_id: u64) -> Result<Address> {
|
||||
if chain_id != 137 {
|
||||
return Err(PolyfillError::config(
|
||||
"Safe wallet auto-derivation is only configured for Polygon mainnet",
|
||||
));
|
||||
}
|
||||
|
||||
let factory = Address::from_str(POLYGON_SAFE_FACTORY)
|
||||
.map_err(|e| PolyfillError::config(format!("Invalid safe factory address: {e}")))?;
|
||||
let init_code_hash = B256::from_str(SAFE_INIT_CODE_HASH)
|
||||
.map_err(|e| PolyfillError::config(format!("Invalid safe init code hash: {e}")))?;
|
||||
let mut padded = [0_u8; 32];
|
||||
padded[12..].copy_from_slice(eoa_address.as_slice());
|
||||
let salt = keccak256(padded);
|
||||
Ok(factory.create2(salt, init_code_hash))
|
||||
}
|
||||
|
||||
pub fn resolve_funder(
|
||||
signer_address: Address,
|
||||
chain_id: u64,
|
||||
sig_type: SigType,
|
||||
funder: Option<Address>,
|
||||
) -> Result<Option<Address>> {
|
||||
match (sig_type, funder) {
|
||||
(SigType::Eoa, Some(_)) => Err(PolyfillError::validation(
|
||||
"funder cannot be set for EOA signature_type",
|
||||
)),
|
||||
(SigType::PolyProxy, None) => derive_proxy_wallet(signer_address, chain_id).map(Some),
|
||||
(SigType::PolyGnosisSafe, None) => derive_safe_wallet(signer_address, chain_id).map(Some),
|
||||
(SigType::Poly1271, None) => Err(PolyfillError::validation(
|
||||
"funder is required for Poly1271 signature_type",
|
||||
)),
|
||||
(_, Some(Address::ZERO)) => Err(PolyfillError::validation("funder cannot be zero address")),
|
||||
(_, explicit) => Ok(explicit),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a random seed for order salt
|
||||
fn generate_seed() -> u64 {
|
||||
let mut rng = rand::thread_rng();
|
||||
@@ -580,6 +653,28 @@ mod tests {
|
||||
assert!(config_unsupported.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_type_from_u8() {
|
||||
assert_eq!(sig_type_from_u8(0).unwrap(), SigType::Eoa);
|
||||
assert_eq!(sig_type_from_u8(1).unwrap(), SigType::PolyProxy);
|
||||
assert_eq!(sig_type_from_u8(2).unwrap(), SigType::PolyGnosisSafe);
|
||||
assert_eq!(sig_type_from_u8(3).unwrap(), SigType::Poly1271);
|
||||
assert!(sig_type_from_u8(4).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derive_polygon_funder_addresses() {
|
||||
let eoa = Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap();
|
||||
assert_eq!(
|
||||
derive_safe_wallet(eoa, 137).unwrap(),
|
||||
Address::from_str("0xd93b25Cb943D14d0d34FBAf01fc93a0F8b5f6e47").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
derive_proxy_wallet(eoa, 137).unwrap(),
|
||||
Address::from_str("0x365f0cA36ae1F641E02Fe3b7743673DA42A13a70").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_optional_bytes32_defaults_to_zero() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -806,6 +806,11 @@ pub struct ClientConfig {
|
||||
pub api_credentials: Option<ApiCredentials>,
|
||||
/// Builder code applied to orders when none is specified on the order itself.
|
||||
pub builder_code: Option<String>,
|
||||
/// Polymarket signature type: 0 EOA, 1 Proxy, 2 Gnosis Safe, 3 Poly1271.
|
||||
pub signature_type: Option<u8>,
|
||||
/// Address that holds funds for proxy/Safe/smart-contract wallet flows.
|
||||
/// If omitted for signature type 1 or 2, the Polygon funder is derived from the signer.
|
||||
pub funder: Option<String>,
|
||||
/// Request timeout
|
||||
pub timeout: Option<std::time::Duration>,
|
||||
/// Maximum number of connections
|
||||
@@ -820,6 +825,8 @@ impl Default for ClientConfig {
|
||||
private_key: None,
|
||||
api_credentials: None,
|
||||
builder_code: None,
|
||||
signature_type: None,
|
||||
funder: None,
|
||||
timeout: Some(std::time::Duration::from_secs(30)),
|
||||
max_connections: Some(100),
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ pub struct TestConfig {
|
||||
pub api_key: Option<String>,
|
||||
pub api_secret: Option<String>,
|
||||
pub api_passphrase: Option<String>,
|
||||
pub signature_type: Option<u8>,
|
||||
pub funder: Option<String>,
|
||||
pub test_timeout: Duration,
|
||||
}
|
||||
|
||||
@@ -33,6 +35,12 @@ impl Default for TestConfig {
|
||||
api_passphrase: env::var("POLYMARKET_API_PASSPHRASE")
|
||||
.or_else(|_| env::var("POLYMARKET_PASSPHRASE"))
|
||||
.ok(),
|
||||
signature_type: env::var("POLYMARKET_SIGNATURE_TYPE")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u8>().ok()),
|
||||
funder: env::var("POLYMARKET_FUNDER")
|
||||
.or_else(|_| env::var("POLYMARKET_FUNDER_ADDRESS"))
|
||||
.ok(),
|
||||
test_timeout: Duration::from_secs(30),
|
||||
}
|
||||
}
|
||||
@@ -73,6 +81,8 @@ impl TestConfig {
|
||||
base_url: self.host.clone(),
|
||||
chain: self.chain,
|
||||
private_key: Some(private_key.clone()),
|
||||
signature_type: self.signature_type,
|
||||
funder: self.funder.clone(),
|
||||
..ClientConfig::default()
|
||||
})
|
||||
}
|
||||
@@ -84,6 +94,8 @@ impl TestConfig {
|
||||
println!(" Chain ID: {}", self.chain);
|
||||
println!(" Has Auth: {}", self.has_auth());
|
||||
println!(" Has API Creds: {}", self.has_api_creds());
|
||||
println!(" Signature Type: {:?}", self.signature_type);
|
||||
println!(" Has Funder Override: {}", self.funder.is_some());
|
||||
println!(" Timeout: {:?}", self.test_timeout);
|
||||
}
|
||||
}
|
||||
|
||||
+93
-25
@@ -25,11 +25,25 @@ fn load_env_vars() -> (String, Option<String>, Option<String>, Option<String>) {
|
||||
(private_key, api_key, api_secret, api_passphrase)
|
||||
}
|
||||
|
||||
fn env_signature_type() -> Option<u8> {
|
||||
env::var("POLYMARKET_SIGNATURE_TYPE")
|
||||
.ok()
|
||||
.and_then(|value| value.parse::<u8>().ok())
|
||||
}
|
||||
|
||||
fn env_funder() -> Option<String> {
|
||||
env::var("POLYMARKET_FUNDER")
|
||||
.or_else(|_| env::var("POLYMARKET_FUNDER_ADDRESS"))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn bootstrap_client(private_key: &str) -> ClobClient {
|
||||
ClobClient::from_config(ClientConfig {
|
||||
base_url: HOST.to_string(),
|
||||
chain: CHAIN_ID,
|
||||
private_key: Some(private_key.to_string()),
|
||||
signature_type: env_signature_type(),
|
||||
funder: env_funder(),
|
||||
..ClientConfig::default()
|
||||
})
|
||||
.expect("failed to build bootstrap client")
|
||||
@@ -44,6 +58,8 @@ fn authenticated_client(
|
||||
chain: CHAIN_ID,
|
||||
private_key: Some(private_key),
|
||||
api_credentials: Some(api_credentials),
|
||||
signature_type: env_signature_type(),
|
||||
funder: env_funder(),
|
||||
..ClientConfig::default()
|
||||
})
|
||||
.expect("failed to build authenticated client")
|
||||
@@ -96,39 +112,48 @@ async fn test_real_api_authenticated_order_flow() {
|
||||
.await
|
||||
.expect("Failed to get markets");
|
||||
|
||||
let active_market = markets
|
||||
.data
|
||||
.iter()
|
||||
.find(|m| m.active && !m.closed)
|
||||
.expect("No active markets found");
|
||||
let mut selected_token = None;
|
||||
let mut selected_midpoint = None;
|
||||
for market in markets.data.iter().filter(|m| m.active && !m.closed) {
|
||||
for token in &market.tokens {
|
||||
let midpoint = match client.get_midpoint(&token.token_id).await {
|
||||
Ok(midpoint) => midpoint.mid,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if midpoint > dec!(0.05) {
|
||||
selected_token = Some(token.token_id.clone());
|
||||
selected_midpoint = Some(midpoint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if selected_token.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let token_id = &active_market.tokens[0].token_id;
|
||||
let token_id = selected_token.expect("No active token with midpoint safely above 0.01 found");
|
||||
let midpoint = selected_midpoint.expect("midpoint should be selected with token");
|
||||
println!("PASS: Found active token: {}", token_id);
|
||||
println!("PASS: Current midpoint: {}", midpoint);
|
||||
|
||||
// Step 3: Get current price to place a reasonable order
|
||||
println!("Step 3: Getting current market price...");
|
||||
let midpoint = client
|
||||
.get_midpoint(token_id)
|
||||
// Step 4: Create and post a tiny non-marketable BUY order.
|
||||
// A BUY at 0.01 validates pUSD balance/allowance without requiring outcome-token inventory.
|
||||
let side = Side::BUY;
|
||||
let order_price = dec!(0.01);
|
||||
let order_book = client
|
||||
.get_order_book(&token_id)
|
||||
.await
|
||||
.expect("Failed to get midpoint");
|
||||
println!("PASS: Current midpoint: {}", midpoint.mid);
|
||||
|
||||
// Step 4: Create and post a small order well away from market price (so it won't fill immediately).
|
||||
// IMPORTANT: choose side consistently with the price so we don't accidentally create a marketable order.
|
||||
let (side, order_price) = if midpoint.mid > dec!(0.5) {
|
||||
(Side::BUY, dec!(0.01)) // Very low buy price, won't fill
|
||||
} else {
|
||||
(Side::SELL, dec!(0.99)) // Very high sell price, won't fill
|
||||
};
|
||||
.expect("Failed to get selected order book");
|
||||
let order_size = order_book.min_order_size.max(dec!(5));
|
||||
|
||||
println!(
|
||||
"Step 4: Posting {:?} order at price {}...",
|
||||
side, order_price
|
||||
"Step 4: Posting {:?} order at price {} and size {}...",
|
||||
side, order_price, order_size
|
||||
);
|
||||
let order_args = OrderArgs {
|
||||
token_id: token_id.clone(),
|
||||
token_id,
|
||||
price: order_price,
|
||||
size: dec!(1.0), // Small size (auth is the thing we're testing here)
|
||||
size: order_size,
|
||||
side,
|
||||
expiration: None,
|
||||
builder_code: None,
|
||||
@@ -255,6 +280,50 @@ async fn test_real_api_get_balance_allowance() {
|
||||
|
||||
println!("Testing get_balance_allowance...");
|
||||
|
||||
use polyfill_rs::types::{AssetType, BalanceAllowanceParams};
|
||||
let collateral_update_params = BalanceAllowanceParams {
|
||||
asset_type: Some(AssetType::COLLATERAL),
|
||||
token_id: None,
|
||||
signature_type: None,
|
||||
};
|
||||
let collateral_update = client
|
||||
.update_balance_allowance(Some(collateral_update_params))
|
||||
.await;
|
||||
match collateral_update {
|
||||
Ok(update) => {
|
||||
println!("PASS: Successfully requested collateral balance/allowance update");
|
||||
println!(" Collateral update: {:?}", update);
|
||||
},
|
||||
Err(e) => {
|
||||
let err_str = format!("{:?}", e);
|
||||
if err_str.contains("401") {
|
||||
panic!("FAIL: 401 Unauthorized - authentication failed!");
|
||||
}
|
||||
println!("WARNING: Collateral balance update failed: {:?}", e);
|
||||
},
|
||||
}
|
||||
|
||||
let collateral_params = BalanceAllowanceParams {
|
||||
asset_type: Some(AssetType::COLLATERAL),
|
||||
token_id: None,
|
||||
signature_type: None,
|
||||
};
|
||||
let collateral_result = client.get_balance_allowance(Some(collateral_params)).await;
|
||||
|
||||
match collateral_result {
|
||||
Ok(balance) => {
|
||||
println!("PASS: Successfully fetched collateral balance/allowance");
|
||||
println!(" Collateral: {:?}", balance);
|
||||
},
|
||||
Err(e) => {
|
||||
let err_str = format!("{:?}", e);
|
||||
if err_str.contains("401") {
|
||||
panic!("FAIL: 401 Unauthorized - authentication failed!");
|
||||
}
|
||||
println!("WARNING: Collateral balance check failed: {:?}", e);
|
||||
},
|
||||
}
|
||||
|
||||
// Get a valid token_id first
|
||||
let markets = client
|
||||
.get_sampling_markets(None)
|
||||
@@ -262,7 +331,6 @@ async fn test_real_api_get_balance_allowance() {
|
||||
.expect("Failed to get markets");
|
||||
let token_id = &markets.data[0].tokens[0].token_id;
|
||||
|
||||
use polyfill_rs::types::{AssetType, BalanceAllowanceParams};
|
||||
let params = BalanceAllowanceParams {
|
||||
asset_type: Some(AssetType::CONDITIONAL),
|
||||
token_id: Some(token_id.clone()),
|
||||
|
||||
Reference in New Issue
Block a user