Skip to main content

cow_composable/
factory.rs

1//! [`ConditionalOrderFactory`] — instantiate conditional orders from on-chain params.
2
3use std::fmt;
4
5use cow_errors::CowError;
6
7use super::{
8    gat::{GatOrder, decode_gat_static_input},
9    stop_loss::{STOP_LOSS_HANDLER_ADDRESS, StopLossOrder, decode_stop_loss_static_input},
10    twap::{TwapOrder, decode_twap_static_input},
11    types::{ConditionalOrderParams, TWAP_HANDLER_ADDRESS},
12};
13
14/// A conditional order decoded from on-chain [`ConditionalOrderParams`].
15#[derive(Debug, Clone)]
16pub enum ConditionalOrderKind {
17    /// A `TWAP` (Time-Weighted Average Price) order.
18    Twap(TwapOrder),
19    /// A stop-loss order that triggers when the price falls below a strike price.
20    StopLoss(StopLossOrder),
21    /// A `GoodAfterTime` order that becomes valid only after a given timestamp.
22    GoodAfterTime(GatOrder),
23    /// An order whose handler is not recognised by this factory.
24    Unknown(ConditionalOrderParams),
25}
26
27impl ConditionalOrderKind {
28    /// Returns a short string label for the order kind.
29    ///
30    /// # Returns
31    ///
32    /// A `&'static str` identifying the variant: `"twap"`, `"stop-loss"`,
33    /// `"good-after-time"`, or `"unknown"`.
34    #[must_use]
35    pub const fn as_str(&self) -> &'static str {
36        match self {
37            Self::Twap(_) => "twap",
38            Self::StopLoss(_) => "stop-loss",
39            Self::GoodAfterTime(_) => "good-after-time",
40            Self::Unknown(_) => "unknown",
41        }
42    }
43
44    /// Returns `true` if this is a `TWAP` conditional order.
45    ///
46    /// ```
47    /// use alloy_primitives::B256;
48    /// use cow_composable::{ConditionalOrderFactory, ConditionalOrderParams, TWAP_HANDLER_ADDRESS};
49    ///
50    /// let params = ConditionalOrderParams {
51    ///     handler: TWAP_HANDLER_ADDRESS,
52    ///     salt: B256::ZERO,
53    ///     static_input: vec![],
54    /// };
55    /// // An unknown handler resolves to Unknown, not Twap.
56    /// use alloy_primitives::Address;
57    /// let unknown = cow_composable::ConditionalOrderKind::Unknown(ConditionalOrderParams {
58    ///     handler: Address::ZERO,
59    ///     salt: B256::ZERO,
60    ///     static_input: vec![],
61    /// });
62    /// assert!(!unknown.is_twap());
63    /// assert!(unknown.is_unknown());
64    /// ```
65    #[must_use]
66    pub const fn is_twap(&self) -> bool {
67        matches!(self, Self::Twap(_))
68    }
69
70    /// Returns `true` if this is a stop-loss conditional order.
71    ///
72    /// ```
73    /// use alloy_primitives::{Address, B256};
74    /// use cow_composable::{ConditionalOrderKind, ConditionalOrderParams};
75    ///
76    /// let unknown = ConditionalOrderKind::Unknown(ConditionalOrderParams {
77    ///     handler: Address::ZERO,
78    ///     salt: B256::ZERO,
79    ///     static_input: vec![],
80    /// });
81    /// assert!(!unknown.is_stop_loss());
82    /// ```
83    #[must_use]
84    pub const fn is_stop_loss(&self) -> bool {
85        matches!(self, Self::StopLoss(_))
86    }
87
88    /// Returns `true` if this is a `GoodAfterTime` conditional order.
89    ///
90    /// ```
91    /// use alloy_primitives::{Address, B256};
92    /// use cow_composable::{ConditionalOrderKind, ConditionalOrderParams};
93    ///
94    /// let unknown = ConditionalOrderKind::Unknown(ConditionalOrderParams {
95    ///     handler: Address::ZERO,
96    ///     salt: B256::ZERO,
97    ///     static_input: vec![],
98    /// });
99    /// assert!(!unknown.is_good_after_time());
100    /// ```
101    #[must_use]
102    pub const fn is_good_after_time(&self) -> bool {
103        matches!(self, Self::GoodAfterTime(_))
104    }
105
106    /// Returns `true` if this order's handler is not recognised by the factory.
107    ///
108    /// # Returns
109    ///
110    /// `true` when the variant is [`ConditionalOrderKind::Unknown`], meaning the
111    /// handler address did not match any known conditional order type.
112    #[must_use]
113    pub const fn is_unknown(&self) -> bool {
114        matches!(self, Self::Unknown(_))
115    }
116}
117
118impl fmt::Display for ConditionalOrderKind {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        match self {
121            Self::Twap(order) => write!(f, "twap({order})"),
122            Self::StopLoss(_) => f.write_str("stop-loss"),
123            Self::GoodAfterTime(_) => f.write_str("good-after-time"),
124            Self::Unknown(params) => write!(f, "unknown({:#x})", params.handler),
125        }
126    }
127}
128
129/// Instantiates conditional orders from [`ConditionalOrderParams`].
130///
131/// Mirrors `ConditionalOrderFactory` from the `TypeScript` SDK.
132/// Extend by matching additional handler addresses in [`from_params`].
133///
134/// [`from_params`]: ConditionalOrderFactory::from_params
135#[derive(Debug, Clone, Default)]
136pub struct ConditionalOrderFactory;
137
138impl ConditionalOrderFactory {
139    /// Create a new factory.
140    ///
141    /// # Returns
142    ///
143    /// A zero-sized [`ConditionalOrderFactory`] instance that can decode
144    /// [`ConditionalOrderParams`] via [`from_params`](Self::from_params).
145    #[must_use]
146    pub const fn new() -> Self {
147        Self
148    }
149
150    /// Decode [`ConditionalOrderParams`] into a typed [`ConditionalOrderKind`].
151    ///
152    /// Handler addresses are matched exactly.  Unknown handlers return
153    /// [`ConditionalOrderKind::Unknown`] rather than an error.
154    ///
155    /// Recognised handlers:
156    /// - [`TWAP_HANDLER_ADDRESS`] → [`ConditionalOrderKind::Twap`]
157    /// - [`STOP_LOSS_HANDLER_ADDRESS`] → [`ConditionalOrderKind::StopLoss`]
158    ///
159    /// Note: the `GoodAfterTime` handler (`GAT_HANDLER_ADDRESS`) shares the
160    /// same on-chain address as the `TWAP` handler, so `TWAP` decoding takes
161    /// priority for that address.
162    ///
163    /// # Errors
164    ///
165    /// Returns [`CowError::AppData`] only if a known handler's static input
166    /// fails ABI decoding.
167    pub fn from_params(
168        &self,
169        params: ConditionalOrderParams,
170    ) -> Result<ConditionalOrderKind, CowError> {
171        if params.handler == TWAP_HANDLER_ADDRESS {
172            // Try TWAP decoding first (TWAP and GAT share the same address).
173            // If the input is TWAP-sized (320 bytes), decode as TWAP.
174            // If it is GAT-sized (448 bytes), decode as GAT.
175            if params.static_input.len() == 14 * 32 {
176                let data = decode_gat_static_input(&params.static_input)?;
177                return Ok(ConditionalOrderKind::GoodAfterTime(GatOrder::with_salt(
178                    data,
179                    params.salt,
180                )));
181            }
182            let data = decode_twap_static_input(&params.static_input)?;
183            return Ok(ConditionalOrderKind::Twap(TwapOrder::with_salt(data, params.salt)));
184        }
185        if params.handler == STOP_LOSS_HANDLER_ADDRESS {
186            let data = decode_stop_loss_static_input(&params.static_input)?;
187            return Ok(ConditionalOrderKind::StopLoss(StopLossOrder::with_salt(data, params.salt)));
188        }
189        Ok(ConditionalOrderKind::Unknown(params))
190    }
191}
192impl fmt::Display for ConditionalOrderFactory {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        f.write_str("conditional-order-factory")
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use alloy_primitives::{Address, B256};
201
202    use super::*;
203
204    #[test]
205    fn factory_new() {
206        let factory = ConditionalOrderFactory::new();
207        assert_eq!(factory.to_string(), "conditional-order-factory");
208    }
209
210    #[test]
211    fn factory_unknown_handler() {
212        let factory = ConditionalOrderFactory::new();
213        let params = ConditionalOrderParams {
214            handler: Address::ZERO,
215            salt: B256::ZERO,
216            static_input: vec![],
217        };
218        let result = factory.from_params(params).unwrap();
219        assert!(result.is_unknown());
220        assert!(!result.is_twap());
221        assert!(!result.is_stop_loss());
222        assert!(!result.is_good_after_time());
223        assert_eq!(result.as_str(), "unknown");
224    }
225
226    #[test]
227    fn factory_twap_handler_empty_static_input_errors() {
228        let factory = ConditionalOrderFactory::new();
229        let params = ConditionalOrderParams {
230            handler: TWAP_HANDLER_ADDRESS,
231            salt: B256::ZERO,
232            static_input: vec![],
233        };
234        // Empty static input is not valid for TWAP
235        assert!(factory.from_params(params).is_err());
236    }
237
238    #[test]
239    fn factory_stop_loss_handler_empty_static_input_errors() {
240        let factory = ConditionalOrderFactory::new();
241        let params = ConditionalOrderParams {
242            handler: STOP_LOSS_HANDLER_ADDRESS,
243            salt: B256::ZERO,
244            static_input: vec![],
245        };
246        assert!(factory.from_params(params).is_err());
247    }
248
249    #[test]
250    fn conditional_order_kind_display_unknown() {
251        let kind = ConditionalOrderKind::Unknown(ConditionalOrderParams {
252            handler: Address::ZERO,
253            salt: B256::ZERO,
254            static_input: vec![],
255        });
256        let s = kind.to_string();
257        assert!(s.contains("unknown"));
258    }
259
260    #[test]
261    fn conditional_order_kind_display_stop_loss() {
262        let kind = ConditionalOrderKind::Unknown(ConditionalOrderParams {
263            handler: Address::ZERO,
264            salt: B256::ZERO,
265            static_input: vec![],
266        });
267        // We can't easily construct a StopLoss without valid data,
268        // so we test the other Display variants through the Unknown variant
269        assert_eq!(kind.as_str(), "unknown");
270    }
271}