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(¶ms.static_input)?;
177 return Ok(ConditionalOrderKind::GoodAfterTime(GatOrder::with_salt(
178 data,
179 params.salt,
180 )));
181 }
182 let data = decode_twap_static_input(¶ms.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(¶ms.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}