use alloy_primitives::{keccak256, Address, U256};
use crate::error::{Error, Result};
use super::constants::*;
use super::constants_v2;
use super::signer::TradingSigner;
use super::types::*;
pub fn derive_safe_address(eoa: Address) -> Address {
let mut encoded = [0u8; 32];
encoded[12..32].copy_from_slice(eoa.as_slice());
let salt = keccak256(&encoded);
create2_address(SAFE_FACTORY, &salt, SAFE_INIT_CODE_HASH)
}
pub fn derive_proxy_address(eoa: Address) -> Address {
let salt = keccak256(eoa.as_slice());
create2_address(PROXY_FACTORY, &salt, PROXY_INIT_CODE_HASH)
}
pub fn derive_funder_address(eoa: Address, sig_type: SignatureType) -> Address {
match sig_type {
SignatureType::Eoa => eoa,
SignatureType::PolyProxy => derive_proxy_address(eoa),
SignatureType::PolyGnosisSafe => derive_safe_address(eoa),
}
}
fn create2_address(factory: &str, salt: &[u8; 32], init_code_hash: &str) -> Address {
let factory_addr = factory.parse::<Address>().expect("invalid factory address");
let init_hash = hex::decode(init_code_hash.strip_prefix("0x").unwrap_or(init_code_hash))
.expect("invalid init code hash");
let mut buf = Vec::with_capacity(1 + 20 + 32 + 32);
buf.push(0xff);
buf.extend_from_slice(factory_addr.as_slice());
buf.extend_from_slice(salt);
buf.extend_from_slice(&init_hash);
let hash = keccak256(&buf);
Address::from_slice(&hash[12..])
}
pub async fn is_safe_deployed(address: Address) -> Result<bool> {
let url = format!("{}/deployed?address={}", RELAYER_HOST, address);
let resp = reqwest::get(&url).await?;
let data: serde_json::Value = resp.json().await?;
Ok(data.get("deployed").and_then(|v| v.as_bool()).unwrap_or(false))
}
pub async fn detect_wallet_type(eoa: Address) -> Result<(SignatureType, Address)> {
let safe_addr = derive_safe_address(eoa);
let proxy_addr = derive_proxy_address(eoa);
let (safe_deployed, proxy_deployed) = tokio::join!(
is_safe_deployed(safe_addr),
is_safe_deployed(proxy_addr),
);
if safe_deployed.unwrap_or(false) {
return Ok((SignatureType::PolyGnosisSafe, safe_addr));
}
if proxy_deployed.unwrap_or(false) {
return Ok((SignatureType::PolyProxy, proxy_addr));
}
Ok((SignatureType::PolyGnosisSafe, safe_addr))
}
pub async fn check_approvals(
funder_address: Address,
rpc_url: &str,
exchange_version: ExchangeVersion,
) -> Result<ApprovalStatus> {
let client = reqwest::Client::new();
let (spender_list, collateral_addr): (&[&str], &str) = match exchange_version {
ExchangeVersion::V1 => (&SPENDERS, USDC),
ExchangeVersion::V2 => (&constants_v2::V2_SPENDERS, constants_v2::POLY_USD),
};
let spenders: Vec<Address> = spender_list.iter().map(|s| s.parse().unwrap()).collect();
let collateral: Address = collateral_addr.parse().unwrap();
let ctf: Address = CTF.parse().unwrap();
let mut usdc_results = [false; 3];
let mut ctf_results = [false; 3];
for (i, spender) in spenders.iter().take(3).enumerate() {
let allowance = eth_call_u256(&client, rpc_url, collateral, encode_allowance(funder_address, *spender)).await;
usdc_results[i] = allowance.map(|v| v > U256::ZERO).unwrap_or(false);
let approved = eth_call_bool(&client, rpc_url, ctf, encode_is_approved_for_all(funder_address, *spender)).await;
ctf_results[i] = approved.unwrap_or(false);
}
let (escrow_token_str, escrow_addr_str) = match exchange_version {
ExchangeVersion::V2 => (constants_v2::POLY_USD, FEE_ESCROW_ADDRESS_V2),
_ => (USDC, FEE_ESCROW_ADDRESS),
};
let escrow_token: Address = escrow_token_str.parse().unwrap();
let escrow_addr: Address = escrow_addr_str.parse().unwrap();
let escrow_allowance = eth_call_u256(&client, rpc_url, escrow_token, encode_allowance(funder_address, escrow_addr)).await;
let fee_escrow_approved = escrow_allowance.map(|v| v > U256::ZERO).unwrap_or(false);
let usdc_approvals = UsdcApprovals {
ctf_exchange: usdc_results[0],
neg_risk_ctf_exchange: usdc_results[1],
neg_risk_adapter: usdc_results[2],
fee_escrow: fee_escrow_approved,
};
let ctf_approvals = CtfApprovals {
ctf_exchange: ctf_results[0],
neg_risk_ctf_exchange: ctf_results[1],
neg_risk_adapter: ctf_results[2],
};
let all_approved = usdc_results.iter().all(|v| *v) && ctf_results.iter().all(|v| *v) && fee_escrow_approved;
Ok(ApprovalStatus {
funder_address: format!("{}", funder_address),
usdc: usdc_approvals,
ctf: ctf_approvals,
all_approved,
})
}
pub async fn check_balance(
funder_address: Address,
rpc_url: &str,
exchange_version: ExchangeVersion,
) -> Result<BalanceInfo> {
let client = reqwest::Client::new();
let collateral_addr = match exchange_version {
ExchangeVersion::V1 => USDC,
ExchangeVersion::V2 => constants_v2::POLY_USD,
};
let collateral: Address = collateral_addr.parse().unwrap();
let usdc_raw = eth_call_u256(&client, rpc_url, collateral, encode_balance_of(funder_address))
.await
.unwrap_or(U256::ZERO);
let matic_raw = eth_get_balance(&client, rpc_url, funder_address)
.await
.unwrap_or(U256::ZERO);
Ok(BalanceInfo {
funder_address: format!("{}", funder_address),
usdc: format_units(usdc_raw, 6),
usdc_raw: usdc_raw.to_string(),
matic: format_units(matic_raw, 18),
})
}
pub(super) fn encode_allowance(owner: Address, spender: Address) -> Vec<u8> {
let mut data = vec![0xdd, 0x62, 0xed, 0x3e]; data.extend_from_slice(&pad_address(owner));
data.extend_from_slice(&pad_address(spender));
data
}
fn encode_is_approved_for_all(account: Address, operator: Address) -> Vec<u8> {
let mut data = vec![0xe9, 0x85, 0xe9, 0xc5]; data.extend_from_slice(&pad_address(account));
data.extend_from_slice(&pad_address(operator));
data
}
pub(super) fn encode_balance_of(account: Address) -> Vec<u8> {
let mut data = vec![0x70, 0xa0, 0x82, 0x31]; data.extend_from_slice(&pad_address(account));
data
}
pub(super) fn pad_address(addr: Address) -> [u8; 32] {
let mut word = [0u8; 32];
word[12..32].copy_from_slice(addr.as_slice());
word
}
pub(super) fn encode_approve(spender: Address, amount: U256) -> Vec<u8> {
let mut data = vec![0x09, 0x5e, 0xa7, 0xb3]; data.extend_from_slice(&pad_address(spender));
data.extend_from_slice(&amount.to_be_bytes::<32>());
data
}
pub(super) fn encode_wrap(underlying_token: Address, recipient: Address, amount: U256) -> Vec<u8> {
let mut data = vec![0x62, 0x35, 0x56, 0x38]; data.extend_from_slice(&pad_address(underlying_token));
data.extend_from_slice(&pad_address(recipient));
data.extend_from_slice(&amount.to_be_bytes::<32>());
data
}
pub(super) fn encode_unwrap(underlying_token: Address, recipient: Address, amount: U256) -> Vec<u8> {
let mut data = vec![0x8c, 0xc7, 0x10, 0x4f]; data.extend_from_slice(&pad_address(underlying_token));
data.extend_from_slice(&pad_address(recipient));
data.extend_from_slice(&amount.to_be_bytes::<32>());
data
}
pub(super) async fn eth_call_u256(client: &reqwest::Client, rpc_url: &str, to: Address, data: Vec<u8>) -> Result<U256> {
let result = eth_call_raw(client, rpc_url, to, data).await?;
let hex_str = result.strip_prefix("0x").unwrap_or(&result);
if hex_str.is_empty() || hex_str == "0" {
return Ok(U256::ZERO);
}
let bytes = hex::decode(hex_str).map_err(|e| Error::Trading(format!("hex decode: {}", e)))?;
Ok(U256::from_be_slice(&bytes))
}
async fn eth_call_bool(client: &reqwest::Client, rpc_url: &str, to: Address, data: Vec<u8>) -> Result<bool> {
let val = eth_call_u256(client, rpc_url, to, data).await?;
Ok(val > U256::ZERO)
}
pub(super) async fn eth_call_raw(client: &reqwest::Client, rpc_url: &str, to: Address, data: Vec<u8>) -> Result<String> {
let body = serde_json::json!({
"jsonrpc": "2.0",
"method": "eth_call",
"params": [{
"to": format!("{}", to),
"data": format!("0x{}", hex::encode(&data)),
}, "latest"],
"id": 1
});
let resp = client.post(rpc_url).json(&body).send().await?;
let data: serde_json::Value = resp.json().await?;
if let Some(err) = data.get("error") {
return Err(Error::Trading(format!("RPC error: {}", err)));
}
Ok(data.get("result").and_then(|v| v.as_str()).unwrap_or("0x0").to_string())
}
async fn eth_get_balance(client: &reqwest::Client, rpc_url: &str, address: Address) -> Result<U256> {
let body = serde_json::json!({
"jsonrpc": "2.0",
"method": "eth_getBalance",
"params": [format!("{}", address), "latest"],
"id": 1
});
let resp = client.post(rpc_url).json(&body).send().await?;
let data: serde_json::Value = resp.json().await?;
let hex_str = data.get("result").and_then(|v| v.as_str()).unwrap_or("0x0");
let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
if hex_str.is_empty() || hex_str == "0" {
return Ok(U256::ZERO);
}
let bytes = hex::decode(if hex_str.len() % 2 == 1 { format!("0{}", hex_str) } else { hex_str.to_string() })
.map_err(|e| Error::Trading(format!("hex decode: {}", e)))?;
Ok(U256::from_be_slice(&bytes))
}
pub(super) async fn eth_get_transaction_count(client: &reqwest::Client, rpc_url: &str, address: Address) -> Result<u64> {
let body = serde_json::json!({
"jsonrpc": "2.0",
"method": "eth_getTransactionCount",
"params": [format!("{}", address), "latest"],
"id": 1
});
let resp = client.post(rpc_url).json(&body).send().await?;
let data: serde_json::Value = resp.json().await?;
if let Some(err) = data.get("error") {
return Err(Error::Trading(format!("RPC error getting nonce: {}", err)));
}
let hex_str = data.get("result").and_then(|v| v.as_str()).unwrap_or("0x0");
let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
u64::from_str_radix(hex_str, 16)
.map_err(|e| Error::Trading(format!("parse nonce: {}", e)))
}
pub(super) async fn eth_gas_price(client: &reqwest::Client, rpc_url: &str) -> Result<u64> {
let body = serde_json::json!({
"jsonrpc": "2.0",
"method": "eth_gasPrice",
"params": [],
"id": 1
});
let resp = client.post(rpc_url).json(&body).send().await?;
let data: serde_json::Value = resp.json().await?;
if let Some(err) = data.get("error") {
return Err(Error::Trading(format!("RPC error getting gas price: {}", err)));
}
let hex_str = data.get("result").and_then(|v| v.as_str()).unwrap_or("0x0");
let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
u64::from_str_radix(hex_str, 16)
.map_err(|e| Error::Trading(format!("parse gas price: {}", e)))
}
pub(super) async fn eth_send_raw_transaction(client: &reqwest::Client, rpc_url: &str, raw_tx: &[u8]) -> Result<String> {
let body = serde_json::json!({
"jsonrpc": "2.0",
"method": "eth_sendRawTransaction",
"params": [format!("0x{}", hex::encode(raw_tx))],
"id": 1
});
let resp = client.post(rpc_url).json(&body).send().await?;
let data: serde_json::Value = resp.json().await?;
if let Some(err) = data.get("error") {
return Err(Error::Trading(format!("eth_sendRawTransaction error: {}", err)));
}
data.get("result")
.and_then(|v| v.as_str())
.map(String::from)
.ok_or_else(|| Error::Trading("No tx hash in sendRawTransaction response".into()))
}
pub(super) fn build_legacy_tx_raw(
nonce: u64,
gas_price: u64,
gas_limit: u64,
to: Address,
value: U256,
data: &[u8],
signature: &[u8], ) -> Vec<u8> {
let chain_id: u64 = CHAIN_ID;
let r = &signature[..32];
let s = &signature[32..64];
let recovery_id = signature[64];
let v = recovery_id as u64 + chain_id * 2 + 35;
let mut items: Vec<Vec<u8>> = Vec::new();
items.push(rlp_encode_u64(nonce));
items.push(rlp_encode_u64(gas_price));
items.push(rlp_encode_u64(gas_limit));
items.push(to.as_slice().to_vec());
items.push(rlp_encode_u256(value));
items.push(data.to_vec());
items.push(rlp_encode_u64(v));
items.push(trim_leading_zeros(r).to_vec());
items.push(trim_leading_zeros(s).to_vec());
rlp_encode_list(&items)
}
pub(super) fn build_legacy_tx_hash(
nonce: u64,
gas_price: u64,
gas_limit: u64,
to: Address,
value: U256,
data: &[u8],
) -> [u8; 32] {
let chain_id: u64 = CHAIN_ID;
let mut items: Vec<Vec<u8>> = Vec::new();
items.push(rlp_encode_u64(nonce));
items.push(rlp_encode_u64(gas_price));
items.push(rlp_encode_u64(gas_limit));
items.push(to.as_slice().to_vec());
items.push(rlp_encode_u256(value));
items.push(data.to_vec());
items.push(rlp_encode_u64(chain_id));
items.push(vec![]); items.push(vec![]);
let encoded = rlp_encode_list(&items);
let hash = keccak256(&encoded);
let mut result = [0u8; 32];
result.copy_from_slice(hash.as_slice());
result
}
fn rlp_encode_u64(val: u64) -> Vec<u8> {
if val == 0 {
return vec![];
}
let bytes = val.to_be_bytes();
let start = bytes.iter().position(|&b| b != 0).unwrap_or(7);
bytes[start..].to_vec()
}
fn rlp_encode_u256(val: U256) -> Vec<u8> {
if val.is_zero() {
return vec![];
}
let bytes = val.to_be_bytes::<32>();
let start = bytes.iter().position(|&b| b != 0).unwrap_or(31);
bytes[start..].to_vec()
}
fn trim_leading_zeros(data: &[u8]) -> &[u8] {
let start = data.iter().position(|&b| b != 0).unwrap_or(data.len());
if start == data.len() { &[] } else { &data[start..] }
}
fn rlp_encode_bytes(data: &[u8]) -> Vec<u8> {
if data.len() == 1 && data[0] < 0x80 {
return data.to_vec();
}
if data.is_empty() {
return vec![0x80];
}
if data.len() <= 55 {
let mut out = vec![0x80 + data.len() as u8];
out.extend_from_slice(data);
out
} else {
let len_bytes = rlp_encode_u64(data.len() as u64);
let mut out = vec![0xb7 + len_bytes.len() as u8];
out.extend_from_slice(&len_bytes);
out.extend_from_slice(data);
out
}
}
fn rlp_encode_list(items: &[Vec<u8>]) -> Vec<u8> {
let mut payload = Vec::new();
for item in items {
payload.extend_from_slice(&rlp_encode_bytes(item));
}
if payload.len() <= 55 {
let mut out = vec![0xc0 + payload.len() as u8];
out.extend_from_slice(&payload);
out
} else {
let len_bytes = rlp_encode_u64(payload.len() as u64);
let mut out = vec![0xf7 + len_bytes.len() as u8];
out.extend_from_slice(&len_bytes);
out.extend_from_slice(&payload);
out
}
}
fn format_units(value: U256, decimals: u32) -> String {
let divisor = U256::from(10u64).pow(U256::from(decimals));
if divisor.is_zero() {
return value.to_string();
}
let whole = value / divisor;
let frac = value % divisor;
if frac.is_zero() {
format!("{}", whole)
} else {
let frac_str = format!("{:0>width$}", frac, width = decimals as usize);
let trimmed = frac_str.trim_end_matches('0');
format!("{}.{}", whole, trimmed)
}
}
async fn build_l1_headers(
signer: &dyn TradingSigner,
nonce: u64,
) -> Result<Vec<(String, String)>> {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let address = format!("{}", signer.address());
let domain = serde_json::json!({
"name": "ClobAuthDomain",
"version": "1",
"chainId": CHAIN_ID,
});
let types = serde_json::json!({
"ClobAuth": [
{"name": "address", "type": "address"},
{"name": "timestamp", "type": "string"},
{"name": "nonce", "type": "uint256"},
{"name": "message", "type": "string"}
]
});
let message = serde_json::json!({
"address": address,
"timestamp": timestamp.to_string(),
"nonce": nonce,
"message": "This message attests that I control the given wallet"
});
let payload = Eip712Payload {
domain,
types,
primary_type: "ClobAuth".into(),
message,
};
let signature = signer.sign_typed_data(&payload).await?;
let sig_hex = format!("0x{}", hex::encode(&signature));
Ok(vec![
("POLY_ADDRESS".into(), address),
("POLY_SIGNATURE".into(), sig_hex),
("POLY_TIMESTAMP".into(), timestamp.to_string()),
("POLY_NONCE".into(), nonce.to_string()),
])
}
fn parse_clob_credentials(data: &serde_json::Value) -> ClobCredentials {
ClobCredentials {
api_key: data.get("apiKey")
.or_else(|| data.get("key"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
api_secret: data.get("secret")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
api_passphrase: data.get("passphrase")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
}
}
fn clob_http_client() -> reqwest::Client {
reqwest::Client::builder()
.user_agent("@polymarket/clob-client")
.http1_only()
.build()
.expect("failed to build HTTP client")
}
fn apply_l1_headers(
mut req: reqwest::RequestBuilder,
headers: &[(String, String)],
) -> reqwest::RequestBuilder {
for (k, v) in headers {
req = req.header(k.as_str(), v.as_str());
}
req.header("Accept", "*/*")
.header("Connection", "keep-alive")
.header("Content-Type", "application/json")
}
pub async fn create_clob_credentials(
signer: &dyn TradingSigner,
_funder_address: Address,
_signature_type: SignatureType,
) -> Result<ClobCredentials> {
let nonce: u64 = 0;
let client = clob_http_client();
let headers = build_l1_headers(signer, nonce).await?;
let create_req = apply_l1_headers(
client.post(format!("{}/auth/api-key", CLOB_HOST)),
&headers,
);
let create_resp = create_req.send().await?;
if create_resp.status().is_success() {
let data: serde_json::Value = create_resp.json().await?;
let creds = parse_clob_credentials(&data);
if !creds.api_key.is_empty() {
return Ok(creds);
}
}
let headers = build_l1_headers(signer, nonce).await?;
let derive_req = apply_l1_headers(
client.get(format!("{}/auth/derive-api-key", CLOB_HOST)),
&headers,
);
let derive_resp = derive_req.send().await?;
if !derive_resp.status().is_success() {
let status = derive_resp.status();
let err = derive_resp.text().await.unwrap_or_default();
return Err(Error::Trading(format!(
"CLOB credential creation failed ({}): {}",
status, err
)));
}
let data: serde_json::Value = derive_resp.json().await?;
Ok(parse_clob_credentials(&data))
}