1use std::str::FromStr;
27
28use alloy::{
29 signers::{SignerSync, local::PrivateKeySigner},
30 sol_types::{SolStruct, eip712_domain},
31};
32use alloy_primitives::{Address, B256, FixedBytes, U256, address};
33use rust_decimal::Decimal;
34
35use crate::{
36 common::{credential::EvmPrivateKey, enums::PolymarketOrderSide},
37 http::{
38 error::{Error, Result},
39 models::PolymarketOrder,
40 },
41};
42
43const CLOB_AUTH_DOMAIN_NAME: &str = "ClobAuthDomain";
45const CLOB_AUTH_DOMAIN_VERSION: &str = "1";
46const CLOB_AUTH_MESSAGE: &str = "This message attests that I control the given wallet";
47
48pub const CTF_EXCHANGE: Address = address!("0xE111180000d2663C0091e4f400237545B87B996B");
50
51pub const NEG_RISK_CTF_EXCHANGE: Address = address!("0xe2222d279d744050d28e00520010520000310F59");
53
54const DOMAIN_NAME: &str = "Polymarket CTF Exchange";
55const DOMAIN_VERSION: &str = "2";
56const POLYGON_CHAIN_ID: u64 = 137;
57
58alloy::sol! {
62 struct ClobAuth {
63 address address;
64 string timestamp;
65 uint256 nonce;
66 string message;
67 }
68}
69
70alloy::sol! {
75 struct Order {
76 uint256 salt;
77 address maker;
78 address signer;
79 uint256 tokenId;
80 uint256 makerAmount;
81 uint256 takerAmount;
82 uint8 side;
83 uint8 signatureType;
84 uint256 timestamp;
85 bytes32 metadata;
86 bytes32 builder;
87 }
88}
89
90#[derive(Debug)]
92pub struct OrderSigner {
93 signer: PrivateKeySigner,
94}
95
96impl OrderSigner {
97 pub fn new(private_key: &EvmPrivateKey) -> Result<Self> {
99 let key_hex = private_key
100 .as_hex()
101 .strip_prefix("0x")
102 .unwrap_or(private_key.as_hex());
103 let signer = PrivateKeySigner::from_str(key_hex)
104 .map_err(|e| Error::bad_request(format!("Failed to create signer: {e}")))?;
105 Ok(Self { signer })
106 }
107
108 #[must_use]
110 pub fn address(&self) -> Address {
111 self.signer.address()
112 }
113
114 pub fn sign_order(&self, order: &PolymarketOrder, neg_risk: bool) -> Result<String> {
123 let order_signer = parse_address(&order.signer, "signer")?;
124 if order_signer != self.signer.address() {
125 return Err(Error::bad_request(format!(
126 "Order signer {order_signer} does not match local signer {}",
127 self.signer.address(),
128 )));
129 }
130
131 let eip712_order = build_eip712_order(order)?;
132
133 let contract = if neg_risk {
134 NEG_RISK_CTF_EXCHANGE
135 } else {
136 CTF_EXCHANGE
137 };
138
139 let domain = eip712_domain! {
140 name: DOMAIN_NAME,
141 version: DOMAIN_VERSION,
142 chain_id: POLYGON_CHAIN_ID,
143 verifying_contract: contract,
144 };
145
146 let signing_hash = eip712_order.eip712_signing_hash(&domain);
147 self.sign_hash(&signing_hash.0)
148 }
149
150 fn sign_hash(&self, hash: &[u8; 32]) -> Result<String> {
151 let hash_b256 = B256::from(*hash);
152 let signature = self
153 .signer
154 .sign_hash_sync(&hash_b256)
155 .map_err(|e| Error::bad_request(format!("Failed to sign order: {e}")))?;
156
157 let r = signature.r();
158 let s = signature.s();
159 let v = if signature.v() { 28u8 } else { 27u8 };
160
161 Ok(format!("0x{r:064x}{s:064x}{v:02x}"))
162 }
163}
164
165pub fn sign_clob_auth(
172 private_key: &EvmPrivateKey,
173 timestamp: &str,
174 nonce: u64,
175) -> Result<(String, String)> {
176 let key_hex = private_key
177 .as_hex()
178 .strip_prefix("0x")
179 .unwrap_or(private_key.as_hex());
180 let signer = PrivateKeySigner::from_str(key_hex)
181 .map_err(|e| Error::bad_request(format!("Failed to create signer: {e}")))?;
182
183 let address = signer.address();
184
185 let auth = ClobAuth {
186 address,
187 timestamp: timestamp.to_string(),
188 nonce: U256::from(nonce),
189 message: CLOB_AUTH_MESSAGE.to_string(),
190 };
191
192 let domain = eip712_domain! {
193 name: CLOB_AUTH_DOMAIN_NAME,
194 version: CLOB_AUTH_DOMAIN_VERSION,
195 chain_id: POLYGON_CHAIN_ID,
196 };
197
198 let signing_hash = auth.eip712_signing_hash(&domain);
199 let signature = signer
200 .sign_hash_sync(&signing_hash)
201 .map_err(|e| Error::bad_request(format!("Failed to sign ClobAuth: {e}")))?;
202
203 let r = signature.r();
204 let s = signature.s();
205 let v = if signature.v() { 28u8 } else { 27u8 };
206
207 Ok((
208 format!("{address:#x}"),
209 format!("0x{r:064x}{s:064x}{v:02x}"),
210 ))
211}
212
213fn build_eip712_order(order: &PolymarketOrder) -> Result<Order> {
215 Ok(Order {
216 salt: U256::from(order.salt),
217 maker: parse_address(&order.maker, "maker")?,
218 signer: parse_address(&order.signer, "signer")?,
219 tokenId: U256::from_str(order.token_id.as_str())
220 .map_err(|e| Error::bad_request(format!("Invalid token ID: {e}")))?,
221 makerAmount: decimal_to_u256(order.maker_amount, "maker_amount")?,
222 takerAmount: decimal_to_u256(order.taker_amount, "taker_amount")?,
223 side: order_side_to_u8(order.side),
224 signatureType: order.signature_type as u8,
225 timestamp: U256::from_str(&order.timestamp)
226 .map_err(|e| Error::bad_request(format!("Invalid timestamp: {e}")))?,
227 metadata: parse_bytes32(&order.metadata, "metadata")?,
228 builder: parse_bytes32(&order.builder, "builder")?,
229 })
230}
231
232fn parse_address(addr: &str, field: &str) -> Result<Address> {
233 Address::from_str(addr).map_err(|e| Error::bad_request(format!("Invalid {field} address: {e}")))
234}
235
236fn parse_bytes32(value: &str, field: &str) -> Result<FixedBytes<32>> {
237 FixedBytes::<32>::from_str(value)
238 .map_err(|e| Error::bad_request(format!("Invalid {field} bytes32: {e}")))
239}
240
241fn decimal_to_u256(d: Decimal, field: &str) -> Result<U256> {
242 let normalized = d.normalize();
243 if normalized.scale() != 0 {
244 return Err(Error::bad_request(format!("{field} must be an integer")));
245 }
246 let mantissa = normalized.mantissa();
247 if mantissa < 0 {
248 return Err(Error::bad_request(format!("{field} must be non-negative")));
249 }
250 Ok(U256::from(mantissa as u128))
251}
252
253fn order_side_to_u8(side: PolymarketOrderSide) -> u8 {
254 match side {
255 PolymarketOrderSide::Buy => 0,
256 PolymarketOrderSide::Sell => 1,
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use alloy_primitives::{Signature, keccak256};
263 use nautilus_core::hex;
264 use rstest::rstest;
265 use rust_decimal_macros::dec;
266 use ustr::Ustr;
267
268 use super::*;
269 use crate::common::enums::SignatureType;
270
271 const TEST_PRIVATE_KEY: &str =
272 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
273
274 fn test_signer() -> OrderSigner {
275 let pk = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
276 OrderSigner::new(&pk).unwrap()
277 }
278
279 const ZERO_BYTES32: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
280
281 fn test_order() -> PolymarketOrder {
282 PolymarketOrder {
283 salt: 123456789,
284 maker: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
285 signer: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
286 token_id: Ustr::from(
287 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
288 ),
289 maker_amount: dec!(100000000),
290 taker_amount: dec!(50000000),
291 side: PolymarketOrderSide::Buy,
292 signature_type: SignatureType::Eoa,
293 expiration: "0".to_string(),
294 timestamp: "1713398400000".to_string(),
295 metadata: ZERO_BYTES32.to_string(),
296 builder: ZERO_BYTES32.to_string(),
297 signature: String::new(),
298 }
299 }
300
301 #[rstest]
302 fn test_order_typehash_matches_contract() {
303 let expected = keccak256(
305 "Order(uint256 salt,address maker,address signer,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,uint256 timestamp,bytes32 metadata,bytes32 builder)",
306 );
307 let order = test_order();
308 let eip712_order = build_eip712_order(&order).unwrap();
309 assert_eq!(eip712_order.eip712_type_hash(), expected);
310 }
311
312 #[rstest]
313 fn test_signer_address_derivation() {
314 let signer = test_signer();
315 let expected = Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap();
317 assert_eq!(signer.address(), expected);
318 }
319
320 #[rstest]
321 fn test_sign_order_format() {
322 let signer = test_signer();
323 let order = test_order();
324
325 let sig = signer.sign_order(&order, false).unwrap();
326
327 assert!(sig.starts_with("0x"));
328 assert_eq!(sig.len(), 132); }
330
331 #[rstest]
332 fn test_sign_order_deterministic() {
333 let signer = test_signer();
334 let order = test_order();
335
336 let sig1 = signer.sign_order(&order, false).unwrap();
337 let sig2 = signer.sign_order(&order, false).unwrap();
338 assert_eq!(sig1, sig2);
339 }
340
341 #[rstest]
342 fn test_sign_order_neg_risk_differs() {
343 let signer = test_signer();
344 let order = test_order();
345
346 let sig_normal = signer.sign_order(&order, false).unwrap();
347 let sig_neg_risk = signer.sign_order(&order, true).unwrap();
348 assert_ne!(sig_normal, sig_neg_risk);
349 }
350
351 #[rstest]
352 fn test_sign_order_sell_side() {
353 let signer = test_signer();
354 let mut order = test_order();
355 let sig_buy = signer.sign_order(&order, false).unwrap();
356
357 order.side = PolymarketOrderSide::Sell;
358 let sig_sell = signer.sign_order(&order, false).unwrap();
359 assert_ne!(sig_buy, sig_sell);
360 }
361
362 #[rstest]
363 fn test_sign_order_different_amounts() {
364 let signer = test_signer();
365 let mut order = test_order();
366 let sig1 = signer.sign_order(&order, false).unwrap();
367
368 order.maker_amount = dec!(200000000);
369 let sig2 = signer.sign_order(&order, false).unwrap();
370 assert_ne!(sig1, sig2);
371 }
372
373 #[rstest]
374 fn test_build_eip712_order() {
375 let order = test_order();
376 let eip712 = build_eip712_order(&order).unwrap();
377
378 assert_eq!(eip712.salt, U256::from(123456789u64));
379 assert_eq!(
380 eip712.maker,
381 Address::from_str("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266").unwrap()
382 );
383 assert_eq!(eip712.makerAmount, U256::from(100000000u128));
384 assert_eq!(eip712.takerAmount, U256::from(50000000u128));
385 assert_eq!(eip712.side, 0); assert_eq!(eip712.signatureType, 0); assert_eq!(eip712.timestamp, U256::from(1713398400000u128));
388 assert_eq!(eip712.metadata, FixedBytes::<32>::ZERO);
389 assert_eq!(eip712.builder, FixedBytes::<32>::ZERO);
390 }
391
392 #[rstest]
393 fn test_build_eip712_order_with_builder_code() {
394 let mut order = test_order();
395 order.builder =
396 "0x0000000000000000000000000000000000000000000000000000000000000001".to_string();
397 let eip712 = build_eip712_order(&order).unwrap();
398
399 let mut expected = [0u8; 32];
400 expected[31] = 1;
401 assert_eq!(eip712.builder, FixedBytes::<32>::from(expected));
402 }
403
404 #[rstest]
405 fn test_decimal_to_u256_integer() {
406 let result = decimal_to_u256(dec!(100000000), "test").unwrap();
407 assert_eq!(result, U256::from(100000000u128));
408 }
409
410 #[rstest]
411 fn test_decimal_to_u256_zero() {
412 let result = decimal_to_u256(dec!(0), "test").unwrap();
413 assert_eq!(result, U256::ZERO);
414 }
415
416 #[rstest]
417 fn test_decimal_to_u256_rejects_fractional() {
418 let result = decimal_to_u256(dec!(100.5), "test");
419 assert!(result.is_err());
420 }
421
422 #[rstest]
423 fn test_decimal_to_u256_rejects_negative() {
424 let result = decimal_to_u256(dec!(-1), "test");
425 assert!(result.is_err());
426 }
427
428 #[rstest]
429 fn test_order_side_mapping() {
430 assert_eq!(order_side_to_u8(PolymarketOrderSide::Buy), 0);
431 assert_eq!(order_side_to_u8(PolymarketOrderSide::Sell), 1);
432 }
433
434 #[rstest]
435 fn test_contract_addresses_nonzero() {
436 assert_ne!(CTF_EXCHANGE, Address::ZERO);
437 assert_ne!(NEG_RISK_CTF_EXCHANGE, Address::ZERO);
438 assert_ne!(CTF_EXCHANGE, NEG_RISK_CTF_EXCHANGE);
439 }
440
441 #[rstest]
442 fn test_v2_contract_addresses_pinned() {
443 assert_eq!(
447 format!("{CTF_EXCHANGE:#x}"),
448 "0xe111180000d2663c0091e4f400237545b87b996b"
449 );
450 assert_eq!(
451 format!("{NEG_RISK_CTF_EXCHANGE:#x}"),
452 "0xe2222d279d744050d28e00520010520000310f59"
453 );
454 }
455
456 #[rstest]
457 fn test_domain_version_is_v2() {
458 assert_eq!(DOMAIN_VERSION, "2");
461 }
462
463 #[rstest]
464 fn test_sign_order_recoverable() {
465 let signer = test_signer();
466 let order = test_order();
467 let sig_hex = signer.sign_order(&order, false).unwrap();
468
469 let sig_bytes = hex::decode(&sig_hex[2..]).unwrap();
470 assert_eq!(sig_bytes.len(), 65);
471
472 let r = U256::from_be_slice(&sig_bytes[..32]);
473 let s = U256::from_be_slice(&sig_bytes[32..64]);
474 let v = sig_bytes[64];
475 let y_parity = v == 28;
476
477 let signature = Signature::new(r, s, y_parity);
478
479 let eip712_order = build_eip712_order(&order).unwrap();
480 let domain = eip712_domain! {
481 name: DOMAIN_NAME,
482 version: DOMAIN_VERSION,
483 chain_id: POLYGON_CHAIN_ID,
484 verifying_contract: CTF_EXCHANGE,
485 };
486 let signing_hash = eip712_order.eip712_signing_hash(&domain);
487
488 let recovered = signature
489 .recover_address_from_prehash(&signing_hash)
490 .unwrap();
491 assert_eq!(recovered, signer.address());
492 }
493
494 const PARITY_TOKEN_ID: &str =
501 "71321045679252212594626385532706912750332728571942532289631379312455583992563";
502
503 fn parity_order(
504 salt: u64,
505 side: PolymarketOrderSide,
506 signature_type: SignatureType,
507 maker_amount: Decimal,
508 taker_amount: Decimal,
509 timestamp: &str,
510 builder: &str,
511 ) -> PolymarketOrder {
512 PolymarketOrder {
513 salt,
514 maker: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
515 signer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
516 token_id: Ustr::from(PARITY_TOKEN_ID),
517 maker_amount,
518 taker_amount,
519 side,
520 signature_type,
521 expiration: "0".to_string(),
522 timestamp: timestamp.to_string(),
523 metadata: ZERO_BYTES32.to_string(),
524 builder: builder.to_string(),
525 signature: String::new(),
526 }
527 }
528
529 fn expected_signing_hash(order: &PolymarketOrder, neg_risk: bool) -> B256 {
530 let eip712_order = build_eip712_order(order).unwrap();
531 let contract = if neg_risk {
532 NEG_RISK_CTF_EXCHANGE
533 } else {
534 CTF_EXCHANGE
535 };
536 let domain = eip712_domain! {
537 name: DOMAIN_NAME,
538 version: DOMAIN_VERSION,
539 chain_id: POLYGON_CHAIN_ID,
540 verifying_contract: contract,
541 };
542 eip712_order.eip712_signing_hash(&domain)
543 }
544
545 #[rstest]
546 #[case::buy_standard_eoa(
547 parity_order(
548 123456789,
549 PolymarketOrderSide::Buy,
550 SignatureType::Eoa,
551 dec!(100000000),
552 dec!(50000000),
553 "1713398400000",
554 ZERO_BYTES32,
555 ),
556 false,
557 "0x32961c48ddac87ed3582f8e02097cd0eff4fcf80460306bd44b3710438dfa64c",
558 "0x89f178136333c8ebb32a19146cb891233e3202d474be6ef730c24dbc06ae4d2a0c99948a86d9b57de0f2c0bb8ec6964aa244ecf17cfa3a95b86878d0b64ad78a1b",
559 )]
560 #[case::sell_neg_risk_eoa(
561 parity_order(
562 987654321,
563 PolymarketOrderSide::Sell,
564 SignatureType::Eoa,
565 dec!(50000000),
566 dec!(100000000),
567 "1713398400000",
568 ZERO_BYTES32,
569 ),
570 true,
571 "0x8b878404bd92dea2bfea9975c9fcd816ec70a57ae431cb20d67bb773744aaef3",
572 "0xf7d60d64364e2615b08d9f69f3ea9afd3b4f83ecfbf05ddd3ca83f4916277fb97d2d080758d17c8e91c928aceb8ff252477ebaec051b672832500f63b4d36b061c",
573 )]
574 #[case::buy_with_builder_code_eoa(
575 parity_order(
576 1,
577 PolymarketOrderSide::Buy,
578 SignatureType::Eoa,
579 dec!(100000000),
580 dec!(50000000),
581 "1713398500000",
582 "0x0000000000000000000000000000000000000000000000000000000000000001",
583 ),
584 false,
585 "0x3df0b6f6ddfca837bc36964cae968b34ad35640b5d98f557c104da97e804e36a",
586 "0xf4d2b34659e8bc07a9572d40ee5a1639a1157409613b4c21566b1f33fd8fe11a364b3f306668cae7248ca7cdf72378f9266bc5628585aa939644400030671e081c",
587 )]
588 #[case::buy_poly_proxy(
589 parity_order(
594 111_111_111,
595 PolymarketOrderSide::Buy,
596 SignatureType::PolyProxy,
597 dec!(100000000),
598 dec!(50000000),
599 "1713398400000",
600 ZERO_BYTES32,
601 ),
602 false,
603 "0x8f88fe2fb3448f4b8ba639992029f0a47a01a14d15b5f2bf9833516571efd279",
604 "0x71a63c85b730cc934688f23ea6374afffef57a61690eba63dcb97a706c8a8d0f3d2a8e0280f3e252eb83be088ebae6b461a8eda18e559e079f59050b90057afa1c",
605 )]
606 #[case::sell_neg_risk_poly_gnosis_safe(
607 parity_order(
608 222_222_222,
609 PolymarketOrderSide::Sell,
610 SignatureType::PolyGnosisSafe,
611 dec!(50000000),
612 dec!(100000000),
613 "1713398400000",
614 ZERO_BYTES32,
615 ),
616 true,
617 "0xb34248702810a1d76580234a33f942a9801c3680de54cb3ef104572a8d482190",
618 "0xab9d33aee8b578fe5588c4a4b16bbef6fa05fc757020f95b98212e877a919e360a90296f02e97ecbabf3c200271ffe88eb9fd86912e8376cd27237bdad5f3abc1c",
619 )]
620 fn test_signature_matches_py_clob_client_v2(
621 #[case] order: PolymarketOrder,
622 #[case] neg_risk: bool,
623 #[case] expected_hash_hex: &str,
624 #[case] expected_signature_hex: &str,
625 ) {
626 let signer = test_signer();
627
628 let hash = expected_signing_hash(&order, neg_risk);
629 assert_eq!(format!("{hash:#x}"), expected_hash_hex, "signing hash");
630
631 let signature = signer.sign_order(&order, neg_risk).unwrap();
632 assert_eq!(signature, expected_signature_hex, "signature");
633 }
634}