Skip to main content

cow_rs/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 crate::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_rs::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_rs::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_rs::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_rs::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_rs::{
453    ///     OrderKind,
454    ///     composable::{TwapData, TwapStartTime},
455    /// };
456    ///
457    /// let mut twap = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3_600);
458    /// twap.kind = OrderKind::Buy;
459    /// assert!(twap.is_buy());
460    /// assert!(!twap.is_sell());
461    /// ```
462    #[must_use]
463    pub const fn is_buy(&self) -> bool {
464        self.kind.is_buy()
465    }
466
467    /// Returns `true` if the `TWAP` has fully expired at the given Unix timestamp.
468    ///
469    /// Returns `false` when `start_time` is [`TwapStartTime::AtMiningTime`]
470    /// (the end time is not yet known).
471    ///
472    /// ```
473    /// use alloy_primitives::{Address, U256};
474    /// use cow_rs::composable::{TwapData, TwapStartTime};
475    ///
476    /// let twap = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3_600)
477    ///     .with_start_time(TwapStartTime::At(1_000_000));
478    /// // ends at 1_000_000 + 4 × 3600 = 1_014_400
479    /// assert!(!twap.is_expired(1_014_399));
480    /// assert!(twap.is_expired(1_014_400));
481    /// ```
482    #[must_use]
483    pub const fn is_expired(&self, timestamp: u64) -> bool {
484        match self.end_time() {
485            Some(end) => timestamp >= end,
486            None => false,
487        }
488    }
489
490    /// Create a minimal sell-kind TWAP order.
491    ///
492    /// Defaults: `receiver = Address::ZERO`, `buy_amount = U256::ZERO` (no min),
493    /// `start_time = TwapStartTime::AtMiningTime`, `app_data = B256::ZERO`,
494    /// `partially_fillable = false`, `duration_of_part = DurationOfPart::Auto`.
495    ///
496    /// Use the `with_*` builder methods to set optional fields.
497    ///
498    /// # Arguments
499    ///
500    /// * `sell_token` - Address of the token to sell.
501    /// * `buy_token` - Address of the token to buy.
502    /// * `sell_amount` - Total amount of `sell_token` to sell across all parts.
503    /// * `num_parts` - Number of parts to split the order into.
504    /// * `part_duration` - Duration of each part in seconds.
505    ///
506    /// # Returns
507    ///
508    /// A new [`TwapData`] configured as a sell order with sensible defaults.
509    #[must_use]
510    pub const fn sell(
511        sell_token: Address,
512        buy_token: Address,
513        sell_amount: U256,
514        num_parts: u32,
515        part_duration: u32,
516    ) -> Self {
517        Self {
518            sell_token,
519            buy_token,
520            receiver: Address::ZERO,
521            sell_amount,
522            buy_amount: U256::ZERO,
523            start_time: TwapStartTime::AtMiningTime,
524            part_duration,
525            num_parts,
526            app_data: B256::ZERO,
527            partially_fillable: false,
528            kind: OrderKind::Sell,
529            duration_of_part: DurationOfPart::Auto,
530        }
531    }
532
533    /// Create a minimal buy-kind TWAP order.
534    ///
535    /// Defaults: `receiver = Address::ZERO`, `sell_amount = U256::MAX` (unlimited),
536    /// `start_time = TwapStartTime::AtMiningTime`, `app_data = B256::ZERO`,
537    /// `partially_fillable = false`, `duration_of_part = DurationOfPart::Auto`.
538    ///
539    /// Use the `with_*` builder methods to set optional fields.
540    ///
541    /// # Arguments
542    ///
543    /// * `sell_token` - Address of the token to sell.
544    /// * `buy_token` - Address of the token to buy.
545    /// * `buy_amount` - Minimum total amount of `buy_token` to receive across all parts.
546    /// * `num_parts` - Number of parts to split the order into.
547    /// * `part_duration` - Duration of each part in seconds.
548    ///
549    /// # Returns
550    ///
551    /// A new [`TwapData`] configured as a buy order with sensible defaults.
552    #[must_use]
553    pub const fn buy(
554        sell_token: Address,
555        buy_token: Address,
556        buy_amount: U256,
557        num_parts: u32,
558        part_duration: u32,
559    ) -> Self {
560        Self {
561            sell_token,
562            buy_token,
563            receiver: Address::ZERO,
564            sell_amount: U256::MAX,
565            buy_amount,
566            start_time: TwapStartTime::AtMiningTime,
567            part_duration,
568            num_parts,
569            app_data: B256::ZERO,
570            partially_fillable: false,
571            kind: OrderKind::Buy,
572            duration_of_part: DurationOfPart::Auto,
573        }
574    }
575
576    /// Set the receiver address for bought tokens.
577    ///
578    /// [`Address::ZERO`] means the order owner (default).
579    ///
580    /// # Returns
581    ///
582    /// The modified [`TwapData`] with the updated receiver (builder pattern).
583    #[must_use]
584    pub const fn with_receiver(mut self, receiver: Address) -> Self {
585        self.receiver = receiver;
586        self
587    }
588
589    /// Set the minimum amount of `buy_token` to receive across all parts.
590    ///
591    /// Useful when building a sell-kind order to set a price floor.
592    ///
593    /// # Returns
594    ///
595    /// The modified [`TwapData`] with the updated buy amount (builder pattern).
596    #[must_use]
597    pub const fn with_buy_amount(mut self, buy_amount: U256) -> Self {
598        self.buy_amount = buy_amount;
599        self
600    }
601
602    /// Set the maximum amount of `sell_token` to sell across all parts.
603    ///
604    /// Useful when building a buy-kind order to cap spending.
605    ///
606    /// # Returns
607    ///
608    /// The modified [`TwapData`] with the updated sell amount (builder pattern).
609    #[must_use]
610    pub const fn with_sell_amount(mut self, sell_amount: U256) -> Self {
611        self.sell_amount = sell_amount;
612        self
613    }
614
615    /// Set when the TWAP order starts executing.
616    ///
617    /// # Returns
618    ///
619    /// The modified [`TwapData`] with the updated start time (builder pattern).
620    #[must_use]
621    pub const fn with_start_time(mut self, start_time: TwapStartTime) -> Self {
622        self.start_time = start_time;
623        self
624    }
625
626    /// Attach an app-data hash to the order.
627    ///
628    /// # Returns
629    ///
630    /// The modified [`TwapData`] with the updated app-data hash (builder pattern).
631    #[must_use]
632    pub const fn with_app_data(mut self, app_data: B256) -> Self {
633        self.app_data = app_data;
634        self
635    }
636
637    /// Allow each individual part to be partially filled.
638    ///
639    /// # Returns
640    ///
641    /// The modified [`TwapData`] with the updated partial-fill setting (builder pattern).
642    #[must_use]
643    pub const fn with_partially_fillable(mut self, partially_fillable: bool) -> Self {
644        self.partially_fillable = partially_fillable;
645        self
646    }
647
648    /// Restrict each part to a shorter validity window within its overall interval.
649    ///
650    /// # Returns
651    ///
652    /// The modified [`TwapData`] with the updated duration-of-part setting (builder pattern).
653    #[must_use]
654    pub const fn with_duration_of_part(mut self, duration_of_part: DurationOfPart) -> Self {
655        self.duration_of_part = duration_of_part;
656        self
657    }
658
659    /// Returns `true` if a non-zero app-data hash is attached.
660    ///
661    /// The zero hash (`B256::ZERO`) means no app-data was set.
662    ///
663    /// ```
664    /// use alloy_primitives::{Address, B256, U256};
665    /// use cow_rs::composable::TwapData;
666    ///
667    /// let twap = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3_600);
668    /// assert!(!twap.has_app_data());
669    ///
670    /// let with_data = twap.with_app_data(B256::repeat_byte(0x01));
671    /// assert!(with_data.has_app_data());
672    /// ```
673    #[must_use]
674    pub fn has_app_data(&self) -> bool {
675        !self.app_data.is_zero()
676    }
677}
678
679/// On-chain `TwapStruct` representation with per-part amounts.
680///
681/// This mirrors the Solidity struct passed to the handler as `staticInput`.
682/// Use [`TwapData`] for the user-facing SDK type; use `TwapStruct` only when
683/// you need direct access to the ABI-level fields.
684#[derive(Debug, Clone)]
685pub struct TwapStruct {
686    /// Token to sell.
687    pub sell_token: Address,
688    /// Token to buy.
689    pub buy_token: Address,
690    /// Receiver of bought tokens.
691    pub receiver: Address,
692    /// Amount of `sell_token` to sell in **each** part (not total).
693    pub part_sell_amount: U256,
694    /// Minimum amount of `buy_token` to buy in **each** part.
695    pub min_part_limit: U256,
696    /// Start timestamp (`0` = use `CurrentBlockTimestampFactory`).
697    pub t0: u32,
698    /// Number of parts.
699    pub n: u32,
700    /// Duration of each part in seconds.
701    pub t: u32,
702    /// Part validity window in seconds (`0` = full window).
703    pub span: u32,
704    /// App-data hash.
705    pub app_data: B256,
706}
707
708impl TwapStruct {
709    /// Returns `true` if a non-zero app-data hash is set.
710    ///
711    /// The zero hash (`B256::ZERO`) means no app-data was attached.
712    ///
713    /// # Returns
714    ///
715    /// `true` if the `app_data` field is not [`B256::ZERO`], `false` otherwise.
716    #[must_use]
717    pub fn has_app_data(&self) -> bool {
718        !self.app_data.is_zero()
719    }
720
721    /// Returns `true` if the receiver is not the zero address.
722    ///
723    /// When `receiver == Address::ZERO`, the settlement contract uses the order
724    /// owner as the effective receiver.
725    ///
726    /// # Returns
727    ///
728    /// `true` if `receiver` is not [`Address::ZERO`], `false` otherwise.
729    #[must_use]
730    pub fn has_custom_receiver(&self) -> bool {
731        !self.receiver.is_zero()
732    }
733
734    /// Returns `true` if a fixed start timestamp is set (`t0 != 0`).
735    ///
736    /// When `t0 == 0`, the order uses `CurrentBlockTimestampFactory` to
737    /// determine the start time at mining time.
738    ///
739    /// # Returns
740    ///
741    /// `true` if `t0` is non-zero, `false` otherwise.
742    #[must_use]
743    pub const fn start_is_fixed(&self) -> bool {
744        self.t0 != 0
745    }
746}
747
748impl TryFrom<&TwapData> for TwapStruct {
749    type Error = crate::CowError;
750
751    /// Convert a high-level [`TwapData`] into the ABI-level [`TwapStruct`].
752    ///
753    /// Delegates to [`crate::composable::data_to_struct`].
754    fn try_from(d: &TwapData) -> Result<Self, Self::Error> {
755        crate::composable::data_to_struct(d)
756    }
757}
758
759impl From<&TwapStruct> for TwapData {
760    /// Convert an ABI-level [`TwapStruct`] back into a high-level [`TwapData`].
761    ///
762    /// Delegates to [`crate::composable::struct_to_data`].
763    fn from(s: &TwapStruct) -> Self {
764        crate::composable::struct_to_data(s)
765    }
766}
767
768impl fmt::Display for TwapStruct {
769    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
770        write!(
771            f,
772            "twap-struct {} × {}s sell {} {:#x} → ≥{} {:#x}",
773            self.n,
774            self.t,
775            self.part_sell_amount,
776            self.sell_token,
777            self.min_part_limit,
778            self.buy_token,
779        )
780    }
781}
782
783// ── GpV2OrderStruct ───────────────────────────────────────────────────────────
784
785/// Raw on-chain `GPv2Order.DataStruct` as emitted by the `GPv2Settlement` contract.
786///
787/// Unlike [`UnsignedOrder`](crate::order_signing::types::UnsignedOrder), the
788/// `kind`, `sell_token_balance`, and `buy_token_balance` fields are stored as
789/// `keccak256` hashes rather than typed enums.
790///
791/// Use [`from_struct_to_order`](crate::composable::from_struct_to_order) to
792/// decode them into a fully typed
793/// [`UnsignedOrder`](crate::order_signing::types::UnsignedOrder).
794///
795/// Mirrors `GPv2Order.DataStruct` from the `@cowprotocol/composable` SDK.
796#[derive(Debug, Clone)]
797pub struct GpV2OrderStruct {
798    /// Token to sell.
799    pub sell_token: Address,
800    /// Token to buy.
801    pub buy_token: Address,
802    /// Address that receives the bought tokens.
803    pub receiver: Address,
804    /// Amount of `sell_token` to sell (in atoms).
805    pub sell_amount: U256,
806    /// Minimum amount of `buy_token` to receive (in atoms).
807    pub buy_amount: U256,
808    /// Order expiry as a Unix timestamp.
809    pub valid_to: u32,
810    /// App-data hash (`bytes32`).
811    pub app_data: B256,
812    /// Protocol fee included in `sell_amount` (in atoms).
813    pub fee_amount: U256,
814    /// `keccak256("sell")` or `keccak256("buy")`.
815    pub kind: B256,
816    /// Whether the order may be partially filled.
817    pub partially_fillable: bool,
818    /// `keccak256("erc20")`, `keccak256("external")`, or `keccak256("internal")`.
819    pub sell_token_balance: B256,
820    /// `keccak256("erc20")`, `keccak256("external")`, or `keccak256("internal")`.
821    pub buy_token_balance: B256,
822}
823impl fmt::Display for GpV2OrderStruct {
824    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
825        write!(
826            f,
827            "gpv2-order({:#x} sell={} → {:#x} buy={})",
828            self.sell_token, self.sell_amount, self.buy_token, self.buy_amount
829        )
830    }
831}
832
833impl GpV2OrderStruct {
834    /// Returns `true` if the receiver is not the zero address.
835    ///
836    /// When `receiver == Address::ZERO`, the settlement contract uses the order
837    /// owner as the effective receiver.
838    ///
839    /// # Returns
840    ///
841    /// `true` if `receiver` is not [`Address::ZERO`], `false` otherwise.
842    #[must_use]
843    pub fn has_custom_receiver(&self) -> bool {
844        !self.receiver.is_zero()
845    }
846
847    /// Returns `true` if this order allows partial fills.
848    ///
849    /// # Returns
850    ///
851    /// `true` if `partially_fillable` is set, `false` for fill-or-kill orders.
852    #[must_use]
853    pub const fn is_partially_fillable(&self) -> bool {
854        self.partially_fillable
855    }
856}
857
858impl TryFrom<&GpV2OrderStruct> for crate::order_signing::types::UnsignedOrder {
859    type Error = crate::CowError;
860
861    /// Decode a raw [`GpV2OrderStruct`] into a fully typed `UnsignedOrder`.
862    ///
863    /// Resolves the hashed `kind`, `sell_token_balance`, and `buy_token_balance`
864    /// fields back into their enum representations via
865    /// [`crate::composable::from_struct_to_order`].
866    fn try_from(s: &GpV2OrderStruct) -> Result<Self, Self::Error> {
867        crate::composable::from_struct_to_order(s)
868    }
869}
870
871// ── PollResult ────────────────────────────────────────────────────────────────
872
873/// Result returned when polling a conditional order for tradability.
874///
875/// On `Success`, contains the on-chain order struct and the pre-signature bytes
876/// ready for submission to the orderbook.
877#[derive(Debug, Clone)]
878pub enum PollResult {
879    /// The order is valid and can be submitted now.
880    ///
881    /// When returned by a full signing poll, `order` and `signature` are set to
882    /// the resolved `GPv2Order.Data` struct and the ABI-encoded signature.
883    /// When returned by an offline validity check (e.g. `TwapOrder::poll_validate`),
884    /// both fields are `None`.
885    Success {
886        /// The resolved order ready for submission (`None` for offline checks).
887        order: Option<crate::order_signing::types::UnsignedOrder>,
888        /// Hex-encoded signature bytes, `0x`-prefixed (`None` for offline checks).
889        signature: Option<String>,
890    },
891    /// Retry on the next block.
892    TryNextBlock,
893    /// Retry once the given block number is reached.
894    TryOnBlock {
895        /// Target block number.
896        block_number: u64,
897    },
898    /// Retry once the given Unix timestamp is reached.
899    TryAtEpoch {
900        /// Target Unix timestamp in seconds.
901        epoch: u64,
902    },
903    /// An unexpected error occurred.
904    UnexpectedError {
905        /// Human-readable error description.
906        message: String,
907    },
908    /// This order should never be polled again.
909    DontTryAgain {
910        /// Reason the order is permanently inactive.
911        reason: String,
912    },
913}
914
915impl PollResult {
916    /// Returns `true` if the order is ready to be submitted.
917    ///
918    /// # Returns
919    ///
920    /// `true` for the [`Success`](Self::Success) variant, `false` for all others.
921    #[must_use]
922    pub const fn is_success(&self) -> bool {
923        matches!(self, Self::Success { .. })
924    }
925
926    /// Returns `true` if polling should be retried in a future block or epoch.
927    ///
928    /// # Returns
929    ///
930    /// `true` for [`TryNextBlock`](Self::TryNextBlock), [`TryOnBlock`](Self::TryOnBlock),
931    /// or [`TryAtEpoch`](Self::TryAtEpoch); `false` otherwise.
932    #[must_use]
933    pub const fn is_retryable(&self) -> bool {
934        matches!(self, Self::TryNextBlock | Self::TryOnBlock { .. } | Self::TryAtEpoch { .. })
935    }
936
937    /// Returns `true` if this order should never be polled again.
938    ///
939    /// # Returns
940    ///
941    /// `true` for the [`DontTryAgain`](Self::DontTryAgain) variant, `false` otherwise.
942    #[must_use]
943    pub const fn is_terminal(&self) -> bool {
944        matches!(self, Self::DontTryAgain { .. })
945    }
946
947    /// Returns `true` if polling should retry on the very next block.
948    ///
949    /// # Returns
950    ///
951    /// `true` for the [`TryNextBlock`](Self::TryNextBlock) variant, `false` otherwise.
952    #[must_use]
953    pub const fn is_try_next_block(&self) -> bool {
954        matches!(self, Self::TryNextBlock)
955    }
956
957    /// Returns `true` if polling should retry once a specific block is reached.
958    ///
959    /// # Returns
960    ///
961    /// `true` for the [`TryOnBlock`](Self::TryOnBlock) variant, `false` otherwise.
962    #[must_use]
963    pub const fn is_try_on_block(&self) -> bool {
964        matches!(self, Self::TryOnBlock { .. })
965    }
966
967    /// Returns `true` if polling should retry once a specific Unix epoch is reached.
968    ///
969    /// # Returns
970    ///
971    /// `true` for the [`TryAtEpoch`](Self::TryAtEpoch) variant, `false` otherwise.
972    #[must_use]
973    pub const fn is_try_at_epoch(&self) -> bool {
974        matches!(self, Self::TryAtEpoch { .. })
975    }
976
977    /// Returns `true` if an unexpected error occurred during polling.
978    ///
979    /// # Returns
980    ///
981    /// `true` for the [`UnexpectedError`](Self::UnexpectedError) variant, `false` otherwise.
982    #[must_use]
983    pub const fn is_unexpected_error(&self) -> bool {
984        matches!(self, Self::UnexpectedError { .. })
985    }
986
987    /// Returns `true` if this order should never be polled again (terminal failure).
988    ///
989    /// # Returns
990    ///
991    /// `true` for the [`DontTryAgain`](Self::DontTryAgain) variant, `false` otherwise.
992    #[must_use]
993    pub const fn is_dont_try_again(&self) -> bool {
994        matches!(self, Self::DontTryAgain { .. })
995    }
996
997    /// Extract the target block number from a [`TryOnBlock`](Self::TryOnBlock) variant.
998    ///
999    /// Returns `None` for all other variants.
1000    ///
1001    /// ```
1002    /// use cow_rs::composable::PollResult;
1003    ///
1004    /// let r = PollResult::TryOnBlock { block_number: 12_345_678 };
1005    /// assert_eq!(r.get_block_number(), Some(12_345_678));
1006    /// assert_eq!(PollResult::TryNextBlock.get_block_number(), None);
1007    /// ```
1008    #[must_use]
1009    pub const fn get_block_number(&self) -> Option<u64> {
1010        if let Self::TryOnBlock { block_number } = self { Some(*block_number) } else { None }
1011    }
1012
1013    /// Extract the target Unix epoch from a [`TryAtEpoch`](Self::TryAtEpoch) variant.
1014    ///
1015    /// Returns `None` for all other variants.
1016    ///
1017    /// ```
1018    /// use cow_rs::composable::PollResult;
1019    ///
1020    /// let r = PollResult::TryAtEpoch { epoch: 1_700_000_000 };
1021    /// assert_eq!(r.get_epoch(), Some(1_700_000_000));
1022    /// assert_eq!(PollResult::TryNextBlock.get_epoch(), None);
1023    /// ```
1024    #[must_use]
1025    pub const fn get_epoch(&self) -> Option<u64> {
1026        if let Self::TryAtEpoch { epoch } = self { Some(*epoch) } else { None }
1027    }
1028
1029    /// Extract the resolved [`UnsignedOrder`](crate::order_signing::types::UnsignedOrder)
1030    /// from a [`PollResult::Success`] variant, if present.
1031    ///
1032    /// Returns `None` for all other variants, or when `order` is `None`
1033    /// inside `Success` (e.g. an offline validity check result).
1034    #[must_use]
1035    pub const fn order_ref(&self) -> Option<&crate::order_signing::types::UnsignedOrder> {
1036        if let Self::Success { order, .. } = self { order.as_ref() } else { None }
1037    }
1038
1039    /// Extract the error message from an [`UnexpectedError`](Self::UnexpectedError)
1040    /// or [`DontTryAgain`](Self::DontTryAgain) variant.
1041    ///
1042    /// Returns `None` for all other variants.
1043    #[must_use]
1044    pub const fn as_error_message(&self) -> Option<&str> {
1045        match self {
1046            Self::UnexpectedError { message } => Some(message.as_str()),
1047            Self::DontTryAgain { reason } => Some(reason.as_str()),
1048            Self::Success { .. } |
1049            Self::TryNextBlock |
1050            Self::TryOnBlock { .. } |
1051            Self::TryAtEpoch { .. } => None,
1052        }
1053    }
1054}
1055
1056impl fmt::Display for PollResult {
1057    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1058        match self {
1059            Self::Success { .. } => f.write_str("success"),
1060            Self::TryNextBlock => f.write_str("try-next-block"),
1061            Self::TryOnBlock { block_number } => write!(f, "try-on-block({block_number})"),
1062            Self::TryAtEpoch { epoch } => write!(f, "try-at-epoch({epoch})"),
1063            Self::UnexpectedError { message } => write!(f, "unexpected-error({message})"),
1064            Self::DontTryAgain { reason } => write!(f, "dont-try-again({reason})"),
1065        }
1066    }
1067}
1068
1069// ── ProofLocation ─────────────────────────────────────────────────────────────
1070
1071/// Where the Merkle proof for a conditional order is stored / communicated.
1072#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1073#[repr(u8)]
1074pub enum ProofLocation {
1075    /// Proof is kept private; only the owner polls.
1076    #[default]
1077    Private = 0,
1078    /// Proof emitted as an on-chain event.
1079    Emitted = 1,
1080    /// Proof stored on Swarm.
1081    Swarm = 2,
1082    /// Proof communicated via Waku.
1083    Waku = 3,
1084    /// Reserved for future use.
1085    Reserved = 4,
1086    /// Proof stored on IPFS.
1087    Ipfs = 5,
1088}
1089
1090impl ProofLocation {
1091    /// Returns a lowercase string label for the proof location.
1092    #[must_use]
1093    pub const fn as_str(self) -> &'static str {
1094        match self {
1095            Self::Private => "private",
1096            Self::Emitted => "emitted",
1097            Self::Swarm => "swarm",
1098            Self::Waku => "waku",
1099            Self::Reserved => "reserved",
1100            Self::Ipfs => "ipfs",
1101        }
1102    }
1103
1104    /// Returns `true` if the proof is kept private (owner-polled only).
1105    #[must_use]
1106    pub const fn is_private(self) -> bool {
1107        matches!(self, Self::Private)
1108    }
1109
1110    /// Returns `true` if the proof is emitted as an on-chain event.
1111    #[must_use]
1112    pub const fn is_emitted(self) -> bool {
1113        matches!(self, Self::Emitted)
1114    }
1115
1116    /// Returns `true` if the proof is stored on Swarm.
1117    #[must_use]
1118    pub const fn is_swarm(self) -> bool {
1119        matches!(self, Self::Swarm)
1120    }
1121
1122    /// Returns `true` if the proof is communicated via Waku.
1123    #[must_use]
1124    pub const fn is_waku(self) -> bool {
1125        matches!(self, Self::Waku)
1126    }
1127
1128    /// Returns `true` if this location is the reserved (future-use) discriminant.
1129    #[must_use]
1130    pub const fn is_reserved(self) -> bool {
1131        matches!(self, Self::Reserved)
1132    }
1133
1134    /// Returns `true` if the proof is stored on IPFS.
1135    #[must_use]
1136    pub const fn is_ipfs(self) -> bool {
1137        matches!(self, Self::Ipfs)
1138    }
1139}
1140
1141impl fmt::Display for ProofLocation {
1142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1143        f.write_str(self.as_str())
1144    }
1145}
1146
1147impl TryFrom<u8> for ProofLocation {
1148    type Error = crate::CowError;
1149
1150    /// Parse a [`ProofLocation`] from its on-chain `uint8` discriminant.
1151    fn try_from(n: u8) -> Result<Self, Self::Error> {
1152        match n {
1153            0 => Ok(Self::Private),
1154            1 => Ok(Self::Emitted),
1155            2 => Ok(Self::Swarm),
1156            3 => Ok(Self::Waku),
1157            4 => Ok(Self::Reserved),
1158            5 => Ok(Self::Ipfs),
1159            other => Err(crate::CowError::Parse {
1160                field: "ProofLocation",
1161                reason: format!("unknown discriminant: {other}"),
1162            }),
1163        }
1164    }
1165}
1166
1167impl TryFrom<&str> for ProofLocation {
1168    type Error = crate::CowError;
1169
1170    /// Parse a [`ProofLocation`] from its string label.
1171    fn try_from(s: &str) -> Result<Self, Self::Error> {
1172        match s {
1173            "private" => Ok(Self::Private),
1174            "emitted" => Ok(Self::Emitted),
1175            "swarm" => Ok(Self::Swarm),
1176            "waku" => Ok(Self::Waku),
1177            "reserved" => Ok(Self::Reserved),
1178            "ipfs" => Ok(Self::Ipfs),
1179            other => Err(crate::CowError::Parse {
1180                field: "ProofLocation",
1181                reason: format!("unknown value: {other}"),
1182            }),
1183        }
1184    }
1185}
1186
1187impl From<ProofLocation> for u8 {
1188    /// Encode a [`ProofLocation`] as its on-chain `uint8` discriminant.
1189    ///
1190    /// This is the inverse of [`TryFrom<u8>`] for [`ProofLocation`].
1191    fn from(loc: ProofLocation) -> Self {
1192        loc as Self
1193    }
1194}
1195
1196impl ProofStruct {
1197    /// Construct a [`ProofStruct`] with the given location and data bytes.
1198    ///
1199    /// # Arguments
1200    ///
1201    /// * `location` - Where the Merkle proof is stored or communicated.
1202    /// * `data` - Location-specific proof bytes (empty for private or emitted proofs).
1203    ///
1204    /// # Returns
1205    ///
1206    /// A new [`ProofStruct`] instance.
1207    #[must_use]
1208    pub const fn new(location: ProofLocation, data: Vec<u8>) -> Self {
1209        Self { location, data }
1210    }
1211
1212    /// A private proof (no location data needed).
1213    ///
1214    /// # Returns
1215    ///
1216    /// A [`ProofStruct`] with [`ProofLocation::Private`] and empty data.
1217    #[must_use]
1218    pub const fn private() -> Self {
1219        Self { location: ProofLocation::Private, data: Vec::new() }
1220    }
1221
1222    /// An emitted proof (no location data needed — the proof is in the tx log).
1223    ///
1224    /// # Returns
1225    ///
1226    /// A [`ProofStruct`] with [`ProofLocation::Emitted`] and empty data.
1227    #[must_use]
1228    pub const fn emitted() -> Self {
1229        Self { location: ProofLocation::Emitted, data: Vec::new() }
1230    }
1231
1232    /// Override the proof location.
1233    ///
1234    /// # Returns
1235    ///
1236    /// The modified [`ProofStruct`] with the updated location (builder pattern).
1237    #[must_use]
1238    pub const fn with_location(mut self, location: ProofLocation) -> Self {
1239        self.location = location;
1240        self
1241    }
1242
1243    /// Override the location-specific proof data bytes.
1244    ///
1245    /// # Returns
1246    ///
1247    /// The modified [`ProofStruct`] with the updated data (builder pattern).
1248    #[must_use]
1249    pub fn with_data(mut self, data: Vec<u8>) -> Self {
1250        self.data = data;
1251        self
1252    }
1253
1254    /// Returns `true` if the proof location is [`ProofLocation::Private`].
1255    ///
1256    /// # Returns
1257    ///
1258    /// `true` if `location` is [`ProofLocation::Private`], `false` otherwise.
1259    #[must_use]
1260    pub const fn is_private(&self) -> bool {
1261        self.location.is_private()
1262    }
1263
1264    /// Returns `true` if the proof location is [`ProofLocation::Emitted`].
1265    ///
1266    /// # Returns
1267    ///
1268    /// `true` if `location` is [`ProofLocation::Emitted`], `false` otherwise.
1269    #[must_use]
1270    pub const fn is_emitted(&self) -> bool {
1271        self.location.is_emitted()
1272    }
1273
1274    /// Returns `true` if the proof location is [`ProofLocation::Swarm`].
1275    ///
1276    /// # Returns
1277    ///
1278    /// `true` if `location` is [`ProofLocation::Swarm`], `false` otherwise.
1279    #[must_use]
1280    pub const fn is_swarm(&self) -> bool {
1281        self.location.is_swarm()
1282    }
1283
1284    /// Returns `true` if the proof location is [`ProofLocation::Waku`].
1285    ///
1286    /// # Returns
1287    ///
1288    /// `true` if `location` is [`ProofLocation::Waku`], `false` otherwise.
1289    #[must_use]
1290    pub const fn is_waku(&self) -> bool {
1291        self.location.is_waku()
1292    }
1293
1294    /// Returns `true` if the proof location is [`ProofLocation::Ipfs`].
1295    ///
1296    /// # Returns
1297    ///
1298    /// `true` if `location` is [`ProofLocation::Ipfs`], `false` otherwise.
1299    #[must_use]
1300    pub const fn is_ipfs(&self) -> bool {
1301        self.location.is_ipfs()
1302    }
1303
1304    /// Returns `true` if the proof location is [`ProofLocation::Reserved`].
1305    ///
1306    /// # Returns
1307    ///
1308    /// `true` if `location` is [`ProofLocation::Reserved`], `false` otherwise.
1309    #[must_use]
1310    pub const fn is_reserved(&self) -> bool {
1311        self.location.is_reserved()
1312    }
1313
1314    /// Returns `true` if this proof has non-empty data bytes.
1315    ///
1316    /// [`ProofLocation::Private`] and [`ProofLocation::Emitted`] proofs carry no
1317    /// data; IPFS, Swarm, and Waku proofs carry location-specific bytes.
1318    #[must_use]
1319    pub const fn has_data(&self) -> bool {
1320        !self.data.is_empty()
1321    }
1322
1323    /// Returns `true` if this proof has no data bytes (complement of [`has_data`](Self::has_data)).
1324    #[must_use]
1325    pub const fn is_empty(&self) -> bool {
1326        self.data.is_empty()
1327    }
1328
1329    /// Returns the number of data bytes in this proof.
1330    #[must_use]
1331    pub const fn data_len(&self) -> usize {
1332        self.data.len()
1333    }
1334}
1335
1336impl fmt::Display for ProofStruct {
1337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1338        write!(f, "proof({})", self.location)
1339    }
1340}
1341
1342// ── Tests ─────────────────────────────────────────────────────────────────────
1343
1344#[cfg(test)]
1345mod tests {
1346    use super::*;
1347
1348    // ── Constants ────────────────────────────────────────────────────────────
1349
1350    #[test]
1351    fn composable_cow_address_matches() {
1352        assert_eq!(
1353            format!("{COMPOSABLE_COW_ADDRESS:#x}"),
1354            "0xfdafc9d1902f4e0b84f65f49f244b32b31013b74"
1355        );
1356    }
1357
1358    #[test]
1359    fn twap_handler_address_matches() {
1360        assert_eq!(
1361            format!("{TWAP_HANDLER_ADDRESS:#x}"),
1362            "0x6cf1e9ca41f7611def408122793c358a3d11e5a5"
1363        );
1364    }
1365
1366    #[test]
1367    fn current_block_timestamp_factory_address_matches() {
1368        assert_eq!(
1369            format!("{CURRENT_BLOCK_TIMESTAMP_FACTORY_ADDRESS:#x}"),
1370            "0x52ed56da04309aca4c3fecc595298d80c2f16bac"
1371        );
1372    }
1373
1374    #[test]
1375    fn max_frequency_is_one_year() {
1376        assert_eq!(MAX_FREQUENCY, 365 * 24 * 60 * 60);
1377        assert_eq!(MAX_FREQUENCY, 31_536_000);
1378    }
1379
1380    // ── ConditionalOrderParams ──────────────────────────────────────────────
1381
1382    #[test]
1383    fn conditional_order_params_new() {
1384        let p = ConditionalOrderParams::new(Address::ZERO, B256::ZERO, vec![1, 2, 3]);
1385        assert_eq!(p.handler, Address::ZERO);
1386        assert_eq!(p.salt, B256::ZERO);
1387        assert_eq!(p.static_input, vec![1, 2, 3]);
1388    }
1389
1390    #[test]
1391    fn conditional_order_params_with_handler() {
1392        let p = ConditionalOrderParams::new(Address::ZERO, B256::ZERO, vec![])
1393            .with_handler(TWAP_HANDLER_ADDRESS);
1394        assert_eq!(p.handler, TWAP_HANDLER_ADDRESS);
1395    }
1396
1397    #[test]
1398    fn conditional_order_params_with_salt() {
1399        let salt = B256::repeat_byte(0xAB);
1400        let p = ConditionalOrderParams::new(Address::ZERO, B256::ZERO, vec![]).with_salt(salt);
1401        assert_eq!(p.salt, salt);
1402    }
1403
1404    #[test]
1405    fn conditional_order_params_with_static_input() {
1406        let p = ConditionalOrderParams::new(Address::ZERO, B256::ZERO, vec![])
1407            .with_static_input(vec![0xDE, 0xAD]);
1408        assert_eq!(p.static_input, vec![0xDE, 0xAD]);
1409    }
1410
1411    #[test]
1412    fn conditional_order_params_empty_static_input() {
1413        let empty = ConditionalOrderParams::new(Address::ZERO, B256::ZERO, vec![]);
1414        assert!(empty.is_empty_static_input());
1415        assert_eq!(empty.static_input_len(), 0);
1416
1417        let non_empty = empty.with_static_input(vec![1]);
1418        assert!(!non_empty.is_empty_static_input());
1419        assert_eq!(non_empty.static_input_len(), 1);
1420    }
1421
1422    #[test]
1423    fn conditional_order_params_salt_ref() {
1424        let salt = B256::repeat_byte(0x42);
1425        let p = ConditionalOrderParams::new(Address::ZERO, salt, vec![]);
1426        assert_eq!(p.salt_ref(), &salt);
1427    }
1428
1429    #[test]
1430    fn conditional_order_params_display() {
1431        let p = ConditionalOrderParams::new(TWAP_HANDLER_ADDRESS, B256::ZERO, vec![]);
1432        let s = p.to_string();
1433        assert!(s.starts_with("params(handler=0x"));
1434    }
1435
1436    // ── TwapStartTime ───────────────────────────────────────────────────────
1437
1438    #[test]
1439    fn twap_start_time_as_str() {
1440        assert_eq!(TwapStartTime::AtMiningTime.as_str(), "at-mining-time");
1441        assert_eq!(TwapStartTime::At(100).as_str(), "at-unix");
1442    }
1443
1444    #[test]
1445    fn twap_start_time_is_at_mining_time() {
1446        assert!(TwapStartTime::AtMiningTime.is_at_mining_time());
1447        assert!(!TwapStartTime::At(42).is_at_mining_time());
1448    }
1449
1450    #[test]
1451    fn twap_start_time_is_fixed() {
1452        assert!(TwapStartTime::At(42).is_fixed());
1453        assert!(!TwapStartTime::AtMiningTime.is_fixed());
1454    }
1455
1456    #[test]
1457    fn twap_start_time_timestamp() {
1458        assert_eq!(TwapStartTime::AtMiningTime.timestamp(), None);
1459        assert_eq!(TwapStartTime::At(1_000).timestamp(), Some(1_000));
1460    }
1461
1462    #[test]
1463    fn twap_start_time_display() {
1464        assert_eq!(TwapStartTime::AtMiningTime.to_string(), "at-mining-time");
1465        assert_eq!(TwapStartTime::At(1_700_000_000).to_string(), "at-unix-1700000000");
1466    }
1467
1468    #[test]
1469    fn twap_start_time_from_u32() {
1470        assert_eq!(TwapStartTime::from(0u32), TwapStartTime::AtMiningTime);
1471        assert_eq!(TwapStartTime::from(42u32), TwapStartTime::At(42));
1472    }
1473
1474    #[test]
1475    fn twap_start_time_into_u32() {
1476        let zero: u32 = TwapStartTime::AtMiningTime.into();
1477        assert_eq!(zero, 0);
1478        let ts: u32 = TwapStartTime::At(999).into();
1479        assert_eq!(ts, 999);
1480    }
1481
1482    #[test]
1483    fn twap_start_time_from_option_u32() {
1484        assert_eq!(TwapStartTime::from(None), TwapStartTime::AtMiningTime);
1485        assert_eq!(TwapStartTime::from(Some(123)), TwapStartTime::At(123));
1486    }
1487
1488    // ── DurationOfPart ──────────────────────────────────────────────────────
1489
1490    #[test]
1491    fn duration_of_part_auto_defaults() {
1492        let d = DurationOfPart::default();
1493        assert!(d.is_auto());
1494        assert!(!d.is_limit_duration());
1495        assert_eq!(d.duration(), None);
1496    }
1497
1498    #[test]
1499    fn duration_of_part_limit() {
1500        let d = DurationOfPart::limit(1_800);
1501        assert!(!d.is_auto());
1502        assert!(d.is_limit_duration());
1503        assert_eq!(d.duration(), Some(1_800));
1504    }
1505
1506    #[test]
1507    fn duration_of_part_display() {
1508        assert_eq!(DurationOfPart::Auto.to_string(), "auto");
1509        assert_eq!(DurationOfPart::limit(600).to_string(), "limit-duration(600s)");
1510    }
1511
1512    #[test]
1513    fn duration_of_part_from_option() {
1514        assert_eq!(DurationOfPart::from(None), DurationOfPart::Auto);
1515        assert_eq!(
1516            DurationOfPart::from(Some(300)),
1517            DurationOfPart::LimitDuration { duration: 300 }
1518        );
1519    }
1520
1521    // ── TwapData constructors ───────────────────────────────────────────────
1522
1523    #[test]
1524    fn twap_data_sell_constructor() {
1525        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1000u64), 4, 3600);
1526        assert!(t.is_sell());
1527        assert!(!t.is_buy());
1528        assert_eq!(t.sell_amount, U256::from(1000u64));
1529        assert_eq!(t.buy_amount, U256::ZERO);
1530        assert_eq!(t.receiver, Address::ZERO);
1531        assert_eq!(t.num_parts, 4);
1532        assert_eq!(t.part_duration, 3600);
1533        assert!(t.start_time.is_at_mining_time());
1534        assert!(!t.partially_fillable);
1535        assert!(t.duration_of_part.is_auto());
1536        assert!(!t.has_app_data());
1537    }
1538
1539    #[test]
1540    fn twap_data_buy_constructor() {
1541        let t = TwapData::buy(Address::ZERO, Address::ZERO, U256::from(500u64), 2, 1800);
1542        assert!(t.is_buy());
1543        assert!(!t.is_sell());
1544        assert_eq!(t.sell_amount, U256::MAX);
1545        assert_eq!(t.buy_amount, U256::from(500u64));
1546        assert_eq!(t.num_parts, 2);
1547        assert_eq!(t.part_duration, 1800);
1548    }
1549
1550    // ── TwapData computed fields ────────────────────────────────────────────
1551
1552    #[test]
1553    fn twap_data_total_duration_secs() {
1554        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3600);
1555        assert_eq!(t.total_duration_secs(), 14_400);
1556    }
1557
1558    #[test]
1559    fn twap_data_end_time_fixed() {
1560        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3600)
1561            .with_start_time(TwapStartTime::At(1_000_000));
1562        assert_eq!(t.end_time(), Some(1_000_000 + 14_400));
1563    }
1564
1565    #[test]
1566    fn twap_data_end_time_at_mining() {
1567        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3600);
1568        assert_eq!(t.end_time(), None);
1569    }
1570
1571    #[test]
1572    fn twap_data_is_expired() {
1573        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3600)
1574            .with_start_time(TwapStartTime::At(1_000_000));
1575        // end = 1_014_400
1576        assert!(!t.is_expired(1_014_399));
1577        assert!(t.is_expired(1_014_400));
1578        assert!(t.is_expired(2_000_000));
1579    }
1580
1581    #[test]
1582    fn twap_data_is_expired_at_mining_time_always_false() {
1583        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 4, 3600);
1584        assert!(!t.is_expired(u64::MAX));
1585    }
1586
1587    // ── TwapData builders ───────────────────────────────────────────────────
1588
1589    #[test]
1590    fn twap_data_with_receiver() {
1591        let recv = Address::repeat_byte(0x01);
1592        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1).with_receiver(recv);
1593        assert_eq!(t.receiver, recv);
1594    }
1595
1596    #[test]
1597    fn twap_data_with_buy_amount() {
1598        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1)
1599            .with_buy_amount(U256::from(42u64));
1600        assert_eq!(t.buy_amount, U256::from(42u64));
1601    }
1602
1603    #[test]
1604    fn twap_data_with_sell_amount() {
1605        let t = TwapData::buy(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1)
1606            .with_sell_amount(U256::from(99u64));
1607        assert_eq!(t.sell_amount, U256::from(99u64));
1608    }
1609
1610    #[test]
1611    fn twap_data_with_start_time() {
1612        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1)
1613            .with_start_time(TwapStartTime::At(12345));
1614        assert_eq!(t.start_time, TwapStartTime::At(12345));
1615    }
1616
1617    #[test]
1618    fn twap_data_with_app_data() {
1619        let hash = B256::repeat_byte(0xAA);
1620        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1).with_app_data(hash);
1621        assert!(t.has_app_data());
1622        assert_eq!(t.app_data, hash);
1623    }
1624
1625    #[test]
1626    fn twap_data_has_app_data_zero_is_false() {
1627        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1);
1628        assert!(!t.has_app_data());
1629    }
1630
1631    #[test]
1632    fn twap_data_with_partially_fillable() {
1633        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1)
1634            .with_partially_fillable(true);
1635        assert!(t.partially_fillable);
1636    }
1637
1638    #[test]
1639    fn twap_data_with_duration_of_part() {
1640        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::ZERO, 1, 1)
1641            .with_duration_of_part(DurationOfPart::limit(900));
1642        assert!(t.duration_of_part.is_limit_duration());
1643        assert_eq!(t.duration_of_part.duration(), Some(900));
1644    }
1645
1646    // ── TwapData display ────────────────────────────────────────────────────
1647
1648    #[test]
1649    fn twap_data_display_at_mining_time() {
1650        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(24_000u64), 24, 3_600)
1651            .with_buy_amount(U256::from(1_000u64));
1652        let s = t.to_string();
1653        assert!(s.contains("24 × 3600s"), "got: {s}");
1654        assert!(s.contains("at-mining-time"), "got: {s}");
1655        assert!(s.contains("24000"), "got: {s}");
1656        assert!(s.contains("1000"), "got: {s}");
1657    }
1658
1659    #[test]
1660    fn twap_data_display_fixed_start() {
1661        let t = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1_000u64), 6, 7_200)
1662            .with_start_time(TwapStartTime::At(1_700_000_000));
1663        let s = t.to_string();
1664        assert!(s.contains("at-unix-1700000000"), "got: {s}");
1665    }
1666
1667    // ── TwapStruct ──────────────────────────────────────────────────────────
1668
1669    fn make_twap_struct() -> TwapStruct {
1670        TwapStruct {
1671            sell_token: Address::ZERO,
1672            buy_token: Address::ZERO,
1673            receiver: Address::ZERO,
1674            part_sell_amount: U256::from(250u64),
1675            min_part_limit: U256::from(100u64),
1676            t0: 0,
1677            n: 4,
1678            t: 3600,
1679            span: 0,
1680            app_data: B256::ZERO,
1681        }
1682    }
1683
1684    #[test]
1685    fn twap_struct_has_app_data() {
1686        let mut s = make_twap_struct();
1687        assert!(!s.has_app_data());
1688        s.app_data = B256::repeat_byte(0x01);
1689        assert!(s.has_app_data());
1690    }
1691
1692    #[test]
1693    fn twap_struct_has_custom_receiver() {
1694        let mut s = make_twap_struct();
1695        assert!(!s.has_custom_receiver());
1696        s.receiver = Address::repeat_byte(0x01);
1697        assert!(s.has_custom_receiver());
1698    }
1699
1700    #[test]
1701    fn twap_struct_start_is_fixed() {
1702        let mut s = make_twap_struct();
1703        assert!(!s.start_is_fixed());
1704        s.t0 = 1_000_000;
1705        assert!(s.start_is_fixed());
1706    }
1707
1708    #[test]
1709    fn twap_struct_display() {
1710        let s = make_twap_struct();
1711        let d = s.to_string();
1712        assert!(d.contains("twap-struct"), "got: {d}");
1713        assert!(d.contains("4 × 3600s"), "got: {d}");
1714    }
1715
1716    // ── TwapData <-> TwapStruct conversions ─────────────────────────────────
1717
1718    #[test]
1719    fn twap_struct_try_from_twap_data() {
1720        let data = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1000u64), 4, 3600)
1721            .with_buy_amount(U256::from(400u64))
1722            .with_start_time(TwapStartTime::At(5000))
1723            .with_duration_of_part(DurationOfPart::limit(1800));
1724        let s = TwapStruct::try_from(&data).unwrap();
1725        assert_eq!(s.part_sell_amount, U256::from(250u64));
1726        assert_eq!(s.min_part_limit, U256::from(100u64));
1727        assert_eq!(s.t0, 5000);
1728        assert_eq!(s.n, 4);
1729        assert_eq!(s.t, 3600);
1730        assert_eq!(s.span, 1800);
1731    }
1732
1733    #[test]
1734    fn twap_struct_try_from_twap_data_zero_parts_errors() {
1735        let mut data = TwapData::sell(Address::ZERO, Address::ZERO, U256::from(1000u64), 4, 3600);
1736        data.num_parts = 0;
1737        assert!(TwapStruct::try_from(&data).is_err());
1738    }
1739
1740    #[test]
1741    fn twap_data_from_twap_struct() {
1742        let s = TwapStruct {
1743            sell_token: Address::ZERO,
1744            buy_token: Address::ZERO,
1745            receiver: Address::ZERO,
1746            part_sell_amount: U256::from(250u64),
1747            min_part_limit: U256::from(100u64),
1748            t0: 5000,
1749            n: 4,
1750            t: 3600,
1751            span: 1800,
1752            app_data: B256::ZERO,
1753        };
1754        let data = TwapData::from(&s);
1755        assert_eq!(data.sell_amount, U256::from(1000u64));
1756        assert_eq!(data.buy_amount, U256::from(400u64));
1757        assert_eq!(data.start_time, TwapStartTime::At(5000));
1758        assert_eq!(data.num_parts, 4);
1759        assert_eq!(data.part_duration, 3600);
1760        assert!(data.duration_of_part.is_limit_duration());
1761        assert_eq!(data.duration_of_part.duration(), Some(1800));
1762    }
1763
1764    #[test]
1765    fn twap_data_from_twap_struct_at_mining_time() {
1766        let mut s = make_twap_struct();
1767        s.t0 = 0;
1768        s.span = 0;
1769        let data = TwapData::from(&s);
1770        assert!(data.start_time.is_at_mining_time());
1771        assert!(data.duration_of_part.is_auto());
1772    }
1773
1774    // ── GpV2OrderStruct ─────────────────────────────────────────────────────
1775
1776    fn make_gpv2_order() -> GpV2OrderStruct {
1777        GpV2OrderStruct {
1778            sell_token: Address::ZERO,
1779            buy_token: Address::ZERO,
1780            receiver: Address::ZERO,
1781            sell_amount: U256::from(1000u64),
1782            buy_amount: U256::from(500u64),
1783            valid_to: 1_700_000_000,
1784            app_data: B256::ZERO,
1785            fee_amount: U256::ZERO,
1786            kind: B256::ZERO,
1787            partially_fillable: false,
1788            sell_token_balance: B256::ZERO,
1789            buy_token_balance: B256::ZERO,
1790        }
1791    }
1792
1793    #[test]
1794    fn gpv2_order_has_custom_receiver() {
1795        let mut o = make_gpv2_order();
1796        assert!(!o.has_custom_receiver());
1797        o.receiver = Address::repeat_byte(0x01);
1798        assert!(o.has_custom_receiver());
1799    }
1800
1801    #[test]
1802    fn gpv2_order_is_partially_fillable() {
1803        let mut o = make_gpv2_order();
1804        assert!(!o.is_partially_fillable());
1805        o.partially_fillable = true;
1806        assert!(o.is_partially_fillable());
1807    }
1808
1809    #[test]
1810    fn gpv2_order_display() {
1811        let o = make_gpv2_order();
1812        let s = o.to_string();
1813        assert!(s.contains("gpv2-order"), "got: {s}");
1814        assert!(s.contains("1000"), "got: {s}");
1815        assert!(s.contains("500"), "got: {s}");
1816    }
1817
1818    // ── PollResult ──────────────────────────────────────────────────────────
1819
1820    #[test]
1821    fn poll_result_success() {
1822        let r = PollResult::Success { order: None, signature: None };
1823        assert!(r.is_success());
1824        assert!(!r.is_retryable());
1825        assert!(!r.is_terminal());
1826        assert!(r.order_ref().is_none());
1827        assert!(r.as_error_message().is_none());
1828    }
1829
1830    #[test]
1831    fn poll_result_try_next_block() {
1832        let r = PollResult::TryNextBlock;
1833        assert!(r.is_try_next_block());
1834        assert!(r.is_retryable());
1835        assert!(!r.is_success());
1836        assert_eq!(r.get_block_number(), None);
1837        assert_eq!(r.get_epoch(), None);
1838    }
1839
1840    #[test]
1841    fn poll_result_try_on_block() {
1842        let r = PollResult::TryOnBlock { block_number: 42 };
1843        assert!(r.is_try_on_block());
1844        assert!(r.is_retryable());
1845        assert_eq!(r.get_block_number(), Some(42));
1846    }
1847
1848    #[test]
1849    fn poll_result_try_at_epoch() {
1850        let r = PollResult::TryAtEpoch { epoch: 1_700_000_000 };
1851        assert!(r.is_try_at_epoch());
1852        assert!(r.is_retryable());
1853        assert_eq!(r.get_epoch(), Some(1_700_000_000));
1854    }
1855
1856    #[test]
1857    fn poll_result_unexpected_error() {
1858        let r = PollResult::UnexpectedError { message: "boom".into() };
1859        assert!(r.is_unexpected_error());
1860        assert!(!r.is_retryable());
1861        assert_eq!(r.as_error_message(), Some("boom"));
1862    }
1863
1864    #[test]
1865    fn poll_result_dont_try_again() {
1866        let r = PollResult::DontTryAgain { reason: "expired".into() };
1867        assert!(r.is_dont_try_again());
1868        assert!(r.is_terminal());
1869        assert!(!r.is_retryable());
1870        assert_eq!(r.as_error_message(), Some("expired"));
1871    }
1872
1873    #[test]
1874    fn poll_result_display() {
1875        assert_eq!(PollResult::Success { order: None, signature: None }.to_string(), "success");
1876        assert_eq!(PollResult::TryNextBlock.to_string(), "try-next-block");
1877        assert_eq!(PollResult::TryOnBlock { block_number: 10 }.to_string(), "try-on-block(10)");
1878        assert_eq!(PollResult::TryAtEpoch { epoch: 99 }.to_string(), "try-at-epoch(99)");
1879        assert_eq!(
1880            PollResult::UnexpectedError { message: "x".into() }.to_string(),
1881            "unexpected-error(x)"
1882        );
1883        assert_eq!(
1884            PollResult::DontTryAgain { reason: "y".into() }.to_string(),
1885            "dont-try-again(y)"
1886        );
1887    }
1888
1889    // ── ProofLocation ───────────────────────────────────────────────────────
1890
1891    #[test]
1892    fn proof_location_as_str() {
1893        assert_eq!(ProofLocation::Private.as_str(), "private");
1894        assert_eq!(ProofLocation::Emitted.as_str(), "emitted");
1895        assert_eq!(ProofLocation::Swarm.as_str(), "swarm");
1896        assert_eq!(ProofLocation::Waku.as_str(), "waku");
1897        assert_eq!(ProofLocation::Reserved.as_str(), "reserved");
1898        assert_eq!(ProofLocation::Ipfs.as_str(), "ipfs");
1899    }
1900
1901    #[test]
1902    fn proof_location_predicates() {
1903        assert!(ProofLocation::Private.is_private());
1904        assert!(ProofLocation::Emitted.is_emitted());
1905        assert!(ProofLocation::Swarm.is_swarm());
1906        assert!(ProofLocation::Waku.is_waku());
1907        assert!(ProofLocation::Reserved.is_reserved());
1908        assert!(ProofLocation::Ipfs.is_ipfs());
1909        // Negative checks
1910        assert!(!ProofLocation::Private.is_emitted());
1911        assert!(!ProofLocation::Ipfs.is_private());
1912    }
1913
1914    #[test]
1915    fn proof_location_default_is_private() {
1916        assert_eq!(ProofLocation::default(), ProofLocation::Private);
1917    }
1918
1919    #[test]
1920    fn proof_location_display() {
1921        assert_eq!(ProofLocation::Ipfs.to_string(), "ipfs");
1922        assert_eq!(ProofLocation::Waku.to_string(), "waku");
1923    }
1924
1925    #[test]
1926    fn proof_location_try_from_u8() {
1927        assert_eq!(ProofLocation::try_from(0u8).unwrap(), ProofLocation::Private);
1928        assert_eq!(ProofLocation::try_from(1u8).unwrap(), ProofLocation::Emitted);
1929        assert_eq!(ProofLocation::try_from(5u8).unwrap(), ProofLocation::Ipfs);
1930        assert!(ProofLocation::try_from(6u8).is_err());
1931        assert!(ProofLocation::try_from(255u8).is_err());
1932    }
1933
1934    #[test]
1935    fn proof_location_try_from_str() {
1936        assert_eq!(ProofLocation::try_from("private").unwrap(), ProofLocation::Private);
1937        assert_eq!(ProofLocation::try_from("ipfs").unwrap(), ProofLocation::Ipfs);
1938        assert!(ProofLocation::try_from("unknown").is_err());
1939    }
1940
1941    #[test]
1942    fn proof_location_into_u8() {
1943        let v: u8 = ProofLocation::Private.into();
1944        assert_eq!(v, 0);
1945        let v: u8 = ProofLocation::Ipfs.into();
1946        assert_eq!(v, 5);
1947    }
1948
1949    // ── ProofStruct ─────────────────────────────────────────────────────────
1950
1951    #[test]
1952    fn proof_struct_new() {
1953        let p = ProofStruct::new(ProofLocation::Swarm, vec![1, 2, 3]);
1954        assert!(p.is_swarm());
1955        assert!(p.has_data());
1956        assert_eq!(p.data_len(), 3);
1957    }
1958
1959    #[test]
1960    fn proof_struct_private() {
1961        let p = ProofStruct::private();
1962        assert!(p.is_private());
1963        assert!(p.is_empty());
1964        assert!(!p.has_data());
1965        assert_eq!(p.data_len(), 0);
1966    }
1967
1968    #[test]
1969    fn proof_struct_emitted() {
1970        let p = ProofStruct::emitted();
1971        assert!(p.is_emitted());
1972        assert!(p.is_empty());
1973    }
1974
1975    #[test]
1976    fn proof_struct_with_location() {
1977        let p = ProofStruct::private().with_location(ProofLocation::Ipfs);
1978        assert!(p.is_ipfs());
1979    }
1980
1981    #[test]
1982    fn proof_struct_with_data() {
1983        let p = ProofStruct::private().with_data(vec![0xCA, 0xFE]);
1984        assert!(p.has_data());
1985        assert_eq!(p.data_len(), 2);
1986    }
1987
1988    #[test]
1989    fn proof_struct_delegated_predicates() {
1990        assert!(ProofStruct::new(ProofLocation::Waku, vec![]).is_waku());
1991        assert!(ProofStruct::new(ProofLocation::Reserved, vec![]).is_reserved());
1992    }
1993
1994    #[test]
1995    fn proof_struct_display() {
1996        let p = ProofStruct::private();
1997        assert_eq!(p.to_string(), "proof(private)");
1998        let p = ProofStruct::new(ProofLocation::Ipfs, vec![1]);
1999        assert_eq!(p.to_string(), "proof(ipfs)");
2000    }
2001
2002    // ── ProofLocation try_from exhaustive ────────────────────────────────
2003
2004    #[test]
2005    fn proof_location_try_from_str_all() {
2006        for (s, expected) in [
2007            ("emitted", ProofLocation::Emitted),
2008            ("swarm", ProofLocation::Swarm),
2009            ("waku", ProofLocation::Waku),
2010            ("reserved", ProofLocation::Reserved),
2011        ] {
2012            assert_eq!(ProofLocation::try_from(s).unwrap(), expected);
2013        }
2014    }
2015
2016    #[test]
2017    fn proof_location_try_from_u8_all() {
2018        assert_eq!(ProofLocation::try_from(2u8).unwrap(), ProofLocation::Swarm);
2019        assert_eq!(ProofLocation::try_from(3u8).unwrap(), ProofLocation::Waku);
2020        assert_eq!(ProofLocation::try_from(4u8).unwrap(), ProofLocation::Reserved);
2021    }
2022
2023    // ── TwapData serde roundtrip ────────────────────────────────────────
2024
2025    #[test]
2026    fn twap_data_serde_roundtrip() {
2027        let data = TwapData::sell(
2028            Address::repeat_byte(0x11),
2029            Address::repeat_byte(0x22),
2030            U256::from(1000u64),
2031            4,
2032            3600,
2033        )
2034        .with_buy_amount(U256::from(800u64))
2035        .with_start_time(TwapStartTime::At(1_000_000))
2036        .with_duration_of_part(DurationOfPart::limit(900));
2037
2038        let json = serde_json::to_string(&data).unwrap();
2039        let back: TwapData = serde_json::from_str(&json).unwrap();
2040        assert_eq!(back.sell_amount, data.sell_amount);
2041        assert_eq!(back.buy_amount, data.buy_amount);
2042        assert_eq!(back.num_parts, data.num_parts);
2043        assert_eq!(back.part_duration, data.part_duration);
2044    }
2045
2046    // ── ConditionalOrderParams serde roundtrip ──────────────────────────
2047
2048    #[test]
2049    fn conditional_order_params_serde_roundtrip() {
2050        let params = ConditionalOrderParams::new(
2051            TWAP_HANDLER_ADDRESS,
2052            B256::repeat_byte(0xAB),
2053            vec![0xDE, 0xAD, 0xBE, 0xEF],
2054        );
2055        let json = serde_json::to_string(&params).unwrap();
2056        let back: ConditionalOrderParams = serde_json::from_str(&json).unwrap();
2057        assert_eq!(back.handler, params.handler);
2058        assert_eq!(back.salt, params.salt);
2059        assert_eq!(back.static_input, params.static_input);
2060    }
2061
2062    // ── BlockInfo ───────────────────────────────────────────────────────
2063
2064    #[test]
2065    fn block_info_new() {
2066        let b = BlockInfo { block_number: 100, block_timestamp: 1_700_000_000 };
2067        assert_eq!(b.block_number, 100);
2068        assert_eq!(b.block_timestamp, 1_700_000_000);
2069    }
2070
2071    // ── GpV2OrderStruct try_from coverage ────────────────────────────────
2072
2073    #[test]
2074    fn gpv2_order_try_from_bad_kind_fails() {
2075        let o = make_gpv2_order();
2076        // kind is B256::ZERO which is neither sell nor buy hash
2077        let result = crate::order_signing::types::UnsignedOrder::try_from(&o);
2078        assert!(result.is_err());
2079    }
2080}
2081
2082// ── ProofStruct ───────────────────────────────────────────────────────────────
2083
2084/// On-chain `Proof` argument passed to `ComposableCow::setRoot`.
2085///
2086/// Bundles the proof location discriminant with location-specific data
2087/// (e.g. an IPFS CID, Swarm hash, or Waku message).  Pass `data: vec![]` for
2088/// [`ProofLocation::Private`] or [`ProofLocation::Emitted`].
2089#[derive(Debug, Clone)]
2090pub struct ProofStruct {
2091    /// Where the Merkle proof is stored/communicated.
2092    pub location: ProofLocation,
2093    /// Location-specific proof bytes (empty for private or emitted proofs).
2094    pub data: Vec<u8>,
2095}
2096
2097// ── Block info ──────────────────────────────────────────────────────────────
2098
2099/// Block information used for conditional order validation.
2100///
2101/// Mirrors `BlockInfo` from the `TypeScript` SDK.
2102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2103pub struct BlockInfo {
2104    /// Block number.
2105    pub block_number: u64,
2106    /// Block timestamp (Unix seconds).
2107    pub block_timestamp: u64,
2108}
2109
2110impl BlockInfo {
2111    /// Construct a new [`BlockInfo`].
2112    ///
2113    /// # Arguments
2114    ///
2115    /// * `block_number` - The block number.
2116    /// * `block_timestamp` - The block timestamp in Unix seconds.
2117    ///
2118    /// # Returns
2119    ///
2120    /// A new [`BlockInfo`] instance.
2121    #[must_use]
2122    pub const fn new(block_number: u64, block_timestamp: u64) -> Self {
2123        Self { block_number, block_timestamp }
2124    }
2125}
2126
2127impl fmt::Display for BlockInfo {
2128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2129        write!(f, "block(#{}, ts={})", self.block_number, self.block_timestamp)
2130    }
2131}
2132
2133// ── IsValidResult ───────────────────────────────────────────────────────────
2134
2135/// Result of validating a conditional order.
2136///
2137/// Mirrors the `IsValidResult` union type from the `TypeScript` SDK.
2138#[derive(Debug, Clone, PartialEq, Eq)]
2139pub enum IsValidResult {
2140    /// The order is valid.
2141    Valid,
2142    /// The order is invalid, with a reason.
2143    Invalid {
2144        /// Human-readable reason why the order is invalid.
2145        reason: String,
2146    },
2147}
2148
2149/// Type alias for the valid variant, mirroring the `TypeScript` SDK's `IsValid` interface.
2150pub type IsValid = ();
2151
2152/// Type alias for the invalid variant, mirroring the `TypeScript` SDK's `IsNotValid` interface.
2153pub type IsNotValid = String;
2154
2155impl IsValidResult {
2156    /// Returns `true` if the result represents a valid order.
2157    ///
2158    /// # Returns
2159    ///
2160    /// `true` for the [`Valid`](Self::Valid) variant, `false` for [`Invalid`](Self::Invalid).
2161    #[must_use]
2162    pub const fn is_valid(&self) -> bool {
2163        matches!(self, Self::Valid)
2164    }
2165
2166    /// Returns the reason string if the result represents an invalid order.
2167    ///
2168    /// # Returns
2169    ///
2170    /// `Some(reason)` for the [`Invalid`](Self::Invalid) variant, `None` for
2171    /// [`Valid`](Self::Valid).
2172    #[must_use]
2173    pub fn reason(&self) -> Option<&str> {
2174        match self {
2175            Self::Valid => None,
2176            Self::Invalid { reason } => Some(reason),
2177        }
2178    }
2179
2180    /// Create a valid result.
2181    ///
2182    /// # Returns
2183    ///
2184    /// An [`IsValidResult::Valid`] instance.
2185    #[must_use]
2186    pub const fn valid() -> Self {
2187        Self::Valid
2188    }
2189
2190    /// Create an invalid result with the given reason.
2191    ///
2192    /// # Arguments
2193    ///
2194    /// * `reason` - A human-readable explanation of why the order is invalid.
2195    ///
2196    /// # Returns
2197    ///
2198    /// An [`IsValidResult::Invalid`] instance containing the reason.
2199    #[must_use]
2200    pub fn invalid(reason: impl Into<String>) -> Self {
2201        Self::Invalid { reason: reason.into() }
2202    }
2203}
2204
2205impl fmt::Display for IsValidResult {
2206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2207        match self {
2208            Self::Valid => f.write_str("valid"),
2209            Self::Invalid { reason } => write!(f, "invalid: {reason}"),
2210        }
2211    }
2212}
2213
2214// ── Test helpers ────────────────────────────────────────────────────────────
2215
2216/// Default parameters for a test conditional order.
2217///
2218/// Mirrors `DEFAULT_ORDER_PARAMS` from the `TypeScript` SDK test helper.
2219pub const DEFAULT_TEST_HANDLER: &str = "0x910d00a310f7Dc5B29FE73458F47f519be547D3d";
2220
2221/// Default salt for test conditional orders.
2222pub const DEFAULT_TEST_SALT: &str =
2223    "0x9379a0bf532ff9a66ffde940f94b1a025d6f18803054c1aef52dc94b15255bbe";
2224
2225/// Parameters for creating a test conditional order.
2226///
2227/// Mirrors `TestConditionalOrderParams` from the `TypeScript` SDK.
2228#[derive(Debug, Clone)]
2229pub struct TestConditionalOrderParams {
2230    /// Handler contract address.
2231    pub handler: Address,
2232    /// 32-byte salt.
2233    pub salt: B256,
2234    /// Static input data.
2235    pub static_input: Vec<u8>,
2236    /// Whether this is a single order (true) or part of a Merkle tree (false).
2237    pub is_single_order: bool,
2238}
2239
2240impl Default for TestConditionalOrderParams {
2241    fn default() -> Self {
2242        Self {
2243            handler: DEFAULT_TEST_HANDLER.parse().map_or(Address::ZERO, |a| a),
2244            salt: DEFAULT_TEST_SALT.parse().map_or(B256::ZERO, |s| s),
2245            static_input: Vec::new(),
2246            is_single_order: true,
2247        }
2248    }
2249}
2250
2251/// Create a test [`ConditionalOrderParams`] with optional overrides.
2252///
2253/// Mirrors `createTestConditionalOrder` from the `TypeScript` SDK.
2254/// Useful in tests to quickly construct valid conditional order params.
2255///
2256/// # Example
2257///
2258/// ```rust
2259/// use cow_rs::composable::create_test_conditional_order;
2260///
2261/// let params = create_test_conditional_order(None);
2262/// assert!(!params.handler.is_zero());
2263/// ```
2264#[must_use]
2265pub fn create_test_conditional_order(
2266    overrides: Option<TestConditionalOrderParams>,
2267) -> ConditionalOrderParams {
2268    let test = overrides.unwrap_or_default();
2269    ConditionalOrderParams {
2270        handler: test.handler,
2271        salt: test.salt,
2272        static_input: test.static_input,
2273    }
2274}