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