1#![deny(unsafe_code)]
17#![warn(missing_docs)]
18
19pub mod flags;
20pub mod trade;
21mod unsigned_order;
22
23pub use unsigned_order::UnsignedOrder;
24
25use std::fmt;
26
27use cow_errors::CowError;
28use serde::{Deserialize, Serialize};
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct CowHook {
62 pub target: String,
64 pub call_data: String,
66 pub gas_limit: String,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub dapp_id: Option<String>,
71}
72
73impl CowHook {
74 #[must_use]
76 pub fn new(
77 target: impl Into<String>,
78 call_data: impl Into<String>,
79 gas_limit: impl Into<String>,
80 ) -> Self {
81 Self {
82 target: target.into(),
83 call_data: call_data.into(),
84 gas_limit: gas_limit.into(),
85 dapp_id: None,
86 }
87 }
88
89 #[must_use]
91 pub fn with_dapp_id(mut self, dapp_id: impl Into<String>) -> Self {
92 self.dapp_id = Some(dapp_id.into());
93 self
94 }
95
96 #[must_use]
98 pub const fn has_dapp_id(&self) -> bool {
99 self.dapp_id.is_some()
100 }
101}
102
103impl fmt::Display for CowHook {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "hook(target={}, gas={})", self.target, self.gas_limit)
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub struct OnchainOrderData {
114 pub sender: alloy_primitives::Address,
117 pub placement_error: Option<String>,
119}
120
121impl OnchainOrderData {
122 #[must_use]
124 pub const fn new(sender: alloy_primitives::Address) -> Self {
125 Self { sender, placement_error: None }
126 }
127
128 #[must_use]
130 pub const fn has_placement_error(&self) -> bool {
131 self.placement_error.is_some()
132 }
133}
134
135impl fmt::Display for OnchainOrderData {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "onchain(sender={:#x})", self.sender)
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "lowercase")]
157pub enum OrderKind {
158 Sell,
160 Buy,
162}
163
164impl OrderKind {
165 #[must_use]
171 pub const fn as_str(self) -> &'static str {
172 match self {
173 Self::Sell => "sell",
174 Self::Buy => "buy",
175 }
176 }
177
178 #[must_use]
187 pub const fn is_sell(self) -> bool {
188 matches!(self, Self::Sell)
189 }
190
191 #[must_use]
200 pub const fn is_buy(self) -> bool {
201 matches!(self, Self::Buy)
202 }
203}
204
205impl fmt::Display for OrderKind {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 f.write_str(self.as_str())
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
227#[serde(rename_all = "lowercase")]
228pub enum TokenBalance {
229 #[default]
231 Erc20,
232 External,
234 Internal,
236}
237
238impl TokenBalance {
239 #[must_use]
245 pub const fn as_str(self) -> &'static str {
246 match self {
247 Self::Erc20 => "erc20",
248 Self::External => "external",
249 Self::Internal => "internal",
250 }
251 }
252
253 #[must_use]
263 pub fn eip712_hash(self) -> alloy_primitives::B256 {
264 alloy_primitives::keccak256(self.as_str().as_bytes())
265 }
266
267 #[must_use]
271 pub const fn is_erc20(self) -> bool {
272 matches!(self, Self::Erc20)
273 }
274
275 #[must_use]
278 pub const fn is_external(self) -> bool {
279 matches!(self, Self::External)
280 }
281
282 #[must_use]
285 pub const fn is_internal(self) -> bool {
286 matches!(self, Self::Internal)
287 }
288}
289
290impl fmt::Display for TokenBalance {
291 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292 f.write_str(self.as_str())
293 }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(rename_all = "lowercase")]
316pub enum SigningScheme {
317 Eip712,
319 EthSign,
321 Eip1271,
323 PreSign,
325}
326
327impl SigningScheme {
328 #[must_use]
334 pub const fn as_str(self) -> &'static str {
335 match self {
336 Self::Eip712 => "eip712",
337 Self::EthSign => "ethsign",
338 Self::Eip1271 => "eip1271",
339 Self::PreSign => "presign",
340 }
341 }
342
343 #[must_use]
347 pub const fn is_eip712(self) -> bool {
348 matches!(self, Self::Eip712)
349 }
350
351 #[must_use]
355 pub const fn is_eth_sign(self) -> bool {
356 matches!(self, Self::EthSign)
357 }
358
359 #[must_use]
365 pub const fn is_eip1271(self) -> bool {
366 matches!(self, Self::Eip1271)
367 }
368
369 #[must_use]
374 pub const fn is_presign(self) -> bool {
375 matches!(self, Self::PreSign)
376 }
377}
378
379impl fmt::Display for SigningScheme {
380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381 f.write_str(self.as_str())
382 }
383}
384
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
401#[serde(rename_all = "lowercase")]
402pub enum EcdsaSigningScheme {
403 #[default]
405 Eip712,
406 EthSign,
408}
409
410impl EcdsaSigningScheme {
411 #[must_use]
417 pub const fn into_signing_scheme(self) -> SigningScheme {
418 match self {
419 Self::Eip712 => SigningScheme::Eip712,
420 Self::EthSign => SigningScheme::EthSign,
421 }
422 }
423
424 #[must_use]
430 pub const fn as_str(self) -> &'static str {
431 match self {
432 Self::Eip712 => "eip712",
433 Self::EthSign => "ethsign",
434 }
435 }
436
437 #[must_use]
439 pub const fn is_eip712(self) -> bool {
440 matches!(self, Self::Eip712)
441 }
442
443 #[must_use]
446 pub const fn is_eth_sign(self) -> bool {
447 matches!(self, Self::EthSign)
448 }
449}
450
451impl fmt::Display for EcdsaSigningScheme {
452 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453 f.write_str(self.as_str())
454 }
455}
456
457impl From<EcdsaSigningScheme> for SigningScheme {
458 fn from(s: EcdsaSigningScheme) -> Self {
463 s.into_signing_scheme()
464 }
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
482#[serde(rename_all = "lowercase")]
483pub enum PriceQuality {
484 Fast,
486 #[default]
488 Optimal,
489 Verified,
491}
492
493impl PriceQuality {
494 #[must_use]
500 pub const fn as_str(self) -> &'static str {
501 match self {
502 Self::Fast => "fast",
503 Self::Optimal => "optimal",
504 Self::Verified => "verified",
505 }
506 }
507
508 #[must_use]
511 pub const fn is_fast(self) -> bool {
512 matches!(self, Self::Fast)
513 }
514
515 #[must_use]
518 pub const fn is_optimal(self) -> bool {
519 matches!(self, Self::Optimal)
520 }
521
522 #[must_use]
525 pub const fn is_verified(self) -> bool {
526 matches!(self, Self::Verified)
527 }
528}
529
530impl fmt::Display for PriceQuality {
531 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
532 f.write_str(self.as_str())
533 }
534}
535
536impl TryFrom<&str> for OrderKind {
537 type Error = CowError;
538
539 fn try_from(s: &str) -> Result<Self, Self::Error> {
543 match s {
544 "sell" => Ok(Self::Sell),
545 "buy" => Ok(Self::Buy),
546 other => Err(CowError::Parse {
547 field: "OrderKind",
548 reason: format!("unknown value: {other}"),
549 }),
550 }
551 }
552}
553
554impl TryFrom<&str> for TokenBalance {
555 type Error = CowError;
556
557 fn try_from(s: &str) -> Result<Self, Self::Error> {
561 match s {
562 "erc20" => Ok(Self::Erc20),
563 "external" => Ok(Self::External),
564 "internal" => Ok(Self::Internal),
565 other => Err(CowError::Parse {
566 field: "TokenBalance",
567 reason: format!("unknown value: {other}"),
568 }),
569 }
570 }
571}
572
573impl TryFrom<&str> for SigningScheme {
574 type Error = CowError;
575
576 fn try_from(s: &str) -> Result<Self, Self::Error> {
580 match s {
581 "eip712" => Ok(Self::Eip712),
582 "ethsign" => Ok(Self::EthSign),
583 "eip1271" => Ok(Self::Eip1271),
584 "presign" => Ok(Self::PreSign),
585 other => Err(CowError::Parse {
586 field: "SigningScheme",
587 reason: format!("unknown value: {other}"),
588 }),
589 }
590 }
591}
592
593impl TryFrom<&str> for EcdsaSigningScheme {
594 type Error = CowError;
595
596 fn try_from(s: &str) -> Result<Self, Self::Error> {
600 match s {
601 "eip712" => Ok(Self::Eip712),
602 "ethsign" => Ok(Self::EthSign),
603 other => Err(CowError::Parse {
604 field: "EcdsaSigningScheme",
605 reason: format!("unknown value: {other}"),
606 }),
607 }
608 }
609}
610
611impl TryFrom<&str> for PriceQuality {
612 type Error = CowError;
613
614 fn try_from(s: &str) -> Result<Self, Self::Error> {
618 match s {
619 "fast" => Ok(Self::Fast),
620 "optimal" => Ok(Self::Optimal),
621 "verified" => Ok(Self::Verified),
622 other => Err(CowError::Parse {
623 field: "PriceQuality",
624 reason: format!("unknown value: {other}"),
625 }),
626 }
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633
634 #[test]
637 fn order_kind_as_str() {
638 assert_eq!(OrderKind::Sell.as_str(), "sell");
639 assert_eq!(OrderKind::Buy.as_str(), "buy");
640 }
641
642 #[test]
643 fn order_kind_predicates() {
644 assert!(OrderKind::Sell.is_sell());
645 assert!(!OrderKind::Sell.is_buy());
646 assert!(OrderKind::Buy.is_buy());
647 assert!(!OrderKind::Buy.is_sell());
648 }
649
650 #[test]
651 fn order_kind_display() {
652 assert_eq!(format!("{}", OrderKind::Sell), "sell");
653 assert_eq!(format!("{}", OrderKind::Buy), "buy");
654 }
655
656 #[test]
657 fn order_kind_roundtrip() {
658 for kind in [OrderKind::Sell, OrderKind::Buy] {
659 let parsed = OrderKind::try_from(kind.as_str()).unwrap();
660 assert_eq!(parsed, kind);
661 }
662 }
663
664 #[test]
665 fn order_kind_invalid() {
666 assert!(OrderKind::try_from("invalid").is_err());
667 assert!(OrderKind::try_from("").is_err());
668 assert!(OrderKind::try_from("SELL").is_err());
669 }
670
671 #[test]
672 fn order_kind_serde_roundtrip() {
673 let json = serde_json::to_string(&OrderKind::Sell).unwrap();
674 assert_eq!(json, "\"sell\"");
675 let back: OrderKind = serde_json::from_str(&json).unwrap();
676 assert_eq!(back, OrderKind::Sell);
677 }
678
679 #[test]
682 fn token_balance_as_str() {
683 assert_eq!(TokenBalance::Erc20.as_str(), "erc20");
684 assert_eq!(TokenBalance::External.as_str(), "external");
685 assert_eq!(TokenBalance::Internal.as_str(), "internal");
686 }
687
688 #[test]
689 fn token_balance_predicates() {
690 assert!(TokenBalance::Erc20.is_erc20());
691 assert!(!TokenBalance::Erc20.is_external());
692 assert!(!TokenBalance::Erc20.is_internal());
693 assert!(TokenBalance::External.is_external());
694 assert!(TokenBalance::Internal.is_internal());
695 }
696
697 #[test]
698 fn token_balance_default() {
699 assert_eq!(TokenBalance::default(), TokenBalance::Erc20);
700 }
701
702 #[test]
703 fn token_balance_roundtrip() {
704 for bal in [TokenBalance::Erc20, TokenBalance::External, TokenBalance::Internal] {
705 let parsed = TokenBalance::try_from(bal.as_str()).unwrap();
706 assert_eq!(parsed, bal);
707 }
708 }
709
710 #[test]
711 fn token_balance_invalid() {
712 assert!(TokenBalance::try_from("ERC20").is_err());
713 assert!(TokenBalance::try_from("").is_err());
714 }
715
716 #[test]
717 fn token_balance_eip712_hash_deterministic() {
718 let h1 = TokenBalance::Erc20.eip712_hash();
719 let h2 = TokenBalance::Erc20.eip712_hash();
720 assert_eq!(h1, h2);
721 assert_ne!(TokenBalance::Erc20.eip712_hash(), TokenBalance::External.eip712_hash());
723 assert_ne!(TokenBalance::External.eip712_hash(), TokenBalance::Internal.eip712_hash());
724 }
725
726 #[test]
727 fn token_balance_display() {
728 assert_eq!(format!("{}", TokenBalance::External), "external");
729 }
730
731 #[test]
734 fn signing_scheme_as_str() {
735 assert_eq!(SigningScheme::Eip712.as_str(), "eip712");
736 assert_eq!(SigningScheme::EthSign.as_str(), "ethsign");
737 assert_eq!(SigningScheme::Eip1271.as_str(), "eip1271");
738 assert_eq!(SigningScheme::PreSign.as_str(), "presign");
739 }
740
741 #[test]
742 fn signing_scheme_predicates() {
743 assert!(SigningScheme::Eip712.is_eip712());
744 assert!(SigningScheme::EthSign.is_eth_sign());
745 assert!(SigningScheme::Eip1271.is_eip1271());
746 assert!(SigningScheme::PreSign.is_presign());
747 assert!(!SigningScheme::Eip712.is_presign());
748 }
749
750 #[test]
751 fn signing_scheme_roundtrip() {
752 for s in [
753 SigningScheme::Eip712,
754 SigningScheme::EthSign,
755 SigningScheme::Eip1271,
756 SigningScheme::PreSign,
757 ] {
758 assert_eq!(SigningScheme::try_from(s.as_str()).unwrap(), s);
759 }
760 }
761
762 #[test]
763 fn signing_scheme_invalid() {
764 assert!(SigningScheme::try_from("eip-712").is_err());
765 assert!(SigningScheme::try_from("").is_err());
766 }
767
768 #[test]
769 fn signing_scheme_display() {
770 assert_eq!(format!("{}", SigningScheme::PreSign), "presign");
771 }
772
773 #[test]
776 fn ecdsa_scheme_default() {
777 assert_eq!(EcdsaSigningScheme::default(), EcdsaSigningScheme::Eip712);
778 }
779
780 #[test]
781 fn ecdsa_scheme_into_signing_scheme() {
782 assert_eq!(EcdsaSigningScheme::Eip712.into_signing_scheme(), SigningScheme::Eip712);
783 assert_eq!(EcdsaSigningScheme::EthSign.into_signing_scheme(), SigningScheme::EthSign);
784 }
785
786 #[test]
787 fn ecdsa_scheme_from_conversion() {
788 let full: SigningScheme = EcdsaSigningScheme::EthSign.into();
789 assert_eq!(full, SigningScheme::EthSign);
790 }
791
792 #[test]
793 fn ecdsa_scheme_predicates() {
794 assert!(EcdsaSigningScheme::Eip712.is_eip712());
795 assert!(!EcdsaSigningScheme::Eip712.is_eth_sign());
796 assert!(EcdsaSigningScheme::EthSign.is_eth_sign());
797 }
798
799 #[test]
800 fn ecdsa_scheme_roundtrip() {
801 for s in [EcdsaSigningScheme::Eip712, EcdsaSigningScheme::EthSign] {
802 assert_eq!(EcdsaSigningScheme::try_from(s.as_str()).unwrap(), s);
803 }
804 }
805
806 #[test]
807 fn ecdsa_scheme_invalid() {
808 assert!(EcdsaSigningScheme::try_from("eip1271").is_err());
809 }
810
811 #[test]
812 fn ecdsa_scheme_display() {
813 assert_eq!(format!("{}", EcdsaSigningScheme::EthSign), "ethsign");
814 }
815
816 #[test]
819 fn price_quality_default() {
820 assert_eq!(PriceQuality::default(), PriceQuality::Optimal);
821 }
822
823 #[test]
824 fn price_quality_as_str() {
825 assert_eq!(PriceQuality::Fast.as_str(), "fast");
826 assert_eq!(PriceQuality::Optimal.as_str(), "optimal");
827 assert_eq!(PriceQuality::Verified.as_str(), "verified");
828 }
829
830 #[test]
831 fn price_quality_predicates() {
832 assert!(PriceQuality::Fast.is_fast());
833 assert!(PriceQuality::Optimal.is_optimal());
834 assert!(PriceQuality::Verified.is_verified());
835 assert!(!PriceQuality::Fast.is_optimal());
836 }
837
838 #[test]
839 fn price_quality_roundtrip() {
840 for q in [PriceQuality::Fast, PriceQuality::Optimal, PriceQuality::Verified] {
841 assert_eq!(PriceQuality::try_from(q.as_str()).unwrap(), q);
842 }
843 }
844
845 #[test]
846 fn price_quality_invalid() {
847 assert!(PriceQuality::try_from("slow").is_err());
848 }
849
850 #[test]
851 fn price_quality_display() {
852 assert_eq!(format!("{}", PriceQuality::Verified), "verified");
853 }
854
855 #[test]
858 fn cow_hook_with_dapp_id_sets_field() {
859 let hook = CowHook::new("0xtarget", "0xdata", "21000").with_dapp_id("my-dapp");
860 assert!(hook.has_dapp_id());
861 assert_eq!(hook.dapp_id.as_deref(), Some("my-dapp"));
862 }
863
864 #[test]
865 fn cow_hook_has_dapp_id_false_by_default() {
866 let hook = CowHook::new("0xtarget", "0xdata", "21000");
867 assert!(!hook.has_dapp_id());
868 }
869
870 #[test]
871 fn cow_hook_display_renders_target_and_gas() {
872 let hook = CowHook::new("0xabc", "0xff", "50000");
873 assert_eq!(format!("{hook}"), "hook(target=0xabc, gas=50000)");
874 }
875
876 #[test]
879 fn onchain_order_data_has_placement_error_predicates() {
880 let mut data = OnchainOrderData::new(alloy_primitives::Address::ZERO);
881 assert!(!data.has_placement_error());
882 data.placement_error = Some("rejected".into());
883 assert!(data.has_placement_error());
884 }
885
886 #[test]
887 fn onchain_order_data_display_includes_sender() {
888 let data = OnchainOrderData::new(alloy_primitives::Address::ZERO);
889 let rendered = format!("{data}");
890 assert!(rendered.starts_with("onchain(sender=0x"));
891 }
892}