Skip to main content

cow_types/
lib.rs

1//! `cow-types` — Layer 1 protocol enums and shared types for the `CoW` Protocol SDK.
2//!
3//! This crate defines the protocol-level enums used across the workspace:
4//!
5//! | Enum | Purpose |
6//! |---|---|
7//! | [`OrderKind`] | `Sell` / `Buy` direction |
8//! | [`TokenBalance`] | `Erc20` / `External` / `Internal` balance source |
9//! | [`SigningScheme`] | `Eip712` / `EthSign` / `Eip1271` / `PreSign` |
10//! | [`EcdsaSigningScheme`] | ECDSA-only subset (`Eip712` / `EthSign`) |
11//! | [`PriceQuality`] | `Fast` / `Optimal` / `Verified` quote hint |
12//!
13//! Numeric constants (`ZERO`, `ONE`, `MAX_UINT256`, ...) live in
14//! [`cow-primitives`](https://docs.rs/cow-primitives).
15
16#![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// ── Shared protocol types pushed down from domain crates ────────────────────
31//
32// Types in this section used to live in higher-layer crates (app-data, order-
33// book) but were referenced from multiple L2 siblings and so had to be pushed
34// down to L1 to avoid cross-sibling dependencies.
35
36/// A single `CoW` Protocol pre- or post-settlement interaction hook.
37///
38/// Hooks are arbitrary contract calls that the `CoW` settlement contract
39/// executes before (`pre`) or after (`post`) the trade. Common use cases
40/// include token approvals, NFT transfers, and flash-loan repayments.
41///
42/// # Fields
43///
44/// * `target` — the contract address to call (`0x`-prefixed, 20 bytes).
45/// * `call_data` — ABI-encoded function selector + arguments (`0x`-prefixed).
46/// * `gas_limit` — maximum gas the hook may consume (decimal string).
47/// * `dapp_id` — optional identifier for the dApp that registered the hook.
48///
49/// # Example
50///
51/// ```
52/// use cow_types::CowHook;
53///
54/// let hook = CowHook::new("0x1234567890abcdef1234567890abcdef12345678", "0xabcdef00", "100000")
55///     .with_dapp_id("my-dapp");
56///
57/// assert!(hook.has_dapp_id());
58/// ```
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct CowHook {
62    /// Target contract address (checksummed hex with `0x` prefix).
63    pub target: String,
64    /// ABI-encoded call data (hex with `0x` prefix).
65    pub call_data: String,
66    /// Maximum gas this hook may consume (decimal string, e.g. `"100000"`).
67    pub gas_limit: String,
68    /// Optional dApp identifier for the hook's origin.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub dapp_id: Option<String>,
71}
72
73impl CowHook {
74    /// Construct a new [`CowHook`] without a dApp identifier.
75    #[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    /// Attach a dApp identifier to this hook.
90    #[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    /// Returns `true` if a dApp identifier is set on this hook.
97    #[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/// On-chain placement metadata for orders submitted directly on-chain
110/// (as opposed to the off-chain API).
111#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub struct OnchainOrderData {
114    /// The address that created the on-chain order (may differ from `owner` for
115    /// `EthFlow` orders where the contract is the technical owner).
116    pub sender: alloy_primitives::Address,
117    /// Non-`None` when the orderbook rejected the order due to a placement error.
118    pub placement_error: Option<String>,
119}
120
121impl OnchainOrderData {
122    /// Construct an [`OnchainOrderData`] record.
123    #[must_use]
124    pub const fn new(sender: alloy_primitives::Address) -> Self {
125        Self { sender, placement_error: None }
126    }
127
128    /// Returns `true` if a placement error was reported for this on-chain order.
129    #[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/// Whether to sell an exact input amount or buy an exact output amount.
142///
143/// Used in every order and quote request to specify the trade direction.
144/// Serialises to `"sell"` or `"buy"` in JSON.
145///
146/// # Example
147///
148/// ```
149/// use cow_types::OrderKind;
150///
151/// let kind = OrderKind::Sell;
152/// assert_eq!(kind.as_str(), "sell");
153/// assert!(kind.is_sell());
154/// ```
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "lowercase")]
157pub enum OrderKind {
158    /// Sell an exact input amount; receive at least `buyAmount`.
159    Sell,
160    /// Buy an exact output amount; spend at most `sellAmount`.
161    Buy,
162}
163
164impl OrderKind {
165    /// Returns the lowercase string used by the `CoW` Protocol API.
166    ///
167    /// # Returns
168    ///
169    /// `"sell"` for [`Sell`](Self::Sell), `"buy"` for [`Buy`](Self::Buy).
170    #[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    /// Returns `true` if this is a sell order.
179    ///
180    /// ```
181    /// use cow_types::OrderKind;
182    ///
183    /// assert!(OrderKind::Sell.is_sell());
184    /// assert!(!OrderKind::Buy.is_sell());
185    /// ```
186    #[must_use]
187    pub const fn is_sell(self) -> bool {
188        matches!(self, Self::Sell)
189    }
190
191    /// Returns `true` if this is a buy order.
192    ///
193    /// ```
194    /// use cow_types::OrderKind;
195    ///
196    /// assert!(OrderKind::Buy.is_buy());
197    /// assert!(!OrderKind::Sell.is_buy());
198    /// ```
199    #[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/// The ERC-20 balance source/destination for `sellToken` and `buyToken`.
212///
213/// Controls whether the `CoW` settlement contract transfers tokens via
214/// standard ERC-20 `transferFrom` or via the Balancer Vault's internal
215/// balance system.
216///
217/// # Example
218///
219/// ```
220/// use cow_types::TokenBalance;
221///
222/// let balance = TokenBalance::Erc20;
223/// assert_eq!(balance.as_str(), "erc20");
224/// assert!(balance.is_erc20());
225/// ```
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
227#[serde(rename_all = "lowercase")]
228pub enum TokenBalance {
229    /// Standard ERC-20 transfer (default).
230    #[default]
231    Erc20,
232    /// Balancer Vault internal balance (sell side only).
233    External,
234    /// Balancer Vault internal balance (buy side only).
235    Internal,
236}
237
238impl TokenBalance {
239    /// Returns the lowercase string used by the `CoW` Protocol API.
240    ///
241    /// # Returns
242    ///
243    /// `"erc20"`, `"external"`, or `"internal"`.
244    #[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    /// Compute `keccak256(self.as_str())` for EIP-712 struct hashing.
254    ///
255    /// The EIP-712 order struct encodes the token balance kind as
256    /// `keccak256(bytes("erc20"))` (or `"external"` / `"internal"`).
257    ///
258    /// # Returns
259    ///
260    /// A 32-byte [`B256`](alloy_primitives::B256) hash of the variant
261    /// string.
262    #[must_use]
263    pub fn eip712_hash(self) -> alloy_primitives::B256 {
264        alloy_primitives::keccak256(self.as_str().as_bytes())
265    }
266
267    /// Returns `true` if the standard ERC-20 transfer mode is used.
268    ///
269    /// This is the default for most orders.
270    #[must_use]
271    pub const fn is_erc20(self) -> bool {
272        matches!(self, Self::Erc20)
273    }
274
275    /// Returns `true` if the Balancer Vault external balance is used
276    /// (sell side only).
277    #[must_use]
278    pub const fn is_external(self) -> bool {
279        matches!(self, Self::External)
280    }
281
282    /// Returns `true` if the Balancer Vault internal balance is used
283    /// (buy side only).
284    #[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/// Signing scheme for a `CoW` Protocol order.
297///
298/// Determines how the order signature is verified:
299///
300/// - **`Eip712`** — standard EIP-712 typed-data signature (most wallets).
301/// - **`EthSign`** — legacy `eth_sign` with EIP-191 prefix.
302/// - **`Eip1271`** — smart-contract signature via `isValidSignature`.
303/// - **`PreSign`** — on-chain pre-approval via `setPreSignature`.
304///
305/// # Example
306///
307/// ```
308/// use cow_types::SigningScheme;
309///
310/// let scheme = SigningScheme::Eip712;
311/// assert_eq!(scheme.as_str(), "eip712");
312/// assert!(scheme.is_eip712());
313/// ```
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(rename_all = "lowercase")]
316pub enum SigningScheme {
317    /// Standard EIP-712 typed-data signature.
318    Eip712,
319    /// Legacy `eth_sign` (EIP-191) signature.
320    EthSign,
321    /// EIP-1271 smart-contract signature.
322    Eip1271,
323    /// On-chain pre-signature via `setPreSignature`.
324    PreSign,
325}
326
327impl SigningScheme {
328    /// Returns the lowercase string used by the `CoW` Protocol API.
329    ///
330    /// # Returns
331    ///
332    /// `"eip712"`, `"ethsign"`, `"eip1271"`, or `"presign"`.
333    #[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    /// Returns `true` if the EIP-712 typed-data signing scheme is used.
344    ///
345    /// This is the most common scheme for EOA wallets.
346    #[must_use]
347    pub const fn is_eip712(self) -> bool {
348        matches!(self, Self::Eip712)
349    }
350
351    /// Returns `true` if the legacy EIP-191 (`eth_sign`) scheme is used.
352    ///
353    /// Some older wallets or hardware signers only support this method.
354    #[must_use]
355    pub const fn is_eth_sign(self) -> bool {
356        matches!(self, Self::EthSign)
357    }
358
359    /// Returns `true` if the EIP-1271 smart-contract signature scheme is
360    /// used.
361    ///
362    /// The signature is verified on-chain by calling `isValidSignature`
363    /// on the signing contract.
364    #[must_use]
365    pub const fn is_eip1271(self) -> bool {
366        matches!(self, Self::Eip1271)
367    }
368
369    /// Returns `true` if the on-chain pre-sign scheme is used.
370    ///
371    /// The order owner calls `setPreSignature` on the settlement contract
372    /// before the order can be filled.
373    #[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/// ECDSA-only signing schemes (EIP-712 or EIP-191).
386///
387/// A subset of [`SigningScheme`] limited to the two schemes that produce
388/// standard ECDSA signatures. Use [`into_signing_scheme`](Self::into_signing_scheme)
389/// to widen to the full enum when needed.
390///
391/// # Example
392///
393/// ```
394/// use cow_types::{EcdsaSigningScheme, SigningScheme};
395///
396/// let ecdsa = EcdsaSigningScheme::Eip712;
397/// let full: SigningScheme = ecdsa.into();
398/// assert_eq!(full, SigningScheme::Eip712);
399/// ```
400#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
401#[serde(rename_all = "lowercase")]
402pub enum EcdsaSigningScheme {
403    /// Standard EIP-712 typed-data signature (default).
404    #[default]
405    Eip712,
406    /// Legacy `eth_sign` (EIP-191) signature.
407    EthSign,
408}
409
410impl EcdsaSigningScheme {
411    /// Widen to the full [`SigningScheme`] enum.
412    ///
413    /// # Returns
414    ///
415    /// [`SigningScheme::Eip712`] or [`SigningScheme::EthSign`].
416    #[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    /// Returns the lowercase string used by the `CoW` Protocol API.
425    ///
426    /// # Returns
427    ///
428    /// `"eip712"` or `"ethsign"`.
429    #[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    /// Returns `true` if the EIP-712 typed-data scheme is selected.
438    #[must_use]
439    pub const fn is_eip712(self) -> bool {
440        matches!(self, Self::Eip712)
441    }
442
443    /// Returns `true` if the legacy EIP-191 (`eth_sign`) scheme is
444    /// selected.
445    #[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    /// Widen an ECDSA-only scheme to the full [`SigningScheme`] enum.
459    ///
460    /// This is the Rust equivalent of `SIGN_SCHEME_MAP` from the
461    /// `TypeScript` SDK.
462    fn from(s: EcdsaSigningScheme) -> Self {
463        s.into_signing_scheme()
464    }
465}
466
467/// Quote price-quality hint passed to the orderbook.
468///
469/// Controls the trade-off between response speed and price accuracy when
470/// requesting a quote via `POST /api/v1/quote`.
471///
472/// # Example
473///
474/// ```
475/// use cow_types::PriceQuality;
476///
477/// let quality = PriceQuality::Optimal;
478/// assert_eq!(quality.as_str(), "optimal");
479/// assert!(quality.is_optimal());
480/// ```
481#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
482#[serde(rename_all = "lowercase")]
483pub enum PriceQuality {
484    /// Fast estimate — may be slightly stale.
485    Fast,
486    /// Optimal price — runs the full solver pipeline.
487    #[default]
488    Optimal,
489    /// Like optimal but includes on-chain simulation to verify executability.
490    Verified,
491}
492
493impl PriceQuality {
494    /// Returns the lowercase string used by the `CoW` Protocol API.
495    ///
496    /// # Returns
497    ///
498    /// `"fast"`, `"optimal"`, or `"verified"`.
499    #[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    /// Returns `true` if the fast (potentially stale) price quality is
509    /// selected.
510    #[must_use]
511    pub const fn is_fast(self) -> bool {
512        matches!(self, Self::Fast)
513    }
514
515    /// Returns `true` if the optimal (full solver pipeline) price quality
516    /// is selected. This is the default.
517    #[must_use]
518    pub const fn is_optimal(self) -> bool {
519        matches!(self, Self::Optimal)
520    }
521
522    /// Returns `true` if the verified (on-chain simulation) price quality
523    /// is selected.
524    #[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    /// Parse a `CoW` Protocol order kind from its API string.
540    ///
541    /// Accepts `"sell"` or `"buy"`.
542    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    /// Parse a `CoW` Protocol token balance kind from its API string.
558    ///
559    /// Accepts `"erc20"`, `"external"`, or `"internal"`.
560    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    /// Parse a `CoW` Protocol signing scheme from its API string.
577    ///
578    /// Accepts `"eip712"`, `"ethsign"`, `"eip1271"`, or `"presign"`.
579    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    /// Parse a `CoW` Protocol ECDSA signing scheme from its API string.
597    ///
598    /// Accepts `"eip712"` or `"ethsign"`.
599    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    /// Parse a `CoW` Protocol price quality hint from its API string.
615    ///
616    /// Accepts `"fast"`, `"optimal"`, or `"verified"`.
617    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    // ── OrderKind ────────────────────────────────────────────────────────
635
636    #[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    // ── TokenBalance ────────────────────────────────────────────────────
680
681    #[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        // Different variants produce different hashes
722        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    // ── SigningScheme ────────────────────────────────────────────────────
732
733    #[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    // ── EcdsaSigningScheme ──────────────────────────────────────────────
774
775    #[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    // ── PriceQuality ────────────────────────────────────────────────────
817
818    #[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    // ── CowHook ─────────────────────────────────────────────────────────
856
857    #[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    // ── OnchainOrderData ────────────────────────────────────────────────
877
878    #[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}