Skip to content

Commit e8535df

Browse files
Michael YuanMichael Yuan
authored andcommitted
Fix Polymarket CLOB: EIP-712 nonce type, token ID parsing, tick size field
- Fixed ClobAuth EIP-712 type: nonce is uint256 not string - Handle clobTokenIds as JSON string or array (gamma API returns string) - Handle minimum_tick_size field name (CLOB returns this, not tickSize) - Use checksummed addresses for Polymarket API - Try create API key first, fall back to derive (for new wallets) - Tested: auth + market data + order signing all pass, blocked only by geo-restriction
1 parent fc1e99e commit e8535df

File tree

1 file changed

+57
-17
lines changed

1 file changed

+57
-17
lines changed

src/polymarket.rs

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,17 @@ fn clob_auth_eip712_hash(address: &Address, timestamp: &str) -> Result<[u8; 32]>
8484

8585
// Struct hash
8686
let type_hash = ethers::utils::keccak256(
87-
"ClobAuth(address address,string timestamp,string nonce,string message)",
87+
"ClobAuth(address address,string timestamp,uint256 nonce,string message)",
8888
);
8989
let timestamp_hash = ethers::utils::keccak256(timestamp.as_bytes());
90-
let nonce_hash = ethers::utils::keccak256("0".as_bytes());
9190
let message_hash =
9291
ethers::utils::keccak256("This message attests that I control the given wallet".as_bytes());
9392

9493
let struct_hash = ethers::utils::keccak256(ethers::abi::encode(&[
9594
ethers::abi::Token::FixedBytes(type_hash.to_vec()),
9695
ethers::abi::Token::Address(*address),
9796
ethers::abi::Token::FixedBytes(timestamp_hash.to_vec()),
98-
ethers::abi::Token::FixedBytes(nonce_hash.to_vec()),
97+
ethers::abi::Token::Uint(U256::zero()),
9998
ethers::abi::Token::FixedBytes(message_hash.to_vec()),
10099
]));
101100

@@ -118,7 +117,7 @@ pub async fn derive_api_credentials(
118117
let wallet: LocalWallet = private_key
119118
.parse()
120119
.context("Invalid private key for Polymarket")?;
121-
let address = format!("{:?}", wallet.address());
120+
let address = ethers::utils::to_checksum(&wallet.address(), None);
122121

123122
// Check cache first
124123
if let Some((api_key, secret, passphrase)) = load_cached_credentials(&address) {
@@ -135,10 +134,32 @@ pub async fn derive_api_credentials(
135134
let signature = wallet.sign_hash(hash.into())?;
136135
let sig_hex = format!("0x{}", hex::encode(signature.to_vec()));
137136

138-
// POST to derive-api-key (or create new)
139-
let url = format!("{}/auth/derive-api-key", CLOB_BASE);
137+
// Try to create API key first (for new wallets), fall back to derive
138+
let create_url = format!("{}/auth/api-key", CLOB_BASE);
140139
let resp = client
141-
.get(&url)
140+
.post(&create_url)
141+
.header("POLY_ADDRESS", &address)
142+
.header("POLY_SIGNATURE", &sig_hex)
143+
.header("POLY_TIMESTAMP", &timestamp)
144+
.header("POLY_NONCE", "0")
145+
.send()
146+
.await?;
147+
148+
if resp.status().is_success() {
149+
let body: serde_json::Value = resp.json().await?;
150+
let api_key = body["apiKey"].as_str().unwrap_or_default().to_string();
151+
let secret = body["secret"].as_str().unwrap_or_default().to_string();
152+
let passphrase = body["passphrase"].as_str().unwrap_or_default().to_string();
153+
if !api_key.is_empty() {
154+
let _ = save_cached_credentials(&address, &api_key, &secret, &passphrase);
155+
return Ok((api_key, secret, passphrase));
156+
}
157+
}
158+
159+
// Fall back to derive existing credentials
160+
let derive_url = format!("{}/auth/derive-api-key", CLOB_BASE);
161+
let resp = client
162+
.get(&derive_url)
142163
.header("POLY_ADDRESS", &address)
143164
.header("POLY_SIGNATURE", &sig_hex)
144165
.header("POLY_TIMESTAMP", &timestamp)
@@ -284,7 +305,7 @@ pub async fn sign_order(
284305
#[allow(dead_code)]
285306
struct GammaMarket {
286307
#[serde(rename = "clobTokenIds")]
287-
clob_token_ids: Option<Vec<String>>,
308+
clob_token_ids: Option<serde_json::Value>,
288309
slug: Option<String>,
289310
#[serde(rename = "negRisk")]
290311
neg_risk: Option<bool>,
@@ -295,22 +316,41 @@ pub async fn get_market_info(client: &reqwest::Client, slug: &str) -> Result<(Ve
295316
let url = format!("{}/markets?slug={}", GAMMA_BASE, urlencoding::encode(slug));
296317
let markets: Vec<GammaMarket> = client.get(&url).send().await?.json().await?;
297318
let market = markets.first().context("Market not found")?;
298-
let token_ids = market.clob_token_ids.clone().context("No CLOB token IDs")?;
299319
let neg_risk = market.neg_risk.unwrap_or(false);
320+
321+
// clobTokenIds can be a JSON array or a JSON string containing an array
322+
let token_ids = match &market.clob_token_ids {
323+
Some(serde_json::Value::Array(arr)) => arr
324+
.iter()
325+
.filter_map(|v| v.as_str().map(String::from))
326+
.collect(),
327+
Some(serde_json::Value::String(s)) => {
328+
serde_json::from_str::<Vec<String>>(s).unwrap_or_default()
329+
}
330+
_ => Vec::new(),
331+
};
332+
if token_ids.is_empty() {
333+
bail!("No CLOB token IDs found for market");
334+
}
300335
Ok((token_ids, neg_risk))
301336
}
302337

303338
/// Get tick size for a token
304339
pub async fn get_tick_size(client: &reqwest::Client, token_id: &str) -> Result<f64> {
305340
let url = format!("{}/tick-size?token_id={}", CLOB_BASE, token_id);
306-
#[derive(Deserialize)]
307-
struct TickResponse {
308-
#[serde(rename = "tickSize")]
309-
tick_size: Option<String>,
310-
}
311-
let resp: TickResponse = client.get(&url).send().await?.json().await?;
312-
resp.tick_size
313-
.and_then(|s| s.parse::<f64>().ok())
341+
let resp: serde_json::Value = client.get(&url).send().await?.json().await?;
342+
// Handle both "minimum_tick_size" (number) and "tickSize" (string)
343+
resp.get("minimum_tick_size")
344+
.and_then(|v| {
345+
v.as_f64()
346+
.or_else(|| v.as_str().and_then(|s| s.parse().ok()))
347+
})
348+
.or_else(|| {
349+
resp.get("tickSize").and_then(|v| {
350+
v.as_f64()
351+
.or_else(|| v.as_str().and_then(|s| s.parse().ok()))
352+
})
353+
})
314354
.context("Failed to get tick size")
315355
}
316356

0 commit comments

Comments
 (0)