1use crate::auth::{
7 sign_order_message, sign_order_message_with_domain, sign_poly1271_order_message_with_domain,
8 PreparedOrderDomain, SignedOrderMessage,
9};
10use crate::errors::{PolyfillError, Result};
11use crate::types::{
12 CreateOrderOptions, MarketOrderArgs, OrderArgs, OrderType, Side, SignedOrderRequest,
13 SCALE_FACTOR,
14};
15use alloy_primitives::{keccak256, Address, B256, U256};
16use alloy_signer_local::PrivateKeySigner;
17use rand::Rng;
18use rust_decimal::Decimal;
19use rust_decimal::RoundingStrategy::{AwayFromZero, MidpointTowardZero, ToZero};
20use std::str::FromStr;
21use std::time::{SystemTime, UNIX_EPOCH};
22
23pub const BYTES32_ZERO: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
24
25#[derive(Copy, Clone, Debug, PartialEq, Eq)]
27pub enum SigType {
28 Eoa = 0,
30 PolyProxy = 1,
32 PolyGnosisSafe = 2,
34 Poly1271 = 3,
36}
37
38#[derive(Clone, Copy)]
40pub struct RoundConfig {
41 price: u32,
42 size: u32,
43 amount: u32,
44}
45
46pub struct ContractConfig {
48 pub exchange: String,
49 pub collateral: String,
50 pub conditional_tokens: String,
51}
52
53#[derive(Clone)]
55pub struct OrderBuilder {
56 signer: PrivateKeySigner,
57 signer_address: Address,
58 signer_checksum: String,
59 sig_type: SigType,
60 funder: Address,
61 funder_checksum: String,
62}
63
64#[derive(Clone)]
66pub struct PreparedOrderPath {
67 builder: OrderBuilder,
68 chain_id: u64,
69 token_id: String,
70 token_id_u256: U256,
71 round_config: RoundConfig,
72 domain: PreparedOrderDomain,
73 builder_bytes: B256,
74 builder_code: String,
75 metadata_bytes: B256,
76 metadata: String,
77}
78
79const POLYGON_PROXY_FACTORY: &str = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052";
80const POLYGON_SAFE_FACTORY: &str = "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b";
81const PROXY_INIT_CODE_HASH: &str =
82 "0xd21df8dc65880a8606f09fe0ce3df9b8869287ab0b058be05aa9e8af6330a00b";
83const SAFE_INIT_CODE_HASH: &str =
84 "0x2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf";
85
86const ROUND_CONFIG_0_1: RoundConfig = RoundConfig {
87 price: 1,
88 size: 2,
89 amount: 3,
90};
91const ROUND_CONFIG_0_01: RoundConfig = RoundConfig {
92 price: 2,
93 size: 2,
94 amount: 4,
95};
96const ROUND_CONFIG_0_001: RoundConfig = RoundConfig {
97 price: 3,
98 size: 2,
99 amount: 5,
100};
101const ROUND_CONFIG_0_0001: RoundConfig = RoundConfig {
102 price: 4,
103 size: 2,
104 amount: 6,
105};
106const TOKEN_UNIT_SCALE: Decimal = Decimal::from_parts(1_000_000, 0, 0, false, 0);
107
108pub fn get_contract_config(chain_id: u64, neg_risk: bool) -> Option<ContractConfig> {
110 match (chain_id, neg_risk) {
111 (137, false) => Some(ContractConfig {
112 exchange: "0xE111180000d2663C0091e4f400237545B87B996B".to_string(),
113 collateral: "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB".to_string(),
114 conditional_tokens: "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045".to_string(),
115 }),
116 (137, true) => Some(ContractConfig {
117 exchange: "0xe2222d279d744050d28e00520010520000310F59".to_string(),
118 collateral: "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB".to_string(),
119 conditional_tokens: "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045".to_string(),
120 }),
121 _ => None,
122 }
123}
124
125fn exchange_address_for(chain_id: u64, neg_risk: bool) -> Result<Address> {
126 let contract_config = get_contract_config(chain_id, neg_risk).ok_or_else(|| {
127 PolyfillError::config("No contract found with given chain_id and neg_risk")
128 })?;
129
130 Address::from_str(&contract_config.exchange)
131 .map_err(|e| PolyfillError::config(format!("Invalid exchange address: {}", e)))
132}
133
134fn parse_token_id(token_id: &str) -> Result<U256> {
135 U256::from_str_radix(token_id, 10)
136 .map_err(|e| PolyfillError::validation(format!("Incorrect tokenId format: {}", e)))
137}
138
139fn parse_optional_bytes32(name: &str, value: Option<&str>) -> Result<(B256, String)> {
140 let normalized = normalize_optional_bytes32(name, value)?;
141 let parsed = B256::from_str(&normalized)
142 .map_err(|e| PolyfillError::validation(format!("Invalid {name} bytes32 value: {e}")))?;
143
144 Ok((parsed, normalized))
145}
146
147pub fn sig_type_from_u8(signature_type: u8) -> Result<SigType> {
148 match signature_type {
149 0 => Ok(SigType::Eoa),
150 1 => Ok(SigType::PolyProxy),
151 2 => Ok(SigType::PolyGnosisSafe),
152 3 => Ok(SigType::Poly1271),
153 other => Err(PolyfillError::validation(format!(
154 "Unsupported signature_type {other}"
155 ))),
156 }
157}
158
159pub fn derive_proxy_wallet(eoa_address: Address, chain_id: u64) -> Result<Address> {
160 if chain_id != 137 {
161 return Err(PolyfillError::config(
162 "Proxy wallet auto-derivation is only configured for Polygon mainnet",
163 ));
164 }
165
166 let factory = Address::from_str(POLYGON_PROXY_FACTORY)
167 .map_err(|e| PolyfillError::config(format!("Invalid proxy factory address: {e}")))?;
168 let init_code_hash = B256::from_str(PROXY_INIT_CODE_HASH)
169 .map_err(|e| PolyfillError::config(format!("Invalid proxy init code hash: {e}")))?;
170 let salt = keccak256(eoa_address);
171 Ok(factory.create2(salt, init_code_hash))
172}
173
174pub fn derive_safe_wallet(eoa_address: Address, chain_id: u64) -> Result<Address> {
175 if chain_id != 137 {
176 return Err(PolyfillError::config(
177 "Safe wallet auto-derivation is only configured for Polygon mainnet",
178 ));
179 }
180
181 let factory = Address::from_str(POLYGON_SAFE_FACTORY)
182 .map_err(|e| PolyfillError::config(format!("Invalid safe factory address: {e}")))?;
183 let init_code_hash = B256::from_str(SAFE_INIT_CODE_HASH)
184 .map_err(|e| PolyfillError::config(format!("Invalid safe init code hash: {e}")))?;
185 let mut padded = [0_u8; 32];
186 padded[12..].copy_from_slice(eoa_address.as_slice());
187 let salt = keccak256(padded);
188 Ok(factory.create2(salt, init_code_hash))
189}
190
191pub fn resolve_funder(
192 signer_address: Address,
193 chain_id: u64,
194 sig_type: SigType,
195 funder: Option<Address>,
196) -> Result<Option<Address>> {
197 match (sig_type, funder) {
198 (SigType::Eoa, Some(_)) => Err(PolyfillError::validation(
199 "funder cannot be set for EOA signature_type",
200 )),
201 (SigType::PolyProxy, None) => derive_proxy_wallet(signer_address, chain_id).map(Some),
202 (SigType::PolyGnosisSafe, None) => derive_safe_wallet(signer_address, chain_id).map(Some),
203 (SigType::Poly1271, None) => Err(PolyfillError::validation(
204 "funder is required for Poly1271 signature_type",
205 )),
206 (_, Some(Address::ZERO)) => Err(PolyfillError::validation("funder cannot be zero address")),
207 (_, explicit) => Ok(explicit),
208 }
209}
210
211fn generate_seed() -> u64 {
213 let mut rng = rand::thread_rng();
214 let y: f64 = rng.gen();
215 let timestamp = SystemTime::now()
216 .duration_since(UNIX_EPOCH)
217 .expect("Time went backwards")
218 .as_secs();
219 (timestamp as f64 * y) as u64
220}
221
222fn decimal_to_token_units(amt: Decimal) -> Result<U256> {
224 let mut amt = TOKEN_UNIT_SCALE * amt;
225 if amt.scale() > 0 {
226 amt = amt.round_dp_with_strategy(0, MidpointTowardZero);
227 }
228 let units: u128 = amt
229 .try_into()
230 .map_err(|_| PolyfillError::validation(format!("Invalid token amount {amt}")))?;
231 Ok(U256::from(units))
232}
233
234fn parse_round_config(tick_size: Decimal) -> Result<&'static RoundConfig> {
235 let scaled = tick_size * Decimal::from(SCALE_FACTOR);
236 if !scaled.is_integer() {
237 return Err(PolyfillError::validation(format!(
238 "Unsupported tick size {tick_size}"
239 )));
240 }
241
242 let tick_size_ticks: u32 = scaled
243 .try_into()
244 .map_err(|_| PolyfillError::validation(format!("Unsupported tick size {tick_size}")))?;
245
246 match tick_size_ticks {
247 1000 => Ok(&ROUND_CONFIG_0_1),
248 100 => Ok(&ROUND_CONFIG_0_01),
249 10 => Ok(&ROUND_CONFIG_0_001),
250 1 => Ok(&ROUND_CONFIG_0_0001),
251 _ => Err(PolyfillError::validation(format!(
252 "Unsupported tick size {tick_size}"
253 ))),
254 }
255}
256
257pub(crate) fn validate_bytes32_hex(field: &str, value: &str) -> Result<()> {
258 if value == BYTES32_ZERO {
259 return Ok(());
260 }
261
262 if !value.starts_with("0x") {
263 return Err(PolyfillError::validation(format!(
264 "{field} must be a 0x-prefixed 32-byte hex string"
265 )));
266 }
267
268 if value.len() != 66 {
269 return Err(PolyfillError::validation(format!(
270 "{field} must be exactly 32 bytes (64 hex chars)"
271 )));
272 }
273
274 if !value
275 .as_bytes()
276 .iter()
277 .skip(2)
278 .all(|byte| byte.is_ascii_hexdigit())
279 {
280 return Err(PolyfillError::validation(format!(
281 "{field} must contain only hexadecimal characters"
282 )));
283 }
284
285 Ok(())
286}
287
288fn normalize_optional_bytes32(field: &str, value: Option<&str>) -> Result<String> {
289 let value = value.unwrap_or(BYTES32_ZERO);
290 validate_bytes32_hex(field, value)?;
291 Ok(value.to_string())
292}
293
294pub fn adjust_buy_amount_for_fees(
295 amount: Decimal,
296 price: Decimal,
297 user_usdc_balance: Decimal,
298 fee_rate: Decimal,
299 fee_exponent: u32,
300 builder_taker_fee_rate: Decimal,
301) -> Result<Decimal> {
302 if price <= Decimal::ZERO {
303 return Err(PolyfillError::validation(
304 "Market buy fee adjustment requires a positive price",
305 ));
306 }
307
308 let base = price * (Decimal::ONE - price);
309 let base_f64: f64 = base
310 .try_into()
311 .map_err(|_| PolyfillError::validation(format!("Invalid fee base {base}")))?;
312 let exp_f64: f64 = Decimal::from(fee_exponent)
313 .try_into()
314 .map_err(|_| PolyfillError::validation(format!("Invalid fee exponent {fee_exponent}")))?;
315 let platform_fee_rate = fee_rate
316 * Decimal::try_from(base_f64.powf(exp_f64)).map_err(|_| {
317 PolyfillError::validation(format!(
318 "Invalid platform fee rate for price {price} and exponent {fee_exponent}"
319 ))
320 })?;
321
322 let platform_fee = amount / price * platform_fee_rate;
323 let total_cost = amount + platform_fee + amount * builder_taker_fee_rate;
324
325 let raw = if user_usdc_balance <= total_cost {
326 let divisor = Decimal::ONE + platform_fee_rate / price + builder_taker_fee_rate;
327 user_usdc_balance / divisor
328 } else {
329 amount
330 };
331
332 let adjusted = raw.trunc_with_scale(6);
333 if adjusted.is_zero() {
334 return Err(PolyfillError::validation(format!(
335 "user_usdc_balance {user_usdc_balance} too small to cover fees at price {price}; \
336 fee-adjusted amount truncated to zero"
337 )));
338 }
339
340 Ok(adjusted)
341}
342
343impl OrderBuilder {
344 pub fn new(
346 signer: PrivateKeySigner,
347 sig_type: Option<SigType>,
348 funder: Option<Address>,
349 ) -> Self {
350 let sig_type = sig_type.unwrap_or(SigType::Eoa);
351 let signer_address = signer.address();
352 let signer_checksum = signer_address.to_checksum(None);
353 let funder = funder.unwrap_or(signer_address);
354 let funder_checksum = funder.to_checksum(None);
355
356 OrderBuilder {
357 signer,
358 signer_address,
359 signer_checksum,
360 sig_type,
361 funder,
362 funder_checksum,
363 }
364 }
365
366 pub fn get_sig_type(&self) -> u8 {
368 self.sig_type as u8
369 }
370
371 pub fn prepare_order_path(
377 &self,
378 chain_id: u64,
379 token_id: impl Into<String>,
380 tick_size: Decimal,
381 neg_risk: bool,
382 builder_code: Option<&str>,
383 metadata: Option<&str>,
384 ) -> Result<PreparedOrderPath> {
385 let token_id = token_id.into();
386 let token_id_u256 = parse_token_id(&token_id)?;
387 let round_config = *parse_round_config(tick_size)?;
388 let exchange = exchange_address_for(chain_id, neg_risk)?;
389 let domain = PreparedOrderDomain::new(chain_id, exchange);
390 let (builder_bytes, builder_code) = parse_optional_bytes32("builder_code", builder_code)?;
391 let (metadata_bytes, metadata) = parse_optional_bytes32("metadata", metadata)?;
392
393 Ok(PreparedOrderPath {
394 builder: self.clone(),
395 chain_id,
396 token_id,
397 token_id_u256,
398 round_config,
399 domain,
400 builder_bytes,
401 builder_code,
402 metadata_bytes,
403 metadata,
404 })
405 }
406
407 fn fix_amount_rounding(&self, mut amt: Decimal, round_config: &RoundConfig) -> Decimal {
409 if amt.scale() > round_config.amount {
410 amt = amt.round_dp_with_strategy(round_config.amount + 4, AwayFromZero);
411 if amt.scale() > round_config.amount {
412 amt = amt.round_dp_with_strategy(round_config.amount, ToZero);
413 }
414 }
415 amt
416 }
417
418 fn get_order_amounts(
420 &self,
421 side: Side,
422 size: Decimal,
423 price: Decimal,
424 round_config: &RoundConfig,
425 ) -> Result<(U256, U256)> {
426 let raw_price = price.round_dp_with_strategy(round_config.price, MidpointTowardZero);
427
428 let amounts = match side {
429 Side::BUY => {
430 let raw_taker_amt = size.round_dp_with_strategy(round_config.size, ToZero);
431 let raw_maker_amt = raw_taker_amt * raw_price;
432 let raw_maker_amt = self.fix_amount_rounding(raw_maker_amt, round_config);
433 (
434 decimal_to_token_units(raw_maker_amt)?,
435 decimal_to_token_units(raw_taker_amt)?,
436 )
437 },
438 Side::SELL => {
439 let raw_maker_amt = size.round_dp_with_strategy(round_config.size, ToZero);
440 let raw_taker_amt = raw_maker_amt * raw_price;
441 let raw_taker_amt = self.fix_amount_rounding(raw_taker_amt, round_config);
442
443 (
444 decimal_to_token_units(raw_maker_amt)?,
445 decimal_to_token_units(raw_taker_amt)?,
446 )
447 },
448 };
449
450 Ok(amounts)
451 }
452
453 fn get_market_order_amounts(
455 &self,
456 side: Side,
457 amount: Decimal,
458 price: Decimal,
459 round_config: &RoundConfig,
460 ) -> Result<(U256, U256)> {
461 let raw_price = price.round_dp_with_strategy(round_config.price, MidpointTowardZero);
462
463 let amounts = match side {
464 Side::BUY => {
465 let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero);
466 let raw_taker_amt =
467 self.fix_amount_rounding(raw_maker_amt / raw_price, round_config);
468
469 (
470 decimal_to_token_units(raw_maker_amt)?,
471 decimal_to_token_units(raw_taker_amt)?,
472 )
473 },
474 Side::SELL => {
475 let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero);
476 let raw_taker_amt =
477 self.fix_amount_rounding(raw_maker_amt * raw_price, round_config);
478
479 (
480 decimal_to_token_units(raw_maker_amt)?,
481 decimal_to_token_units(raw_taker_amt)?,
482 )
483 },
484 };
485
486 Ok(amounts)
487 }
488
489 pub fn calculate_market_price(
491 &self,
492 positions: &[crate::types::BookLevel],
493 amount_to_match: Decimal,
494 side: Side,
495 order_type: OrderType,
496 ) -> Result<Decimal> {
497 let mut sum = Decimal::ZERO;
498 let mut last_price = None;
499
500 for level in positions {
501 sum += match side {
502 Side::BUY => level.size * level.price,
503 Side::SELL => level.size,
504 };
505 last_price = Some(level.price);
506 if sum >= amount_to_match {
507 return Ok(level.price);
508 }
509 }
510
511 match (order_type, last_price) {
512 (OrderType::FAK, Some(price)) => Ok(price),
513 _ => Err(PolyfillError::order(
514 format!(
515 "Not enough liquidity to create market order with amount {}",
516 amount_to_match
517 ),
518 crate::errors::OrderErrorKind::InsufficientBalance,
519 )),
520 }
521 }
522
523 pub fn create_market_order(
525 &self,
526 chain_id: u64,
527 order_args: &MarketOrderArgs,
528 price: Decimal,
529 options: &CreateOrderOptions,
530 ) -> Result<SignedOrderRequest> {
531 if !matches!(order_args.order_type, OrderType::FOK | OrderType::FAK) {
532 return Err(PolyfillError::validation(
533 "Market orders only support FOK and FAK order types",
534 ));
535 }
536
537 let tick_size = options
538 .tick_size
539 .ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?;
540 let round_config = parse_round_config(tick_size)?;
541
542 let (maker_amount, taker_amount) =
543 self.get_market_order_amounts(order_args.side, order_args.amount, price, round_config)?;
544
545 let neg_risk = options
546 .neg_risk
547 .ok_or_else(|| PolyfillError::validation("Cannot create order without neg_risk"))?;
548
549 let exchange_address = exchange_address_for(chain_id, neg_risk)?;
550
551 self.build_signed_order(
552 order_args.token_id.clone(),
553 order_args.side,
554 chain_id,
555 exchange_address,
556 maker_amount,
557 taker_amount,
558 0,
559 order_args.builder_code.as_deref(),
560 order_args.metadata.as_deref(),
561 )
562 }
563
564 pub fn create_order(
566 &self,
567 chain_id: u64,
568 order_args: &OrderArgs,
569 options: &CreateOrderOptions,
570 ) -> Result<SignedOrderRequest> {
571 let tick_size = options
572 .tick_size
573 .ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?;
574 let round_config = parse_round_config(tick_size)?;
575
576 let (maker_amount, taker_amount) = self.get_order_amounts(
577 order_args.side,
578 order_args.size,
579 order_args.price,
580 round_config,
581 )?;
582
583 let neg_risk = options
584 .neg_risk
585 .ok_or_else(|| PolyfillError::validation("Cannot create order without neg_risk"))?;
586
587 let exchange_address = exchange_address_for(chain_id, neg_risk)?;
588
589 self.build_signed_order(
590 order_args.token_id.clone(),
591 order_args.side,
592 chain_id,
593 exchange_address,
594 maker_amount,
595 taker_amount,
596 order_args.expiration.unwrap_or(0),
597 order_args.builder_code.as_deref(),
598 order_args.metadata.as_deref(),
599 )
600 }
601
602 #[allow(clippy::too_many_arguments)]
604 fn build_signed_order(
605 &self,
606 token_id: String,
607 side: Side,
608 chain_id: u64,
609 exchange: Address,
610 maker_amount: U256,
611 taker_amount: U256,
612 expiration: u64,
613 builder_code: Option<&str>,
614 metadata: Option<&str>,
615 ) -> Result<SignedOrderRequest> {
616 let seed = generate_seed();
617 let timestamp = SystemTime::now()
618 .duration_since(UNIX_EPOCH)
619 .expect("Time went backwards")
620 .as_millis();
621
622 let u256_token_id = parse_token_id(&token_id)?;
623 let (builder_bytes, builder) = parse_optional_bytes32("builder_code", builder_code)?;
624 let (metadata_bytes, metadata) = parse_optional_bytes32("metadata", metadata)?;
625
626 let signer_address = self.order_signer_address();
627 let signer_checksum = self.order_signer_checksum();
628 let order = SignedOrderMessage {
629 salt: U256::from(seed),
630 maker: self.funder,
631 signer: signer_address,
632 token_id: u256_token_id,
633 maker_amount,
634 taker_amount,
635 side: side as u8,
636 signature_type: self.sig_type as u8,
637 timestamp: U256::from(timestamp),
638 metadata: metadata_bytes,
639 builder: builder_bytes,
640 };
641
642 let signature = match self.sig_type {
643 SigType::Poly1271 => sign_poly1271_order_message_with_domain(
644 &self.signer,
645 order,
646 &PreparedOrderDomain::new(chain_id, exchange),
647 self.funder,
648 chain_id,
649 )?,
650 _ => sign_order_message(&self.signer, order, chain_id, exchange)?,
651 };
652
653 Ok(SignedOrderRequest {
654 salt: seed,
655 maker: self.funder_checksum.clone(),
656 signer: signer_checksum,
657 token_id,
658 maker_amount: maker_amount.to_string(),
659 taker_amount: taker_amount.to_string(),
660 expiration: expiration.to_string(),
661 side: side.as_str().to_string(),
662 signature_type: self.sig_type as u8,
663 timestamp: timestamp.to_string(),
664 metadata,
665 builder,
666 signature,
667 })
668 }
669}
670
671impl OrderBuilder {
672 fn order_signer_address(&self) -> Address {
673 match self.sig_type {
674 SigType::Poly1271 => self.funder,
675 _ => self.signer_address,
676 }
677 }
678
679 fn order_signer_checksum(&self) -> String {
680 match self.sig_type {
681 SigType::Poly1271 => self.funder_checksum.clone(),
682 _ => self.signer_checksum.clone(),
683 }
684 }
685}
686
687impl PreparedOrderPath {
688 pub fn create_limit_order(
690 &self,
691 side: Side,
692 price: Decimal,
693 size: Decimal,
694 expiration: Option<u64>,
695 ) -> Result<SignedOrderRequest> {
696 let (maker_amount, taker_amount) =
697 self.builder
698 .get_order_amounts(side, size, price, &self.round_config)?;
699
700 self.build_signed_order(side, maker_amount, taker_amount, expiration.unwrap_or(0))
701 }
702
703 pub fn create_market_order(
705 &self,
706 side: Side,
707 amount: Decimal,
708 price: Decimal,
709 order_type: OrderType,
710 ) -> Result<SignedOrderRequest> {
711 if !matches!(order_type, OrderType::FOK | OrderType::FAK) {
712 return Err(PolyfillError::validation(
713 "Market orders only support FOK and FAK order types",
714 ));
715 }
716
717 let (maker_amount, taker_amount) =
718 self.builder
719 .get_market_order_amounts(side, amount, price, &self.round_config)?;
720
721 self.build_signed_order(side, maker_amount, taker_amount, 0)
722 }
723
724 fn build_signed_order(
725 &self,
726 side: Side,
727 maker_amount: U256,
728 taker_amount: U256,
729 expiration: u64,
730 ) -> Result<SignedOrderRequest> {
731 let seed = generate_seed();
732 let timestamp = SystemTime::now()
733 .duration_since(UNIX_EPOCH)
734 .expect("Time went backwards")
735 .as_millis();
736
737 let signer_address = self.builder.order_signer_address();
738 let signer_checksum = self.builder.order_signer_checksum();
739 let order = SignedOrderMessage {
740 salt: U256::from(seed),
741 maker: self.builder.funder,
742 signer: signer_address,
743 token_id: self.token_id_u256,
744 maker_amount,
745 taker_amount,
746 side: side as u8,
747 signature_type: self.builder.sig_type as u8,
748 timestamp: U256::from(timestamp),
749 metadata: self.metadata_bytes,
750 builder: self.builder_bytes,
751 };
752
753 let signature = match self.builder.sig_type {
754 SigType::Poly1271 => sign_poly1271_order_message_with_domain(
755 &self.builder.signer,
756 order,
757 &self.domain,
758 self.builder.funder,
759 self.chain_id,
760 )?,
761 _ => sign_order_message_with_domain(&self.builder.signer, order, &self.domain)?,
762 };
763
764 Ok(SignedOrderRequest {
765 salt: seed,
766 maker: self.builder.funder_checksum.clone(),
767 signer: signer_checksum,
768 token_id: self.token_id.clone(),
769 maker_amount: maker_amount.to_string(),
770 taker_amount: taker_amount.to_string(),
771 expiration: expiration.to_string(),
772 side: side.as_str().to_string(),
773 signature_type: self.builder.sig_type as u8,
774 timestamp: timestamp.to_string(),
775 metadata: self.metadata.clone(),
776 builder: self.builder_code.clone(),
777 signature,
778 })
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785 use alloy_signer_local::PrivateKeySigner;
786 use serde_json::Value;
787
788 fn test_builder() -> OrderBuilder {
789 let signer: PrivateKeySigner =
790 "0x1234567890123456789012345678901234567890123456789012345678901234"
791 .parse()
792 .expect("valid private key");
793 OrderBuilder::new(signer, None, None)
794 }
795
796 #[test]
797 fn test_decimal_to_token_units() {
798 let result = decimal_to_token_units(Decimal::from_str("1.5").unwrap()).unwrap();
799 assert_eq!(result, U256::from(1_500_000));
800 }
801
802 #[test]
803 fn test_generate_seed() {
804 let seed1 = generate_seed();
805 let seed2 = generate_seed();
806 assert_ne!(seed1, seed2);
807 }
808
809 #[test]
810 fn test_decimal_to_token_units_edge_cases() {
811 let result = decimal_to_token_units(Decimal::ZERO).unwrap();
813 assert_eq!(result, U256::ZERO);
814
815 let result = decimal_to_token_units(Decimal::from_str("0.000001").unwrap()).unwrap();
817 assert_eq!(result, U256::from(1));
818
819 let result = decimal_to_token_units(Decimal::from_str("1000.0").unwrap()).unwrap();
821 assert_eq!(result, U256::from(1_000_000_000));
822 }
823
824 #[test]
825 fn test_decimal_to_token_units_supports_amounts_above_u32() {
826 let result = decimal_to_token_units(Decimal::from_str("5000").unwrap()).unwrap();
827 assert_eq!(result, U256::from(5_000_000_000_u64));
828 }
829
830 #[test]
831 fn test_decimal_to_token_units_rejects_negative_amounts() {
832 let result = decimal_to_token_units(Decimal::from_str("-1").unwrap());
833 assert!(matches!(result, Err(PolyfillError::Validation { .. })));
834 }
835
836 #[test]
837 fn test_parse_round_config_accepts_exact_supported_tick_sizes() {
838 let config = parse_round_config(Decimal::from_str("0.1").unwrap()).unwrap();
839 assert_eq!(config.price, 1);
840
841 let config = parse_round_config(Decimal::from_str("0.0100").unwrap()).unwrap();
842 assert_eq!(config.price, 2);
843
844 let config = parse_round_config(Decimal::from_str("0.001").unwrap()).unwrap();
845 assert_eq!(config.price, 3);
846
847 let config = parse_round_config(Decimal::from_str("0.00010").unwrap()).unwrap();
848 assert_eq!(config.price, 4);
849 }
850
851 #[test]
852 fn test_parse_round_config_rejects_fractional_or_unsupported_tick_sizes() {
853 for tick_size in ["0.00005", "0.00009", "0.00011", "0.0002", "0", "-0.01"] {
854 let result = parse_round_config(Decimal::from_str(tick_size).unwrap());
855 assert!(matches!(result, Err(PolyfillError::Validation { .. })));
856 }
857 }
858
859 #[test]
860 fn test_get_contract_config() {
861 let config = get_contract_config(137, false).expect("polygon config");
863 assert_eq!(
864 config.exchange,
865 "0xE111180000d2663C0091e4f400237545B87B996B"
866 );
867 assert_eq!(
868 config.collateral,
869 "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
870 );
871 assert_eq!(
872 config.conditional_tokens,
873 "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
874 );
875
876 let config_neg = get_contract_config(137, true).expect("neg risk polygon config");
878 assert_eq!(
879 config_neg.exchange,
880 "0xe2222d279d744050d28e00520010520000310F59"
881 );
882 assert_eq!(
883 config_neg.collateral,
884 "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
885 );
886
887 let config_unsupported = get_contract_config(999, false);
889 assert!(config_unsupported.is_none());
890 }
891
892 #[test]
893 fn test_signature_type_from_u8() {
894 assert_eq!(sig_type_from_u8(0).unwrap(), SigType::Eoa);
895 assert_eq!(sig_type_from_u8(1).unwrap(), SigType::PolyProxy);
896 assert_eq!(sig_type_from_u8(2).unwrap(), SigType::PolyGnosisSafe);
897 assert_eq!(sig_type_from_u8(3).unwrap(), SigType::Poly1271);
898 assert!(sig_type_from_u8(4).is_err());
899 }
900
901 #[test]
902 fn test_derive_polygon_funder_addresses() {
903 let eoa = Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap();
904 assert_eq!(
905 derive_safe_wallet(eoa, 137).unwrap(),
906 Address::from_str("0xd93b25Cb943D14d0d34FBAf01fc93a0F8b5f6e47").unwrap()
907 );
908 assert_eq!(
909 derive_proxy_wallet(eoa, 137).unwrap(),
910 Address::from_str("0x365f0cA36ae1F641E02Fe3b7743673DA42A13a70").unwrap()
911 );
912 }
913
914 #[test]
915 fn test_normalize_optional_bytes32_defaults_to_zero() {
916 assert_eq!(
917 normalize_optional_bytes32("builder_code", None).unwrap(),
918 BYTES32_ZERO
919 );
920 }
921
922 #[test]
923 fn test_normalize_optional_bytes32_rejects_invalid_hex() {
924 let err = normalize_optional_bytes32("metadata", Some("deadbeef")).unwrap_err();
925 assert!(matches!(err, PolyfillError::Validation { .. }));
926 }
927
928 #[test]
929 fn test_create_order_serializes_v2_fields_without_legacy_fields() {
930 let builder = test_builder();
931 let order = builder
932 .create_order(
933 137,
934 &OrderArgs {
935 token_id: "123456".to_string(),
936 price: Decimal::from_str("0.45").unwrap(),
937 size: Decimal::from_str("12.34").unwrap(),
938 side: Side::BUY,
939 expiration: Some(1_900_000_000),
940 builder_code: Some(BYTES32_ZERO.to_string()),
941 metadata: None,
942 },
943 &CreateOrderOptions {
944 tick_size: Some(Decimal::from_str("0.01").unwrap()),
945 neg_risk: Some(false),
946 },
947 )
948 .unwrap();
949
950 let serialized = serde_json::to_value(&order).unwrap();
951 let object = serialized.as_object().unwrap();
952 assert!(object.contains_key("timestamp"));
953 assert!(object.contains_key("metadata"));
954 assert!(object.contains_key("builder"));
955 assert!(object.contains_key("expiration"));
956 assert!(!object.contains_key("taker"));
957 assert!(!object.contains_key("nonce"));
958 assert!(!object.contains_key("feeRateBps"));
959 assert_eq!(order.builder, BYTES32_ZERO);
960 assert_eq!(order.metadata, BYTES32_ZERO);
961 }
962
963 #[test]
964 fn test_poly1271_order_uses_deposit_wallet_as_signer_and_wrapped_signature() {
965 let signer: PrivateKeySigner =
966 "0x2222222222222222222222222222222222222222222222222222222222222222"
967 .parse()
968 .expect("valid private key");
969 let funder = Address::from_str("0x000000000000000000000000000000000000d077").unwrap();
970 let builder = OrderBuilder::new(signer, Some(SigType::Poly1271), Some(funder));
971
972 let order = builder
973 .create_order(
974 137,
975 &OrderArgs {
976 token_id: "123456".to_string(),
977 price: Decimal::from_str("0.50").unwrap(),
978 size: Decimal::from_str("10").unwrap(),
979 side: Side::BUY,
980 expiration: None,
981 builder_code: Some(BYTES32_ZERO.to_string()),
982 metadata: Some(BYTES32_ZERO.to_string()),
983 },
984 &CreateOrderOptions {
985 tick_size: Some(Decimal::from_str("0.01").unwrap()),
986 neg_risk: Some(false),
987 },
988 )
989 .unwrap();
990
991 let funder_checksum = funder.to_checksum(None);
992 assert_eq!(order.maker, funder_checksum);
993 assert_eq!(order.signer, funder_checksum);
994 assert_eq!(order.signature_type, SigType::Poly1271 as u8);
995 assert!(order.signature.starts_with("0x"));
996 assert!(
997 order.signature.len() > 600,
998 "POLY_1271 signature should be ERC-7739 wrapped"
999 );
1000 }
1001
1002 #[test]
1003 fn test_prepared_order_path_creates_equivalent_limit_order_fields() {
1004 let builder = test_builder();
1005 let args = OrderArgs {
1006 token_id: "123456".to_string(),
1007 price: Decimal::from_str("0.45").unwrap(),
1008 size: Decimal::from_str("12.34").unwrap(),
1009 side: Side::BUY,
1010 expiration: Some(1_900_000_000),
1011 builder_code: Some(BYTES32_ZERO.to_string()),
1012 metadata: None,
1013 };
1014 let options = CreateOrderOptions {
1015 tick_size: Some(Decimal::from_str("0.01").unwrap()),
1016 neg_risk: Some(false),
1017 };
1018
1019 let order = builder.create_order(137, &args, &options).unwrap();
1020 let prepared = builder
1021 .prepare_order_path(
1022 137,
1023 args.token_id.clone(),
1024 options.tick_size.unwrap(),
1025 options.neg_risk.unwrap(),
1026 args.builder_code.as_deref(),
1027 args.metadata.as_deref(),
1028 )
1029 .unwrap();
1030 let prepared_order = prepared
1031 .create_limit_order(args.side, args.price, args.size, args.expiration)
1032 .unwrap();
1033
1034 assert_eq!(prepared_order.maker, order.maker);
1035 assert_eq!(prepared_order.signer, order.signer);
1036 assert_eq!(prepared_order.token_id, order.token_id);
1037 assert_eq!(prepared_order.maker_amount, order.maker_amount);
1038 assert_eq!(prepared_order.taker_amount, order.taker_amount);
1039 assert_eq!(prepared_order.expiration, order.expiration);
1040 assert_eq!(prepared_order.side, order.side);
1041 assert_eq!(prepared_order.signature_type, order.signature_type);
1042 assert_eq!(prepared_order.metadata, order.metadata);
1043 assert_eq!(prepared_order.builder, order.builder);
1044 assert!(prepared_order.signature.starts_with("0x"));
1045 }
1046
1047 #[test]
1048 fn test_prepared_market_order_rejects_unsupported_order_type() {
1049 let builder = test_builder();
1050 let prepared = builder
1051 .prepare_order_path(
1052 137,
1053 "123456",
1054 Decimal::from_str("0.01").unwrap(),
1055 false,
1056 None,
1057 None,
1058 )
1059 .unwrap();
1060
1061 let err = prepared
1062 .create_market_order(
1063 Side::BUY,
1064 Decimal::from_str("10").unwrap(),
1065 Decimal::from_str("0.45").unwrap(),
1066 OrderType::GTC,
1067 )
1068 .unwrap_err();
1069
1070 assert!(matches!(err, PolyfillError::Validation { .. }));
1071 }
1072
1073 #[test]
1074 fn test_create_market_order_supports_fak() {
1075 let builder = test_builder();
1076 let order = builder
1077 .create_market_order(
1078 137,
1079 &MarketOrderArgs {
1080 token_id: "123456".to_string(),
1081 amount: Decimal::from_str("10.0").unwrap(),
1082 side: Side::BUY,
1083 order_type: OrderType::FAK,
1084 price_limit: None,
1085 user_usdc_balance: None,
1086 builder_code: None,
1087 metadata: None,
1088 },
1089 Decimal::from_str("0.25").unwrap(),
1090 &CreateOrderOptions {
1091 tick_size: Some(Decimal::from_str("0.01").unwrap()),
1092 neg_risk: Some(false),
1093 },
1094 )
1095 .unwrap();
1096
1097 assert_eq!(order.side, "BUY");
1098 assert!(!order.timestamp.is_empty());
1099 }
1100
1101 #[test]
1102 fn test_adjust_buy_amount_for_fees_uses_builder_rate_decimal() {
1103 let adjusted = adjust_buy_amount_for_fees(
1104 Decimal::from_str("100").unwrap(),
1105 Decimal::from_str("0.5").unwrap(),
1106 Decimal::from_str("100").unwrap(),
1107 Decimal::ZERO,
1108 0,
1109 Decimal::from_str("0.01").unwrap(),
1110 )
1111 .unwrap();
1112
1113 assert_eq!(adjusted, Decimal::from_str("99.009900").unwrap());
1114 }
1115
1116 #[test]
1117 fn test_adjust_buy_amount_for_fees_rejects_zero_after_truncation() {
1118 let err = adjust_buy_amount_for_fees(
1119 Decimal::from_str("1").unwrap(),
1120 Decimal::from_str("0.5").unwrap(),
1121 Decimal::from_str("0.0000009").unwrap(),
1122 Decimal::ZERO,
1123 0,
1124 Decimal::ZERO,
1125 )
1126 .unwrap_err();
1127
1128 assert!(matches!(err, PolyfillError::Validation { .. }));
1129 }
1130
1131 #[test]
1132 fn test_market_order_amounts_differ_for_buy_and_sell() {
1133 let builder = test_builder();
1134 let round_config = parse_round_config(Decimal::from_str("0.01").unwrap()).unwrap();
1135
1136 let (buy_maker, buy_taker) = builder
1137 .get_market_order_amounts(
1138 Side::BUY,
1139 Decimal::from_str("10").unwrap(),
1140 Decimal::from_str("0.25").unwrap(),
1141 round_config,
1142 )
1143 .unwrap();
1144 let (sell_maker, sell_taker) = builder
1145 .get_market_order_amounts(
1146 Side::SELL,
1147 Decimal::from_str("10").unwrap(),
1148 Decimal::from_str("0.25").unwrap(),
1149 round_config,
1150 )
1151 .unwrap();
1152
1153 assert_eq!(buy_maker, U256::from(10_000_000));
1154 assert_eq!(buy_taker, U256::from(40_000_000));
1155 assert_eq!(sell_maker, U256::from(10_000_000));
1156 assert_eq!(sell_taker, U256::from(2_500_000));
1157 }
1158
1159 #[test]
1160 fn test_calculate_market_price_returns_last_level_for_fak() {
1161 let builder = test_builder();
1162 let levels = vec![
1163 crate::types::BookLevel {
1164 price: Decimal::from_str("0.40").unwrap(),
1165 size: Decimal::from_str("2.0").unwrap(),
1166 },
1167 crate::types::BookLevel {
1168 price: Decimal::from_str("0.45").unwrap(),
1169 size: Decimal::from_str("1.0").unwrap(),
1170 },
1171 ];
1172
1173 let price = builder
1174 .calculate_market_price(
1175 &levels,
1176 Decimal::from_str("10.0").unwrap(),
1177 Side::SELL,
1178 OrderType::FAK,
1179 )
1180 .unwrap();
1181 assert_eq!(price, Decimal::from_str("0.45").unwrap());
1182 }
1183
1184 #[test]
1185 fn test_signed_order_json_uses_camel_case_wire_shape() {
1186 let builder = test_builder();
1187 let order = builder
1188 .create_order(
1189 137,
1190 &OrderArgs {
1191 token_id: "123456".to_string(),
1192 price: Decimal::from_str("0.55").unwrap(),
1193 size: Decimal::from_str("5.0").unwrap(),
1194 side: Side::SELL,
1195 expiration: Some(1_900_000_000),
1196 builder_code: None,
1197 metadata: Some(BYTES32_ZERO.to_string()),
1198 },
1199 &CreateOrderOptions {
1200 tick_size: Some(Decimal::from_str("0.01").unwrap()),
1201 neg_risk: Some(true),
1202 },
1203 )
1204 .unwrap();
1205
1206 let json = serde_json::to_value(order).unwrap();
1207 assert!(matches!(json.get("tokenId"), Some(Value::String(_))));
1208 assert!(matches!(json.get("makerAmount"), Some(Value::String(_))));
1209 assert!(matches!(json.get("takerAmount"), Some(Value::String(_))));
1210 assert!(matches!(json.get("signatureType"), Some(Value::Number(_))));
1211 }
1212
1213 #[test]
1214 fn test_seed_generation_uniqueness() {
1215 let mut seeds = std::collections::HashSet::new();
1216
1217 for _ in 0..1000 {
1219 let seed = generate_seed();
1220 assert!(seeds.insert(seed), "Duplicate seed generated");
1221 }
1222 }
1223
1224 #[test]
1225 fn test_seed_generation_range() {
1226 for _ in 0..100 {
1227 let seed = generate_seed();
1228 assert!(seed > 0);
1230 assert!(seed < u64::MAX);
1231 }
1232 }
1233}