use std::str::FromStr;
use alloy::{
signers::{SignerSync, local::PrivateKeySigner},
sol_types::{SolStruct, SolValue, eip712_domain},
};
use alloy_primitives::{Address, B256, FixedBytes, U256, address, keccak256};
use rust_decimal::Decimal;
use crate::{
common::{
credential::EvmPrivateKey,
enums::{PolymarketOrderSide, SignatureType},
},
http::{
error::{Error, Result},
models::PolymarketOrder,
},
};
const CLOB_AUTH_DOMAIN_NAME: &str = "ClobAuthDomain";
const CLOB_AUTH_DOMAIN_VERSION: &str = "1";
const CLOB_AUTH_MESSAGE: &str = "This message attests that I control the given wallet";
pub const CTF_EXCHANGE: Address = address!("0xE111180000d2663C0091e4f400237545B87B996B");
pub const NEG_RISK_CTF_EXCHANGE: Address = address!("0xe2222d279d744050d28e00520010520000310F59");
const DOMAIN_NAME: &str = "Polymarket CTF Exchange";
const DOMAIN_VERSION: &str = "2";
const POLYGON_CHAIN_ID: u64 = 137;
const ORDER_TYPE_STRING: &str = concat!(
"Order(uint256 salt,address maker,address signer,uint256 tokenId,",
"uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,",
"uint256 timestamp,bytes32 metadata,bytes32 builder)",
);
const SOLADY_TYPE_STRING: &str = concat!(
"TypedDataSign(Order contents,string name,string version,uint256 chainId,",
"address verifyingContract,bytes32 salt)",
"Order(uint256 salt,address maker,address signer,uint256 tokenId,",
"uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,",
"uint256 timestamp,bytes32 metadata,bytes32 builder)",
);
const DOMAIN_TYPE_STRING: &str =
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
const DEPOSIT_WALLET_DOMAIN_NAME: &str = "DepositWallet";
const DEPOSIT_WALLET_DOMAIN_VERSION: &str = "1";
alloy::sol! {
struct ClobAuth {
address address;
string timestamp;
uint256 nonce;
string message;
}
}
alloy::sol! {
struct Order {
uint256 salt;
address maker;
address signer;
uint256 tokenId;
uint256 makerAmount;
uint256 takerAmount;
uint8 side;
uint8 signatureType;
uint256 timestamp;
bytes32 metadata;
bytes32 builder;
}
}
#[derive(Debug)]
pub struct OrderSigner {
signer: PrivateKeySigner,
}
impl OrderSigner {
pub fn new(private_key: &EvmPrivateKey) -> Result<Self> {
let key_hex = private_key
.as_hex()
.strip_prefix("0x")
.unwrap_or(private_key.as_hex());
let signer = PrivateKeySigner::from_str(key_hex)
.map_err(|e| Error::bad_request(format!("Failed to create signer: {e}")))?;
Ok(Self { signer })
}
#[must_use]
pub fn address(&self) -> Address {
self.signer.address()
}
pub fn sign_order(&self, order: &PolymarketOrder, neg_risk: bool) -> Result<String> {
let order_signer = parse_address(&order.signer, "signer")?;
let order_maker = parse_address(&order.maker, "maker")?;
if order.signature_type == SignatureType::Poly1271 {
if order_signer != order_maker {
return Err(Error::bad_request(format!(
"POLY_1271 orders require maker and signer to both be the deposit wallet, maker was {order_maker}, signer was {order_signer}",
)));
}
} else if order_signer != self.signer.address() {
return Err(Error::bad_request(format!(
"Order signer {order_signer} does not match local signer {}",
self.signer.address(),
)));
}
let eip712_order = build_eip712_order(order)?;
let contract = exchange_contract(neg_risk);
if order.signature_type == SignatureType::Poly1271 {
return self.sign_poly_1271_order(&eip712_order, contract);
}
let domain = eip712_domain! {
name: DOMAIN_NAME,
version: DOMAIN_VERSION,
chain_id: POLYGON_CHAIN_ID,
verifying_contract: contract,
};
let signing_hash = eip712_order.eip712_signing_hash(&domain);
self.sign_hash(&signing_hash.0)
}
fn sign_poly_1271_order(&self, order: &Order, contract: Address) -> Result<String> {
let contents_hash = poly_1271_contents_hash(order);
let wallet_struct_hash = poly_1271_wallet_struct_hash(contents_hash, order.signer);
let app_domain_separator = ctf_exchange_domain_separator(contract);
let signing_hash = typed_data_hash(app_domain_separator, wallet_struct_hash);
let signature = self.sign_hash_b256(&signing_hash)?;
let mut encoded =
Vec::with_capacity(65 + 32 + 32 + ORDER_TYPE_STRING.len() + std::mem::size_of::<u16>());
encoded.extend_from_slice(&signature);
encoded.extend_from_slice(app_domain_separator.as_slice());
encoded.extend_from_slice(contents_hash.as_slice());
encoded.extend_from_slice(ORDER_TYPE_STRING.as_bytes());
encoded.extend_from_slice(&(ORDER_TYPE_STRING.len() as u16).to_be_bytes());
Ok(format!(
"0x{}",
alloy_primitives::hex::encode(encoded.as_slice())
))
}
fn sign_hash_b256(&self, hash: &B256) -> Result<[u8; 65]> {
let signature = self
.signer
.sign_hash_sync(hash)
.map_err(|e| Error::bad_request(format!("Failed to sign order: {e}")))?;
Ok(signature.as_bytes())
}
fn sign_hash(&self, hash: &[u8; 32]) -> Result<String> {
let hash_b256 = B256::from(*hash);
let signature = self.sign_hash_b256(&hash_b256)?;
Ok(format!(
"0x{}",
alloy_primitives::hex::encode(signature.as_slice())
))
}
}
pub fn order_hash(order: &PolymarketOrder, neg_risk: bool) -> Result<B256> {
let eip712_order = build_eip712_order(order)?;
let contract = exchange_contract(neg_risk);
let domain = eip712_domain! {
name: DOMAIN_NAME,
version: DOMAIN_VERSION,
chain_id: POLYGON_CHAIN_ID,
verifying_contract: contract,
};
Ok(eip712_order.eip712_signing_hash(&domain))
}
const fn exchange_contract(neg_risk: bool) -> Address {
if neg_risk {
NEG_RISK_CTF_EXCHANGE
} else {
CTF_EXCHANGE
}
}
fn order_type_hash() -> B256 {
keccak256(ORDER_TYPE_STRING.as_bytes())
}
fn solady_type_hash() -> B256 {
keccak256(SOLADY_TYPE_STRING.as_bytes())
}
fn domain_type_hash() -> B256 {
keccak256(DOMAIN_TYPE_STRING.as_bytes())
}
fn domain_name_hash() -> B256 {
keccak256(DOMAIN_NAME.as_bytes())
}
fn domain_version_hash() -> B256 {
keccak256(DOMAIN_VERSION.as_bytes())
}
fn deposit_wallet_name_hash() -> B256 {
keccak256(DEPOSIT_WALLET_DOMAIN_NAME.as_bytes())
}
fn deposit_wallet_version_hash() -> B256 {
keccak256(DEPOSIT_WALLET_DOMAIN_VERSION.as_bytes())
}
fn poly_1271_contents_hash(order: &Order) -> B256 {
let tuple = (
order_type_hash(),
order.salt,
order.maker,
order.signer,
order.tokenId,
order.makerAmount,
order.takerAmount,
U256::from(order.side),
U256::from(order.signatureType),
order.timestamp,
order.metadata,
order.builder,
);
keccak256(tuple.abi_encode())
}
fn poly_1271_wallet_struct_hash(contents_hash: B256, deposit_wallet: Address) -> B256 {
let tuple = (
solady_type_hash(),
contents_hash,
deposit_wallet_name_hash(),
deposit_wallet_version_hash(),
U256::from(POLYGON_CHAIN_ID),
deposit_wallet,
FixedBytes::<32>::ZERO,
);
keccak256(tuple.abi_encode())
}
fn ctf_exchange_domain_separator(contract: Address) -> B256 {
let tuple = (
domain_type_hash(),
domain_name_hash(),
domain_version_hash(),
U256::from(POLYGON_CHAIN_ID),
contract,
);
keccak256(tuple.abi_encode())
}
fn typed_data_hash(domain_separator: B256, struct_hash: B256) -> B256 {
let mut bytes = Vec::with_capacity(2 + 32 + 32);
bytes.push(0x19);
bytes.push(0x01);
bytes.extend_from_slice(domain_separator.as_slice());
bytes.extend_from_slice(struct_hash.as_slice());
keccak256(bytes)
}
pub fn sign_clob_auth(
private_key: &EvmPrivateKey,
timestamp: &str,
nonce: u64,
) -> Result<(String, String)> {
let key_hex = private_key
.as_hex()
.strip_prefix("0x")
.unwrap_or(private_key.as_hex());
let signer = PrivateKeySigner::from_str(key_hex)
.map_err(|e| Error::bad_request(format!("Failed to create signer: {e}")))?;
let address = signer.address();
let auth = ClobAuth {
address,
timestamp: timestamp.to_string(),
nonce: U256::from(nonce),
message: CLOB_AUTH_MESSAGE.to_string(),
};
let domain = eip712_domain! {
name: CLOB_AUTH_DOMAIN_NAME,
version: CLOB_AUTH_DOMAIN_VERSION,
chain_id: POLYGON_CHAIN_ID,
};
let signing_hash = auth.eip712_signing_hash(&domain);
let signature = signer
.sign_hash_sync(&signing_hash)
.map_err(|e| Error::bad_request(format!("Failed to sign ClobAuth: {e}")))?;
let r = signature.r();
let s = signature.s();
let v = if signature.v() { 28u8 } else { 27u8 };
Ok((
format!("{address:#x}"),
format!("0x{r:064x}{s:064x}{v:02x}"),
))
}
fn build_eip712_order(order: &PolymarketOrder) -> Result<Order> {
Ok(Order {
salt: U256::from(order.salt),
maker: parse_address(&order.maker, "maker")?,
signer: parse_address(&order.signer, "signer")?,
tokenId: U256::from_str(order.token_id.as_str())
.map_err(|e| Error::bad_request(format!("Invalid token ID: {e}")))?,
makerAmount: decimal_to_u256(order.maker_amount, "maker_amount")?,
takerAmount: decimal_to_u256(order.taker_amount, "taker_amount")?,
side: order_side_to_u8(order.side),
signatureType: order.signature_type as u8,
timestamp: U256::from_str(&order.timestamp)
.map_err(|e| Error::bad_request(format!("Invalid timestamp: {e}")))?,
metadata: parse_bytes32(&order.metadata, "metadata")?,
builder: parse_bytes32(&order.builder, "builder")?,
})
}
fn parse_address(addr: &str, field: &str) -> Result<Address> {
Address::from_str(addr).map_err(|e| Error::bad_request(format!("Invalid {field} address: {e}")))
}
fn parse_bytes32(value: &str, field: &str) -> Result<FixedBytes<32>> {
FixedBytes::<32>::from_str(value)
.map_err(|e| Error::bad_request(format!("Invalid {field} bytes32: {e}")))
}
fn decimal_to_u256(d: Decimal, field: &str) -> Result<U256> {
let normalized = d.normalize();
if normalized.scale() != 0 {
return Err(Error::bad_request(format!("{field} must be an integer")));
}
let mantissa = normalized.mantissa();
if mantissa < 0 {
return Err(Error::bad_request(format!("{field} must be non-negative")));
}
Ok(U256::from(mantissa as u128))
}
fn order_side_to_u8(side: PolymarketOrderSide) -> u8 {
match side {
PolymarketOrderSide::Buy => 0,
PolymarketOrderSide::Sell => 1,
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::{Signature, keccak256};
use nautilus_core::hex;
use rstest::rstest;
use rust_decimal_macros::dec;
use ustr::Ustr;
use super::*;
use crate::common::enums::SignatureType;
const TEST_PRIVATE_KEY: &str =
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
fn test_signer() -> OrderSigner {
let pk = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
OrderSigner::new(&pk).unwrap()
}
const ZERO_BYTES32: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
fn test_order() -> PolymarketOrder {
PolymarketOrder {
salt: 123456789,
maker: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
signer: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
token_id: Ustr::from(
"71321045679252212594626385532706912750332728571942532289631379312455583992563",
),
maker_amount: dec!(100000000),
taker_amount: dec!(50000000),
side: PolymarketOrderSide::Buy,
signature_type: SignatureType::Eoa,
expiration: "0".to_string(),
timestamp: "1713398400000".to_string(),
metadata: ZERO_BYTES32.to_string(),
builder: ZERO_BYTES32.to_string(),
signature: String::new(),
}
}
#[rstest]
fn test_order_typehash_matches_contract() {
let expected = keccak256(
"Order(uint256 salt,address maker,address signer,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,uint256 timestamp,bytes32 metadata,bytes32 builder)",
);
let order = test_order();
let eip712_order = build_eip712_order(&order).unwrap();
assert_eq!(eip712_order.eip712_type_hash(), expected);
}
#[rstest]
fn test_signer_address_derivation() {
let signer = test_signer();
let expected = Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap();
assert_eq!(signer.address(), expected);
}
#[rstest]
fn test_sign_order_format() {
let signer = test_signer();
let order = test_order();
let sig = signer.sign_order(&order, false).unwrap();
assert!(sig.starts_with("0x"));
assert_eq!(sig.len(), 132); }
#[rstest]
fn test_sign_poly_1271_order_format() {
let signer = test_signer();
let mut order = test_order();
order.maker = "0x1111111111111111111111111111111111111111".to_string();
order.signer = order.maker.clone();
order.signature_type = SignatureType::Poly1271;
let sig = signer.sign_order(&order, false).unwrap();
assert!(sig.starts_with("0x"));
assert_eq!(sig.len(), 636);
}
#[rstest]
fn test_sign_poly_1271_order_requires_deposit_wallet_signer() {
let signer = test_signer();
let mut order = test_order();
order.maker = "0x1111111111111111111111111111111111111111".to_string();
order.signature_type = SignatureType::Poly1271;
let err = signer.sign_order(&order, false).unwrap_err();
assert!(
err.to_string()
.contains("maker and signer to both be the deposit wallet")
);
}
#[rstest]
fn test_sign_order_deterministic() {
let signer = test_signer();
let order = test_order();
let sig1 = signer.sign_order(&order, false).unwrap();
let sig2 = signer.sign_order(&order, false).unwrap();
assert_eq!(sig1, sig2);
}
#[rstest]
fn test_sign_order_neg_risk_differs() {
let signer = test_signer();
let order = test_order();
let sig_normal = signer.sign_order(&order, false).unwrap();
let sig_neg_risk = signer.sign_order(&order, true).unwrap();
assert_ne!(sig_normal, sig_neg_risk);
}
#[rstest]
fn test_sign_order_sell_side() {
let signer = test_signer();
let mut order = test_order();
let sig_buy = signer.sign_order(&order, false).unwrap();
order.side = PolymarketOrderSide::Sell;
let sig_sell = signer.sign_order(&order, false).unwrap();
assert_ne!(sig_buy, sig_sell);
}
#[rstest]
fn test_sign_order_different_amounts() {
let signer = test_signer();
let mut order = test_order();
let sig1 = signer.sign_order(&order, false).unwrap();
order.maker_amount = dec!(200000000);
let sig2 = signer.sign_order(&order, false).unwrap();
assert_ne!(sig1, sig2);
}
#[rstest]
fn test_build_eip712_order() {
let order = test_order();
let eip712 = build_eip712_order(&order).unwrap();
assert_eq!(eip712.salt, U256::from(123456789u64));
assert_eq!(
eip712.maker,
Address::from_str("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266").unwrap()
);
assert_eq!(eip712.makerAmount, U256::from(100000000u128));
assert_eq!(eip712.takerAmount, U256::from(50000000u128));
assert_eq!(eip712.side, 0); assert_eq!(eip712.signatureType, 0); assert_eq!(eip712.timestamp, U256::from(1713398400000u128));
assert_eq!(eip712.metadata, FixedBytes::<32>::ZERO);
assert_eq!(eip712.builder, FixedBytes::<32>::ZERO);
}
#[rstest]
fn test_build_eip712_order_with_builder_code() {
let mut order = test_order();
order.builder =
"0x0000000000000000000000000000000000000000000000000000000000000001".to_string();
let eip712 = build_eip712_order(&order).unwrap();
let mut expected = [0u8; 32];
expected[31] = 1;
assert_eq!(eip712.builder, FixedBytes::<32>::from(expected));
}
#[rstest]
fn test_decimal_to_u256_integer() {
let result = decimal_to_u256(dec!(100000000), "test").unwrap();
assert_eq!(result, U256::from(100000000u128));
}
#[rstest]
fn test_decimal_to_u256_zero() {
let result = decimal_to_u256(dec!(0), "test").unwrap();
assert_eq!(result, U256::ZERO);
}
#[rstest]
fn test_decimal_to_u256_rejects_fractional() {
let result = decimal_to_u256(dec!(100.5), "test");
assert!(result.is_err());
}
#[rstest]
fn test_decimal_to_u256_rejects_negative() {
let result = decimal_to_u256(dec!(-1), "test");
assert!(result.is_err());
}
#[rstest]
fn test_order_side_mapping() {
assert_eq!(order_side_to_u8(PolymarketOrderSide::Buy), 0);
assert_eq!(order_side_to_u8(PolymarketOrderSide::Sell), 1);
}
#[rstest]
fn test_contract_addresses_nonzero() {
assert_ne!(CTF_EXCHANGE, Address::ZERO);
assert_ne!(NEG_RISK_CTF_EXCHANGE, Address::ZERO);
assert_ne!(CTF_EXCHANGE, NEG_RISK_CTF_EXCHANGE);
}
#[rstest]
fn test_v2_contract_addresses_pinned() {
assert_eq!(
format!("{CTF_EXCHANGE:#x}"),
"0xe111180000d2663c0091e4f400237545b87b996b"
);
assert_eq!(
format!("{NEG_RISK_CTF_EXCHANGE:#x}"),
"0xe2222d279d744050d28e00520010520000310f59"
);
}
#[rstest]
fn test_domain_version_is_v2() {
assert_eq!(DOMAIN_VERSION, "2");
}
#[rstest]
fn test_sign_order_recoverable() {
let signer = test_signer();
let order = test_order();
let sig_hex = signer.sign_order(&order, false).unwrap();
let sig_bytes = hex::decode(&sig_hex[2..]).unwrap();
assert_eq!(sig_bytes.len(), 65);
let r = U256::from_be_slice(&sig_bytes[..32]);
let s = U256::from_be_slice(&sig_bytes[32..64]);
let v = sig_bytes[64];
let y_parity = v == 28;
let signature = Signature::new(r, s, y_parity);
let eip712_order = build_eip712_order(&order).unwrap();
let domain = eip712_domain! {
name: DOMAIN_NAME,
version: DOMAIN_VERSION,
chain_id: POLYGON_CHAIN_ID,
verifying_contract: CTF_EXCHANGE,
};
let signing_hash = eip712_order.eip712_signing_hash(&domain);
let recovered = signature
.recover_address_from_prehash(&signing_hash)
.unwrap();
assert_eq!(recovered, signer.address());
}
const PARITY_TOKEN_ID: &str =
"71321045679252212594626385532706912750332728571942532289631379312455583992563";
fn parity_order(
salt: u64,
side: PolymarketOrderSide,
signature_type: SignatureType,
maker_amount: Decimal,
taker_amount: Decimal,
timestamp: &str,
builder: &str,
) -> PolymarketOrder {
PolymarketOrder {
salt,
maker: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
signer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
token_id: Ustr::from(PARITY_TOKEN_ID),
maker_amount,
taker_amount,
side,
signature_type,
expiration: "0".to_string(),
timestamp: timestamp.to_string(),
metadata: ZERO_BYTES32.to_string(),
builder: builder.to_string(),
signature: String::new(),
}
}
#[rstest]
#[case::buy_standard_eoa(
parity_order(
123456789,
PolymarketOrderSide::Buy,
SignatureType::Eoa,
dec!(100000000),
dec!(50000000),
"1713398400000",
ZERO_BYTES32,
),
false,
"0x32961c48ddac87ed3582f8e02097cd0eff4fcf80460306bd44b3710438dfa64c",
"0x89f178136333c8ebb32a19146cb891233e3202d474be6ef730c24dbc06ae4d2a0c99948a86d9b57de0f2c0bb8ec6964aa244ecf17cfa3a95b86878d0b64ad78a1b",
)]
#[case::sell_neg_risk_eoa(
parity_order(
987654321,
PolymarketOrderSide::Sell,
SignatureType::Eoa,
dec!(50000000),
dec!(100000000),
"1713398400000",
ZERO_BYTES32,
),
true,
"0x8b878404bd92dea2bfea9975c9fcd816ec70a57ae431cb20d67bb773744aaef3",
"0xf7d60d64364e2615b08d9f69f3ea9afd3b4f83ecfbf05ddd3ca83f4916277fb97d2d080758d17c8e91c928aceb8ff252477ebaec051b672832500f63b4d36b061c",
)]
#[case::buy_with_builder_code_eoa(
parity_order(
1,
PolymarketOrderSide::Buy,
SignatureType::Eoa,
dec!(100000000),
dec!(50000000),
"1713398500000",
"0x0000000000000000000000000000000000000000000000000000000000000001",
),
false,
"0x3df0b6f6ddfca837bc36964cae968b34ad35640b5d98f557c104da97e804e36a",
"0xf4d2b34659e8bc07a9572d40ee5a1639a1157409613b4c21566b1f33fd8fe11a364b3f306668cae7248ca7cdf72378f9266bc5628585aa939644400030671e081c",
)]
#[case::buy_poly_proxy(
// V2 unblocks EIP-1271 smart contract wallet signing. signatureType
// enters the typed-data hash directly, so a regression that only
// manifests for proxy/safe wallets is undetectable from the Eoa
// cases above.
parity_order(
111_111_111,
PolymarketOrderSide::Buy,
SignatureType::PolyProxy,
dec!(100000000),
dec!(50000000),
"1713398400000",
ZERO_BYTES32,
),
false,
"0x8f88fe2fb3448f4b8ba639992029f0a47a01a14d15b5f2bf9833516571efd279",
"0x71a63c85b730cc934688f23ea6374afffef57a61690eba63dcb97a706c8a8d0f3d2a8e0280f3e252eb83be088ebae6b461a8eda18e559e079f59050b90057afa1c",
)]
#[case::sell_neg_risk_poly_gnosis_safe(
parity_order(
222_222_222,
PolymarketOrderSide::Sell,
SignatureType::PolyGnosisSafe,
dec!(50000000),
dec!(100000000),
"1713398400000",
ZERO_BYTES32,
),
true,
"0xb34248702810a1d76580234a33f942a9801c3680de54cb3ef104572a8d482190",
"0xab9d33aee8b578fe5588c4a4b16bbef6fa05fc757020f95b98212e877a919e360a90296f02e97ecbabf3c200271ffe88eb9fd86912e8376cd27237bdad5f3abc1c",
)]
fn test_signature_matches_py_clob_client_v2(
#[case] order: PolymarketOrder,
#[case] neg_risk: bool,
#[case] expected_hash_hex: &str,
#[case] expected_signature_hex: &str,
) {
let signer = test_signer();
let hash = order_hash(&order, neg_risk).unwrap();
assert_eq!(format!("{hash:#x}"), expected_hash_hex, "signing hash");
let signature = signer.sign_order(&order, neg_risk).unwrap();
assert_eq!(signature, expected_signature_hex, "signature");
}
#[rstest]
#[case::standard_exchange(
false,
"0x48cfd4c03dcee72230750e2dc5ea71048e91244c2a9fec2ed6ef790a74869596",
concat!(
"0x780beffb568c4510b8a135a92ca8071f124cf368fb0e902fe451c5f51e555791",
"0a2a87968caa08a242de85e83c27375fc09242e897aa77680f56622a54e6e7c",
"61c3264e159346253e26a64e00b69032db0e7d32f94628de3e6eecb50304d",
"7af3d2d3db4f9eed41f0490532a9460395d96602c6534a931bde4f0e3aad5",
"71e4f84a04f726465722875696e743235362073616c742c6164647265737320",
"6d616b65722c61646472657373207369676e65722c75696e7432353620746f",
"6b656e49642c75696e74323536206d616b6572416d6f756e742c75696e7432",
"35362074616b6572416d6f756e742c75696e743820736964652c75696e7438",
"207369676e6174757265547970652c75696e743235362074696d657374616d",
"702c62797465733332206d657461646174612c62797465733332206275696c",
"6465722900ba",
),
)]
#[case::neg_risk_exchange(
true,
"0x82b94b9d570fdd7b07f517ea848606a9b74029f2387fbf07d8b9c856d652e608",
concat!(
"0xa411986b67c58113f8787ee54c41def9376d57ef4262b56438438710720604b7",
"00137e0175d7ae6afc40c2988179ad84b1b71a739f0974c8df3c12372f31b22c",
"1c9b858f53327b0bd13af8ec14cfb35234fb9eb7b0504d1a4e61f433840d3",
"0e81ad3db4f9eed41f0490532a9460395d96602c6534a931bde4f0e3aad571",
"e4f84a04f726465722875696e743235362073616c742c61646472657373206d",
"616b65722c61646472657373207369676e65722c75696e7432353620746f6b",
"656e49642c75696e74323536206d616b6572416d6f756e742c75696e743235",
"362074616b6572416d6f756e742c75696e743820736964652c75696e743820",
"7369676e6174757265547970652c75696e743235362074696d657374616d70",
"2c62797465733332206d657461646174612c62797465733332206275696c64",
"65722900ba",
),
)]
fn test_poly_1271_signature_matches_py_clob_client_v2(
#[case] neg_risk: bool,
#[case] expected_hash: &str,
#[case] expected_signature: &str,
) {
let signer = test_signer();
let mut order = parity_order(
333_333_333,
PolymarketOrderSide::Buy,
SignatureType::Poly1271,
dec!(100000000),
dec!(50000000),
"1713398400000",
ZERO_BYTES32,
);
order.maker = "0x1111111111111111111111111111111111111111".to_string();
order.signer = order.maker.clone();
let hash = order_hash(&order, neg_risk).unwrap();
assert_eq!(format!("{hash:#x}"), expected_hash, "signing hash");
let signature = signer.sign_order(&order, neg_risk).unwrap();
assert_eq!(signature, expected_signature, "signature");
}
}