Skip to main content

cow_composable/
types.rs

1//! Types for `CoW` Protocol composable (conditional) orders.
2
3use std::fmt;
4
5use alloy_primitives::{Address, B256, U256};
6use serde::{Deserialize, Serialize};
7
8use cow_types::OrderKind;
9
10// ── Handler addresses ─────────────────────────────────────────────────────────
11
12/// `ComposableCow` factory contract — same address on all supported chains.
13///
14/// `0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74`
15pub const COMPOSABLE_COW_ADDRESS: Address = Address::new([
16    0xfd, 0xaf, 0xc9, 0xd1, 0x90, 0x2f, 0x4e, 0x0b, 0x84, 0xf6, 0x5f, 0x49, 0xf2, 0x44, 0xb3, 0x2b,
17    0x31, 0x01, 0x3b, 0x74,
18]);
19
20/// Default `TWAP` handler contract address.
21///
22/// `0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5`
23pub const TWAP_HANDLER_ADDRESS: Address = Address::new([
24    0x6c, 0xf1, 0xe9, 0xca, 0x41, 0xf7, 0x61, 0x1d, 0xef, 0x40, 0x81, 0x22, 0x79, 0x3c, 0x35, 0x8a,
25    0x3d, 0x11, 0xe5, 0xa5,
26]);
27
28/// `CurrentBlockTimestampFactory` contract address.
29///
30/// Used as the `ContextFactory` when a `TWAP` order has `start_time =
31/// AtMiningTime` (`t0 = 0`). The factory reads `block.timestamp` at order
32/// creation and writes it into the `ComposableCow` cabinet so that every part
33/// is measured from the same anchor.
34///
35/// `0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc`
36pub const CURRENT_BLOCK_TIMESTAMP_FACTORY_ADDRESS: Address = Address::new([
37    0x52, 0xed, 0x56, 0xda, 0x04, 0x30, 0x9a, 0xca, 0x4c, 0x3f, 0xec, 0xc5, 0x95, 0x29, 0x8d, 0x80,
38    0xc2, 0xf1, 0x6b, 0xac,
39]);
40
41/// Maximum allowed `part_duration` in seconds (1 year).
42///
43/// Mirrors `MAX_FREQUENCY` from the `TypeScript` SDK.
44pub const MAX_FREQUENCY: u32 = 365 * 24 * 60 * 60; // 31_536_000 s
45
46// ── ConditionalOrderParams ────────────────────────────────────────────────────
47
48/// ABI-encoded parameters identifying a conditional order on-chain.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct ConditionalOrderParams {
51    /// Address of the handler contract that validates the order.
52    pub handler: Address,
53    /// 32-byte salt providing uniqueness per order.
54    pub salt: B256,
55    /// ABI-encoded static input consumed by the handler.
56    pub static_input: Vec<u8>,
57}
58
59impl ConditionalOrderParams {
60    /// Construct [`ConditionalOrderParams`] from its three constituent fields.
61    ///
62    /// # Arguments
63    ///
64    /// * `handler` - Address of the handler contract that validates the order.
65    /// * `salt` - 32-byte salt providing uniqueness per order.
66    /// * `static_input` - ABI-encoded static input consumed by the handler.
67    ///
68    /// # Returns
69    ///
70    /// A new [`ConditionalOrderParams`] instance.
71    #[must_use]
72    pub const fn new(handler: Address, salt: B256, static_input: Vec<u8>) -> Self {
73        Self { handler, salt, static_input }
74    }
75
76    /// Override the handler contract address.
77    ///
78    /// # Arguments
79    ///
80    /// * `handler` - The new handler contract address.
81    ///
82    /// # Returns
83    ///
84    /// The modified [`ConditionalOrderParams`] with the updated handler (builder pattern).
85    #[must_use]
86    pub const fn with_handler(mut self, handler: Address) -> Self {
87        self.handler = handler;
88        self
89    }
90
91    /// Override the 32-byte salt.
92    ///
93    /// # Arguments
94    ///
95    /// * `salt` - The new 32-byte salt value.
96    ///
97    /// # Returns
98    ///
99    /// The modified [`ConditionalOrderParams`] with the updated salt (builder pattern).
100    #[must_use]
101    pub const fn with_salt(mut self, salt: B256) -> Self {
102        self.salt = salt;
103        self
104    }
105
106    /// Override the ABI-encoded static input.
107    ///
108    /// # Arguments
109    ///
110    /// * `static_input` - The new ABI-encoded static input bytes.
111    ///
112    /// # Returns
113    ///
114    /// The modified [`ConditionalOrderParams`] with the updated static input (builder pattern).
115    #[must_use]
116    pub fn with_static_input(mut self, static_input: Vec<u8>) -> Self {
117        self.static_input = static_input;
118        self
119    }
120
121    /// Returns `true` if the static input bytes are empty.
122    ///
123    /// # Returns
124    ///
125    /// `true` if the `static_input` field contains zero bytes, `false` otherwise.
126    #[must_use]
127    pub const fn is_empty_static_input(&self) -> bool {
128        self.static_input.is_empty()
129    }
130
131    /// Returns the length of the static input bytes.
132    ///
133    /// # Returns
134    ///
135    /// The number of bytes in the `static_input` field.
136    #[must_use]
137    pub const fn static_input_len(&self) -> usize {
138        self.static_input.len()
139    }
140
141    /// Returns a reference to the 32-byte salt.
142    ///
143    /// ```
144    /// use alloy_primitives::{Address, B256};
145    /// use cow_composable::ConditionalOrderParams;
146    ///
147    /// let params = ConditionalOrderParams::new(Address::ZERO, B256::ZERO, vec![]);
148    /// assert_eq!(params.salt_ref(), &B256::ZERO);
149    /// ```
150    #[must_use]
151    pub const fn salt_ref(&self) -> &B256 {
152        &self.salt
153    }
154}
155
156impl fmt::Display for ConditionalOrderParams {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        write!(f, "params(handler={:#x})", self.handler)
159    }
160}
161
162// ── TWAP ──────────────────────────────────────────────────────────────────────
163
164/// Start time specification for a `TWAP` order.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166pub enum TwapStartTime {
167    /// Start immediately at the block containing the order creation tx.
168    AtMiningTime,
169    /// Start at a specific Unix timestamp.
170    At(u32),
171}
172
173impl TwapStartTime {
174    /// Returns a human-readable string label for the start time.
175    ///
176    /// # Returns
177    ///
178    /// `"at-mining-time"` for [`AtMiningTime`](Self::AtMiningTime), or
179    /// `"at-unix"` for [`At`](Self::At).
180    #[must_use]
181    pub const fn as_str(self) -> &'static str {
182        match self {
183            Self::AtMiningTime => "at-mining-time",
184            Self::At(_) => "at-unix",
185        }
186    }
187
188    /// Returns `true` if the order starts at the block it is mined in.
189    ///
190    /// # Returns
191    ///
192    /// `true` for [`AtMiningTime`](Self::AtMiningTime), `false` for [`At`](Self::At).
193    #[must_use]
194    pub const fn is_at_mining_time(self) -> bool {
195        matches!(self, Self::AtMiningTime)
196    }
197
198    /// Returns `true` if the order starts at a fixed Unix timestamp.
199    ///
200    /// # Returns
201    ///
202    /// `true` for [`At`](Self::At), `false` for [`AtMiningTime`](Self::AtMiningTime).
203    #[must_use]
204    pub const fn is_fixed(self) -> bool {
205        matches!(self, Self::At(_))
206    }
207
208    /// Return the fixed start timestamp, or `None` for [`AtMiningTime`](Self::AtMiningTime).
209    ///
210    /// # Returns
211    ///
212    /// `Some(ts)` containing the Unix timestamp for [`At`](Self::At),
213    /// or `None` for [`AtMiningTime`](Self::AtMiningTime).
214    #[must_use]
215    pub const fn timestamp(self) -> Option<u32> {
216        match self {
217            Self::At(ts) => Some(ts),
218            Self::AtMiningTime => None,
219        }
220    }
221}
222
223impl fmt::Display for TwapStartTime {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        match self {
226            Self::AtMiningTime => f.write_str("at-mining-time"),
227            Self::At(ts) => write!(f, "at-unix-{ts}"),
228        }
229    }
230}
231
232impl From<u32> for TwapStartTime {
233    /// Convert a Unix timestamp into a [`TwapStartTime`].
234    ///
235    /// `0` maps to [`TwapStartTime::AtMiningTime`]; any other value maps to
236    /// [`TwapStartTime::At`].  This mirrors the on-chain `t0` field encoding.
237    fn from(ts: u32) -> Self {
238        if ts == 0 { Self::AtMiningTime } else { Self::At(ts) }
239    }
240}
241
242impl From<TwapStartTime> for u32 {
243    /// Encode a [`TwapStartTime`] as the on-chain `t0` field.
244    ///
245    /// [`TwapStartTime::AtMiningTime`] encodes as `0`; [`TwapStartTime::At`]
246    /// encodes as the contained Unix timestamp.
247    fn from(t: TwapStartTime) -> Self {
248        match t {
249            TwapStartTime::AtMiningTime => 0,
250            TwapStartTime::At(ts) => ts,
251        }
252    }
253}
254
255impl From<Option<u32>> for TwapStartTime {
256    /// Convert an optional Unix timestamp to a [`TwapStartTime`].
257    ///
258    /// `Some(ts)` maps to [`TwapStartTime::At`];
259    /// `None` maps to [`TwapStartTime::AtMiningTime`].
260    fn from(ts: Option<u32>) -> Self {
261        match ts {
262            Some(t) => Self::At(t),
263            None => Self::AtMiningTime,
264        }
265    }
266}
267
268/// Duration constraint for each individual `TWAP` part.
269///
270/// - [`DurationOfPart::Auto`] encodes `span = 0` on-chain, meaning each part is valid for the
271///   entire `part_duration` window.
272/// - [`DurationOfPart::LimitDuration`] encodes `span = duration`, restricting each part to a
273///   shorter window within the overall interval.
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
275pub enum DurationOfPart {
276    /// Each part is valid for the full `part_duration` window (default).
277    #[default]
278    Auto,
279    /// Each part is valid only for `duration` seconds within the window.
280    LimitDuration {
281        /// Active window for the part, in seconds. Must be ≤ `part_duration`.
282        duration: u32,
283    },
284}
285
286impl DurationOfPart {
287    /// Return the limit duration in seconds, or `None` for [`Auto`](Self::Auto).
288    #[must_use]
289    pub const fn duration(self) -> Option<u32> {
290        match self {
291            Self::LimitDuration { duration } => Some(duration),
292            Self::Auto => None,
293        }
294    }
295
296    /// Returns `true` if the part spans the full `part_duration` window.
297    #[must_use]
298    pub const fn is_auto(self) -> bool {
299        matches!(self, Self::Auto)
300    }
301
302    /// Construct a [`LimitDuration`](Self::LimitDuration) variant.
303    ///
304    /// ```
305    /// use cow_composable::DurationOfPart;
306    ///
307    /// let d = DurationOfPart::limit(1_800);
308    /// assert!(!d.is_auto());
309    /// assert_eq!(d.duration(), Some(1_800));
310    /// ```
311    #[must_use]
312    pub const fn limit(duration: u32) -> Self {
313        Self::LimitDuration { duration }
314    }
315
316    /// Returns `true` if this is a [`LimitDuration`](Self::LimitDuration) variant.
317    ///
318    /// ```
319    /// use cow_composable::DurationOfPart;
320    ///
321    /// assert!(DurationOfPart::limit(600).is_limit_duration());
322    /// assert!(!DurationOfPart::Auto.is_limit_duration());
323    /// ```
324    #[must_use]
325    pub const fn is_limit_duration(self) -> bool {
326        matches!(self, Self::LimitDuration { .. })
327    }
328}
329
330impl fmt::Display for DurationOfPart {
331    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332        match self {
333            Self::Auto => f.write_str("auto"),
334            Self::LimitDuration { duration } => write!(f, "limit-duration({duration}s)"),
335        }
336    }
337}
338
339impl From<Option<u32>> for DurationOfPart {
340    /// Convert an optional duration to a [`DurationOfPart`].
341    ///
342    /// `Some(d)` maps to [`DurationOfPart::LimitDuration`];
343    /// `None` maps to [`DurationOfPart::Auto`].
344    fn from(d: Option<u32>) -> Self {
345        match d {
346            Some(duration) => Self::LimitDuration { duration },
347            None => Self::Auto,
348        }
349    }
350}
351
352/// Parameters for a Time-Weighted Average Price (`TWAP`) order.
353///
354/// A `TWAP` order splits a large trade into `num_parts` equal parts executed
355/// over `num_parts × part_duration` seconds, reducing market impact.
356#[derive(Debug, Clone, Serialize, Deserialize)]
357#[serde(rename_all = "camelCase")]
358pub struct TwapData {
359    /// Token to sell.
360    pub sell_token: Address,
361    /// Token to buy.
362    pub buy_token: Address,
363    /// Address to receive bought tokens (use [`Address::ZERO`] for the order owner).
364    pub receiver: Address,
365    /// Total amount to sell across all parts.
366    pub sell_amount: U256,
367    /// Minimum total amount to buy across all parts.
368    pub buy_amount: U256,
369    /// When to start the `TWAP`.
370    pub start_time: TwapStartTime,
371    /// Duration of each part in seconds.
372    pub part_duration: u32,
373    /// Number of parts to split the order into.
374    pub num_parts: u32,
375    /// App-data hash (use [`B256::ZERO`] for none).
376    pub app_data: B256,
377    /// Whether each individual part may be partially filled.
378    pub partially_fillable: bool,
379    /// Order kind (`Sell` or `Buy`).
380    pub kind: OrderKind,
381    /// How long each part remains valid within its window.
382    ///
383    /// Defaults to [`DurationOfPart::Auto`] (full window, `span = 0`).
384    #[serde(default)]
385    pub duration_of_part: DurationOfPart,
386}
387
388impl fmt::Display for TwapData {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        write!(
391            f,
392            "TWAP {} × {}s [{}] sell {} {:#x} → buy ≥ {} {:#x}",
393            self.num_parts,
394            self.part_duration,
395            self.start_time,
396            self.sell_amount,
397            self.sell_token,
398            self.buy_amount,
399            self.buy_token,
400        )
401    }
402}
403
404impl TwapData {
405    /// Total duration of the `TWAP` order in seconds.
406    ///
407    /// Equals `num_parts × part_duration`.
408    ///
409    /// # Returns
410    ///
411    /// The total duration in seconds as a `u64`.
412    #[must_use]
413    pub const fn total_duration_secs(&self) -> u64 {
414        self.num_parts as u64 * self.part_duration as u64
415    }
416
417    /// Absolute Unix timestamp at which the last part expires, if the start
418    /// time is known.
419    ///
420    /// # Returns
421    ///
422    /// `Some(end_timestamp)` when `start_time` is [`TwapStartTime::At`], computed
423    /// as `start + total_duration_secs()`. Returns `None` when `start_time` is
424    /// [`TwapStartTime::AtMiningTime`] (the exact start is only known at mining time).
425    #[must_use]
426    pub const fn end_time(&self) -> Option<u64> {
427        match self.start_time {
428            TwapStartTime::At(ts) => Some(ts as u64 + self.total_duration_secs()),
429            TwapStartTime::AtMiningTime => None,
430        }
431    }
432
433    /// Returns `true` if this is a sell-direction `TWAP` order.
434    ///
435    /// ```
436    /// use alloy_primitives::{Address, U256};
437    /// use cow_composable::TwapData;
438    ///
439    /// let twap = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3_600);
440    /// assert!(twap.is_sell());
441    /// assert!(!twap.is_buy());
442    /// ```
443    #[must_use]
444    pub const fn is_sell(&self) -> bool {
445        self.kind.is_sell()
446    }
447
448    /// Returns `true` if this is a buy-direction `TWAP` order.
449    ///
450    /// ```
451    /// use alloy_primitives::{Address, U256};
452    /// use cow_composable::{TwapData, TwapStartTime};
453    /// use cow_types::OrderKind;
454    ///
455    /// let mut twap = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3_600);
456    /// twap.kind = OrderKind::Buy;
457    /// assert!(twap.is_buy());
458    /// assert!(!twap.is_sell());
459    /// ```
460    #[must_use]
461    pub const fn is_buy(&self) -> bool {
462        self.kind.is_buy()
463    }
464
465    /// Returns `true` if the `TWAP` has fully expired at the given Unix timestamp.
466    ///
467    /// Returns `false` when `start_time` is [`TwapStartTime::AtMiningTime`]
468    /// (the end time is not yet known).
469    ///
470    /// ```
471    /// use alloy_primitives::{Address, U256};
472    /// use cow_composable::{TwapData, TwapStartTime};
473    ///
474    /// let twap = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3_600)
475    ///     .with_start_time(TwapStartTime::At(1_000_000));
476    /// // ends at 1_000_000 + 4 × 3600 = 1_014_400
477    /// assert!(!twap.is_expired(1_014_399));
478    /// assert!(twap.is_expired(1_014_400));
479    /// ```
480    #[must_use]
481    pub const fn is_expired(&self, timestamp: u64) -> bool {
482        match self.end_time() {
483            Some(end) => timestamp >= end,
484            None => false,
485        }
486    }
487
488    /// Create a minimal sell-kind TWAP order.
489    ///
490    /// Defaults: `receiver = Address::ZERO`, `buy_amount = U256::ZERO` (no min),
491    /// `start_time = TwapStartTime::AtMiningTime`, `app_data = B256::ZERO`,
492    /// `partially_fillable = false`, `duration_of_part = DurationOfPart::Auto`.
493    ///
494    /// Use the `with_*` builder methods to set optional fields.
495    ///
496    /// # Arguments
497    ///
498    /// * `sell_token` - Address of the token to sell.
499    /// * `buy_token` - Address of the token to buy.
500    /// * `sell_amount` - Total amount of `sell_token` to sell across all parts.
501    /// * `num_parts` - Number of parts to split the order into.
502    /// * `part_duration` - Duration of each part in seconds.
503    ///
504    /// # Returns
505    ///
506    /// A new [`TwapData`] configured as a sell order with sensible defaults.
507    #[must_use]
508    pub const fn sell(
509        sell_token: Address,
510        buy_token: Address,
511        sell_amount: U256,
512        num_parts: u32,
513        part_duration: u32,
514    ) -> Self {
515        Self {
516            sell_token,
517            buy_token,
518            receiver: Address::ZERO,
519            sell_amount,
520            buy_amount: U256::ZERO,
521            start_time: TwapStartTime::AtMiningTime,
522            part_duration,
523            num_parts,
524            app_data: B256::ZERO,
525            partially_fillable: false,
526            kind: OrderKind::Sell,
527            duration_of_part: DurationOfPart::Auto,
528        }
529    }
530
531    /// Create a minimal buy-kind TWAP order.
532    ///
533    /// Defaults: `receiver = Address::ZERO`, `sell_amount = U256::MAX` (unlimited),
534    /// `start_time = TwapStartTime::AtMiningTime`, `app_data = B256::ZERO`,
535    /// `partially_fillable = false`, `duration_of_part = DurationOfPart::Auto`.
536    ///
537    /// Use the `with_*` builder methods to set optional fields.
538    ///
539    /// # Arguments
540    ///
541    /// * `sell_token` - Address of the token to sell.
542    /// * `buy_token` - Address of the token to buy.
543    /// * `buy_amount` - Minimum total amount of `buy_token` to receive across all parts.
544    /// * `num_parts` - Number of parts to split the order into.
545    /// * `part_duration` - Duration of each part in seconds.
546    ///
547    /// # Returns
548    ///
549    /// A new [`TwapData`] configured as a buy order with sensible defaults.
550    #[must_use]
551    pub const fn buy(
552        sell_token: Address,
553        buy_token: Address,
554        buy_amount: U256,
555        num_parts: u32,
556        part_duration: u32,
557    ) -> Self {
558        Self {
559            sell_token,
560            buy_token,
561            receiver: Address::ZERO,
562            sell_amount: U256::MAX,
563            buy_amount,
564            start_time: TwapStartTime::AtMiningTime,
565            part_duration,
566            num_parts,
567            app_data: B256::ZERO,
568            partially_fillable: false,
569            kind: OrderKind::Buy,
570            duration_of_part: DurationOfPart::Auto,
571        }
572    }
573
574    /// Set the receiver address for bought tokens.
575    ///
576    /// [`Address::ZERO`] means the order owner (default).
577    ///
578    /// # Returns
579    ///
580    /// The modified [`TwapData`] with the updated receiver (builder pattern).
581    #[must_use]
582    pub const fn with_receiver(mut self, receiver: Address) -> Self {
583        self.receiver = receiver;
584        self
585    }
586
587    /// Set the minimum amount of `buy_token` to receive across all parts.
588    ///
589    /// Useful when building a sell-kind order to set a price floor.
590    ///
591    /// # Returns
592    ///
593    /// The modified [`TwapData`] with the updated buy amount (builder pattern).
594    #[must_use]
595    pub const fn with_buy_amount(mut self, buy_amount: U256) -> Self {
596        self.buy_amount = buy_amount;
597        self
598    }
599
600    /// Set the maximum amount of `sell_token` to sell across all parts.
601    ///
602    /// Useful when building a buy-kind order to cap spending.
603    ///
604    /// # Returns
605    ///
606    /// The modified [`TwapData`] with the updated sell amount (builder pattern).
607    #[must_use]
608    pub const fn with_sell_amount(mut self, sell_amount: U256) -> Self {
609        self.sell_amount = sell_amount;
610        self
611    }
612
613    /// Set when the TWAP order starts executing.
614    ///
615    /// # Returns
616    ///
617    /// The modified [`TwapData`] with the updated start time (builder pattern).
618    #[must_use]
619    pub const fn with_start_time(mut self, start_time: TwapStartTime) -> Self {
620        self.start_time = start_time;
621        self
622    }
623
624    /// Attach an app-data hash to the order.
625    ///
626    /// # Returns
627    ///
628    /// The modified [`TwapData`] with the updated app-data hash (builder pattern).
629    #[must_use]
630    pub const fn with_app_data(mut self, app_data: B256) -> Self {
631        self.app_data = app_data;
632        self
633    }
634
635    /// Allow each individual part to be partially filled.
636    ///
637    /// # Returns
638    ///
639    /// The modified [`TwapData`] with the updated partial-fill setting (builder pattern).
640    #[must_use]
641    pub const fn with_partially_fillable(mut self, partially_fillable: bool) -> Self {
642        self.partially_fillable = partially_fillable;
643        self
644    }
645
646    /// Restrict each part to a shorter validity window within its overall interval.
647    ///
648    /// # Returns
649    ///
650    /// The modified [`TwapData`] with the updated duration-of-part setting (builder pattern).
651    #[must_use]
652    pub const fn with_duration_of_part(mut self, duration_of_part: DurationOfPart) -> Self {
653        self.duration_of_part = duration_of_part;
654        self
655    }
656
657    /// Returns `true` if a non-zero app-data hash is attached.
658    ///
659    /// The zero hash (`B256::ZERO`) means no app-data was set.
660    ///
661    /// ```
662    /// use alloy_primitives::{Address, B256, U256};
663    /// use cow_composable::TwapData;
664    ///
665    /// let twap = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3_600);
666    /// assert!(!twap.has_app_data());
667    ///
668    /// let with_data = twap.with_app_data(B256::repeat_byte(0x01));
669    /// assert!(with_data.has_app_data());
670    /// ```
671    #[must_use]
672    pub fn has_app_data(&self) -> bool {
673        !self.app_data.is_zero()
674    }
675}
676
677/// On-chain `TwapStruct` representation with per-part amounts.
678///
679/// This mirrors the Solidity struct passed to the handler as `staticInput`.
680/// Use [`TwapData`] for the user-facing SDK type; use `TwapStruct` only when
681/// you need direct access to the ABI-level fields.
682#[derive(Debug, Clone)]
683pub struct TwapStruct {
684    /// Token to sell.
685    pub sell_token: Address,
686    /// Token to buy.
687    pub buy_token: Address,
688    /// Receiver of bought tokens.
689    pub receiver: Address,
690    /// Amount of `sell_token` to sell in **each** part (not total).
691    pub part_sell_amount: U256,
692    /// Minimum amount of `buy_token` to buy in **each** part.
693    pub min_part_limit: U256,
694    /// Start timestamp (`0` = use `CurrentBlockTimestampFactory`).
695    pub t0: u32,
696    /// Number of parts.
697    pub n: u32,
698    /// Duration of each part in seconds.
699    pub t: u32,
700    /// Part validity window in seconds (`0` = full window).
701    pub span: u32,
702    /// App-data hash.
703    pub app_data: B256,
704}
705
706impl TwapStruct {
707    /// Returns `true` if a non-zero app-data hash is set.
708    ///
709    /// The zero hash (`B256::ZERO`) means no app-data was attached.
710    ///
711    /// # Returns
712    ///
713    /// `true` if the `app_data` field is not [`B256::ZERO`], `false` otherwise.
714    #[must_use]
715    pub fn has_app_data(&self) -> bool {
716        !self.app_data.is_zero()
717    }
718
719    /// Returns `true` if the receiver is not the zero address.
720    ///
721    /// When `receiver == Address::ZERO`, the settlement contract uses the order
722    /// owner as the effective receiver.
723    ///
724    /// # Returns
725    ///
726    /// `true` if `receiver` is not [`Address::ZERO`], `false` otherwise.
727    #[must_use]
728    pub fn has_custom_receiver(&self) -> bool {
729        !self.receiver.is_zero()
730    }
731
732    /// Returns `true` if a fixed start timestamp is set (`t0 != 0`).
733    ///
734    /// When `t0 == 0`, the order uses `CurrentBlockTimestampFactory` to
735    /// determine the start time at mining time.
736    ///
737    /// # Returns
738    ///
739    /// `true` if `t0` is non-zero, `false` otherwise.
740    #[must_use]
741    pub const fn start_is_fixed(&self) -> bool {
742        self.t0 != 0
743    }
744}
745
746impl TryFrom<&TwapData> for TwapStruct {
747    type Error = cow_errors::CowError;
748
749    /// Convert a high-level [`TwapData`] into the ABI-level [`TwapStruct`].
750    ///
751    /// Delegates to [`crate::data_to_struct`].
752    fn try_from(d: &TwapData) -> Result<Self, Self::Error> {
753        crate::data_to_struct(d)
754    }
755}
756
757impl From<&TwapStruct> for TwapData {
758    /// Convert an ABI-level [`TwapStruct`] back into a high-level [`TwapData`].
759    ///
760    /// Delegates to [`crate::struct_to_data`].
761    fn from(s: &TwapStruct) -> Self {
762        crate::struct_to_data(s)
763    }
764}
765
766impl fmt::Display for TwapStruct {
767    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
768        write!(
769            f,
770            "twap-struct {} × {}s sell {} {:#x} → ≥{} {:#x}",
771            self.n,
772            self.t,
773            self.part_sell_amount,
774            self.sell_token,
775            self.min_part_limit,
776            self.buy_token,
777        )
778    }
779}
780
781// ── GpV2OrderStruct ───────────────────────────────────────────────────────────
782
783/// Raw on-chain `GPv2Order.DataStruct` as emitted by the `GPv2Settlement` contract.
784///
785/// Unlike [`UnsignedOrder`](cow_signing::types::UnsignedOrder), the
786/// `kind`, `sell_token_balance`, and `buy_token_balance` fields are stored as
787/// `keccak256` hashes rather than typed enums.
788///
789/// Use [`from_struct_to_order`](crate::from_struct_to_order) to
790/// decode them into a fully typed
791/// [`UnsignedOrder`](cow_signing::types::UnsignedOrder).
792///
793/// Mirrors `GPv2Order.DataStruct` from the `@cowprotocol/composable` SDK.
794#[derive(Debug, Clone)]
795pub struct GpV2OrderStruct {
796    /// Token to sell.
797    pub sell_token: Address,
798    /// Token to buy.
799    pub buy_token: Address,
800    /// Address that receives the bought tokens.
801    pub receiver: Address,
802    /// Amount of `sell_token` to sell (in atoms).
803    pub sell_amount: U256,
804    /// Minimum amount of `buy_token` to receive (in atoms).
805    pub buy_amount: U256,
806    /// Order expiry as a Unix timestamp.
807    pub valid_to: u32,
808    /// App-data hash (`bytes32`).
809    pub app_data: B256,
810    /// Protocol fee included in `sell_amount` (in atoms).
811    pub fee_amount: U256,
812    /// `keccak256("sell")` or `keccak256("buy")`.
813    pub kind: B256,
814    /// Whether the order may be partially filled.
815    pub partially_fillable: bool,
816    /// `keccak256("erc20")`, `keccak256("external")`, or `keccak256("internal")`.
817    pub sell_token_balance: B256,
818    /// `keccak256("erc20")`, `keccak256("external")`, or `keccak256("internal")`.
819    pub buy_token_balance: B256,
820}
821impl fmt::Display for GpV2OrderStruct {
822    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
823        write!(
824            f,
825            "gpv2-order({:#x} sell={} → {:#x} buy={})",
826            self.sell_token, self.sell_amount, self.buy_token, self.buy_amount
827        )
828    }
829}
830
831impl GpV2OrderStruct {
832    /// Returns `true` if the receiver is not the zero address.
833    ///
834    /// When `receiver == Address::ZERO`, the settlement contract uses the order
835    /// owner as the effective receiver.
836    ///
837    /// # Returns
838    ///
839    /// `true` if `receiver` is not [`Address::ZERO`], `false` otherwise.
840    #[must_use]
841    pub fn has_custom_receiver(&self) -> bool {
842        !self.receiver.is_zero()
843    }
844
845    /// Returns `true` if this order allows partial fills.
846    ///
847    /// # Returns
848    ///
849    /// `true` if `partially_fillable` is set, `false` for fill-or-kill orders.
850    #[must_use]
851    pub const fn is_partially_fillable(&self) -> bool {
852        self.partially_fillable
853    }
854}
855
856impl TryFrom<&GpV2OrderStruct> for cow_signing::types::UnsignedOrder {
857    type Error = cow_errors::CowError;
858
859    /// Decode a raw [`GpV2OrderStruct`] into a fully typed `UnsignedOrder`.
860    ///
861    /// Resolves the hashed `kind`, `sell_token_balance`, and `buy_token_balance`
862    /// fields back into their enum representations via
863    /// [`crate::from_struct_to_order`].
864    fn try_from(s: &GpV2OrderStruct) -> Result<Self, Self::Error> {
865        crate::from_struct_to_order(s)
866    }
867}
868
869// ── PollResult ────────────────────────────────────────────────────────────────
870
871/// Result returned when polling a conditional order for tradability.
872///
873/// On `Success`, contains the on-chain order struct and the pre-signature bytes
874/// ready for submission to the orderbook.
875#[derive(Debug, Clone)]
876pub enum PollResult {
877    /// The order is valid and can be submitted now.
878    ///
879    /// When returned by a full signing poll, `order` and `signature` are set to
880    /// the resolved `GPv2Order.Data` struct and the ABI-encoded signature.
881    /// When returned by an offline validity check (e.g. `TwapOrder::poll_validate`),
882    /// both fields are `None`.
883    Success {
884        /// The resolved order ready for submission (`None` for offline checks).
885        order: Option<cow_signing::types::UnsignedOrder>,
886        /// Hex-encoded signature bytes, `0x`-prefixed (`None` for offline checks).
887        signature: Option<String>,
888    },
889    /// Retry on the next block.
890    TryNextBlock,
891    /// Retry once the given block number is reached.
892    TryOnBlock {
893        /// Target block number.
894        block_number: u64,
895    },
896    /// Retry once the given Unix timestamp is reached.
897    TryAtEpoch {
898        /// Target Unix timestamp in seconds.
899        epoch: u64,
900    },
901    /// An unexpected error occurred.
902    UnexpectedError {
903        /// Human-readable error description.
904        message: String,
905    },
906    /// This order should never be polled again.
907    DontTryAgain {
908        /// Reason the order is permanently inactive.
909        reason: String,
910    },
911}
912
913impl PollResult {
914    /// Returns `true` if the order is ready to be submitted.
915    ///
916    /// # Returns
917    ///
918    /// `true` for the [`Success`](Self::Success) variant, `false` for all others.
919    #[must_use]
920    pub const fn is_success(&self) -> bool {
921        matches!(self, Self::Success { .. })
922    }
923
924    /// Returns `true` if polling should be retried in a future block or epoch.
925    ///
926    /// # Returns
927    ///
928    /// `true` for [`TryNextBlock`](Self::TryNextBlock), [`TryOnBlock`](Self::TryOnBlock),
929    /// or [`TryAtEpoch`](Self::TryAtEpoch); `false` otherwise.
930    #[must_use]
931    pub const fn is_retryable(&self) -> bool {
932        matches!(self, Self::TryNextBlock | Self::TryOnBlock { .. } | Self::TryAtEpoch { .. })
933    }
934
935    /// Returns `true` if this order should never be polled again.
936    ///
937    /// # Returns
938    ///
939    /// `true` for the [`DontTryAgain`](Self::DontTryAgain) variant, `false` otherwise.
940    #[must_use]
941    pub const fn is_terminal(&self) -> bool {
942        matches!(self, Self::DontTryAgain { .. })
943    }
944
945    /// Returns `true` if polling should retry on the very next block.
946    ///
947    /// # Returns
948    ///
949    /// `true` for the [`TryNextBlock`](Self::TryNextBlock) variant, `false` otherwise.
950    #[must_use]
951    pub const fn is_try_next_block(&self) -> bool {
952        matches!(self, Self::TryNextBlock)
953    }
954
955    /// Returns `true` if polling should retry once a specific block is reached.
956    ///
957    /// # Returns
958    ///
959    /// `true` for the [`TryOnBlock`](Self::TryOnBlock) variant, `false` otherwise.
960    #[must_use]
961    pub const fn is_try_on_block(&self) -> bool {
962        matches!(self, Self::TryOnBlock { .. })
963    }
964
965    /// Returns `true` if polling should retry once a specific Unix epoch is reached.
966    ///
967    /// # Returns
968    ///
969    /// `true` for the [`TryAtEpoch`](Self::TryAtEpoch) variant, `false` otherwise.
970    #[must_use]
971    pub const fn is_try_at_epoch(&self) -> bool {
972        matches!(self, Self::TryAtEpoch { .. })
973    }
974
975    /// Returns `true` if an unexpected error occurred during polling.
976    ///
977    /// # Returns
978    ///
979    /// `true` for the [`UnexpectedError`](Self::UnexpectedError) variant, `false` otherwise.
980    #[must_use]
981    pub const fn is_unexpected_error(&self) -> bool {
982        matches!(self, Self::UnexpectedError { .. })
983    }
984
985    /// Returns `true` if this order should never be polled again (terminal failure).
986    ///
987    /// # Returns
988    ///
989    /// `true` for the [`DontTryAgain`](Self::DontTryAgain) variant, `false` otherwise.
990    #[must_use]
991    pub const fn is_dont_try_again(&self) -> bool {
992        matches!(self, Self::DontTryAgain { .. })
993    }
994
995    /// Extract the target block number from a [`TryOnBlock`](Self::TryOnBlock) variant.
996    ///
997    /// Returns `None` for all other variants.
998    ///
999    /// ```
1000    /// use cow_composable::PollResult;
1001    ///
1002    /// let r = PollResult::TryOnBlock { block_number: 12_345_678 };
1003    /// assert_eq!(r.get_block_number(), Some(12_345_678));
1004    /// assert_eq!(PollResult::TryNextBlock.get_block_number(), None);
1005    /// ```
1006    #[must_use]
1007    pub const fn get_block_number(&self) -> Option<u64> {
1008        if let Self::TryOnBlock { block_number } = self { Some(*block_number) } else { None }
1009    }
1010
1011    /// Extract the target Unix epoch from a [`TryAtEpoch`](Self::TryAtEpoch) variant.
1012    ///
1013    /// Returns `None` for all other variants.
1014    ///
1015    /// ```
1016    /// use cow_composable::PollResult;
1017    ///
1018    /// let r = PollResult::TryAtEpoch { epoch: 1_700_000_000 };
1019    /// assert_eq!(r.get_epoch(), Some(1_700_000_000));
1020    /// assert_eq!(PollResult::TryNextBlock.get_epoch(), None);
1021    /// ```
1022    #[must_use]
1023    pub const fn get_epoch(&self) -> Option<u64> {
1024        if let Self::TryAtEpoch { epoch } = self { Some(*epoch) } else { None }
1025    }
1026
1027    /// Extract the resolved [`UnsignedOrder`](cow_signing::types::UnsignedOrder)
1028    /// from a [`PollResult::Success`] variant, if present.
1029    ///
1030    /// Returns `None` for all other variants, or when `order` is `None`
1031    /// inside `Success` (e.g. an offline validity check result).
1032    #[must_use]
1033    pub const fn order_ref(&self) -> Option<&cow_signing::types::UnsignedOrder> {
1034        if let Self::Success { order, .. } = self { order.as_ref() } else { None }
1035    }
1036
1037    /// Extract the error message from an [`UnexpectedError`](Self::UnexpectedError)
1038    /// or [`DontTryAgain`](Self::DontTryAgain) variant.
1039    ///
1040    /// Returns `None` for all other variants.
1041    #[must_use]
1042    pub const fn as_error_message(&self) -> Option<&str> {
1043        match self {
1044            Self::UnexpectedError { message } => Some(message.as_str()),
1045            Self::DontTryAgain { reason } => Some(reason.as_str()),
1046            Self::Success { .. } |
1047            Self::TryNextBlock |
1048            Self::TryOnBlock { .. } |
1049            Self::TryAtEpoch { .. } => None,
1050        }
1051    }
1052}
1053
1054impl fmt::Display for PollResult {
1055    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1056        match self {
1057            Self::Success { .. } => f.write_str("success"),
1058            Self::TryNextBlock => f.write_str("try-next-block"),
1059            Self::TryOnBlock { block_number } => write!(f, "try-on-block({block_number})"),
1060            Self::TryAtEpoch { epoch } => write!(f, "try-at-epoch({epoch})"),
1061            Self::UnexpectedError { message } => write!(f, "unexpected-error({message})"),
1062            Self::DontTryAgain { reason } => write!(f, "dont-try-again({reason})"),
1063        }
1064    }
1065}
1066
1067// ── ProofLocation ─────────────────────────────────────────────────────────────
1068
1069/// Where the Merkle proof for a conditional order is stored / communicated.
1070#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1071#[repr(u8)]
1072pub enum ProofLocation {
1073    /// Proof is kept private; only the owner polls.
1074    #[default]
1075    Private = 0,
1076    /// Proof emitted as an on-chain event.
1077    Emitted = 1,
1078    /// Proof stored on Swarm.
1079    Swarm = 2,
1080    /// Proof communicated via Waku.
1081    Waku = 3,
1082    /// Reserved for future use.
1083    Reserved = 4,
1084    /// Proof stored on IPFS.
1085    Ipfs = 5,
1086}
1087
1088impl ProofLocation {
1089    /// Returns a lowercase string label for the proof location.
1090    #[must_use]
1091    pub const fn as_str(self) -> &'static str {
1092        match self {
1093            Self::Private => "private",
1094            Self::Emitted => "emitted",
1095            Self::Swarm => "swarm",
1096            Self::Waku => "waku",
1097            Self::Reserved => "reserved",
1098            Self::Ipfs => "ipfs",
1099        }
1100    }
1101
1102    /// Returns `true` if the proof is kept private (owner-polled only).
1103    #[must_use]
1104    pub const fn is_private(self) -> bool {
1105        matches!(self, Self::Private)
1106    }
1107
1108    /// Returns `true` if the proof is emitted as an on-chain event.
1109    #[must_use]
1110    pub const fn is_emitted(self) -> bool {
1111        matches!(self, Self::Emitted)
1112    }
1113
1114    /// Returns `true` if the proof is stored on Swarm.
1115    #[must_use]
1116    pub const fn is_swarm(self) -> bool {
1117        matches!(self, Self::Swarm)
1118    }
1119
1120    /// Returns `true` if the proof is communicated via Waku.
1121    #[must_use]
1122    pub const fn is_waku(self) -> bool {
1123        matches!(self, Self::Waku)
1124    }
1125
1126    /// Returns `true` if this location is the reserved (future-use) discriminant.
1127    #[must_use]
1128    pub const fn is_reserved(self) -> bool {
1129        matches!(self, Self::Reserved)
1130    }
1131
1132    /// Returns `true` if the proof is stored on IPFS.
1133    #[must_use]
1134    pub const fn is_ipfs(self) -> bool {
1135        matches!(self, Self::Ipfs)
1136    }
1137}
1138
1139impl fmt::Display for ProofLocation {
1140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1141        f.write_str(self.as_str())
1142    }
1143}
1144
1145impl TryFrom<u8> for ProofLocation {
1146    type Error = cow_errors::CowError;
1147
1148    /// Parse a [`ProofLocation`] from its on-chain `uint8` discriminant.
1149    fn try_from(n: u8) -> Result<Self, Self::Error> {
1150        match n {
1151            0 => Ok(Self::Private),
1152            1 => Ok(Self::Emitted),
1153            2 => Ok(Self::Swarm),
1154            3 => Ok(Self::Waku),
1155            4 => Ok(Self::Reserved),
1156            5 => Ok(Self::Ipfs),
1157            other => Err(cow_errors::CowError::Parse {
1158                field: "ProofLocation",
1159                reason: format!("unknown discriminant: {other}"),
1160            }),
1161        }
1162    }
1163}
1164
1165impl TryFrom<&str> for ProofLocation {
1166    type Error = cow_errors::CowError;
1167
1168    /// Parse a [`ProofLocation`] from its string label.
1169    fn try_from(s: &str) -> Result<Self, Self::Error> {
1170        match s {
1171            "private" => Ok(Self::Private),
1172            "emitted" => Ok(Self::Emitted),
1173            "swarm" => Ok(Self::Swarm),
1174            "waku" => Ok(Self::Waku),
1175            "reserved" => Ok(Self::Reserved),
1176            "ipfs" => Ok(Self::Ipfs),
1177            other => Err(cow_errors::CowError::Parse {
1178                field: "ProofLocation",
1179                reason: format!("unknown value: {other}"),
1180            }),
1181        }
1182    }
1183}
1184
1185impl From<ProofLocation> for u8 {
1186    /// Encode a [`ProofLocation`] as its on-chain `uint8` discriminant.
1187    ///
1188    /// This is the inverse of [`TryFrom<u8>`] for [`ProofLocation`].
1189    fn from(loc: ProofLocation) -> Self {
1190        loc as Self
1191    }
1192}
1193
1194impl ProofStruct {
1195    /// Construct a [`ProofStruct`] with the given location and data bytes.
1196    ///
1197    /// # Arguments
1198    ///
1199    /// * `location` - Where the Merkle proof is stored or communicated.
1200    /// * `data` - Location-specific proof bytes (empty for private or emitted proofs).
1201    ///
1202    /// # Returns
1203    ///
1204    /// A new [`ProofStruct`] instance.
1205    #[must_use]
1206    pub const fn new(location: ProofLocation, data: Vec<u8>) -> Self {
1207        Self { location, data }
1208    }
1209
1210    /// A private proof (no location data needed).
1211    ///
1212    /// # Returns
1213    ///
1214    /// A [`ProofStruct`] with [`ProofLocation::Private`] and empty data.
1215    #[must_use]
1216    pub const fn private() -> Self {
1217        Self { location: ProofLocation::Private, data: Vec::new() }
1218    }
1219
1220    /// An emitted proof (no location data needed — the proof is in the tx log).
1221    ///
1222    /// # Returns
1223    ///
1224    /// A [`ProofStruct`] with [`ProofLocation::Emitted`] and empty data.
1225    #[must_use]
1226    pub const fn emitted() -> Self {
1227        Self { location: ProofLocation::Emitted, data: Vec::new() }
1228    }
1229
1230    /// Override the proof location.
1231    ///
1232    /// # Returns
1233    ///
1234    /// The modified [`ProofStruct`] with the updated location (builder pattern).
1235    #[must_use]
1236    pub const fn with_location(mut self, location: ProofLocation) -> Self {
1237        self.location = location;
1238        self
1239    }
1240
1241    /// Override the location-specific proof data bytes.
1242    ///
1243    /// # Returns
1244    ///
1245    /// The modified [`ProofStruct`] with the updated data (builder pattern).
1246    #[must_use]
1247    pub fn with_data(mut self, data: Vec<u8>) -> Self {
1248        self.data = data;
1249        self
1250    }
1251
1252    /// Returns `true` if the proof location is [`ProofLocation::Private`].
1253    ///
1254    /// # Returns
1255    ///
1256    /// `true` if `location` is [`ProofLocation::Private`], `false` otherwise.
1257    #[must_use]
1258    pub const fn is_private(&self) -> bool {
1259        self.location.is_private()
1260    }
1261
1262    /// Returns `true` if the proof location is [`ProofLocation::Emitted`].
1263    ///
1264    /// # Returns
1265    ///
1266    /// `true` if `location` is [`ProofLocation::Emitted`], `false` otherwise.
1267    #[must_use]
1268    pub const fn is_emitted(&self) -> bool {
1269        self.location.is_emitted()
1270    }
1271
1272    /// Returns `true` if the proof location is [`ProofLocation::Swarm`].
1273    ///
1274    /// # Returns
1275    ///
1276    /// `true` if `location` is [`ProofLocation::Swarm`], `false` otherwise.
1277    #[must_use]
1278    pub const fn is_swarm(&self) -> bool {
1279        self.location.is_swarm()
1280    }
1281
1282    /// Returns `true` if the proof location is [`ProofLocation::Waku`].
1283    ///
1284    /// # Returns
1285    ///
1286    /// `true` if `location` is [`ProofLocation::Waku`], `false` otherwise.
1287    #[must_use]
1288    pub const fn is_waku(&self) -> bool {
1289        self.location.is_waku()
1290    }
1291
1292    /// Returns `true` if the proof location is [`ProofLocation::Ipfs`].
1293    ///
1294    /// # Returns
1295    ///
1296    /// `true` if `location` is [`ProofLocation::Ipfs`], `false` otherwise.
1297    #[must_use]
1298    pub const fn is_ipfs(&self) -> bool {
1299        self.location.is_ipfs()
1300    }
1301
1302    /// Returns `true` if the proof location is [`ProofLocation::Reserved`].
1303    ///
1304    /// # Returns
1305    ///
1306    /// `true` if `location` is [`ProofLocation::Reserved`], `false` otherwise.
1307    #[must_use]
1308    pub const fn is_reserved(&self) -> bool {
1309        self.location.is_reserved()
1310    }
1311
1312    /// Returns `true` if this proof has non-empty data bytes.
1313    ///
1314    /// [`ProofLocation::Private`] and [`ProofLocation::Emitted`] proofs carry no
1315    /// data; IPFS, Swarm, and Waku proofs carry location-specific bytes.
1316    #[must_use]
1317    pub const fn has_data(&self) -> bool {
1318        !self.data.is_empty()
1319    }
1320
1321    /// Returns `true` if this proof has no data bytes (complement of [`has_data`](Self::has_data)).
1322    #[must_use]
1323    pub const fn is_empty(&self) -> bool {
1324        self.data.is_empty()
1325    }
1326
1327    /// Returns the number of data bytes in this proof.
1328    #[must_use]
1329    pub const fn data_len(&self) -> usize {
1330        self.data.len()
1331    }
1332}
1333
1334impl fmt::Display for ProofStruct {
1335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1336        write!(f, "proof({})", self.location)
1337    }
1338}
1339
1340// ── Tests ─────────────────────────────────────────────────────────────────────
1341
1342#[cfg(test)]
1343mod tests {
1344    use super::*;
1345
1346    // ── Constants ────────────────────────────────────────────────────────────
1347
1348    #[test]
1349    fn composable_cow_address_matches() {
1350        assert_eq!(
1351            format!("{COMPOSABLE_COW_ADDRESS:#x}"),
1352            "0xfdafc9d1902f4e0b84f65f49f244b32b31013b74"
1353        );
1354    }
1355
1356    #[test]
1357    fn twap_handler_address_matches() {
1358        assert_eq!(
1359            format!("{TWAP_HANDLER_ADDRESS:#x}"),
1360            "0x6cf1e9ca41f7611def408122793c358a3d11e5a5"
1361        );
1362    }
1363
1364    #[test]
1365    fn current_block_timestamp_factory_address_matches() {
1366        assert_eq!(
1367            format!("{CURRENT_BLOCK_TIMESTAMP_FACTORY_ADDRESS:#x}"),
1368            "0x52ed56da04309aca4c3fecc595298d80c2f16bac"
1369        );
1370    }
1371
1372    #[test]
1373    fn max_frequency_is_one_year() {
1374        assert_eq!(MAX_FREQUENCY, 365 * 24 * 60 * 60);
1375        assert_eq!(MAX_FREQUENCY, 31_536_000);
1376    }
1377
1378    // ── ConditionalOrderParams ──────────────────────────────────────────────
1379
1380    #[test]
1381    fn conditional_order_params_new() {
1382        let p = ConditionalOrderParams::new(Address::ZERO, B256::ZERO, vec![1, 2, 3]);
1383        assert_eq!(p.handler, Address::ZERO);
1384        assert_eq!(p.salt, B256::ZERO);
1385        assert_eq!(p.static_input, vec![1, 2, 3]);
1386    }
1387
1388    #[test]
1389    fn conditional_order_params_with_handler() {
1390        let p = ConditionalOrderParams::new(Address::ZERO, B256::ZERO, vec![])
1391            .with_handler(TWAP_HANDLER_ADDRESS);
1392        assert_eq!(p.handler, TWAP_HANDLER_ADDRESS);
1393    }
1394
1395    #[test]
1396    fn conditional_order_params_with_salt() {
1397        let salt = B256::repeat_byte(0xAB);
1398        let p = ConditionalOrderParams::new(Address::ZERO, B256::ZERO, vec![]).with_salt(salt);
1399        assert_eq!(p.salt, salt);
1400    }
1401
1402    #[test]
1403    fn conditional_order_params_with_static_input() {
1404        let p = ConditionalOrderParams::new(Address::ZERO, B256::ZERO, vec![])
1405            .with_static_input(vec![0xDE, 0xAD]);
1406        assert_eq!(p.static_input, vec![0xDE, 0xAD]);
1407    }
1408
1409    #[test]
1410    fn conditional_order_params_empty_static_input() {
1411        let empty = ConditionalOrderParams::new(Address::ZERO, B256::ZERO, vec![]);
1412        assert!(empty.is_empty_static_input());
1413        assert_eq!(empty.static_input_len(), 0);
1414
1415        let non_empty = empty.with_static_input(vec![1]);
1416        assert!(!non_empty.is_empty_static_input());
1417        assert_eq!(non_empty.static_input_len(), 1);
1418    }
1419
1420    #[test]
1421    fn conditional_order_params_salt_ref() {
1422        let salt = B256::repeat_byte(0x42);
1423        let p = ConditionalOrderParams::new(Address::ZERO, salt, vec![]);
1424        assert_eq!(p.salt_ref(), &salt);
1425    }
1426
1427    #[test]
1428    fn conditional_order_params_display() {
1429        let p = ConditionalOrderParams::new(TWAP_HANDLER_ADDRESS, B256::ZERO, vec![]);
1430        let s = p.to_string();
1431        assert!(s.starts_with("params(handler=0x"));
1432    }
1433
1434    // ── TwapStartTime ───────────────────────────────────────────────────────
1435
1436    #[test]
1437    fn twap_start_time_as_str() {
1438        assert_eq!(TwapStartTime::AtMiningTime.as_str(), "at-mining-time");
1439        assert_eq!(TwapStartTime::At(100).as_str(), "at-unix");
1440    }
1441
1442    #[test]
1443    fn twap_start_time_is_at_mining_time() {
1444        assert!(TwapStartTime::AtMiningTime.is_at_mining_time());
1445        assert!(!TwapStartTime::At(42).is_at_mining_time());
1446    }
1447
1448    #[test]
1449    fn twap_start_time_is_fixed() {
1450        assert!(TwapStartTime::At(42).is_fixed());
1451        assert!(!TwapStartTime::AtMiningTime.is_fixed());
1452    }
1453
1454    #[test]
1455    fn twap_start_time_timestamp() {
1456        assert_eq!(TwapStartTime::AtMiningTime.timestamp(), None);
1457        assert_eq!(TwapStartTime::At(1_000).timestamp(), Some(1_000));
1458    }
1459
1460    #[test]
1461    fn twap_start_time_display() {
1462        assert_eq!(TwapStartTime::AtMiningTime.to_string(), "at-mining-time");
1463        assert_eq!(TwapStartTime::At(1_700_000_000).to_string(), "at-unix-1700000000");
1464    }
1465
1466    #[test]
1467    fn twap_start_time_from_u32() {
1468        assert_eq!(TwapStartTime::from(0u32), TwapStartTime::AtMiningTime);
1469        assert_eq!(TwapStartTime::from(42u32), TwapStartTime::At(42));
1470    }
1471
1472    #[test]
1473    fn twap_start_time_into_u32() {
1474        let zero: u32 = TwapStartTime::AtMiningTime.into();
1475        assert_eq!(zero, 0);
1476        let ts: u32 = TwapStartTime::At(999).into();
1477        assert_eq!(ts, 999);
1478    }
1479
1480    #[test]
1481    fn twap_start_time_from_option_u32() {
1482        assert_eq!(TwapStartTime::from(None), TwapStartTime::AtMiningTime);
1483        assert_eq!(TwapStartTime::from(Some(123)), TwapStartTime::At(123));
1484    }
1485
1486    // ── DurationOfPart ──────────────────────────────────────────────────────
1487
1488    #[test]
1489    fn duration_of_part_auto_defaults() {
1490        let d = DurationOfPart::default();
1491        assert!(d.is_auto());
1492        assert!(!d.is_limit_duration());
1493        assert_eq!(d.duration(), None);
1494    }
1495
1496    #[test]
1497    fn duration_of_part_limit() {
1498        let d = DurationOfPart::limit(1_800);
1499        assert!(!d.is_auto());
1500        assert!(d.is_limit_duration());
1501        assert_eq!(d.duration(), Some(1_800));
1502    }
1503
1504    #[test]
1505    fn duration_of_part_display() {
1506        assert_eq!(DurationOfPart::Auto.to_string(), "auto");
1507        assert_eq!(DurationOfPart::limit(600).to_string(), "limit-duration(600s)");
1508    }
1509
1510    #[test]
1511    fn duration_of_part_from_option() {
1512        assert_eq!(DurationOfPart::from(None), DurationOfPart::Auto);
1513        assert_eq!(
1514            DurationOfPart::from(Some(300)),
1515            DurationOfPart::LimitDuration { duration: 300 }
1516        );
1517    }
1518
1519    // ── TwapData constructors ───────────────────────────────────────────────
1520
1521    #[test]
1522    fn twap_data_sell_constructor() {
1523        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1000u64), 4, 3600);
1524        assert!(t.is_sell());
1525        assert!(!t.is_buy());
1526        assert_eq!(t.sell_amount, U256::from(1000u64));
1527        assert_eq!(t.buy_amount, U256::ZERO);
1528        assert_eq!(t.receiver, Address::ZERO);
1529        assert_eq!(t.num_parts, 4);
1530        assert_eq!(t.part_duration, 3600);
1531        assert!(t.start_time.is_at_mining_time());
1532        assert!(!t.partially_fillable);
1533        assert!(t.duration_of_part.is_auto());
1534        assert!(!t.has_app_data());
1535    }
1536
1537    #[test]
1538    fn twap_data_buy_constructor() {
1539        let t = TwapData::buy(Address::ZERO, Address::ZERO, U256::from(500u64), 2, 1800);
1540        assert!(t.is_buy());
1541        assert!(!t.is_sell());
1542        assert_eq!(t.sell_amount, U256::MAX);
1543        assert_eq!(t.buy_amount, U256::from(500u64));
1544        assert_eq!(t.num_parts, 2);
1545        assert_eq!(t.part_duration, 1800);
1546    }
1547
1548    // ── TwapData computed fields ────────────────────────────────────────────
1549
1550    #[test]
1551    fn twap_data_total_duration_secs() {
1552        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3600);
1553        assert_eq!(t.total_duration_secs(), 14_400);
1554    }
1555
1556    #[test]
1557    fn twap_data_end_time_fixed() {
1558        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3600)
1559            .with_start_time(TwapStartTime::At(1_000_000));
1560        assert_eq!(t.end_time(), Some(1_000_000 + 14_400));
1561    }
1562
1563    #[test]
1564    fn twap_data_end_time_at_mining() {
1565        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3600);
1566        assert_eq!(t.end_time(), None);
1567    }
1568
1569    #[test]
1570    fn twap_data_is_expired() {
1571        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3600)
1572            .with_start_time(TwapStartTime::At(1_000_000));
1573        // end = 1_014_400
1574        assert!(!t.is_expired(1_014_399));
1575        assert!(t.is_expired(1_014_400));
1576        assert!(t.is_expired(2_000_000));
1577    }
1578
1579    #[test]
1580    fn twap_data_is_expired_at_mining_time_always_false() {
1581        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3600);
1582        assert!(!t.is_expired(u64::MAX));
1583    }
1584
1585    // ── TwapData builders ───────────────────────────────────────────────────
1586
1587    #[test]
1588    fn twap_data_with_receiver() {
1589        let recv = Address::repeat_byte(0x01);
1590        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1).with_receiver(recv);
1591        assert_eq!(t.receiver, recv);
1592    }
1593
1594    #[test]
1595    fn twap_data_with_buy_amount() {
1596        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1)
1597            .with_buy_amount(U256::from(42u64));
1598        assert_eq!(t.buy_amount, U256::from(42u64));
1599    }
1600
1601    #[test]
1602    fn twap_data_with_sell_amount() {
1603        let t = TwapData::buy(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1)
1604            .with_sell_amount(U256::from(99u64));
1605        assert_eq!(t.sell_amount, U256::from(99u64));
1606    }
1607
1608    #[test]
1609    fn twap_data_with_start_time() {
1610        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1)
1611            .with_start_time(TwapStartTime::At(12345));
1612        assert_eq!(t.start_time, TwapStartTime::At(12345));
1613    }
1614
1615    #[test]
1616    fn twap_data_with_app_data() {
1617        let hash = B256::repeat_byte(0xAA);
1618        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1).with_app_data(hash);
1619        assert!(t.has_app_data());
1620        assert_eq!(t.app_data, hash);
1621    }
1622
1623    #[test]
1624    fn twap_data_has_app_data_zero_is_false() {
1625        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1);
1626        assert!(!t.has_app_data());
1627    }
1628
1629    #[test]
1630    fn twap_data_with_partially_fillable() {
1631        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1)
1632            .with_partially_fillable(true);
1633        assert!(t.partially_fillable);
1634    }
1635
1636    #[test]
1637    fn twap_data_with_duration_of_part() {
1638        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1)
1639            .with_duration_of_part(DurationOfPart::limit(900));
1640        assert!(t.duration_of_part.is_limit_duration());
1641        assert_eq!(t.duration_of_part.duration(), Some(900));
1642    }
1643
1644    // ── TwapData display ────────────────────────────────────────────────────
1645
1646    #[test]
1647    fn twap_data_display_at_mining_time() {
1648        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(24_000u64), 24, 3_600)
1649            .with_buy_amount(U256::from(1_000u64));
1650        let s = t.to_string();
1651        assert!(s.contains("24 × 3600s"), "got: {s}");
1652        assert!(s.contains("at-mining-time"), "got: {s}");
1653        assert!(s.contains("24000"), "got: {s}");
1654        assert!(s.contains("1000"), "got: {s}");
1655    }
1656
1657    #[test]
1658    fn twap_data_display_fixed_start() {
1659        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1_000u64), 6, 7_200)
1660            .with_start_time(TwapStartTime::At(1_700_000_000));
1661        let s = t.to_string();
1662        assert!(s.contains("at-unix-1700000000"), "got: {s}");
1663    }
1664
1665    // ── TwapStruct ──────────────────────────────────────────────────────────
1666
1667    fn make_twap_struct() -> TwapStruct {
1668        TwapStruct {
1669            sell_token: Address::ZERO,
1670            buy_token: Address::ZERO,
1671            receiver: Address::ZERO,
1672            part_sell_amount: U256::from(250u64),
1673            min_part_limit: U256::from(100u64),
1674            t0: 0,
1675            n: 4,
1676            t: 3600,
1677            span: 0,
1678            app_data: B256::ZERO,
1679        }
1680    }
1681
1682    #[test]
1683    fn twap_struct_has_app_data() {
1684        let mut s = make_twap_struct();
1685        assert!(!s.has_app_data());
1686        s.app_data = B256::repeat_byte(0x01);
1687        assert!(s.has_app_data());
1688    }
1689
1690    #[test]
1691    fn twap_struct_has_custom_receiver() {
1692        let mut s = make_twap_struct();
1693        assert!(!s.has_custom_receiver());
1694        s.receiver = Address::repeat_byte(0x01);
1695        assert!(s.has_custom_receiver());
1696    }
1697
1698    #[test]
1699    fn twap_struct_start_is_fixed() {
1700        let mut s = make_twap_struct();
1701        assert!(!s.start_is_fixed());
1702        s.t0 = 1_000_000;
1703        assert!(s.start_is_fixed());
1704    }
1705
1706    #[test]
1707    fn twap_struct_display() {
1708        let s = make_twap_struct();
1709        let d = s.to_string();
1710        assert!(d.contains("twap-struct"), "got: {d}");
1711        assert!(d.contains("4 × 3600s"), "got: {d}");
1712    }
1713
1714    // ── TwapData <-> TwapStruct conversions ─────────────────────────────────
1715
1716    #[test]
1717    fn twap_struct_try_from_twap_data() {
1718        let data = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1000u64), 4, 3600)
1719            .with_buy_amount(U256::from(400u64))
1720            .with_start_time(TwapStartTime::At(5000))
1721            .with_duration_of_part(DurationOfPart::limit(1800));
1722        let s = TwapStruct::try_from(&data).unwrap();
1723        assert_eq!(s.part_sell_amount, U256::from(250u64));
1724        assert_eq!(s.min_part_limit, U256::from(100u64));
1725        assert_eq!(s.t0, 5000);
1726        assert_eq!(s.n, 4);
1727        assert_eq!(s.t, 3600);
1728        assert_eq!(s.span, 1800);
1729    }
1730
1731    #[test]
1732    fn twap_struct_try_from_twap_data_zero_parts_errors() {
1733        let mut data = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1000u64), 4, 3600);
1734        data.num_parts = 0;
1735        assert!(TwapStruct::try_from(&data).is_err());
1736    }
1737
1738    #[test]
1739    fn twap_data_from_twap_struct() {
1740        let s = TwapStruct {
1741            sell_token: Address::ZERO,
1742            buy_token: Address::ZERO,
1743            receiver: Address::ZERO,
1744            part_sell_amount: U256::from(250u64),
1745            min_part_limit: U256::from(100u64),
1746            t0: 5000,
1747            n: 4,
1748            t: 3600,
1749            span: 1800,
1750            app_data: B256::ZERO,
1751        };
1752        let data = TwapData::from(&s);
1753        assert_eq!(data.sell_amount, U256::from(1000u64));
1754        assert_eq!(data.buy_amount, U256::from(400u64));
1755        assert_eq!(data.start_time, TwapStartTime::At(5000));
1756        assert_eq!(data.num_parts, 4);
1757        assert_eq!(data.part_duration, 3600);
1758        assert!(data.duration_of_part.is_limit_duration());
1759        assert_eq!(data.duration_of_part.duration(), Some(1800));
1760    }
1761
1762    #[test]
1763    fn twap_data_from_twap_struct_at_mining_time() {
1764        let mut s = make_twap_struct();
1765        s.t0 = 0;
1766        s.span = 0;
1767        let data = TwapData::from(&s);
1768        assert!(data.start_time.is_at_mining_time());
1769        assert!(data.duration_of_part.is_auto());
1770    }
1771
1772    // ── GpV2OrderStruct ─────────────────────────────────────────────────────
1773
1774    fn make_gpv2_order() -> GpV2OrderStruct {
1775        GpV2OrderStruct {
1776            sell_token: Address::ZERO,
1777            buy_token: Address::ZERO,
1778            receiver: Address::ZERO,
1779            sell_amount: U256::from(1000u64),
1780            buy_amount: U256::from(500u64),
1781            valid_to: 1_700_000_000,
1782            app_data: B256::ZERO,
1783            fee_amount: U256::ZERO,
1784            kind: B256::ZERO,
1785            partially_fillable: false,
1786            sell_token_balance: B256::ZERO,
1787            buy_token_balance: B256::ZERO,
1788        }
1789    }
1790
1791    #[test]
1792    fn gpv2_order_has_custom_receiver() {
1793        let mut o = make_gpv2_order();
1794        assert!(!o.has_custom_receiver());
1795        o.receiver = Address::repeat_byte(0x01);
1796        assert!(o.has_custom_receiver());
1797    }
1798
1799    #[test]
1800    fn gpv2_order_is_partially_fillable() {
1801        let mut o = make_gpv2_order();
1802        assert!(!o.is_partially_fillable());
1803        o.partially_fillable = true;
1804        assert!(o.is_partially_fillable());
1805    }
1806
1807    #[test]
1808    fn gpv2_order_display() {
1809        let o = make_gpv2_order();
1810        let s = o.to_string();
1811        assert!(s.contains("gpv2-order"), "got: {s}");
1812        assert!(s.contains("1000"), "got: {s}");
1813        assert!(s.contains("500"), "got: {s}");
1814    }
1815
1816    // ── PollResult ──────────────────────────────────────────────────────────
1817
1818    #[test]
1819    fn poll_result_success() {
1820        let r = PollResult::Success { order: None, signature: None };
1821        assert!(r.is_success());
1822        assert!(!r.is_retryable());
1823        assert!(!r.is_terminal());
1824        assert!(r.order_ref().is_none());
1825        assert!(r.as_error_message().is_none());
1826    }
1827
1828    #[test]
1829    fn poll_result_try_next_block() {
1830        let r = PollResult::TryNextBlock;
1831        assert!(r.is_try_next_block());
1832        assert!(r.is_retryable());
1833        assert!(!r.is_success());
1834        assert_eq!(r.get_block_number(), None);
1835        assert_eq!(r.get_epoch(), None);
1836    }
1837
1838    #[test]
1839    fn poll_result_try_on_block() {
1840        let r = PollResult::TryOnBlock { block_number: 42 };
1841        assert!(r.is_try_on_block());
1842        assert!(r.is_retryable());
1843        assert_eq!(r.get_block_number(), Some(42));
1844    }
1845
1846    #[test]
1847    fn poll_result_try_at_epoch() {
1848        let r = PollResult::TryAtEpoch { epoch: 1_700_000_000 };
1849        assert!(r.is_try_at_epoch());
1850        assert!(r.is_retryable());
1851        assert_eq!(r.get_epoch(), Some(1_700_000_000));
1852    }
1853
1854    #[test]
1855    fn poll_result_unexpected_error() {
1856        let r = PollResult::UnexpectedError { message: "boom".into() };
1857        assert!(r.is_unexpected_error());
1858        assert!(!r.is_retryable());
1859        assert_eq!(r.as_error_message(), Some("boom"));
1860    }
1861
1862    #[test]
1863    fn poll_result_dont_try_again() {
1864        let r = PollResult::DontTryAgain { reason: "expired".into() };
1865        assert!(r.is_dont_try_again());
1866        assert!(r.is_terminal());
1867        assert!(!r.is_retryable());
1868        assert_eq!(r.as_error_message(), Some("expired"));
1869    }
1870
1871    #[test]
1872    fn poll_result_display() {
1873        assert_eq!(PollResult::Success { order: None, signature: None }.to_string(), "success");
1874        assert_eq!(PollResult::TryNextBlock.to_string(), "try-next-block");
1875        assert_eq!(PollResult::TryOnBlock { block_number: 10 }.to_string(), "try-on-block(10)");
1876        assert_eq!(PollResult::TryAtEpoch { epoch: 99 }.to_string(), "try-at-epoch(99)");
1877        assert_eq!(
1878            PollResult::UnexpectedError { message: "x".into() }.to_string(),
1879            "unexpected-error(x)"
1880        );
1881        assert_eq!(
1882            PollResult::DontTryAgain { reason: "y".into() }.to_string(),
1883            "dont-try-again(y)"
1884        );
1885    }
1886
1887    // ── ProofLocation ───────────────────────────────────────────────────────
1888
1889    #[test]
1890    fn proof_location_as_str() {
1891        assert_eq!(ProofLocation::Private.as_str(), "private");
1892        assert_eq!(ProofLocation::Emitted.as_str(), "emitted");
1893        assert_eq!(ProofLocation::Swarm.as_str(), "swarm");
1894        assert_eq!(ProofLocation::Waku.as_str(), "waku");
1895        assert_eq!(ProofLocation::Reserved.as_str(), "reserved");
1896        assert_eq!(ProofLocation::Ipfs.as_str(), "ipfs");
1897    }
1898
1899    #[test]
1900    fn proof_location_predicates() {
1901        assert!(ProofLocation::Private.is_private());
1902        assert!(ProofLocation::Emitted.is_emitted());
1903        assert!(ProofLocation::Swarm.is_swarm());
1904        assert!(ProofLocation::Waku.is_waku());
1905        assert!(ProofLocation::Reserved.is_reserved());
1906        assert!(ProofLocation::Ipfs.is_ipfs());
1907        // Negative checks
1908        assert!(!ProofLocation::Private.is_emitted());
1909        assert!(!ProofLocation::Ipfs.is_private());
1910    }
1911
1912    #[test]
1913    fn proof_location_default_is_private() {
1914        assert_eq!(ProofLocation::default(), ProofLocation::Private);
1915    }
1916
1917    #[test]
1918    fn proof_location_display() {
1919        assert_eq!(ProofLocation::Ipfs.to_string(), "ipfs");
1920        assert_eq!(ProofLocation::Waku.to_string(), "waku");
1921    }
1922
1923    #[test]
1924    fn proof_location_try_from_u8() {
1925        assert_eq!(ProofLocation::try_from(0u8).unwrap(), ProofLocation::Private);
1926        assert_eq!(ProofLocation::try_from(1u8).unwrap(), ProofLocation::Emitted);
1927        assert_eq!(ProofLocation::try_from(5u8).unwrap(), ProofLocation::Ipfs);
1928        assert!(ProofLocation::try_from(6u8).is_err());
1929        assert!(ProofLocation::try_from(255u8).is_err());
1930    }
1931
1932    #[test]
1933    fn proof_location_try_from_str() {
1934        assert_eq!(ProofLocation::try_from("private").unwrap(), ProofLocation::Private);
1935        assert_eq!(ProofLocation::try_from("ipfs").unwrap(), ProofLocation::Ipfs);
1936        assert!(ProofLocation::try_from("unknown").is_err());
1937    }
1938
1939    #[test]
1940    fn proof_location_into_u8() {
1941        let v: u8 = ProofLocation::Private.into();
1942        assert_eq!(v, 0);
1943        let v: u8 = ProofLocation::Ipfs.into();
1944        assert_eq!(v, 5);
1945    }
1946
1947    // ── ProofStruct ─────────────────────────────────────────────────────────
1948
1949    #[test]
1950    fn proof_struct_new() {
1951        let p = ProofStruct::new(ProofLocation::Swarm, vec![1, 2, 3]);
1952        assert!(p.is_swarm());
1953        assert!(p.has_data());
1954        assert_eq!(p.data_len(), 3);
1955    }
1956
1957    #[test]
1958    fn proof_struct_private() {
1959        let p = ProofStruct::private();
1960        assert!(p.is_private());
1961        assert!(p.is_empty());
1962        assert!(!p.has_data());
1963        assert_eq!(p.data_len(), 0);
1964    }
1965
1966    #[test]
1967    fn proof_struct_emitted() {
1968        let p = ProofStruct::emitted();
1969        assert!(p.is_emitted());
1970        assert!(p.is_empty());
1971    }
1972
1973    #[test]
1974    fn proof_struct_with_location() {
1975        let p = ProofStruct::private().with_location(ProofLocation::Ipfs);
1976        assert!(p.is_ipfs());
1977    }
1978
1979    #[test]
1980    fn proof_struct_with_data() {
1981        let p = ProofStruct::private().with_data(vec![0xCA, 0xFE]);
1982        assert!(p.has_data());
1983        assert_eq!(p.data_len(), 2);
1984    }
1985
1986    #[test]
1987    fn proof_struct_delegated_predicates() {
1988        assert!(ProofStruct::new(ProofLocation::Waku, vec![]).is_waku());
1989        assert!(ProofStruct::new(ProofLocation::Reserved, vec![]).is_reserved());
1990    }
1991
1992    #[test]
1993    fn proof_struct_display() {
1994        let p = ProofStruct::private();
1995        assert_eq!(p.to_string(), "proof(private)");
1996        let p = ProofStruct::new(ProofLocation::Ipfs, vec![1]);
1997        assert_eq!(p.to_string(), "proof(ipfs)");
1998    }
1999
2000    // ── ProofLocation try_from exhaustive ────────────────────────────────
2001
2002    #[test]
2003    fn proof_location_try_from_str_all() {
2004        for (s, expected) in [
2005            ("emitted", ProofLocation::Emitted),
2006            ("swarm", ProofLocation::Swarm),
2007            ("waku", ProofLocation::Waku),
2008            ("reserved", ProofLocation::Reserved),
2009        ] {
2010            assert_eq!(ProofLocation::try_from(s).unwrap(), expected);
2011        }
2012    }
2013
2014    #[test]
2015    fn proof_location_try_from_u8_all() {
2016        assert_eq!(ProofLocation::try_from(2u8).unwrap(), ProofLocation::Swarm);
2017        assert_eq!(ProofLocation::try_from(3u8).unwrap(), ProofLocation::Waku);
2018        assert_eq!(ProofLocation::try_from(4u8).unwrap(), ProofLocation::Reserved);
2019    }
2020
2021    // ── TwapData serde roundtrip ────────────────────────────────────────
2022
2023    #[test]
2024    fn twap_data_serde_roundtrip() {
2025        let data = TwapData::sell(
2026            Address::repeat_byte(0x11),
2027            Address::repeat_byte(0x22),
2028            U256::from(1000u64),
2029            4,
2030            3600,
2031        )
2032        .with_buy_amount(U256::from(800u64))
2033        .with_start_time(TwapStartTime::At(1_000_000))
2034        .with_duration_of_part(DurationOfPart::limit(900));
2035
2036        let json = serde_json::to_string(&data).unwrap();
2037        let back: TwapData = serde_json::from_str(&json).unwrap();
2038        assert_eq!(back.sell_amount, data.sell_amount);
2039        assert_eq!(back.buy_amount, data.buy_amount);
2040        assert_eq!(back.num_parts, data.num_parts);
2041        assert_eq!(back.part_duration, data.part_duration);
2042    }
2043
2044    // ── ConditionalOrderParams serde roundtrip ──────────────────────────
2045
2046    #[test]
2047    fn conditional_order_params_serde_roundtrip() {
2048        let params = ConditionalOrderParams::new(
2049            TWAP_HANDLER_ADDRESS,
2050            B256::repeat_byte(0xAB),
2051            vec![0xDE, 0xAD, 0xBE, 0xEF],
2052        );
2053        let json = serde_json::to_string(&params).unwrap();
2054        let back: ConditionalOrderParams = serde_json::from_str(&json).unwrap();
2055        assert_eq!(back.handler, params.handler);
2056        assert_eq!(back.salt, params.salt);
2057        assert_eq!(back.static_input, params.static_input);
2058    }
2059
2060    // ── BlockInfo ───────────────────────────────────────────────────────
2061
2062    #[test]
2063    fn block_info_new() {
2064        let b = BlockInfo { block_number: 100, block_timestamp: 1_700_000_000 };
2065        assert_eq!(b.block_number, 100);
2066        assert_eq!(b.block_timestamp, 1_700_000_000);
2067    }
2068
2069    // ── GpV2OrderStruct try_from coverage ────────────────────────────────
2070
2071    #[test]
2072    fn gpv2_order_try_from_bad_kind_fails() {
2073        let o = make_gpv2_order();
2074        // kind is B256::ZERO which is neither sell nor buy hash
2075        let result = cow_signing::types::UnsignedOrder::try_from(&o);
2076        assert!(result.is_err());
2077    }
2078}
2079
2080// ── ProofStruct ───────────────────────────────────────────────────────────────
2081
2082/// On-chain `Proof` argument passed to `ComposableCow::setRoot`.
2083///
2084/// Bundles the proof location discriminant with location-specific data
2085/// (e.g. an IPFS CID, Swarm hash, or Waku message).  Pass `data: vec![]` for
2086/// [`ProofLocation::Private`] or [`ProofLocation::Emitted`].
2087#[derive(Debug, Clone)]
2088pub struct ProofStruct {
2089    /// Where the Merkle proof is stored/communicated.
2090    pub location: ProofLocation,
2091    /// Location-specific proof bytes (empty for private or emitted proofs).
2092    pub data: Vec<u8>,
2093}
2094
2095// ── Block info ──────────────────────────────────────────────────────────────
2096
2097/// Block information used for conditional order validation.
2098///
2099/// Mirrors `BlockInfo` from the `TypeScript` SDK.
2100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2101pub struct BlockInfo {
2102    /// Block number.
2103    pub block_number: u64,
2104    /// Block timestamp (Unix seconds).
2105    pub block_timestamp: u64,
2106}
2107
2108impl BlockInfo {
2109    /// Construct a new [`BlockInfo`].
2110    ///
2111    /// # Arguments
2112    ///
2113    /// * `block_number` - The block number.
2114    /// * `block_timestamp` - The block timestamp in Unix seconds.
2115    ///
2116    /// # Returns
2117    ///
2118    /// A new [`BlockInfo`] instance.
2119    #[must_use]
2120    pub const fn new(block_number: u64, block_timestamp: u64) -> Self {
2121        Self { block_number, block_timestamp }
2122    }
2123}
2124
2125impl fmt::Display for BlockInfo {
2126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2127        write!(f, "block(#{}, ts={})", self.block_number, self.block_timestamp)
2128    }
2129}
2130
2131// ── IsValidResult ───────────────────────────────────────────────────────────
2132
2133/// Result of validating a conditional order.
2134///
2135/// Mirrors the `IsValidResult` union type from the `TypeScript` SDK.
2136#[derive(Debug, Clone, PartialEq, Eq)]
2137pub enum IsValidResult {
2138    /// The order is valid.
2139    Valid,
2140    /// The order is invalid, with a reason.
2141    Invalid {
2142        /// Human-readable reason why the order is invalid.
2143        reason: String,
2144    },
2145}
2146
2147/// Type alias for the valid variant, mirroring the `TypeScript` SDK's `IsValid` interface.
2148pub type IsValid = ();
2149
2150/// Type alias for the invalid variant, mirroring the `TypeScript` SDK's `IsNotValid` interface.
2151pub type IsNotValid = String;
2152
2153impl IsValidResult {
2154    /// Returns `true` if the result represents a valid order.
2155    ///
2156    /// # Returns
2157    ///
2158    /// `true` for the [`Valid`](Self::Valid) variant, `false` for [`Invalid`](Self::Invalid).
2159    #[must_use]
2160    pub const fn is_valid(&self) -> bool {
2161        matches!(self, Self::Valid)
2162    }
2163
2164    /// Returns the reason string if the result represents an invalid order.
2165    ///
2166    /// # Returns
2167    ///
2168    /// `Some(reason)` for the [`Invalid`](Self::Invalid) variant, `None` for
2169    /// [`Valid`](Self::Valid).
2170    #[must_use]
2171    pub fn reason(&self) -> Option<&str> {
2172        match self {
2173            Self::Valid => None,
2174            Self::Invalid { reason } => Some(reason),
2175        }
2176    }
2177
2178    /// Create a valid result.
2179    ///
2180    /// # Returns
2181    ///
2182    /// An [`IsValidResult::Valid`] instance.
2183    #[must_use]
2184    pub const fn valid() -> Self {
2185        Self::Valid
2186    }
2187
2188    /// Create an invalid result with the given reason.
2189    ///
2190    /// # Arguments
2191    ///
2192    /// * `reason` - A human-readable explanation of why the order is invalid.
2193    ///
2194    /// # Returns
2195    ///
2196    /// An [`IsValidResult::Invalid`] instance containing the reason.
2197    #[must_use]
2198    pub fn invalid(reason: impl Into<String>) -> Self {
2199        Self::Invalid { reason: reason.into() }
2200    }
2201}
2202
2203impl fmt::Display for IsValidResult {
2204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2205        match self {
2206            Self::Valid => f.write_str("valid"),
2207            Self::Invalid { reason } => write!(f, "invalid: {reason}"),
2208        }
2209    }
2210}
2211
2212// ── Test helpers ────────────────────────────────────────────────────────────
2213
2214/// Default parameters for a test conditional order.
2215///
2216/// Mirrors `DEFAULT_ORDER_PARAMS` from the `TypeScript` SDK test helper.
2217pub const DEFAULT_TEST_HANDLER: &str = "0x910d00a310f7Dc5B29FE73458F47f519be547D3d";
2218
2219/// Default salt for test conditional orders.
2220pub const DEFAULT_TEST_SALT: &str =
2221    "0x9379a0bf532ff9a66ffde940f94b1a025d6f18803054c1aef52dc94b15255bbe";
2222
2223/// Parameters for creating a test conditional order.
2224///
2225/// Mirrors `TestConditionalOrderParams` from the `TypeScript` SDK.
2226#[derive(Debug, Clone)]
2227pub struct TestConditionalOrderParams {
2228    /// Handler contract address.
2229    pub handler: Address,
2230    /// 32-byte salt.
2231    pub salt: B256,
2232    /// Static input data.
2233    pub static_input: Vec<u8>,
2234    /// Whether this is a single order (true) or part of a Merkle tree (false).
2235    pub is_single_order: bool,
2236}
2237
2238impl Default for TestConditionalOrderParams {
2239    fn default() -> Self {
2240        Self {
2241            handler: DEFAULT_TEST_HANDLER.parse().map_or(Address::ZERO, |a| a),
2242            salt: DEFAULT_TEST_SALT.parse().map_or(B256::ZERO, |s| s),
2243            static_input: Vec::new(),
2244            is_single_order: true,
2245        }
2246    }
2247}
2248
2249/// Create a test [`ConditionalOrderParams`] with optional overrides.
2250///
2251/// Mirrors `createTestConditionalOrder` from the `TypeScript` SDK.
2252/// Useful in tests to quickly construct valid conditional order params.
2253///
2254/// # Example
2255///
2256/// ```rust
2257/// use cow_composable::create_test_conditional_order;
2258///
2259/// let params = create_test_conditional_order(None);
2260/// assert!(!params.handler.is_zero());
2261/// ```
2262#[must_use]
2263pub fn create_test_conditional_order(
2264    overrides: Option<TestConditionalOrderParams>,
2265) -> ConditionalOrderParams {
2266    let test = overrides.unwrap_or_default();
2267    ConditionalOrderParams {
2268        handler: test.handler,
2269        salt: test.salt,
2270        static_input: test.static_input,
2271    }
2272}
2273
2274#[cfg(test)]
2275#[allow(clippy::tests_outside_test_module, reason = "inner module pattern")]
2276mod post_definition_tests {
2277    use super::*;
2278
2279    // ── BlockInfo ───────────────────────────────────────────────────────
2280
2281    #[test]
2282    fn block_info_constructor_sets_fields() {
2283        let b = BlockInfo::new(42, 1_700_000_000);
2284        assert_eq!(b.block_number, 42);
2285        assert_eq!(b.block_timestamp, 1_700_000_000);
2286    }
2287
2288    #[test]
2289    fn block_info_display_contains_number_and_timestamp() {
2290        let b = BlockInfo::new(123, 456);
2291        let rendered = format!("{b}");
2292        assert!(rendered.contains("#123"));
2293        assert!(rendered.contains("456"));
2294    }
2295
2296    // ── IsValidResult ───────────────────────────────────────────────────
2297
2298    #[test]
2299    fn is_valid_result_constructors_and_predicates() {
2300        let ok = IsValidResult::valid();
2301        assert!(ok.is_valid());
2302        assert_eq!(ok.reason(), None);
2303
2304        let bad = IsValidResult::invalid("price too low");
2305        assert!(!bad.is_valid());
2306        assert_eq!(bad.reason(), Some("price too low"));
2307    }
2308
2309    #[test]
2310    fn is_valid_result_display_renders_both_variants() {
2311        assert_eq!(format!("{}", IsValidResult::valid()), "valid");
2312        let rendered = format!("{}", IsValidResult::invalid("bad strike"));
2313        assert!(rendered.starts_with("invalid"));
2314        assert!(rendered.contains("bad strike"));
2315    }
2316
2317    // ── TestConditionalOrderParams / create_test_conditional_order ─────
2318
2319    #[test]
2320    fn test_conditional_order_params_default_resolves_constants() {
2321        let defaults = TestConditionalOrderParams::default();
2322        // The default handler must parse to a non-zero address, and the salt
2323        // must parse to a non-zero B256 — guarding against a regression
2324        // where the fallback branches kick in silently.
2325        assert_ne!(defaults.handler, Address::ZERO);
2326        assert_ne!(defaults.salt, B256::ZERO);
2327        assert!(defaults.static_input.is_empty());
2328        assert!(defaults.is_single_order);
2329    }
2330
2331    #[test]
2332    fn create_test_conditional_order_uses_defaults_when_none() {
2333        let params = create_test_conditional_order(None);
2334        let defaults = TestConditionalOrderParams::default();
2335        assert_eq!(params.handler, defaults.handler);
2336        assert_eq!(params.salt, defaults.salt);
2337        assert_eq!(params.static_input, defaults.static_input);
2338    }
2339
2340    #[test]
2341    fn create_test_conditional_order_accepts_overrides() {
2342        let overrides = TestConditionalOrderParams {
2343            handler: Address::repeat_byte(0xAB),
2344            salt: B256::repeat_byte(0xCD),
2345            static_input: vec![1, 2, 3, 4],
2346            is_single_order: false,
2347        };
2348        let params = create_test_conditional_order(Some(overrides.clone()));
2349        assert_eq!(params.handler, overrides.handler);
2350        assert_eq!(params.salt, overrides.salt);
2351        assert_eq!(params.static_input, overrides.static_input);
2352    }
2353}